Unlock all users before moving or migrating.

When moving apps or shared storage between storage media on FBE
devices, we need all users to be unlocked to successfully move
the data.  This change asks the user to enter the credentials for
any locked users as part of the moving/migration wizard flows.

To do this we relax Utils.enforceSameOwner() to let us prompt for the
credentials of unrelated users, but we carefully only extend this
capability to callers interacting with the "internal" activities,
which require the MANAGE_USERS permission.

Test: builds, boots, users are unlocked before moving
Bug: 29923055, 25861755
Change-Id: Ifaeb2557c4f8c4354e1d380eaa0e413768ee239f
diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java
index cab3139..6b0a5b8 100644
--- a/src/com/android/settings/Utils.java
+++ b/src/com/android/settings/Utils.java
@@ -106,6 +106,7 @@
 import com.android.internal.app.UnlaunchableAppActivity;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.settings.password.ChooseLockSettingsHelper;
 import com.android.settings.wrapper.DevicePolicyManagerWrapper;
 import com.android.settings.wrapper.FingerprintManagerWrapper;
 
@@ -995,20 +996,37 @@
     }
 
     /**
-     * Returns the user id present in the bundle with {@link Intent#EXTRA_USER_ID} if it
-     * belongs to the current user.
+     * Returns the user id present in the bundle with
+     * {@link Intent#EXTRA_USER_ID} if it belongs to the current user.
      *
-     * @throws SecurityException if the given userId does not belong to the current user group.
+     * @throws SecurityException if the given userId does not belong to the
+     *             current user group.
      */
     public static int getUserIdFromBundle(Context context, Bundle bundle) {
+        return getUserIdFromBundle(context, bundle, false);
+    }
+
+    /**
+     * Returns the user id present in the bundle with
+     * {@link Intent#EXTRA_USER_ID} if it belongs to the current user.
+     *
+     * @param isInternal indicating if the caller is "internal" to the system,
+     *            meaning we're willing to trust extras like
+     *            {@link ChooseLockSettingsHelper#EXTRA_ALLOW_ANY_USER}.
+     * @throws SecurityException if the given userId does not belong to the
+     *             current user group.
+     */
+    public static int getUserIdFromBundle(Context context, Bundle bundle, boolean isInternal) {
         if (bundle == null) {
             return getCredentialOwnerUserId(context);
         }
+        final boolean allowAnyUser = isInternal
+                && bundle.getBoolean(ChooseLockSettingsHelper.EXTRA_ALLOW_ANY_USER, false);
         int userId = bundle.getInt(Intent.EXTRA_USER_ID, UserHandle.myUserId());
         if (userId == LockPatternUtils.USER_FRP) {
-            return enforceSystemUser(context, userId);
+            return allowAnyUser ? userId : enforceSystemUser(context, userId);
         } else {
-            return enforceSameOwner(context, userId);
+            return allowAnyUser ? userId : enforceSameOwner(context, userId);
         }
     }
 
diff --git a/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java b/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java
index d9e42df..8c8b90e 100644
--- a/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java
+++ b/src/com/android/settings/deviceinfo/StorageWizardMigrateConfirm.java
@@ -16,22 +16,28 @@
 
 package com.android.settings.deviceinfo;
 
+import static com.android.settings.deviceinfo.StorageSettings.TAG;
+
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
 import android.os.Bundle;
+import android.os.UserManager;
 import android.os.storage.DiskInfo;
 import android.os.storage.StorageManager;
 import android.os.storage.VolumeInfo;
+import android.text.TextUtils;
 import android.util.Log;
-
 import android.widget.Toast;
+
 import com.android.settings.R;
+import com.android.settings.password.ChooseLockSettingsHelper;
 
 import java.util.Objects;
 
-import static com.android.settings.deviceinfo.StorageSettings.TAG;
-
 public class StorageWizardMigrateConfirm extends StorageWizardBase {
+    private static final int REQUEST_CREDENTIAL = 100;
+
     private MigrateEstimateTask mEstimate;
 
     @Override
@@ -75,9 +81,22 @@
 
     @Override
     public void onNavigateNext() {
-        int moveId;
+        // Ensure that all users are unlocked so that we can move their data
+        if (StorageManager.isFileEncryptedNativeOrEmulated()) {
+            for (UserInfo user : getSystemService(UserManager.class).getUsers()) {
+                if (!StorageManager.isUserKeyUnlocked(user.id)) {
+                    Log.d(TAG, "User " + user.id + " is currently locked; requesting unlock");
+                    final CharSequence description = TextUtils.expandTemplate(
+                            getText(R.string.storage_wizard_move_unlock), user.name);
+                    new ChooseLockSettingsHelper(this).launchConfirmationActivityForAnyUser(
+                            REQUEST_CREDENTIAL, null, null, description, user.id);
+                    return;
+                }
+            }
+        }
 
         // We only expect exceptions from StorageManagerService#setPrimaryStorageUuid
+        int moveId;
         try {
             moveId = getPackageManager().movePrimaryStorage(mVolume);
         } catch (IllegalArgumentException e) {
@@ -108,4 +127,22 @@
         startActivity(intent);
         finishAffinity();
     }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CREDENTIAL) {
+            if (resultCode == RESULT_OK) {
+                // Credentials confirmed, so storage should be unlocked; let's
+                // go look for the next locked user.
+                onNavigateNext();
+            } else {
+                // User wasn't able to confirm credentials, so we're okay
+                // landing back at the wizard page again, where they read
+                // instructions again and tap "Next" to try again.
+                Log.w(TAG, "Failed to confirm credentials");
+            }
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
 }
diff --git a/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java b/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java
index 65b3d1f..e82612c 100644
--- a/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java
+++ b/src/com/android/settings/deviceinfo/StorageWizardMoveConfirm.java
@@ -16,20 +16,30 @@
 
 package com.android.settings.deviceinfo;
 
-import android.content.Intent;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.os.Bundle;
-
-import com.android.internal.util.Preconditions;
-import com.android.settings.R;
-
 import static android.content.Intent.EXTRA_PACKAGE_NAME;
 import static android.content.Intent.EXTRA_TITLE;
 import static android.content.pm.PackageManager.EXTRA_MOVE_ID;
 import static android.os.storage.VolumeInfo.EXTRA_VOLUME_ID;
 
+import static com.android.settings.deviceinfo.StorageSettings.TAG;
+
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.UserInfo;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+import com.android.settings.R;
+import com.android.settings.password.ChooseLockSettingsHelper;
+
 public class StorageWizardMoveConfirm extends StorageWizardBase {
+    private static final int REQUEST_CREDENTIAL = 100;
+
     private String mPackageName;
     private ApplicationInfo mApp;
 
@@ -66,6 +76,20 @@
 
     @Override
     public void onNavigateNext() {
+        // Ensure that all users are unlocked so that we can move their data
+        if (StorageManager.isFileEncryptedNativeOrEmulated()) {
+            for (UserInfo user : getSystemService(UserManager.class).getUsers()) {
+                if (!StorageManager.isUserKeyUnlocked(user.id)) {
+                    Log.d(TAG, "User " + user.id + " is currently locked; requesting unlock");
+                    final CharSequence description = TextUtils.expandTemplate(
+                            getText(R.string.storage_wizard_move_unlock), user.name);
+                    new ChooseLockSettingsHelper(this).launchConfirmationActivityForAnyUser(
+                            REQUEST_CREDENTIAL, null, null, description, user.id);
+                    return;
+                }
+            }
+        }
+
         // Kick off move before we transition
         final String appName = getPackageManager().getApplicationLabel(mApp).toString();
         final int moveId = getPackageManager().movePackage(mPackageName, mVolume);
@@ -77,4 +101,22 @@
         startActivity(intent);
         finishAffinity();
     }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CREDENTIAL) {
+            if (resultCode == RESULT_OK) {
+                // Credentials confirmed, so storage should be unlocked; let's
+                // go look for the next locked user.
+                onNavigateNext();
+            } else {
+                // User wasn't able to confirm credentials, so we're okay
+                // landing back at the wizard page again, where they read
+                // instructions again and tap "Next" to try again.
+                Log.w(TAG, "Failed to confirm credentials");
+            }
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
 }
diff --git a/src/com/android/settings/password/ChooseLockSettingsHelper.java b/src/com/android/settings/password/ChooseLockSettingsHelper.java
index 806ee67..11d40a3 100644
--- a/src/com/android/settings/password/ChooseLockSettingsHelper.java
+++ b/src/com/android/settings/password/ChooseLockSettingsHelper.java
@@ -23,6 +23,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.Intent;
 import android.content.IntentSender;
+import android.os.Bundle;
 import android.os.UserManager;
 
 import com.android.internal.annotations.VisibleForTesting;
@@ -42,6 +43,12 @@
     public static final String EXTRA_KEY_FOR_FINGERPRINT = "for_fingerprint";
     public static final String EXTRA_KEY_FOR_CHANGE_CRED_REQUIRED_FOR_BOOT = "for_cred_req_boot";
 
+    /**
+     * When invoked via {@link ConfirmLockPassword.InternalActivity}, this flag
+     * controls if we relax the enforcement of
+     * {@link Utils#enforceSameOwner(android.content.Context, int)}.
+     */
+    public static final String EXTRA_ALLOW_ANY_USER = "allow_any_user";
 
     @VisibleForTesting LockPatternUtils mLockPatternUtils;
     private Activity mActivity;
@@ -200,25 +207,47 @@
                 external, true, challenge, Utils.enforceSameOwner(mActivity, userId));
     }
 
+    /**
+     * Variant that allows you to prompt for credentials of any user, including
+     * those which aren't associated with the current user. As an example, this
+     * is useful when unlocking the storage for secondary users.
+     */
+    public boolean launchConfirmationActivityForAnyUser(int request,
+            @Nullable CharSequence title, @Nullable CharSequence header,
+            @Nullable CharSequence description, int userId) {
+        final Bundle extras = new Bundle();
+        extras.putBoolean(EXTRA_ALLOW_ANY_USER, true);
+        return launchConfirmationActivity(request, title, header, description, false,
+                false, true, 0, userId, extras);
+    }
+
     private boolean launchConfirmationActivity(int request, @Nullable CharSequence title,
             @Nullable CharSequence header, @Nullable CharSequence description,
             boolean returnCredentials, boolean external, boolean hasChallenge,
             long challenge, int userId) {
         return launchConfirmationActivity(request, title, header, description, returnCredentials,
-                external, hasChallenge, challenge, userId, null /* alternateButton */);
+                external, hasChallenge, challenge, userId, null /* alternateButton */, null);
+    }
+
+    private boolean launchConfirmationActivity(int request, @Nullable CharSequence title,
+            @Nullable CharSequence header, @Nullable CharSequence description,
+            boolean returnCredentials, boolean external, boolean hasChallenge,
+            long challenge, int userId, Bundle extras) {
+        return launchConfirmationActivity(request, title, header, description, returnCredentials,
+                external, hasChallenge, challenge, userId, null /* alternateButton */, extras);
     }
 
     public boolean launchFrpConfirmationActivity(int request, @Nullable CharSequence header,
             @Nullable CharSequence description, @Nullable CharSequence alternateButton) {
         return launchConfirmationActivity(request, null /* title */, header, description,
                 false /* returnCredentials */, true /* external */, false /* hasChallenge */,
-                0 /* challenge */, LockPatternUtils.USER_FRP, alternateButton);
+                0 /* challenge */, LockPatternUtils.USER_FRP, alternateButton, null);
     }
 
     private boolean launchConfirmationActivity(int request, @Nullable CharSequence title,
             @Nullable CharSequence header, @Nullable CharSequence description,
             boolean returnCredentials, boolean external, boolean hasChallenge,
-            long challenge, int userId, @Nullable CharSequence alternateButton) {
+            long challenge, int userId, @Nullable CharSequence alternateButton, Bundle extras) {
         final int effectiveUserId = UserManager.get(mActivity).getCredentialOwnerProfile(userId);
         boolean launched = false;
 
@@ -228,7 +257,7 @@
                         returnCredentials || hasChallenge
                                 ? ConfirmLockPattern.InternalActivity.class
                                 : ConfirmLockPattern.class, returnCredentials, external,
-                                hasChallenge, challenge, userId, alternateButton);
+                                hasChallenge, challenge, userId, alternateButton, extras);
                 break;
             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX:
@@ -240,7 +269,7 @@
                         returnCredentials || hasChallenge
                                 ? ConfirmLockPassword.InternalActivity.class
                                 : ConfirmLockPassword.class, returnCredentials, external,
-                                hasChallenge, challenge, userId, alternateButton);
+                                hasChallenge, challenge, userId, alternateButton, extras);
                 break;
         }
         return launched;
@@ -249,7 +278,7 @@
     private boolean launchConfirmationActivity(int request, CharSequence title, CharSequence header,
             CharSequence message, Class<?> activityClass, boolean returnCredentials,
             boolean external, boolean hasChallenge, long challenge,
-            int userId, @Nullable CharSequence alternateButton) {
+            int userId, @Nullable CharSequence alternateButton, Bundle extras) {
         final boolean frp = (userId == LockPatternUtils.USER_FRP);
         final Intent intent = new Intent();
         intent.putExtra(ConfirmDeviceCredentialBaseFragment.TITLE_TEXT, title);
@@ -266,6 +295,9 @@
         intent.putExtra(SettingsActivity.EXTRA_HIDE_DRAWER, true);
         intent.putExtra(Intent.EXTRA_USER_ID, userId);
         intent.putExtra(KeyguardManager.EXTRA_ALTERNATE_BUTTON_LABEL, alternateButton);
+        if (extras != null) {
+            intent.putExtras(extras);
+        }
         intent.setClassName(ConfirmDeviceCredentialBaseFragment.PACKAGE, activityClass.getName());
         if (external) {
             intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java
index d2bd934..ab8c3dc 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseActivity.java
@@ -45,10 +45,15 @@
     private boolean mIsKeyguardLocked = false;
     private ConfirmCredentialTheme mConfirmCredentialTheme;
 
+    private boolean isInternalActivity() {
+        return (this instanceof ConfirmLockPassword.InternalActivity)
+                || (this instanceof ConfirmLockPattern.InternalActivity);
+    }
+
     @Override
     protected void onCreate(Bundle savedState) {
         int credentialOwnerUserId = Utils.getCredentialOwnerUserId(this,
-                Utils.getUserIdFromBundle(this, getIntent().getExtras()));
+                Utils.getUserIdFromBundle(this, getIntent().getExtras(), isInternalActivity()));
         if (UserManager.get(this).isManagedProfile(credentialOwnerUserId)) {
             setTheme(R.style.Theme_ConfirmDeviceCredentialsWork);
             mConfirmCredentialTheme = ConfirmCredentialTheme.WORK;
diff --git a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
index 350fc76..5b18925 100644
--- a/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
+++ b/src/com/android/settings/password/ConfirmDeviceCredentialBaseFragment.java
@@ -94,6 +94,11 @@
     protected boolean mFrp;
     private CharSequence mFrpAlternateButtonText;
 
+    private boolean isInternalActivity() {
+        return (getActivity() instanceof ConfirmLockPassword.InternalActivity)
+                || (getActivity() instanceof ConfirmLockPattern.InternalActivity);
+    }
+
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -103,7 +108,8 @@
                 ChooseLockSettingsHelper.EXTRA_KEY_RETURN_CREDENTIALS, false);
         // Only take this argument into account if it belongs to the current profile.
         Intent intent = getActivity().getIntent();
-        mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras());
+        mUserId = Utils.getUserIdFromBundle(getActivity(), intent.getExtras(),
+                isInternalActivity());
         mFrp = (mUserId == LockPatternUtils.USER_FRP);
         mUserManager = UserManager.get(getActivity());
         mEffectiveUserId = mUserManager.getCredentialOwnerProfile(mUserId);
@@ -141,7 +147,7 @@
                 getActivity(),
                 Utils.getUserIdFromBundle(
                         getActivity(),
-                        getActivity().getIntent().getExtras()));
+                        getActivity().getIntent().getExtras(), isInternalActivity()));
         if (mUserManager.isManagedProfile(credentialOwnerUserId)) {
             setWorkChallengeBackground(view, credentialOwnerUserId);
         }