Don't use backup restricted mode in certain cases

In this change we introduce a new ApplicationManifest property which
allows apps to specify whether they want to be put in restricted mode
for B&R operations. If the app explicitly set the property, it's always
respected.

If the app has not set the property then we use targetSdk gating:
* For targetSdk < 36, we keep the status quo and put the app into
  restricted mode.
* For targetSdk >= 36, we call a new API to the BackupTransport for it
  to make a decision on a per-package basis.

Some implementation details explained:
* In order to not block process creation in ActivityManager on an IPC to
  the BackupTransport, we call the transport earlier in
  PerformFullTransportBackupTask (for backup) and
  PerforUnifiedRestoreTask (for restore). We cache the list in memory in
  BackupManagerService.
* When AMS#bindBackupAgent is called, the BackupManagerService tells it
  whether to use restricted mode. AMS stores this in the existing
  BackupRecord data class and uses it in attachApplication().
* PerformUnifiedRestoreTask is an extremely untestable state machine and
  testing this properly is difficult without a significant rewrite so
  I'm using @VisibleForTesting.
* Seems like AMS#attachApplication also requires a lot of mocking so
  couldn't add tests there.

Flag: com.android.server.backup.enable_restricted_mode_changes
Bug: 376661510
Test: atest (see tests changed) and manually with a test app that
  defined its own Application subclass.

API-Coverage-Bug: 379086316
Change-Id: Ie060890131ba526d58ec9134e52fd80acc23ef63
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index d53f949..fcb7934 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -60,3 +60,12 @@
     bug: "331749778"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "enable_restricted_mode_changes"
+    namespace: "onboarding"
+    description: "Enables the new framework behavior of not putting apps in restricted mode for "
+            "B&R operations in certain cases."
+    bug: "376661510"
+    is_fixed_read_only: true
+}
diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
index 466d477..5de2fb3 100644
--- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java
@@ -43,6 +43,7 @@
 import android.app.ActivityManagerInternal;
 import android.app.AlarmManager;
 import android.app.AppGlobals;
+import android.app.ApplicationThreadConstants;
 import android.app.IActivityManager;
 import android.app.IBackupAgent;
 import android.app.PendingIntent;
@@ -59,6 +60,9 @@
 import android.app.backup.IFullBackupRestoreObserver;
 import android.app.backup.IRestoreSession;
 import android.app.backup.ISelectBackupTransportCallback;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
@@ -298,6 +302,15 @@
     private static final String BACKUP_FINISHED_ACTION = "android.intent.action.BACKUP_FINISHED";
     private static final String BACKUP_FINISHED_PACKAGE_EXTRA = "packageName";
 
+    /**
+     * Enables the OS making a decision on whether backup restricted mode should be used for apps
+     * that haven't explicitly opted in or out. See
+     * {@link PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE} for details.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
+    public static final long OS_DECIDES_BACKUP_RESTRICTED_MODE = 376661510;
+
     // Time delay for initialization operations that can be delayed so as not to consume too much
     // CPU on bring-up and increase time-to-UI.
     private static final long INITIALIZATION_DELAY_MILLIS = 3000;
@@ -352,6 +365,9 @@
     // Backups that we haven't started yet.  Keys are package names.
     private final HashMap<String, BackupRequest> mPendingBackups = new HashMap<>();
 
+    private final ArraySet<String> mRestoreNoRestrictedModePackages = new ArraySet<>();
+    private final ArraySet<String> mBackupNoRestrictedModePackages = new ArraySet<>();
+
     // locking around the pending-backup management
     private final Object mQueueLock = new Object();
 
@@ -523,7 +539,8 @@
     @VisibleForTesting
     UserBackupManagerService(Context context, PackageManager packageManager,
             LifecycleOperationStorage operationStorage, TransportManager transportManager,
-            BackupHandler backupHandler, BackupManagerConstants backupManagerConstants) {
+            BackupHandler backupHandler, BackupManagerConstants backupManagerConstants,
+            IActivityManager activityManager, ActivityManagerInternal activityManagerInternal) {
         mContext = context;
 
         mUserId = 0;
@@ -534,6 +551,8 @@
         mFullBackupQueue = new ArrayList<>();
         mBackupHandler = backupHandler;
         mConstants = backupManagerConstants;
+        mActivityManager = activityManager;
+        mActivityManagerInternal = activityManagerInternal;
 
         mBaseStateDir = null;
         mDataDir = null;
@@ -543,13 +562,11 @@
         mRunInitReceiver = null;
         mRunInitIntent = null;
         mAgentTimeoutParameters = null;
-        mActivityManagerInternal = null;
         mAlarmManager = null;
         mWakelock = null;
         mBackupPreferences = null;
         mBackupPasswordManager = null;
         mPackageManagerBinder = null;
-        mActivityManager = null;
         mBackupManagerBinder = null;
         mScheduledBackupEligibility = null;
     }
@@ -1651,9 +1668,11 @@
         synchronized (mAgentConnectLock) {
             mConnecting = true;
             mConnectedAgent = null;
+            boolean useRestrictedMode = shouldUseRestrictedBackupModeForPackage(mode,
+                    app.packageName);
             try {
                 if (mActivityManager.bindBackupAgent(app.packageName, mode, mUserId,
-                        backupDestination)) {
+                        backupDestination, useRestrictedMode)) {
                     Slog.d(TAG, addUserIdToLogMessage(mUserId, "awaiting agent for " + app));
 
                     // success; wait for the agent to arrive
@@ -3103,6 +3122,91 @@
         }
     }
 
+    /**
+     * Marks the given set of packages as packages that should not be put into restricted mode if
+     * they are started for the given {@link BackupAnnotations.OperationType}.
+     */
+    public void setNoRestrictedModePackages(Set<String> packageNames,
+            @BackupAnnotations.OperationType int opType) {
+        if (opType == BackupAnnotations.OperationType.BACKUP) {
+            mBackupNoRestrictedModePackages.clear();
+            mBackupNoRestrictedModePackages.addAll(packageNames);
+        } else if (opType == BackupAnnotations.OperationType.RESTORE) {
+            mRestoreNoRestrictedModePackages.clear();
+            mRestoreNoRestrictedModePackages.addAll(packageNames);
+        } else {
+            throw new IllegalArgumentException("opType must be BACKUP or RESTORE");
+        }
+    }
+
+    /**
+     * Clears the list of packages that should not be put into restricted mode for either backup or
+     * restore.
+     */
+    public void clearNoRestrictedModePackages() {
+        mBackupNoRestrictedModePackages.clear();
+        mRestoreNoRestrictedModePackages.clear();
+    }
+
+    /**
+     * If the app has specified {@link PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE}, then
+     * its value is returned. If it hasn't and it targets an SDK below
+     * {@link Build.VERSION_CODES#BAKLAVA} then returns true. If it targets a newer SDK, then
+     * returns the decision made by the {@link android.app.backup.BackupTransport}.
+     *
+     * <p>When this method is called, we should have already asked the transport and cached its
+     * response in {@link #mBackupNoRestrictedModePackages} or
+     * {@link #mRestoreNoRestrictedModePackages} so this method will immediately return without
+     * any IPC to the transport.
+     */
+    private boolean shouldUseRestrictedBackupModeForPackage(
+            @BackupAnnotations.OperationType int mode, String packageName) {
+        if (!Flags.enableRestrictedModeChanges()) {
+            return true;
+        }
+
+        // Key/Value apps are never put in restricted mode.
+        if (mode == ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL
+                || mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE) {
+            return false;
+        }
+
+        try {
+            PackageManager.Property property = mPackageManager.getPropertyAsUser(
+                    PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE,
+                    packageName, /* className= */ null,
+                    mUserId);
+            if (property.isBoolean()) {
+                // If the package has explicitly specified, we won't ask the transport.
+                return property.getBoolean();
+            } else {
+                Slog.w(TAG, PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE
+                        + "must be a boolean.");
+            }
+        } catch (NameNotFoundException e) {
+            // This is expected when the package has not defined the property in its manifest.
+        }
+
+        // The package has not specified the property. The behavior depends on the package's
+        // targetSdk.
+        // <36 gets the old behavior of always using restricted mode.
+        if (!CompatChanges.isChangeEnabled(OS_DECIDES_BACKUP_RESTRICTED_MODE, packageName,
+                UserHandle.of(mUserId))) {
+            return true;
+        }
+
+        // Apps targeting >=36 get the behavior decided by the transport.
+        // By this point, we should have asked the transport and cached its decision.
+        if ((mode == ApplicationThreadConstants.BACKUP_MODE_FULL
+                && mBackupNoRestrictedModePackages.contains(packageName))
+                || (mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL
+                && mRestoreNoRestrictedModePackages.contains(packageName))) {
+            Slog.d(TAG, "Transport requested no restricted mode for: " + packageName);
+            return false;
+        }
+        return true;
+    }
+
     private boolean startConfirmationUi(int token, String action) {
         try {
             Intent confIntent = new Intent(action);
diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
index cca166b..be9cdc8 100644
--- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
+++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java
@@ -16,6 +16,8 @@
 
 package com.android.server.backup.fullbackup;
 
+import static android.app.backup.BackupAnnotations.OperationType.BACKUP;
+
 import static com.android.server.backup.BackupManagerService.DEBUG;
 import static com.android.server.backup.BackupManagerService.DEBUG_SCHEDULING;
 import static com.android.server.backup.BackupManagerService.MORE_DEBUG;
@@ -34,6 +36,7 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.Log;
 import android.util.Slog;
@@ -388,6 +391,10 @@
                 }
             }
 
+            // We ask the transport which packages should not be put in restricted mode and cache
+            // the result in UBMS to be used later when the apps are started for backup.
+            setNoRestrictedModePackages(transport, mPackages);
+
             // Set up to send data to the transport
             final int N = mPackages.size();
             int chunkSizeInBytes = 8 * 1024; // 8KB
@@ -694,6 +701,9 @@
                 mUserBackupManagerService.scheduleNextFullBackupJob(backoff);
             }
 
+            // Clear this to avoid using the memory until reboot.
+            mUserBackupManagerService.clearNoRestrictedModePackages();
+
             Slog.i(TAG, "Full data backup pass finished.");
             mUserBackupManagerService.getWakelock().release();
         }
@@ -722,6 +732,21 @@
         }
     }
 
+    private void setNoRestrictedModePackages(BackupTransportClient transport,
+            List<PackageInfo> packages) {
+        try {
+            Set<String> packageNames = new ArraySet<>();
+            for (int i = 0; i < packages.size(); i++) {
+                packageNames.add(packages.get(i).packageName);
+            }
+            packageNames = transport.getPackagesThatShouldNotUseRestrictedMode(packageNames,
+                    BACKUP);
+            mUserBackupManagerService.setNoRestrictedModePackages(packageNames, BACKUP);
+        } catch (RemoteException e) {
+            Slog.i(TAG, "Failed to retrieve no restricted mode packages from transport");
+        }
+    }
+
     // Run the backup and pipe it back to the given socket -- expects to run on
     // a standalone thread.  The  runner owns this half of the pipe, and closes
     // it to indicate EOD to the other end.
diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index e536876..5ee51a5 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -53,6 +53,7 @@
 import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
+import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.Slog;
 
@@ -482,6 +483,10 @@
                 return;
             }
 
+            // We ask the transport which packages should not be put in restricted mode and cache
+            // the result in UBMS to be used later when the apps are started for restore.
+            setNoRestrictedModePackages(transport, packages);
+
             RestoreDescription desc = transport.nextRestorePackage();
             if (desc == null) {
                 Slog.e(TAG, "No restore metadata available; halting");
@@ -1358,6 +1363,9 @@
         // Clear any ongoing session timeout.
         backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT);
 
+        // Clear this to avoid using the memory until reboot.
+        backupManagerService.clearNoRestrictedModePackages();
+
         // If we have a PM token, we must under all circumstances be sure to
         // handshake when we've finished.
         if (mPmToken > 0) {
@@ -1819,4 +1827,20 @@
 
         return packageInfo;
     }
+
+    @VisibleForTesting
+    void setNoRestrictedModePackages(BackupTransportClient transport,
+            PackageInfo[] packages) {
+        try {
+            Set<String> packageNames = new ArraySet<>();
+            for (int i = 0; i < packages.length; i++) {
+                packageNames.add(packages[i].packageName);
+            }
+            packageNames = transport.getPackagesThatShouldNotUseRestrictedMode(packageNames,
+                    RESTORE);
+            backupManagerService.setNoRestrictedModePackages(packageNames, RESTORE);
+        } catch (RemoteException e) {
+            Slog.i(TAG, "Failed to retrieve restricted mode packages from transport");
+        }
+    }
 }
diff --git a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
index daf34152..373811f 100644
--- a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
+++ b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java
@@ -17,6 +17,7 @@
 package com.android.server.backup.transport;
 
 import android.annotation.Nullable;
+import android.app.backup.BackupAnnotations;
 import android.app.backup.BackupTransport;
 import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.RestoreDescription;
@@ -26,6 +27,7 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.util.ArraySet;
 import android.util.Slog;
 
 import com.android.internal.backup.IBackupTransport;
@@ -375,6 +377,26 @@
     }
 
     /**
+     * See
+     * {@link IBackupTransport#getPackagesThatShouldNotUseRestrictedMode(List, int, AndroidFuture)}.
+     */
+    public Set<String> getPackagesThatShouldNotUseRestrictedMode(Set<String> packageNames,
+            @BackupAnnotations.OperationType
+            int operationType) throws RemoteException {
+        AndroidFuture<List<String>> resultFuture = mTransportFutures.newFuture();
+        mTransportBinder.getPackagesThatShouldNotUseRestrictedMode(List.copyOf(packageNames),
+                operationType,
+                resultFuture);
+        List<String> resultList = getFutureResult(resultFuture);
+        Set<String> set = new ArraySet<>();
+        if (resultList == null) {
+            return set;
+        }
+        set.addAll(resultList);
+        return set;
+    }
+
+    /**
      * Allows the {@link TransportConnection} to notify this client
      * if the underlying transport has become unusable.  If that happens
      * we want to cancel all active futures or callbacks.