Migrate primary external storage.

Wire up through MountService to call down into vold.  Watch for
unsolicited events that report progress, including special value "82"
that signals that copy has finished.  We use this value to persist
the volumeUuid in case of unexpected reboot, since it indicates the
new volume is ready.

Wire progress updates through existing callback pipeline.

Update the volume mounting code to match against the persisted UUID
when selecting the primary external storage.

Bug: 19993667
Change-Id: Id46957610fb43517bbfbc368f29b7d430664590d
diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java
index b84b1e2..39de1dc7 100644
--- a/cmds/pm/src/com/android/commands/pm/Pm.java
+++ b/cmds/pm/src/com/android/commands/pm/Pm.java
@@ -30,7 +30,6 @@
 import android.content.pm.IPackageDataObserver;
 import android.content.pm.IPackageInstaller;
 import android.content.pm.IPackageManager;
-import android.content.pm.IPackageMoveObserver;
 import android.content.pm.InstrumentationInfo;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageInstaller;
@@ -237,8 +236,12 @@
             return runForceDexOpt();
         }
 
-        if ("move".equals(op)) {
-            return runMove();
+        if ("move-package".equals(op)) {
+            return runMovePackage();
+        }
+
+        if ("move-primary-storage".equals(op)) {
+            return runMovePrimaryStorage();
         }
 
         try {
@@ -1285,7 +1288,7 @@
         }
     }
 
-    public int runMove() {
+    public int runMovePackage() {
         final String packageName = nextArg();
         String volumeUuid = nextArg();
         if ("internal".equals(volumeUuid)) {
@@ -1313,6 +1316,33 @@
         }
     }
 
+    public int runMovePrimaryStorage() {
+        String volumeUuid = nextArg();
+        if ("internal".equals(volumeUuid)) {
+            volumeUuid = null;
+        }
+
+        try {
+            final int moveId = mPm.movePrimaryStorage(volumeUuid);
+
+            int status = mPm.getMoveStatus(moveId);
+            while (!PackageManager.isMoveStatusFinished(status)) {
+                SystemClock.sleep(DateUtils.SECOND_IN_MILLIS);
+                status = mPm.getMoveStatus(moveId);
+            }
+
+            if (status == PackageManager.MOVE_SUCCEEDED) {
+                System.out.println("Success");
+                return 0;
+            } else {
+                System.err.println("Failure [" + status + "]");
+                return 1;
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
     private int runUninstall() throws RemoteException {
         int flags = 0;
         int userId = UserHandle.USER_ALL;
@@ -1860,7 +1890,8 @@
         System.err.println("       pm install-abandon SESSION_ID");
         System.err.println("       pm uninstall [-k] [--user USER_ID] PACKAGE");
         System.err.println("       pm set-installer PACKAGE INSTALLER");
-        System.err.println("       pm move PACKAGE [internal|UUID]");
+        System.err.println("       pm move-package PACKAGE [internal|UUID]");
+        System.err.println("       pm move-primary-storage [internal|UUID]");
         System.err.println("       pm clear [--user USER_ID] PACKAGE");
         System.err.println("       pm enable [--user USER_ID] PACKAGE_OR_COMPONENT");
         System.err.println("       pm disable [--user USER_ID] PACKAGE_OR_COMPONENT");
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 10f5960..16a2430 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -1556,6 +1556,7 @@
         }
     }
 
+    @Override
     public @Nullable VolumeInfo getPrimaryStorageCurrentVolume() {
         final StorageManager storage = mContext.getSystemService(StorageManager.class);
         final String volumeUuid = storage.getPrimaryStorageUuid();
@@ -1568,6 +1569,7 @@
         }
     }
 
+    @Override
     public @NonNull List<VolumeInfo> getPrimaryStorageCandidateVolumes() {
         final StorageManager storage = mContext.getSystemService(StorageManager.class);
         final VolumeInfo currentVol = getPrimaryStorageCurrentVolume();
diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java
index 0b1031c..16e0bf7 100644
--- a/core/java/android/os/storage/IMountService.java
+++ b/core/java/android/os/storage/IMountService.java
@@ -16,6 +16,7 @@
 
 package android.os.storage;
 
+import android.content.pm.IPackageMoveObserver;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.IInterface;
@@ -1082,12 +1083,14 @@
             }
 
             @Override
-            public void setPrimaryStorageUuid(String volumeUuid) throws RemoteException {
+            public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback)
+                    throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
                 try {
                     _data.writeInterfaceToken(DESCRIPTOR);
                     _data.writeString(volumeUuid);
+                    _data.writeStrongBinder((callback != null ? callback.asBinder() : null));
                     mRemote.transact(Stub.TRANSACTION_setPrimaryStorageUuid, _data, _reply, 0);
                     _reply.readException();
                 } finally {
@@ -1714,7 +1717,9 @@
                 case TRANSACTION_setPrimaryStorageUuid: {
                     data.enforceInterface(DESCRIPTOR);
                     String volumeUuid = data.readString();
-                    setPrimaryStorageUuid(volumeUuid);
+                    IPackageMoveObserver listener = IPackageMoveObserver.Stub.asInterface(
+                            data.readStrongBinder());
+                    setPrimaryStorageUuid(volumeUuid, listener);
                     reply.writeNoException();
                     return true;
                 }
@@ -2020,5 +2025,6 @@
     public void setVolumeUserFlags(String volId, int flags, int mask) throws RemoteException;
 
     public String getPrimaryStorageUuid() throws RemoteException;
-    public void setPrimaryStorageUuid(String volumeUuid) throws RemoteException;
+    public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback)
+            throws RemoteException;
 }
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 747fb40..6116aef 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -22,6 +22,8 @@
 import android.annotation.Nullable;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.IPackageMoveObserver;
+import android.content.pm.PackageManager;
 import android.os.Environment;
 import android.os.FileUtils;
 import android.os.Handler;
@@ -642,7 +644,12 @@
         }
     }
 
-    /** {@hide} */
+    /**
+     * This is not the API you're looking for.
+     *
+     * @see PackageManager#getPrimaryStorageCurrentVolume()
+     * @hide
+     */
     public String getPrimaryStorageUuid() {
         try {
             return mMountService.getPrimaryStorageUuid();
@@ -651,10 +658,15 @@
         }
     }
 
-    /** {@hide} */
-    public void setPrimaryStorageUuid(String volumeUuid) {
+    /**
+     * This is not the API you're looking for.
+     *
+     * @see PackageManager#movePrimaryStorage(VolumeInfo)
+     * @hide
+     */
+    public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) {
         try {
-            mMountService.setPrimaryStorageUuid(volumeUuid);
+            mMountService.setPrimaryStorageUuid(volumeUuid, callback);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index 81088c4..7172ab7 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -30,6 +30,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.IPackageMoveObserver;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.content.res.ObbInfo;
 import android.mtp.MtpStorage;
@@ -178,6 +180,9 @@
     /** Maximum number of ASEC containers allowed to be mounted. */
     private static final int MAX_CONTAINERS = 250;
 
+    /** Magic value sent by MoveTask.cpp */
+    private static final int MOVE_STATUS_COPY_FINISHED = 82;
+
     /*
      * Internal vold response code constants
      */
@@ -226,6 +231,8 @@
         public static final int VOLUME_PATH_CHANGED = 655;
         public static final int VOLUME_DESTROYED = 659;
 
+        public static final int MOVE_STATUS = 660;
+
         /*
          * 700 series - fstrim
          */
@@ -314,6 +321,11 @@
     @GuardedBy("mLock")
     private ArrayMap<String, CountDownLatch> mDiskScanLatches = new ArrayMap<>();
 
+    @GuardedBy("mLock")
+    private IPackageMoveObserver mMoveCallback;
+    @GuardedBy("mLock")
+    private String mMoveTargetUuid;
+
     private DiskInfo findDiskById(String id) {
         synchronized (mLock) {
             final DiskInfo disk = mDisks.get(id);
@@ -347,6 +359,17 @@
         throw new IllegalArgumentException("No volume found for path " + path);
     }
 
+    private VolumeInfo findStorageForUuid(String volumeUuid) {
+        final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, volumeUuid)) {
+            return findVolumeById(VolumeInfo.ID_EMULATED_INTERNAL);
+        } else if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, volumeUuid)) {
+            return storage.getPrimaryPhysicalVolume();
+        } else {
+            return storage.findEmulatedForPrivate(storage.findVolumeByUuid(volumeUuid));
+        }
+    }
+
     private VolumeMetadata findOrCreateMetadataLocked(VolumeInfo vol) {
         VolumeMetadata meta = mMetadata.get(vol.fsUuid);
         if (meta == null) {
@@ -937,6 +960,12 @@
                 break;
             }
 
+            case VoldResponseCode.MOVE_STATUS: {
+                final int status = Integer.parseInt(cooked[1]);
+                onMoveStatusLocked(status);
+                break;
+            }
+
             case VoldResponseCode.FstrimCompleted: {
                 EventLogTags.writeFstrimFinish(SystemClock.elapsedRealtime());
                 break;
@@ -972,24 +1001,36 @@
     }
 
     private void onVolumeCreatedLocked(VolumeInfo vol) {
-        final boolean primaryPhysical = SystemProperties.getBoolean(
-                StorageManager.PROP_PRIMARY_PHYSICAL, false);
-        // TODO: enable switching to another emulated primary
-        if (VolumeInfo.ID_EMULATED_INTERNAL.equals(vol.id) && !primaryPhysical) {
-            vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
-            vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
-            mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
+        if (vol.type == VolumeInfo.TYPE_EMULATED) {
+            final StorageManager storage = mContext.getSystemService(StorageManager.class);
+            final VolumeInfo privateVol = storage.findPrivateForEmulated(vol);
+
+            if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid)
+                    && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id)) {
+                Slog.v(TAG, "Found primary storage at " + vol);
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
+                mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
+
+            } else if (Objects.equals(privateVol.fsUuid, mPrimaryStorageUuid)) {
+                Slog.v(TAG, "Found primary storage at " + vol);
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
+                vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
+                mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
+            }
 
         } else if (vol.type == VolumeInfo.TYPE_PUBLIC) {
-            if (primaryPhysical) {
+            // TODO: only look at first public partition
+            if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid)
+                    && vol.disk.isDefaultPrimary()) {
+                Slog.v(TAG, "Found primary storage at " + vol);
                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_PRIMARY;
                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
             }
 
             // Adoptable public disks are visible to apps, since they meet
             // public API requirement of being in a stable location.
-            final DiskInfo disk = mDisks.get(vol.getDiskId());
-            if (disk != null && disk.isAdoptable()) {
+            if (vol.disk.isAdoptable()) {
                 vol.mountFlags |= VolumeInfo.MOUNT_FLAG_VISIBLE;
             }
 
@@ -1066,6 +1107,35 @@
         }
     }
 
+    private void onMoveStatusLocked(int status) {
+        if (mMoveCallback == null) {
+            Slog.w(TAG, "Odd, status but no move requested");
+            return;
+        }
+
+        // TODO: estimate remaining time
+        try {
+            mMoveCallback.onStatusChanged(-1, status, -1);
+        } catch (RemoteException ignored) {
+        }
+
+        // We've finished copying and we're about to clean up old data, so
+        // remember that move was successful if we get rebooted
+        if (status == MOVE_STATUS_COPY_FINISHED) {
+            Slog.d(TAG, "Move to " + mMoveTargetUuid + " copy phase finshed; persisting");
+
+            mPrimaryStorageUuid = mMoveTargetUuid;
+            writeMetadataLocked();
+        }
+
+        if (PackageManager.isMoveStatusFinished(status)) {
+            Slog.d(TAG, "Move to " + mMoveTargetUuid + " finished with status " + status);
+
+            mMoveCallback = null;
+            mMoveTargetUuid = null;
+        }
+    }
+
     /**
      * Refresh latest metadata into any currently active {@link VolumeInfo}.
      */
@@ -1322,12 +1392,17 @@
         final VolumeInfo vol = findVolumeById(volId);
 
         // TODO: expand PMS to know about multiple volumes
-        if (vol.isPrimary()) {
-            synchronized (mUnmountLock) {
-                mUnmountSignal = new CountDownLatch(1);
-                mPms.updateExternalMediaStatus(false, true);
-                waitForLatch(mUnmountSignal, "mUnmountSignal");
-                mUnmountSignal = null;
+        if (vol.isPrimaryPhysical()) {
+            final long ident = Binder.clearCallingIdentity();
+            try {
+                synchronized (mUnmountLock) {
+                    mUnmountSignal = new CountDownLatch(1);
+                    mPms.updateExternalMediaStatus(false, true);
+                    waitForLatch(mUnmountSignal, "mUnmountSignal");
+                    mUnmountSignal = null;
+                }
+            } finally {
+                Binder.restoreCallingIdentity(ident);
             }
         }
 
@@ -1424,20 +1499,41 @@
     }
 
     @Override
-    public String getPrimaryStorageUuid() throws RemoteException {
+    public String getPrimaryStorageUuid() {
+        enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
+        waitForReady();
+
         synchronized (mLock) {
             return mPrimaryStorageUuid;
         }
     }
 
     @Override
-    public void setPrimaryStorageUuid(String volumeUuid) throws RemoteException {
-        synchronized (mLock) {
-            Slog.d(TAG, "Changing primary storage UUID to " + volumeUuid);
-            mPrimaryStorageUuid = volumeUuid;
-            writeMetadataLocked();
+    public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) {
+        enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
+        waitForReady();
 
-            // TODO: reevaluate all volumes we know about!
+        synchronized (mLock) {
+            final VolumeInfo from = Preconditions.checkNotNull(
+                    findStorageForUuid(mPrimaryStorageUuid));
+            final VolumeInfo to = Preconditions.checkNotNull(
+                    findStorageForUuid(volumeUuid));
+
+            if (Objects.equals(from, to)) {
+                throw new IllegalArgumentException("Primary storage already at " + from);
+            }
+
+            if (mMoveCallback != null) {
+                throw new IllegalStateException("Move already in progress");
+            }
+            mMoveCallback = callback;
+            mMoveTargetUuid = volumeUuid;
+
+            try {
+                mConnector.execute("volume", "move_storage", from.id, to.id);
+            } catch (NativeDaemonConnectorException e) {
+                throw e.rethrowAsParcelableException();
+            }
         }
     }
 
@@ -2875,6 +2971,9 @@
                 meta.dump(pw);
             }
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("Primary storage UUID: " + mPrimaryStorageUuid);
         }
 
         synchronized (mObbMounts) {
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index bd22524..f087c33 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -14307,12 +14307,22 @@
     public int movePrimaryStorage(String volumeUuid) throws RemoteException {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MOVE_PACKAGE, null);
 
-        final int moveId = mNextMoveId.getAndIncrement();
+        final int realMoveId = mNextMoveId.getAndIncrement();
+        final IPackageMoveObserver callback = new IPackageMoveObserver.Stub() {
+            @Override
+            public void onStarted(int moveId, String title) {
+                // Ignored
+            }
 
-        // TODO: ask mountservice to take down both, connect over to DCS to
-        // migrate, and then bring up new storage
+            @Override
+            public void onStatusChanged(int moveId, int status, long estMillis) {
+                mMoveCallbacks.notifyStatusChanged(realMoveId, status, estMillis);
+            }
+        };
 
-        return moveId;
+        final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        storage.setPrimaryStorageUuid(volumeUuid, callback);
+        return realMoveId;
     }
 
     @Override