Persist nickname and flags for volumes.

StorageManager now offers to persist a nickname and user flags for
active volumes.  This metadata is kept around and spliced into
any future VolumeInfo when the same UUID is present.  Current user
flags indicate "initialized" and "snoozed" states to control how
notifications are shown.

Notify listeners when metadata changes, and kick public notification
after a volume is initialized.  Make unique PendingIntents when
multiple volumes are active.

Beginnings of plumbing to ask for missing volumes.

Offer explicit accessors for VolumeInfo and DiskInfo to give better
path to documentation and deprecation.

Bug: 19993667
Change-Id: I3d8b68be83f43ba992d21d51cad5b775776d681c
diff --git a/core/java/android/os/storage/DiskInfo.java b/core/java/android/os/storage/DiskInfo.java
index e6160aa..dc96640 100644
--- a/core/java/android/os/storage/DiskInfo.java
+++ b/core/java/android/os/storage/DiskInfo.java
@@ -16,6 +16,7 @@
 
 package android.os.storage;
 
+import android.annotation.NonNull;
 import android.content.res.Resources;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -59,6 +60,10 @@
         volumeIds = parcel.readStringArray();
     }
 
+    public @NonNull String getId() {
+        return id;
+    }
+
     public String getDescription() {
         // TODO: splice vendor label into these strings
         if ((flags & FLAG_SD) != 0) {
diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java
index 10ffd48..0a8187e 100644
--- a/core/java/android/os/storage/IMountService.java
+++ b/core/java/android/os/storage/IMountService.java
@@ -923,12 +923,13 @@
             }
 
             @Override
-            public VolumeInfo[] getVolumes() throws RemoteException {
+            public VolumeInfo[] getVolumes(int _flags) throws RemoteException {
                 Parcel _data = Parcel.obtain();
                 Parcel _reply = Parcel.obtain();
                 VolumeInfo[] _result;
                 try {
                     _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeInt(_flags);
                     mRemote.transact(Stub.TRANSACTION_getVolumes, _data, _reply, 0);
                     _reply.readException();
                     _result = _reply.createTypedArray(VolumeInfo.CREATOR);
@@ -1029,6 +1030,39 @@
                     _data.recycle();
                 }
             }
+
+            @Override
+            public void setVolumeNickname(String volId, String nickname) throws RemoteException {
+                Parcel _data = Parcel.obtain();
+                Parcel _reply = Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeString(volId);
+                    _data.writeString(nickname);
+                    mRemote.transact(Stub.TRANSACTION_setVolumeNickname, _data, _reply, 0);
+                    _reply.readException();
+                } finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
+
+            @Override
+            public void setVolumeUserFlags(String volId, int flags, int mask) throws RemoteException {
+                Parcel _data = Parcel.obtain();
+                Parcel _reply = Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeString(volId);
+                    _data.writeInt(flags);
+                    _data.writeInt(mask);
+                    mRemote.transact(Stub.TRANSACTION_setVolumeUserFlags, _data, _reply, 0);
+                    _reply.readException();
+                } finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
         }
 
         private static final String DESCRIPTOR = "IMountService";
@@ -1132,6 +1166,9 @@
         static final int TRANSACTION_partitionPrivate = IBinder.FIRST_CALL_TRANSACTION + 50;
         static final int TRANSACTION_partitionMixed = IBinder.FIRST_CALL_TRANSACTION + 51;
 
+        static final int TRANSACTION_setVolumeNickname = IBinder.FIRST_CALL_TRANSACTION + 52;
+        static final int TRANSACTION_setVolumeUserFlags = IBinder.FIRST_CALL_TRANSACTION + 53;
+
         /**
          * Cast an IBinder object into an IMountService interface, generating a
          * proxy if needed.
@@ -1566,7 +1603,8 @@
                 }
                 case TRANSACTION_getVolumes: {
                     data.enforceInterface(DESCRIPTOR);
-                    VolumeInfo[] volumes = getVolumes();
+                    int _flags = data.readInt();
+                    VolumeInfo[] volumes = getVolumes(_flags);
                     reply.writeNoException();
                     reply.writeTypedArray(volumes, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
                     return true;
@@ -1614,6 +1652,23 @@
                     reply.writeNoException();
                     return true;
                 }
+                case TRANSACTION_setVolumeNickname: {
+                    data.enforceInterface(DESCRIPTOR);
+                    String volId = data.readString();
+                    String nickname = data.readString();
+                    setVolumeNickname(volId, nickname);
+                    reply.writeNoException();
+                    return true;
+                }
+                case TRANSACTION_setVolumeUserFlags: {
+                    data.enforceInterface(DESCRIPTOR);
+                    String volId = data.readString();
+                    int _flags = data.readInt();
+                    int _mask = data.readInt();
+                    setVolumeUserFlags(volId, _flags, _mask);
+                    reply.writeNoException();
+                    return true;
+                }
             }
             return super.onTransact(code, data, reply, flags);
         }
@@ -1902,7 +1957,7 @@
     public void waitForAsecScan() throws RemoteException;
 
     public DiskInfo[] getDisks() throws RemoteException;
-    public VolumeInfo[] getVolumes() throws RemoteException;
+    public VolumeInfo[] getVolumes(int flags) throws RemoteException;
 
     public void mount(String volId) throws RemoteException;
     public void unmount(String volId) throws RemoteException;
@@ -1911,4 +1966,7 @@
     public void partitionPublic(String diskId) throws RemoteException;
     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;
 }
diff --git a/core/java/android/os/storage/IMountServiceListener.java b/core/java/android/os/storage/IMountServiceListener.java
index 3965f9d..fd914bc 100644
--- a/core/java/android/os/storage/IMountServiceListener.java
+++ b/core/java/android/os/storage/IMountServiceListener.java
@@ -91,6 +91,13 @@
                     reply.writeNoException();
                     return true;
                 }
+                case TRANSACTION_onVolumeMetadataChanged: {
+                    data.enforceInterface(DESCRIPTOR);
+                    final VolumeInfo vol = (VolumeInfo) data.readParcelable(null);
+                    onVolumeMetadataChanged(vol);
+                    reply.writeNoException();
+                    return true;
+                }
             }
             return super.onTransact(code, data, reply, flags);
         }
@@ -175,6 +182,22 @@
                     _data.recycle();
                 }
             }
+
+            @Override
+            public void onVolumeMetadataChanged(VolumeInfo vol) throws RemoteException {
+                Parcel _data = Parcel.obtain();
+                Parcel _reply = Parcel.obtain();
+                try {
+                    _data.writeInterfaceToken(DESCRIPTOR);
+                    _data.writeParcelable(vol, 0);
+                    mRemote.transact(Stub.TRANSACTION_onVolumeMetadataChanged, _data, _reply,
+                            android.os.IBinder.FLAG_ONEWAY);
+                    _reply.readException();
+                } finally {
+                    _reply.recycle();
+                    _data.recycle();
+                }
+            }
         }
 
         static final int TRANSACTION_onUsbMassStorageConnectionChanged = (IBinder.FIRST_CALL_TRANSACTION + 0);
@@ -182,6 +205,7 @@
         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);
     }
 
     /**
@@ -204,4 +228,6 @@
 
     public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState)
             throws RemoteException;
+
+    public void onVolumeMetadataChanged(VolumeInfo vol) throws RemoteException;
 }
diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java
index 29d5494..28a187d 100644
--- a/core/java/android/os/storage/StorageEventListener.java
+++ b/core/java/android/os/storage/StorageEventListener.java
@@ -40,4 +40,7 @@
 
     public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
     }
+
+    public void onVolumeMetadataChanged(VolumeInfo vol) {
+    }
 }
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index eb77477..0e977ff 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -70,6 +70,9 @@
     /** {@hide} */
     public static final String PROP_PRIMARY_PHYSICAL = "ro.vold.primary_physical";
 
+    /** {@hide} */
+    public static final int FLAG_ALL_METADATA = 1 << 0;
+
     private final Context mContext;
     private final ContentResolver mResolver;
 
@@ -83,6 +86,7 @@
             Handler.Callback {
         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;
 
         final StorageEventListener mCallback;
         final Handler mHandler;
@@ -105,6 +109,10 @@
                     mCallback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3);
                     args.recycle();
                     return true;
+                case MSG_VOLUME_METADATA_CHANGED:
+                    mCallback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
+                    args.recycle();
+                    return true;
             }
             args.recycle();
             return false;
@@ -132,6 +140,13 @@
             args.argi3 = newState;
             mHandler.obtainMessage(MSG_VOLUME_STATE_CHANGED, args).sendToTarget();
         }
+
+        @Override
+        public void onVolumeMetadataChanged(VolumeInfo vol) {
+            final SomeArgs args = SomeArgs.obtain();
+            args.arg1 = vol;
+            mHandler.obtainMessage(MSG_VOLUME_METADATA_CHANGED, args).sendToTarget();
+        }
     }
 
     /**
@@ -480,8 +495,13 @@
 
     /** {@hide} */
     public @NonNull List<VolumeInfo> getVolumes() {
+        return getVolumes(0);
+    }
+
+    /** {@hide} */
+    public @NonNull List<VolumeInfo> getVolumes(int flags) {
         try {
-            return Arrays.asList(mMountService.getVolumes());
+            return Arrays.asList(mMountService.getVolumes(flags));
         } catch (RemoteException e) {
             throw e.rethrowAsRuntimeException();
         }
@@ -556,6 +576,35 @@
     }
 
     /** {@hide} */
+    public void setVolumeNickname(String volId, String nickname) {
+        try {
+            mMountService.setVolumeNickname(volId, nickname);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /** {@hide} */
+    public void setVolumeInited(String volId, boolean inited) {
+        try {
+            mMountService.setVolumeUserFlags(volId, inited ? VolumeInfo.USER_FLAG_INITED : 0,
+                    VolumeInfo.USER_FLAG_INITED);
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /** {@hide} */
+    public void setVolumeSnoozed(String volId, boolean snoozed) {
+        try {
+            mMountService.setVolumeUserFlags(volId, snoozed ? VolumeInfo.USER_FLAG_SNOOZED : 0,
+                    VolumeInfo.USER_FLAG_SNOOZED);
+        } 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 fe1e206..f06fc8c 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -71,6 +71,9 @@
     public static final int FLAG_PRIMARY = 1 << 0;
     public static final int 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<>();
 
@@ -104,8 +107,9 @@
 
     /** Framework state */
     public final int mtpIndex;
-    public String nickname;
     public String diskId;
+    public String nickname;
+    public int userFlags = 0;
 
     public VolumeInfo(String id, int type, int mtpIndex) {
         this.id = Preconditions.checkNotNull(id);
@@ -124,8 +128,9 @@
         fsLabel = parcel.readString();
         path = parcel.readString();
         mtpIndex = parcel.readInt();
-        nickname = parcel.readString();
         diskId = parcel.readString();
+        nickname = parcel.readString();
+        userFlags = parcel.readInt();
     }
 
     public static @NonNull String getEnvironmentForState(int state) {
@@ -145,6 +150,30 @@
         return getBroadcastForEnvironment(getEnvironmentForState(state));
     }
 
+    public @NonNull String getId() {
+        return id;
+    }
+
+    public @Nullable String getDiskId() {
+        return diskId;
+    }
+
+    public int getType() {
+        return type;
+    }
+
+    public int getState() {
+        return state;
+    }
+
+    public @Nullable String getFsUuid() {
+        return fsUuid;
+    }
+
+    public @Nullable String getNickname() {
+        return nickname;
+    }
+
     public @Nullable String getDescription() {
         if (ID_PRIVATE_INTERNAL.equals(id)) {
             return Resources.getSystem().getString(com.android.internal.R.string.storage_internal);
@@ -165,6 +194,14 @@
         return (flags & 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.userId) {
             return isVisible();
@@ -175,6 +212,10 @@
         }
     }
 
+    public File getPath() {
+        return new File(path);
+    }
+
     public File getPathForUser(int userId) {
         if (path == null) {
             return null;
@@ -284,8 +325,9 @@
         pw.println();
         pw.printPair("path", path);
         pw.printPair("mtpIndex", mtpIndex);
-        pw.printPair("nickname", nickname);
         pw.printPair("diskId", diskId);
+        pw.printPair("nickname", nickname);
+        pw.printPair("userFlags", DebugUtils.flagsToString(getClass(), "USER_FLAG_", userFlags));
         pw.decreaseIndent();
         pw.println();
     }
@@ -331,7 +373,8 @@
         parcel.writeString(fsLabel);
         parcel.writeString(path);
         parcel.writeInt(mtpIndex);
-        parcel.writeString(nickname);
         parcel.writeString(diskId);
+        parcel.writeString(nickname);
+        parcel.writeInt(userFlags);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
index 6830957..818f5ee 100644
--- a/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
+++ b/packages/SystemUI/src/com/android/systemui/usb/StorageNotification.java
@@ -20,7 +20,10 @@
 import android.app.Notification.Action;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.UserHandle;
 import android.os.storage.DiskInfo;
 import android.os.storage.StorageEventListener;
@@ -38,6 +41,8 @@
 
     private static final int NOTIF_ID = 0x53544f52; // STOR
 
+    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
 
@@ -49,6 +54,25 @@
         public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
             onVolumeStateChangedInternal(vol, oldState, newState);
         }
+
+        @Override
+        public void onVolumeMetadataChanged(VolumeInfo vol) {
+            // 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.getState() == VolumeInfo.STATE_MOUNTED) {
+                onVolumeStateChangedInternal(vol, vol.getState(), vol.getState());
+            }
+        }
+    };
+
+    private final BroadcastReceiver mSnoozeReceiver = new BroadcastReceiver() {
+        @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);
+        }
     };
 
     @Override
@@ -58,23 +82,26 @@
         mStorageManager = mContext.getSystemService(StorageManager.class);
         mStorageManager.registerListener(mListener);
 
+        mContext.registerReceiver(mSnoozeReceiver, new IntentFilter(ACTION_SNOOZE_VOLUME),
+                android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS, null);
+
         // Kick current state into place
         final List<VolumeInfo> vols = mStorageManager.getVolumes();
         for (VolumeInfo vol : vols) {
-            onVolumeStateChangedInternal(vol, vol.state, vol.state);
+            onVolumeStateChangedInternal(vol, vol.getState(), vol.getState());
         }
     }
 
     public void onVolumeStateChangedInternal(VolumeInfo vol, int oldState, int newState) {
         // We only care about public volumes
-        if (vol.type != VolumeInfo.TYPE_PUBLIC) {
+        if (vol.getType() != VolumeInfo.TYPE_PUBLIC) {
             return;
         }
 
         Log.d(TAG, vol.toString());
 
         // New state means we tear down any old notifications
-        mNotificationManager.cancelAsUser(vol.id, NOTIF_ID, UserHandle.ALL);
+        mNotificationManager.cancelAsUser(vol.getId(), NOTIF_ID, UserHandle.ALL);
 
         switch (newState) {
             case VolumeInfo.STATE_UNMOUNTED:
@@ -106,7 +133,7 @@
     }
 
     private void onVolumeMounting(VolumeInfo vol) {
-        final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
         final CharSequence title = mContext.getString(
                 R.string.ext_media_checking_notification_title, disk.getDescription());
         final CharSequence text = mContext.getString(
@@ -119,13 +146,16 @@
                 .setOngoing(true)
                 .build();
 
-        mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+        mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
     }
 
     private void onVolumeMounted(VolumeInfo vol) {
-        final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+        // Don't annoy when user dismissed in past
+        if (vol.isSnoozed()) return;
+
+        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
         final Notification notif;
-        if (disk.isAdoptable()) {
+        if (disk.isAdoptable() && !vol.isInited()) {
             final CharSequence title = disk.getDescription();
             final CharSequence text = mContext.getString(
                     R.string.ext_media_new_notification_message, disk.getDescription());
@@ -136,6 +166,7 @@
                             buildInitPendingIntent(vol)))
                     .addAction(new Action(0, mContext.getString(R.string.ext_media_unmount_action),
                             buildUnmountPendingIntent(vol)))
+                    .setDeleteIntent(buildSnoozeIntent(vol))
                     .setCategory(Notification.CATEGORY_SYSTEM)
                     .build();
 
@@ -150,12 +181,13 @@
                             buildBrowsePendingIntent(vol)))
                     .addAction(new Action(0, mContext.getString(R.string.ext_media_unmount_action),
                             buildUnmountPendingIntent(vol)))
+                    .setDeleteIntent(buildSnoozeIntent(vol))
                     .setCategory(Notification.CATEGORY_SYSTEM)
                     .setPriority(Notification.PRIORITY_LOW)
                     .build();
         }
 
-        mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+        mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
     }
 
     private void onVolumeFormatting(VolumeInfo vol) {
@@ -163,7 +195,7 @@
     }
 
     private void onVolumeUnmounting(VolumeInfo vol) {
-        final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
         final CharSequence title = mContext.getString(
                 R.string.ext_media_unmounting_notification_title, disk.getDescription());
         final CharSequence text = mContext.getString(
@@ -176,11 +208,11 @@
                 .setOngoing(true)
                 .build();
 
-        mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+        mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
     }
 
     private void onVolumeUnmountable(VolumeInfo vol) {
-        final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
         final CharSequence title = mContext.getString(
                 R.string.ext_media_unmountable_notification_title, disk.getDescription());
         final CharSequence text = mContext.getString(
@@ -192,7 +224,7 @@
                 .setCategory(Notification.CATEGORY_ERROR)
                 .build();
 
-        mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+        mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
     }
 
     private void onVolumeRemoved(VolumeInfo vol) {
@@ -201,7 +233,7 @@
             return;
         }
 
-        final DiskInfo disk = mStorageManager.findDiskById(vol.diskId);
+        final DiskInfo disk = mStorageManager.findDiskById(vol.getDiskId());
         final CharSequence title = mContext.getString(
                 R.string.ext_media_nomedia_notification_title, disk.getDescription());
         final CharSequence text = mContext.getString(
@@ -212,7 +244,7 @@
                 .setCategory(Notification.CATEGORY_ERROR)
                 .build();
 
-        mNotificationManager.notifyAsUser(vol.id, NOTIF_ID, notif, UserHandle.ALL);
+        mNotificationManager.notifyAsUser(vol.getId(), NOTIF_ID, notif, UserHandle.ALL);
     }
 
     private Notification.Builder buildNotificationBuilder(CharSequence title, CharSequence text) {
@@ -229,28 +261,49 @@
         final Intent intent = new Intent();
         intent.setClassName("com.android.settings",
                 "com.android.settings.deviceinfo.StorageWizardInit");
-        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
-        return PendingIntent.getActivityAsUser(mContext, 0, intent, 0, null, UserHandle.CURRENT);
+        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+        final int requestKey = vol.getId().hashCode();
+        return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
     }
 
     private PendingIntent buildUnmountPendingIntent(VolumeInfo vol) {
         final Intent intent = new Intent();
         intent.setClassName("com.android.settings",
                 "com.android.settings.deviceinfo.StorageUnmountReceiver");
-        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
-        return PendingIntent.getBroadcastAsUser(mContext, 0, intent, 0, UserHandle.CURRENT);
+        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+        final int requestKey = vol.getId().hashCode();
+        return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT);
     }
 
     private PendingIntent buildBrowsePendingIntent(VolumeInfo vol) {
         final Intent intent = vol.buildBrowseIntent();
-        return PendingIntent.getActivityAsUser(mContext, 0, intent, 0, null, UserHandle.CURRENT);
+
+        final int requestKey = vol.getId().hashCode();
+        return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
     }
 
     private PendingIntent buildDetailsPendingIntent(VolumeInfo vol) {
         final Intent intent = new Intent();
         intent.setClassName("com.android.settings",
                 "com.android.settings.Settings$StorageVolumeSettingsActivity");
-        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.id);
-        return PendingIntent.getActivityAsUser(mContext, 0, intent, 0, null, UserHandle.CURRENT);
+        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+        final int requestKey = vol.getId().hashCode();
+        return PendingIntent.getActivityAsUser(mContext, requestKey, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
+    }
+
+    private PendingIntent buildSnoozeIntent(VolumeInfo vol) {
+        final Intent intent = new Intent(ACTION_SNOOZE_VOLUME);
+        intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
+
+        final int requestKey = vol.getId().hashCode();
+        return PendingIntent.getBroadcastAsUser(mContext, requestKey, intent,
+                PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.CURRENT);
     }
 }
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index a99f387..4c937f7 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -16,6 +16,13 @@
 
 package com.android.server;
 
+import static com.android.internal.util.XmlUtils.readIntAttribute;
+import static com.android.internal.util.XmlUtils.readStringAttribute;
+import static com.android.internal.util.XmlUtils.writeIntAttribute;
+import static com.android.internal.util.XmlUtils.writeStringAttribute;
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
+
 import android.Manifest;
 import android.app.ActivityManagerNative;
 import android.app.AppOpsManager;
@@ -55,9 +62,13 @@
 import android.os.storage.VolumeInfo;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.AtomicFile;
+import android.util.DebugUtils;
 import android.util.Log;
 import android.util.Slog;
+import android.util.Xml;
 
+import libcore.io.IoUtils;
 import libcore.util.EmptyArray;
 import libcore.util.HexEncoding;
 
@@ -66,14 +77,21 @@
 import com.android.internal.app.IMediaContainerService;
 import com.android.internal.os.SomeArgs;
 import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 import com.android.server.NativeDaemonConnector.Command;
 import com.android.server.NativeDaemonConnector.SensitiveArg;
 import com.android.server.pm.PackageManagerService;
 
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
 import java.io.File;
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
@@ -214,6 +232,57 @@
         public static final int FstrimCompleted                = 700;
     }
 
+    private static final String TAG_VOLUMES = "volumes";
+    private static final String TAG_VOLUME = "volume";
+    private static final String ATTR_TYPE = "type";
+    private static final String ATTR_FS_UUID = "fsUuid";
+    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();
+        }
+    }
+
     /**
      * <em>Never</em> hold the lock while performing downcalls into vold, since
      * unsolicited events can suddenly appear to update data structures.
@@ -222,11 +291,18 @@
 
     @GuardedBy("mLock")
     private int[] mStartedUsers = EmptyArray.INT;
+
+    /** Map from disk ID to disk */
     @GuardedBy("mLock")
     private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>();
+    /** Map from volume ID to disk */
     @GuardedBy("mLock")
     private ArrayMap<String, VolumeInfo> mVolumes = new ArrayMap<>();
 
+    /** Map from UUID to metadata */
+    @GuardedBy("mLock")
+    private ArrayMap<String, VolumeMetadata> mMetadata = new ArrayMap<>();
+
     private DiskInfo findDiskById(String id) {
         synchronized (mLock) {
             final DiskInfo disk = mDisks.get(id);
@@ -260,6 +336,15 @@
         throw new IllegalArgumentException("No volume found for path " + path);
     }
 
+    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 static int sNextMtpIndex = 1;
 
     private static int allocateMtpIndex(String volId) {
@@ -799,6 +884,7 @@
                 if (vol != null) {
                     vol.fsType = cooked[2];
                 }
+                mCallbacks.notifyVolumeMetadataChanged(vol.clone());
                 break;
             }
             case VoldResponseCode.VOLUME_FS_UUID_CHANGED: {
@@ -807,6 +893,8 @@
                 if (vol != null) {
                     vol.fsUuid = cooked[2];
                 }
+                refreshMetadataLocked();
+                mCallbacks.notifyVolumeMetadataChanged(vol.clone());
                 break;
             }
             case VoldResponseCode.VOLUME_FS_LABEL_CHANGED: {
@@ -815,6 +903,7 @@
                 if (vol != null) {
                     vol.fsLabel = cooked[2];
                 }
+                mCallbacks.notifyVolumeMetadataChanged(vol.clone());
                 break;
             }
             case VoldResponseCode.VOLUME_PATH_CHANGED: {
@@ -901,6 +990,25 @@
         }
     }
 
+    /**
+     * 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);
     }
@@ -949,6 +1057,13 @@
             mLastMaintenance = mLastMaintenanceFile.lastModified();
         }
 
+        mMetadataFile = new AtomicFile(
+                new File(Environment.getSystemSecureDirectory(), "storage.xml"));
+
+        synchronized (mLock) {
+            readMetadataLocked();
+        }
+
         /*
          * Create the connection to vold with a maximum queue of twice the
          * amount of containers we'd ever expect to have. This keeps an
@@ -972,6 +1087,61 @@
         mHandler.obtainMessage(H_SYSTEM_READY).sendToTarget();
     }
 
+    private void readMetadataLocked() {
+        mMetadata.clear();
+
+        FileInputStream fis = null;
+        try {
+            fis = mMetadataFile.openRead();
+            final XmlPullParser in = Xml.newPullParser();
+            in.setInput(fis, null);
+
+            int type;
+            while ((type = in.next()) != END_DOCUMENT) {
+                if (type == START_TAG) {
+                    final String tag = in.getName();
+                    if (TAG_VOLUME.equals(tag)) {
+                        final VolumeMetadata meta = VolumeMetadata.read(in);
+                        mMetadata.put(meta.fsUuid, meta);
+                    }
+                }
+            }
+        } catch (FileNotFoundException e) {
+            // Missing metadata is okay, probably first boot
+        } catch (IOException e) {
+            Slog.wtf(TAG, "Failed reading metadata", e);
+        } catch (XmlPullParserException e) {
+            Slog.wtf(TAG, "Failed reading metadata", e);
+        } finally {
+            IoUtils.closeQuietly(fis);
+        }
+    }
+
+    private void writeMetadataLocked() {
+        FileOutputStream fos = null;
+        try {
+            fos = mMetadataFile.startWrite();
+
+            XmlSerializer out = new FastXmlSerializer();
+            out.setOutput(fos, "utf-8");
+            out.startDocument(null, true);
+            out.startTag(null, TAG_VOLUMES);
+            final int size = mMetadata.size();
+            for (int i = 0; i < size; i++) {
+                final VolumeMetadata meta = mMetadata.valueAt(i);
+                VolumeMetadata.write(out, meta);
+            }
+            out.endTag(null, TAG_VOLUMES);
+            out.endDocument();
+
+            mMetadataFile.finishWrite(fos);
+        } catch (IOException e) {
+            if (fos != null) {
+                mMetadataFile.failWrite(fos);
+            }
+        }
+    }
+
     /**
      * Exposed API calls below here
      */
@@ -1126,6 +1296,36 @@
     }
 
     @Override
+    public void setVolumeNickname(String volId, 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());
+        }
+    }
+
+    @Override
+    public void setVolumeUserFlags(String volId, 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());
+        }
+    }
+
+    @Override
     public int[] getStorageUsers(String path) {
         enforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS);
         waitForReady();
@@ -1909,7 +2109,12 @@
     }
 
     @Override
-    public VolumeInfo[] getVolumes() {
+    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++) {
@@ -2422,6 +2627,7 @@
     private static class Callbacks extends Handler {
         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 final RemoteCallbackList<IMountServiceListener>
                 mCallbacks = new RemoteCallbackList<>();
@@ -2465,6 +2671,10 @@
                     callback.onVolumeStateChanged((VolumeInfo) args.arg1, args.argi2, args.argi3);
                     break;
                 }
+                case MSG_VOLUME_METADATA_CHANGED: {
+                    callback.onVolumeMetadataChanged((VolumeInfo) args.arg1);
+                    break;
+                }
             }
         }
 
@@ -2483,6 +2693,12 @@
             args.argi3 = newState;
             obtainMessage(MSG_VOLUME_STATE_CHANGED, args).sendToTarget();
         }
+
+        private void notifyVolumeMetadataChanged(VolumeInfo vol) {
+            final SomeArgs args = SomeArgs.obtain();
+            args.arg1 = vol;
+            obtainMessage(MSG_VOLUME_METADATA_CHANGED, args).sendToTarget();
+        }
     }
 
     @Override
@@ -2539,6 +2755,15 @@
                 vol.dump(pw);
             }
             pw.decreaseIndent();
+
+            pw.println();
+            pw.println("Metadata:");
+            pw.increaseIndent();
+            for (int i = 0; i < mMetadata.size(); i++) {
+                final VolumeMetadata meta = mMetadata.valueAt(i);
+                meta.dump(pw);
+            }
+            pw.decreaseIndent();
         }
 
         pw.println();