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));
}
}