Cache LocalCallingIdentity across FUSE requests

We now listen for app op changes for packages and invalidate the
LocalCallingIdentity cache entry for the uid.

Additionally, we invalidate the cache entry on any package changes
like install/uninstall for the uid.

Test: Manual
Bug: 148904081
Change-Id: I9811efca7af223eaf99e28c79a948eca41579dbe
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 06f0bc4..3253bb7 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -95,6 +95,8 @@
             throw new IllegalArgumentException("UID " + uid + " has no associated package");
         }
         LocalCallingIdentity ident =  fromExternal(context, uid, sharedPackageNames[0], null);
+        ident.sharedPackageNames = sharedPackageNames;
+        ident.sharedPackageNamesResolved = true;
         if (uid == Process.SHELL_UID) {
             // This is useful for debugging/testing/development
             if (SystemProperties.getBoolean("persist.sys.fuse.shell.redaction-needed", false)) {
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index c951a83..718c094 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media;
 
+import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
+import static android.app.AppOpsManager.permissionToOp;
 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
@@ -69,6 +71,7 @@
 
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
+import android.app.AppOpsManager.OnOpChangedListener;
 import android.app.DownloadManager;
 import android.app.PendingIntent;
 import android.app.RecoverableSecurityException;
@@ -355,6 +358,29 @@
     };
 
     /**
+     * Map from UID to cached {@link LocalCallingIdentity}. Values are only
+     * maintained in this map until there's any change in the appops needed or packages
+     * used in the {@link LocalCallingIdentity}.
+     */
+    @GuardedBy("mCachedCallingIdentityForFuse")
+    private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse =
+            new SparseArray<>();
+
+    private OnOpChangedListener mModeListener =
+            (op, packageName) -> invalidateLocalCallingIdentityCache(packageName, "op " + op);
+
+    private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) {
+        synchronized (mCachedCallingIdentityForFuse) {
+            LocalCallingIdentity ident = mCachedCallingIdentityForFuse.get(uid);
+            if (ident == null) {
+               ident = LocalCallingIdentity.fromExternal(getContext(), uid);
+               mCachedCallingIdentityForFuse.put(uid, ident);
+            }
+            return ident;
+        }
+    }
+
+    /**
      * Calling identity state about on the current thread. Populated on demand,
      * and invalidated by {@link #onCallingPackageChanged()} when each remote
      * call is finished.
@@ -440,6 +466,36 @@
         }
     };
 
+    private BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case Intent.ACTION_PACKAGE_REMOVED:
+                case Intent.ACTION_PACKAGE_ADDED:
+                    Uri uri = intent.getData();
+                    String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
+                    if (pkg != null) {
+                        invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction());
+                    } else {
+                        Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction());
+                    }
+                    break;
+            }
+        }
+    };
+
+    private void invalidateLocalCallingIdentityCache(String packageName, String reason) {
+        synchronized (mCachedCallingIdentityForFuse) {
+            try {
+                Log.i(TAG, "Invalidating LocalCallingIdentity cache for package " + packageName
+                        + ". Reason: " + reason);
+                mCachedCallingIdentityForFuse.remove(
+                        getContext().getPackageManager().getPackageUid(packageName, 0));
+            } catch (NameNotFoundException ignored) {
+            }
+        }
+    }
+
     private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) {
         File file;
         try {
@@ -701,6 +757,13 @@
         filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
         context.registerReceiver(mMediaReceiver, filter);
 
+        final IntentFilter packageFilter = new IntentFilter();
+        packageFilter.setPriority(10);
+        filter.addDataScheme("package");
+        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        context.registerReceiver(mPackageReceiver, packageFilter);
+
         // Watch for invalidation of cached volumes
         mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(),
                 new StorageVolumeCallback() {
@@ -721,6 +784,24 @@
                 AppOpsManager.OPSTR_CAMERA
         }, context.getMainExecutor(), mActiveListener);
 
+
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE,
+                null /* all packages */, mModeListener);
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE,
+                null /* all packages */, mModeListener);
+        mAppOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION),
+                null /* all packages */, mModeListener);
+        // Legacy apps
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE,
+                null /* all packages */, mModeListener);
+        // File managers
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE,
+                null /* all packages */, mModeListener);
+        // Default gallery changes
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
+                null /* all packages */, mModeListener);
+        mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO,
+                null /* all packages */, mModeListener);
         return true;
     }
 
@@ -891,8 +972,7 @@
      */
     @Keep
     public void scanFileForFuse(String file, int uid) {
-        final String callingPackage =
-                LocalCallingIdentity.fromExternal(getContext(), uid).getPackageName();
+        final String callingPackage = getCachedCallingIdentityForFuse(uid).getPackageName();
         scanFile(new File(file), REASON_DEMAND, callingPackage);
     }
 
@@ -1118,8 +1198,9 @@
      */
     @Keep
     public String[] getFilesInDirectoryForFuse(String path, int uid) {
-        final LocalCallingIdentity token = clearLocalCallingIdentity(
-                LocalCallingIdentity.fromExternal(getContext(), uid));
+        final LocalCallingIdentity token =
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
         try {
             final String appSpecificDir = extractPathOwnerPackageName(path);
             // Apps are allowed to list files only in their own external directory.
@@ -1551,8 +1632,9 @@
     @Keep
     public int renameForFuse(String oldPath, String newPath, int uid) {
         final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
-        final LocalCallingIdentity token = clearLocalCallingIdentity(
-                LocalCallingIdentity.fromExternal(getContext(), uid));
+        final LocalCallingIdentity token =
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
         try {
             final String oldPathPackageName = extractPathOwnerPackageName(oldPath);
             final String newPathPackageName = extractPathOwnerPackageName(newPath);
@@ -1640,8 +1722,9 @@
     @Override
     public int checkUriPermission(@NonNull Uri uri, int uid,
             /* @Intent.AccessUriMode */ int modeFlags) {
-        final LocalCallingIdentity token = clearLocalCallingIdentity(
-                LocalCallingIdentity.fromExternal(getContext(), uid));
+        final LocalCallingIdentity token =
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
         try {
             final boolean allowHidden = isCallingPackageAllowedHidden();
             final int table = matchUri(uri, allowHidden);
@@ -5324,8 +5407,8 @@
             return getRedactionRanges(file).redactionRanges;
         }
 
-        LocalCallingIdentity token =
-                clearLocalCallingIdentity(LocalCallingIdentity.fromExternal(getContext(), uid));
+        final LocalCallingIdentity token =
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
 
         long[] res = new long[0];
         try {
@@ -5454,7 +5537,9 @@
     @Keep
     public int isOpenAllowedForFuse(String path, int uid, boolean forWrite) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(LocalCallingIdentity.fromExternal(getContext(), uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
+
         try {
             // Returns null if the path doesn't correspond to an app specific directory
             final String appSpecificDir = extractPathOwnerPackageName(path);
@@ -5639,7 +5724,8 @@
     @Keep
     public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(LocalCallingIdentity.fromExternal(getContext(), uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
         try {
             // Returns null if the path doesn't correspond to an app specific directory
             final String appSpecificDir = extractPathOwnerPackageName(path);
@@ -5721,7 +5807,8 @@
     @Keep
     public int deleteFileForFuse(@NonNull String path, int uid) throws IOException {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(LocalCallingIdentity.fromExternal(getContext(), uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
         try {
             // Check if app is deleting a file under an app specific directory
             final String appSpecificDir = extractPathOwnerPackageName(path);
@@ -5796,7 +5883,8 @@
     public int isDirectoryCreationOrDeletionAllowedForFuse(
             @NonNull String path, int uid, boolean forCreate) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(LocalCallingIdentity.fromExternal(getContext(), uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
         try {
             // Returns null if the path doesn't correspond to an app specific directory
             final String appSpecificDir = extractPathOwnerPackageName(path);
@@ -5855,7 +5943,8 @@
     @Keep
     public int isOpendirAllowedForFuse(@NonNull String path, int uid) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(LocalCallingIdentity.fromExternal(getContext(), uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
         try {
             // Returns null if the path doesn't correspond to an app specific directory
             final String appSpecificDir = extractPathOwnerPackageName(path);