Split some VolumeInfo state into VolumeRecord.

VolumeRecord is a historical record of a volume that we've seen in
the past.  It's now surfaced outside the framework for SystemUI to
drive the notifications that bug users to reinsert missing private
volumes.

Show progress notifications for both storage and package movement
operations.  Notify when an empty disk is inserted (no usable volumes)
which launches into the normal format flow.

Add API to forget volumes.

Bug: 20275424, 20275424
Change-Id: I75602c17fdcd4d1f1f62324e1a08c4a33093eefa
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 16a2430..6e511f3 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -79,6 +79,7 @@
 import dalvik.system.VMRuntime;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.SomeArgs;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.UserIcons;
 
@@ -2054,8 +2055,7 @@
     /** {@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;
+        private static final int MSG_STATUS_CHANGED = 1;
 
         final MoveCallback mCallback;
         final Handler mHandler;
@@ -2067,26 +2067,25 @@
 
         @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);
+                    final SomeArgs args = (SomeArgs) msg.obj;
+                    mCallback.onStatusChanged(args.argi1, (String) args.arg2, args.argi3,
+                            (long) args.arg4);
+                    args.recycle();
                     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();
+        public void onStatusChanged(int moveId, String moveTitle, int status, long estMillis) {
+            final SomeArgs args = SomeArgs.obtain();
+            args.argi1 = moveId;
+            args.arg2 = moveTitle;
+            args.argi3 = status;
+            args.arg4 = estMillis;
+            mHandler.obtainMessage(MSG_STATUS_CHANGED, args).sendToTarget();
         }
     }
 
diff --git a/core/java/android/content/pm/IPackageMoveObserver.aidl b/core/java/android/content/pm/IPackageMoveObserver.aidl
index 50ab3b5..155ed0b 100644
--- a/core/java/android/content/pm/IPackageMoveObserver.aidl
+++ b/core/java/android/content/pm/IPackageMoveObserver.aidl
@@ -22,6 +22,5 @@
  * @hide
  */
 oneway interface IPackageMoveObserver {
-    void onStarted(int moveId, String title);
-    void onStatusChanged(int moveId, int status, long estMillis);
+    void onStatusChanged(int moveId, String moveTitle, int status, long estMillis);
 }
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index f01ca09..a1ee7fc 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4212,8 +4212,8 @@
 
     /** {@hide} */
     public static abstract class MoveCallback {
-        public abstract void onStarted(int moveId, String title);
-        public abstract void onStatusChanged(int moveId, int status, long estMillis);
+        public abstract void onStatusChanged(int moveId, String moveTitle, int status,
+                long estMillis);
     }
 
     /** {@hide} */
diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java
index 16e0bf7..fcde3f4 100644
--- a/core/java/android/os/storage/IMountService.java
+++ b/core/java/android/os/storage/IMountService.java
@@ -942,6 +942,24 @@
             }
 
             @Override
+            public VolumeRecord[] getVolumeRecords(int _flags) throws RemoteException {
+                Parcel _data = Parcel.obtain();
+                Parcel _reply = Parcel.obtain();
+                VolumeRecord[] _result;
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeInt(_flags);
+                    mRemote.transact(Stub.TRANSACTION_getVolumeRecords, _data, _reply, 0);
+                    _reply.readException();
+                    _result = _reply.createTypedArray(VolumeRecord.CREATOR);
+                } finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+                return _result;
+            }
+
+            @Override
             public void mount(String volId) throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
@@ -1033,12 +1051,12 @@
             }
 
             @Override
-            public void setVolumeNickname(String volId, String nickname) throws RemoteException {
+            public void setVolumeNickname(String fsUuid, String nickname) throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
                 try {
                     _data.writeInterfaceToken(DESCRIPTOR);
-                    _data.writeString(volId);
+                    _data.writeString(fsUuid);
                     _data.writeString(nickname);
                     mRemote.transact(Stub.TRANSACTION_setVolumeNickname, _data, _reply, 0);
                     _reply.readException();
@@ -1049,12 +1067,12 @@
             }
 
             @Override
-            public void setVolumeUserFlags(String volId, int flags, int mask) throws RemoteException {
+            public void setVolumeUserFlags(String fsUuid, int flags, int mask) throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
                 try {
                     _data.writeInterfaceToken(DESCRIPTOR);
-                    _data.writeString(volId);
+                    _data.writeString(fsUuid);
                     _data.writeInt(flags);
                     _data.writeInt(mask);
                     mRemote.transact(Stub.TRANSACTION_setVolumeUserFlags, _data, _reply, 0);
@@ -1066,6 +1084,21 @@
             }
 
             @Override
+            public void forgetVolume(String fsUuid) throws RemoteException {
+                Parcel _data = Parcel.obtain();
+                Parcel _reply = Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeString(fsUuid);
+                    mRemote.transact(Stub.TRANSACTION_forgetVolume, _data, _reply, 0);
+                    _reply.readException();
+                } finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
+
+            @Override
             public String getPrimaryStorageUuid() throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
@@ -1192,20 +1225,22 @@
 
         static final int TRANSACTION_getDisks = IBinder.FIRST_CALL_TRANSACTION + 44;
         static final int TRANSACTION_getVolumes = IBinder.FIRST_CALL_TRANSACTION + 45;
+        static final int TRANSACTION_getVolumeRecords = IBinder.FIRST_CALL_TRANSACTION + 46;
 
-        static final int TRANSACTION_mount = IBinder.FIRST_CALL_TRANSACTION + 46;
-        static final int TRANSACTION_unmount = IBinder.FIRST_CALL_TRANSACTION + 47;
-        static final int TRANSACTION_format = IBinder.FIRST_CALL_TRANSACTION + 48;
+        static final int TRANSACTION_mount = IBinder.FIRST_CALL_TRANSACTION + 47;
+        static final int TRANSACTION_unmount = IBinder.FIRST_CALL_TRANSACTION + 48;
+        static final int TRANSACTION_format = IBinder.FIRST_CALL_TRANSACTION + 49;
 
-        static final int TRANSACTION_partitionPublic = IBinder.FIRST_CALL_TRANSACTION + 49;
-        static final int TRANSACTION_partitionPrivate = IBinder.FIRST_CALL_TRANSACTION + 50;
-        static final int TRANSACTION_partitionMixed = IBinder.FIRST_CALL_TRANSACTION + 51;
+        static final int TRANSACTION_partitionPublic = IBinder.FIRST_CALL_TRANSACTION + 50;
+        static final int TRANSACTION_partitionPrivate = IBinder.FIRST_CALL_TRANSACTION + 51;
+        static final int TRANSACTION_partitionMixed = IBinder.FIRST_CALL_TRANSACTION + 52;
 
-        static final int TRANSACTION_setVolumeNickname = IBinder.FIRST_CALL_TRANSACTION + 52;
-        static final int TRANSACTION_setVolumeUserFlags = IBinder.FIRST_CALL_TRANSACTION + 53;
+        static final int TRANSACTION_setVolumeNickname = IBinder.FIRST_CALL_TRANSACTION + 53;
+        static final int TRANSACTION_setVolumeUserFlags = IBinder.FIRST_CALL_TRANSACTION + 54;
+        static final int TRANSACTION_forgetVolume = IBinder.FIRST_CALL_TRANSACTION + 55;
 
-        static final int TRANSACTION_getPrimaryStorageUuid = IBinder.FIRST_CALL_TRANSACTION + 54;
-        static final int TRANSACTION_setPrimaryStorageUuid = IBinder.FIRST_CALL_TRANSACTION + 55;
+        static final int TRANSACTION_getPrimaryStorageUuid = IBinder.FIRST_CALL_TRANSACTION + 56;
+        static final int TRANSACTION_setPrimaryStorageUuid = IBinder.FIRST_CALL_TRANSACTION + 57;
 
         /**
          * Cast an IBinder object into an IMountService interface, generating a
@@ -1647,6 +1682,14 @@
                     reply.writeTypedArray(volumes, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
                     return true;
                 }
+                case TRANSACTION_getVolumeRecords: {
+                    data.enforceInterface(DESCRIPTOR);
+                    int _flags = data.readInt();
+                    VolumeRecord[] volumes = getVolumeRecords(_flags);
+                    reply.writeNoException();
+                    reply.writeTypedArray(volumes, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+                    return true;
+                }
                 case TRANSACTION_mount: {
                     data.enforceInterface(DESCRIPTOR);
                     String volId = data.readString();
@@ -1707,6 +1750,13 @@
                     reply.writeNoException();
                     return true;
                 }
+                case TRANSACTION_forgetVolume: {
+                    data.enforceInterface(DESCRIPTOR);
+                    String fsUuid = data.readString();
+                    forgetVolume(fsUuid);
+                    reply.writeNoException();
+                    return true;
+                }
                 case TRANSACTION_getPrimaryStorageUuid: {
                     data.enforceInterface(DESCRIPTOR);
                     String volumeUuid = getPrimaryStorageUuid();
@@ -2012,6 +2062,7 @@
 
     public DiskInfo[] getDisks() throws RemoteException;
     public VolumeInfo[] getVolumes(int flags) throws RemoteException;
+    public VolumeRecord[] getVolumeRecords(int flags) throws RemoteException;
 
     public void mount(String volId) throws RemoteException;
     public void unmount(String volId) throws RemoteException;
@@ -2021,8 +2072,9 @@
     public void partitionPrivate(String diskId) throws RemoteException;
     public void partitionMixed(String diskId, int ratio) throws RemoteException;
 
-    public void setVolumeNickname(String volId, String nickname) throws RemoteException;
-    public void setVolumeUserFlags(String volId, int flags, int mask) throws RemoteException;
+    public void setVolumeNickname(String fsUuid, String nickname) throws RemoteException;
+    public void setVolumeUserFlags(String fsUuid, int flags, int mask) throws RemoteException;
+    public void forgetVolume(String fsUuid) throws RemoteException;
 
     public String getPrimaryStorageUuid() throws RemoteException;
     public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback)
diff --git a/core/java/android/os/storage/IMountServiceListener.java b/core/java/android/os/storage/IMountServiceListener.java
index fcb4779..2d13e49 100644
--- a/core/java/android/os/storage/IMountServiceListener.java
+++ b/core/java/android/os/storage/IMountServiceListener.java
@@ -93,8 +93,8 @@
                 }
                 case TRANSACTION_onVolumeMetadataChanged: {
                     data.enforceInterface(DESCRIPTOR);
-                    final VolumeInfo vol = (VolumeInfo) data.readParcelable(null);
-                    onVolumeMetadataChanged(vol);
+                    final String fsUuid = data.readString();
+                    onVolumeMetadataChanged(fsUuid);
                     reply.writeNoException();
                     return true;
                 }
@@ -192,12 +192,12 @@
             }
 
             @Override
-            public void onVolumeMetadataChanged(VolumeInfo vol) throws RemoteException {
+            public void onVolumeMetadataChanged(String fsUuid) throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
                 try {
                     _data.writeInterfaceToken(DESCRIPTOR);
-                    _data.writeParcelable(vol, 0);
+                    _data.writeString(fsUuid);
                     mRemote.transact(Stub.TRANSACTION_onVolumeMetadataChanged, _data, _reply,
                             android.os.IBinder.FLAG_ONEWAY);
                     _reply.readException();
@@ -253,7 +253,7 @@
     public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState)
             throws RemoteException;
 
-    public void onVolumeMetadataChanged(VolumeInfo vol) throws RemoteException;
+    public void onVolumeMetadataChanged(String fsUuid) 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 6a0140e..536aca9 100644
--- a/core/java/android/os/storage/StorageEventListener.java
+++ b/core/java/android/os/storage/StorageEventListener.java
@@ -41,7 +41,7 @@
     public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
     }
 
-    public void onVolumeMetadataChanged(VolumeInfo vol) {
+    public void onVolumeMetadataChanged(String fsUuid) {
     }
 
     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 6116aef..271ed9d 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -79,9 +79,6 @@
     /** {@hide} */
     public static final String UUID_PRIMARY_PHYSICAL = "primary_physical";
 
-    /** {@hide} */
-    public static final int FLAG_ALL_METADATA = 1 << 0;
-
     private final Context mContext;
     private final ContentResolver mResolver;
 
@@ -120,7 +117,7 @@
                     args.recycle();
                     return true;
                 case MSG_VOLUME_METADATA_CHANGED:
-                    mCallback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
+                    mCallback.onVolumeMetadataChanged((String) args.arg1);
                     args.recycle();
                     return true;
                 case MSG_DISK_SCANNED:
@@ -156,9 +153,9 @@
         }
 
         @Override
-        public void onVolumeMetadataChanged(VolumeInfo vol) {
+        public void onVolumeMetadataChanged(String fsUuid) {
             final SomeArgs args = SomeArgs.obtain();
-            args.arg1 = vol;
+            args.arg1 = fsUuid;
             mHandler.obtainMessage(MSG_VOLUME_METADATA_CHANGED, args).sendToTarget();
         }
 
@@ -516,6 +513,18 @@
     }
 
     /** {@hide} */
+    public @Nullable VolumeRecord findRecordByUuid(String fsUuid) {
+        Preconditions.checkNotNull(fsUuid);
+        // TODO; go directly to service to make this faster
+        for (VolumeRecord rec : getVolumeRecords()) {
+            if (Objects.equals(rec.fsUuid, fsUuid)) {
+                return rec;
+            }
+        }
+        return null;
+    }
+
+    /** {@hide} */
     public @Nullable VolumeInfo findPrivateForEmulated(VolumeInfo emulatedVol) {
         return findVolumeById(emulatedVol.getId().replace("emulated", "private"));
     }
@@ -527,13 +536,17 @@
 
     /** {@hide} */
     public @NonNull List<VolumeInfo> getVolumes() {
-        return getVolumes(0);
+        try {
+            return Arrays.asList(mMountService.getVolumes(0));
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
     }
 
     /** {@hide} */
-    public @NonNull List<VolumeInfo> getVolumes(int flags) {
+    public @NonNull List<VolumeRecord> getVolumeRecords() {
         try {
-            return Arrays.asList(mMountService.getVolumes(flags));
+            return Arrays.asList(mMountService.getVolumeRecords(0));
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
@@ -541,13 +554,23 @@
 
     /** {@hide} */
     public @Nullable String getBestVolumeDescription(VolumeInfo vol) {
-        String descrip = vol.getDescription();
-        if (vol.disk != null) {
-            if (TextUtils.isEmpty(descrip)) {
-                descrip = vol.disk.getDescription();
+        // Nickname always takes precedence when defined
+        if (!TextUtils.isEmpty(vol.fsUuid)) {
+            final VolumeRecord rec = findRecordByUuid(vol.fsUuid);
+            if (!TextUtils.isEmpty(rec.nickname)) {
+                return rec.nickname;
             }
         }
-        return descrip;
+
+        if (!TextUtils.isEmpty(vol.getDescription())) {
+            return vol.getDescription();
+        }
+
+        if (vol.disk != null) {
+            return vol.disk.getDescription();
+        }
+
+        return null;
     }
 
     /** {@hide} */
@@ -616,29 +639,38 @@
     }
 
     /** {@hide} */
-    public void setVolumeNickname(String volId, String nickname) {
+    public void setVolumeNickname(String fsUuid, String nickname) {
         try {
-            mMountService.setVolumeNickname(volId, nickname);
+            mMountService.setVolumeNickname(fsUuid, nickname);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
     }
 
     /** {@hide} */
-    public void setVolumeInited(String volId, boolean inited) {
+    public void setVolumeInited(String fsUuid, boolean inited) {
         try {
-            mMountService.setVolumeUserFlags(volId, inited ? VolumeInfo.USER_FLAG_INITED : 0,
-                    VolumeInfo.USER_FLAG_INITED);
+            mMountService.setVolumeUserFlags(fsUuid, inited ? VolumeRecord.USER_FLAG_INITED : 0,
+                    VolumeRecord.USER_FLAG_INITED);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
     }
 
     /** {@hide} */
-    public void setVolumeSnoozed(String volId, boolean snoozed) {
+    public void setVolumeSnoozed(String fsUuid, boolean snoozed) {
         try {
-            mMountService.setVolumeUserFlags(volId, snoozed ? VolumeInfo.USER_FLAG_SNOOZED : 0,
-                    VolumeInfo.USER_FLAG_SNOOZED);
+            mMountService.setVolumeUserFlags(fsUuid, snoozed ? VolumeRecord.USER_FLAG_SNOOZED : 0,
+                    VolumeRecord.USER_FLAG_SNOOZED);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /** {@hide} */
+    public void forgetVolume(String fsUuid) {
+        try {
+            mMountService.forgetVolume(fsUuid);
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java
index 4e9cfc7..fd10cae 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -78,9 +78,6 @@
     public static final int MOUNT_FLAG_PRIMARY = 1 << 0;
     public static final int MOUNT_FLAG_VISIBLE = 1 << 1;
 
-    public static final int USER_FLAG_INITED = 1 << 0;
-    public static final int USER_FLAG_SNOOZED = 1 << 1;
-
     private static SparseArray<String> sStateToEnvironment = new SparseArray<>();
     private static ArrayMap<String, String> sEnvironmentToBroadcast = new ArrayMap<>();
 
@@ -135,8 +132,6 @@
 
     /** Framework state */
     public final int mtpIndex;
-    public String nickname;
-    public int userFlags = 0;
 
     public VolumeInfo(String id, int type, DiskInfo disk, int mtpIndex) {
         this.id = Preconditions.checkNotNull(id);
@@ -161,8 +156,6 @@
         fsLabel = parcel.readString();
         path = parcel.readString();
         mtpIndex = parcel.readInt();
-        nickname = parcel.readString();
-        userFlags = parcel.readInt();
     }
 
     public static @NonNull String getEnvironmentForState(int state) {
@@ -210,10 +203,6 @@
         return fsUuid;
     }
 
-    public @Nullable String getNickname() {
-        return nickname;
-    }
-
     public int getMountUserId() {
         return mountUserId;
     }
@@ -221,8 +210,6 @@
     public @Nullable String getDescription() {
         if (ID_PRIVATE_INTERNAL.equals(id)) {
             return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
-        } else if (!TextUtils.isEmpty(nickname)) {
-            return nickname;
         } else if (!TextUtils.isEmpty(fsLabel)) {
             return fsLabel;
         } else {
@@ -250,14 +237,6 @@
         return (mountFlags & MOUNT_FLAG_VISIBLE) != 0;
     }
 
-    public boolean isInited() {
-        return (userFlags & USER_FLAG_INITED) != 0;
-    }
-
-    public boolean isSnoozed() {
-        return (userFlags & USER_FLAG_SNOOZED) != 0;
-    }
-
     public boolean isVisibleToUser(int userId) {
         if (type == TYPE_PUBLIC && userId == this.mountUserId) {
             return isVisible();
@@ -394,8 +373,6 @@
         pw.println();
         pw.printPair("path", path);
         pw.printPair("mtpIndex", mtpIndex);
-        pw.printPair("nickname", nickname);
-        pw.printPair("userFlags", DebugUtils.flagsToString(getClass(), "USER_FLAG_", userFlags));
         pw.decreaseIndent();
         pw.println();
     }
@@ -461,7 +438,5 @@
         parcel.writeString(fsLabel);
         parcel.writeString(path);
         parcel.writeInt(mtpIndex);
-        parcel.writeString(nickname);
-        parcel.writeInt(userFlags);
     }
 }
diff --git a/core/java/android/os/storage/VolumeRecord.java b/core/java/android/os/storage/VolumeRecord.java
new file mode 100644
index 0000000..d12d150
--- /dev/null
+++ b/core/java/android/os/storage/VolumeRecord.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os.storage;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.DebugUtils;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * Notes for a storage volume which may not be currently present.
+ *
+ * @hide
+ */
+public class VolumeRecord implements Parcelable {
+    public static final String EXTRA_FS_UUID =
+            "android.os.storage.extra.FS_UUID";
+
+    public static final int USER_FLAG_INITED = 1 << 0;
+    public static final int USER_FLAG_SNOOZED = 1 << 1;
+
+    public final int type;
+    public final String fsUuid;
+    public String nickname;
+    public int userFlags;
+
+    public VolumeRecord(int type, String fsUuid) {
+        this.type = type;
+        this.fsUuid = Preconditions.checkNotNull(fsUuid);
+    }
+
+    public VolumeRecord(Parcel parcel) {
+        type = parcel.readInt();
+        fsUuid = parcel.readString();
+        nickname = parcel.readString();
+        userFlags = parcel.readInt();
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public String getFsUuid() {
+        return fsUuid;
+    }
+
+    public String getNickname() {
+        return nickname;
+    }
+
+    public boolean isInited() {
+        return (userFlags & USER_FLAG_INITED) != 0;
+    }
+
+    public boolean isSnoozed() {
+        return (userFlags & USER_FLAG_SNOOZED) != 0;
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("VolumeRecord:");
+        pw.increaseIndent();
+        pw.printPair("type", DebugUtils.valueToString(VolumeInfo.class, "TYPE_", type));
+        pw.printPair("fsUuid", fsUuid);
+        pw.printPair("nickname", nickname);
+        pw.printPair("userFlags",
+                DebugUtils.flagsToString(VolumeRecord.class, "USER_FLAG_", userFlags));
+        pw.decreaseIndent();
+        pw.println();
+    }
+
+    @Override
+    public VolumeRecord clone() {
+        final Parcel temp = Parcel.obtain();
+        try {
+            writeToParcel(temp, 0);
+            temp.setDataPosition(0);
+            return CREATOR.createFromParcel(temp);
+        } finally {
+            temp.recycle();
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o instanceof VolumeRecord) {
+            return Objects.equals(fsUuid, ((VolumeRecord) o).fsUuid);
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return fsUuid.hashCode();
+    }
+
+    public static final Creator<VolumeRecord> CREATOR = new Creator<VolumeRecord>() {
+        @Override
+        public VolumeRecord createFromParcel(Parcel in) {
+            return new VolumeRecord(in);
+        }
+
+        @Override
+        public VolumeRecord[] newArray(int size) {
+            return new VolumeRecord[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeInt(type);
+        parcel.writeString(fsUuid);
+        parcel.writeString(nickname);
+        parcel.writeInt(userFlags);
+    }
+}
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 51c2062..3146f41 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3012,6 +3012,26 @@
     <!-- Notification action to browse external media [CHAR LIMIT=20] -->
     <string name="ext_media_browse_action">Explore</string>
 
+    <!-- Notification title when external media is missing [CHAR LIMIT=30] -->
+    <string name="ext_media_missing_title"><xliff:g id="name" example="SD card">%s</xliff:g> missing</string>
+    <!-- Notification body when external media is missing [CHAR LIMIT=30] -->
+    <string name="ext_media_missing_message">Reinsert this device</string>
+
+    <!-- Notification title when moving an application to external storage [CHAR LIMIT=30] -->
+    <string name="ext_media_move_specific_title">Moving <xliff:g id="name" example="Calculator">%s</xliff:g></string>
+    <!-- Notification title when moving data to external storage [CHAR LIMIT=32] -->
+    <string name="ext_media_move_title">Moving data</string>
+
+    <!-- Notification title when moving data to external storage [CHAR LIMIT=32] -->
+    <string name="ext_media_move_success_title">Move complete</string>
+    <!-- Notification title when moving data to external storage [CHAR LIMIT=64] -->
+    <string name="ext_media_move_success_message">Data moved to <xliff:g id="name" example="SD card">%s</xliff:g></string>
+
+    <!-- Notification title when moving data to external storage failed [CHAR LIMIT=32] -->
+    <string name="ext_media_move_failure_title">Couldn\'t move data</string>
+    <!-- Notification title when moving data to external storage failed [CHAR LIMIT=64] -->
+    <string name="ext_media_move_failure_message">Data left at original location</string>
+
     <!-- Shown in LauncherActivity when the requested target Intent didn't return any matching Activities, leaving the list empty. -->
     <string name="activity_list_empty">No matching activities found.</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b05fe07..90437b9 100755
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1899,6 +1899,14 @@
   <java-symbol type="string" name="ext_media_init_action" />
   <java-symbol type="string" name="ext_media_unmount_action" />
   <java-symbol type="string" name="ext_media_browse_action" />
+  <java-symbol type="string" name="ext_media_missing_title" />
+  <java-symbol type="string" name="ext_media_missing_message" />
+  <java-symbol type="string" name="ext_media_move_specific_title" />
+  <java-symbol type="string" name="ext_media_move_title" />
+  <java-symbol type="string" name="ext_media_move_success_title" />
+  <java-symbol type="string" name="ext_media_move_success_message" />
+  <java-symbol type="string" name="ext_media_move_failure_title" />
+  <java-symbol type="string" name="ext_media_move_failure_message" />
   <java-symbol type="string" name="usb_storage_error_message" />
   <java-symbol type="string" name="usb_storage_message" />
   <java-symbol type="string" name="usb_storage_notification_message" />
diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
index 240c210..deed895 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
@@ -24,11 +24,17 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.MoveCallback;
+import android.os.Handler;
 import android.os.UserHandle;
 import android.os.storage.DiskInfo;
 import android.os.storage.StorageEventListener;
 import android.os.storage.StorageManager;
 import android.os.storage.VolumeInfo;
+import android.os.storage.VolumeRecord;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.util.Log;
 
 import com.android.internal.R;
@@ -39,12 +45,14 @@
 public class StorageNotification extends SystemUI {
     private static final String TAG = "StorageNotification";
 
-    private static final int NOTIF_ID = 0x53544f52; // STOR
+    private static final int PUBLIC_ID = 0x53505542; // SPUB
+    private static final int PRIVATE_ID = 0x53505256; // SPRV
+    private static final int DISK_ID = 0x5344534b; // SDSK
+    private static final int MOVE_ID = 0x534d4f56; // SMOV
 
     private static final String ACTION_SNOOZE_VOLUME = "com.android.systemui.action.SNOOZE_VOLUME";
 
     // TODO: delay some notifications to avoid bumpy fast operations
-    // TODO: annoy user when private media is missing
 
     private NotificationManager mNotificationManager;
     private StorageManager mStorageManager;
@@ -52,17 +60,29 @@
     private final StorageEventListener mListener = new StorageEventListener() {
         @Override
         public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
-            onVolumeStateChangedInternal(vol, oldState, newState);
+            onVolumeStateChangedInternal(vol);
         }
 
         @Override
-        public void onVolumeMetadataChanged(VolumeInfo vol) {
+        public void onVolumeMetadataChanged(String fsUuid) {
             // Avoid kicking notifications when getting early metadata before
             // mounted. If already mounted, we're being kicked because of a
             // nickname or init'ed change.
-            if (vol.isMountedReadable()) {
-                onVolumeStateChangedInternal(vol, vol.getState(), vol.getState());
+            final VolumeInfo vol = mStorageManager.findVolumeByUuid(fsUuid);
+            if (vol != null && vol.isMountedReadable()) {
+                onVolumeStateChangedInternal(vol);
             }
+
+            final VolumeRecord rec = mStorageManager.findRecordByUuid(fsUuid);
+            if (rec == null) {
+                // Private volume was probably just forgotten
+                mNotificationManager.cancelAsUser(fsUuid, PRIVATE_ID, UserHandle.ALL);
+            }
+        }
+
+        @Override
+        public void onDiskScanned(DiskInfo disk, int volumeCount) {
+            onDiskScannedInternal(disk, volumeCount);
         }
     };
 
@@ -70,8 +90,19 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             // TODO: kick this onto background thread
-            final String volId = intent.getStringExtra(VolumeInfo.EXTRA_VOLUME_ID);
-            mStorageManager.setVolumeSnoozed(volId, true);
+            final String fsUuid = intent.getStringExtra(VolumeRecord.EXTRA_FS_UUID);
+            mStorageManager.setVolumeSnoozed(fsUuid, true);
+        }
+    };
+
+    private final MoveCallback mMoveCallback = new MoveCallback() {
+        @Override
+        public void onStatusChanged(int moveId, String moveTitle, int status, long estMillis) {
+            if (PackageManager.isMoveStatusFinished(status)) {
+                onMoveFinished(moveId, moveTitle, status);
+            } else {
+                onMoveProgress(moveId, moveTitle, status, estMillis);
+            }
         }
     };
 
@@ -88,20 +119,99 @@
         // Kick current state into place
         final List<VolumeInfo> vols = mStorageManager.getVolumes();
         for (VolumeInfo vol : vols) {
-            onVolumeStateChangedInternal(vol, vol.getState(), vol.getState());
+            onVolumeStateChangedInternal(vol);
+        }
+
+        mContext.getPackageManager().registerMoveCallback(mMoveCallback, new Handler());
+
+        updateMissingPrivateVolumes();
+    }
+
+    private void updateMissingPrivateVolumes() {
+        final List<VolumeRecord> recs = mStorageManager.getVolumeRecords();
+        for (VolumeRecord rec : recs) {
+            if (rec.getType() != VolumeInfo.TYPE_PRIVATE) continue;
+
+            final String fsUuid = rec.getFsUuid();
+            final VolumeInfo info = mStorageManager.findVolumeByUuid(fsUuid);
+            if (info != null && info.isMountedWritable()) {
+                // Yay, private volume is here!
+                mNotificationManager.cancelAsUser(fsUuid, PRIVATE_ID, UserHandle.ALL);
+
+            } else {
+                // Boo, annoy the user to reinsert the private volume
+                final CharSequence title = mContext.getString(R.string.ext_media_missing_title,
+                        rec.getNickname());
+                final CharSequence text = mContext.getString(R.string.ext_media_missing_message);
+
+                final Notification notif = new Notification.Builder(mContext)
+                        .setSmallIcon(R.drawable.stat_notify_sdcard)
+                        .setColor(mContext.getColor(R.color.system_notification_accent_color))
+                        .setContentTitle(title)
+                        .setContentText(text)
+                        .setStyle(new Notification.BigTextStyle().bigText(text))
+                        .setVisibility(Notification.VISIBILITY_PUBLIC)
+                        .setLocalOnly(true)
+                        .setContentIntent(buildForgetPendingIntent(rec))
+                        .setCategory(Notification.CATEGORY_SYSTEM)
+                        .setOngoing(true)
+                        .build();
+
+                mNotificationManager.notifyAsUser(fsUuid, PRIVATE_ID, notif, UserHandle.ALL);
+            }
         }
     }
 
-    public void onVolumeStateChangedInternal(VolumeInfo vol, int oldState, int newState) {
-        // We only care about public volumes
-        if (vol.getType() != VolumeInfo.TYPE_PUBLIC) {
-            return;
-        }
+    private void onDiskScannedInternal(DiskInfo disk, int volumeCount) {
+        if (volumeCount == 0) {
+            // No supported volumes found, give user option to format
+            final CharSequence title = mContext.getString(
+                    R.string.ext_media_unmountable_notification_title, disk.getDescription());
+            final CharSequence text = mContext.getString(
+                    R.string.ext_media_unmountable_notification_message, disk.getDescription());
 
-        Log.d(TAG, vol.toString());
+            final Notification notif = new Notification.Builder(mContext)
+                    .setSmallIcon(getSmallIcon(disk, VolumeInfo.STATE_UNMOUNTABLE))
+                    .setColor(mContext.getColor(R.color.system_notification_accent_color))
+                    .setContentTitle(title)
+                    .setContentText(text)
+                    .setStyle(new Notification.BigTextStyle().bigText(text))
+                    .setVisibility(Notification.VISIBILITY_PUBLIC)
+                    .setLocalOnly(true)
+                    .setContentIntent(buildInitPendingIntent(disk))
+                    .setCategory(Notification.CATEGORY_ERROR)
+                    .build();
+
+            mNotificationManager.notifyAsUser(disk.getId(), DISK_ID, notif, UserHandle.ALL);
+
+        } else {
+            // Yay, we have volumes!
+            mNotificationManager.cancelAsUser(disk.getId(), DISK_ID, UserHandle.ALL);
+        }
+    }
+
+    private void onVolumeStateChangedInternal(VolumeInfo vol) {
+        switch (vol.getType()) {
+            case VolumeInfo.TYPE_PRIVATE:
+                onPrivateVolumeStateChangedInternal(vol);
+                break;
+            case VolumeInfo.TYPE_PUBLIC:
+                onPublicVolumeStateChangedInternal(vol);
+                break;
+        }
+    }
+
+    private void onPrivateVolumeStateChangedInternal(VolumeInfo vol) {
+        Log.d(TAG, "Notifying about private volume: " + vol.toString());
+
+        updateMissingPrivateVolumes();
+    }
+
+    private void onPublicVolumeStateChangedInternal(VolumeInfo vol) {
+        Log.d(TAG, "Notifying about public volume: " + vol.toString());
 
         final Notification notif;
-        switch (newState) {
+        switch (vol.getState()) {
             case VolumeInfo.STATE_UNMOUNTED:
                 notif = onVolumeUnmounted(vol);
                 break;
@@ -133,9 +243,9 @@
         }
 
         if (notif != null) {
-            mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
+            mNotificationManager.notifyAsUser(vol.getId(), PUBLIC_ID, notif, UserHandle.ALL);
         } else {
-            mNotificationManager.cancelAsUser(vol.getId(), NOTIF_ID, UserHandle.ALL);
+            mNotificationManager.cancelAsUser(vol.getId(), PUBLIC_ID, UserHandle.ALL);
         }
     }
 
@@ -159,20 +269,24 @@
     }
 
     private Notification onVolumeMounted(VolumeInfo vol) {
+        final VolumeRecord rec = mStorageManager.findRecordByUuid(vol.getFsUuid());
+
         // Don't annoy when user dismissed in past
-        if (vol.isSnoozed()) return null;
+        if (rec.isSnoozed()) return null;
 
         final DiskInfo disk = vol.getDisk();
-        if (disk.isAdoptable() && !vol.isInited()) {
+        if (disk.isAdoptable() && !rec.isInited()) {
             final CharSequence title = disk.getDescription();
             final CharSequence text = mContext.getString(
                     R.string.ext_media_new_notification_message, disk.getDescription());
 
+            final PendingIntent initAction = buildInitPendingIntent(vol);
             return buildNotificationBuilder(vol, title, text)
                     .addAction(new Action(0, mContext.getString(R.string.ext_media_init_action),
-                            buildInitPendingIntent(vol)))
+                            initAction))
                     .addAction(new Action(0, mContext.getString(R.string.ext_media_unmount_action),
                             buildUnmountPendingIntent(vol)))
+                    .setContentIntent(initAction)
                     .setDeleteIntent(buildSnoozeIntent(vol))
                     .setCategory(Notification.CATEGORY_SYSTEM)
                     .build();
@@ -182,11 +296,13 @@
             final CharSequence text = mContext.getString(
                     R.string.ext_media_ready_notification_message, disk.getDescription());
 
+            final PendingIntent browseAction = buildBrowsePendingIntent(vol);
             return buildNotificationBuilder(vol, title, text)
                     .addAction(new Action(0, mContext.getString(R.string.ext_media_browse_action),
-                            buildBrowsePendingIntent(vol)))
+                            browseAction))
                     .addAction(new Action(0, mContext.getString(R.string.ext_media_unmount_action),
                             buildUnmountPendingIntent(vol)))
+                    .setContentIntent(browseAction)
                     .setDeleteIntent(buildSnoozeIntent(vol))
                     .setCategory(Notification.CATEGORY_SYSTEM)
                     .setPriority(Notification.PRIORITY_LOW)
@@ -260,16 +376,84 @@
                 .build();
     }
 
-    private int getSmallIcon(VolumeInfo vol) {
-        if (vol.disk.isSd()) {
-            switch (vol.getState()) {
+    private void onMoveProgress(int moveId, String moveTitle, int status, long estMillis) {
+        final CharSequence title;
+        if (!TextUtils.isEmpty(moveTitle)) {
+            title = mContext.getString(R.string.ext_media_move_specific_title, moveTitle);
+        } else {
+            title = mContext.getString(R.string.ext_media_move_title);
+        }
+
+        final CharSequence text;
+        if (estMillis < 0) {
+            text = null;
+        } else {
+            text = DateUtils.formatDuration(estMillis);
+        }
+
+        final Notification notif = new Notification.Builder(mContext)
+                .setSmallIcon(R.drawable.stat_notify_sdcard)
+                .setColor(mContext.getColor(R.color.system_notification_accent_color))
+                .setContentTitle(title)
+                .setContentText(text)
+                .setStyle(new Notification.BigTextStyle().bigText(text))
+                .setVisibility(Notification.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+                .setCategory(Notification.CATEGORY_PROGRESS)
+                .setPriority(Notification.PRIORITY_LOW)
+                .setProgress(100, status, false)
+                .setOngoing(true)
+                .build();
+
+        mNotificationManager.notifyAsUser(moveTitle, MOVE_ID, notif, UserHandle.ALL);
+    }
+
+    private void onMoveFinished(int moveId, String moveTitle, int status) {
+        if (!TextUtils.isEmpty(moveTitle)) {
+            // We currently ignore finished app moves; just clear the last
+            // published progress
+            mNotificationManager.cancelAsUser(moveTitle, MOVE_ID, UserHandle.ALL);
+            return;
+        }
+
+        final VolumeInfo vol = mContext.getPackageManager().getPrimaryStorageCurrentVolume();
+        final String descrip = mStorageManager.getBestVolumeDescription(vol);
+
+        final CharSequence title;
+        final CharSequence text;
+        if (status == PackageManager.MOVE_SUCCEEDED) {
+            title = mContext.getString(R.string.ext_media_move_success_title);
+            text = mContext.getString(R.string.ext_media_move_success_message, descrip);
+        } else {
+            title = mContext.getString(R.string.ext_media_move_failure_title);
+            text = mContext.getString(R.string.ext_media_move_failure_message);
+        }
+
+        final Notification notif = new Notification.Builder(mContext)
+                .setSmallIcon(R.drawable.stat_notify_sdcard)
+                .setColor(mContext.getColor(R.color.system_notification_accent_color))
+                .setContentTitle(title)
+                .setContentText(text)
+                .setStyle(new Notification.BigTextStyle().bigText(text))
+                .setVisibility(Notification.VISIBILITY_PUBLIC)
+                .setLocalOnly(true)
+                .setCategory(Notification.CATEGORY_SYSTEM)
+                .setPriority(Notification.PRIORITY_LOW)
+                .build();
+
+        mNotificationManager.notifyAsUser(moveTitle, MOVE_ID, notif, UserHandle.ALL);
+    }
+
+    private int getSmallIcon(DiskInfo disk, int state) {
+        if (disk.isSd()) {
+            switch (state) {
                 case VolumeInfo.STATE_CHECKING:
                 case VolumeInfo.STATE_EJECTING:
                     return R.drawable.stat_notify_sdcard_prepare;
                 default:
                     return R.drawable.stat_notify_sdcard;
             }
-        } else if (vol.disk.isUsb()) {
+        } else if (disk.isUsb()) {
             return R.drawable.stat_sys_data_usb;
         } else {
             return R.drawable.stat_notify_sdcard;
@@ -279,7 +463,7 @@
     private Notification.Builder buildNotificationBuilder(VolumeInfo vol, CharSequence title,
             CharSequence text) {
         return new Notification.Builder(mContext)
-                .setSmallIcon(getSmallIcon(vol))
+                .setSmallIcon(getSmallIcon(vol.getDisk(), vol.getState()))
                 .setColor(mContext.getColor(R.color.system_notification_accent_color))
                 .setContentTitle(title)
                 .setContentText(text)
@@ -288,6 +472,17 @@
                 .setLocalOnly(true);
     }
 
+    private PendingIntent buildInitPendingIntent(DiskInfo disk) {
+        final Intent intent = new Intent();
+        intent.setClassName("com.android.settings",
+                "com.android.settings.deviceinfo.StorageWizardInit");
+        intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.getId());
+
+        final int requestKey = disk.getId().hashCode();
+        return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+    }
+
     private PendingIntent buildInitPendingIntent(VolumeInfo vol) {
         final Intent intent = new Intent();
         intent.setClassName("com.android.settings",
@@ -321,7 +516,7 @@
     private PendingIntent buildDetailsPendingIntent(VolumeInfo vol) {
         final Intent intent = new Intent();
         intent.setClassName("com.android.settings",
-                "com.android.settings.Settings$StorageVolumeSettingsActivity");
+                "com.android.settings.Settings$PublicVolumeSettingsActivity");
         intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
 
         final int requestKey = vol.getId().hashCode();
@@ -331,10 +526,21 @@
 
     private PendingIntent buildSnoozeIntent(VolumeInfo vol) {
         final Intent intent = new Intent(ACTION_SNOOZE_VOLUME);
-        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+        intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid());
 
         final int requestKey = vol.getId().hashCode();
         return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent,
                 PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT);
     }
+
+    private PendingIntent buildForgetPendingIntent(VolumeRecord rec) {
+        final Intent intent = new Intent();
+        intent.setClassName("com.android.settings",
+                "com.android.settings.Settings$PrivateVolumeForgetActivity");
+        intent.putExtra(VolumeRecord.EXTRA_FS_UUID, rec.getFsUuid());
+
+        final int requestKey = rec.getFsUuid().hashCode();
+        return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+    }
 }
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index 643ff5f..74adeb7 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -62,11 +62,11 @@
 import android.os.storage.StorageResultCode;
 import android.os.storage.StorageVolume;
 import android.os.storage.VolumeInfo;
+import android.os.storage.VolumeRecord;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.AtomicFile;
-import android.util.DebugUtils;
 import android.util.Log;
 import android.util.Slog;
 import android.util.Xml;
@@ -251,49 +251,7 @@
     private static final String ATTR_NICKNAME = "nickname";
     private static final String ATTR_USER_FLAGS = "userFlags";
 
-    private final AtomicFile mMetadataFile;
-
-    private static class VolumeMetadata {
-        public final int type;
-        public final String fsUuid;
-        public String nickname;
-        public int userFlags;
-
-        public VolumeMetadata(int type, String fsUuid) {
-            this.type = type;
-            this.fsUuid = Preconditions.checkNotNull(fsUuid);
-        }
-
-        public static VolumeMetadata read(XmlPullParser in) throws IOException {
-            final int type = readIntAttribute(in, ATTR_TYPE);
-            final String fsUuid = readStringAttribute(in, ATTR_FS_UUID);
-            final VolumeMetadata meta = new VolumeMetadata(type, fsUuid);
-            meta.nickname = readStringAttribute(in, ATTR_NICKNAME);
-            meta.userFlags = readIntAttribute(in, ATTR_USER_FLAGS);
-            return meta;
-        }
-
-        public static void write(XmlSerializer out, VolumeMetadata meta) throws IOException {
-            out.startTag(null, TAG_VOLUME);
-            writeIntAttribute(out, ATTR_TYPE, meta.type);
-            writeStringAttribute(out, ATTR_FS_UUID, meta.fsUuid);
-            writeStringAttribute(out, ATTR_NICKNAME, meta.nickname);
-            writeIntAttribute(out, ATTR_USER_FLAGS, meta.userFlags);
-            out.endTag(null, TAG_VOLUME);
-        }
-
-        public void dump(IndentingPrintWriter pw) {
-            pw.println("VolumeMetadata:");
-            pw.increaseIndent();
-            pw.printPair("type", DebugUtils.valueToString(VolumeInfo.class, "TYPE_", type));
-            pw.printPair("fsUuid", fsUuid);
-            pw.printPair("nickname", nickname);
-            pw.printPair("userFlags",
-                    DebugUtils.flagsToString(VolumeInfo.class, "USER_FLAG_", userFlags));
-            pw.decreaseIndent();
-            pw.println();
-        }
-    }
+    private final AtomicFile mSettingsFile;
 
     /**
      * <em>Never</em> hold the lock while performing downcalls into vold, since
@@ -311,9 +269,9 @@
     @GuardedBy("mLock")
     private ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>();
 
-    /** Map from UUID to metadata */
+    /** Map from UUID to record */
     @GuardedBy("mLock")
-    private ArrayMap<String, VolumeMetadata> mMetadata = new ArrayMap<>();
+    private ArrayMap<String, VolumeRecord> mRecords = new ArrayMap<>();
     @GuardedBy("mLock")
     private String mPrimaryStorageUuid;
 
@@ -370,15 +328,6 @@
         }
     }
 
-    private VolumeMetadata findOrCreateMetadataLocked(VolumeInfo vol) {
-        VolumeMetadata meta = mMetadata.get(vol.fsUuid);
-        if (meta == null) {
-            meta = new VolumeMetadata(vol.type, vol.fsUuid);
-            mMetadata.put(meta.fsUuid, meta);
-        }
-        return meta;
-    }
-
     private CountDownLatch findOrCreateDiskScanLatch(String diskId) {
         synchronized (mLock) {
             CountDownLatch latch = mDiskScanLatches.get(diskId);
@@ -913,7 +862,7 @@
                     final int oldState = vol.state;
                     final int newState = Integer.parseInt(cooked[2]);
                     vol.state = newState;
-                    onVolumeStateChangedLocked(vol.clone(), oldState, newState);
+                    onVolumeStateChangedLocked(vol, oldState, newState);
                 }
                 break;
             }
@@ -923,7 +872,6 @@
                 if (vol != null) {
                     vol.fsType = cooked[2];
                 }
-                mCallbacks.notifyVolumeMetadataChanged(vol.clone());
                 break;
             }
             case VoldResponseCode.VOLUME_FS_UUID_CHANGED: {
@@ -932,8 +880,6 @@
                 if (vol != null) {
                     vol.fsUuid = cooked[2];
                 }
-                refreshMetadataLocked();
-                mCallbacks.notifyVolumeMetadataChanged(vol.clone());
                 break;
             }
             case VoldResponseCode.VOLUME_FS_LABEL_CHANGED: {
@@ -945,7 +891,7 @@
                     }
                     vol.fsLabel = builder.toString().trim();
                 }
-                mCallbacks.notifyVolumeMetadataChanged(vol.clone());
+                // TODO: notify listeners that label changed
                 break;
             }
             case VoldResponseCode.VOLUME_PATH_CHANGED: {
@@ -1070,6 +1016,19 @@
     }
 
     private void onVolumeStateChangedLocked(VolumeInfo vol, int oldState, int newState) {
+        // Remember that we saw this volume so we're ready to accept user
+        // metadata, or so we can annoy them when a private volume is ejected
+        if (vol.isMountedReadable() && !TextUtils.isEmpty(vol.fsUuid)) {
+            if (!mRecords.containsKey(vol.fsUuid)) {
+                final VolumeRecord rec = new VolumeRecord(vol.type, vol.fsUuid);
+                if (vol.type == VolumeInfo.TYPE_PRIVATE) {
+                    rec.nickname = vol.disk.getDescription();
+                }
+                mRecords.put(rec.fsUuid, rec);
+                writeSettingsLocked();
+            }
+        }
+
         mCallbacks.notifyVolumeStateChanged(vol, oldState, newState);
 
         if (isBroadcastWorthy(vol)) {
@@ -1117,7 +1076,7 @@
 
         // TODO: estimate remaining time
         try {
-            mMoveCallback.onStatusChanged(-1, status, -1);
+            mMoveCallback.onStatusChanged(-1, null, status, -1);
         } catch (RemoteException ignored) {
         }
 
@@ -1127,7 +1086,7 @@
             Slog.d(TAG, "Move to " + mMoveTargetUuid + " copy phase finshed; persisting");
 
             mPrimaryStorageUuid = mMoveTargetUuid;
-            writeMetadataLocked();
+            writeSettingsLocked();
         }
 
         if (PackageManager.isMoveStatusFinished(status)) {
@@ -1138,25 +1097,6 @@
         }
     }
 
-    /**
-     * Refresh latest metadata into any currently active {@link VolumeInfo}.
-     */
-    private void refreshMetadataLocked() {
-        final int size = mVolumes.size();
-        for (int i = 0; i < size; i++) {
-            final VolumeInfo vol = mVolumes.valueAt(i);
-            final VolumeMetadata meta = mMetadata.get(vol.fsUuid);
-
-            if (meta != null) {
-                vol.nickname = meta.nickname;
-                vol.userFlags = meta.userFlags;
-            } else {
-                vol.nickname = null;
-                vol.userFlags = 0;
-            }
-        }
-    }
-
     private void enforcePermission(String perm) {
         mContext.enforceCallingOrSelfPermission(perm, perm);
     }
@@ -1205,11 +1145,11 @@
             mLastMaintenance = mLastMaintenanceFile.lastModified();
         }
 
-        mMetadataFile = new AtomicFile(
+        mSettingsFile = new AtomicFile(
                 new File(Environment.getSystemSecureDirectory(), "storage.xml"));
 
         synchronized (mLock) {
-            readMetadataLocked();
+            readSettingsLocked();
         }
 
         /*
@@ -1235,12 +1175,12 @@
         mHandler.obtainMessage(H_SYSTEM_READY).sendToTarget();
     }
 
-    private void readMetadataLocked() {
-        mMetadata.clear();
+    private void readSettingsLocked() {
+        mRecords.clear();
 
         FileInputStream fis = null;
         try {
-            fis = mMetadataFile.openRead();
+            fis = mSettingsFile.openRead();
             final XmlPullParser in = Xml.newPullParser();
             in.setInput(fis, null);
 
@@ -1263,8 +1203,8 @@
                         }
 
                     } else if (TAG_VOLUME.equals(tag)) {
-                        final VolumeMetadata meta = VolumeMetadata.read(in);
-                        mMetadata.put(meta.fsUuid, meta);
+                        final VolumeRecord rec = readVolumeRecord(in);
+                        mRecords.put(rec.fsUuid, rec);
                     }
                 }
             }
@@ -1279,10 +1219,10 @@
         }
     }
 
-    private void writeMetadataLocked() {
+    private void writeSettingsLocked() {
         FileOutputStream fos = null;
         try {
-            fos = mMetadataFile.startWrite();
+            fos = mSettingsFile.startWrite();
 
             XmlSerializer out = new FastXmlSerializer();
             out.setOutput(fos, "utf-8");
@@ -1290,22 +1230,40 @@
             out.startTag(null, TAG_VOLUMES);
             writeIntAttribute(out, ATTR_VERSION, VERSION_ADD_PRIMARY);
             writeStringAttribute(out, ATTR_PRIMARY_STORAGE_UUID, mPrimaryStorageUuid);
-            final int size = mMetadata.size();
+            final int size = mRecords.size();
             for (int i = 0; i < size; i++) {
-                final VolumeMetadata meta = mMetadata.valueAt(i);
-                VolumeMetadata.write(out, meta);
+                final VolumeRecord rec = mRecords.valueAt(i);
+                writeVolumeRecord(out, rec);
             }
             out.endTag(null, TAG_VOLUMES);
             out.endDocument();
 
-            mMetadataFile.finishWrite(fos);
+            mSettingsFile.finishWrite(fos);
         } catch (IOException e) {
             if (fos != null) {
-                mMetadataFile.failWrite(fos);
+                mSettingsFile.failWrite(fos);
             }
         }
     }
 
+    public static VolumeRecord readVolumeRecord(XmlPullParser in) throws IOException {
+        final int type = readIntAttribute(in, ATTR_TYPE);
+        final String fsUuid = readStringAttribute(in, ATTR_FS_UUID);
+        final VolumeRecord meta = new VolumeRecord(type, fsUuid);
+        meta.nickname = readStringAttribute(in, ATTR_NICKNAME);
+        meta.userFlags = readIntAttribute(in, ATTR_USER_FLAGS);
+        return meta;
+    }
+
+    public static void writeVolumeRecord(XmlSerializer out, VolumeRecord rec) throws IOException {
+        out.startTag(null, TAG_VOLUME);
+        writeIntAttribute(out, ATTR_TYPE, rec.type);
+        writeStringAttribute(out, ATTR_FS_UUID, rec.fsUuid);
+        writeStringAttribute(out, ATTR_NICKNAME, rec.nickname);
+        writeIntAttribute(out, ATTR_USER_FLAGS, rec.userFlags);
+        out.endTag(null, TAG_VOLUME);
+    }
+
     /**
      * Exposed API calls below here
      */
@@ -1471,32 +1429,40 @@
     }
 
     @Override
-    public void setVolumeNickname(String volId, String nickname) {
+    public void setVolumeNickname(String fsUuid, String nickname) {
         enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
         waitForReady();
 
         synchronized (mLock) {
-            final VolumeInfo vol = findVolumeById(volId);
-            final VolumeMetadata meta = findOrCreateMetadataLocked(vol);
-            meta.nickname = nickname;
-            refreshMetadataLocked();
-            writeMetadataLocked();
-            mCallbacks.notifyVolumeMetadataChanged(vol.clone());
+            final VolumeRecord rec = mRecords.get(fsUuid);
+            rec.nickname = nickname;
+            mCallbacks.notifyVolumeMetadataChanged(fsUuid);
+            writeSettingsLocked();
         }
     }
 
     @Override
-    public void setVolumeUserFlags(String volId, int flags, int mask) {
+    public void setVolumeUserFlags(String fsUuid, int flags, int mask) {
         enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
         waitForReady();
 
         synchronized (mLock) {
-            final VolumeInfo vol = findVolumeById(volId);
-            final VolumeMetadata meta = findOrCreateMetadataLocked(vol);
-            meta.userFlags = (meta.userFlags & ~mask) | (flags & mask);
-            refreshMetadataLocked();
-            writeMetadataLocked();
-            mCallbacks.notifyVolumeMetadataChanged(vol.clone());
+            final VolumeRecord rec = mRecords.get(fsUuid);
+            rec.userFlags = (rec.userFlags & ~mask) | (flags & mask);
+            mCallbacks.notifyVolumeMetadataChanged(fsUuid);
+            writeSettingsLocked();
+        }
+    }
+
+    @Override
+    public void forgetVolume(String fsUuid) {
+        enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
+        waitForReady();
+
+        synchronized (mLock) {
+            mRecords.remove(fsUuid);
+            mCallbacks.notifyVolumeMetadataChanged(fsUuid);
+            writeSettingsLocked();
         }
     }
 
@@ -2329,11 +2295,6 @@
 
     @Override
     public VolumeInfo[] getVolumes(int flags) {
-        if ((flags & StorageManager.FLAG_ALL_METADATA) != 0) {
-            // TODO: implement support for returning all metadata
-            throw new UnsupportedOperationException();
-        }
-
         synchronized (mLock) {
             final VolumeInfo[] res = new VolumeInfo[mVolumes.size()];
             for (int i = 0; i < mVolumes.size(); i++) {
@@ -2343,6 +2304,17 @@
         }
     }
 
+    @Override
+    public VolumeRecord[] getVolumeRecords(int flags) {
+        synchronized (mLock) {
+            final VolumeRecord[] res = new VolumeRecord[mRecords.size()];
+            for (int i = 0; i < mRecords.size(); i++) {
+                res[i] = mRecords.valueAt(i);
+            }
+            return res;
+        }
+    }
+
     private void addObbStateLocked(ObbState obbState) throws RemoteException {
         final IBinder binder = obbState.getBinder();
         List<ObbState> obbStates = mObbMounts.get(binder);
@@ -2892,7 +2864,7 @@
                     break;
                 }
                 case MSG_VOLUME_METADATA_CHANGED: {
-                    callback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
+                    callback.onVolumeMetadataChanged((String) args.arg1);
                     break;
                 }
                 case MSG_DISK_SCANNED: {
@@ -2912,21 +2884,21 @@
 
         private void notifyVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
             final SomeArgs args = SomeArgs.obtain();
-            args.arg1 = vol;
+            args.arg1 = vol.clone();
             args.argi2 = oldState;
             args.argi3 = newState;
             obtainMessage(MSG_VOLUME_STATE_CHANGED, args).sendToTarget();
         }
 
-        private void notifyVolumeMetadataChanged(VolumeInfo vol) {
+        private void notifyVolumeMetadataChanged(String fsUuid) {
             final SomeArgs args = SomeArgs.obtain();
-            args.arg1 = vol;
+            args.arg1 = fsUuid;
             obtainMessage(MSG_VOLUME_METADATA_CHANGED, args).sendToTarget();
         }
 
         private void notifyDiskScanned(DiskInfo disk, int volumeCount) {
             final SomeArgs args = SomeArgs.obtain();
-            args.arg1 = disk;
+            args.arg1 = disk.clone();
             args.argi2 = volumeCount;
             obtainMessage(MSG_DISK_SCANNED, args).sendToTarget();
         }
@@ -2937,10 +2909,10 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, TAG);
 
         for (String arg : args) {
-            if ("--clear-metadata".equals(arg)) {
+            if ("--clear".equals(arg)) {
                 synchronized (mLock) {
-                    mMetadata.clear();
-                    writeMetadataLocked();
+                    mRecords.clear();
+                    writeSettingsLocked();
                 }
             }
         }
@@ -2966,11 +2938,11 @@
             pw.decreaseIndent();
 
             pw.println();
-            pw.println("Metadata:");
+            pw.println("Records:");
             pw.increaseIndent();
-            for (int i = 0; i < mMetadata.size(); i++) {
-                final VolumeMetadata meta = mMetadata.valueAt(i);
-                meta.dump(pw);
+            for (int i = 0; i < mRecords.size(); i++) {
+                final VolumeRecord note = mRecords.valueAt(i);
+                note.dump(pw);
             }
             pw.decreaseIndent();
 
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 6c18e25..26f8f70 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -14192,7 +14192,8 @@
             movePackageInternal(packageName, volumeUuid, moveId);
         } catch (PackageManagerException e) {
             Slog.d(TAG, "Failed to move " + packageName, e);
-            mMoveCallbacks.notifyStatusChanged(moveId, PackageManager.MOVE_FAILED_INTERNAL_ERROR);
+            mMoveCallbacks.notifyStatusChanged(moveId, null,
+                    PackageManager.MOVE_FAILED_INTERNAL_ERROR);
         }
         return moveId;
     }
@@ -14209,6 +14210,7 @@
         final String packageAbiOverride;
         final int appId;
         final String seinfo;
+        final String moveTitle;
 
         // reader
         synchronized (mPackages) {
@@ -14228,9 +14230,6 @@
 
             // TODO: yell if already in desired location
 
-            mMoveCallbacks.notifyStarted(moveId,
-                    String.valueOf(pm.getApplicationLabel(pkg.applicationInfo)));
-
             pkg.mOperationPending = true;
 
             currentAsec = pkg.applicationInfo.isForwardLocked()
@@ -14241,6 +14240,7 @@
             packageAbiOverride = ps.cpuAbiOverrideString;
             appId = UserHandle.getAppId(pkg.applicationInfo.uid);
             seinfo = pkg.applicationInfo.seinfo;
+            moveTitle = String.valueOf(pm.getApplicationLabel(pkg.applicationInfo));
         }
 
         int installFlags;
@@ -14268,7 +14268,7 @@
         }
 
         Slog.d(TAG, "Moving " + packageName + " from " + currentVolumeUuid + " to " + volumeUuid);
-        mMoveCallbacks.notifyStatusChanged(moveId, 10, -1);
+        mMoveCallbacks.notifyStatusChanged(moveId, moveTitle, 10);
 
         if (moveData) {
             synchronized (mInstallLock) {
@@ -14288,7 +14288,7 @@
             }
         }
 
-        mMoveCallbacks.notifyStatusChanged(moveId, 50);
+        mMoveCallbacks.notifyStatusChanged(moveId, moveTitle, 50);
 
         final IPackageInstallObserver2 installObserver = new IPackageInstallObserver2.Stub() {
             @Override
@@ -14315,15 +14315,15 @@
                 final int status = PackageManager.installStatusToPublicStatus(returnCode);
                 switch (status) {
                     case PackageInstaller.STATUS_SUCCESS:
-                        mMoveCallbacks.notifyStatusChanged(moveId,
+                        mMoveCallbacks.notifyStatusChanged(moveId, moveTitle,
                                 PackageManager.MOVE_SUCCEEDED);
                         break;
                     case PackageInstaller.STATUS_FAILURE_STORAGE:
-                        mMoveCallbacks.notifyStatusChanged(moveId,
+                        mMoveCallbacks.notifyStatusChanged(moveId, moveTitle,
                                 PackageManager.MOVE_FAILED_INSUFFICIENT_STORAGE);
                         break;
                     default:
-                        mMoveCallbacks.notifyStatusChanged(moveId,
+                        mMoveCallbacks.notifyStatusChanged(moveId, moveTitle,
                                 PackageManager.MOVE_FAILED_INTERNAL_ERROR);
                         break;
                 }
@@ -14346,15 +14346,12 @@
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MOVE_PACKAGE, null);
 
         final int realMoveId = mNextMoveId.getAndIncrement();
+        final String realTitle = null;
+
         final IPackageMoveObserver callback = new IPackageMoveObserver.Stub() {
             @Override
-            public void onStarted(int moveId, String title) {
-                // Ignored
-            }
-
-            @Override
-            public void onStatusChanged(int moveId, int status, long estMillis) {
-                mMoveCallbacks.notifyStatusChanged(realMoveId, status, estMillis);
+            public void onStatusChanged(int moveId, String title, int status, long estMillis) {
+                mMoveCallbacks.notifyStatusChanged(realMoveId, realTitle, status, estMillis);
             }
         };
 
@@ -14709,7 +14706,6 @@
     }
 
     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>
@@ -14747,37 +14743,26 @@
         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);
+                    callback.onStatusChanged(args.argi1, (String) args.arg2, args.argi3,
+                            (long) args.arg4);
                     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, String moveTitle, int status) {
+            notifyStatusChanged(moveId, moveTitle, status, -1);
         }
 
-        private void notifyStatusChanged(int moveId, int status) {
-            notifyStatusChanged(moveId, status, -1);
-        }
-
-        private void notifyStatusChanged(int moveId, int status, long estMillis) {
+        private void notifyStatusChanged(int moveId, String moveTitle, 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;
+            args.arg2 = moveTitle;
+            args.argi3 = status;
+            args.arg4 = estMillis;
             obtainMessage(MSG_STATUS_CHANGED, args).sendToTarget();
 
             synchronized (mLastStatus) {