Merge "Introduce user cache and resolve cross-profile access correctly." into sc-dev
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 6b410c4..e13d874 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();
@@ -9214,8 +9218,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);
+        }
+    }
+}