Merge "Append GET_ID custom function using bindSelection" into rvc-dev
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 4d586d1..105e087 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -129,8 +129,6 @@
     void Close(int fd) { SendMessage(Message::close, fd); }
 
   private:
-    std::thread thread_;
-
     struct Message {
         enum Type { record, close, quit };
         Type type;
@@ -218,8 +216,9 @@
         cv_.notify_one();
     }
 
-    std::queue<Message> queue_;
     std::mutex mutex_;
+    std::thread thread_;
+    std::queue<Message> queue_;
     std::condition_variable cv_;
 
     typedef std::multimap<size_t, int> Sizes;
@@ -378,6 +377,25 @@
     return std::regex_match(path, PATTERN_OWNED_PATH);
 }
 
+static void invalidate_case_insensitive_dentry_matches(struct fuse* fuse, node* parent,
+                                                       const string& name) {
+    vector<string> children = parent->MatchChildrenCaseInsensitive(name);
+    if (children.empty() || (children.size() == 1 && children[0] == name)) {
+        return;
+    }
+
+    fuse_ino_t parent_ino = fuse->ToInode(parent);
+    std::thread t([=]() {
+        for (const string& child_name : children) {
+            if (fuse_lowlevel_notify_inval_entry(fuse->se, parent_ino, child_name.c_str(),
+                                                 child_name.size())) {
+                LOG(ERROR) << "Failed to invalidate dentry " << child_name;
+            }
+        }
+    });
+    t.detach();
+}
+
 static node* make_node_entry(fuse_req_t req, node* parent, const string& name, const string& path,
                              struct fuse_entry_param* e, int* error_code) {
     struct fuse* fuse = get_fuse(req);
@@ -396,6 +414,9 @@
     }
 
     TRACE_NODE(node);
+
+    invalidate_case_insensitive_dentry_matches(fuse, parent, name);
+
     // This FS is not being exported via NFS so just a fixed generation number
     // for now. If we do need this, we need to increment the generation ID each
     // time the fuse daemon restarts because that's what it takes for us to
@@ -579,16 +600,15 @@
             if (to_set & FATTR_ATIME_NOW) {
                 times[0].tv_nsec = UTIME_NOW;
             } else {
-                times[0].tv_sec = attr->st_atime;
-                // times[0].tv_nsec = attr->st_atime.tv_nsec;
+                times[0] = attr->st_atim;
             }
         }
+
         if (to_set & FATTR_MTIME) {
             if (to_set & FATTR_MTIME_NOW) {
                 times[1].tv_nsec = UTIME_NOW;
             } else {
-                times[1].tv_sec = attr->st_mtime;
-                // times[1].tv_nsec = attr->st_mtime.tv_nsec;
+                times[1] = attr->st_mtim;
             }
         }
 
@@ -1337,8 +1357,36 @@
     const string path = node->BuildPath();
     TRACE_NODE(node);
 
-    int res = access(path.c_str(), F_OK);
-    fuse_reply_err(req, res ? errno : 0);
+    // exists() checks are always allowed.
+    if (mask == F_OK) {
+        int res = access(path.c_str(), F_OK);
+        fuse_reply_err(req, res ? errno : 0);
+        return;
+    }
+    struct stat stat;
+    if (lstat(path.c_str(), &stat)) {
+        // File doesn't exist
+        fuse_reply_err(req, ENOENT);
+        return;
+    }
+
+    // For read and write permission checks we go to MediaProvider.
+    int status = 0;
+    bool is_directory = S_ISDIR(stat.st_mode);
+    if (is_directory) {
+        status = fuse->mp->IsOpendirAllowed(path, ctx->uid);
+    } else {
+        if (mask & X_OK) {
+            // Fuse is mounted with MS_NOEXEC.
+            fuse_reply_err(req, EACCES);
+            return;
+        }
+
+        bool for_write = mask & W_OK;
+        status = fuse->mp->IsOpenAllowed(path, ctx->uid, for_write);
+    }
+
+    fuse_reply_err(req, status);
 }
 
 static void pf_create(fuse_req_t req,
@@ -1601,7 +1649,9 @@
     }
 
     // Custom logging for libfuse
-    fuse_set_log_func(fuse_logger);
+    if (android::base::GetBoolProperty("persist.sys.fuse.log", false)) {
+        fuse_set_log_func(fuse_logger);
+    }
 
     struct fuse_session
             * se = fuse_session_new(&args, &ops, sizeof(ops), &fuse_default);
@@ -1611,7 +1661,7 @@
     }
     fuse_default.se = se;
     fuse_default.active = &active;
-    se->fd = fd;
+    se->fd = fd.release();  // libfuse owns the FD now
     se->mountpoint = strdup(path.c_str());
 
     // Single thread. Useful for debugging
diff --git a/jni/node-inl.h b/jni/node-inl.h
index 4c523e6..c7d30df 100644
--- a/jni/node-inl.h
+++ b/jni/node-inl.h
@@ -72,7 +72,7 @@
 // can assert that we only ever return an active node in response to a lookup.
 class NodeTracker {
   public:
-    NodeTracker(std::recursive_mutex* lock) : lock_(lock) {}
+    explicit NodeTracker(std::recursive_mutex* lock) : lock_(lock) {}
 
     void CheckTracked(__u64 ino) const {
         if (kEnableInodeTracking) {
@@ -221,6 +221,22 @@
         return parent_;
     }
 
+    std::vector<std::string> MatchChildrenCaseInsensitive(const std::string& name) const {
+        std::lock_guard<std::recursive_mutex> guard(*lock_);
+
+        const char* name_char = name.c_str();
+        std::vector<std::string> matches;
+
+        for (node* child : children_) {
+            const std::string& child_name = child->GetName();
+            if (!strcasecmp(name_char, child_name.c_str())) {
+                matches.push_back(child_name);
+            }
+        }
+
+        return matches;
+    }
+
     inline void AddHandle(handle* h) {
         std::lock_guard<std::recursive_mutex> guard(*lock_);
         handles_.emplace_back(std::unique_ptr<handle>(h));
diff --git a/jni/node_test.cpp b/jni/node_test.cpp
index 027181d..901704d 100644
--- a/jni/node_test.cpp
+++ b/jni/node_test.cpp
@@ -228,3 +228,17 @@
             new handle(-1, new mediaprovider::fuse::RedactionInfo, true /* cached */));
     EXPECT_DEATH(node->DestroyHandle(h2.get()), "");
 }
+
+TEST_F(NodeTest, CaseInsensitive) {
+    unique_node_ptr parent = CreateNode(nullptr, "/path");
+    unique_node_ptr lower_child = CreateNode(parent.get(), "child");
+    unique_node_ptr upper_child = CreateNode(parent.get(), "CHILD");
+    unique_node_ptr mixed_child = CreateNode(parent.get(), "cHiLd");
+
+    std::vector<std::string> children = parent->MatchChildrenCaseInsensitive("ChIld");
+
+    ASSERT_EQ(3, children.size());
+    ASSERT_EQ("child", children[0]);
+    ASSERT_EQ("CHILD", children[1]);
+    ASSERT_EQ("cHiLd", children[2]);
+}
diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml
index 74d70be..5c4d315 100644
--- a/res/values-ar/strings.xml
+++ b/res/values-ar/strings.xml
@@ -23,7 +23,7 @@
     <string name="unknown" msgid="2059049215682829375">"غير معروف"</string>
     <string name="root_images" msgid="5861633549189045666">"الصور"</string>
     <string name="root_videos" msgid="8792703517064649453">"الفيديوهات"</string>
-    <string name="root_audio" msgid="3505830755201326018">"الصوت"</string>
+    <string name="root_audio" msgid="3505830755201326018">"صوتيات"</string>
     <string name="root_documents" msgid="3829103301363849237">"المستندات"</string>
     <string name="permission_required" msgid="1460820436132943754">"مطلوب الحصول على إذن لتعديل هذا العنصر أو حذفه."</string>
     <string name="permission_required_action" msgid="706370952366113539">"متابعة"</string>
diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml
index 4279f89..12ec655 100644
--- a/res/values-hi/strings.xml
+++ b/res/values-hi/strings.xml
@@ -21,7 +21,7 @@
     <string name="app_label" msgid="9035307001052716210">"मीडिया मेमोरी"</string>
     <string name="artist_label" msgid="8105600993099120273">"कलाकार"</string>
     <string name="unknown" msgid="2059049215682829375">"अज्ञात"</string>
-    <string name="root_images" msgid="5861633549189045666">"चित्र"</string>
+    <string name="root_images" msgid="5861633549189045666">"इमेज"</string>
     <string name="root_videos" msgid="8792703517064649453">"वीडियो"</string>
     <string name="root_audio" msgid="3505830755201326018">"ऑडियो"</string>
     <string name="root_documents" msgid="3829103301363849237">"दस्तावेज़"</string>
diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml
index 1efefcd..2eed59d 100644
--- a/res/values-pa/strings.xml
+++ b/res/values-pa/strings.xml
@@ -21,7 +21,7 @@
     <string name="app_label" msgid="9035307001052716210">"ਮੀਡੀਆ ਸਟੋਰੇਜ"</string>
     <string name="artist_label" msgid="8105600993099120273">"ਕਲਾਕਾਰ"</string>
     <string name="unknown" msgid="2059049215682829375">"ਅਗਿਆਤ"</string>
-    <string name="root_images" msgid="5861633549189045666">"ਚਿਤਰ"</string>
+    <string name="root_images" msgid="5861633549189045666">"ਚਿੱਤਰ"</string>
     <string name="root_videos" msgid="8792703517064649453">"ਵੀਡੀਓ"</string>
     <string name="root_audio" msgid="3505830755201326018">" ਆਡੀਓ"</string>
     <string name="root_documents" msgid="3829103301363849237">"ਦਸਤਾਵੇਜ਼"</string>
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 90a9ed1..5bd8c80 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -33,6 +33,7 @@
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteVideo;
+import static com.android.providers.media.util.PermissionUtils.generateAppOpMessage;
 
 import android.annotation.Nullable;
 import android.app.AppOpsManager;
@@ -58,7 +59,9 @@
     public final int pid;
     public final int uid;
     public final String packageNameUnchecked;
+    // Info used for logging permission checks
     public @Nullable String attributionTag;
+    private @Nullable String opDescription;
 
     private LocalCallingIdentity(Context context, int pid, int uid, String packageNameUnchecked,
             @Nullable String attributionTag) {
@@ -67,6 +70,7 @@
         this.uid = uid;
         this.packageNameUnchecked = packageNameUnchecked;
         this.attributionTag = attributionTag;
+        this.opDescription = null;
     }
 
     /**
@@ -212,7 +216,7 @@
 
     public boolean hasPermission(int permission) {
         if ((hasPermissionResolved & permission) == 0) {
-            if (hasPermissionInternal(permission)) {
+            if (hasPermissionInternal(permission, opDescription)) {
                 hasPermission |= permission;
             }
             hasPermissionResolved |= permission;
@@ -220,7 +224,7 @@
         return (hasPermission & permission) != 0;
     }
 
-    private boolean hasPermissionInternal(int permission) {
+    private boolean hasPermissionInternal(int permission, @Nullable String description) {
         // While we're here, enforce any broad user-level restrictions
         if ((uid == Process.SHELL_UID) && context.getSystemService(UserManager.class)
                 .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
@@ -242,19 +246,26 @@
             case PERMISSION_IS_REDACTION_NEEDED:
                 return isRedactionNeededInternal();
             case PERMISSION_READ_AUDIO:
-                return checkPermissionReadAudio(context, pid, uid, getPackageName());
+                return checkPermissionReadAudio(context, pid, uid, getPackageName(), attributionTag,
+                        generateAppOpMessage(packageName, description));
             case PERMISSION_READ_VIDEO:
-                return checkPermissionReadVideo(context, pid, uid, getPackageName());
+                return checkPermissionReadVideo(context, pid, uid, getPackageName(), attributionTag,
+                        generateAppOpMessage(packageName, description));
             case PERMISSION_READ_IMAGES:
-                return checkPermissionReadImages(context, pid, uid, getPackageName());
+                return checkPermissionReadImages(context, pid, uid, getPackageName(),
+                        attributionTag, generateAppOpMessage(packageName, description));
             case PERMISSION_WRITE_AUDIO:
-                return checkPermissionWriteAudio(context, pid, uid, getPackageName());
+                return checkPermissionWriteAudio(context, pid, uid, getPackageName(),
+                        attributionTag, generateAppOpMessage(packageName, description));
             case PERMISSION_WRITE_VIDEO:
-                return checkPermissionWriteVideo(context, pid, uid, getPackageName());
+                return checkPermissionWriteVideo(context, pid, uid, getPackageName(),
+                        attributionTag, generateAppOpMessage(packageName, description));
             case PERMISSION_WRITE_IMAGES:
-                return checkPermissionWriteImages(context, pid, uid, getPackageName());
+                return checkPermissionWriteImages(context, pid, uid, getPackageName(),
+                        attributionTag, generateAppOpMessage(packageName, description));
             case PERMISSION_MANAGE_EXTERNAL_STORAGE:
-                return checkPermissionManageExternalStorage(context, pid, uid, getPackageName());
+                return checkPermissionManageExternalStorage(context, pid, uid, getPackageName(),
+                        attributionTag, generateAppOpMessage(packageName, description));
             default:
                 return false;
         }
@@ -297,13 +308,15 @@
     }
 
     private boolean isLegacyWriteInternal() {
-        return hasPermission(PERMISSION_IS_LEGACY_GRANTED) &&
-                checkPermissionWriteStorage(context, pid, uid, getPackageName());
+        return hasPermission(PERMISSION_IS_LEGACY_GRANTED)
+                && checkPermissionWriteStorage(context, pid, uid, getPackageName(), attributionTag,
+                        /*opMessage*/ null);
     }
 
     private boolean isLegacyReadInternal() {
-        return hasPermission(PERMISSION_IS_LEGACY_GRANTED) &&
-                checkPermissionReadStorage(context, pid, uid, getPackageName());
+        return hasPermission(PERMISSION_IS_LEGACY_GRANTED)
+                && checkPermissionReadStorage(context, pid, uid, getPackageName(), attributionTag,
+                        /*opMessage*/ null);
     }
 
     /** System internals or callers holding permission have no redaction */
@@ -358,4 +371,8 @@
     public long getDeletedRowId(@NonNull String path) {
         return rowIdOfDeletedPaths.getOrDefault(path, UNKNOWN_ROW_ID);
     }
+
+    public void setOpDescription(@Nullable String opDescription) {
+        this.opDescription = opDescription;
+    }
 }
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 952f895..762dbf4 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -66,6 +66,7 @@
 import static com.android.providers.media.util.Logging.LOGV;
 import static com.android.providers.media.util.Logging.TAG;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
+import static com.android.providers.media.util.PermissionUtils.generateAppOpMessage;
 
 import android.app.AppOpsManager;
 import android.app.AppOpsManager.OnOpActiveChangedListener;
@@ -120,7 +121,6 @@
 import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 import android.os.ParcelFileDescriptor.OnCloseListener;
-import android.os.Process;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -407,13 +407,19 @@
     private OnOpChangedListener mModeListener =
             (op, packageName) -> invalidateLocalCallingIdentityCache(packageName, "op " + op);
 
-    private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) {
+    /**
+     * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op
+     * description for the calling identity.
+     */
+    private LocalCallingIdentity getCachedCallingIdentityForFuse(
+            int uid, @Nullable String opDescription) {
         synchronized (mCachedCallingIdentityForFuse) {
             LocalCallingIdentity ident = mCachedCallingIdentityForFuse.get(uid);
             if (ident == null) {
                ident = LocalCallingIdentity.fromExternal(getContext(), uid);
                mCachedCallingIdentityForFuse.put(uid, ident);
             }
+            ident.setOpDescription(opDescription);
             return ident;
         }
     }
@@ -1054,7 +1060,9 @@
      * to clear other apps' cache directories.
      */
     static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) {
-        return checkPermissionManageExternalStorage(context, /*pid*/-1, ai.uid, ai.packageName);
+        final String opMessage = generateAppOpMessage(ai.packageName, "clear app cache");
+        return checkPermissionManageExternalStorage(context, /*pid*/ -1, ai.uid, ai.packageName,
+                /*attributionTag*/ null, opMessage);
     }
 
     /**
@@ -1272,7 +1280,7 @@
     @Keep
     public String[] getFilesInDirectoryForFuse(String path, int uid) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid, "readdir " + path));
 
         try {
             if (isPrivatePackagePathNotOwnedByCaller(path)) {
@@ -1759,8 +1767,8 @@
     @Keep
     public int renameForFuse(String oldPath, String newPath, int uid) {
         final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
-        final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        final LocalCallingIdentity token = clearLocalCallingIdentity(
+                getCachedCallingIdentityForFuse(uid, "rename " + oldPath + " to " + newPath));
 
         try {
             if (isPrivatePackagePathNotOwnedByCaller(oldPath)
@@ -1843,7 +1851,8 @@
     public int checkUriPermission(@NonNull Uri uri, int uid,
             /* @Intent.AccessUriMode */ int modeFlags) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid,
+                        /*opDescription*/ null));
 
         try {
             final boolean allowHidden = isCallingPackageAllowedHidden();
@@ -2694,7 +2703,12 @@
 
         if (mimeType != null) {
             values.put(FileColumns.MIME_TYPE, mimeType);
-            values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
+            if (isCallingPackageSystem() && values.containsKey(FileColumns.MEDIA_TYPE)) {
+                // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
+                // FileColumns.MEDIA_TYPE is already populated.
+            } else{
+                values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
+            }
         } else {
             values.put(FileColumns.MEDIA_TYPE, mediaType);
         }
@@ -5797,8 +5811,8 @@
             return getRedactionRanges(file).redactionRanges;
         }
 
-        final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        final LocalCallingIdentity token = clearLocalCallingIdentity(
+                getCachedCallingIdentityForFuse(uid, "read metadata from " + path));
 
         long[] res = new long[0];
         try {
@@ -5918,9 +5932,8 @@
      */
     @Keep
     public int isOpenAllowedForFuse(String path, int uid, boolean forWrite) {
-        final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
-
+        final LocalCallingIdentity token = clearLocalCallingIdentity(
+                getCachedCallingIdentityForFuse(uid, (forWrite ? "write " : "read ") + path));
 
         try {
             if (isPrivatePackagePathNotOwnedByCaller(path)) {
@@ -6111,7 +6124,7 @@
     @Keep
     public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid, "create " + path));
 
         try {
             if (isPrivatePackagePathNotOwnedByCaller(path)) {
@@ -6181,7 +6194,7 @@
     @Keep
     public int deleteFileForFuse(@NonNull String path, int uid) throws IOException {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid, "delete " + path));
 
         try {
             if (isPrivatePackagePathNotOwnedByCaller(path)) {
@@ -6248,8 +6261,8 @@
     @Keep
     public int isDirectoryCreationOrDeletionAllowedForFuse(
             @NonNull String path, int uid, boolean forCreate) {
-        final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        final LocalCallingIdentity token = clearLocalCallingIdentity(
+                getCachedCallingIdentityForFuse(uid, (forCreate ? "mkdir " : "rmdir ") + path));
 
         try {
             // App dirs are not indexed, so we don't create an entry for the file.
@@ -6301,8 +6314,8 @@
      */
     @Keep
     public int isOpendirAllowedForFuse(@NonNull String path, int uid) {
-        final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        final LocalCallingIdentity token = clearLocalCallingIdentity(
+                getCachedCallingIdentityForFuse(uid, "directory access " + path));
 
         try {
             if (isPrivatePackagePathNotOwnedByCaller(path)) {
@@ -6328,7 +6341,8 @@
     @Keep
     public boolean isUidForPackageForFuse(@NonNull String packageName, int uid) {
         final LocalCallingIdentity token =
-                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+                clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid,
+                        /*opDescription*/ null));
         try {
             return isCallingIdentitySharedPackageName(packageName);
         } finally {
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 387ced0..e09feca 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -17,8 +17,6 @@
 package com.android.providers.media.util;
 
 import static android.Manifest.permission.BACKUP;
-import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
-import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
 import static android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE;
@@ -33,6 +31,7 @@
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.AppOpsManager;
 import android.content.Context;
 import android.provider.MediaStore;
@@ -44,8 +43,8 @@
 
     private static volatile int sLegacyMediaProviderUid = -1;
 
-    public static boolean checkPermissionSystem(Context context,
-            int pid, int uid, String packageName) {
+    public static boolean checkPermissionSystem(
+            @NonNull Context context, int pid, int uid, String packageName) {
         // Apps sharing legacy MediaProvider's uid like DownloadProvider and MTP are treated as
         // system.
         return uid == android.os.Process.SYSTEM_UID || uid == android.os.Process.myUid()
@@ -53,87 +52,124 @@
                 || isLegacyMediaProvider(context, uid);
     }
 
-    public static boolean checkPermissionBackup(Context context, int pid, int uid) {
+    public static boolean checkPermissionBackup(@NonNull Context context, int pid, int uid) {
         return context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED;
     }
 
-    public static boolean checkPermissionManageExternalStorage(Context context, int pid, int uid,
-            String packageName) {
-        return hasAppOpPermission(context, pid, uid, packageName, OPSTR_MANAGE_EXTERNAL_STORAGE);
+    public static boolean checkPermissionManageExternalStorage(@NonNull Context context, int pid,
+            int uid, @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        return noteAppOpPermission(context, pid, uid, packageName, OPSTR_MANAGE_EXTERNAL_STORAGE,
+                attributionTag, opMessage);
     }
 
-    public static boolean checkPermissionWriteStorage(Context context,
-            int pid, int uid, String packageName) {
-        return checkPermissionAndAppOp(context, pid,
-                uid, packageName, WRITE_EXTERNAL_STORAGE, OPSTR_WRITE_EXTERNAL_STORAGE);
+    public static boolean checkPermissionWriteStorage(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        return noteAppOpPermission(context, pid, uid, packageName, OPSTR_WRITE_EXTERNAL_STORAGE,
+                attributionTag, opMessage);
     }
 
-    public static boolean checkPermissionReadStorage(Context context,
-            int pid, int uid, String packageName) {
-        return checkPermissionAndAppOp(context, pid,
-                uid, packageName, READ_EXTERNAL_STORAGE, OPSTR_READ_EXTERNAL_STORAGE);
+    public static boolean checkPermissionReadStorage(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        return noteAppOpPermission(context, pid, uid, packageName, OPSTR_READ_EXTERNAL_STORAGE,
+                attributionTag, opMessage);
     }
 
-    public static boolean checkIsLegacyStorageGranted(Context context, int uid,
-            String packageName) {
+    public static boolean checkIsLegacyStorageGranted(
+            @NonNull Context context, int uid, String packageName) {
         return context.getSystemService(AppOpsManager.class)
                 .unsafeCheckOp(OPSTR_LEGACY_STORAGE, uid, packageName) == MODE_ALLOWED;
     }
 
-    public static boolean checkPermissionReadAudio(Context context,
-            int pid, int uid, String packageName) {
-        if (!checkPermissionAndAppOp(context, pid, uid, packageName,
-                READ_EXTERNAL_STORAGE, OPSTR_READ_EXTERNAL_STORAGE)) return false;
-        return noteAppOpAllowingLegacy(context, pid, uid, packageName,
-                OPSTR_READ_MEDIA_AUDIO);
-    }
-
-    public static boolean checkPermissionWriteAudio(Context context,
-            int pid, int uid, String packageName) {
-        if (!checkPermissionAndAppOpAllowingNonLegacy(context, pid, uid, packageName,
-                WRITE_EXTERNAL_STORAGE, OPSTR_WRITE_EXTERNAL_STORAGE)) return false;
-        return noteAppOpAllowingLegacy(context, pid, uid, packageName,
-                OPSTR_WRITE_MEDIA_AUDIO);
-    }
-
-    public static boolean checkPermissionReadVideo(Context context,
-            int pid, int uid, String packageName) {
-        if (!checkPermissionAndAppOp(context, pid, uid, packageName,
-                READ_EXTERNAL_STORAGE, OPSTR_READ_EXTERNAL_STORAGE)) return false;
-        return noteAppOpAllowingLegacy(context, pid, uid, packageName,
-                OPSTR_READ_MEDIA_VIDEO);
-    }
-
-    public static boolean checkPermissionWriteVideo(Context context,
-            int pid, int uid, String packageName) {
-        if (!checkPermissionAndAppOpAllowingNonLegacy(context, pid, uid, packageName,
-                WRITE_EXTERNAL_STORAGE, OPSTR_WRITE_EXTERNAL_STORAGE)) return false;
-        return noteAppOpAllowingLegacy(context, pid, uid, packageName,
-                OPSTR_WRITE_MEDIA_VIDEO);
-    }
-
-    public static boolean checkPermissionReadImages(Context context,
-            int pid, int uid, String packageName) {
-        if (!checkPermissionAndAppOp(context, pid, uid, packageName,
-                READ_EXTERNAL_STORAGE, OPSTR_READ_EXTERNAL_STORAGE)) return false;
-        return noteAppOpAllowingLegacy(context, pid, uid, packageName,
-                OPSTR_READ_MEDIA_IMAGES);
-    }
-
-    public static boolean checkPermissionWriteImages(Context context,
-            int pid, int uid, String packageName) {
-        if (!checkPermissionAndAppOpAllowingNonLegacy(context, pid, uid, packageName,
-                WRITE_EXTERNAL_STORAGE, OPSTR_WRITE_EXTERNAL_STORAGE)) return false;
-        return noteAppOpAllowingLegacy(context, pid, uid, packageName,
-                OPSTR_WRITE_MEDIA_IMAGES);
-    }
-
-    private static boolean checkPermissionAndAppOp(Context context,
-            int pid, int uid, String packageName, String permission, String op) {
-        if (context.checkPermission(permission, pid, uid) != PERMISSION_GRANTED) {
+    public static boolean checkPermissionReadAudio(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        if (!checkPermissionAppOp(context, pid, uid, packageName, OPSTR_READ_EXTERNAL_STORAGE)) {
             return false;
         }
+        return noteAppOpAllowingLegacy(
+                context, pid, uid, packageName, OPSTR_READ_MEDIA_AUDIO, attributionTag, opMessage);
+    }
 
+    public static boolean checkPermissionWriteAudio(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        if (!checkPermissionAppOpAllowingNonLegacy(
+                    context, pid, uid, packageName, OPSTR_WRITE_EXTERNAL_STORAGE)) {
+            return false;
+        }
+        return noteAppOpAllowingLegacy(
+                context, pid, uid, packageName, OPSTR_WRITE_MEDIA_AUDIO, attributionTag, opMessage);
+    }
+
+    public static boolean checkPermissionReadVideo(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        if (!checkPermissionAppOp(context, pid, uid, packageName, OPSTR_READ_EXTERNAL_STORAGE)) {
+            return false;
+        }
+        return noteAppOpAllowingLegacy(
+                context, pid, uid, packageName, OPSTR_READ_MEDIA_VIDEO, attributionTag, opMessage);
+    }
+
+    public static boolean checkPermissionWriteVideo(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        if (!checkPermissionAppOpAllowingNonLegacy(
+                    context, pid, uid, packageName, OPSTR_WRITE_EXTERNAL_STORAGE)) {
+            return false;
+        }
+        return noteAppOpAllowingLegacy(
+                context, pid, uid, packageName, OPSTR_WRITE_MEDIA_VIDEO, attributionTag, opMessage);
+    }
+
+    public static boolean checkPermissionReadImages(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        if (!checkPermissionAppOp(context, pid, uid, packageName, OPSTR_READ_EXTERNAL_STORAGE)) {
+            return false;
+        }
+        return noteAppOpAllowingLegacy(
+                context, pid, uid, packageName, OPSTR_READ_MEDIA_IMAGES, attributionTag, opMessage);
+    }
+
+    public static boolean checkPermissionWriteImages(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @Nullable String attributionTag,
+            @Nullable String opMessage) {
+        if (!checkPermissionAppOpAllowingNonLegacy(
+                    context, pid, uid, packageName, OPSTR_WRITE_EXTERNAL_STORAGE)) {
+            return false;
+        }
+        return noteAppOpAllowingLegacy(context, pid, uid, packageName, OPSTR_WRITE_MEDIA_IMAGES,
+                attributionTag, opMessage);
+    }
+
+    /**
+     * Generates a message to be used with the different {@link AppOpsManager#noteOp} variations.
+     * If the supplied description is {@code null}, the returned message will be {@code null}.
+     */
+    public static String generateAppOpMessage(
+            @NonNull String packageName, @Nullable String description) {
+        if (description == null) {
+            return null;
+        }
+        return "Package: " + packageName + ". Description: " + description + ".";
+    }
+
+    /**
+     * Checks the permission associated with the given app-op, if it's not granted, returns false.
+     * Else, checks the app-op and returns true iff it's {@link AppOpsManager#MODE_ALLOWED}.
+     * The permission is retrieved from {@link AppOpsManager#opToPermission(String)}.
+     */
+    private static boolean checkPermissionAppOp(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @NonNull String op) {
+        final String permission = AppOpsManager.opToPermission(op);
+        if (permission != null
+                && context.checkPermission(permission, pid, uid) != PERMISSION_GRANTED) {
+            return false;
+        }
         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
         try {
             appOps.checkPackage(uid, packageName);
@@ -155,50 +191,57 @@
     }
 
     /**
-     * Checks if the given package has the given {@code permission} and {@code op}, but allows it
-     * to bypass the permission and app-op check if it's NOT a legacy app, i.e. doesn't hold
-     * {@link AppOpsManager#OPSTR_LEGACY_STORAGE}. This is useful for deprecated permissions and/or
-     * app-ops, like {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE}
-     * @see #checkPermissionAndAppOp
+     * Similar to {@link #checkPermissionAppOp(Context, int, int, String, String)}, but also returns
+     * true for non-legacy apps.
+     * @see #checkPermissionAppOp
      */
-    private static boolean checkPermissionAndAppOpAllowingNonLegacy(Context context,
-            int pid, int uid, String packageName, String permission, String op) {
+    private static boolean checkPermissionAppOpAllowingNonLegacy(@NonNull Context context, int pid,
+            int uid, @NonNull String packageName, @NonNull String op) {
         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
-        try {
-            appOps.checkPackage(uid, packageName);
-        } catch (SecurityException e) {
-            return false;
-        }
+
         // Allowing non legacy apps to bypass this check
         if (appOps.unsafeCheckOpNoThrow(OPSTR_LEGACY_STORAGE, uid,
                 packageName) != AppOpsManager.MODE_ALLOWED) return true;
 
         // Seems like it's a legacy app, so it has to pass the permission and app-op check
-        return checkPermissionAndAppOp(context, pid, uid, packageName, permission, op);
+        return checkPermissionAppOp(context, pid, uid, packageName, op);
     }
 
     /**
-     * Checks if calling app is allowed the app-op. If its app-op mode is
+     * Notes app-op for the callings package. If its app-op mode is
      * {@link AppOpsManager#MODE_DEFAULT} then it falls back to checking the appropriate permission
      * for the app-op. The permission is retrieved from
      * {@link AppOpsManager#opToPermission(String)}.
      */
-    private static boolean hasAppOpPermission(@NonNull Context context, int pid, int uid,
-            @NonNull String packageName, @NonNull String op) {
+    private static boolean noteAppOpPermission(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @NonNull String op, @Nullable String attributionTag,
+            @Nullable String opMessage) {
         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
-        final int mode = appOps.noteOpNoThrow(op, uid, packageName, null, null);
-        if (mode == AppOpsManager.MODE_DEFAULT) {
-            final String permission = AppOpsManager.opToPermission(op);
-            return permission != null
-                    && context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
+        final int mode = appOps.noteOpNoThrow(op, uid, packageName, attributionTag, opMessage);
+        switch (mode) {
+            case AppOpsManager.MODE_ALLOWED:
+                return true;
+            case AppOpsManager.MODE_DEFAULT:
+                final String permission = AppOpsManager.opToPermission(op);
+                return permission != null
+                        && context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
+            case AppOpsManager.MODE_IGNORED:
+            case AppOpsManager.MODE_ERRORED:
+                return false;
+            default:
+                throw new IllegalStateException(op + " has unknown mode " + mode);
         }
-        return mode == AppOpsManager.MODE_ALLOWED;
     }
 
-    private static boolean noteAppOpAllowingLegacy(Context context,
-            int pid, int uid, String packageName, String op) {
+    /**
+     * Similar to  {@link #noteAppOpPermission(Context, int, int, String, String, String, String)},
+     * but also returns true for legacy apps.
+     */
+    private static boolean noteAppOpAllowingLegacy(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @NonNull String op, @Nullable String attributionTag,
+            @Nullable String opMessage) {
         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
-        final int mode = appOps.noteOpNoThrow(op, uid, packageName);
+        final int mode = appOps.noteOpNoThrow(op, uid, packageName, attributionTag, opMessage);
         switch (mode) {
             case AppOpsManager.MODE_ALLOWED:
                 return true;
diff --git a/tests/jni/FuseDaemonTest/AndroidManifest.xml b/tests/jni/FuseDaemonTest/AndroidManifest.xml
index cdae039..17e2f22 100644
--- a/tests/jni/FuseDaemonTest/AndroidManifest.xml
+++ b/tests/jni/FuseDaemonTest/AndroidManifest.xml
@@ -16,7 +16,8 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.tests.fused" >
-
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
     <application>
         <receiver android:name="com.android.cts.install.lib.LocalIntentSender"
diff --git a/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java
index f7af146..aba6537 100644
--- a/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java
+++ b/tests/jni/FuseDaemonTest/FilePathAccessTestHelper/src/com/android/tests/fused/FilePathAccessTestHelper.java
@@ -20,6 +20,7 @@
 import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadata;
 import static com.android.tests.fused.lib.TestUtils.CREATE_FILE_QUERY;
 import static com.android.tests.fused.lib.TestUtils.DELETE_FILE_QUERY;
+import static com.android.tests.fused.lib.TestUtils.CAN_READ_WRITE_QUERY;
 import static com.android.tests.fused.lib.TestUtils.INTENT_EXCEPTION;
 import static com.android.tests.fused.lib.TestUtils.INTENT_EXTRA_PATH;
 import static com.android.tests.fused.lib.TestUtils.OPEN_FILE_FOR_READ_QUERY;
@@ -30,6 +31,7 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
+import android.os.Environment;
 import android.util.Log;
 
 import com.android.tests.fused.lib.ReaddirTestHelper;
@@ -46,6 +48,9 @@
  */
 public class FilePathAccessTestHelper extends Activity {
     private static final String TAG = "FilePathAccessTestHelper";
+    private static final File ANDROID_DIR = new File(Environment.getExternalStorageDirectory(),
+            "Android");
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -55,6 +60,7 @@
             case READDIR_QUERY:
                 sendDirectoryEntries(queryType);
                 break;
+            case CAN_READ_WRITE_QUERY:
             case CREATE_FILE_QUERY:
             case DELETE_FILE_QUERY:
             case OPEN_FILE_FOR_READ_QUERY:
@@ -110,7 +116,10 @@
             final File file = new File(filePath);
             boolean returnStatus = false;
             try {
-                if (queryType.equals(CREATE_FILE_QUERY)) {
+                if (queryType.equals(CAN_READ_WRITE_QUERY)) {
+                    returnStatus = file.exists() && file.canRead() && file.canWrite();
+                } else if (queryType.equals(CREATE_FILE_QUERY)) {
+                    maybeCreateParentDirInAndroid(file);
                     returnStatus = file.createNewFile();
                 } else if (queryType.equals(DELETE_FILE_QUERY)) {
                     returnStatus = file.delete();
@@ -129,4 +138,40 @@
             Log.e(TAG, "file path not set from launcher app");
         }
     }
+
+    private void maybeCreateParentDirInAndroid(File file) {
+        if (!file.getAbsolutePath().startsWith(ANDROID_DIR.getAbsolutePath())) {
+            return;
+        }
+        String[] segments = file.getAbsolutePath().split("/");
+        int index = ANDROID_DIR.getAbsolutePath().split("/").length;
+        if (index < segments.length) {
+            // Create the external app dir first.
+            if (createExternalAppDir(segments[index])) {
+                // Then create everything along the path.
+                file.getParentFile().mkdirs();
+            }
+        }
+    }
+
+    private boolean createExternalAppDir(String name) {
+        // Apps are not allowed to create data/cache/obb etc under Android directly and are expected
+        // to call one of the following methods.
+        switch (name) {
+            case "data":
+                getApplicationContext().getExternalFilesDir(null);
+                return true;
+            case "cache":
+                getApplicationContext().getExternalCacheDir();
+                return true;
+            case "obb":
+                getApplicationContext().getObbDir();
+                return true;
+            case "media":
+                getApplicationContext().getExternalMediaDirs();
+                return true;
+            default:
+                return false;
+        }
+    }
 }
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index 7309cf3..ef05f13 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
@@ -64,6 +64,12 @@
         executeShellCommand("mkdir /sdcard/Android/data/com.android.shell/files -m 2770");
     }
 
+    @Before
+    public void revokeStoragePermissions() throws Exception {
+        revokePermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+    }
+
     @After
     public void tearDown() throws Exception {
         executeShellCommand("rm -r /sdcard/Android/data/com.android.shell");
@@ -162,6 +168,13 @@
     }
 
     @Test
+    public void testCaseInsensitivity() throws Exception {
+        runDeviceTest("testCreateLowerCaseDeleteUpperCase");
+        runDeviceTest("testCreateUpperCaseDeleteLowerCase");
+        runDeviceTest("testCreateMixedCaseDeleteDifferentMixedCase");
+    }
+
+    @Test
     public void testCallingIdentityCacheInvalidation() throws Exception {
         // General IO access
         runDeviceTest("testReadStorageInvalidation");
@@ -287,4 +300,38 @@
     public void testRenameCanRestoreDeletedRowId() throws Exception {
         runDeviceTest("testRenameCanRestoreDeletedRowId");
     }
+
+    @Test
+    public void testAccess_file() throws Exception {
+        grantPermissions("android.permission.READ_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testAccess_file");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE");
+        }
+    }
+
+    @Test
+    public void testAccess_directory() throws Exception {
+        grantPermissions("android.permission.WRITE_EXTERNAL_STORAGE",
+                "android.permission.READ_EXTERNAL_STORAGE");
+        try {
+            runDeviceTest("testAccess_directory");
+        } finally {
+            revokePermissions("android.permission.READ_EXTERNAL_STORAGE",
+                    "android.permission.READ_EXTERNAL_STORAGE");
+        }
+    }
+
+    private void grantPermissions(String... perms) throws Exception {
+        for (String perm : perms) {
+            executeShellCommand("pm grant com.android.tests.fused " + perm);
+        }
+    }
+
+    private void revokePermissions(String... perms) throws Exception {
+        for (String perm : perms) {
+            executeShellCommand("pm revoke com.android.tests.fused " + perm);
+        }
+    }
 }
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
index db60c18..98c5aa1 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
@@ -83,6 +83,7 @@
     public static final String DELETE_FILE_QUERY = "com.android.tests.fused.deletefile";
     public static final String OPEN_FILE_FOR_READ_QUERY = "com.android.tests.fused.openfile_read";
     public static final String OPEN_FILE_FOR_WRITE_QUERY = "com.android.tests.fused.openfile_write";
+    public static final String CAN_READ_WRITE_QUERY = "com.android.tests.fused.can_read_and_write";
 
     public static final String STR_DATA1 = "Just some random text";
     public static final String STR_DATA2 = "More arbitrary stuff";
@@ -192,6 +193,15 @@
     }
 
     /**
+     * Returns {@code true} iff the given {@code path} exists and is readable and
+     * writable for for {@code testApp}.
+     */
+    public static boolean canReadAndWriteAs(TestApp testApp, String path)
+            throws Exception {
+        return getResultFromTestApp(testApp, path, CAN_READ_WRITE_QUERY);
+    }
+
+    /**
      * Makes the given {@code testApp} read the EXIF metadata from the given file and returns the
      * result as an {@link HashMap}
      */
@@ -248,6 +258,13 @@
     }
 
     /**
+     * Installs a {@link TestApp} without storage permissions.
+     */
+    public static void installApp(TestApp testApp) throws Exception {
+        installApp(testApp, /* grantStoragePermission */ false);
+    }
+
+    /**
      * Installs a {@link TestApp} and may grant it storage permissions.
      */
     public static void installApp(TestApp testApp, boolean grantStoragePermission)
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index 87f7641..13eb57d 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -19,6 +19,9 @@
 import static android.app.AppOpsManager.permissionToOp;
 import static android.os.SystemProperties.getBoolean;
 import static android.provider.MediaStore.MediaColumns;
+import static android.system.OsConstants.F_OK;
+import static android.system.OsConstants.R_OK;
+import static android.system.OsConstants.W_OK;
 
 import static androidx.test.InstrumentationRegistry.getContext;
 
@@ -38,6 +41,7 @@
 import static com.android.tests.fused.lib.TestUtils.assertFileContent;
 import static com.android.tests.fused.lib.TestUtils.assertThrows;
 import static com.android.tests.fused.lib.TestUtils.canOpen;
+import static com.android.tests.fused.lib.TestUtils.canReadAndWriteAs;
 import static com.android.tests.fused.lib.TestUtils.createFileAs;
 import static com.android.tests.fused.lib.TestUtils.deleteFileAs;
 import static com.android.tests.fused.lib.TestUtils.deleteFileAsNoThrow;
@@ -54,6 +58,7 @@
 import static com.android.tests.fused.lib.TestUtils.listAs;
 import static com.android.tests.fused.lib.TestUtils.openFileAs;
 import static com.android.tests.fused.lib.TestUtils.openWithMediaProvider;
+import static com.android.tests.fused.lib.TestUtils.pollForPermission;
 import static com.android.tests.fused.lib.TestUtils.readExifMetadataFromTestApp;
 import static com.android.tests.fused.lib.TestUtils.revokePermission;
 import static com.android.tests.fused.lib.TestUtils.setupDefaultDirectories;
@@ -79,6 +84,7 @@
 
 import static junit.framework.Assert.fail;
 
+import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
 import android.Manifest;
@@ -140,6 +146,8 @@
     static final String NONMEDIA_FILE_NAME = "FilePathAccessTest_file.pdf";
 
     static final String FILE_CREATION_ERROR_MESSAGE = "No such file or directory";
+    private static final File ANDROID_DIR = new File(Environment.getExternalStorageDirectory(),
+            "Android");
 
     private static final TestApp TEST_APP_A  = new TestApp("TestAppA",
             "com.android.tests.fused.testapp.A", 1, false, "TestAppA.apk");
@@ -582,14 +590,8 @@
             assertThat(listAs(TEST_APP_B, dir.getPath())).doesNotContain(videoFileName);
         } finally {
             uninstallAppNoThrow(TEST_APP_B);
-            if(videoFile.exists()) {
-                deleteFileAsNoThrow(TEST_APP_A, videoFile.getPath());
-            }
-            if (dir.exists()) {
-                  // Try deleting the directory. Do we delete directory if app doesn't own all
-                  // files in it?
-                  dir.delete();
-            }
+            deleteFileAsNoThrow(TEST_APP_A, videoFile.getPath());
+            dir.delete();
             uninstallAppNoThrow(TEST_APP_A);
         }
     }
@@ -624,12 +626,8 @@
             assertThat(listAs(TEST_APP_B, dir.getPath())).doesNotContain(pdfFileName);
         } finally {
             uninstallAppNoThrow(TEST_APP_B);
-            if(pdfFile.exists()) {
-                deleteFileAsNoThrow(TEST_APP_A, pdfFile.getPath());
-            }
-            if (dir.exists()) {
-                  dir.delete();
-            }
+            deleteFileAsNoThrow(TEST_APP_A, pdfFile.getPath());
+            dir.delete();
             uninstallAppNoThrow(TEST_APP_A);
         }
     }
@@ -955,6 +953,46 @@
     }
 
     @Test
+    public void testCreateLowerCaseDeleteUpperCase() throws Exception {
+        File upperCase = new File(DOWNLOAD_DIR, "CREATE_LOWER_DELETE_UPPER");
+        File lowerCase = new File(DOWNLOAD_DIR, "create_lower_delete_upper");
+
+        createDeleteCreate(lowerCase, upperCase);
+    }
+
+    @Test
+    public void testCreateUpperCaseDeleteLowerCase() throws Exception {
+        File upperCase = new File(DOWNLOAD_DIR, "CREATE_UPPER_DELETE_LOWER");
+        File lowerCase = new File(DOWNLOAD_DIR, "create_upper_delete_lower");
+
+        createDeleteCreate(upperCase, lowerCase);
+    }
+
+    @Test
+    public void testCreateMixedCaseDeleteDifferentMixedCase() throws Exception {
+        File mixedCase1 = new File(DOWNLOAD_DIR, "CrEaTe_MiXeD_dElEtE_mIxEd");
+        File mixedCase2 = new File(DOWNLOAD_DIR, "cReAtE_mIxEd_DeLeTe_MiXeD");
+
+        createDeleteCreate(mixedCase1, mixedCase2);
+    }
+
+    private void createDeleteCreate(File create, File delete) throws Exception {
+        try {
+            assertThat(create.createNewFile()).isTrue();
+            Thread.sleep(5);
+
+            assertThat(delete.delete()).isTrue();
+            Thread.sleep(5);
+
+            assertThat(create.createNewFile()).isTrue();
+            Thread.sleep(5);
+        } finally {
+            create.delete();
+            create.delete();
+        }
+    }
+
+    @Test
     public void testReadStorageInvalidation() throws Exception {
         testAppOpInvalidation(TEST_APP_C, new File(DCIM_DIR, "read_storage.jpg"),
                 Manifest.permission.READ_EXTERNAL_STORAGE,
@@ -1361,9 +1399,7 @@
             // TODO(b/146346138): Test that app with right URI permission should be able to rename
             // the corresponding file
         } finally {
-            if(videoFile1.exists()) {
-                deleteFileAsNoThrow(TEST_APP_A, videoFile1.getAbsolutePath());
-            }
+            deleteFileAsNoThrow(TEST_APP_A, videoFile1.getAbsolutePath());
             videoFile2.delete();
             uninstallAppNoThrow(TEST_APP_A);
         }
@@ -1561,6 +1597,83 @@
     }
 
     @Test
+    public void testAccess_file() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+
+        final File otherAppPdf = new File(DOWNLOAD_DIR, "other-" + NONMEDIA_FILE_NAME);
+        final File otherAppImage = new File(DCIM_DIR, "other-" + IMAGE_FILE_NAME);
+        final File myAppPdf = new File(DOWNLOAD_DIR, "my-" + NONMEDIA_FILE_NAME);
+        final File doesntExistPdf = new File(DOWNLOAD_DIR, "nada-" + NONMEDIA_FILE_NAME);
+
+        try {
+            installApp(TEST_APP_A);
+
+            assertThat(createFileAs(TEST_APP_A, otherAppPdf.getPath())).isTrue();
+            assertThat(createFileAs(TEST_APP_A, otherAppImage.getPath())).isTrue();
+
+            // We can read our image and pdf files.
+            assertThat(myAppPdf.createNewFile()).isTrue();
+            assertFileAccess_readWrite(myAppPdf);
+
+            // We can read the other app's image file because we hold R_E_S, but we can only
+            // check exists for the pdf file.
+            assertFileAccess_readOnly(otherAppImage);
+            assertFileAccess_existsOnly(otherAppPdf);
+            assertAccess(doesntExistPdf, false, false, false);
+        } finally {
+            deleteFileAsNoThrow(TEST_APP_A, otherAppPdf.getAbsolutePath());
+            deleteFileAsNoThrow(TEST_APP_A, otherAppImage.getAbsolutePath());
+            myAppPdf.delete();
+            uninstallApp(TEST_APP_A);
+        }
+    }
+
+    @Test
+    public void testAccess_directory() throws Exception {
+        pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, /*granted*/ true);
+        pollForPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, /*granted*/ true);
+        try {
+            installApp(TEST_APP_A);
+
+            // Let app A create a file in its data dir
+            final File otherAppExternalDataDir = new File(EXTERNAL_FILES_DIR.getPath()
+                    .replace(THIS_PACKAGE_NAME, TEST_APP_A.getPackageName()));
+            final File otherAppExternalDataSubDir = new File(otherAppExternalDataDir, "subdir");
+            final File otherAppExternalDataFile = new File(otherAppExternalDataSubDir, "abc.jpg");
+            assertThat(
+                    createFileAs(TEST_APP_A, otherAppExternalDataFile.getAbsolutePath())).isTrue();
+
+            // TODO(152645823): Readd app data dir testss
+//            // We cannot read or write the file, but app A can.
+//            assertThat(canReadAndWriteAs(TEST_APP_A,
+//                    otherAppExternalDataFile.getAbsolutePath())).isTrue();
+//            assertAccess(otherAppExternalDataFile, true, false, false);
+//
+//            // We cannot read or write the dir, but app A can.
+//            assertThat(canReadAndWriteAs(TEST_APP_A,
+//                    otherAppExternalDataDir.getAbsolutePath())).isTrue();
+//            assertAccess(otherAppExternalDataDir, true, false, false);
+//
+//            // We cannot read or write the sub dir, but app A can.
+//            assertThat(canReadAndWriteAs(TEST_APP_A,
+//                    otherAppExternalDataSubDir.getAbsolutePath())).isTrue();
+//            assertAccess(otherAppExternalDataSubDir, true, false, false);
+//
+//            // We can read and write our own app dir, but app A cannot.
+//            assertThat(canReadAndWriteAs(TEST_APP_A,
+//                    EXTERNAL_FILES_DIR.getAbsolutePath())).isFalse();
+            assertAccess(EXTERNAL_FILES_DIR, true, true, true);
+
+            assertDirectoryAccess(DCIM_DIR, /* exists */ true);
+            assertDirectoryAccess(EXTERNAL_STORAGE_DIR, true);
+            assertDirectoryAccess(new File(EXTERNAL_STORAGE_DIR, "Android"), true);
+            assertDirectoryAccess(new File(EXTERNAL_STORAGE_DIR, "doesnt/exist"), false);
+        } finally {
+            uninstallApp(TEST_APP_A);  // Uninstalling deletes external app dirs
+        }
+    }
+
+    @Test
     public void testManageExternalStorageCanRenameOtherAppsContents() throws Exception {
         final File otherAppPdf = new File(DOWNLOAD_DIR, "other" + NONMEDIA_FILE_NAME);
         final File pdf = new File(DOWNLOAD_DIR, NONMEDIA_FILE_NAME);
@@ -1704,7 +1817,7 @@
         final File otherAppVideoFile2 = new File(dirInPictures, "other_" + VIDEO_FILE_NAME);
         final File otherAppPdfFile2 = new File(dirInPictures, "other_" + NONMEDIA_FILE_NAME);
         try {
-            assertThat(dirInDcim.mkdir()).isTrue();
+            assertThat( dirInDcim.exists() || dirInDcim.mkdir()).isTrue();
 
             executeShellCommand("touch " + otherAppPdfFile1);
 
@@ -1888,4 +2001,57 @@
                     + "running the test!");
         }
     }
+
+    private static void assertFileAccess_existsOnly(File file) throws Exception {
+        assertThat(file.isFile()).isTrue();
+        assertAccess(file, true, false, false);
+    }
+
+    private static void assertFileAccess_readOnly(File file) throws Exception {
+        assertThat(file.isFile()).isTrue();
+        assertAccess(file, true, true, false);
+    }
+
+    private static void assertFileAccess_readWrite(File file) throws Exception {
+        assertThat(file.isFile()).isTrue();
+        assertAccess(file, true, true, true);
+    }
+
+    private static void assertDirectoryAccess(File dir, boolean exists) throws Exception {
+        // This util does not handle app data directories.
+        assumeFalse(dir.getAbsolutePath().startsWith(ANDROID_DIR.getAbsolutePath())
+                && !dir.equals(ANDROID_DIR));
+        assertThat(dir.isDirectory()).isEqualTo(exists);
+        // For non-app data directories, exists => canRead() and canWrite().
+         assertAccess(dir, exists, exists, exists);
+    }
+
+    private static void assertAccess(File file, boolean exists, boolean canRead, boolean canWrite)
+            throws Exception {
+        assertThat(file.exists()).isEqualTo(exists);
+        assertThat(file.canRead()).isEqualTo(canRead);
+        assertThat(file.canWrite()).isEqualTo(canWrite);
+        if (file.isDirectory()) {
+            assertThat(file.canExecute()).isEqualTo(exists);
+        } else {
+            assertThat(file.canExecute()).isFalse();  // Filesytem is mounted with MS_NOEXEC
+        }
+
+        // Test some combinations of mask.
+        assertAccess(file, R_OK, canRead);
+        assertAccess(file, W_OK, canWrite);
+        assertAccess(file, R_OK | W_OK, canRead && canWrite);
+        assertAccess(file, W_OK | F_OK, canWrite);
+        assertAccess(file, F_OK, exists);
+    }
+
+    private static void assertAccess(File file, int mask, boolean expected) throws Exception {
+        if (expected) {
+            assertThat(Os.access(file.getAbsolutePath(), mask)).isTrue();
+        } else {
+            assertThrows(ErrnoException.class, () -> {
+                Os.access(file.getAbsolutePath(), mask);
+            });
+        }
+    }
 }
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index 425bb67..d581921 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -59,16 +59,17 @@
 
         assertTrue(checkPermissionSystem(context, pid, uid, packageName));
         assertFalse(checkPermissionBackup(context, pid, uid));
-        assertFalse(checkPermissionManageExternalStorage(context, pid, uid, packageName));
+        assertFalse(
+                checkPermissionManageExternalStorage(context, pid, uid, packageName, null, null));
 
-        assertTrue(checkPermissionReadStorage(context, pid, uid, packageName));
-        assertTrue(checkPermissionWriteStorage(context, pid, uid, packageName));
+        assertTrue(checkPermissionReadStorage(context, pid, uid, packageName, null, null));
+        assertTrue(checkPermissionWriteStorage(context, pid, uid, packageName, null, null));
 
-        assertTrue(checkPermissionReadAudio(context, pid, uid, packageName));
-        assertFalse(checkPermissionWriteAudio(context, pid, uid, packageName));
-        assertTrue(checkPermissionReadVideo(context, pid, uid, packageName));
-        assertFalse(checkPermissionWriteVideo(context, pid, uid, packageName));
-        assertTrue(checkPermissionReadImages(context, pid, uid, packageName));
-        assertFalse(checkPermissionWriteImages(context, pid, uid, packageName));
+        assertTrue(checkPermissionReadAudio(context, pid, uid, packageName, null, null));
+        assertFalse(checkPermissionWriteAudio(context, pid, uid, packageName, null, null));
+        assertTrue(checkPermissionReadVideo(context, pid, uid, packageName, null, null));
+        assertFalse(checkPermissionWriteVideo(context, pid, uid, packageName, null, null));
+        assertTrue(checkPermissionReadImages(context, pid, uid, packageName, null, null));
+        assertFalse(checkPermissionWriteImages(context, pid, uid, packageName, null, null));
     }
 }