Package and storage movement callbacks.

Since package and primary storage movement can take quite awhile,
we want to have SystemUI surface progress and allow the Settings
app to be torn down while the movement proceeds in the background.

Movement requests now return a unique ID that identifies an ongoing
operation, and interested parties can observe ongoing progress and
final status.  Internally, progress and status are overloaded so
the values 0-100 are progress, and any values outside that range
are terminal status.

Add explicit constants for special-cased volume UUIDs, and change
the APIs to accept VolumeInfo to reduce confusion.  Internally the
UUID value "null" means internal storage, and "primary_physical"
means the current primary physical volume.  These values are used
for both package and primary storage movement destinations.

Persist the current primary storage location in MountService
metadata, since it can be moved over time.

Surface disk scanned events with separate volume count so we can
determine when it's partitioned successfully.  Also send broadcast
to support TvSettings launching into adoption flow.

Bug: 19993667
Change-Id: Ic8a4034033c3cb3262023dba4a642efc6795af10
diff --git a/cmds/pm/src/com/android/commands/pm/Pm.java b/cmds/pm/src/com/android/commands/pm/Pm.java
index d5cc8cc..b84b1e2 100644
--- a/cmds/pm/src/com/android/commands/pm/Pm.java
+++ b/cmds/pm/src/com/android/commands/pm/Pm.java
@@ -51,9 +51,11 @@
 import android.os.IUserManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.Log;
 
 import libcore.io.IoUtils;
@@ -1283,20 +1285,6 @@
         }
     }
 
-    class LocalPackageMoveObserver extends IPackageMoveObserver.Stub {
-        boolean finished;
-        int returnCode;
-
-        @Override
-        public void packageMoved(String packageName, int returnCode) throws RemoteException {
-            synchronized (this) {
-                this.finished = true;
-                this.returnCode = returnCode;
-                notifyAll();
-            }
-        }
-    }
-
     public int runMove() {
         final String packageName = nextArg();
         String volumeUuid = nextArg();
@@ -1304,24 +1292,21 @@
             volumeUuid = null;
         }
 
-        final LocalPackageMoveObserver obs = new LocalPackageMoveObserver();
         try {
-            mPm.movePackageAndData(packageName, volumeUuid, obs);
+            final int moveId = mPm.movePackage(packageName, volumeUuid);
 
-            synchronized (obs) {
-                while (!obs.finished) {
-                    try {
-                        obs.wait();
-                    } catch (InterruptedException e) {
-                    }
-                }
-                if (obs.returnCode == PackageManager.MOVE_SUCCEEDED) {
-                    System.out.println("Success");
-                    return 0;
-                } else {
-                    System.err.println("Failure [" + obs.returnCode + "]");
-                    return 1;
-                }
+            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();
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index dfe7e18..10f5960 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -62,6 +62,9 @@
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemProperties;
@@ -81,7 +84,9 @@
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Objects;
 
 /*package*/
 final class ApplicationPackageManager extends PackageManager {
@@ -98,6 +103,9 @@
     @GuardedBy("mLock")
     private PackageInstaller mInstaller;
 
+    @GuardedBy("mDelegates")
+    private final ArrayList<MoveCallbackDelegate> mDelegates = new ArrayList<>();
+
     UserManager getUserManager() {
         synchronized (mLock) {
             if (mUserManager == null) {
@@ -1410,57 +1418,100 @@
     }
 
     @Override
-    public void movePackage(String packageName, IPackageMoveObserver observer, int flags) {
+    public String getInstallerPackageName(String packageName) {
         try {
-            mPM.movePackage(packageName, observer, flags);
+            return mPM.getInstallerPackageName(packageName);
+        } catch (RemoteException e) {
+            // Should never happen!
+        }
+        return null;
+    }
+
+    @Override
+    public int getMoveStatus(int moveId) {
+        try {
+            return mPM.getMoveStatus(moveId);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
     }
 
     @Override
-    public void movePackageAndData(String packageName, String volumeUuid,
-            IPackageMoveObserver observer) {
-        try {
-            mPM.movePackageAndData(packageName, volumeUuid, observer);
-        } catch (RemoteException e) {
-            throw e.rethrowAsRuntimeException();
+    public void registerMoveCallback(MoveCallback callback, Handler handler) {
+        synchronized (mDelegates) {
+            final MoveCallbackDelegate delegate = new MoveCallbackDelegate(callback,
+                    handler.getLooper());
+            try {
+                mPM.registerMoveCallback(delegate);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+            mDelegates.add(delegate);
         }
     }
 
     @Override
-    public @NonNull VolumeInfo getApplicationCurrentVolume(ApplicationInfo app) {
-        final StorageManager storage = mContext.getSystemService(StorageManager.class);
-        if (app.isInternal()) {
-            return Preconditions.checkNotNull(
-                    storage.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL));
-        } else if (app.isExternalAsec()) {
-            final List<VolumeInfo> vols = storage.getVolumes();
-            for (VolumeInfo vol : vols) {
-                if ((vol.getType() == VolumeInfo.TYPE_PUBLIC) && vol.isPrimary()) {
-                    return vol;
+    public void unregisterMoveCallback(MoveCallback callback) {
+        synchronized (mDelegates) {
+            for (Iterator<MoveCallbackDelegate> i = mDelegates.iterator(); i.hasNext();) {
+                final MoveCallbackDelegate delegate = i.next();
+                if (delegate.mCallback == callback) {
+                    try {
+                        mPM.unregisterMoveCallback(delegate);
+                    } catch (RemoteException e) {
+                        throw e.rethrowAsRuntimeException();
+                    }
+                    i.remove();
                 }
             }
-            throw new IllegalStateException("Failed to find primary public volume");
-        } else {
-            return Preconditions.checkNotNull(storage.findVolumeByUuid(app.volumeUuid));
         }
     }
 
     @Override
-    public @NonNull List<VolumeInfo> getApplicationCandidateVolumes(ApplicationInfo app) {
+    public int movePackage(String packageName, VolumeInfo vol) {
+        try {
+            final String volumeUuid;
+            if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) {
+                volumeUuid = StorageManager.UUID_PRIVATE_INTERNAL;
+            } else if (vol.isPrimaryPhysical()) {
+                volumeUuid = StorageManager.UUID_PRIMARY_PHYSICAL;
+            } else {
+                volumeUuid = Preconditions.checkNotNull(vol.fsUuid);
+            }
+
+            return mPM.movePackage(packageName, volumeUuid);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    @Override
+    public @Nullable VolumeInfo getPackageCurrentVolume(ApplicationInfo app) {
         final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        if (app.isInternal()) {
+            return storage.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL);
+        } else if (app.isExternalAsec()) {
+            return storage.getPrimaryPhysicalVolume();
+        } else {
+            return storage.findVolumeByUuid(app.volumeUuid);
+        }
+    }
+
+    @Override
+    public @NonNull List<VolumeInfo> getPackageCandidateVolumes(ApplicationInfo app) {
+        final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        final VolumeInfo currentVol = getPackageCurrentVolume(app);
         final List<VolumeInfo> vols = storage.getVolumes();
         final List<VolumeInfo> candidates = new ArrayList<>();
         for (VolumeInfo vol : vols) {
-            if (isCandidateVolume(app, vol)) {
+            if (Objects.equals(vol, currentVol) || isPackageCandidateVolume(app, vol)) {
                 candidates.add(vol);
             }
         }
         return candidates;
     }
 
-    private static boolean isCandidateVolume(ApplicationInfo app, VolumeInfo vol) {
+    private static boolean isPackageCandidateVolume(ApplicationInfo app, VolumeInfo vol) {
         // Private internal is always an option
         if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) {
             return true;
@@ -1473,10 +1524,14 @@
             return false;
         }
 
-        // Moving into an ASEC on public primary is only an option when app is
-        // internal, or already in ASEC
-        if ((vol.getType() == VolumeInfo.TYPE_PUBLIC) && vol.isPrimary()) {
-            return app.isInternal() || app.isExternalAsec();
+        // Gotta be able to write there
+        if (!vol.isMountedWritable()) {
+            return false;
+        }
+
+        // Moving into an ASEC on public primary is only option internal
+        if (vol.isPrimaryPhysical()) {
+            return app.isInternal();
         }
 
         // Otherwise we can move to any private volume
@@ -1484,13 +1539,66 @@
     }
 
     @Override
-    public String getInstallerPackageName(String packageName) {
+    public int movePrimaryStorage(VolumeInfo vol) {
         try {
-            return mPM.getInstallerPackageName(packageName);
+            final String volumeUuid;
+            if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.id)) {
+                volumeUuid = StorageManager.UUID_PRIVATE_INTERNAL;
+            } else if (vol.isPrimaryPhysical()) {
+                volumeUuid = StorageManager.UUID_PRIMARY_PHYSICAL;
+            } else {
+                volumeUuid = Preconditions.checkNotNull(vol.fsUuid);
+            }
+
+            return mPM.movePrimaryStorage(volumeUuid);
         } catch (RemoteException e) {
-            // Should never happen!
+            throw e.rethrowAsRuntimeException();
         }
-        return null;
+    }
+
+    public @Nullable VolumeInfo getPrimaryStorageCurrentVolume() {
+        final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        final String volumeUuid = storage.getPrimaryStorageUuid();
+        if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, volumeUuid)) {
+            return storage.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL);
+        } else if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, volumeUuid)) {
+            return storage.getPrimaryPhysicalVolume();
+        } else {
+            return storage.findVolumeByUuid(volumeUuid);
+        }
+    }
+
+    public @NonNull List<VolumeInfo> getPrimaryStorageCandidateVolumes() {
+        final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        final VolumeInfo currentVol = getPrimaryStorageCurrentVolume();
+        final List<VolumeInfo> vols = storage.getVolumes();
+        final List<VolumeInfo> candidates = new ArrayList<>();
+        for (VolumeInfo vol : vols) {
+            if (Objects.equals(vol, currentVol) || isPrimaryStorageCandidateVolume(vol)) {
+                candidates.add(vol);
+            }
+        }
+        return candidates;
+    }
+
+    private static boolean isPrimaryStorageCandidateVolume(VolumeInfo vol) {
+        // Private internal is always an option
+        if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) {
+            return true;
+        }
+
+        // Gotta be able to write there
+        if (!vol.isMountedWritable()) {
+            return false;
+        }
+
+        // We can move to public volumes on legacy devices
+        if ((vol.getType() == VolumeInfo.TYPE_PUBLIC) && vol.getDisk().isDefaultPrimary()) {
+            return true;
+        }
+
+        // Otherwise we can move to any private volume
+        return (vol.getType() == VolumeInfo.TYPE_PRIVATE);
     }
 
     @Override
@@ -1941,6 +2049,45 @@
         return null;
     }
 
+    /** {@hide} */
+    private static class MoveCallbackDelegate extends IPackageMoveObserver.Stub implements
+            Handler.Callback {
+        private static final int MSG_STARTED = 1;
+        private static final int MSG_STATUS_CHANGED = 2;
+
+        final MoveCallback mCallback;
+        final Handler mHandler;
+
+        public MoveCallbackDelegate(MoveCallback callback, Looper looper) {
+            mCallback = callback;
+            mHandler = new Handler(looper, this);
+        }
+
+        @Override
+        public boolean handleMessage(Message msg) {
+            final int moveId = msg.arg1;
+            switch (msg.what) {
+                case MSG_STARTED:
+                    mCallback.onStarted(moveId, (String) msg.obj);
+                    return true;
+                case MSG_STATUS_CHANGED:
+                    mCallback.onStatusChanged(moveId, msg.arg2, (long) msg.obj);
+                    return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void onStarted(int moveId, String title) {
+            mHandler.obtainMessage(MSG_STARTED, moveId, 0, title).sendToTarget();
+        }
+
+        @Override
+        public void onStatusChanged(int moveId, int status, long estMillis) {
+            mHandler.obtainMessage(MSG_STATUS_CHANGED, moveId, status, estMillis).sendToTarget();
+        }
+    }
+
     private final ContextImpl mContext;
     private final IPackageManager mPM;
 
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 447c668..ae59bfc 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -50,7 +50,6 @@
 import android.os.Bundle;
 import android.os.ParcelFileDescriptor;
 import android.content.IntentSender;
-import com.android.internal.os.IResultReceiver;
 
 /**
  *  See {@link PackageManager} for documentation on most of the APIs
@@ -431,8 +430,13 @@
 
     PackageCleanItem nextPackageToClean(in PackageCleanItem lastPackage);
 
-    void movePackage(String packageName, IPackageMoveObserver observer, int flags);
-    void movePackageAndData(String packageName, String volumeUuid, IPackageMoveObserver observer);
+    int getMoveStatus(int moveId);
+
+    void registerMoveCallback(in IPackageMoveObserver callback);
+    void unregisterMoveCallback(in IPackageMoveObserver callback);
+
+    int movePackage(in String packageName, in String volumeUuid);
+    int movePrimaryStorage(in String volumeUuid);
 
     boolean addPermissionAsync(in PermissionInfo info);
 
diff --git a/core/java/android/content/pm/IPackageMoveObserver.aidl b/core/java/android/content/pm/IPackageMoveObserver.aidl
index baa1595..50ab3b5 100644
--- a/core/java/android/content/pm/IPackageMoveObserver.aidl
+++ b/core/java/android/content/pm/IPackageMoveObserver.aidl
@@ -22,6 +22,6 @@
  * @hide
  */
 oneway interface IPackageMoveObserver {
-    void packageMoved(in String packageName, int returnCode);
+    void onStarted(int moveId, String title);
+    void onStatusChanged(int moveId, int status, long estMillis);
 }
-
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index e4108b1..e1c271d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -42,6 +42,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
+import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.storage.VolumeInfo;
@@ -875,7 +876,8 @@
      *
      * @hide
      */
-    public static final int MOVE_SUCCEEDED = 1;
+    public static final int MOVE_SUCCEEDED = -100;
+
     /**
      * Error code that is passed to the {@link IPackageMoveObserver} by
      * {@link #movePackage(android.net.Uri, IPackageMoveObserver)}
@@ -941,6 +943,7 @@
      * been installed on external media.
      * @hide
      */
+    @Deprecated
     public static final int MOVE_INTERNAL = 0x00000001;
 
     /**
@@ -948,8 +951,12 @@
      * the package should be moved to external media.
      * @hide
      */
+    @Deprecated
     public static final int MOVE_EXTERNAL_MEDIA = 0x00000002;
 
+    /** {@hide} */
+    public static final String EXTRA_MOVE_ID = "android.content.pm.extra.MOVE_ID";
+
     /**
      * Usable by the required verifier as the {@code verificationCode} argument
      * for {@link PackageManager#verifyPendingInstall} to indicate that it will
@@ -4183,17 +4190,42 @@
      * @hide
      */
     @Deprecated
-    public abstract void movePackage(String packageName, IPackageMoveObserver observer, int flags);
+    public void movePackage(String packageName, IPackageMoveObserver observer, int flags) {
+        throw new UnsupportedOperationException();
+    }
 
     /** {@hide} */
-    public abstract void movePackageAndData(String packageName, String volumeUuid,
-            IPackageMoveObserver observer);
+    public static boolean isMoveStatusFinished(int status) {
+        return (status < 0 || status > 100);
+    }
 
     /** {@hide} */
-    public abstract @Nullable VolumeInfo getApplicationCurrentVolume(ApplicationInfo app);
+    public static abstract class MoveCallback {
+        public abstract void onStarted(int moveId, String title);
+        public abstract void onStatusChanged(int moveId, int status, long estMillis);
+    }
 
     /** {@hide} */
-    public abstract @NonNull List<VolumeInfo> getApplicationCandidateVolumes(ApplicationInfo app);
+    public abstract int getMoveStatus(int moveId);
+
+    /** {@hide} */
+    public abstract void registerMoveCallback(MoveCallback callback, Handler handler);
+    /** {@hide} */
+    public abstract void unregisterMoveCallback(MoveCallback callback);
+
+    /** {@hide} */
+    public abstract int movePackage(String packageName, VolumeInfo vol);
+    /** {@hide} */
+    public abstract @Nullable VolumeInfo getPackageCurrentVolume(ApplicationInfo app);
+    /** {@hide} */
+    public abstract @NonNull List<VolumeInfo> getPackageCandidateVolumes(ApplicationInfo app);
+
+    /** {@hide} */
+    public abstract int movePrimaryStorage(VolumeInfo vol);
+    /** {@hide} */
+    public abstract @Nullable VolumeInfo getPrimaryStorageCurrentVolume();
+    /** {@hide} */
+    public abstract @NonNull List<VolumeInfo> getPrimaryStorageCandidateVolumes();
 
     /**
      * Returns the device identity that verifiers can use to associate their scheme to a particular
diff --git a/core/java/android/os/storage/DiskInfo.java b/core/java/android/os/storage/DiskInfo.java
index 64f2a05..9623695 100644
--- a/core/java/android/os/storage/DiskInfo.java
+++ b/core/java/android/os/storage/DiskInfo.java
@@ -36,7 +36,10 @@
  * @hide
  */
 public class DiskInfo implements Parcelable {
-    public static final String EXTRA_DISK_ID = "android.os.storage.extra.DISK_ID";
+    public static final String ACTION_DISK_SCANNED =
+            "android.os.storage.action.DISK_SCANNED";
+    public static final String EXTRA_DISK_ID =
+            "android.os.storage.extra.DISK_ID";
 
     public static final int FLAG_ADOPTABLE = 1 << 0;
     public static final int FLAG_DEFAULT_PRIMARY = 1 << 1;
@@ -96,6 +99,14 @@
         }
     }
 
+    public boolean isAdoptable() {
+        return (flags & FLAG_ADOPTABLE) != 0;
+    }
+
+    public boolean isDefaultPrimary() {
+        return (flags & FLAG_DEFAULT_PRIMARY) != 0;
+    }
+
     public boolean isSd() {
         return (flags & FLAG_SD) != 0;
     }
@@ -104,10 +115,6 @@
         return (flags & FLAG_USB) != 0;
     }
 
-    public boolean isAdoptable() {
-        return (flags & FLAG_ADOPTABLE) != 0;
-    }
-
     @Override
     public String toString() {
         final CharArrayWriter writer = new CharArrayWriter();
diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java
index 0a8187e..0b1031c 100644
--- a/core/java/android/os/storage/IMountService.java
+++ b/core/java/android/os/storage/IMountService.java
@@ -1063,6 +1063,38 @@
                     _data.recycle();
                 }
             }
+
+            @Override
+            public String getPrimaryStorageUuid() throws RemoteException {
+                Parcel _data = Parcel.obtain();
+                Parcel _reply = Parcel.obtain();
+                String _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    mRemote.transact(Stub.TRANSACTION_getPrimaryStorageUuid, _data, _reply, 0);
+                    _reply.readException();
+                    _result = _reply.readString();
+                } finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+
+            @Override
+            public void setPrimaryStorageUuid(String volumeUuid) throws RemoteException {
+                Parcel _data = Parcel.obtain();
+                Parcel _reply = Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeString(volumeUuid);
+                    mRemote.transact(Stub.TRANSACTION_setPrimaryStorageUuid, _data, _reply, 0);
+                    _reply.readException();
+                } finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
         }
 
         private static final String DESCRIPTOR = "IMountService";
@@ -1169,6 +1201,9 @@
         static final int TRANSACTION_setVolumeNickname = IBinder.FIRST_CALL_TRANSACTION + 52;
         static final int TRANSACTION_setVolumeUserFlags = IBinder.FIRST_CALL_TRANSACTION + 53;
 
+        static final int TRANSACTION_getPrimaryStorageUuid = IBinder.FIRST_CALL_TRANSACTION + 54;
+        static final int TRANSACTION_setPrimaryStorageUuid = IBinder.FIRST_CALL_TRANSACTION + 55;
+
         /**
          * Cast an IBinder object into an IMountService interface, generating a
          * proxy if needed.
@@ -1669,6 +1704,20 @@
                     reply.writeNoException();
                     return true;
                 }
+                case TRANSACTION_getPrimaryStorageUuid: {
+                    data.enforceInterface(DESCRIPTOR);
+                    String volumeUuid = getPrimaryStorageUuid();
+                    reply.writeNoException();
+                    reply.writeString(volumeUuid);
+                    return true;
+                }
+                case TRANSACTION_setPrimaryStorageUuid: {
+                    data.enforceInterface(DESCRIPTOR);
+                    String volumeUuid = data.readString();
+                    setPrimaryStorageUuid(volumeUuid);
+                    reply.writeNoException();
+                    return true;
+                }
             }
             return super.onTransact(code, data, reply, flags);
         }
@@ -1969,4 +2018,7 @@
 
     public void setVolumeNickname(String volId, String nickname) throws RemoteException;
     public void setVolumeUserFlags(String volId, int flags, int mask) throws RemoteException;
+
+    public String getPrimaryStorageUuid() throws RemoteException;
+    public void setPrimaryStorageUuid(String volumeUuid) throws RemoteException;
 }
diff --git a/core/java/android/os/storage/IMountServiceListener.java b/core/java/android/os/storage/IMountServiceListener.java
index 8e878a4..fcb4779 100644
--- a/core/java/android/os/storage/IMountServiceListener.java
+++ b/core/java/android/os/storage/IMountServiceListener.java
@@ -98,10 +98,11 @@
                     reply.writeNoException();
                     return true;
                 }
-                case TRANSACTION_onDiskUnsupported: {
+                case TRANSACTION_onDiskScanned: {
                     data.enforceInterface(DESCRIPTOR);
                     final DiskInfo disk = (DiskInfo) data.readParcelable(null);
-                    onDiskUnsupported(disk);
+                    final int volumeCount = data.readInt();
+                    onDiskScanned(disk, volumeCount);
                     reply.writeNoException();
                     return true;
                 }
@@ -207,13 +208,14 @@
             }
 
             @Override
-            public void onDiskUnsupported(DiskInfo disk) throws RemoteException {
+            public void onDiskScanned(DiskInfo disk, int volumeCount) throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
                 try {
                     _data.writeInterfaceToken(DESCRIPTOR);
                     _data.writeParcelable(disk, 0);
-                    mRemote.transact(Stub.TRANSACTION_onDiskUnsupported, _data, _reply,
+                    _data.writeInt(volumeCount);
+                    mRemote.transact(Stub.TRANSACTION_onDiskScanned, _data, _reply,
                             android.os.IBinder.FLAG_ONEWAY);
                     _reply.readException();
                 } finally {
@@ -224,12 +226,10 @@
         }
 
         static final int TRANSACTION_onUsbMassStorageConnectionChanged = (IBinder.FIRST_CALL_TRANSACTION + 0);
-
         static final int TRANSACTION_onStorageStateChanged = (IBinder.FIRST_CALL_TRANSACTION + 1);
-
         static final int TRANSACTION_onVolumeStateChanged = (IBinder.FIRST_CALL_TRANSACTION + 2);
         static final int TRANSACTION_onVolumeMetadataChanged = (IBinder.FIRST_CALL_TRANSACTION + 3);
-        static final int TRANSACTION_onDiskUnsupported = (IBinder.FIRST_CALL_TRANSACTION + 4);
+        static final int TRANSACTION_onDiskScanned = (IBinder.FIRST_CALL_TRANSACTION + 4);
     }
 
     /**
@@ -255,5 +255,5 @@
 
     public void onVolumeMetadataChanged(VolumeInfo vol) throws RemoteException;
 
-    public void onDiskUnsupported(DiskInfo disk) throws RemoteException;
+    public void onDiskScanned(DiskInfo disk, int volumeCount) throws RemoteException;
 }
diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java
index ad2fae0..6a0140e 100644
--- a/core/java/android/os/storage/StorageEventListener.java
+++ b/core/java/android/os/storage/StorageEventListener.java
@@ -44,6 +44,6 @@
     public void onVolumeMetadataChanged(VolumeInfo vol) {
     }
 
-    public void onDiskUnsupported(DiskInfo disk) {
+    public void onDiskScanned(DiskInfo disk, int volumeCount) {
     }
 }
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index f101352..747fb40 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -73,6 +73,11 @@
     public static final String PROP_FORCE_ADOPTABLE = "persist.fw.force_adoptable";
 
     /** {@hide} */
+    public static final String UUID_PRIVATE_INTERNAL = null;
+    /** {@hide} */
+    public static final String UUID_PRIMARY_PHYSICAL = "primary_physical";
+
+    /** {@hide} */
     public static final int FLAG_ALL_METADATA = 1 << 0;
 
     private final Context mContext;
@@ -89,7 +94,7 @@
         private static final int MSG_STORAGE_STATE_CHANGED = 1;
         private static final int MSG_VOLUME_STATE_CHANGED = 2;
         private static final int MSG_VOLUME_METADATA_CHANGED = 3;
-        private static final int MSG_DISK_UNSUPPORTED = 4;
+        private static final int MSG_DISK_SCANNED = 4;
 
         final StorageEventListener mCallback;
         final Handler mHandler;
@@ -116,8 +121,8 @@
                     mCallback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
                     args.recycle();
                     return true;
-                case MSG_DISK_UNSUPPORTED:
-                    mCallback.onDiskUnsupported((DiskInfo) args.arg1);
+                case MSG_DISK_SCANNED:
+                    mCallback.onDiskScanned((DiskInfo) args.arg1, args.argi2);
                     args.recycle();
                     return true;
             }
@@ -156,10 +161,11 @@
         }
 
         @Override
-        public void onDiskUnsupported(DiskInfo disk) {
+        public void onDiskScanned(DiskInfo disk, int volumeCount) {
             final SomeArgs args = SomeArgs.obtain();
             args.arg1 = disk;
-            mHandler.obtainMessage(MSG_DISK_UNSUPPORTED, args).sendToTarget();
+            args.argi2 = volumeCount;
+            mHandler.obtainMessage(MSG_DISK_SCANNED, args).sendToTarget();
         }
     }
 
@@ -534,17 +540,26 @@
     /** {@hide} */
     public @Nullable String getBestVolumeDescription(VolumeInfo vol) {
         String descrip = vol.getDescription();
-
         if (vol.disk != null) {
             if (TextUtils.isEmpty(descrip)) {
                 descrip = vol.disk.getDescription();
             }
         }
-
         return descrip;
     }
 
     /** {@hide} */
+    public @Nullable VolumeInfo getPrimaryPhysicalVolume() {
+        final List<VolumeInfo> vols = getVolumes();
+        for (VolumeInfo vol : vols) {
+            if (vol.isPrimaryPhysical()) {
+                return vol;
+            }
+        }
+        return null;
+    }
+
+    /** {@hide} */
     public void mount(String volId) {
         try {
             mMountService.mount(volId);
@@ -628,6 +643,24 @@
     }
 
     /** {@hide} */
+    public String getPrimaryStorageUuid() {
+        try {
+            return mMountService.getPrimaryStorageUuid();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /** {@hide} */
+    public void setPrimaryStorageUuid(String volumeUuid) {
+        try {
+            mMountService.setPrimaryStorageUuid(volumeUuid);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /** {@hide} */
     public @Nullable StorageVolume getStorageVolume(File file) {
         return getStorageVolume(getVolumeList(), file);
     }
diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java
index f3498d5..4e9cfc7 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -242,6 +242,10 @@
         return (mountFlags & MOUNT_FLAG_PRIMARY) != 0;
     }
 
+    public boolean isPrimaryPhysical() {
+        return isPrimary() && (getType() == TYPE_PUBLIC);
+    }
+
     public boolean isVisible() {
         return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
     }
diff --git a/core/java/com/android/internal/util/Preconditions.java b/core/java/com/android/internal/util/Preconditions.java
index 414b7bc..b692a18 100644
--- a/core/java/com/android/internal/util/Preconditions.java
+++ b/core/java/com/android/internal/util/Preconditions.java
@@ -24,6 +24,12 @@
  */
 public class Preconditions {
 
+    public static void checkArgument(boolean expression) {
+        if (!expression) {
+            throw new IllegalArgumentException();
+        }
+    }
+
     /**
      * Ensures that an object reference passed as a parameter to the calling
      * method is not null.
diff --git a/core/tests/coretests/src/android/content/pm/PackageManagerTests.java b/core/tests/coretests/src/android/content/pm/PackageManagerTests.java
index 279bfbf..baa772e 100644
--- a/core/tests/coretests/src/android/content/pm/PackageManagerTests.java
+++ b/core/tests/coretests/src/android/content/pm/PackageManagerTests.java
@@ -1587,91 +1587,13 @@
         }
     }
 
-    private class PackageMoveObserver extends IPackageMoveObserver.Stub {
-        public int returnCode;
-
-        private boolean doneFlag = false;
-
-        public String packageName;
-
-        public PackageMoveObserver(String pkgName) {
-            packageName = pkgName;
-        }
-
-        public void packageMoved(String packageName, int returnCode) {
-            Log.i("DEBUG_MOVE::", "pkg = " + packageName + ", " + "ret = " + returnCode);
-            if (!packageName.equals(this.packageName)) {
-                return;
-            }
-            synchronized (this) {
-                this.returnCode = returnCode;
-                doneFlag = true;
-                notifyAll();
-            }
-        }
-
-        public boolean isDone() {
-            return doneFlag;
-        }
-    }
-
     public boolean invokeMovePackage(String pkgName, int flags, GenericReceiver receiver)
             throws Exception {
-        PackageMoveObserver observer = new PackageMoveObserver(pkgName);
-        final boolean received = false;
-        mContext.registerReceiver(receiver, receiver.filter);
-        try {
-            // Wait on observer
-            synchronized (observer) {
-                synchronized (receiver) {
-                    getPm().movePackage(pkgName, observer, flags);
-                    long waitTime = 0;
-                    while ((!observer.isDone()) && (waitTime < MAX_WAIT_TIME)) {
-                        observer.wait(WAIT_TIME_INCR);
-                        waitTime += WAIT_TIME_INCR;
-                    }
-                    if (!observer.isDone()) {
-                        throw new Exception("Timed out waiting for pkgmove callback");
-                    }
-                    if (observer.returnCode != PackageManager.MOVE_SUCCEEDED) {
-                        return false;
-                    }
-                    // Verify we received the broadcast
-                    waitTime = 0;
-                    while ((!receiver.isDone()) && (waitTime < MAX_WAIT_TIME)) {
-                        receiver.wait(WAIT_TIME_INCR);
-                        waitTime += WAIT_TIME_INCR;
-                    }
-                    if (!receiver.isDone()) {
-                        throw new Exception("Timed out waiting for MOVE notifications");
-                    }
-                    return receiver.received;
-                }
-            }
-        } finally {
-            mContext.unregisterReceiver(receiver);
-        }
+        throw new UnsupportedOperationException();
     }
 
     private boolean invokeMovePackageFail(String pkgName, int flags, int errCode) throws Exception {
-        PackageMoveObserver observer = new PackageMoveObserver(pkgName);
-        try {
-            // Wait on observer
-            synchronized (observer) {
-                getPm().movePackage(pkgName, observer, flags);
-                long waitTime = 0;
-                while ((!observer.isDone()) && (waitTime < MAX_WAIT_TIME)) {
-                    observer.wait(WAIT_TIME_INCR);
-                    waitTime += WAIT_TIME_INCR;
-                }
-                if (!observer.isDone()) {
-                    throw new Exception("Timed out waiting for pkgmove callback");
-                }
-                assertEquals(errCode, observer.returnCode);
-            }
-        } finally {
-        }
-        return true;
+        throw new UnsupportedOperationException();
     }
 
     private int getDefaultInstallLoc() {
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index f88802a..89a7173 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -232,7 +232,12 @@
         public static final int FstrimCompleted                = 700;
     }
 
+    private static final int VERSION_INIT = 1;
+    private static final int VERSION_ADD_PRIMARY = 2;
+
     private static final String TAG_VOLUMES = "volumes";
+    private static final String ATTR_VERSION = "version";
+    private static final String ATTR_PRIMARY_STORAGE_UUID = "primaryStorageUuid";
     private static final String TAG_VOLUME = "volume";
     private static final String ATTR_TYPE = "type";
     private static final String ATTR_FS_UUID = "fsUuid";
@@ -302,6 +307,8 @@
     /** Map from UUID to metadata */
     @GuardedBy("mLock")
     private ArrayMap<String, VolumeMetadata> mMetadata = new ArrayMap<>();
+    @GuardedBy("mLock")
+    private String mPrimaryStorageUuid;
 
     /** Map from disk ID to latches */
     @GuardedBy("mLock")
@@ -943,22 +950,25 @@
     }
 
     private void onDiskScannedLocked(DiskInfo disk) {
+        final Intent intent = new Intent(DiskInfo.ACTION_DISK_SCANNED);
+        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+        mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
+                android.Manifest.permission.WRITE_MEDIA_STORAGE);
+
         final CountDownLatch latch = mDiskScanLatches.remove(disk.id);
         if (latch != null) {
             latch.countDown();
         }
 
-        boolean empty = true;
+        int volumeCount = 0;
         for (int i = 0; i < mVolumes.size(); i++) {
             final VolumeInfo vol = mVolumes.valueAt(i);
             if (Objects.equals(disk.id, vol.getDiskId())) {
-                empty = false;
+                volumeCount++;
             }
         }
 
-        if (empty) {
-            mCallbacks.notifyDiskUnsupported(disk);
-        }
+        mCallbacks.notifyDiskScanned(disk, volumeCount);
     }
 
     private void onVolumeCreatedLocked(VolumeInfo vol) {
@@ -1022,8 +1032,8 @@
         if (isBroadcastWorthy(vol)) {
             final Intent intent = new Intent(VolumeInfo.ACTION_VOLUME_STATE_CHANGED);
             intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
-            // TODO: require receiver to hold permission
-            mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+            mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
+                    android.Manifest.permission.WRITE_MEDIA_STORAGE);
         }
 
         final String oldStateEnv = VolumeInfo.getEnvironmentForState(oldState);
@@ -1166,7 +1176,21 @@
             while ((type = in.next()) != END_DOCUMENT) {
                 if (type == START_TAG) {
                     final String tag = in.getName();
-                    if (TAG_VOLUME.equals(tag)) {
+                    if (TAG_VOLUMES.equals(tag)) {
+                        final int version = readIntAttribute(in, ATTR_VERSION, VERSION_INIT);
+                        if (version >= VERSION_ADD_PRIMARY) {
+                            mPrimaryStorageUuid = readStringAttribute(in,
+                                    ATTR_PRIMARY_STORAGE_UUID);
+                        } else {
+                            if (SystemProperties.getBoolean(StorageManager.PROP_PRIMARY_PHYSICAL,
+                                    false)) {
+                                mPrimaryStorageUuid = StorageManager.UUID_PRIMARY_PHYSICAL;
+                            } else {
+                                mPrimaryStorageUuid = StorageManager.UUID_PRIVATE_INTERNAL;
+                            }
+                        }
+
+                    } else if (TAG_VOLUME.equals(tag)) {
                         final VolumeMetadata meta = VolumeMetadata.read(in);
                         mMetadata.put(meta.fsUuid, meta);
                     }
@@ -1192,6 +1216,8 @@
             out.setOutput(fos, "utf-8");
             out.startDocument(null, true);
             out.startTag(null, TAG_VOLUMES);
+            writeIntAttribute(out, ATTR_VERSION, VERSION_ADD_PRIMARY);
+            writeStringAttribute(out, ATTR_PRIMARY_STORAGE_UUID, mPrimaryStorageUuid);
             final int size = mMetadata.size();
             for (int i = 0; i < size; i++) {
                 final VolumeMetadata meta = mMetadata.valueAt(i);
@@ -1398,6 +1424,24 @@
     }
 
     @Override
+    public String getPrimaryStorageUuid() throws RemoteException {
+        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();
+
+            // TODO: reevaluate all volumes we know about!
+        }
+    }
+
+    @Override
     public int[] getStorageUsers(String path) {
         enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
         waitForReady();
@@ -2700,7 +2744,7 @@
         private static final int MSG_STORAGE_STATE_CHANGED = 1;
         private static final int MSG_VOLUME_STATE_CHANGED = 2;
         private static final int MSG_VOLUME_METADATA_CHANGED = 3;
-        private static final int MSG_DISK_UNSUPPORTED = 4;
+        private static final int MSG_DISK_SCANNED = 4;
 
         private final RemoteCallbackList<IMountServiceListener>
                 mCallbacks = new RemoteCallbackList<>();
@@ -2748,8 +2792,8 @@
                     callback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
                     break;
                 }
-                case MSG_DISK_UNSUPPORTED: {
-                    callback.onDiskUnsupported((DiskInfo) args.arg1);
+                case MSG_DISK_SCANNED: {
+                    callback.onDiskScanned((DiskInfo) args.arg1, args.argi2);
                     break;
                 }
             }
@@ -2777,10 +2821,11 @@
             obtainMessage(MSG_VOLUME_METADATA_CHANGED, args).sendToTarget();
         }
 
-        private void notifyDiskUnsupported(DiskInfo disk) {
+        private void notifyDiskScanned(DiskInfo disk, int volumeCount) {
             final SomeArgs args = SomeArgs.obtain();
             args.arg1 = disk;
-            obtainMessage(MSG_DISK_UNSUPPORTED, args).sendToTarget();
+            args.argi2 = volumeCount;
+            obtainMessage(MSG_DISK_SCANNED, args).sendToTarget();
         }
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 89fa320..a406175 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -717,7 +717,7 @@
         } else {
             final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
             if (vol != null && vol.type == VolumeInfo.TYPE_PRIVATE
-                    && vol.state == VolumeInfo.STATE_MOUNTED) {
+                    && vol.isMountedWritable()) {
                 return new File(vol.path, "app");
             } else {
                 throw new FileNotFoundException("Failed to find volume for UUID " + volumeUuid);
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 1c339f5..bd22524 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -49,12 +49,10 @@
 import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK;
 import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER;
 import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED;
-import static android.content.pm.PackageManager.MOVE_EXTERNAL_MEDIA;
 import static android.content.pm.PackageManager.MOVE_FAILED_DOESNT_EXIST;
 import static android.content.pm.PackageManager.MOVE_FAILED_INTERNAL_ERROR;
 import static android.content.pm.PackageManager.MOVE_FAILED_OPERATION_PENDING;
 import static android.content.pm.PackageManager.MOVE_FAILED_SYSTEM_PACKAGE;
-import static android.content.pm.PackageManager.MOVE_INTERNAL;
 import static android.content.pm.PackageParser.isApkFile;
 import static android.os.Process.PACKAGE_INFO_GID;
 import static android.os.Process.SYSTEM_UID;
@@ -144,6 +142,7 @@
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Process;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SELinux;
 import android.os.ServiceManager;
@@ -174,6 +173,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
 import android.util.Xml;
 import android.view.Display;
 
@@ -189,11 +189,14 @@
 import com.android.internal.content.NativeLibraryHelper;
 import com.android.internal.content.PackageHelper;
 import com.android.internal.os.IParcelFileDescriptorFactory;
+import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastPrintWriter;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
 import com.android.server.EventLogTags;
+import com.android.server.FgThread;
 import com.android.server.IntentResolver;
 import com.android.server.LocalServices;
 import com.android.server.ServiceThread;
@@ -237,6 +240,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 
 /**
@@ -504,6 +508,10 @@
     final PackageInstallerService mInstallerService;
 
     private final PackageDexOptimizer mPackageDexOptimizer;
+
+    private AtomicInteger mNextMoveId = new AtomicInteger();
+    private final MoveCallbacks mMoveCallbacks;
+
     // Cache of users who need badging.
     SparseBooleanArray mUserNeedsBadging = new SparseBooleanArray();
 
@@ -1698,6 +1706,7 @@
 
         mInstaller = installer;
         mPackageDexOptimizer = new PackageDexOptimizer(this);
+        mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
 
         getDefaultDisplayMetrics(context, mMetrics);
 
@@ -14137,49 +14146,25 @@
     }
 
     @Override
-    public void movePackage(final String packageName, final IPackageMoveObserver observer,
-            final int flags) {
+    public int movePackage(final String packageName, final String volumeUuid) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MOVE_PACKAGE, null);
 
-        final int installFlags;
-        if ((flags & MOVE_INTERNAL) != 0) {
-            installFlags = INSTALL_INTERNAL;
-        } else if ((flags & MOVE_EXTERNAL_MEDIA) != 0) {
-            installFlags = INSTALL_EXTERNAL;
-        } else {
-            throw new IllegalArgumentException("Unsupported move flags " + flags);
-        }
-
+        final int moveId = mNextMoveId.getAndIncrement();
         try {
-            movePackageInternal(packageName, null, installFlags, false, observer);
+            movePackageInternal(packageName, volumeUuid, moveId);
         } catch (PackageManagerException e) {
             Slog.d(TAG, "Failed to move " + packageName, e);
-            try {
-                observer.packageMoved(packageName, e.error);
-            } catch (RemoteException ignored) {
-            }
+            mMoveCallbacks.notifyStatusChanged(moveId, PackageManager.MOVE_FAILED_INTERNAL_ERROR);
         }
+        return moveId;
     }
 
-    @Override
-    public void movePackageAndData(final String packageName, final String volumeUuid,
-            final IPackageMoveObserver observer) {
-        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MOVE_PACKAGE, null);
-        try {
-            movePackageInternal(packageName, volumeUuid, INSTALL_INTERNAL, true, observer);
-        } catch (PackageManagerException e) {
-            Slog.d(TAG, "Failed to move " + packageName, e);
-            try {
-                observer.packageMoved(packageName, e.error);
-            } catch (RemoteException ignored) {
-            }
-        }
-    }
-
-    private void movePackageInternal(final String packageName, String volumeUuid, int installFlags,
-            boolean andData, final IPackageMoveObserver observer) throws PackageManagerException {
+    private void movePackageInternal(final String packageName, final String volumeUuid,
+            final int moveId) throws PackageManagerException {
         final UserHandle user = new UserHandle(UserHandle.getCallingUserId());
+        final PackageManager pm = mContext.getPackageManager();
 
+        final boolean currentAsec;
         final String currentVolumeUuid;
         final File codeFile;
         final String installerPackageName;
@@ -14205,8 +14190,13 @@
 
             // TODO: yell if already in desired location
 
+            mMoveCallbacks.notifyStarted(moveId,
+                    String.valueOf(pm.getApplicationLabel(pkg.applicationInfo)));
+
             pkg.mOperationPending = true;
 
+            currentAsec = pkg.applicationInfo.isForwardLocked()
+                    || pkg.applicationInfo.isExternalAsec();
             currentVolumeUuid = ps.volumeUuid;
             codeFile = new File(pkg.codePath);
             installerPackageName = ps.installerPackageName;
@@ -14215,10 +14205,36 @@
             seinfo = pkg.applicationInfo.seinfo;
         }
 
-        if (andData) {
-            Slog.d(TAG, "Moving " + packageName + " private data from " + currentVolumeUuid + " to "
-                    + volumeUuid);
+        int installFlags;
+        final boolean moveData;
+
+        if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, volumeUuid)) {
+            installFlags = INSTALL_INTERNAL;
+            moveData = !currentAsec;
+        } else if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, volumeUuid)) {
+            installFlags = INSTALL_EXTERNAL;
+            moveData = false;
+        } else {
+            final StorageManager storage = mContext.getSystemService(StorageManager.class);
+            final VolumeInfo volume = storage.findVolumeByUuid(volumeUuid);
+            if (volume == null || volume.getType() != VolumeInfo.TYPE_PRIVATE
+                    || !volume.isMountedWritable()) {
+                throw new PackageManagerException(MOVE_FAILED_INTERNAL_ERROR,
+                        "Move location not mounted private volume");
+            }
+
+            Preconditions.checkState(!currentAsec);
+
+            installFlags = INSTALL_INTERNAL;
+            moveData = true;
+        }
+
+        Slog.d(TAG, "Moving " + packageName + " from " + currentVolumeUuid + " to " + volumeUuid);
+        mMoveCallbacks.notifyStatusChanged(moveId, 10, -1);
+
+        if (moveData) {
             synchronized (mInstallLock) {
+                // TODO: split this into separate copy and delete operations
                 if (mInstaller.moveUserDataDirs(currentVolumeUuid, volumeUuid, packageName, appId,
                         seinfo) != 0) {
                     synchronized (mPackages) {
@@ -14234,6 +14250,8 @@
             }
         }
 
+        mMoveCallbacks.notifyStatusChanged(moveId, 50);
+
         final IPackageInstallObserver2 installObserver = new IPackageInstallObserver2.Stub() {
             @Override
             public void onUserActionRequired(Intent intent) throws RemoteException {
@@ -14259,13 +14277,16 @@
                 final int status = PackageManager.installStatusToPublicStatus(returnCode);
                 switch (status) {
                     case PackageInstaller.STATUS_SUCCESS:
-                        observer.packageMoved(packageName, PackageManager.MOVE_SUCCEEDED);
+                        mMoveCallbacks.notifyStatusChanged(moveId,
+                                PackageManager.MOVE_SUCCEEDED);
                         break;
                     case PackageInstaller.STATUS_FAILURE_STORAGE:
-                        observer.packageMoved(packageName, PackageManager.MOVE_FAILED_INSUFFICIENT_STORAGE);
+                        mMoveCallbacks.notifyStatusChanged(moveId,
+                                PackageManager.MOVE_FAILED_INSUFFICIENT_STORAGE);
                         break;
                     default:
-                        observer.packageMoved(packageName, PackageManager.MOVE_FAILED_INTERNAL_ERROR);
+                        mMoveCallbacks.notifyStatusChanged(moveId,
+                                PackageManager.MOVE_FAILED_INTERNAL_ERROR);
                         break;
                 }
             }
@@ -14283,6 +14304,39 @@
     }
 
     @Override
+    public int movePrimaryStorage(String volumeUuid) throws RemoteException {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MOVE_PACKAGE, null);
+
+        final int moveId = mNextMoveId.getAndIncrement();
+
+        // TODO: ask mountservice to take down both, connect over to DCS to
+        // migrate, and then bring up new storage
+
+        return moveId;
+    }
+
+    @Override
+    public int getMoveStatus(int moveId) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null);
+        return mMoveCallbacks.mLastStatus.get(moveId);
+    }
+
+    @Override
+    public void registerMoveCallback(IPackageMoveObserver callback) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null);
+        mMoveCallbacks.register(callback);
+    }
+
+    @Override
+    public void unregisterMoveCallback(IPackageMoveObserver callback) {
+        mContext.enforceCallingOrSelfPermission(
+                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null);
+        mMoveCallbacks.unregister(callback);
+    }
+
+    @Override
     public boolean setInstallLocation(int loc) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS,
                 null);
@@ -14605,4 +14659,82 @@
             }
         }
     }
+
+    private static class MoveCallbacks extends Handler {
+        private static final int MSG_STARTED = 1;
+        private static final int MSG_STATUS_CHANGED = 2;
+
+        private final RemoteCallbackList<IPackageMoveObserver>
+                mCallbacks = new RemoteCallbackList<>();
+
+        private final SparseIntArray mLastStatus = new SparseIntArray();
+
+        public MoveCallbacks(Looper looper) {
+            super(looper);
+        }
+
+        public void register(IPackageMoveObserver callback) {
+            mCallbacks.register(callback);
+        }
+
+        public void unregister(IPackageMoveObserver callback) {
+            mCallbacks.unregister(callback);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            final SomeArgs args = (SomeArgs) msg.obj;
+            final int n = mCallbacks.beginBroadcast();
+            for (int i = 0; i < n; i++) {
+                final IPackageMoveObserver callback = mCallbacks.getBroadcastItem(i);
+                try {
+                    invokeCallback(callback, msg.what, args);
+                } catch (RemoteException ignored) {
+                }
+            }
+            mCallbacks.finishBroadcast();
+            args.recycle();
+        }
+
+        private void invokeCallback(IPackageMoveObserver callback, int what, SomeArgs args)
+                throws RemoteException {
+            switch (what) {
+                case MSG_STARTED: {
+                    callback.onStarted(args.argi1, (String) args.arg2);
+                    break;
+                }
+                case MSG_STATUS_CHANGED: {
+                    callback.onStatusChanged(args.argi1, args.argi2, (long) args.arg3);
+                    break;
+                }
+            }
+        }
+
+        private void notifyStarted(int moveId, String title) {
+            Slog.v(TAG, "Move " + moveId + " started with title " + title);
+
+            final SomeArgs args = SomeArgs.obtain();
+            args.argi1 = moveId;
+            args.arg2 = title;
+            obtainMessage(MSG_STARTED, args).sendToTarget();
+        }
+
+        private void notifyStatusChanged(int moveId, int status) {
+            notifyStatusChanged(moveId, status, -1);
+        }
+
+        private void notifyStatusChanged(int moveId, int status, long estMillis) {
+            Slog.v(TAG, "Move " + moveId + " status " + status);
+
+            final SomeArgs args = SomeArgs.obtain();
+            args.argi1 = moveId;
+            args.argi2 = status;
+            args.arg3 = estMillis;
+            obtainMessage(MSG_STATUS_CHANGED, args).sendToTarget();
+
+            synchronized (mLastStatus) {
+                mLastStatus.put(moveId, status);
+            }
+        }
+    }
 }
diff --git a/test-runner/src/android/test/mock/MockPackageManager.java b/test-runner/src/android/test/mock/MockPackageManager.java
index 276b713..9efea0d 100644
--- a/test-runner/src/android/test/mock/MockPackageManager.java
+++ b/test-runner/src/android/test/mock/MockPackageManager.java
@@ -16,7 +16,6 @@
 
 package android.test.mock;
 
-import android.annotation.NonNull;
 import android.app.PackageInstallObserver;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -29,7 +28,6 @@
 import android.content.pm.IPackageDataObserver;
 import android.content.pm.IPackageDeleteObserver;
 import android.content.pm.IPackageInstallObserver;
-import android.content.pm.IPackageMoveObserver;
 import android.content.pm.IPackageStatsObserver;
 import android.content.pm.InstrumentationInfo;
 import android.content.pm.IntentFilterVerificationInfo;
@@ -46,11 +44,14 @@
 import android.content.pm.ServiceInfo;
 import android.content.pm.VerificationParams;
 import android.content.pm.VerifierDeviceIdentity;
+import android.content.pm.PackageManager.MoveCallback;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Handler;
+import android.os.ResultReceiver;
 import android.os.UserHandle;
 import android.os.storage.VolumeInfo;
 
@@ -487,36 +488,65 @@
         throw new UnsupportedOperationException();
     }
 
-    /** {@hide} */
-    @Override
-    public void movePackage(String packageName, IPackageMoveObserver observer, int flags) {
-        throw new UnsupportedOperationException();
-    }
-
-    /** {@hide} */
-    @Override
-    public void movePackageAndData(String packageName, String volumeUuid,
-            IPackageMoveObserver observer) {
-        throw new UnsupportedOperationException();
-    }
-
-    /** {@hide} */
-    @Override
-    public @NonNull VolumeInfo getApplicationCurrentVolume(ApplicationInfo app) {
-        throw new UnsupportedOperationException();
-    }
-
-    /** {@hide} */
-    @Override
-    public @NonNull List<VolumeInfo> getApplicationCandidateVolumes(ApplicationInfo app) {
-        throw new UnsupportedOperationException();
-    }
-
     @Override
     public String getInstallerPackageName(String packageName) {
         throw new UnsupportedOperationException();
     }
 
+    /** {@hide} */
+    @Override
+    public int getMoveStatus(int moveId) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public void registerMoveCallback(MoveCallback callback, Handler handler) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public void unregisterMoveCallback(MoveCallback callback) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public int movePackage(String packageName, VolumeInfo vol) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public VolumeInfo getPackageCurrentVolume(ApplicationInfo app) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public List<VolumeInfo> getPackageCandidateVolumes(ApplicationInfo app) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public int movePrimaryStorage(VolumeInfo vol) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public VolumeInfo getPrimaryStorageCurrentVolume() {
+        throw new UnsupportedOperationException();
+    }
+
+    /** {@hide} */
+    @Override
+    public List<VolumeInfo> getPrimaryStorageCandidateVolumes() {
+        throw new UnsupportedOperationException();
+    }
+
     /**
      * @hide - to match hiding in superclass
      */