Merge commit '1db64c19' into merge3

Change-Id: I0aea6817876a5820a7d67a4de5bef0f86ce702a2
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 538007a..49386f9 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -4448,22 +4448,6 @@
     public abstract @NonNull PackageInstaller getPackageInstaller();
 
     /**
-     * Returns the data directory for a particular package and user.
-     *
-     * @hide
-     */
-    public static File getDataDirForUser(String volumeUuid, String packageName, int userId) {
-        // TODO: This should be shared with Installer's knowledge of user directory
-        final File base;
-        if (TextUtils.isEmpty(volumeUuid)) {
-            base = Environment.getDataDirectory();
-        } else {
-            base = new File("/mnt/expand/" + volumeUuid);
-        }
-        return new File(base, "user/" + userId + "/" + packageName);
-    }
-
-    /**
      * Adds a {@link CrossProfileIntentFilter}. After calling this method all intents sent from the
      * user with id sourceUserId can also be be resolved by activities in the user with id
      * targetUserId if they match the specified intent filter.
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 64376c1..48ffb98 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -36,6 +36,7 @@
 import android.content.res.XmlResourceParser;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.Environment;
 import android.os.FileUtils;
 import android.os.PatternMatcher;
 import android.os.UserHandle;
@@ -4785,7 +4786,7 @@
         // Make shallow copy so we can store the metadata/libraries safely
         ApplicationInfo ai = new ApplicationInfo(p.applicationInfo);
         ai.uid = UserHandle.getUid(userId, ai.uid);
-        ai.dataDir = PackageManager.getDataDirForUser(ai.volumeUuid, ai.packageName, userId)
+        ai.dataDir = Environment.getDataUserPackageDirectory(ai.volumeUuid, userId, ai.packageName)
                 .getAbsolutePath();
         if ((flags & PackageManager.GET_META_DATA) != 0) {
             ai.metaData = p.mAppMetaData;
@@ -4812,7 +4813,7 @@
         // make a copy.
         ai = new ApplicationInfo(ai);
         ai.uid = UserHandle.getUid(userId, ai.uid);
-        ai.dataDir = PackageManager.getDataDirForUser(ai.volumeUuid, ai.packageName, userId)
+        ai.dataDir = Environment.getDataUserPackageDirectory(ai.volumeUuid, userId, ai.packageName)
                 .getAbsolutePath();
         if (state.stopped) {
             ai.flags |= ApplicationInfo.FLAG_STOPPED;
diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java
index 8e0584a..2080856 100644
--- a/core/java/android/os/Environment.java
+++ b/core/java/android/os/Environment.java
@@ -244,14 +244,36 @@
     }
 
     /** {@hide} */
-    public static File getDataAppDirectory(String volumeUuid) {
+    public static File getDataDirectory(String volumeUuid) {
         if (TextUtils.isEmpty(volumeUuid)) {
-            return new File("/data/app");
+            return new File("/data");
         } else {
-            return new File("/mnt/expand/" + volumeUuid + "/app");
+            return new File("/mnt/expand/" + volumeUuid);
         }
     }
 
+    /** {@hide} */
+    public static File getDataAppDirectory(String volumeUuid) {
+        return new File(getDataDirectory(volumeUuid), "app");
+    }
+
+    /** {@hide} */
+    public static File getDataUserDirectory(String volumeUuid) {
+        return new File(getDataDirectory(volumeUuid), "user");
+    }
+
+    /** {@hide} */
+    public static File getDataUserDirectory(String volumeUuid, int userId) {
+        return new File(getDataUserDirectory(volumeUuid), String.valueOf(userId));
+    }
+
+    /** {@hide} */
+    public static File getDataUserPackageDirectory(String volumeUuid, int userId,
+            String packageName) {
+        // TODO: keep consistent with installd
+        return new File(getDataUserDirectory(volumeUuid, userId), packageName);
+    }
+
     /**
      * Return the primary external storage directory. This directory may not
      * currently be accessible if it has been mounted by the user on their
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 4e91093..7f40421 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -586,6 +586,21 @@
     }
 
     /** {@hide} */
+    public @NonNull List<VolumeInfo> getWritablePrivateVolumes() {
+        try {
+            final ArrayList<VolumeInfo> res = new ArrayList<>();
+            for (VolumeInfo vol : mMountService.getVolumes(0)) {
+                if (vol.getType() == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
+                    res.add(vol);
+                }
+            }
+            return res;
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /** {@hide} */
     public @NonNull List<VolumeRecord> getVolumeRecords() {
         try {
             return Arrays.asList(mMountService.getVolumeRecords(0));
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index f1750bc..b318cf9 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -2888,7 +2888,8 @@
                 Slog.i(TAG, "Trying to bind to DefaultContainerService");
 
             Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT);
-            if (mContext.bindService(service, mDefContainerConn, Context.BIND_AUTO_CREATE)) {
+            if (mContext.bindServiceAsUser(service, mDefContainerConn, Context.BIND_AUTO_CREATE,
+                    UserHandle.OWNER)) {
                 mBound = true;
                 return true;
             }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 96300f2..c50efdc 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20395,8 +20395,9 @@
         if (info == null) return null;
         ApplicationInfo newInfo = new ApplicationInfo(info);
         newInfo.uid = applyUserId(info.uid, userId);
-        newInfo.dataDir = PackageManager.getDataDirForUser(info.volumeUuid, info.packageName,
-                userId).getAbsolutePath();
+        newInfo.dataDir = Environment
+                .getDataUserPackageDirectory(info.volumeUuid, userId, info.packageName)
+                .getAbsolutePath();
         return newInfo;
     }
 
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index ca24e3a..2abd924 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -219,29 +219,17 @@
         synchronized (mSessions) {
             readSessionsLocked();
 
-            final File internalStagingDir = buildInternalStagingDir();
-            final ArraySet<File> unclaimedStages = Sets.newArraySet(
-                    internalStagingDir.listFiles(sStageFilter));
+            reconcileStagesLocked(StorageManager.UUID_PRIVATE_INTERNAL);
+
             final ArraySet<File> unclaimedIcons = Sets.newArraySet(
                     mSessionsDir.listFiles());
 
             // Ignore stages and icons claimed by active sessions
             for (int i = 0; i < mSessions.size(); i++) {
                 final PackageInstallerSession session = mSessions.valueAt(i);
-                unclaimedStages.remove(session.stageDir);
                 unclaimedIcons.remove(buildAppIconFile(session.sessionId));
             }
 
-            // Clean up orphaned staging directories
-            for (File stage : unclaimedStages) {
-                Slog.w(TAG, "Deleting orphan stage " + stage);
-                if (stage.isDirectory()) {
-                    mPm.mInstaller.rmPackageDir(stage.getAbsolutePath());
-                } else {
-                    stage.delete();
-                }
-            }
-
             // Clean up orphaned icons
             for (File icon : unclaimedIcons) {
                 Slog.w(TAG, "Deleting orphan icon " + icon);
@@ -255,6 +243,36 @@
         mStorage = mContext.getSystemService(StorageManager.class);
     }
 
+    private void reconcileStagesLocked(String volumeUuid) {
+        final File stagingDir = buildStagingDir(volumeUuid);
+        final ArraySet<File> unclaimedStages = Sets.newArraySet(
+                stagingDir.listFiles(sStageFilter));
+
+        // Ignore stages claimed by active sessions
+        for (int i = 0; i < mSessions.size(); i++) {
+            final PackageInstallerSession session = mSessions.valueAt(i);
+            unclaimedStages.remove(session.stageDir);
+        }
+
+        // Clean up orphaned staging directories
+        for (File stage : unclaimedStages) {
+            Slog.w(TAG, "Deleting orphan stage " + stage);
+            synchronized (mPm.mInstallLock) {
+                if (stage.isDirectory()) {
+                    mPm.mInstaller.rmPackageDir(stage.getAbsolutePath());
+                } else {
+                    stage.delete();
+                }
+            }
+        }
+    }
+
+    public void onPrivateVolumeMounted(String volumeUuid) {
+        synchronized (mSessions) {
+            reconcileStagesLocked(volumeUuid);
+        }
+    }
+
     public void onSecureContainersAvailable() {
         synchronized (mSessions) {
             final ArraySet<String> unclaimed = new ArraySet<>();
@@ -713,25 +731,11 @@
         throw new IllegalStateException("Failed to allocate session ID");
     }
 
-    private File buildInternalStagingDir() {
-        return new File(Environment.getDataDirectory(), "app");
+    private File buildStagingDir(String volumeUuid) {
+        return Environment.getDataAppDirectory(volumeUuid);
     }
 
-    private File buildStagingDir(String volumeUuid) throws FileNotFoundException {
-        if (volumeUuid == null) {
-            return buildInternalStagingDir();
-        } else {
-            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
-            if (vol != null && vol.type == VolumeInfo.TYPE_PRIVATE
-                    && vol.isMountedWritable()) {
-                return new File(vol.path, "app");
-            } else {
-                throw new FileNotFoundException("Failed to find volume for UUID " + volumeUuid);
-            }
-        }
-    }
-
-    private File buildStageDir(String volumeUuid, int sessionId) throws FileNotFoundException {
+    private File buildStageDir(String volumeUuid, int sessionId) {
         final File stagingDir = buildStagingDir(volumeUuid);
         return new File(stagingDir, "vmdl" + sessionId + ".tmp");
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index a4b3d09..9ec9c43 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -191,6 +191,7 @@
 import libcore.util.EmptyArray;
 
 import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IMediaContainerService;
 import com.android.internal.app.ResolverActivity;
 import com.android.internal.content.NativeLibraryHelper;
@@ -428,6 +429,7 @@
 
     // Used for privilege escalation. MUST NOT BE CALLED WITH mPackages
     // LOCK HELD.  Can be called with mInstallLock held.
+    @GuardedBy("mInstallLock")
     final Installer mInstaller;
 
     /** Directory where installed third-party apps stored */
@@ -455,6 +457,7 @@
     // Keys are String (package name), values are Package.  This also serves
     // as the lock for the global state.  Methods that must be called with
     // this lock held have the prefix "LP".
+    @GuardedBy("mPackages")
     final ArrayMap<String, PackageParser.Package> mPackages =
             new ArrayMap<String, PackageParser.Package>();
 
@@ -1605,9 +1608,19 @@
         public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) {
             if (vol.type == VolumeInfo.TYPE_PRIVATE) {
                 if (vol.state == VolumeInfo.STATE_MOUNTED) {
-                    // TODO: ensure that private directories exist for all active users
-                    // TODO: remove user data whose serial number doesn't match
+                    final String volumeUuid = vol.getFsUuid();
+
+                    // Clean up any users or apps that were removed or recreated
+                    // while this volume was missing
+                    reconcileUsers(volumeUuid);
+                    reconcileApps(volumeUuid);
+
+                    // Clean up any install sessions that expired or were
+                    // cancelled while this volume was missing
+                    mInstallerService.onPrivateVolumeMounted(volumeUuid);
+
                     loadPrivatePackages(vol);
+
                 } else if (vol.state == VolumeInfo.STATE_EJECTING) {
                     unloadPrivatePackages(vol);
                 }
@@ -1624,7 +1637,17 @@
 
         @Override
         public void onVolumeForgotten(String fsUuid) {
-            // TODO: remove all packages hosted on this uuid
+            // Remove any apps installed on the forgotten volume
+            synchronized (mPackages) {
+                final List<PackageSetting> packages = mSettings.getVolumePackagesLPr(fsUuid);
+                for (PackageSetting ps : packages) {
+                    Slog.d(TAG, "Destroying " + ps.name + " because volume was forgotten");
+                    deletePackage(ps.name, new LegacyPackageDeleteObserver(null).getBinder(),
+                            UserHandle.USER_OWNER, PackageManager.DELETE_ALL_USERS);
+                }
+
+                mSettings.writeLPr();
+            }
         }
     };
 
@@ -2770,8 +2793,9 @@
                 pkg.applicationInfo.packageName = packageName;
                 pkg.applicationInfo.flags = ps.pkgFlags | ApplicationInfo.FLAG_IS_DATA_ONLY;
                 pkg.applicationInfo.privateFlags = ps.pkgPrivateFlags;
-                pkg.applicationInfo.dataDir = PackageManager.getDataDirForUser(ps.volumeUuid,
-                        packageName, userId).getAbsolutePath();
+                pkg.applicationInfo.dataDir = Environment
+                        .getDataUserPackageDirectory(ps.volumeUuid, userId, packageName)
+                        .getAbsolutePath();
                 pkg.applicationInfo.primaryCpuAbi = ps.primaryCpuAbiString;
                 pkg.applicationInfo.secondaryCpuAbi = ps.secondaryCpuAbiString;
             }
@@ -6615,8 +6639,8 @@
 
         } else {
             // This is a normal package, need to make its data directory.
-            dataPath = PackageManager.getDataDirForUser(pkg.volumeUuid, pkg.packageName,
-                    UserHandle.USER_OWNER);
+            dataPath = Environment.getDataUserPackageDirectory(pkg.volumeUuid,
+                    UserHandle.USER_OWNER, pkg.packageName);
 
             boolean uidError = false;
             if (dataPath.exists()) {
@@ -6770,6 +6794,18 @@
         if (DEBUG_INSTALL) Slog.i(TAG, "Linking native library dir for " + path);
         final int[] userIds = sUserManager.getUserIds();
         synchronized (mInstallLock) {
+            // Make sure all user data directories are ready to roll; we're okay
+            // if they already exist
+            if (!TextUtils.isEmpty(pkg.volumeUuid)) {
+                for (int userId : userIds) {
+                    if (userId != 0) {
+                        mInstaller.createUserData(pkg.volumeUuid, pkg.packageName,
+                                UserHandle.getUid(userId, pkg.applicationInfo.uid), userId,
+                                pkg.applicationInfo.seinfo);
+                    }
+                }
+            }
+
             // Create a native library symlink only if we have native libraries
             // and if the native libraries are 32 bit libraries. We do not provide
             // this symlink for 64 bit libraries.
@@ -11441,8 +11477,8 @@
         String pkgName = pkg.packageName;
 
         if (DEBUG_INSTALL) Slog.d(TAG, "installNewPackageLI: " + pkg);
-        final boolean dataDirExists = PackageManager.getDataDirForUser(volumeUuid, pkgName,
-                UserHandle.USER_OWNER).exists();
+        final boolean dataDirExists = Environment
+                .getDataUserPackageDirectory(volumeUuid, UserHandle.USER_OWNER, pkgName).exists();
         synchronized(mPackages) {
             if (mSettings.mRenamedPackages.containsKey(pkgName)) {
                 // A package with the same name is already installed, though
@@ -12299,6 +12335,8 @@
             final IPackageDeleteObserver2 observer, final int userId, final int flags) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.DELETE_PACKAGES, null);
+        Preconditions.checkNotNull(packageName);
+        Preconditions.checkNotNull(observer);
         final int uid = Binder.getCallingUid();
         if (UserHandle.getUserId(uid) != userId) {
             mContext.enforceCallingPermission(
@@ -15237,6 +15275,127 @@
         sendResourcesChangedBroadcast(false, false, unloaded, null);
     }
 
+    /**
+     * Examine all users present on given mounted volume, and destroy data
+     * belonging to users that are no longer valid, or whose user ID has been
+     * recycled.
+     */
+    private void reconcileUsers(String volumeUuid) {
+        final File[] files = Environment.getDataUserDirectory(volumeUuid).listFiles();
+        if (ArrayUtils.isEmpty(files)) {
+            Slog.d(TAG, "No users found on " + volumeUuid);
+            return;
+        }
+
+        for (File file : files) {
+            if (!file.isDirectory()) continue;
+
+            final int userId;
+            final UserInfo info;
+            try {
+                userId = Integer.parseInt(file.getName());
+                info = sUserManager.getUserInfo(userId);
+            } catch (NumberFormatException e) {
+                Slog.w(TAG, "Invalid user directory " + file);
+                continue;
+            }
+
+            boolean destroyUser = false;
+            if (info == null) {
+                logCriticalInfo(Log.WARN, "Destroying user directory " + file
+                        + " because no matching user was found");
+                destroyUser = true;
+            } else {
+                try {
+                    UserManagerService.enforceSerialNumber(file, info.serialNumber);
+                } catch (IOException e) {
+                    logCriticalInfo(Log.WARN, "Destroying user directory " + file
+                            + " because we failed to enforce serial number: " + e);
+                    destroyUser = true;
+                }
+            }
+
+            if (destroyUser) {
+                synchronized (mInstallLock) {
+                    mInstaller.removeUserDataDirs(volumeUuid, userId);
+                }
+            }
+        }
+
+        final UserManager um = mContext.getSystemService(UserManager.class);
+        for (UserInfo user : um.getUsers()) {
+            final File userDir = Environment.getDataUserDirectory(volumeUuid, user.id);
+            if (userDir.exists()) continue;
+
+            try {
+                UserManagerService.prepareUserDirectory(mContext, volumeUuid, user.id);
+                UserManagerService.enforceSerialNumber(userDir, user.serialNumber);
+            } catch (IOException e) {
+                Log.wtf(TAG, "Failed to create user directory on " + volumeUuid, e);
+            }
+        }
+    }
+
+    /**
+     * Examine all apps present on given mounted volume, and destroy apps that
+     * aren't expected, either due to uninstallation or reinstallation on
+     * another volume.
+     */
+    private void reconcileApps(String volumeUuid) {
+        final File[] files = Environment.getDataAppDirectory(volumeUuid).listFiles();
+        if (ArrayUtils.isEmpty(files)) {
+            Slog.d(TAG, "No apps found on " + volumeUuid);
+            return;
+        }
+
+        for (File file : files) {
+            final boolean isPackage = (isApkFile(file) || file.isDirectory())
+                    && !PackageInstallerService.isStageName(file.getName());
+            if (!isPackage) {
+                // Ignore entries which are not packages
+                continue;
+            }
+
+            boolean destroyApp = false;
+            String packageName = null;
+            try {
+                final PackageLite pkg = PackageParser.parsePackageLite(file,
+                        PackageParser.PARSE_MUST_BE_APK);
+                packageName = pkg.packageName;
+
+                synchronized (mPackages) {
+                    final PackageSetting ps = mSettings.mPackages.get(packageName);
+                    if (ps == null) {
+                        logCriticalInfo(Log.WARN, "Destroying " + packageName + " on + "
+                                + volumeUuid + " because we found no install record");
+                        destroyApp = true;
+                    } else if (!TextUtils.equals(volumeUuid, ps.volumeUuid)) {
+                        logCriticalInfo(Log.WARN, "Destroying " + packageName + " on "
+                                + volumeUuid + " because we expected it on " + ps.volumeUuid);
+                        destroyApp = true;
+                    }
+                }
+
+            } catch (PackageParserException e) {
+                logCriticalInfo(Log.WARN, "Destroying " + file + " due to parse failure: " + e);
+                destroyApp = true;
+            }
+
+            if (destroyApp) {
+                synchronized (mInstallLock) {
+                    if (packageName != null) {
+                        removeDataDirsLI(volumeUuid, packageName);
+                    }
+                    if (file.isDirectory()) {
+                        mInstaller.rmPackageDir(file.getAbsolutePath());
+                    } else {
+                        file.delete();
+                    }
+                }
+            }
+        }
+    }
+
     private void unfreezePackage(String packageName) {
         synchronized (mPackages) {
             final PackageSetting ps = mSettings.mPackages.get(packageName);
@@ -15536,14 +15695,11 @@
             // Technically, we shouldn't be doing this with the package lock
             // held.  However, this is very rare, and there is already so much
             // other disk I/O going on, that we'll let it slide for now.
-            final StorageManager storage = StorageManager.from(mContext);
-            final List<VolumeInfo> vols = storage.getVolumes();
-            for (VolumeInfo vol : vols) {
-                if (vol.getType() == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
-                    final String volumeUuid = vol.getFsUuid();
-                    if (DEBUG_INSTALL) Slog.d(TAG, "Removing user data on volume " + volumeUuid);
-                    mInstaller.removeUserDataDirs(volumeUuid, userHandle);
-                }
+            final StorageManager storage = mContext.getSystemService(StorageManager.class);
+            for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
+                final String volumeUuid = vol.getFsUuid();
+                if (DEBUG_INSTALL) Slog.d(TAG, "Removing user data on volume " + volumeUuid);
+                mInstaller.removeUserDataDirs(volumeUuid, userHandle);
             }
         }
         mUserNeedsBadging.delete(userHandle);
@@ -15597,10 +15753,10 @@
     }
 
     /** Called by UserManagerService */
-    void createNewUserLILPw(int userHandle, File path) {
+    void createNewUserLILPw(int userHandle) {
         if (mInstaller != null) {
             mInstaller.createUserConfig(userHandle);
-            mSettings.createNewUserLILPw(this, mInstaller, userHandle, path);
+            mSettings.createNewUserLILPw(this, mInstaller, userHandle);
             applyFactoryDefaultBrowserLPw(userHandle);
         }
     }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 2a5c0cd..c6d4a66 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -3617,10 +3617,7 @@
         }
     }
 
-    void createNewUserLILPw(PackageManagerService service, Installer installer,
-            int userHandle, File path) {
-        service.mContext.getSystemService(StorageManager.class)
-            .createNewUserDir(userHandle, path);
+    void createNewUserLILPw(PackageManagerService service, Installer installer, int userHandle) {
         for (PackageSetting ps : mPackages.values()) {
             if (ps.pkg == null || ps.pkg.applicationInfo == null) {
                 continue;
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index fb84f0d..05ae342 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -43,6 +43,10 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
 import android.util.AtomicFile;
 import android.util.Log;
 import android.util.Slog;
@@ -138,6 +142,8 @@
     static final int WRITE_USER_MSG = 1;
     static final int WRITE_USER_DELAY = 2*1000;  // 2 seconds
 
+    private static final String XATTR_SERIAL = "user.serial";
+
     private final Context mContext;
     private final PackageManagerService mPm;
     private final Object mInstallLock;
@@ -147,7 +153,6 @@
 
     private final File mUsersDir;
     private final File mUserListFile;
-    private final File mBaseUserPath;
 
     private final SparseArray<UserInfo> mUsers = new SparseArray<UserInfo>();
     private final SparseArray<Bundle> mUserRestrictions = new SparseArray<Bundle>();
@@ -211,7 +216,6 @@
                 // Make zeroth user directory, for services to migrate their files to that location
                 File userZeroDir = new File(mUsersDir, String.valueOf(UserHandle.USER_SYSTEM));
                 userZeroDir.mkdirs();
-                mBaseUserPath = baseUserPath;
                 FileUtils.setPermissions(mUsersDir.toString(),
                         FileUtils.S_IRWXU|FileUtils.S_IRWXG
                         |FileUtils.S_IROTH|FileUtils.S_IXOTH,
@@ -1253,7 +1257,6 @@
                     }
                     int userId = getNextAvailableIdLocked();
                     userInfo = new UserInfo(userId, name, null, flags);
-                    File userPath = new File(mBaseUserPath, Integer.toString(userId));
                     userInfo.serialNumber = mNextSerialNumber++;
                     long now = System.currentTimeMillis();
                     userInfo.creationTime = (now > EPOCH_PLUS_30_YEARS) ? now : 0;
@@ -1268,7 +1271,19 @@
                         }
                         userInfo.profileGroupId = parent.profileGroupId;
                     }
-                    mPm.createNewUserLILPw(userId, userPath);
+                    final StorageManager storage = mContext.getSystemService(StorageManager.class);
+                    for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
+                        final String volumeUuid = vol.getFsUuid();
+                        try {
+                            final File userDir = Environment.getDataUserDirectory(volumeUuid,
+                                    userId);
+                            prepareUserDirectory(mContext, volumeUuid, userId);
+                            enforceSerialNumber(userDir, userInfo.serialNumber);
+                        } catch (IOException e) {
+                            Log.wtf(LOG_TAG, "Failed to create user directory on " + volumeUuid, e);
+                        }
+                    }
+                    mPm.createNewUserLILPw(userId);
                     userInfo.partial = false;
                     scheduleWriteUserLocked(userInfo);
                     updateUserIdsLocked();
@@ -1874,6 +1889,81 @@
         return RESTRICTIONS_FILE_PREFIX + packageName + XML_SUFFIX;
     }
 
+    /**
+     * Create new {@code /data/user/[id]} directory and sets default
+     * permissions.
+     */
+    public static void prepareUserDirectory(Context context, String volumeUuid, int userId) {
+        final StorageManager storage = context.getSystemService(StorageManager.class);
+        final File userDir = Environment.getDataUserDirectory(volumeUuid, userId);
+        storage.createNewUserDir(userId, userDir);
+    }
+
+    /**
+     * Enforce that serial number stored in user directory inode matches the
+     * given expected value. Gracefully sets the serial number if currently
+     * undefined.
+     *
+     * @throws IOException when problem extracting serial number, or serial
+     *             number is mismatched.
+     */
+    public static void enforceSerialNumber(File file, int serialNumber) throws IOException {
+        final int foundSerial = getSerialNumber(file);
+        Slog.v(LOG_TAG, "Found " + file + " with serial number " + foundSerial);
+
+        if (foundSerial == -1) {
+            Slog.d(LOG_TAG, "Serial number missing on " + file + "; assuming current is valid");
+            try {
+                setSerialNumber(file, serialNumber);
+            } catch (IOException e) {
+                Slog.w(LOG_TAG, "Failed to set serial number on " + file, e);
+            }
+
+        } else if (foundSerial != serialNumber) {
+            throw new IOException("Found serial number " + foundSerial
+                    + " doesn't match expected " + serialNumber);
+        }
+    }
+
+    /**
+     * Set serial number stored in user directory inode.
+     *
+     * @throws IOException if serial number was already set
+     */
+    private static void setSerialNumber(File file, int serialNumber)
+            throws IOException {
+        try {
+            final byte[] buf = Integer.toString(serialNumber).getBytes(StandardCharsets.UTF_8);
+            Os.setxattr(file.getAbsolutePath(), XATTR_SERIAL, buf, OsConstants.XATTR_CREATE);
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    /**
+     * Return serial number stored in user directory inode.
+     *
+     * @return parsed serial number, or -1 if not set
+     */
+    private static int getSerialNumber(File file) throws IOException {
+        try {
+            final byte[] buf = new byte[256];
+            final int len = Os.getxattr(file.getAbsolutePath(), XATTR_SERIAL, buf);
+            final String serial = new String(buf, 0, len);
+            try {
+                return Integer.parseInt(serial);
+            } catch (NumberFormatException e) {
+                throw new IOException("Bad serial number: " + serial);
+            }
+        } catch (ErrnoException e) {
+            if (e.errno == OsConstants.ENODATA) {
+                return -1;
+            } else {
+                throw e.rethrowAsIOException();
+            }
+        }
+    }
+
     @Override
     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)