Kernel mapping for package scope by user

Inform the kernel via configfs, of userids that
should be excluded when an app is not installed
for them.

Also push userid to remove_userid when a user is
removed so that the exclude list of that user can
be cleaned up in one command for all packages.

Test: runtest -x ....KernelPackageMappingTests.java
Change-Id: Ib94b1a0b737f45b2d03deb9650f0f0eb68e363d9
diff --git a/core/tests/packagemanagertests/Android.mk b/core/tests/packagemanagertests/Android.mk
new file mode 100644
index 0000000..c1e8c98
--- /dev/null
+++ b/core/tests/packagemanagertests/Android.mk
@@ -0,0 +1,20 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Include all test java files.
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+    frameworks-base-testutils
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_PACKAGE_NAME := FrameworksCorePackageManagerTests
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/core/tests/packagemanagertests/AndroidManifest.xml b/core/tests/packagemanagertests/AndroidManifest.xml
new file mode 100644
index 0000000..8f49008
--- /dev/null
+++ b/core/tests/packagemanagertests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          android:installLocation="internalOnly"
+          package="com.android.frameworks.coretests.packagemanager"
+          android:sharedUserId="android.uid.system">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+            android:name="android.support.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.frameworks.coretests.packagemanager"
+            android:label="Frameworks PackageManager Core Tests" />
+
+</manifest>
diff --git a/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java b/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java
new file mode 100644
index 0000000..1097bc7
--- /dev/null
+++ b/core/tests/packagemanagertests/src/android/content/pm/KernelPackageMappingTests.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 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.content.pm;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.Context;
+import android.os.FileUtils;
+import android.os.Process;
+import android.os.ServiceManager;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * This test needs to be run without any secondary users on the device,
+ * and selinux needs to be disabled with "adb shell setenforce 0".
+ */
+@RunWith(AndroidJUnit4.class)
+public class KernelPackageMappingTests {
+
+    private static final String TAG = "KernelPackageMapping";
+    private static final String SDCARDFS_PATH = "/config/sdcardfs";
+
+    private UserInfo mSecondaryUser;
+
+    private static File getKernelPackageDir(String packageName) {
+        return new File(new File(SDCARDFS_PATH), packageName);
+    }
+
+    private static File getKernelPackageFile(String packageName, String filename) {
+        return new File(getKernelPackageDir(packageName), filename);
+    }
+
+    private UserManager getUserManager() {
+        UserManager um = (UserManager) InstrumentationRegistry.getContext().getSystemService(
+                Context.USER_SERVICE);
+        return um;
+    }
+
+    private IPackageManager getIPackageManager() {
+        IPackageManager ipm = IPackageManager.Stub.asInterface(
+                ServiceManager.getService("package"));
+        return ipm;
+    }
+
+    private static String getContent(File file) {
+        try {
+            return FileUtils.readTextFile(file, 0, null).trim();
+        } catch (IOException ioe) {
+            Log.w(TAG, "Couldn't read file " + file.getAbsolutePath() + "\n" + ioe);
+            return "<error>";
+        }
+    }
+
+    @Test
+    public void testInstalledPrimary() throws Exception {
+        assertEquals("1000", getContent(getKernelPackageFile("com.android.settings", "appid")));
+    }
+
+    @Test
+    public void testInstalledAll() throws Exception {
+        assertEquals("", getContent(getKernelPackageFile("com.android.settings",
+                "excluded_userids")));
+    }
+
+    @Test
+    public void testNotInstalledSecondary() throws Exception {
+        mSecondaryUser = getUserManager().createUser("Secondary", 0);
+        assertEquals(Integer.toString(mSecondaryUser.id),
+                getContent(
+                        getKernelPackageFile("com.android.frameworks.coretests.packagemanager",
+                                "excluded_userids")));
+    }
+
+    @After
+    public void shutDown() throws Exception {
+        if (mSecondaryUser != null) {
+            getUserManager().removeUser(mSecondaryUser.id);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index ebd0b34..9ec27b3 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2141,6 +2141,7 @@
                     pkgSetting.setInstalled(install, UserHandle.USER_SYSTEM);
                 }
             }
+            scheduleWritePackageRestrictionsLocked(UserHandle.USER_SYSTEM);
         }
     }
 
@@ -13251,6 +13252,7 @@
                     pkgSetting.setHidden(false, userId);
                     pkgSetting.setInstallReason(installReason, userId);
                     mSettings.writePackageRestrictionsLPr(userId);
+                    mSettings.writeKernelMappingLPr(pkgSetting);
                     installed = true;
                 }
             }
@@ -16345,6 +16347,7 @@
             //note that the new package setting would have already been
             //added to mPackages. It hasn't been persisted yet.
             mSettings.setInstallStatus(pkgName, PackageSettingBase.PKG_INSTALL_INCOMPLETE);
+            // TODO: Remove this write? It's also written at the end of this method
             Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "writeSettings");
             mSettings.writeLPr();
             Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
@@ -16418,6 +16421,7 @@
                 } else if (!previousUserIds.contains(userId)) {
                     ps.setInstallReason(installReason, userId);
                 }
+                mSettings.writeKernelMappingLPr(ps);
             }
             res.name = pkgName;
             res.uid = newPackage.applicationInfo.uid;
@@ -17597,6 +17601,7 @@
 
         // writer
         synchronized (mPackages) {
+            boolean installedStateChanged = false;
             if (deletedPs != null) {
                 if ((flags&PackageManager.DELETE_KEEP_DATA) == 0) {
                     clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL);
@@ -17644,6 +17649,9 @@
                         if (DEBUG_REMOVE) {
                             Slog.d(TAG, "    user " + userId + " => " + installed);
                         }
+                        if (installed != ps.getInstalled(userId)) {
+                            installedStateChanged = true;
+                        }
                         ps.setInstalled(installed, userId);
                     }
                 }
@@ -17653,6 +17661,9 @@
                 // Save settings now
                 mSettings.writeLPr();
             }
+            if (installedStateChanged) {
+                mSettings.writeKernelMappingLPr(ps);
+            }
         }
         if (removedAppId != -1) {
             // A user ID was deleted here. Go through all users and remove it
@@ -17795,6 +17806,7 @@
                     UPDATE_PERMISSIONS_ALL | UPDATE_PERMISSIONS_REPLACE_PKG);
 
             if (applyUserRestrictions) {
+                boolean installedStateChanged = false;
                 if (DEBUG_REMOVE) {
                     Slog.d(TAG, "Propagating install state across reinstall");
                 }
@@ -17803,6 +17815,9 @@
                     if (DEBUG_REMOVE) {
                         Slog.d(TAG, "    user " + userId + " => " + installed);
                     }
+                    if (installed != ps.getInstalled(userId)) {
+                        installedStateChanged = true;
+                    }
                     ps.setInstalled(installed, userId);
 
                     mSettings.writeRuntimePermissionsForUserLPr(userId, false);
@@ -17810,6 +17825,9 @@
                 // Regardless of writeSettings we need to ensure that this restriction
                 // state propagation is persisted
                 mSettings.writeAllUsersPackageRestrictionsLPr();
+                if (installedStateChanged) {
+                    mSettings.writeKernelMappingLPr(ps);
+                }
             }
             // can downgrade to reader here
             if (writeSettings) {
@@ -18013,6 +18031,7 @@
                     // broadcasts will be sent correctly.
                     if (DEBUG_REMOVE) Slog.d(TAG, "Not installed by other users, full delete");
                     ps.setInstalled(true, user.getIdentifier());
+                    mSettings.writeKernelMappingLPr(ps);
                 }
             } else {
                 // This is a system app, so we assume that the
@@ -18122,6 +18141,7 @@
                     ps.readUserState(nextUserId).domainVerificationStatus, 0,
                     PackageManager.INSTALL_REASON_UNKNOWN);
         }
+        mSettings.writeKernelMappingLPr(ps);
     }
 
     private boolean clearPackageStateForUserLIF(PackageSetting ps, int userId,
diff --git a/services/core/java/com/android/server/pm/PackageSettingBase.java b/services/core/java/com/android/server/pm/PackageSettingBase.java
index b63edfd..0e11b0c 100644
--- a/services/core/java/com/android/server/pm/PackageSettingBase.java
+++ b/services/core/java/com/android/server/pm/PackageSettingBase.java
@@ -40,6 +40,9 @@
  * Settings base class for pending and resolved classes.
  */
 abstract class PackageSettingBase extends SettingBase {
+
+    private static final int[] EMPTY_INT_ARRAY = new int[0];
+
     /**
      * Indicates the state of installation. Used by PackageManager to figure out
      * incomplete installations. Say a package is being installed (the state is
@@ -502,6 +505,25 @@
         userState.delete(userId);
     }
 
+    public int[] getNotInstalledUserIds() {
+        int count = 0;
+        int userStateCount = userState.size();
+        for (int i = 0; i < userStateCount; i++) {
+            if (userState.valueAt(i).installed == false) {
+                count++;
+            }
+        }
+        if (count == 0) return EMPTY_INT_ARRAY;
+        int[] excludedUserIds = new int[count];
+        int idx = 0;
+        for (int i = 0; i < userStateCount; i++) {
+            if (userState.valueAt(i).installed == false) {
+                excludedUserIds[idx++] = userState.keyAt(i);
+            }
+        }
+        return excludedUserIds;
+    }
+
     IntentFilterVerificationInfo getIntentFilterVerificationInfo() {
         return verificationInfo;
     }
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 281e445..6156802 100644
--- a/services/core/java/com/android/server/pm/Settings.java
+++ b/services/core/java/com/android/server/pm/Settings.java
@@ -252,6 +252,7 @@
     private final File mPackageListFilename;
     private final File mStoppedPackagesFilename;
     private final File mBackupStoppedPackagesFilename;
+    /** The top level directory in configfs for sdcardfs to push the package->uid,userId mappings */
     private final File mKernelMappingFilename;
 
     /** Map from package name to settings */
@@ -260,8 +261,8 @@
     /** List of packages that installed other packages */
     final ArraySet<String> mInstallerPackages = new ArraySet<>();
 
-    /** Map from package name to appId */
-    private final ArrayMap<String, Integer> mKernelMapping = new ArrayMap<>();
+    /** Map from package name to appId and excluded userids */
+    private final ArrayMap<String, KernelPackageState> mKernelMapping = new ArrayMap<>();
 
     // List of replaced system applications
     private final ArrayMap<String, PackageSetting> mDisabledSysPackages =
@@ -271,6 +272,11 @@
     private final ArrayMap<String, IntentFilterVerificationInfo> mRestoredIntentFilterVerifications =
             new ArrayMap<String, IntentFilterVerificationInfo>();
 
+    private static final class KernelPackageState {
+        int appId;
+        int[] excludedUserIds;
+    }
+
     // Bookkeeping for restored user permission grants
     final class RestoredPermissionGrant {
         String permissionName;
@@ -2512,6 +2518,15 @@
         //Debug.stopMethodTracing();
     }
 
+    private void writeKernelRemoveUserLPr(int userId) {
+        if (mKernelMappingFilename == null) return;
+
+        File removeUserIdFile = new File(mKernelMappingFilename, "remove_userid");
+        if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + userId + " to " + removeUserIdFile
+                .getAbsolutePath());
+        writeIntToFile(removeUserIdFile, userId);
+    }
+
     void writeKernelMappingLPr() {
         if (mKernelMappingFilename == null) return;
 
@@ -2538,27 +2553,63 @@
     }
 
     void writeKernelMappingLPr(PackageSetting ps) {
-        if (mKernelMappingFilename == null) return;
+        if (mKernelMappingFilename == null || ps == null || ps.name == null) return;
 
-        final Integer cur = mKernelMapping.get(ps.name);
-        if (cur != null && cur.intValue() == ps.appId) {
-            // Ignore when mapping already matches
-            return;
+        KernelPackageState cur = mKernelMapping.get(ps.name);
+        final boolean firstTime = cur == null;
+        int[] excludedUserIds = ps.getNotInstalledUserIds();
+        final boolean userIdsChanged = firstTime
+                || !Arrays.equals(excludedUserIds, cur.excludedUserIds);
+
+        // Package directory
+        final File dir = new File(mKernelMappingFilename, ps.name);
+
+        if (firstTime) {
+            dir.mkdir();
+            // Create a new mapping state
+            cur = new KernelPackageState();
+            mKernelMapping.put(ps.name, cur);
         }
 
-        if (DEBUG_KERNEL) Slog.d(TAG, "Mapping " + ps.name + " to " + ps.appId);
+        // If mapping is incorrect or non-existent, write the appid file
+        if (cur.appId != ps.appId) {
+            final File appIdFile = new File(dir, "appid");
+            writeIntToFile(appIdFile, ps.appId);
+            if (DEBUG_KERNEL) Slog.d(TAG, "Mapping " + ps.name + " to " + ps.appId);
+        }
 
-        final File dir = new File(mKernelMappingFilename, ps.name);
-        dir.mkdir();
+        if (userIdsChanged) {
+            // Build the exclusion list -- the ids to add to the exclusion list
+            for (int i = 0; i < excludedUserIds.length; i++) {
+                if (cur.excludedUserIds == null || !ArrayUtils.contains(cur.excludedUserIds,
+                        excludedUserIds[i])) {
+                    writeIntToFile(new File(dir, "excluded_userids"), excludedUserIds[i]);
+                    if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + excludedUserIds[i] + " to "
+                            + ps.name + "/excluded_userids");
+                }
+            }
+            // Build the inclusion list -- the ids to remove from the exclusion list
+            if (cur.excludedUserIds != null) {
+                for (int i = 0; i < cur.excludedUserIds.length; i++) {
+                    if (!ArrayUtils.contains(excludedUserIds, cur.excludedUserIds[i])) {
+                        writeIntToFile(new File(dir, "clear_userid"),
+                                cur.excludedUserIds[i]);
+                        if (DEBUG_KERNEL) Slog.d(TAG, "Writing " + cur.excludedUserIds[i] + " to "
+                                + ps.name + "/clear_userid");
 
-        final File file = new File(dir, "appid");
+                    }
+                }
+            }
+            cur.excludedUserIds = excludedUserIds;
+        }
+    }
+
+    private void writeIntToFile(File file, int value) {
         try {
-            // Note that the use of US_ASCII here is safe, we're only writing a decimal
-            // number to the file.
             FileUtils.bytesToFile(file.getAbsolutePath(),
-                    Integer.toString(ps.appId).getBytes(StandardCharsets.US_ASCII));
-            mKernelMapping.put(ps.name, ps.appId);
+                    Integer.toString(value).getBytes(StandardCharsets.US_ASCII));
         } catch (IOException ignored) {
+            Slog.w(TAG, "Couldn't write " + value + " to " + file.getAbsolutePath());
         }
     }
 
@@ -4081,6 +4132,9 @@
                         !ArrayUtils.contains(disallowedPackages, ps.name);
                 // Only system apps are initially installed.
                 ps.setInstalled(shouldInstall, userHandle);
+                if (!shouldInstall) {
+                    writeKernelMappingLPr(ps);
+                }
                 // Need to create a data directory for all apps under this user. Accumulate all
                 // required args and call the installer after mPackages lock has been released
                 volumeUuids[i] = ps.volumeUuid;
@@ -4123,6 +4177,10 @@
         mRuntimePermissionsPersistence.onUserRemovedLPw(userId);
 
         writePackageListLPr();
+
+        // Inform kernel that the user was removed, so that packages are marked uninstalled
+        // for sdcardfs
+        writeKernelRemoveUserLPr(userId);
     }
 
     void removeCrossProfileIntentFiltersLPw(int userId) {