Skip restore of apps that have been launched or restored.

This is to ensure we don't overwrite existing state. The behavior will
be controlled by the BackupTransport implementation that provides the
restore data.

Bug: 308401499
Test: atest BackupEligibilityRulesTest
      atest ActiveRestoreSessionTest
      Manual: perform initial restore, verify all apps restore, rerun
      restore for already restored / launched apps, verify no restore
Change-Id: I6ee1a2aca49224f0228f7ad340d3424fbe50e7bb
diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java
index 0b8d1df..6b558d0 100644
--- a/core/java/android/app/backup/BackupAgent.java
+++ b/core/java/android/app/backup/BackupAgent.java
@@ -184,6 +184,14 @@
     public static final int FLAG_DEVICE_TO_DEVICE_TRANSFER = 2;
 
     /**
+     * Flag for {@link RestoreSet#backupTransportFlags} to indicate if restore should be skipped
+     * for apps that have already been launched.
+     *
+     * @hide
+     */
+    public static final int FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS = 1 << 2;
+
+    /**
      * Flag for {@link BackupDataOutput#getTransportFlags()} and
      * {@link FullBackupDataOutput#getTransportFlags()} only.
      *
diff --git a/services/backup/Android.bp b/services/backup/Android.bp
index b086406..acb5911 100644
--- a/services/backup/Android.bp
+++ b/services/backup/Android.bp
@@ -19,5 +19,16 @@
     defaults: ["platform_service_defaults"],
     srcs: [":services.backup-sources"],
     libs: ["services.core"],
-    static_libs: ["app-compat-annotations"],
+    static_libs: ["app-compat-annotations", "backup_flags_lib"],
+}
+
+aconfig_declarations {
+    name: "backup_flags",
+    package: "com.android.server.backup",
+    srcs: ["flags.aconfig"],
+}
+
+java_aconfig_library {
+    name: "backup_flags_lib",
+    aconfig_declarations: "backup_flags",
 }
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
new file mode 100644
index 0000000..d695d36
--- /dev/null
+++ b/services/backup/flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.backup"
+
+flag {
+    name: "enable_skipping_restore_launched_apps"
+    namespace: "onboarding"
+    description: "Enforce behavior determined by BackupTransport implementation on whether to skip "
+            "restore for apps that have been launched."
+    bug: "308401499"
+    is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
index 70d7fac..9f7b627 100644
--- a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
+++ b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java
@@ -24,6 +24,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.backup.BackupAgent;
 import android.app.backup.BackupAnnotations.BackupDestination;
 import android.app.backup.IBackupManagerMonitor;
 import android.app.backup.IRestoreObserver;
@@ -32,11 +33,15 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
 import android.os.Binder;
 import android.os.Handler;
 import android.os.Message;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.LocalServices;
+import com.android.server.backup.Flags;
 import com.android.server.backup.TransportManager;
 import com.android.server.backup.UserBackupManagerService;
 import com.android.server.backup.internal.OnTaskFinishedListener;
@@ -296,12 +301,26 @@
         return -1;
     }
 
-    private BackupEligibilityRules getBackupEligibilityRules(RestoreSet restoreSet) {
+    @VisibleForTesting
+    BackupEligibilityRules getBackupEligibilityRules(RestoreSet restoreSet) {
         // TODO(b/182986784): Remove device name comparison once a designated field for operation
         //  type is added to RestoreSet object.
         int backupDestination = DEVICE_NAME_FOR_D2D_SET.equals(restoreSet.device)
                 ? BackupDestination.DEVICE_TRANSFER : BackupDestination.CLOUD;
-        return mBackupManagerService.getEligibilityRulesForOperation(backupDestination);
+
+        if (!Flags.enableSkippingRestoreLaunchedApps()) {
+            return mBackupManagerService.getEligibilityRulesForOperation(backupDestination);
+        }
+
+        boolean skipRestoreForLaunchedApps = (restoreSet.backupTransportFlags
+                & BackupAgent.FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS) != 0;
+
+        return new BackupEligibilityRules(mBackupManagerService.getPackageManager(),
+                LocalServices.getService(PackageManagerInternal.class),
+                mUserId,
+                mBackupManagerService.getContext(),
+                backupDestination,
+                skipRestoreForLaunchedApps);
     }
 
     public synchronized int restorePackage(String packageName, IRestoreObserver observer,
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 bbec79d..96a873e 100644
--- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -63,6 +63,7 @@
 import com.android.server.backup.BackupAndRestoreFeatureFlags;
 import com.android.server.backup.BackupRestoreTask;
 import com.android.server.backup.BackupUtils;
+import com.android.server.backup.Flags;
 import com.android.server.backup.OperationStorage;
 import com.android.server.backup.OperationStorage.OpType;
 import com.android.server.backup.PackageManagerBackupAgent;
@@ -263,7 +264,14 @@
                         continue;
                     }
 
-                    if (backupEligibilityRules.appIsEligibleForBackup(info.applicationInfo)) {
+
+                    ApplicationInfo applicationInfo = info.applicationInfo;
+                    if (backupEligibilityRules.appIsEligibleForBackup(applicationInfo)) {
+                        if (Flags.enableSkippingRestoreLaunchedApps()
+                            && !backupEligibilityRules.isAppEligibleForRestore(applicationInfo)) {
+                            continue;
+                        }
+
                         mAcceptSet.add(info);
                     }
                 } catch (NameNotFoundException e) {
diff --git a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
index 7c47f1e..f24a3c1 100644
--- a/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
+++ b/services/backup/java/com/android/server/backup/utils/BackupEligibilityRules.java
@@ -80,6 +80,7 @@
     private final int mUserId;
     private boolean mIsProfileUser = false;
     @BackupDestination  private final int mBackupDestination;
+    private final boolean mSkipRestoreForLaunchedApps;
 
     /**
      * When  this change is enabled, {@code adb backup}  is automatically turned on for apps
@@ -112,12 +113,23 @@
             int userId,
             Context context,
             @BackupDestination int backupDestination) {
+        this(packageManager, packageManagerInternal, userId, context, backupDestination,
+                /* skipRestoreForLaunchedApps */ false);
+    }
+
+    public BackupEligibilityRules(PackageManager packageManager,
+            PackageManagerInternal packageManagerInternal,
+            int userId,
+            Context context,
+            @BackupDestination int backupDestination,
+            boolean skipRestoreForLaunchedApps) {
         mPackageManager = packageManager;
         mPackageManagerInternal = packageManagerInternal;
         mUserId = userId;
         mBackupDestination = backupDestination;
         UserManager userManager = context.getSystemService(UserManager.class);
         mIsProfileUser = userManager.isProfile();
+        mSkipRestoreForLaunchedApps = skipRestoreForLaunchedApps;
     }
 
     /**
@@ -132,6 +144,9 @@
      *     <li>it is the special shared-storage backup package used for 'adb backup'
      * </ol>
      *
+     * These eligibility conditions are also checked before restore, in case the backup happened on
+     * a device / from the version of the app where these rules were not enforced.
+     *
      * However, the above eligibility rules are ignored for non-system apps in in case of
      * device-to-device migration, see {@link BackupDestination}.
      */
@@ -283,6 +298,27 @@
         }
     }
 
+    /**
+     * Determine if data restore should be run for the given package.
+     *
+     * <p>This is used in combination with {@link #appIsEligibleForBackup(ApplicationInfo)} that
+     * checks whether the backup being restored should have happened in the first place.</p>
+     */
+    public boolean isAppEligibleForRestore(ApplicationInfo app) {
+        if (!mSkipRestoreForLaunchedApps) {
+            return true;
+        }
+
+        // If an app implemented a BackupAgent, they are expected to handle being restored even
+        // after first launch and avoid conflicts between existing app data and restored data.
+        if (app.backupAgentName != null) {
+            return true;
+        }
+
+        // Otherwise only restore an app if it hasn't been launched before.
+        return !mPackageManagerInternal.wasPackageEverLaunched(app.packageName, mUserId);
+    }
+
     /** Avoid backups of 'disabled' apps. */
     @VisibleForTesting
     boolean appIsDisabled(
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 063af57..113511e 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -46,6 +46,7 @@
         "androidx.test.espresso.core",
         "androidx.test.espresso.contrib",
         "androidx.test.ext.truth",
+        "backup_flags_lib",
         "flag-junit",
         "frameworks-base-testutils",
         "hamcrest-library",
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
new file mode 100644
index 0000000..0006d0a
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.restore;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.backup.BackupAgent;
+import android.app.backup.RestoreSet;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.os.UserManager;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.server.LocalServices;
+import com.android.server.backup.Flags;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.UserBackupManagerService;
+import com.android.server.backup.utils.BackupEligibilityRules;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ActiveRestoreSessionTest {
+    private static final String TEST_APP_NAME = "test_app";
+
+    private ActiveRestoreSession mRestoreSession;
+    private ApplicationInfo mTestApp;
+
+    @Mock
+    private UserBackupManagerService mBackupManagerService;
+    @Mock
+    private BackupEligibilityRules mBackupEligibilityRules;
+    @Mock
+    private Context mContext;
+    @Mock
+    private PackageManagerInternal mPackageManagerInternal;
+    @Mock
+    private TransportManager mTransportManager;
+    @Mock private UserManager mUserManager;
+
+    @Rule
+    public final SetFlagsRule mFlagsRule = new SetFlagsRule();
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule
+            .Builder(/* testClassInstance */ this)
+            .mockStatic(LocalServices.class)
+            .afterSessionFinished(
+                    () -> LocalServices.removeServiceForTest(PackageManagerInternal.class)
+            ).build();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(/* testClass */ this);
+        when(mBackupEligibilityRules.isAppEligibleForRestore(any())).thenReturn(true);
+        when(mBackupManagerService.getEligibilityRulesForOperation(anyInt())).thenReturn(
+                mBackupEligibilityRules);
+        when(mBackupManagerService.getTransportManager()).thenReturn(mTransportManager);
+        when(mBackupManagerService.getContext()).thenReturn(mContext);
+        when(mContext.getSystemService(eq(UserManager.class))).thenReturn(mUserManager);
+        when(LocalServices.getService(PackageManagerInternal.class)).thenReturn(
+                mPackageManagerInternal);
+
+        mRestoreSession = new ActiveRestoreSession(mBackupManagerService,
+                /* packageName */ null,
+                /* transportName */ "",
+                mBackupEligibilityRules);
+        mTestApp = new ApplicationInfo();
+        mTestApp.packageName = TEST_APP_NAME;
+    }
+
+    @Test
+    public void testGetBackupEligibilityRules_skipRestoreFlagOn_skipsLaunchedAppRestore() {
+        mFlagsRule.enableFlags(Flags.FLAG_ENABLE_SKIPPING_RESTORE_LAUNCHED_APPS);
+        RestoreSet restoreSet = new RestoreSet(
+                /* name */ null,
+                /* device */ null,
+                /* token */ 0,
+                /* backupTransportFlags */ BackupAgent.FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS);
+        when(mPackageManagerInternal.wasPackageEverLaunched(eq(TEST_APP_NAME), anyInt()))
+                .thenReturn(true);
+
+        BackupEligibilityRules eligibilityRules = mRestoreSession.getBackupEligibilityRules(
+                restoreSet);
+
+        assertThat(eligibilityRules.isAppEligibleForRestore(mTestApp)).isFalse();
+    }
+
+    @Test
+    public void testGetBackupEligibilityRules_skipRestoreFlagOff_allowsAppRestore() {
+        mFlagsRule.disableFlags(Flags.FLAG_ENABLE_SKIPPING_RESTORE_LAUNCHED_APPS);
+        RestoreSet restoreSet = new RestoreSet(
+                /* name */ null,
+                /* device */ null,
+                /* token */ 0,
+                /* backupTransportFlags */ BackupAgent.FLAG_SKIP_RESTORE_FOR_LAUNCHED_APPS);
+        when(mPackageManagerInternal.wasPackageEverLaunched(eq(TEST_APP_NAME), anyInt()))
+                .thenReturn(true);
+
+        BackupEligibilityRules eligibilityRules = mRestoreSession.getBackupEligibilityRules(
+                restoreSet);
+
+        assertThat(eligibilityRules.isAppEligibleForRestore(mTestApp)).isTrue();
+    }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
index 0306655..290b260 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/utils/BackupEligibilityRulesTest.java
@@ -860,6 +860,66 @@
         assertThat(result).isFalse();
     }
 
+    @Test
+    public void isAppEligibleForRestore_hasBeenLaunched_returnsFalse() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(true);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ null);
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ true);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isFalse();
+    }
+
+    @Test
+    public void isAppEligibleForRestore_hasNotBeenLaunched_returnsTrue() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(false);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ null);
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ false);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isTrue();
+    }
+
+    @Test
+    public void isAppEligibleForRestore_launchedButHasBackupAgent_returnsTrue() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(true);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ "BackupAgent");
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ false);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isTrue();
+    }
+
+    @Test
+    public void isAppEligibleForRestore_doNotSkipRestoreForLaunched_returnsTrue() {
+        when(mMockPackageManagerInternal.wasPackageEverLaunched(eq(TEST_PACKAGE_NAME), eq(mUserId)))
+                .thenReturn(true);
+        ApplicationInfo applicationInfo = getApplicationInfo(/* appUid */ 0, /* flags */ 0,
+                /* backupAgentName */ null);
+        BackupEligibilityRules backupEligibilityRules = new BackupEligibilityRules(mPackageManager,
+                mMockPackageManagerInternal, mUserId, mContext, BackupDestination.CLOUD,
+                /* skipRestoreForLaunchedApps */ false);
+
+        boolean isEligible = backupEligibilityRules.isAppEligibleForRestore(applicationInfo);
+
+        assertThat(isEligible).isTrue();
+    }
+
     private BackupEligibilityRules getBackupEligibilityRules(
             @BackupDestination int backupDestination) {
         return new BackupEligibilityRules(mPackageManager, mMockPackageManagerInternal, mUserId,