Introduce user cache and resolve cross-profile access correctly.

The current MediaProvider code assumed that when a binder call comes in
from a process with a different user-id than ourselves, it is aimed at
the storage of that user. While this is true for clone users (and other
profiles that share media with the parent), it is generally not true for
cross-profile intents like the ones used with a work profile, where an
image from the primary user can be returned to the work profile. When
the work profile resolves such an image, it will go to the owner
MediaProvider, and it should also use the owner database in that case.

To fix this, introduce a user cache that keeps track of the users for
which we are handling storage volumes; if the binder call is coming from
a user for which we are handling storage, use that user-id in the
LocalCallingIdentity; if it's coming from a user-id for which we are not
handling storage, assume it's targeted at the owner users storage
instead.

Bug: 186889014
Bug: 186893062
Bug: 187289954
Bug: 187386209
Test: TEST_MAPPING; manual CTS Verifier tests described in bugs
Change-Id: I061276998205b9f0908ca2264d3af80415c9cf32
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index d19627c..60b4e0e 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -63,6 +63,7 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.util.LongArray;
+import com.android.providers.media.util.UserCache;
 
 import java.util.Locale;
 
@@ -104,7 +105,8 @@
 
     private static final long UNKNOWN_ROW_ID = -1;
 
-    public static LocalCallingIdentity fromBinder(Context context, ContentProvider provider) {
+    public static LocalCallingIdentity fromBinder(Context context, ContentProvider provider,
+            UserCache userCache) {
         String callingPackage = provider.getCallingPackageUnchecked();
         int binderUid = Binder.getCallingUid();
         if (callingPackage == null) {
@@ -128,6 +130,12 @@
         } else {
             user = UserHandle.getUserHandleForUid(binderUid);
         }
+        if (!userCache.userSharesMediaWithParent(user)) {
+            // It's possible that we got a cross-profile intent from a regular work profile; in
+            // that case, the request was explicitly targeted at the media database of the owner
+            // user; reflect that here.
+            user = Process.myUserHandle();
+        }
         return new LocalCallingIdentity(context, Binder.getCallingPid(), binderUid,
                 user, callingPackage, callingAttributionTag);
     }
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 69efcae..5e01c06 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -210,6 +210,7 @@
 import com.android.providers.media.util.MimeUtils;
 import com.android.providers.media.util.PermissionUtils;
 import com.android.providers.media.util.SQLiteQueryBuilder;
+import com.android.providers.media.util.UserCache;
 import com.android.providers.media.util.XmpInterface;
 
 import com.google.common.hash.Hashing;
@@ -442,6 +443,7 @@
     private DevicePolicyManager mDevicePolicyManager;
     private UserManager mUserManager;
 
+    private UserCache mUserCache;
     private VolumeCache mVolumeCache;
 
     private int mExternalStorageAuthorityAppId;
@@ -519,7 +521,7 @@
                     final LocalCallingIdentity cached = mCachedCallingIdentity
                             .get(Binder.getCallingUid());
                     return (cached != null) ? cached
-                            : LocalCallingIdentity.fromBinder(getContext(), this);
+                            : LocalCallingIdentity.fromBinder(getContext(), this, mUserCache);
                 }
             });
 
@@ -896,6 +898,8 @@
     public boolean onCreate() {
         final Context context = getContext();
 
+        mUserCache = new UserCache(context);
+
         // Shift call statistics back to the original caller
         Binder.setProxyTransactListener(mTransactListener);
 
@@ -904,7 +908,7 @@
         mPackageManager = context.getPackageManager();
         mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
         mUserManager = context.getSystemService(UserManager.class);
-        mVolumeCache = new VolumeCache(context);
+        mVolumeCache = new VolumeCache(context, mUserCache);
 
         // Reasonable thumbnail size is half of the smallest screen edge width
         final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
@@ -9213,8 +9217,8 @@
         final String volumeName = resolveVolumeName(uri);
         synchronized (mAttachedVolumes) {
             boolean volumeAttached = false;
+            UserHandle user = mCallingIdentity.get().getUser();
             for (MediaVolume vol : mAttachedVolumes) {
-                UserHandle user = mCallingIdentity.get().getUser();
                 if (vol.getName().equals(volumeName) && vol.isVisibleToUser(user)) {
                     volumeAttached = true;
                     break;
diff --git a/src/com/android/providers/media/VolumeCache.java b/src/com/android/providers/media/VolumeCache.java
index d94ddfa..cbcd230 100644
--- a/src/com/android/providers/media/VolumeCache.java
+++ b/src/com/android/providers/media/VolumeCache.java
@@ -19,7 +19,6 @@
 import static com.android.providers.media.util.Logging.TAG;
 
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -35,6 +34,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.providers.media.util.FileUtils;
+import com.android.providers.media.util.UserCache;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -54,6 +54,7 @@
     private final Object mLock = new Object();
 
     private final UserManager mUserManager;
+    private final UserCache mUserCache;
 
     @GuardedBy("mLock")
     private final ArrayList<MediaVolume> mExternalVolumes = new ArrayList<>();
@@ -64,12 +65,10 @@
     @GuardedBy("mLock")
     private Collection<File> mCachedInternalScanPaths;
 
-    @GuardedBy("mLock")
-    private final LongSparseArray<Context> mUserContexts = new LongSparseArray<>();
-
-    public VolumeCache(Context context) {
+    public VolumeCache(Context context, UserCache userCache) {
         mContext = context;
         mUserManager = context.getSystemService(UserManager.class);
+        mUserCache = userCache;
     }
 
     public @NonNull List<MediaVolume> getExternalVolumes() {
@@ -112,7 +111,7 @@
                 // Try again by using FileUtils below
             }
 
-            final Context userContext = getContextForUser(user);
+            final Context userContext = mUserCache.getContextForUser(user);
             return FileUtils.getVolumePath(userContext, volumeName);
         }
     }
@@ -134,7 +133,7 @@
             }
 
             // Nothing found above; let's ask directly
-            final Context userContext = getContextForUser(user);
+            final Context userContext = mUserCache.getContextForUser(user);
             final Collection<File> res = FileUtils.getVolumeScanPaths(userContext, volumeName);
 
             return res;
@@ -167,22 +166,6 @@
         return volume.getId();
     }
 
-    private @NonNull Context getContextForUser(UserHandle user) {
-        synchronized (mLock) {
-            Context userContext = mUserContexts.get(user.getIdentifier());
-            if (userContext != null) {
-                return userContext;
-            }
-            try {
-                userContext = mContext.createPackageContextAsUser("system", 0, user);
-                mUserContexts.put(user.getIdentifier(), userContext);
-                return userContext;
-            } catch (PackageManager.NameNotFoundException e) {
-                throw new RuntimeException("Failed to create context for user " + user, e);
-            }
-        }
-    }
-
     @GuardedBy("mLock")
     private void updateExternalVolumesForUserLocked(Context userContext) {
         final StorageManager sm = userContext.getSystemService(StorageManager.class);
@@ -210,17 +193,10 @@
                 Log.wtf(TAG, "Failed to update volume " + MediaStore.VOLUME_INTERNAL,e );
             }
             mExternalVolumes.clear();
-            for (UserHandle profile : mUserManager.getEnabledProfiles()) {
-                if (profile.equals(mContext.getUser())) {
-                    // Volumes of the user id that MediaProvider runs as
-                    updateExternalVolumesForUserLocked(mContext);
-                } else {
-                    Context userContext = getContextForUser(profile);
-                    if (userContext.getSystemService(UserManager.class).isMediaSharedWithParent()) {
-                        // This profile shares media with its parent - add its volumes, too
-                        updateExternalVolumesForUserLocked(userContext);
-                    }
-                }
+            List<UserHandle> users = mUserCache.updateAndGetUsers();
+            for (UserHandle user : users) {
+                Context userContext = mUserCache.getContextForUser(user);
+                updateExternalVolumesForUserLocked(userContext);
             }
         }
     }
diff --git a/src/com/android/providers/media/util/UserCache.java b/src/com/android/providers/media/util/UserCache.java
new file mode 100644
index 0000000..885e07e
--- /dev/null
+++ b/src/com/android/providers/media/util/UserCache.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2021 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 com.android.providers.media.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.LongSparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UserCache is a class that keeps track of all users that the current MediaProvider
+ * instance is responsible for. By default, it handles storage for the user it is running as,
+ * but as of Android API 31, it will also handle storage for profiles that share media
+ * with their parent - profiles for which @link{UserManager#isMediaSharedWithParent} is set.
+ *
+ * It also keeps a cache of user contexts, for improving these lookups.
+ *
+ * Note that we don't use the USER_ broadcasts for keeping this state up to date, because they
+ * aren't guaranteed to be received before the volume events for a user.
+ */
+public class UserCache {
+    final Object mLock = new Object();
+    final Context mContext;
+    final UserManager mUserManager;
+
+    @GuardedBy("mLock")
+    final LongSparseArray<Context> mUserContexts = new LongSparseArray<>();
+
+    @GuardedBy("mLock")
+    final ArrayList<UserHandle> mUsers = new ArrayList<>();
+
+    public UserCache(Context context) {
+        mContext = context;
+        mUserManager = context.getSystemService(UserManager.class);
+
+        update();
+    }
+
+    private void update() {
+        List<UserHandle> profiles = mUserManager.getEnabledProfiles();
+        synchronized (mLock) {
+            mUsers.clear();
+            // Add the user we're running as by default
+            mUsers.add(Process.myUserHandle());
+            // And find all profiles that share media with us
+            for (UserHandle profile : profiles) {
+                if (!profile.equals(mContext.getUser())) {
+                    // Check if it's a profile that shares media with us
+                    Context userContext = getContextForUser(profile);
+                    if (userContext.getSystemService(UserManager.class).isMediaSharedWithParent()) {
+                        mUsers.add(profile);
+                    }
+                }
+            }
+        }
+    }
+
+    public @NonNull List<UserHandle> updateAndGetUsers() {
+        update();
+        synchronized (mLock) {
+            return (List<UserHandle>) mUsers.clone();
+        }
+    }
+
+    public @NonNull Context getContextForUser(@NonNull UserHandle user) {
+        Context userContext;
+        synchronized (mLock) {
+            userContext = mUserContexts.get(user.getIdentifier());
+            if (userContext != null) {
+                return userContext;
+            }
+        }
+        try {
+            userContext = mContext.createPackageContextAsUser("system", 0, user);
+            synchronized (mLock) {
+                mUserContexts.put(user.getIdentifier(), userContext);
+            }
+            return userContext;
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException("Failed to create context for user " + user, e);
+        }
+    }
+
+    /**
+     *  Returns whether the passed in user shares media with its parent (or peer).
+     *  Note that the value returned here is based on cached data; it relies on
+     *  other callers to keep the user cache up-to-date.
+     *
+     * @param user user to check
+     * @return whether the user shares media with its parent
+     */
+    public boolean userSharesMediaWithParent(@NonNull UserHandle user) {
+        synchronized (mLock) {
+            return mUsers.contains(user);
+        }
+    }
+}