Merge "Revert "Add permission checks for setattr"" into rvc-dev
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 6c7f671..185d29b 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -992,6 +992,17 @@
/**
* Absolute filesystem path to the media item on disk.
* <p>
+ * On Android 11, you can use this value when you access an existing
+ * media file using direct file paths. That's because this value has
+ * a valid file path. However, don't assume that the file is always
+ * available. Be prepared to handle any file-based I/O errors that
+ * could occur.
+ * <p>
+ * Don't use this value when you create or update a media file, even
+ * if you're on Android 11 and are using direct file paths. Instead,
+ * use the values of the {@link #DISPLAY_NAME} and
+ * {@link #RELATIVE_PATH} columns.
+ * <p>
* Note that apps may not have filesystem permissions to directly access
* this path. Instead of trying to open this path directly, apps should
* use {@link ContentResolver#openFileDescriptor(Uri, String)} to gain
diff --git a/apex/testing/test_manifest.json b/apex/testing/test_manifest.json
index ffef8fb..8ec7fba 100644
--- a/apex/testing/test_manifest.json
+++ b/apex/testing/test_manifest.json
@@ -1,4 +1,4 @@
{
"name": "com.android.mediaprovider",
- "version": 300000000
+ "version": 2147483647
}
diff --git a/jni/Android.bp b/jni/Android.bp
index 5688e5c..280ddcd 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -21,6 +21,7 @@
"jni_init.cpp",
"com_android_providers_media_FuseDaemon.cpp",
"FuseDaemon.cpp",
+ "FuseUtils.cpp",
"MediaProviderWrapper.cpp",
"ReaddirHelper.cpp",
"RedactionInfo.cpp",
@@ -128,3 +129,39 @@
sdk_version: "current",
stl: "c++_static",
}
+
+cc_test {
+ name: "FuseUtilsTest",
+ test_suites: ["device-tests", "mts"],
+ test_config: "FuseUtilsTest.xml",
+
+ compile_multilib: "both",
+ multilib: {
+ lib32: { suffix: "32", },
+ lib64: { suffix: "64", },
+ },
+
+ srcs: [
+ "FuseUtilsTest.cpp",
+ "FuseUtils.cpp",
+ ],
+
+ header_libs: [
+ "libnativehelper_header_only",
+ ],
+
+ local_include_dirs: ["include"],
+
+ static_libs: [
+ "libbase_ndk",
+ ],
+
+ shared_libs: [
+ "liblog",
+ ],
+
+ tidy: true,
+
+ sdk_version: "current",
+ stl: "c++_static",
+}
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index b23fc52..87d403f 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -61,6 +61,7 @@
#include <vector>
#include "MediaProviderWrapper.h"
+#include "libfuse_jni/FuseUtils.h"
#include "libfuse_jni/ReaddirHelper.h"
#include "libfuse_jni/RedactionInfo.h"
#include "node-inl.h"
@@ -75,8 +76,9 @@
using std::vector;
// logging macros to avoid duplication.
-#define TRACE_NODE(__node) \
- LOG(VERBOSE) << __FUNCTION__ << " : " << #__node << " = [" << get_name(__node) << "] "
+#define TRACE_NODE(__node, __req) \
+ LOG(VERBOSE) << __FUNCTION__ << " : " << #__node << " = [" << get_name(__node) \
+ << "] (uid=" << __req->ctx.uid << ") "
#define ATRACE_NAME(name) ScopedTrace ___tracer(name)
#define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)
@@ -108,9 +110,6 @@
"^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
std::regex_constants::icase);
-const std::regex ANDROID_DATA_OBB_PATH("^/storage/[^/]+/(?:[0-9]+/)?(?:Android)/?(?:data|obb)?$",
- std::regex_constants::icase);
-
/*
* In order to avoid double caching with fuse, call fadvise on the file handles
* in the underlying file system. However, if this is done on every read/write,
@@ -220,9 +219,9 @@
}
std::mutex mutex_;
- std::thread thread_;
- std::queue<Message> queue_;
std::condition_variable cv_;
+ std::queue<Message> queue_;
+ std::thread thread_;
typedef std::multimap<size_t, int> Sizes;
typedef std::map<int, Sizes::iterator> Files;
@@ -246,6 +245,13 @@
inline bool IsRoot(const node* node) const { return node == root; }
+ inline string GetEffectiveRootPath() {
+ if (path.find("/storage/emulated", 0) == 0) {
+ return path + "/" + std::to_string(getuid() / PER_USER_RANGE);
+ }
+ return path;
+ }
+
// Note that these two (FromInode / ToInode) conversion wrappers are required
// because fuse_lowlevel_ops documents that the root inode is always one
// (see FUSE_ROOT_ID in fuse_lowlevel.h). There are no particular requirements
@@ -293,10 +299,8 @@
static inline string get_name(node* n) {
if (n) {
- std::string name("node_path: " + n->BuildSafePath());
- if (IS_OS_DEBUGABLE) {
- name += " real_path: " + n->BuildPath();
- }
+ std::string name = IS_OS_DEBUGABLE ? "real_path: " + n->BuildPath() + " " : "";
+ name += "node_path: " + n->BuildSafePath();
return name;
}
return "?";
@@ -363,18 +367,36 @@
return std::regex_match(path, PATTERN_OWNED_PATH);
}
-static void invalidate_case_insensitive_dentry_matches(struct fuse* fuse, node* parent,
- const vector<string>& children) {
- 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();
+// See fuse_lowlevel.h fuse_lowlevel_notify_inval_entry for how to call this safetly without
+// deadlocking the kernel
+static void fuse_inval(fuse_session* se, fuse_ino_t parent_ino, fuse_ino_t child_ino,
+ const string& child_name, const string& path) {
+ if (mediaprovider::fuse::containsMount(path, std::to_string(getuid() / PER_USER_RANGE))) {
+ LOG(WARNING) << "Ignoring attempt to invalidate dentry for FUSE mounts";
+ return;
+ }
+
+ if (fuse_lowlevel_notify_inval_entry(se, parent_ino, child_name.c_str(), child_name.size())) {
+ // Invalidating the dentry can fail if there's no dcache entry, however, there may still
+ // be cached attributes, so attempt to invalidate those by invalidating the inode
+ fuse_lowlevel_notify_inval_inode(se, child_ino, 0, 0);
+ }
+}
+
+static double get_timeout(struct fuse* fuse, const string& path, bool should_inval) {
+ string media_path = fuse->GetEffectiveRootPath() + "/Android/media";
+ if (should_inval || path.find(media_path, 0) == 0 || is_package_owned_path(path, fuse->path)) {
+ // We set dentry timeout to 0 for the following reasons:
+ // 1. Case-insensitive lookups need to invalidate other case-insensitive dentry matches
+ // 2. Installd might delete Android/media/<package> dirs when app data is cleared.
+ // This can leave a stale entry in the kernel dcache, and break subsequent creation of the
+ // dir via FUSE.
+ // 3. With app data isolation enabled, app A should not guess existence of app B from the
+ // Android/{data,obb}/<package> paths, hence we prevent the kernel from caching that
+ // information.
+ return 0;
+ }
+ return std::numeric_limits<double>::max();
}
static node* make_node_entry(fuse_req_t req, node* parent, const string& name, const string& path,
@@ -389,21 +411,30 @@
return NULL;
}
+ bool should_inval = false;
node = parent->LookupChildByName(name, true /* acquire */);
if (!node) {
node = ::node::Create(parent, name, &fuse->lock, &fuse->tracker);
- } else if (!std::regex_match(node->BuildPath(), ANDROID_DATA_OBB_PATH)) {
+ } else if (!mediaprovider::fuse::containsMount(path, std::to_string(getuid() / PER_USER_RANGE))) {
+ should_inval = true;
+ // Only invalidate a path if it does not contain mount.
// Invalidate both names to ensure there's no dentry left in the kernel after the following
// operations:
// 1) touch foo, touch FOO, unlink *foo*
// 2) touch foo, touch FOO, unlink *FOO*
// Invalidating lookup_name fixes (1) and invalidating node_name fixes (2)
- vector<string> children;
- children.push_back(name);
- children.push_back(node->GetName());
- invalidate_case_insensitive_dentry_matches(fuse, parent, children);
+ // |should_inval| invalidates lookup_name by using 0 timeout below and we explicitly
+ // invalidate node_name if different case
+ // Note that we invalidate async otherwise we will deadlock the kernel
+ if (name != node->GetName()) {
+ std::thread t([=]() {
+ fuse_inval(fuse->se, fuse->ToInode(parent), fuse->ToInode(node), node->GetName(),
+ path);
+ });
+ t.detach();
+ }
}
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
// 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
@@ -411,10 +442,10 @@
// reuse inode numbers.
e->generation = 0;
e->ino = fuse->ToInode(node);
- e->entry_timeout = is_package_owned_path(path, fuse->path) ?
- 0 : std::numeric_limits<double>::max();
- e->attr_timeout = is_package_owned_path(path, fuse->path) ?
- 0 : std::numeric_limits<double>::max();
+ e->entry_timeout = get_timeout(fuse, path, should_inval);
+ e->attr_timeout = is_package_owned_path(path, fuse->path) || should_inval
+ ? 0
+ : std::numeric_limits<double>::max();
return node;
}
@@ -459,6 +490,12 @@
return true;
}
+ if (path == "/storage/emulated") {
+ // Apps should never refer to /storage/emulated - they should be using the user-spcific
+ // subdirs, eg /storage/emulated/0
+ return false;
+ }
+
std::smatch match;
if (std::regex_match(path, match, PATTERN_OWNED_PATH)) {
const std::string& pkg = match[1];
@@ -485,14 +522,16 @@
return nullptr;
}
string parent_path = parent_node->BuildPath();
- if (!is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
+ // We should always allow lookups on the root, because failing them could cause
+ // bind mounts to be invalidated.
+ if (!fuse->IsRoot(parent_node) && !is_app_accessible_path(fuse->mp, parent_path, req->ctx.uid)) {
*error_code = ENOENT;
return nullptr;
}
string child_path = parent_path + "/" + name;
- TRACE_NODE(parent_node);
+ TRACE_NODE(parent_node, req);
std::smatch match;
std::regex_search(child_path, match, storage_emulated_regex);
@@ -517,9 +556,9 @@
}
}
-static void do_forget(struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
+static void do_forget(fuse_req_t req, struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
node* node = fuse->FromInode(ino);
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
if (node) {
// This is a narrowing conversion from an unsigned 64bit to a 32bit value. For
// some reason we only keep 32 bit refcounts but the kernel issues
@@ -534,7 +573,7 @@
node* node;
struct fuse* fuse = get_fuse(req);
- do_forget(fuse, ino, nlookup);
+ do_forget(req, fuse, ino, nlookup);
fuse_reply_none(req);
}
@@ -545,7 +584,7 @@
struct fuse* fuse = get_fuse(req);
for (int i = 0; i < count; i++) {
- do_forget(fuse, forgets[i].ino, forgets[i].nlookup);
+ do_forget(req, fuse, forgets[i].ino, forgets[i].nlookup);
}
fuse_reply_none(req);
}
@@ -565,7 +604,7 @@
fuse_reply_err(req, ENOENT);
return;
}
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
struct stat s;
memset(&s, 0, sizeof(s));
@@ -596,7 +635,7 @@
}
struct timespec times[2];
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
/* XXX: incomplete implementation on purpose.
* chmod/chown should NEVER be implemented.*/
@@ -632,7 +671,7 @@
}
}
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
if (utimensat(-1, path.c_str(), times, 0) < 0) {
fuse_reply_err(req, errno);
return;
@@ -676,7 +715,7 @@
return;
}
- TRACE_NODE(parent_node);
+ TRACE_NODE(parent_node, req);
const string child_path = parent_path + "/" + name;
@@ -714,7 +753,7 @@
return;
}
- TRACE_NODE(parent_node);
+ TRACE_NODE(parent_node, req);
const string child_path = parent_path + "/" + name;
@@ -755,7 +794,7 @@
return;
}
- TRACE_NODE(parent_node);
+ TRACE_NODE(parent_node, req);
const string child_path = parent_path + "/" + name;
@@ -766,7 +805,7 @@
}
node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
- TRACE_NODE(child_node);
+ TRACE_NODE(child_node, req);
if (child_node) {
child_node->SetDeleted();
}
@@ -787,7 +826,7 @@
fuse_reply_err(req, ENOENT);
return;
}
- TRACE_NODE(parent_node);
+ TRACE_NODE(parent_node, req);
const string child_path = parent_path + "/" + name;
@@ -803,7 +842,7 @@
}
node* child_node = parent_node->LookupChildByName(name, false /* acquire */);
- TRACE_NODE(child_node);
+ TRACE_NODE(child_node, req);
if (child_node) {
child_node->SetDeleted();
}
@@ -848,11 +887,11 @@
return 0;
}
- TRACE_NODE(old_parent_node);
- TRACE_NODE(new_parent_node);
+ TRACE_NODE(old_parent_node, req);
+ TRACE_NODE(new_parent_node, req);
node* child_node = old_parent_node->LookupChildByName(name, true /* acquire */);
- TRACE_NODE(child_node) << "old_child";
+ TRACE_NODE(child_node, req) << "old_child";
const string old_child_path = child_node->BuildPath();
const string new_child_path = new_parent_path + "/" + new_name;
@@ -864,7 +903,7 @@
if (res == 0) {
child_node->Rename(new_name, new_parent_node);
}
- TRACE_NODE(child_node) << "new_child";
+ TRACE_NODE(child_node, req) << "new_child";
child_node->Release(1);
return res;
@@ -922,7 +961,7 @@
return;
}
- TRACE_NODE(node) << (is_requesting_write(fi->flags) ? "write" : "read");
+ TRACE_NODE(node, req) << (is_requesting_write(fi->flags) ? "write" : "read");
if (fi->flags & O_DIRECT) {
fi->flags &= ~O_DIRECT;
@@ -1153,7 +1192,7 @@
struct fuse_file_info* fi) {
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
- TRACE_NODE(nullptr) << "noop";
+ TRACE_NODE(nullptr, req) << "noop";
fuse_reply_err(req, 0);
}
@@ -1165,7 +1204,7 @@
node* node = fuse->FromInode(ino);
handle* h = reinterpret_cast<handle*>(fi->fh);
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
fuse->fadviser.Close(h->fd);
if (node) {
@@ -1220,7 +1259,7 @@
return;
}
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
int status = fuse->mp->IsOpendirAllowed(path, ctx->uid);
if (status) {
@@ -1270,7 +1309,7 @@
return;
}
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
// Get all directory entries from MediaProvider on first readdir() call of
// directory handle. h->next_off = 0 indicates that current readdir() call
// is first readdir() call for the directory handle, Avoid multiple JNI calls
@@ -1307,7 +1346,7 @@
// Ignore lookup errors on
// 1. non-existing files returned from MediaProvider database.
// 2. path that doesn't match FuseDaemon UID and calling uid.
- if (error_code == ENOENT || error_code == EPERM) continue;
+ if (error_code == ENOENT || error_code == EPERM || error_code == EACCES) continue;
fuse_reply_err(req, error_code);
return;
}
@@ -1328,7 +1367,7 @@
// When an entry is rejected, lookup called by readdir_plus will not be tracked by
// kernel. Call forget on the rejected node to decrement the reference count.
if (plus) {
- do_forget(fuse, e.ino, 1);
+ do_forget(req, fuse, e.ino, 1);
}
break;
}
@@ -1361,7 +1400,7 @@
node* node = fuse->FromInode(ino);
dirhandle* h = reinterpret_cast<dirhandle*>(fi->fh);
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
if (node) {
node->DestroyDirHandle(h);
}
@@ -1416,7 +1455,7 @@
fuse_reply_err(req, ENOENT);
return;
}
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
// exists() checks are always allowed.
if (mask == F_OK) {
@@ -1468,7 +1507,7 @@
return;
}
- TRACE_NODE(parent_node);
+ TRACE_NODE(parent_node, req);
const string child_path = parent_path + "/" + name;
@@ -1497,20 +1536,19 @@
return;
}
- // File was inserted to MP database with default mime type/media type values. ScanFile will
- // update the db columns with appropriate values. This is used for hidden file handling.
- fuse->mp->ScanFile(child_path.c_str());
-
int error_code = 0;
struct fuse_entry_param e;
node* node = make_node_entry(req, parent_node, name, child_path, &e, &error_code);
- TRACE_NODE(node);
+ TRACE_NODE(node, req);
if (!node) {
CHECK(error_code != 0);
fuse_reply_err(req, error_code);
return;
}
+ // Let MediaProvider know we've created a new file
+ fuse->mp->OnFileCreated(child_path);
+
// TODO(b/147274248): Assume there will be no EXIF to redact.
// This prevents crashing during reads but can be a security hole if a malicious app opens an fd
// to the file before all the EXIF content is written. We could special case reads before the
@@ -1650,19 +1688,19 @@
if (active.load(std::memory_order_acquire)) {
string name;
fuse_ino_t parent;
-
+ fuse_ino_t child;
{
std::lock_guard<std::recursive_mutex> guard(fuse->lock);
const node* node = node::LookupAbsolutePath(fuse->root, path);
if (node) {
name = node->GetName();
+ child = fuse->ToInode(const_cast<class node*>(node));
parent = fuse->ToInode(node->GetParent());
}
}
- if (!name.empty() &&
- fuse_lowlevel_notify_inval_entry(fuse->se, parent, name.c_str(), name.size())) {
- LOG(WARNING) << "Failed to invalidate dentry for path";
+ if (!name.empty()) {
+ fuse_inval(fuse->se, parent, child, name, path);
}
} else {
LOG(WARNING) << "FUSE daemon is inactive. Cannot invalidate dentry";
@@ -1677,11 +1715,11 @@
}
void FuseDaemon::Start(android::base::unique_fd fd, const std::string& path) {
+ android::base::SetDefaultTag(LOG_TAG);
+
struct fuse_args args;
struct fuse_cmdline_opts opts;
- SetMinimumLogSeverity(android::base::VERBOSE);
-
struct stat stat;
if (lstat(path.c_str(), &stat)) {
diff --git a/jni/FuseUtils.cpp b/jni/FuseUtils.cpp
new file mode 100644
index 0000000..8d6c35a
--- /dev/null
+++ b/jni/FuseUtils.cpp
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#define LOG_TAG "FuseUtils"
+
+#include "include/libfuse_jni/FuseUtils.h"
+
+#include <string>
+#include <vector>
+
+using std::string;
+
+namespace mediaprovider {
+namespace fuse {
+
+bool containsMount(const string& path, const string& userid) {
+ const string& prefix = "/storage/emulated/" + userid;
+ std::vector<string> suffixes = {"/Android", "/Android/data", "/Android/obb"};
+
+ if (path.find(prefix) != 0) {
+ return false;
+ }
+
+ const string& path_suffix = path.substr(prefix.length());
+ return std::find(suffixes.begin(), suffixes.end(), path_suffix) != suffixes.end();
+}
+
+} // namespace fuse
+} // namespace mediaprovider
diff --git a/jni/FuseUtilsTest.cpp b/jni/FuseUtilsTest.cpp
new file mode 100644
index 0000000..9d05bc7
--- /dev/null
+++ b/jni/FuseUtilsTest.cpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "FuseUtilsTest"
+
+#include "libfuse_jni/FuseUtils.h"
+
+#include <gtest/gtest.h>
+
+using namespace mediaprovider::fuse;
+
+TEST(FuseUtilsTest, testContainsMount_isTrueForAndroidDataObb) {
+ EXPECT_TRUE(containsMount("/storage/emulated/1234/Android", "1234"));
+ EXPECT_TRUE(containsMount("/storage/emulated/1234/Android/data", "1234"));
+ EXPECT_TRUE(containsMount("/storage/emulated/1234/Android/obb", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount) {
+ EXPECT_FALSE(containsMount("/random/path", "1234"));
+ EXPECT_FALSE(containsMount("/storage/abc-123", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isCaseSensitive) {
+ EXPECT_FALSE(containsMount("/storage/emulated/1234/android", "1234"));
+ EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/Data", "1234"));
+ EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/OBB", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithAdditionalSlash) {
+ EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/", "1234"));
+ EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/data/", "1234"));
+ EXPECT_FALSE(containsMount("/storage/emulated/1234/Android/obb/", "1234"));
+
+ EXPECT_FALSE(containsMount("//storage/emulated/1234/Android", "1234"));
+ EXPECT_FALSE(containsMount("/storage/emulated/1234//Android/data", "1234"));
+}
+
+TEST(FuseUtilsTest, testContainsMount_isFalseForPathWithWrongUserid) {
+ EXPECT_FALSE(containsMount("/storage/emulated/11234/Android", "1234"));
+ EXPECT_FALSE(containsMount("/storage/emulated/0/Android/data", "1234"));
+ EXPECT_FALSE(containsMount("/storage/emulated/12345/Android/obb", "1234"));
+}
diff --git a/jni/FuseUtilsTest.xml b/jni/FuseUtilsTest.xml
new file mode 100644
index 0000000..46fccac
--- /dev/null
+++ b/jni/FuseUtilsTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- Note: this is derived from the autogenerated configuration. We require
+ root support. -->
+<configuration description="Runs FuseUtilsTest">
+ <option name="test-suite-tag" value="mts" />
+ <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+ <option name="cleanup" value="true" />
+ <option name="push" value="FuseUtilsTest->/data/local/tmp/FuseUtilsTest" />
+ <option name="append-bitness" value="true" />
+ </target_preparer>
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+ <test class="com.android.tradefed.testtype.GTest" >
+ <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="module-name" value="FuseUtilsTest" />
+ <option name="runtime-hint" value="10m" />
+ <!-- test-timeout unit is ms, value = 10 min -->
+ <option name="native-test-timeout" value="600000" />
+ </test>
+
+ <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.mediaprovider" />
+ </object>
+</configuration>
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 3a85923..ac84ff7 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -217,6 +217,16 @@
}
return res;
}
+
+void onFileCreatedInternal(JNIEnv* env, jobject media_provider_object,
+ jmethodID mid_on_file_created, const string& path) {
+ ScopedLocalRef<jstring> j_path(env, env->NewStringUTF(path.c_str()));
+
+ env->CallVoidMethod(media_provider_object, mid_on_file_created, j_path.get());
+ CheckForJniException(env);
+ return;
+}
+
} // namespace
/*****************************************************************************************/
/******************************* Public API Implementation *******************************/
@@ -266,6 +276,8 @@
/*is_static*/ false);
mid_is_uid_for_package_ = CacheMethod(env, "isUidForPackage", "(Ljava/lang/String;I)Z",
/*is_static*/ false);
+ mid_on_file_created_ = CacheMethod(env, "onFileCreated", "(Ljava/lang/String;)V",
+ /*is_static*/ false);
}
MediaProviderWrapper::~MediaProviderWrapper() {
@@ -292,7 +304,7 @@
}
int MediaProviderWrapper::InsertFile(const string& path, uid_t uid) {
- if (shouldBypassMediaProvider(uid)) {
+ if (uid == ROOT_UID) {
return 0;
}
@@ -301,9 +313,8 @@
}
int MediaProviderWrapper::DeleteFile(const string& path, uid_t uid) {
- if (shouldBypassMediaProvider(uid)) {
+ if (uid == ROOT_UID) {
int res = unlink(path.c_str());
- ScanFile(path);
return res;
}
@@ -391,7 +402,9 @@
}
int MediaProviderWrapper::Rename(const string& old_path, const string& new_path, uid_t uid) {
- if (shouldBypassMediaProvider(uid)) {
+ // Rename from SHELL_UID should go through MediaProvider to update database rows, so only bypass
+ // MediaProvider for ROOT_UID.
+ if (uid == ROOT_UID) {
int res = rename(old_path.c_str(), new_path.c_str());
if (res != 0) res = -errno;
return res;
@@ -401,6 +414,12 @@
return renameInternal(env, media_provider_object_, mid_rename_, old_path, new_path, uid);
}
+void MediaProviderWrapper::OnFileCreated(const string& path) {
+ JNIEnv* env = MaybeAttachCurrentThread();
+
+ return onFileCreatedInternal(env, media_provider_object_, mid_on_file_created_, path);
+}
+
/*****************************************************************************************/
/******************************** Private member functions *******************************/
/*****************************************************************************************/
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index 9ad30ae..9d4fad9 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -157,6 +157,13 @@
int Rename(const std::string& old_path, const std::string& new_path, uid_t uid);
/**
+ * Called whenever a file has been created through FUSE.
+ *
+ * @param path path of the file that has been created.
+ */
+ void OnFileCreated(const std::string& path);
+
+ /**
* Initializes per-process static variables associated with the lifetime of
* a managed runtime.
*/
@@ -179,6 +186,7 @@
jmethodID mid_get_files_in_dir_;
jmethodID mid_rename_;
jmethodID mid_is_uid_for_package_;
+ jmethodID mid_on_file_created_;
/**
* Auxiliary for caching MediaProvider methods.
diff --git a/jni/RedactionInfo.cpp b/jni/RedactionInfo.cpp
index d943c8a..17de22e 100644
--- a/jni/RedactionInfo.cpp
+++ b/jni/RedactionInfo.cpp
@@ -14,12 +14,8 @@
* limitations under the License.
*/
-#define LOG_TAG "RedactionInfo"
-
#include "include/libfuse_jni/RedactionInfo.h"
-#include <android-base/logging.h>
-
using std::unique_ptr;
using std::vector;
@@ -94,7 +90,6 @@
unique_ptr<vector<RedactionRange>> RedactionInfo::getOverlappingRedactionRanges(size_t size,
off64_t off) const {
- LOG(DEBUG) << "Computing redaction ranges for request: sz = " << size << " off = " << off;
if (hasOverlapWithReadRequest(size, off)) {
auto first_redaction = redaction_ranges_.end();
auto last_redaction = redaction_ranges_.end();
@@ -112,12 +107,9 @@
last_redaction = iter;
}
if (first_redaction != redaction_ranges_.end()) {
- LOG(DEBUG) << "Returning " << (int)(last_redaction - first_redaction + 1)
- << " redaction ranges!";
return std::make_unique<vector<RedactionRange>>(first_redaction, last_redaction + 1);
}
}
- LOG(DEBUG) << "No relevant redaction ranges!";
return std::make_unique<vector<RedactionRange>>();
}
} // namespace fuse
diff --git a/jni/TEST_MAPPING b/jni/TEST_MAPPING
index a871459..5ee1bc6 100644
--- a/jni/TEST_MAPPING
+++ b/jni/TEST_MAPPING
@@ -1,6 +1,9 @@
{
"presubmit": [
{
+ "name": "FuseUtilsTest"
+ },
+ {
"name": "RedactionInfoTest"
},
{
diff --git a/jni/include/libfuse_jni/FuseUtils.h b/jni/include/libfuse_jni/FuseUtils.h
new file mode 100644
index 0000000..3528ca0
--- /dev/null
+++ b/jni/include/libfuse_jni/FuseUtils.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specic language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef MEDIAPROVIDER_JNI_UTILS_H_
+#define MEDIAPROVIDER_JNI_UTILS_H_
+
+#include <string>
+
+namespace mediaprovider {
+namespace fuse {
+
+/**
+ * Returns true if the given path is mounted for the given userid. Mounted paths
+ * are:
+ * "/storage/emulated/<userid>/Android"
+ * "/storage/emulated/<userid>/Android/data"
+ * "/storage/emulated/<userid>/Android/obb" *
+ */
+bool containsMount(const std::string& path, const std::string& userid);
+
+} // namespace fuse
+} // namespace mediaprovider
+
+#endif // MEDIAPROVIDER_JNI_UTILS_H_
diff --git a/logging.sh b/logging.sh
index 43f0d86..fd2a3a5 100755
--- a/logging.sh
+++ b/logging.sh
@@ -20,10 +20,12 @@
adb shell setprop log.tag.SQLiteQueryBuilder VERBOSE
adb shell setprop log.tag.FuseDaemon VERBOSE
adb shell setprop log.tag.libfuse VERBOSE
+ adb shell setprop persist.sys.fuse.log true
else
adb shell setprop log.tag.SQLiteQueryBuilder INFO
adb shell setprop log.tag.FuseDaemon INFO
adb shell setprop log.tag.libfuse INFO
+ adb shell setprop persist.sys.fuse.log false
fi
# Kill process to kick new settings into place
diff --git a/res/drawable/ic_warning.xml b/res/drawable/ic_warning.xml
new file mode 100644
index 0000000..2163203
--- /dev/null
+++ b/res/drawable/ic_warning.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="#1A73E8"
+ android:pathData="M12 5.99L19.53 19H4.47L12 5.99M12 2L1 21h22L12 2zm1 14h-2v2h2v-2zm0-6h-2v4h2v-4z" />
+</vector>
\ No newline at end of file
diff --git a/res/drawable/thumb_clip.xml b/res/drawable/thumb_clip.xml
index 0a05ecd..f075e41 100644
--- a/res/drawable/thumb_clip.xml
+++ b/res/drawable/thumb_clip.xml
@@ -16,5 +16,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
+ <solid android:color="@color/thumb_gray_color"/>
<corners android:radius="4dp" />
</shape>
diff --git a/res/layout/cache_clearing_dialog.xml b/res/layout/cache_clearing_dialog.xml
index f5e9bde..06f97b8 100644
--- a/res/layout/cache_clearing_dialog.xml
+++ b/res/layout/cache_clearing_dialog.xml
@@ -21,13 +21,17 @@
android:paddingStart="?android:attr/dialogPreferredPadding"
android:paddingEnd="?android:attr/dialogPreferredPadding"
android:orientation="vertical">
+ <ImageView
+ android:adjustViewBounds="true"
+ android:layout_gravity="bottom|center_horizontal"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginTop="20dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_warning"
+ android:contentDescription="@null"/>
+
<TextView
android:id="@+id/dialog_title"
- android:gravity="center"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingTop="15dp"
- android:textSize="20sp"
- android:textColor="?android:attr/textColorPrimary"
- android:textAppearance="@*android:style/TextAppearance.DeviceDefault.Title" />
+ style="@style/CacheClearingAlertDialogTitle"/>
</LinearLayout>
diff --git a/res/layout/permission_body.xml b/res/layout/permission_body.xml
index a4d1250..6282c7c 100644
--- a/res/layout/permission_body.xml
+++ b/res/layout/permission_body.xml
@@ -77,6 +77,7 @@
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
+ android:src="@color/thumb_gray_color"
android:visibility="gone" />
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml
index 8be0253..beace75 100644
--- a/res/values-fr/strings.xml
+++ b/res/values-fr/strings.xml
@@ -30,7 +30,7 @@
<string name="grant_dialog_button_allow" msgid="1644287024501033471">"Autoriser"</string>
<string name="grant_dialog_button_deny" msgid="6190589471415815741">"Refuser"</string>
<plurals name="permission_more_thumb" formatted="false" msgid="4392079224649478923">
- <item quantity="one">+<xliff:g id="COUNT_1">^1</xliff:g></item>
+ <item quantity="one">+ <xliff:g id="COUNT_1">^1</xliff:g></item>
<item quantity="other">+ <xliff:g id="COUNT_1">^1</xliff:g></item>
</plurals>
<plurals name="permission_more_text" formatted="false" msgid="7291997297174507324">
@@ -43,67 +43,67 @@
<string name="allow" msgid="8885707816848569619">"Autoriser"</string>
<string name="deny" msgid="6040983710442068936">"Refuser"</string>
<plurals name="permission_write_audio" formatted="false" msgid="8914759422381305478">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> audio files?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichier audio ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> fichiers audio ?</item>
</plurals>
<plurals name="permission_write_video" formatted="false" msgid="1098082003326873084">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> videos?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéo ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> vidéos ?</item>
</plurals>
<plurals name="permission_write_image" formatted="false" msgid="748745548893845892">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> photos?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> photo ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> photos ?</item>
</plurals>
<plurals name="permission_write_generic" formatted="false" msgid="3270172714743671779">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to modify <xliff:g id="COUNT">^2</xliff:g> items?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> élément ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à modifier <xliff:g id="COUNT">^2</xliff:g> éléments ?</item>
</plurals>
<plurals name="permission_trash_audio" formatted="false" msgid="8907813869381755423">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> audio files to trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> fichier audio dans la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> fichiers audio dans la corbeille ?</item>
</plurals>
<plurals name="permission_trash_video" formatted="false" msgid="4672871911555787438">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> videos to trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> vidéo dans la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> vidéos dans la corbeille ?</item>
</plurals>
<plurals name="permission_trash_image" formatted="false" msgid="6400475304599873227">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> photos to trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> photo dans la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> photos dans la corbeille ?</item>
</plurals>
<plurals name="permission_trash_generic" formatted="false" msgid="3814167365075039711">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> items to trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> élément dans la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à placer <xliff:g id="COUNT">^2</xliff:g> éléments dans la corbeille ?</item>
</plurals>
<plurals name="permission_untrash_audio" formatted="false" msgid="7795265980168966321">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> audio files out of trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> fichier audio de la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> fichiers audio de la corbeille ?</item>
</plurals>
<plurals name="permission_untrash_video" formatted="false" msgid="332894888445508879">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> videos out of trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> vidéo de la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> vidéos de la corbeille ?</item>
</plurals>
<plurals name="permission_untrash_image" formatted="false" msgid="7024071378733595056">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> photos out of trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> photo de la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> photos de la corbeille ?</item>
</plurals>
<plurals name="permission_untrash_generic" formatted="false" msgid="6872817093731198374">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to move <xliff:g id="COUNT">^2</xliff:g> items out of trash?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> élément de la corbeille ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à retirer <xliff:g id="COUNT">^2</xliff:g> éléments de la corbeille ?</item>
</plurals>
<plurals name="permission_delete_audio" formatted="false" msgid="6848547621165184719">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> audio files?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> fichier audio ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> fichiers audio ?</item>
</plurals>
<plurals name="permission_delete_video" formatted="false" msgid="1251942606336748563">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> videos?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> vidéo ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> vidéos ?</item>
</plurals>
<plurals name="permission_delete_image" formatted="false" msgid="2303409455224710111">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> photos?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> photo ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> photos ?</item>
</plurals>
<plurals name="permission_delete_generic" formatted="false" msgid="1412218850351841181">
- <item quantity="one">Allow <xliff:g id="APP_NAME_1">^1</xliff:g> to delete <xliff:g id="COUNT">^2</xliff:g> items?</item>
+ <item quantity="one">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> élément ?</item>
<item quantity="other">Autoriser l\'application <xliff:g id="APP_NAME_1">^1</xliff:g> à supprimer <xliff:g id="COUNT">^2</xliff:g> éléments ?</item>
</plurals>
</resources>
diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml
new file mode 100644
index 0000000..56d8417
--- /dev/null
+++ b/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <color name="thumb_gray_color">#3c4043</color>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
new file mode 100644
index 0000000..28f31ee
--- /dev/null
+++ b/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <color name="thumb_gray_color">#1f000000</color>
+</resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 1e866cb..d910577 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -17,12 +17,14 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PickerDialogTheme" parent="@*android:style/Theme.DeviceDefault.Settings.Dialog">
+ <item name="android:windowNoTitle">true</item>
</style>
<style name="AlertDialogTheme"
- parent="@*android:style/Theme.DeviceDefault.Light.Dialog.Alert" />
+ parent="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight" />
- <style name="CacheClearingAlertDialogTheme">
+ <style name="CacheClearingAlertDialogTheme"
+ parent="@*android:style/Theme.DeviceDefault.Dialog.Alert.DayNight">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
@@ -32,4 +34,12 @@
<item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
</style>
+ <style name="CacheClearingAlertDialogTitle"
+ parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">center</item>
+ <item name="android:textSize">16sp</item>
+ </style>
+
</resources>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index f35ad83..84ed540 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -437,6 +437,12 @@
public boolean successful;
/**
+ * List of tasks that should be executed in a blocking fashion when this
+ * transaction has been successfully finished.
+ */
+ public final ArrayList<Runnable> blockingTasks = new ArrayList<>();
+
+ /**
* Map from {@code flags} value to set of {@link Uri} that would have
* been sent directly via {@link ContentResolver#notifyChange}, but are
* instead being collected due to this ongoing transaction.
@@ -511,6 +517,10 @@
mSchemaLock.readLock().unlock();
if (state.successful) {
+ for (int i = 0; i < state.blockingTasks.size(); i++) {
+ state.blockingTasks.get(i).run();
+ }
+
// We carefully "phase" our two sets of work here to ensure that we
// completely finish dispatching all change notifications before we
// process background tasks, to ensure that the background work
@@ -632,9 +642,23 @@
}
/**
- * Post given task to be run in background. This enqueues the task if
- * currently inside a transaction, and they'll be clustered and sent when
- * the transaction completes.
+ * Post the given task to be run in a blocking fashion after any current
+ * transaction has finished. If there is no active transaction, the task is
+ * immediately executed.
+ */
+ public void postBlocking(@NonNull Runnable command) {
+ final TransactionState state = mTransactionState.get();
+ if (state != null) {
+ state.blockingTasks.add(command);
+ } else {
+ command.run();
+ }
+ }
+
+ /**
+ * Post the given task to be run in background after any current transaction
+ * has finished. If there is no active transaction, the task is immediately
+ * dispatched to run in the background.
*/
public void postBackground(@NonNull Runnable command) {
final TransactionState state = mTransactionState.get();
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index 9b13c38..6f554b4 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -22,13 +22,14 @@
import static android.content.pm.PackageManager.PERMISSION_DENIED;
import static com.android.providers.media.util.PermissionUtils.checkIsLegacyStorageGranted;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionDelegator;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionSystem;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionShell;
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteAudio;
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
@@ -132,7 +133,8 @@
ident.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
ident.targetSdkVersionResolved = true;
ident.hasPermission = ~(PERMISSION_IS_LEGACY_GRANTED | PERMISSION_IS_LEGACY_WRITE
- | PERMISSION_IS_LEGACY_READ | PERMISSION_IS_REDACTION_NEEDED);
+ | PERMISSION_IS_LEGACY_READ | PERMISSION_IS_REDACTION_NEEDED
+ | PERMISSION_IS_SHELL | PERMISSION_IS_DELEGATOR);
ident.hasPermissionResolved = ~0;
return ident;
}
@@ -194,19 +196,22 @@
return Build.VERSION_CODES.CUR_DEVELOPMENT;
}
- public static final int PERMISSION_IS_SYSTEM = 1 << 0;
- public static final int PERMISSION_IS_LEGACY_WRITE = 1 << 1;
- public static final int PERMISSION_IS_REDACTION_NEEDED = 1 << 2;
- public static final int PERMISSION_READ_AUDIO = 1 << 3;
- public static final int PERMISSION_READ_VIDEO = 1 << 4;
- public static final int PERMISSION_READ_IMAGES = 1 << 5;
- public static final int PERMISSION_WRITE_AUDIO = 1 << 6;
- public static final int PERMISSION_WRITE_VIDEO = 1 << 7;
- public static final int PERMISSION_WRITE_IMAGES = 1 << 8;
- public static final int PERMISSION_IS_LEGACY_READ = 1 << 9;
- public static final int PERMISSION_IS_LEGACY_GRANTED = 1 << 10;
- public static final int PERMISSION_IS_BACKUP = 1 << 11;
- public static final int PERMISSION_MANAGE_EXTERNAL_STORAGE = 1 << 12;
+ public static final int PERMISSION_IS_SELF = 1 << 0;
+ public static final int PERMISSION_IS_SHELL = 1 << 1;
+ public static final int PERMISSION_IS_MANAGER = 1 << 2;
+ public static final int PERMISSION_IS_DELEGATOR = 1 << 3;
+
+ public static final int PERMISSION_IS_REDACTION_NEEDED = 1 << 8;
+ public static final int PERMISSION_IS_LEGACY_GRANTED = 1 << 9;
+ public static final int PERMISSION_IS_LEGACY_READ = 1 << 10;
+ public static final int PERMISSION_IS_LEGACY_WRITE = 1 << 11;
+
+ public static final int PERMISSION_READ_AUDIO = 1 << 16;
+ public static final int PERMISSION_READ_VIDEO = 1 << 17;
+ public static final int PERMISSION_READ_IMAGES = 1 << 18;
+ public static final int PERMISSION_WRITE_AUDIO = 1 << 19;
+ public static final int PERMISSION_WRITE_VIDEO = 1 << 20;
+ public static final int PERMISSION_WRITE_IMAGES = 1 << 21;
private int hasPermission;
private int hasPermissionResolved;
@@ -230,22 +235,30 @@
}
switch (permission) {
- case PERMISSION_IS_SYSTEM:
- return isSystemInternal();
- case PERMISSION_IS_BACKUP:
- return isBackupInternal();
- case PERMISSION_IS_LEGACY_GRANTED:
- return isLegacyStorageGranted();
- case PERMISSION_IS_LEGACY_WRITE:
- return isLegacyWriteInternal();
- case PERMISSION_IS_LEGACY_READ:
- return isLegacyReadInternal();
+ case PERMISSION_IS_SELF:
+ return checkPermissionSelf(context, pid, uid);
+ case PERMISSION_IS_SHELL:
+ return checkPermissionShell(context, pid, uid);
+ case PERMISSION_IS_MANAGER:
+ return checkPermissionManager(context, pid, uid, getPackageName(), attributionTag);
+ case PERMISSION_IS_DELEGATOR:
+ return checkPermissionDelegator(context, pid, uid);
+
case PERMISSION_IS_REDACTION_NEEDED:
return isRedactionNeededInternal();
+ case PERMISSION_IS_LEGACY_GRANTED:
+ return isLegacyStorageGranted();
+ case PERMISSION_IS_LEGACY_READ:
+ return isLegacyReadInternal();
+ case PERMISSION_IS_LEGACY_WRITE:
+ return isLegacyWriteInternal();
+
case PERMISSION_READ_AUDIO:
- return checkPermissionReadAudio(context, pid, uid, getPackageName(), attributionTag);
+ return checkPermissionReadAudio(
+ context, pid, uid, getPackageName(), attributionTag);
case PERMISSION_READ_VIDEO:
- return checkPermissionReadVideo(context, pid, uid, getPackageName(), attributionTag);
+ return checkPermissionReadVideo(
+ context, pid, uid, getPackageName(), attributionTag);
case PERMISSION_READ_IMAGES:
return checkPermissionReadImages(
context, pid, uid, getPackageName(), attributionTag);
@@ -258,22 +271,11 @@
case PERMISSION_WRITE_IMAGES:
return checkPermissionWriteImages(
context, pid, uid, getPackageName(), attributionTag);
- case PERMISSION_MANAGE_EXTERNAL_STORAGE:
- return checkPermissionManageExternalStorage(
- context, pid, uid, getPackageName(), attributionTag);
default:
return false;
}
}
- private boolean isSystemInternal() {
- return checkPermissionSystem(context, pid, uid, getPackageName());
- }
-
- private boolean isBackupInternal() {
- return checkPermissionBackup(context, pid, uid);
- }
-
private boolean isLegacyStorageGranted() {
boolean defaultScopedStorage = CompatChanges.isChangeEnabled(
DEFAULT_SCOPED_STORAGE, getPackageName(), UserHandle.getUserHandleForUid(uid));
@@ -314,7 +316,7 @@
/** System internals or callers holding permission have no redaction */
private boolean isRedactionNeededInternal() {
- if (hasPermission(PERMISSION_IS_SYSTEM)) {
+ if (hasPermission(PERMISSION_IS_SELF) || hasPermission(PERMISSION_IS_SHELL)) {
return false;
}
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 38482b3..f26716d 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -76,6 +76,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
/**
* Presents a {@link DocumentsContract} view of {@link MediaProvider} external
@@ -1059,13 +1060,15 @@
private boolean isEmpty(Uri uri) {
final ContentResolver resolver = getContext().getContentResolver();
final long token = Binder.clearCallingIdentity();
- Cursor cursor = null;
- try {
- cursor = resolver.query(uri, new String[] {
- BaseColumns._ID }, null, null, null);
- return (cursor == null) || (cursor.getCount() == 0);
+ try (Cursor cursor = resolver.query(uri,
+ new String[] { "COUNT(_id)" }, null, null, null)) {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0) == 0;
+ } else {
+ // No count information means we need to assume empty
+ return true;
+ }
} finally {
- FileUtils.closeQuietly(cursor);
Binder.restoreCallingIdentity(token);
}
}
@@ -1172,13 +1175,15 @@
final String[] PROJECTION = new String[] {
ImageColumns.BUCKET_ID,
ImageColumns.BUCKET_DISPLAY_NAME,
- ImageColumns.DATE_MODIFIED };
+ ImageColumns.DATE_MODIFIED,
+ ImageColumns.VOLUME_NAME };
final String SORT_ORDER = ImageColumns.BUCKET_ID + ", " + ImageColumns.DATE_MODIFIED
+ " DESC";
final int BUCKET_ID = 0;
final int BUCKET_DISPLAY_NAME = 1;
final int DATE_MODIFIED = 2;
+ final int VOLUME_NAME = 3;
}
private void includeImagesBucket(MatrixCursor result, Cursor cursor) {
@@ -1187,8 +1192,9 @@
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
- row.add(Document.COLUMN_DISPLAY_NAME,
- cleanUpMediaBucketName(cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME)));
+ row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
+ cursor.getString(ImagesBucketQuery.BUCKET_DISPLAY_NAME),
+ cursor.getString(ImagesBucketQuery.VOLUME_NAME)));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(ImagesBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
@@ -1232,13 +1238,15 @@
final String[] PROJECTION = new String[] {
VideoColumns.BUCKET_ID,
VideoColumns.BUCKET_DISPLAY_NAME,
- VideoColumns.DATE_MODIFIED };
+ VideoColumns.DATE_MODIFIED,
+ VideoColumns.VOLUME_NAME };
final String SORT_ORDER = VideoColumns.BUCKET_ID + ", " + VideoColumns.DATE_MODIFIED
+ " DESC";
final int BUCKET_ID = 0;
final int BUCKET_DISPLAY_NAME = 1;
final int DATE_MODIFIED = 2;
+ final int VOLUME_NAME = 3;
}
private void includeVideosBucket(MatrixCursor result, Cursor cursor) {
@@ -1247,8 +1255,9 @@
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
- row.add(Document.COLUMN_DISPLAY_NAME,
- cleanUpMediaBucketName(cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME)));
+ row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
+ cursor.getString(VideosBucketQuery.BUCKET_DISPLAY_NAME),
+ cursor.getString(VideosBucketQuery.VOLUME_NAME)));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(VideosBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
@@ -1292,13 +1301,15 @@
final String[] PROJECTION = new String[] {
FileColumns.BUCKET_ID,
FileColumns.BUCKET_DISPLAY_NAME,
- FileColumns.DATE_MODIFIED };
+ FileColumns.DATE_MODIFIED,
+ FileColumns.VOLUME_NAME };
final String SORT_ORDER = FileColumns.BUCKET_ID + ", " + FileColumns.DATE_MODIFIED
+ " DESC";
final int BUCKET_ID = 0;
final int BUCKET_DISPLAY_NAME = 1;
final int DATE_MODIFIED = 2;
+ final int VOLUME_NAME = 3;
}
private void includeDocumentsBucket(MatrixCursor result, Cursor cursor) {
@@ -1307,8 +1318,9 @@
final RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
- row.add(Document.COLUMN_DISPLAY_NAME,
- cleanUpMediaBucketName(cursor.getString(DocumentsBucketQuery.BUCKET_DISPLAY_NAME)));
+ row.add(Document.COLUMN_DISPLAY_NAME, cleanUpMediaBucketName(
+ cursor.getString(DocumentsBucketQuery.BUCKET_DISPLAY_NAME),
+ cursor.getString(DocumentsBucketQuery.VOLUME_NAME)));
row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
row.add(Document.COLUMN_LAST_MODIFIED,
cursor.getLong(DocumentsBucketQuery.DATE_MODIFIED) * DateUtils.SECOND_IN_MILLIS);
@@ -1495,10 +1507,13 @@
return getContext().getResources().getString(R.string.unknown);
}
- private String cleanUpMediaBucketName(String bucketDisplayName) {
+ private String cleanUpMediaBucketName(String bucketDisplayName, String volumeName) {
if (!TextUtils.isEmpty(bucketDisplayName)) {
return bucketDisplayName;
+ } else if (!Objects.equals(volumeName, MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
+ return volumeName;
+ } else {
+ return getContext().getResources().getString(R.string.unknown);
}
- return getContext().getResources().getString(R.string.unknown);
}
}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 6d10191..0af4bb9 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -36,13 +36,14 @@
import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
-import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_BACKUP;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED;
-import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM;
-import static com.android.providers.media.LocalCallingIdentity.PERMISSION_MANAGE_EXTERNAL_STORAGE;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
@@ -66,7 +67,6 @@
import static com.android.providers.media.util.FileUtils.sanitizePath;
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 android.app.AppOpsManager;
import android.app.AppOpsManager.OnOpActiveChangedListener;
@@ -126,8 +126,8 @@
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
-import android.os.storage.StorageManager.StorageVolumeCallback;
import android.os.storage.StorageManager;
+import android.os.storage.StorageManager.StorageVolumeCallback;
import android.os.storage.StorageVolume;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
@@ -156,6 +156,7 @@
import android.util.LongSparseArray;
import android.util.Size;
import android.util.SparseArray;
+import android.webkit.MimeTypeMap;
import androidx.annotation.GuardedBy;
import androidx.annotation.Keep;
@@ -541,14 +542,6 @@
private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) {
Trace.beginSection("updateQuotaTypeForUri");
- try {
- updateQuotaTypeForUriInternal(uri, mediaType);
- } finally {
- Trace.endSection();
- }
- }
-
- private final void updateQuotaTypeForUriInternal(@NonNull Uri uri, int mediaType) {
File file;
try {
file = queryForDataFile(uri, null);
@@ -556,10 +549,24 @@
// This can happen if an item is inserted in MediaStore before it is created
return;
}
+
+ if (mediaType == FileColumns.MEDIA_TYPE_NONE) {
+ // This might be because the file is hidden; but we still want to
+ // attribute its quota to the correct type, so get the type from
+ // the extension instead.
+ mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
+ }
+
+ updateQuotaTypeForFileInternal(file, mediaType);
} catch (FileNotFoundException e) {
// Ignore
return;
+ } finally {
+ Trace.endSection();
}
+ }
+
+ private final void updateQuotaTypeForFileInternal(File file, int mediaType) {
try {
switch (mediaType) {
case FileColumns.MEDIA_TYPE_AUDIO:
@@ -922,6 +929,17 @@
null /* all packages */, mModeListener);
mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO,
null /* all packages */, mModeListener);
+ try {
+ // Here we are forced to depend on the non-public API of AppOpsManager. If
+ // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will
+ // throw an IllegalArgumentException during MediaProvider startup. In combination with
+ // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE
+ // is defined.
+ mAppOpsManager.startWatchingMode(PermissionUtils.OPSTR_NO_ISOLATED_STORAGE,
+ null /* all packages */, mModeListener);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed to start watching " + PermissionUtils.OPSTR_NO_ISOLATED_STORAGE, e);
+ }
return true;
}
@@ -1113,14 +1131,31 @@
}
/**
+ * Called when a new file is created through FUSE
+ *
+ * @param file path of the file that was created
+ *
+ * Called from JNI in jni/MediaProviderWrapper.cpp
+ */
+ @Keep
+ public void onFileCreatedForFuse(String path) {
+ // Make sure we update the quota type of the file
+ BackgroundThread.getExecutor().execute(() -> {
+ File file = new File(path);
+ int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
+ updateQuotaTypeForFileInternal(file, mediaType);
+ });
+ }
+
+ /**
* Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed
* to clear other apps' cache directories.
*/
static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) {
PermissionUtils.setOpDescription("clear app cache");
try {
- return checkPermissionManageExternalStorage(context, /*pid*/ -1, ai.uid, ai.packageName,
- /*attributionTag*/ null);
+ return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid,
+ ai.packageName, /* attributionTag */ null);
} finally {
PermissionUtils.clearOpDescription();
}
@@ -2222,7 +2257,8 @@
if (c != null) {
// As a performance optimization, only configure notifications when
// resulting cursor will leave our process
- if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
+ final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid();
+ if (callerIsRemote && !isFuseThread()) {
c.setNotificationUri(getContext().getContentResolver(), uri);
}
@@ -2424,13 +2460,55 @@
if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
FileUtils.computeValuesFromData(values, isFuseThread());
}
- // Extract the MIME type from the display name if we couldn't resolve it from the raw path
- if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
- final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
- if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
- values.put(
- MediaColumns.MIME_TYPE, MimeUtils.resolveMimeType(new File(displayName)));
+ final boolean isTargetSdkROrHigher =
+ getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R;
+ final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
+ final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null :
+ MimeUtils.resolveMimeType(new File(displayName));
+
+ if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
+ if (isTargetSdkROrHigher) {
+ // Extract the MIME type from the display name if we couldn't resolve it from the
+ // raw path
+ if (mimeTypeFromExt != null) {
+ values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
+ } else {
+ // We couldn't resolve mimeType, it means that both display name and MIME type
+ // were missing in values, so we use defaultMimeType.
+ values.put(MediaColumns.MIME_TYPE, defaultMimeType);
+ }
+ } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
+ values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
+ } else {
+ // We don't use mimeTypeFromExt to preserve legacy behavior.
+ values.put(MediaColumns.MIME_TYPE, defaultMimeType);
+ }
+ }
+
+ String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
+ if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
+ // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE.
+ } else if (mimeType != null &&
+ MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) {
+ if (mimeTypeFromExt != null &&
+ defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) {
+ // If mimeType from extension matches the defaultMediaType of uri, we use mimeType
+ // from file extension as mimeType. This is an effort to guess the mimeType when we
+ // get unsupported mimeType.
+ // Note: We can't force defaultMimeType because when we force defaultMimeType, we
+ // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and
+ // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file
+ // name with the new file extension i.e., "Foo.png.jpg" where as the expected file
+ // name was "Foo.png"
+ values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
+ } else if (isTargetSdkROrHigher) {
+ // We are here because given mimeType is unsupported also we couldn't guess valid
+ // mimeType from file extension.
+ throw new IllegalArgumentException("Unsupported MIME type " + mimeType);
+ } else {
+ // We can't throw error for legacy apps, so we try to use defaultMimeType.
+ values.put(MediaColumns.MIME_TYPE, defaultMimeType);
}
}
@@ -2443,12 +2521,10 @@
final int format = formatObject == null ? 0 : formatObject.intValue();
if (format == MtpConstants.FORMAT_ASSOCIATION) {
values.putNull(MediaColumns.MIME_TYPE);
- } else if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
- values.put(MediaColumns.MIME_TYPE, defaultMimeType);
}
+ mimeType = values.getAsString(MediaColumns.MIME_TYPE);
// Sanity check MIME type against table
- final String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
if (mimeType != null) {
final int actualMediaType = MimeUtils.resolveMediaType(mimeType);
if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
@@ -2577,7 +2653,7 @@
// Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere
if (!validPath) {
- validPath = isCallingPackageExternalStorageManager();
+ validPath = isCallingPackageManager();
}
// Allow system gallery to create image/video files.
@@ -2604,6 +2680,10 @@
throw new IllegalStateException("Failed to create directory: " + res);
}
values.put(MediaColumns.DATA, res.getAbsolutePath());
+ // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME.
+ // Note: We can't extract displayName from res.getPath() because for pending & trashed
+ // files DISPLAY_NAME will not be same as file name.
+ FileUtils.computeValuesFromData(values, isFuseThread());
} else {
assertFileColumnsSane(match, uri, values);
}
@@ -2914,10 +2994,12 @@
if (mimeType != null) {
values.put(FileColumns.MIME_TYPE, mimeType);
- if (isCallingPackageSystem() && values.containsKey(FileColumns.MEDIA_TYPE)) {
+ if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) {
// Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
// FileColumns.MEDIA_TYPE is already populated.
- } else{
+ } else if (path != null && shouldFileBeHidden(new File(path))) {
+ values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
+ } else {
values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
}
} else {
@@ -3038,7 +3120,7 @@
// names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since
// DownloadProvider can upsert a row on behalf of app, we should include all shared packages
// of givenOwnerPackage.
- if (givenOwnerPackage != null && isCallingPackageSystem() &&
+ if (givenOwnerPackage != null && isCallingPackageDelegator() &&
!isCallingIdentitySharedPackageName(givenOwnerPackage)) {
// Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row.
packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage)));
@@ -3202,7 +3284,7 @@
for (String column : sDataColumns.keySet()) {
if (!initialValues.containsKey(column)) continue;
- if (isCallingPackageSystem() || isCallingPackageLegacyWrite()) {
+ if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
// Mutation allowed
} else {
Log.w(TAG, "Ignoring mutation of " + column + " from "
@@ -3213,7 +3295,7 @@
path = initialValues.getAsString(MediaStore.MediaColumns.DATA);
- if (!isCallingPackageSystem()) {
+ if (!isCallingPackageSelf()) {
initialValues.remove(FileColumns.IS_DOWNLOAD);
}
@@ -3225,14 +3307,23 @@
initialValues.putNull(ImageColumns.LONGITUDE);
}
- if (isCallingPackageSystem() || isCallingPackageBackup()) {
- // When media inserted by ourselves during a scan, or by a
- // backup app, the best we can do is guess ownership based on
- // path when it's not explicitly provided
+ if (isCallingPackageSelf() || isCallingPackageShell()) {
+ // When media inserted by ourselves during a scan, or by the
+ // shell, the best we can do is guess ownership based on path
+ // when it's not explicitly provided
ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
if (TextUtils.isEmpty(ownerPackageName)) {
ownerPackageName = extractPathOwnerPackageName(path);
}
+ } else if (isCallingPackageDelegator()) {
+ // When caller is a delegator, we handle ownership as a hybrid
+ // of the two other cases: we're willing to accept any ownership
+ // transfer attempted during insert, but we fall back to using
+ // the Binder identity if they don't request a specific owner
+ ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
+ if (TextUtils.isEmpty(ownerPackageName)) {
+ ownerPackageName = getCallingPackageOrSelf();
+ }
} else {
// Remote callers have no direct control over owner column; we force
// it be whoever is creating the content.
@@ -3327,7 +3418,7 @@
values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
// Playlist names are stored as display names, but leave
// values untouched if the caller is ModernMediaScanner
- if (!isCallingPackageSystem()) {
+ if (!isCallingPackageSelf()) {
if (values.containsKey(Playlists.NAME)) {
values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME));
}
@@ -3545,7 +3636,7 @@
qb.setDistinct(true);
}
qb.setStrict(true);
- if (isCallingPackageSystem()) {
+ if (isCallingPackageSelf()) {
// When caller is system, such as the media scanner, we're willing
// to let them access any columns they want
} else {
@@ -4294,16 +4385,15 @@
}
if (deletedDownloadIds.size() > 0) {
- final long token = Binder.clearCallingIdentity();
- try {
+ // Do this on a background thread, since we don't want to make binder
+ // calls as part of a FUSE call.
+ helper.postBackground(() -> {
getContext().getSystemService(DownloadManager.class)
.onMediaStoreDownloadsDeleted(deletedDownloadIds);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ });
}
- if (isFilesTable && !isCallingPackageSystem()) {
+ if (isFilesTable && !isCallingPackageSelf()) {
Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
getCallingPackageOrSelf(), count);
}
@@ -4522,13 +4612,7 @@
final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA);
final List<Uri> uris = collectUris(clipData);
- final String volumeName = MediaStore.getVolumeName(uris.get(0));
for (Uri uri : uris) {
- // Require that everything is on the same volume
- if (!Objects.equals(volumeName, MediaStore.getVolumeName(uri))) {
- throw new IllegalArgumentException("All requested items must be on same volume");
- }
-
final int match = matchUri(uri, false);
switch (match) {
case IMAGES_MEDIA_ID:
@@ -4946,7 +5030,7 @@
for (String column : sDataColumns.keySet()) {
if (!initialValues.containsKey(column)) continue;
- if (isCallingPackageSystem() || isCallingPackageLegacyWrite()) {
+ if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
// Mutation allowed
} else {
Log.w(TAG, "Ignoring mutation of " + column + " from "
@@ -4955,12 +5039,44 @@
}
}
- if (!isCallingPackageSystem()) {
- Trace.beginSection("filter");
+ // Enforce allowed ownership transfers
+ if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) {
+ if (isCallingPackageSelf() || isCallingPackageShell()) {
+ // When the caller is the media scanner or the shell, we let
+ // them change ownership however they see fit; nothing to do
+ } else if (isCallingPackageDelegator()) {
+ // When the caller is a delegator, allow them to shift
+ // ownership only when current owner, or when ownerless
+ final String currentOwner;
+ final String proposedOwner = initialValues
+ .getAsString(MediaColumns.OWNER_PACKAGE_NAME);
+ final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
+ ContentUris.parseId(uri));
+ try (Cursor c = queryForSingleItem(genericUri,
+ new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) {
+ currentOwner = c.getString(0);
+ } catch (FileNotFoundException e) {
+ throw new IllegalStateException(e);
+ }
+ final boolean transferAllowed = (currentOwner == null)
+ || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf()))
+ .contains(currentOwner);
+ if (transferAllowed) {
+ Log.v(TAG, "Ownership transfer from " + currentOwner + " to "
+ + proposedOwner + " allowed");
+ } else {
+ Log.w(TAG, "Ownership transfer from " + currentOwner + " to "
+ + proposedOwner + " blocked");
+ initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
+ }
+ } else {
+ // Otherwise no ownership changes are allowed
+ initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
+ }
+ }
- // Remote callers have no direct control over owner column; we
- // force it be whoever is creating the content.
- initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
+ if (!isCallingPackageSelf()) {
+ Trace.beginSection("filter");
// We default to filtering mutable columns, except when we know
// the single item being updated is pending; when it's finally
@@ -5036,7 +5152,7 @@
case AUDIO_PLAYLISTS_ID:
// Playlist names are stored as display names, but leave
// values untouched if the caller is ModernMediaScanner
- if (!isCallingPackageSystem()) {
+ if (!isCallingPackageSelf()) {
if (initialValues.containsKey(Playlists.NAME)) {
initialValues.put(MediaColumns.DISPLAY_NAME,
initialValues.getAsString(Playlists.NAME));
@@ -5051,7 +5167,7 @@
// If we're touching columns that would change placement of a file,
// blend in current values and recalculate path
final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
- !isCallingPackageSystem());
+ !isCallingPackageSelf());
if (containsAny(initialValues.keySet(), sPlacementColumns)
&& !initialValues.containsKey(MediaColumns.DATA)
&& !isThumbnail
@@ -5073,7 +5189,9 @@
}
final LocalCallingIdentity token = clearLocalCallingIdentity();
- try (Cursor c = queryForSingleItem(uri,
+ final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
+ ContentUris.parseId(uri));
+ try (Cursor c = queryForSingleItem(genericUri,
sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) {
for (int i = 0; i < c.getColumnCount(); i++) {
final String column = c.getColumnName(i);
@@ -5121,7 +5239,11 @@
invalidateFuseDentry(beforePath);
invalidateFuseDentry(afterPath);
} catch (ErrnoException e) {
- throw new IllegalStateException(e);
+ if (e.errno == OsConstants.ENOENT) {
+ Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway");
+ } else {
+ throw new IllegalStateException(e);
+ }
}
initialValues.put(MediaColumns.DATA, afterPath);
@@ -5150,7 +5272,7 @@
// If we're already doing this update from an internal scan, no need to
// kick off another no-op scan
- if (isCallingPackageSystem()) {
+ if (isCallingPackageSelf()) {
triggerScan = false;
}
@@ -5213,7 +5335,15 @@
if (triggerScan) {
try (Cursor c = queryForSingleItem(updatedUri,
new String[] { FileColumns.DATA }, null, null, null)) {
- mMediaScanner.scanFile(new File(c.getString(0)), REASON_DEMAND);
+ final File file = new File(c.getString(0));
+ helper.postBlocking(() -> {
+ final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
+ try {
+ mMediaScanner.scanFile(file, REASON_DEMAND);
+ } finally {
+ restoreLocalCallingIdentity(tokenInner);
+ }
+ });
} catch (Exception e) {
Log.w(TAG, "Failed to update metadata for " + updatedUri, e);
}
@@ -6063,7 +6193,7 @@
return true;
}
- if (isCallingPackageExternalStorageManager()) {
+ if (isCallingPackageManager()) {
return true;
}
@@ -6536,17 +6666,30 @@
final String mimeType = MimeUtils.resolveMimeType(new File(path));
if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
- // Ignore insert errors for apps that bypass scoped storage restriction.
+ final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy();
if (!fileExists(path)) {
// If app has already inserted the db row, inserting the row again might set
// IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE
// operation, hence, insert the db row only when it doesn't exist.
try {
- insertFileForFuse(path, FileUtils.getContentUriForPath(path), mimeType,
- /*useData*/ isCallingPackageRequestingLegacy());
+ insertFileForFuse(path, FileUtils.getContentUriForPath(path),
+ mimeType, /*useData*/ callerRequestingLegacy);
} catch (Exception ignored) {
}
+ } else {
+ // Upon creating a file via FUSE, if a row matching the path already exists
+ // but a file doesn't exist on the filesystem, we transfer ownership to the
+ // app attempting to create the file. If we don't update ownership, then the
+ // app that inserted the original row may be able to observe the contents of
+ // written file even though they don't hold the right permissions to do so.
+ if (callerRequestingLegacy) {
+ final String owner = getCallingPackageOrSelf();
+ if (owner != null && !updateOwnerForPath(path, owner)) {
+ return OsConstants.EPERM;
+ }
+ }
}
+
return 0;
}
@@ -6575,6 +6718,23 @@
}
}
+ private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) {
+ final DatabaseHelper helper;
+ try {
+ helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
+ } catch (VolumeNotFoundException e) {
+ // Cannot happen, as this is a path that we already resolved.
+ throw new AssertionError("Path must already be resolved", e);
+ }
+
+ ContentValues values = new ContentValues(1);
+ values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner);
+
+ return helper.runWithoutTransaction((db) -> {
+ return db.update("files", values, "_data=?", new String[] { path });
+ }) == 1;
+ }
+
private static int deleteFileUnchecked(@NonNull String path) {
final File toDelete = new File(path);
if (toDelete.delete()) {
@@ -6761,12 +6921,12 @@
private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
// System internals can work with all media
- if (isCallingPackageSystem()) {
+ if (isCallingPackageSelf() || isCallingPackageShell()) {
return true;
}
// Apps that have permission to manage external storage can work with all files
- if (isCallingPackageExternalStorageManager()) {
+ if (isCallingPackageManager()) {
return true;
}
@@ -7372,6 +7532,7 @@
sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
+ sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME);
sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
@@ -7383,6 +7544,9 @@
sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID);
sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
+ sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI);
+ sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI);
+
sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE);
sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE);
}
@@ -7476,28 +7640,34 @@
}
@Deprecated
- private int getCallingPackageTargetSdkVersion() {
+ @VisibleForTesting
+ public int getCallingPackageTargetSdkVersion() {
return mCallingIdentity.get().getTargetSdkVersion();
}
@Deprecated
private boolean isCallingPackageAllowedHidden() {
- return isCallingPackageSystem();
+ return isCallingPackageSelf();
}
@Deprecated
- private boolean isCallingPackageSystem() {
- return mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM);
+ private boolean isCallingPackageSelf() {
+ return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
}
@Deprecated
- private boolean isCallingPackageBackup() {
- return mCallingIdentity.get().hasPermission(PERMISSION_IS_BACKUP);
+ private boolean isCallingPackageShell() {
+ return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL);
}
@Deprecated
- private boolean isCallingPackageLegacyWrite() {
- return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
+ private boolean isCallingPackageManager() {
+ return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER);
+ }
+
+ @Deprecated
+ private boolean isCallingPackageDelegator() {
+ return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR);
}
@Deprecated
@@ -7505,11 +7675,11 @@
return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ);
}
- private boolean isCallingPackageExternalStorageManager() {
- return mCallingIdentity.get().hasPermission(PERMISSION_MANAGE_EXTERNAL_STORAGE);
+ @Deprecated
+ private boolean isCallingPackageLegacyWrite() {
+ return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
}
-
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
writer.println("mThumbSize=" + mThumbSize);
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 3f97464..041d89f 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -25,6 +25,7 @@
import android.app.Activity;
import android.app.AlertDialog;
+import android.app.ProgressDialog;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
@@ -43,6 +44,7 @@
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
+import android.os.Handler;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils;
@@ -95,8 +97,11 @@
private String volumeName;
private ApplicationInfo appInfo;
+ private ProgressDialog progressDialog;
private TextView titleView;
+ private static final Long LEAST_SHOW_PROGRESS_TIME_MS = 300L;
+
private static final String VERB_WRITE = "write";
private static final String VERB_TRASH = "trash";
private static final String VERB_UNTRASH = "untrash";
@@ -174,9 +179,17 @@
titleView = (TextView) findViewByPredicate(dialog.getWindow().getDecorView(), (view) -> {
return (view instanceof TextView) && view.isImportantForAccessibility();
});
+
+ progressDialog = new ProgressDialog(this);
}
private void onPositiveAction(DialogInterface dialog, int which) {
+ // Disable the buttons
+ ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+ ((AlertDialog) dialog).getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
+
+ progressDialog.show();
+ final long startTime = System.currentTimeMillis();
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -226,7 +239,18 @@
@Override
protected void onPostExecute(Void result) {
setResult(Activity.RESULT_OK);
- finish();
+ // Don't dismiss the progress dialog too quick, it will cause bad UX.
+ final long duration = System.currentTimeMillis() - startTime;
+ if (duration > LEAST_SHOW_PROGRESS_TIME_MS) {
+ progressDialog.dismiss();
+ finish();
+ } else {
+ Handler handler = new Handler(getMainLooper());
+ handler.postDelayed(() -> {
+ progressDialog.dismiss();
+ finish();
+ }, LEAST_SHOW_PROGRESS_TIME_MS - duration);
+ }
}
}.execute();
}
@@ -423,13 +447,40 @@
final List<Uri> uris = params[0];
final List<Description> res = new ArrayList<>();
+ // If the size is zero, return the res directly.
+ if (uris.isEmpty()) {
+ return res;
+ }
+
// Default information that we'll load for each item
int loadFlags = Description.LOAD_THUMBNAIL | Description.LOAD_CONTENT_DESCRIPTION;
int neededThumbs = MAX_THUMBS;
// If we're only asking for single item, load the full image
if (uris.size() == 1) {
+ // Set visible to the thumb_full to avoid the size
+ // changed of the dialog in full decoding.
+ final ImageView thumbFull = bodyView.requireViewById(R.id.thumb_full);
+ thumbFull.setVisibility(View.VISIBLE);
loadFlags |= Description.LOAD_FULL;
+ } else {
+ // If the size equals 2, we will remove thumb1 later.
+ // Set visible to the thumb2 and thumb3 first to avoid
+ // the size changed of the dialog.
+ ImageView thumb = bodyView.requireViewById(R.id.thumb2);
+ thumb.setVisibility(View.VISIBLE);
+ thumb = bodyView.requireViewById(R.id.thumb3);
+ thumb.setVisibility(View.VISIBLE);
+ // If the count of thumbs equals to MAX_THUMBS, set visible to thumb1.
+ if (uris.size() == MAX_THUMBS) {
+ thumb = bodyView.requireViewById(R.id.thumb1);
+ thumb.setVisibility(View.VISIBLE);
+ } else if (uris.size() > MAX_THUMBS) {
+ // If the count is larger than MAX_THUMBS, set visible to
+ // thumb_more_container.
+ final View container = bodyView.requireViewById(R.id.thumb_more_container);
+ container.setVisibility(View.VISIBLE);
+ }
}
for (Uri uri : uris) {
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 60770ff..4515c6f 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -957,13 +957,29 @@
@VisibleForTesting
public static @Nullable String extractRelativePathForDirectory(@Nullable String directoryPath) {
if (directoryPath == null) return null;
+
+ if (directoryPath.equals("/storage/emulated") ||
+ directoryPath.equals("/storage/emulated/")) {
+ // This path is not reachable for MediaProvider.
+ return null;
+ }
+
+ // We are extracting relative path for the directory itself, we add "/" so that we can use
+ // same PATTERN_RELATIVE_PATH to match relative path for directory. For example, relative
+ // path of '/storage/<volume_name>' is null where as relative path for directory is "/", for
+ // PATTERN_RELATIVE_PATH to match '/storage/<volume_name>', it should end with "/".
+ if (!directoryPath.endsWith("/")) {
+ // Relative path for directory should end with "/".
+ directoryPath += "/";
+ }
+
final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(directoryPath);
if (matcher.find()) {
- if (matcher.end() == directoryPath.length() - 1) {
+ if (matcher.end() == directoryPath.length()) {
// This is the top-level directory, so relative path is "/"
return "/";
}
- return directoryPath.substring(matcher.end()) + "/";
+ return directoryPath.substring(matcher.end());
}
return null;
}
diff --git a/src/com/android/providers/media/util/IsoInterface.java b/src/com/android/providers/media/util/IsoInterface.java
index d91a1a5..5de4b6b 100644
--- a/src/com/android/providers/media/util/IsoInterface.java
+++ b/src/com/android/providers/media/util/IsoInterface.java
@@ -49,6 +49,7 @@
private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE);
public static final int BOX_FTYP = 0x66747970;
+ public static final int BOX_HDLR = 0x68646c72;
public static final int BOX_UUID = 0x75756964;
public static final int BOX_META = 0x6d657461;
public static final int BOX_XMP = 0x584d505f;
@@ -99,6 +100,7 @@
public UUID uuid;
public byte[] data;
public List<Box> children;
+ public int headerSize;
public Box(int type, long[] range) {
this.type = type;
@@ -133,44 +135,75 @@
private static @Nullable Box parseNextBox(@NonNull FileDescriptor fd, long end,
@NonNull String prefix) throws ErrnoException, IOException {
final long pos = Os.lseek(fd, 0, OsConstants.SEEK_CUR);
- if (pos == end) {
+
+ int headerSize = 8;
+ if (end - pos < headerSize) {
return null;
}
- final long len = Integer.toUnsignedLong(readInt(fd));
- if (len <= 0 || pos + len > end) {
- Log.w(TAG, "Invalid box at " + pos + " of length " + len
- + " reached beyond end of parent " + end);
- return null;
- }
-
- // Skip past legacy data on 'meta' box
+ long len = Integer.toUnsignedLong(readInt(fd));
final int type = readInt(fd);
- if (type == BOX_META) {
- readInt(fd);
+
+ if (len == 0) {
+ // Length 0 means the box extends to the end of the file.
+ len = end - pos;
+ } else if (len == 1) {
+ // Actually 64-bit box length.
+ headerSize += 8;
+ long high = readInt(fd);
+ long low = readInt(fd);
+ len = (high << 32L) | (low & 0xffffffffL);
+ }
+
+ if (len < headerSize || pos + len > end) {
+ Log.w(TAG, "Invalid box at " + pos + " of length " + len
+ + ". End of parent " + end);
+ return null;
}
final Box box = new Box(type, new long[] { pos, len });
- if (LOGV) {
- Log.v(TAG, prefix + "Found box " + typeToString(type)
- + " at " + pos + " length " + len);
- }
+ box.headerSize = headerSize;
// Parse UUID box
if (type == BOX_UUID) {
+ box.headerSize += 16;
box.uuid = readUuid(fd);
if (LOGV) {
Log.v(TAG, prefix + " UUID " + box.uuid);
}
- box.data = new byte[(int) (len - 8 - 16)];
+ if (len > Integer.MAX_VALUE) {
+ Log.w(TAG, "Skipping abnormally large uuid box");
+ return null;
+ }
+
+ box.data = new byte[(int) (len - box.headerSize)];
Os.read(fd, box.data, 0, box.data.length);
+ } else if (type == BOX_XMP) {
+ if (len > Integer.MAX_VALUE) {
+ Log.w(TAG, "Skipping abnormally large xmp box");
+ return null;
+ }
+ box.data = new byte[(int) (len - box.headerSize)];
+ Os.read(fd, box.data, 0, box.data.length);
+ } else if (type == BOX_META && len != headerSize) {
+ // The format of this differs in ISO and QT encoding:
+ // (iso) [1 byte version + 3 bytes flags][4 byte size of next atom]
+ // (qt) [4 byte size of next atom ][4 byte hdlr atom type ]
+ // In case of (iso) we need to skip the next 4 bytes before parsing
+ // the children.
+ readInt(fd);
+ int maybeBoxType = readInt(fd);
+ if (maybeBoxType != BOX_HDLR) {
+ // ISO, skip 4 bytes.
+ box.headerSize += 4;
+ }
+ Os.lseek(fd, pos + box.headerSize, OsConstants.SEEK_SET);
}
- // Parse XMP box
- if (type == BOX_XMP) {
- box.data = new byte[(int) (len - 8)];
- Os.read(fd, box.data, 0, box.data.length);
+ if (LOGV) {
+ Log.v(TAG, prefix + "Found box " + typeToString(type)
+ + " at " + pos + " hdr " + box.headerSize + " length " + len);
}
// Recursively parse any children boxes
@@ -248,7 +281,7 @@
LongArray res = new LongArray();
for (Box box : mFlattened) {
if (box.type == type) {
- res.add(box.range[0] + 8);
+ res.add(box.range[0] + box.headerSize);
res.add(box.range[0] + box.range[1]);
}
}
@@ -259,7 +292,7 @@
LongArray res = new LongArray();
for (Box box : mFlattened) {
if (box.type == BOX_UUID && Objects.equals(box.uuid, uuid)) {
- res.add(box.range[0] + 8 + 16);
+ res.add(box.range[0] + box.headerSize);
res.add(box.range[0] + box.range[1]);
}
}
diff --git a/src/com/android/providers/media/util/MimeUtils.java b/src/com/android/providers/media/util/MimeUtils.java
index a932fd9..09417bd 100644
--- a/src/com/android/providers/media/util/MimeUtils.java
+++ b/src/com/android/providers/media/util/MimeUtils.java
@@ -43,7 +43,8 @@
*/
public static boolean startsWithIgnoreCase(@Nullable String target, @Nullable String other) {
if (target == null || other == null) return false;
- return target.regionMatches(true, 0, other, 0, Math.min(target.length(), other.length()));
+ if (other.length() > target.length()) return false;
+ return target.regionMatches(true, 0, other, 0, other.length());
}
/**
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 181dbb3..fc10b78 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.BACKUP;
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
+import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
@@ -31,21 +32,22 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.app.AppOpsManager;
+import android.app.DownloadManager;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.content.pm.ProviderInfo;
-import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
public class PermissionUtils {
+
+ public static final String OPSTR_NO_ISOLATED_STORAGE = "android:no_isolated_storage";
+
// Callers must hold both the old and new permissions, so that we can
// handle obscure cases like when an app targets Q but was installed on
// a device that was originally running on P before being upgraded to Q.
- private static volatile int sLegacyMediaProviderUid = -1;
-
private static ThreadLocal<String> sOpDescription = new ThreadLocal<>();
public static void setOpDescription(@Nullable String description) {
@@ -54,24 +56,50 @@
public static void clearOpDescription() { sOpDescription.set(null); }
- 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()
- || uid == android.os.Process.SHELL_UID || uid == android.os.Process.ROOT_UID
- || isLegacyMediaProvider(context, uid);
+ public static boolean checkPermissionSelf(@NonNull Context context, int pid, int uid) {
+ return android.os.Process.myUid() == uid;
}
- public static boolean checkPermissionBackup(@NonNull Context context, int pid, int uid) {
- return context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED;
+ public static boolean checkPermissionShell(@NonNull Context context, int pid, int uid) {
+ switch (uid) {
+ case android.os.Process.ROOT_UID:
+ case android.os.Process.SHELL_UID:
+ return true;
+ default:
+ return false;
+ }
}
- public static boolean checkPermissionManageExternalStorage(@NonNull Context context, int pid,
+ /**
+ * Check if the given package has been granted the "file manager" role on
+ * the device, which should grant them certain broader access.
+ */
+ public static boolean checkPermissionManager(@NonNull Context context, int pid,
int uid, @NonNull String packageName, @Nullable String attributionTag) {
- return checkPermissionForDataDelivery(context, MANAGE_EXTERNAL_STORAGE, pid, uid,
+ if (checkPermissionForDataDelivery(context, MANAGE_EXTERNAL_STORAGE, pid, uid,
packageName, attributionTag,
- generateAppOpMessage(packageName,sOpDescription.get()));
+ generateAppOpMessage(packageName,sOpDescription.get()))) {
+ return true;
+ }
+ // Fallback to OPSTR_NO_ISOLATED_STORAGE app op.
+ return checkNoIsolatedStorageGranted(context, uid, packageName, attributionTag);
+ }
+
+ /**
+ * Check if the given package has the ability to "delegate" the ownership of
+ * media items that they own to other apps, typically when they've finished
+ * performing operations on behalf of those apps.
+ * <p>
+ * One use-case for this is backup/restore apps, where the app restoring the
+ * content needs to shift the ownership back to the app that originally
+ * owned that media.
+ * <p>
+ * Another use-case is {@link DownloadManager}, which shifts ownership of
+ * finished downloads to the app that originally requested them.
+ */
+ public static boolean checkPermissionDelegator(@NonNull Context context, int pid, int uid) {
+ return (context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED)
+ || (context.checkPermission(UPDATE_DEVICE_STATS, pid, uid) == PERMISSION_GRANTED);
}
public static boolean checkPermissionWriteStorage(@NonNull Context context, int pid, int uid,
@@ -157,6 +185,15 @@
generateAppOpMessage(packageName, sOpDescription.get()));
}
+ @VisibleForTesting
+ static boolean checkNoIsolatedStorageGranted(@NonNull Context context, int uid,
+ @NonNull String packageName, @Nullable String attributionTag) {
+ final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+ int ret = appOps.noteOpNoThrow(OPSTR_NO_ISOLATED_STORAGE, uid, packageName, attributionTag,
+ generateAppOpMessage(packageName, "am instrument --no-isolated-storage"));
+ return ret == AppOpsManager.MODE_ALLOWED;
+ }
+
/**
* 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}.
@@ -210,21 +247,6 @@
}
}
- private static boolean isLegacyMediaProvider(Context context, int uid) {
- if (sLegacyMediaProviderUid == -1) {
- // Uid stays constant while legacy Media Provider stays installed. Cache legacy
- // MediaProvider's uid for the first time.
- ProviderInfo pi = context.getPackageManager()
- .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0);
- if (pi == null) {
- return false;
- }
-
- sLegacyMediaProviderUid = pi.applicationInfo.uid;
- }
- return (uid == sLegacyMediaProviderUid);
- }
-
/**
* Checks whether a given package in a UID and PID has a given permission
* and whether the app op that corresponds to this permission is allowed.
diff --git a/src/com/android/providers/media/util/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index 27c0a25..e95d28c 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -76,6 +76,9 @@
private static final Pattern sPattern156832140 = Pattern.compile(
"(?i)%com\\.gopro\\.smarty%");
+ private static final Pattern sCustomCollatorPattern = Pattern.compile(
+ "(?i)custom_[a-zA-Z]+");
+
private Map<String, String> mProjectionMap = null;
private Collection<Pattern> mProjectionGreylist = null;
@@ -786,6 +789,7 @@
private void enforceStrictToken(@NonNull String token) {
if (TextUtils.isEmpty(token)) return;
if (isTableOrColumn(token)) return;
+ if (isCustomCollator(token)) return;
if (SQLiteTokenizer.isFunction(token)) return;
if (SQLiteTokenizer.isType(token)) return;
@@ -1078,6 +1082,10 @@
return computeSingleProjection(token) != null;
}
+ private boolean isCustomCollator(String token) {
+ return sCustomCollatorPattern.matcher(token).matches();
+ }
+
/** {@hide} */
public @Nullable String computeWhere(@Nullable String selection) {
final boolean hasInternal = !TextUtils.isEmpty(mWhereClause);
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 0d29bbf..26a0a0b 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -20,6 +20,7 @@
static_libs: [
"androidx.test.rules",
+ "collector-device-lib-platform",
"mockito-target",
"truth-prebuilt",
],
diff --git a/tests/client/src/com/android/providers/media/client/DelegatorTest.java b/tests/client/src/com/android/providers/media/client/DelegatorTest.java
new file mode 100644
index 0000000..042223c
--- /dev/null
+++ b/tests/client/src/com/android/providers/media/client/DelegatorTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.client;
+
+import static com.android.providers.media.client.LegacyProviderMigrationTest.executeShellCommand;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.provider.MediaStore.MediaColumns;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Verify media ownership delegation behaviors for an app holding
+ * {@code UPDATE_DEVICE_STATS} permission.
+ */
+@RunWith(AndroidJUnit4.class)
+public class DelegatorTest {
+ private static final String TAG = "DelegatorTest";
+
+ /**
+ * To confirm behaviors, we need to pick an app installed on all devices
+ * which has no permissions, and the best candidate is the "Easter Egg" app.
+ */
+ private static final String PERMISSIONLESS_APP = "com.android.egg";
+
+ private ContentResolver mResolver;
+
+ private Uri mExternalAudio = MediaStore.Audio.Media
+ .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+
+ @Before
+ public void setUp() throws Exception {
+ mResolver = InstrumentationRegistry.getTargetContext().getContentResolver();
+ }
+
+ /**
+ * Delegation allows us to push ownership of items we own to someone else.
+ */
+ @Test
+ public void testPushAllowed() throws Exception {
+ final Uri uri = createAudio();
+
+ assertEquals(InstrumentationRegistry.getTargetContext().getPackageName(), getOwner(uri));
+
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.OWNER_PACKAGE_NAME, PERMISSIONLESS_APP);
+ mResolver.update(uri, values, null);
+
+ assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+ }
+
+ /**
+ * Delegation allows us to push orphaned items to someone else.
+ */
+ @Test
+ public void testOrphanedAllowed() throws Exception {
+ final Uri uri = createAudio();
+ clearOwner(uri);
+
+ assertEquals(null, getOwner(uri));
+
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.OWNER_PACKAGE_NAME, PERMISSIONLESS_APP);
+ mResolver.update(uri, values, null);
+
+ assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+ }
+
+ /**
+ * However, attempting to steal items belonging to someone else is blocked.
+ */
+ @Test
+ public void testPullBlocked() throws Exception {
+ final Uri uri = createAudio();
+ setOwner(uri, PERMISSIONLESS_APP);
+
+ assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.OWNER_PACKAGE_NAME,
+ InstrumentationRegistry.getTargetContext().getPackageName());
+ mResolver.update(uri, values, null);
+
+ assertEquals(PERMISSIONLESS_APP, getOwner(uri));
+ }
+
+ private Uri createAudio() throws IOException {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.DISPLAY_NAME, "Song " + System.nanoTime());
+ values.put(MediaColumns.MIME_TYPE, "audio/mpeg");
+
+ final Uri uri = mResolver.insert(mExternalAudio, values);
+ try (OutputStream out = mResolver.openOutputStream(uri)) {
+ }
+ return uri;
+ }
+
+ private String getOwner(Uri uri) throws Exception {
+ try (Cursor cursor = mResolver.query(uri,
+ new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null)) {
+ assertTrue(cursor.moveToFirst());
+ return cursor.getString(0);
+ }
+ }
+
+ public static void setOwner(Uri uri, String packageName) throws Exception {
+ executeShellCommand("content update"
+ + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
+ + " --uri " + uri
+ + " --bind owner_package_name:s:" + packageName,
+ InstrumentationRegistry.getInstrumentation().getUiAutomation());
+ }
+
+ public static void clearOwner(Uri uri) throws Exception {
+ executeShellCommand("content update"
+ + " --user " + InstrumentationRegistry.getTargetContext().getUserId()
+ + " --uri " + uri
+ + " --bind owner_package_name:n:",
+ InstrumentationRegistry.getInstrumentation().getUiAutomation());
+ }
+}
diff --git a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
index cb642b2..0c1fc65 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -65,6 +65,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
@@ -359,6 +360,21 @@
public static String executeShellCommand(String command, UiAutomation uiAutomation)
throws IOException {
+ int attempt = 0;
+ while (attempt++ < 5) {
+ try {
+ return executeShellCommandInternal(command, uiAutomation);
+ } catch (InterruptedIOException e) {
+ // Hmm, we had trouble executing the shell command; the best we
+ // can do is try again a few more times
+ Log.v(TAG, "Trouble executing " + command + "; trying again", e);
+ }
+ }
+ throw new IOException("Failed to execute " + command);
+ }
+
+ public static String executeShellCommandInternal(String command, UiAutomation uiAutomation)
+ throws IOException {
Log.v(TAG, "$ " + command);
ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
BufferedReader br = null;
diff --git a/tests/client/src/com/android/providers/media/client/PerformanceTest.java b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
index 9d89465..1e1abe7 100644
--- a/tests/client/src/com/android/providers/media/client/PerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
@@ -16,6 +16,7 @@
package com.android.providers.media.client;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.ContentProviderOperation;
@@ -25,6 +26,7 @@
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Environment;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
@@ -36,9 +38,12 @@
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.File;
+import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -232,6 +237,62 @@
MediaStore.waitForIdle(resolver);
}
+ @Test
+ public void testDirOperations_10() throws Exception {
+ Timer createTimer = new Timer("mkdir");
+ Timer readTimer = new Timer("readdir");
+ Timer deleteTimer = new Timer("rmdir");
+ for (int i = 0; i < COUNT_REPEAT; i++ ){
+ doDirOperations(10, createTimer, readTimer, deleteTimer);
+ }
+ createTimer.dumpResults();
+ readTimer.dumpResults();
+ deleteTimer.dumpResults();
+ }
+
+ @Test
+ public void testDirOperations_100() throws Exception {
+ Timer createTimer = new Timer("mkdir");
+ Timer readTimer = new Timer("readdir");
+ Timer deleteTimer = new Timer("rmdir");
+ for (int i = 0; i < COUNT_REPEAT; i++ ){
+ doDirOperations(100, createTimer, readTimer, deleteTimer);
+ }
+ createTimer.dumpResults();
+ readTimer.dumpResults();
+ deleteTimer.dumpResults();
+ }
+
+ private void doDirOperations(int size, Timer createTimer, Timer readTimer, Timer deleteTimer)
+ throws Exception {
+ createTimer.start();
+ File testDir = new File(new File(Environment.getExternalStorageDirectory(),
+ "Download"), "test_dir_" + System.nanoTime());
+ testDir.mkdir();
+ List<File> files = new ArrayList<>();
+ for (int i = 0; i < size; i++) {
+ File file = new File(testDir, "file_" + System.nanoTime());
+ assertTrue(file.createNewFile());
+ files.add(file);
+ }
+ createTimer.stop();
+
+ try {
+ readTimer.start();
+ File[] result = testDir.listFiles();
+ readTimer.stop();
+ assertEquals(size, result.length);
+
+ } finally {
+ deleteTimer.start();
+ for (File file : files) {
+ assertTrue(file.delete());
+ }
+ assertTrue(testDir.delete());
+ deleteTimer.stop();
+ }
+ }
+
private static Set<Uri> asSet(Collection<Uri> uris) {
return new HashSet<>(uris);
}
diff --git a/tests/res/raw/testvideo_meta.mp4 b/tests/res/raw/testvideo_meta.mp4
new file mode 100644
index 0000000..e83c61d
--- /dev/null
+++ b/tests/res/raw/testvideo_meta.mp4
Binary files differ
diff --git a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
index 64411d3..e30ed92 100644
--- a/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
+++ b/tests/src/com/android/providers/media/LocalCallingIdentityTest.java
@@ -67,19 +67,22 @@
assertEquals(Arrays.asList(pm.getPackagesForUid(android.os.Process.myUid())),
Arrays.asList(ident.getSharedPackageNames()));
- assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SYSTEM));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+ assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SELF));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SHELL));
+ assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_MANAGER));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_DELEGATOR));
+
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_AUDIO));
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_VIDEO));
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_IMAGES));
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_AUDIO));
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_VIDEO));
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_IMAGES));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
- assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_BACKUP));
- assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_MANAGE_EXTERNAL_STORAGE));
}
@Test
@@ -95,18 +98,21 @@
assertEquals(Arrays.asList(MediaProviderTest.PERMISSIONLESS_APP),
Arrays.asList(ident.getSharedPackageNames()));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SYSTEM));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SELF));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_SHELL));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_MANAGER));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_DELEGATOR));
+
assertTrue(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
+ assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE));
+
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_AUDIO));
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_VIDEO));
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_READ_IMAGES));
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_AUDIO));
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_VIDEO));
assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_WRITE_IMAGES));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_READ));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_IS_BACKUP));
- assertFalse(ident.hasPermission(LocalCallingIdentity.PERMISSION_MANAGE_EXTERNAL_STORAGE));
}
}
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 56a9424..7e898f2 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -17,9 +17,12 @@
package com.android.providers.media;
import static com.android.providers.media.scan.MediaScannerTest.stage;
+import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
import static com.android.providers.media.util.FileUtils.isDownload;
import static com.android.providers.media.util.FileUtils.isDownloadDir;
+import static com.google.common.truth.Truth.assertWithMessage;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -503,6 +506,73 @@
}
@Test
+ public void testBuildData_InvalidSecondaryTypes() throws Exception {
+ assertEndsWith("/Pictures/foo.png",
+ buildFile(MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+ null, "foo.png", "image/*"));
+
+ assertThrows(IllegalArgumentException.class, () -> {
+ buildFile(MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+ null, "foo", "video/*");
+ });
+ assertThrows(IllegalArgumentException.class, () -> {
+ buildFile(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
+ null, "foo.mp4", "audio/*");
+ });
+ }
+
+ @Test
+ public void testBuildData_EmptyTypes() throws Exception {
+ Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ assertEndsWith("/Pictures/foo.png",
+ buildFile(uri, null, "foo.png", ""));
+
+ uri = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ assertEndsWith(".mp4",
+ buildFile(uri, null, "", ""));
+ }
+
+ @Test
+ public void testEnsureFileColumns_InvalidMimeType_targetSdkQ() throws Exception {
+ final MediaProvider provider = new MediaProvider() {
+ @Override
+ public boolean isFuseThread() {
+ return false;
+ }
+
+ @Override
+ public int getCallingPackageTargetSdkVersion() {
+ return Build.VERSION_CODES.Q;
+ }
+ };
+
+ final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ final ContentValues values = new ContentValues();
+
+ values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
+ provider.ensureFileColumns(uri, values);
+ assertMimetype(values, "image/jpeg");
+ assertDisplayName(values, "pngimage.png.jpg");
+
+ values.clear();
+ values.put(MediaColumns.DISPLAY_NAME, "pngimage.png");
+ values.put(MediaColumns.MIME_TYPE, "");
+ provider.ensureFileColumns(uri, values);
+ assertMimetype(values, "image/jpeg");
+ assertDisplayName(values, "pngimage.png.jpg");
+
+ values.clear();
+ values.put(MediaColumns.MIME_TYPE, "");
+ provider.ensureFileColumns(uri, values);
+ assertMimetype(values, "image/jpeg");
+
+ values.clear();
+ values.put(MediaColumns.DISPLAY_NAME, "foo.foo");
+ provider.ensureFileColumns(uri, values);
+ assertMimetype(values, "image/jpeg");
+ assertDisplayName(values, "foo.foo.jpg");
+ }
+
@Ignore("Enable as part of b/142561358")
public void testBuildData_Charset() throws Exception {
final Uri uri = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
@@ -885,6 +955,11 @@
public boolean isFuseThread() {
return false;
}
+
+ @Override
+ public int getCallingPackageTargetSdkVersion() {
+ return Build.VERSION_CODES.CUR_DEVELOPMENT;
+ }
};
provider.ensureFileColumns(uri, values);
@@ -893,29 +968,39 @@
@Test
public void testRelativePathForInvalidDirectories() throws Exception {
- for (String data : new String[] {
- "/storage/IMG1024.JPG",
- "/data/media/IMG1024.JPG",
- "IMG1024.JPG",
- "storage/emulated/",
+ for (String path : new String[] {
+ "/storage/emulated",
+ "/storage",
+ "/data/media/Foo.jpg",
+ "Foo.jpg",
+ "storage/Foo"
}) {
- assertEquals(FileUtils.extractRelativePathForDirectory(data), null);
+ assertEquals(null, FileUtils.extractRelativePathForDirectory(path));
}
}
@Test
public void testRelativePathForValidDirectories() throws Exception {
- for (Pair<String, String> top : Arrays.asList(
- Pair.create("/storage/emulated/0", new String("/")),
- Pair.create("/storage/emulated/0/DCIM", "DCIM/"),
- Pair.create("/storage/emulated/0/DCIM/Camera", "DCIM/Camera/"),
- Pair.create("/storage/emulated/0/Android/media/com.example/Foo",
- "Android/media/com.example/Foo/"),
- Pair.create("/storage/0000-0000/DCIM/Camera", "DCIM/Camera/"))) {
- assertEquals(top.second, FileUtils.extractRelativePathForDirectory(top.first));
+ for (String prefix : new String[] {
+ "/storage/emulated/0",
+ "/storage/emulated/10",
+ "/storage/ABCD-1234"
+ }) {
+ assertRelativePathForDirectory(prefix, "/");
+ assertRelativePathForDirectory(prefix + "/DCIM", "DCIM/");
+ assertRelativePathForDirectory(prefix + "/DCIM/Camera", "DCIM/Camera/");
+ assertRelativePathForDirectory(prefix + "/Z", "Z/");
+ assertRelativePathForDirectory(prefix + "/Android/media/com.example/Foo",
+ "Android/media/com.example/Foo/");
}
}
+ private static void assertRelativePathForDirectory(String directoryPath, String relativePath) {
+ assertWithMessage("extractRelativePathForDirectory(" + directoryPath + ") :")
+ .that(extractRelativePathForDirectory(directoryPath))
+ .isEqualTo(relativePath);
+ }
+
private static ContentValues computeDataValues(String path) {
final ContentValues values = new ContentValues();
values.put(MediaColumns.DATA, path);
@@ -948,6 +1033,10 @@
assertEquals(type, values.get(MediaColumns.MIME_TYPE));
}
+ private static void assertDisplayName(ContentValues values, String type) {
+ assertEquals(type, values.get(MediaColumns.DISPLAY_NAME));
+ }
+
private static boolean isGreylistMatch(String raw) {
for (Pattern p : MediaProvider.sGreylist) {
if (p.matcher(raw).matches()) {
diff --git a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
index 227a8fd..7783957 100644
--- a/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
+++ b/tests/src/com/android/providers/media/util/IsoInterfaceTest.java
@@ -84,6 +84,28 @@
assertEquals("3F9DD7A46B26513A7C35272F0D623A06", xmp.getOriginalDocumentId());
}
+ @Test
+ public void testIsoMeta() throws Exception {
+ final IsoInterface isoMeta = IsoInterface.fromFile(stageFile(R.raw.test_video_xmp));
+ final long[] hdlrRanges = isoMeta.getBoxRanges(IsoInterface.BOX_HDLR);
+
+ // There are 3 hdlr boxes, the 3rd is inside the meta box. Check it was parsed correctly.
+ assertEquals(3 * 2, hdlrRanges.length);
+ assertEquals(30145, hdlrRanges[2 * 2 + 0]);
+ assertEquals(30170, hdlrRanges[2 * 2 + 1]);
+ }
+
+ @Test
+ public void testQtMeta() throws Exception {
+ final IsoInterface qtMeta = IsoInterface.fromFile(stageFile(R.raw.testvideo_meta));
+ final long[] hdlrRanges = qtMeta.getBoxRanges(IsoInterface.BOX_HDLR);
+
+ // There are 3 hdlr boxes, the 1st is inside the meta box. Check it was parsed correctly.
+ assertEquals(3 * 2, hdlrRanges.length);
+ assertEquals(16636, hdlrRanges[2 * 0 + 0]);
+ assertEquals(16661, hdlrRanges[2 * 0 + 1]);
+ }
+
private static File stageFile(int resId) throws Exception {
final Context context = InstrumentationRegistry.getContext();
final File file = File.createTempFile("test", ".mp4");
diff --git a/tests/src/com/android/providers/media/util/MimeUtilsTest.java b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
index abf8ff0..b57b5c5 100644
--- a/tests/src/com/android/providers/media/util/MimeUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/MimeUtilsTest.java
@@ -57,12 +57,12 @@
assertTrue(startsWithIgnoreCase("image/jpg", "image/"));
assertTrue(startsWithIgnoreCase("Image/Jpg", "image/"));
- assertFalse(equalIgnoreCase("image/", "image/jpg"));
+ assertFalse(startsWithIgnoreCase("image/", "image/jpg"));
- assertFalse(equalIgnoreCase("image/jpg", "audio/"));
- assertFalse(equalIgnoreCase("image/jpg", null));
- assertFalse(equalIgnoreCase(null, "audio/"));
- assertFalse(equalIgnoreCase(null, null));
+ assertFalse(startsWithIgnoreCase("image/jpg", "audio/"));
+ assertFalse(startsWithIgnoreCase("image/jpg", null));
+ assertFalse(startsWithIgnoreCase(null, "audio/"));
+ assertFalse(startsWithIgnoreCase(null, null));
}
@Test
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index 5c54e3f..1eba379 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -16,13 +16,15 @@
package com.android.providers.media.util;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
+import static com.android.providers.media.util.PermissionUtils.checkNoIsolatedStorageGranted;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionDelegator;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManager;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
-import static com.android.providers.media.util.PermissionUtils.checkPermissionSystem;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionShell;
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteAudio;
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
@@ -57,9 +59,10 @@
final int uid = android.os.Process.myUid();
final String packageName = context.getPackageName();
- assertTrue(checkPermissionSystem(context, pid, uid, packageName));
- assertFalse(checkPermissionBackup(context, pid, uid));
- assertFalse(checkPermissionManageExternalStorage(context, pid, uid, packageName, null));
+ assertTrue(checkPermissionSelf(context, pid, uid));
+ assertFalse(checkPermissionShell(context, pid, uid));
+ assertFalse(checkPermissionManager(context, pid, uid, packageName, null));
+ assertFalse(checkPermissionDelegator(context, pid, uid));
assertTrue(checkPermissionReadStorage(context, pid, uid, packageName, null));
assertTrue(checkPermissionWriteStorage(context, pid, uid, packageName, null));
@@ -71,4 +74,15 @@
assertTrue(checkPermissionReadImages(context, pid, uid, packageName, null));
assertFalse(checkPermissionWriteImages(context, pid, uid, packageName, null));
}
+
+ /**
+ * Test that {@code android:no_isolated_storage} app op is by default denied.
+ */
+ @Test
+ public void testNoIsolatedStorageIsByDefaultDenied() throws Exception {
+ final Context context = InstrumentationRegistry.getContext();
+ final int uid = android.os.Process.myUid();
+ final String packageName = context.getPackageName();
+ assertFalse(checkNoIsolatedStorageGranted(context, uid, packageName, null));
+ }
}
diff --git a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
index 2d304d3..9af8e5d 100644
--- a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
+++ b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
@@ -740,6 +740,17 @@
"Month", "Month In (1,2)", null, null);
}
+ @Test
+ public void testStrictCustomCollator() {
+ final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
+ final HashMap<String, String> map = new HashMap<>();
+ map.put("bucket_id", "bucket_id");
+ builder.setProjectionMap(map);
+
+ final String sortOrder = "bucket_id COLLATE custom_zh ASC";
+ builder.enforceStrictGrammar(null, null, null, sortOrder, null);
+ }
+
private void assertStrictInsertValid(ContentValues values) {
mStrictBuilder.insert(mDatabase, values);
}