Merge "Const correctness handle / RedactionInfo."
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 3c0b842..41eb152 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -157,9 +157,10 @@
/** {@hide} */
public static final String CREATE_DELETE_REQUEST_CALL = "create_delete_request";
-
/** {@hide} */
public static final String GET_VERSION_CALL = "get_version";
+ /** {@hide} */
+ public static final String GET_GENERATION_CALL = "get_generation";
/** {@hide} */
@Deprecated
@@ -1196,6 +1197,40 @@
@Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
public static final String IS_DOWNLOAD = "is_download";
+ /**
+ * Generation number at which metadata for this media item was first
+ * inserted. This is useful for apps that are attempting to quickly
+ * identify exactly which media items have been added since a previous
+ * point in time. Generation numbers are monotonically increasing over
+ * time, and can be safely arithmetically compared.
+ * <p>
+ * Detecting media additions using generation numbers is more robust
+ * than using {@link #DATE_ADDED}, since those values may change in
+ * unexpected ways when apps use {@link File#setLastModified(long)} or
+ * when the system clock is set incorrectly.
+ *
+ * @see MediaStore#getGeneration(Context, String)
+ */
+ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+ public static final String GENERATION_ADDED = "generation_added";
+
+ /**
+ * Generation number at which metadata for this media item was last
+ * changed. This is useful for apps that are attempting to quickly
+ * identify exactly which media items have changed since a previous
+ * point in time. Generation numbers are monotonically increasing over
+ * time, and can be safely arithmetically compared.
+ * <p>
+ * Detecting media changes using generation numbers is more robust than
+ * using {@link #DATE_MODIFIED}, since those values may change in
+ * unexpected ways when apps use {@link File#setLastModified(long)} or
+ * when the system clock is set incorrectly.
+ *
+ * @see MediaStore#getGeneration(Context, String)
+ */
+ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+ public static final String GENERATION_MODIFIED = "generation_modified";
+
// =======================================
// ==== MediaMetadataRetriever values ====
// =======================================
@@ -3634,6 +3669,39 @@
}
/**
+ * Return the latest generation value for the given volume.
+ * <p>
+ * Generation numbers are useful for apps that are attempting to quickly
+ * identify exactly which media items have been added or changed since a
+ * previous point in time. Generation numbers are monotonically increasing
+ * over time, and can be safely arithmetically compared.
+ * <p>
+ * Detecting media changes using generation numbers is more robust than
+ * using {@link MediaColumns#DATE_ADDED} or
+ * {@link MediaColumns#DATE_MODIFIED}, since those values may change in
+ * unexpected ways when apps use {@link File#setLastModified(long)} or when
+ * the system clock is set incorrectly.
+ *
+ * @param volumeName specific volume to obtain an generation value for. Must
+ * be one of the values returned from
+ * {@link #getExternalVolumeNames(Context)}.
+ * @see MediaColumns#GENERATION_ADDED
+ * @see MediaColumns#GENERATION_MODIFIED
+ */
+ public static long getGeneration(@NonNull Context context, @NonNull String volumeName) {
+ return getGeneration(context.getContentResolver(), volumeName);
+ }
+
+ /** {@hide} */
+ public static long getGeneration(@NonNull ContentResolver resolver,
+ @NonNull String volumeName) {
+ final Bundle in = new Bundle();
+ in.putString(Intent.EXTRA_TEXT, volumeName);
+ final Bundle out = resolver.call(AUTHORITY, GET_GENERATION_CALL, null, in);
+ return out.getLong(Intent.EXTRA_INDEX);
+ }
+
+ /**
* Return a {@link DocumentsProvider} Uri that is an equivalent to the given
* {@link MediaStore} Uri.
* <p>
diff --git a/jni/Android.bp b/jni/Android.bp
index 3fe9c56..0ff937b 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -88,3 +88,27 @@
sdk_version: "current",
stl: "c++_static",
}
+
+cc_test {
+ name: "RedactionInfoTest",
+ test_suites: ["device-tests"],
+ srcs: [
+ "RedactionInfoTest.cpp",
+ "RedactionInfo.cpp",
+ ],
+
+ local_include_dirs: ["include"],
+
+ static_libs: [
+ "libbase_ndk",
+ ],
+
+ shared_libs: [
+ "liblog",
+ ],
+
+ tidy: true,
+
+ sdk_version: "current",
+ stl: "c++_static",
+}
\ No newline at end of file
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 5dfbb3f..8c96088 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -14,6 +14,7 @@
#define ATRACE_TAG ATRACE_TAG_APP
#define LOG_TAG "FuseDaemon"
+#define LIBFUSE_LOG_TAG "libfuse"
#include "FuseDaemon.h"
@@ -74,7 +75,9 @@
// logging macros to avoid duplication.
#define TRACE LOG(DEBUG)
+#define TRACE_VERBOSE LOG(VERBOSE)
#define TRACE_FUSE(__fuse) TRACE << "[" << __fuse->path << "] "
+#define TRACE_FUSE_VERBOSE(__fuse) TRACE_VERBOSE << "[" << __fuse->path << "] "
#define ATRACE_NAME(name) ScopedTrace ___tracer(name)
#define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)
@@ -319,7 +322,7 @@
*/
static int set_file_lock(int fd, bool for_read, const std::string& path) {
std::string lock_str = (for_read ? "read" : "write");
- TRACE << "Setting " << lock_str << " lock for path " << path;
+ TRACE_VERBOSE << "Setting " << lock_str << " lock for path " << path;
struct flock fl{};
fl.l_type = for_read ? F_RDLCK : F_WRLCK;
@@ -330,7 +333,7 @@
PLOG(ERROR) << "Failed to set " << lock_str << " lock on path " << path;
return res;
}
- TRACE << "Successfully set " << lock_str << " lock on path " << path;
+ TRACE_VERBOSE << "Successfully set " << lock_str << " lock on path " << path;
return res;
}
@@ -344,7 +347,7 @@
* Returns true if fd may have a lock, false otherwise
*/
static bool is_file_locked(int fd, const std::string& path) {
- TRACE << "Checking if file is locked " << path;
+ TRACE_VERBOSE << "Checking if file is locked " << path;
struct flock fl{};
fl.l_type = F_WRLCK;
@@ -357,7 +360,7 @@
return true;
}
bool locked = fl.l_type != F_UNLCK;
- TRACE << "File " << path << " is " << (locked ? "locked" : "unlocked");
+ TRACE_VERBOSE << "File " << path << " is " << (locked ? "locked" : "unlocked");
return locked;
}
@@ -453,8 +456,8 @@
node* parent_node = fuse->FromInode(parent);
string parent_path = parent_node->BuildPath();
- TRACE_FUSE(fuse) << "LOOKUP " << name << " @ " << parent << " (" << safe_name(parent_node)
- << ")";
+ TRACE_FUSE_VERBOSE(fuse) << "LOOKUP " << name << " @ " << parent << " ("
+ << safe_name(parent_node) << ")";
string child_path = parent_path + "/" + name;
@@ -523,7 +526,6 @@
const struct fuse_ctx* ctx = fuse_req_ctx(req);
node* node = fuse->FromInode(ino);
string path = node->BuildPath();
-
TRACE_FUSE(fuse) << "GETATTR @ " << ino << " (" << safe_name(node) << ")";
if (!node) fuse_reply_err(req, ENOENT);
@@ -601,12 +603,19 @@
lstat(path.c_str(), attr);
fuse_reply_attr(req, attr, 10);
}
-/*
-static void pf_readlink(fuse_req_t req, fuse_ino_t ino)
+
+static void pf_canonical_path(fuse_req_t req, fuse_ino_t ino)
{
- cout << "TODO:" << __func__;
+ node* node = get_fuse(req)->FromInode(ino);
+
+ if (node) {
+ // TODO(b/147482155): Check that uid has access to |path| and its contents
+ fuse_reply_canonical_path(req, node->BuildPath().c_str());
+ return;
+ }
+ fuse_reply_err(req, ENOENT);
}
-*/
+
static void pf_mknod(fuse_req_t req,
fuse_ino_t parent,
const char* name,
@@ -985,7 +994,6 @@
ATRACE_CALL();
handle* h = reinterpret_cast<handle*>(fi->fh);
struct fuse* fuse = get_fuse(req);
- TRACE_FUSE(fuse) << "READ";
fuse->fadviser.Record(h->fd, size);
@@ -1306,7 +1314,7 @@
node* node = fuse->FromInode(ino);
const string path = node->BuildPath();
- TRACE_FUSE(fuse) << "ACCESS " << path;
+ TRACE_FUSE_VERBOSE(fuse) << "ACCESS " << path;
int res = access(path.c_str(), F_OK);
fuse_reply_err(req, res ? errno : 0);
@@ -1422,7 +1430,7 @@
.destroy = pf_destroy,
.lookup = pf_lookup, .forget = pf_forget, .getattr = pf_getattr,
.setattr = pf_setattr,
- /*.readlink = pf_readlink,*/
+ .canonical_path = pf_canonical_path,
.mknod = pf_mknod, .mkdir = pf_mkdir, .unlink = pf_unlink,
.rmdir = pf_rmdir,
/*.symlink = pf_symlink,*/
@@ -1469,11 +1477,11 @@
});
static void fuse_logger(enum fuse_log_level level, const char* fmt, va_list ap) {
- __android_log_vprint(fuse_to_android_loglevel.at(level), LOG_TAG, fmt, ap);
+ __android_log_vprint(fuse_to_android_loglevel.at(level), LIBFUSE_LOG_TAG, fmt, ap);
}
bool FuseDaemon::ShouldOpenWithFuse(int fd, bool for_read, const std::string& path) {
- TRACE << "Checking if file should be opened with FUSE " << path;
+ TRACE_VERBOSE << "Checking if file should be opened with FUSE " << path;
bool use_fuse = false;
if (active.load(std::memory_order_acquire)) {
diff --git a/tests/jni/RedactionInfoTest.cpp b/jni/RedactionInfoTest.cpp
similarity index 79%
rename from tests/jni/RedactionInfoTest.cpp
rename to jni/RedactionInfoTest.cpp
index fc6be40..9d98058 100644
--- a/tests/jni/RedactionInfoTest.cpp
+++ b/jni/RedactionInfoTest.cpp
@@ -43,8 +43,8 @@
RedactionInfo info(0, nullptr);
EXPECT_EQ(0, info.size());
EXPECT_EQ(false, info.isRedactionNeeded());
-
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1000, /*off*/1000);
+
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 1000);
EXPECT_EQ(0, overlapping_rr->size());
}
@@ -52,31 +52,34 @@
* Test the case where there is 1 redaction range.
*/
TEST(RedactionInfoTest, testSingleRedactionRange) {
- off64_t ranges[2] = { 1, 10, };
+ off64_t ranges[2] = {
+ 1,
+ 10,
+ };
RedactionInfo info(1, ranges);
EXPECT_EQ(1, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Overlapping ranges
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1000, /*off*/0);
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/5, /*off*/0);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 5, /*off*/ 0);
EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/5, /*off*/5);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 5, /*off*/ 5);
EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/10, /*off*/1);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 10, /*off*/ 1);
EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1, /*off*/1);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1, /*off*/ 1);
EXPECT_EQ(*(createRedactionRangeVector(1, ranges)), *overlapping_rr);
// Non-overlapping range
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/11);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 11);
EXPECT_EQ(*(createRedactionRangeVector(0, nullptr)), *overlapping_rr);
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1, /*off*/11);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1, /*off*/ 11);
EXPECT_EQ(*(createRedactionRangeVector(0, nullptr)), *overlapping_rr);
}
@@ -84,29 +87,33 @@
* Test the case where the redaction ranges don't require sorting or merging
*/
TEST(RedactionInfoTest, testSortedAndNonOverlappingRedactionRanges) {
- off64_t ranges[6] = { 1, 10,
- 15, 21,
- 32, 40, };
+ off64_t ranges[6] = {
+ 1, 10, 15, 21, 32, 40,
+ };
RedactionInfo info = RedactionInfo(3, ranges);
EXPECT_EQ(3, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Read request strictly contains all ranges: [0, 49]
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/50, /*off*/0);
- off64_t expected1[] = { 1, 10,
- 15, 21,
- 32, 40, };
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+ off64_t expected1[] = {
+ 1, 10, 15, 21, 32, 40,
+ };
EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
// Read request strictly contains a subset of the ranges: [15, 40]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
- off64_t expected2[] = { 15, 21,
- 32, 40, };
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+ off64_t expected2[] = {
+ 15,
+ 21,
+ 32,
+ 40,
+ };
EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
// Read request intersects with a subset of the ranges" [16, 32]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/17, /*off*/16);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
}
@@ -114,29 +121,33 @@
* Test the case where the redaction ranges require sorting
*/
TEST(RedactionInfoTest, testSortRedactionRanges) {
- off64_t ranges[6] = { 1, 10,
- 32, 40,
- 15, 21, };
+ off64_t ranges[6] = {
+ 1, 10, 32, 40, 15, 21,
+ };
RedactionInfo info = RedactionInfo(3, ranges);
EXPECT_EQ(3, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Read request strictly contains all ranges: [0, 49]
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/50, /*off*/0);
- off64_t expected1[] = { 1, 10,
- 15, 21,
- 32, 40, };
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+ off64_t expected1[] = {
+ 1, 10, 15, 21, 32, 40,
+ };
EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
// Read request strictly contains a subset of the ranges: [15, 40]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
- off64_t expected2[] = { 15, 21,
- 32, 40, };
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+ off64_t expected2[] = {
+ 15,
+ 21,
+ 32,
+ 40,
+ };
EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
// Read request intersects with a subset of the ranges" [16, 32]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/17, /*off*/16);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
}
@@ -144,30 +155,33 @@
* Test the case where the redaction ranges require sorting or merging
*/
TEST(RedactionInfoTest, testSortAndMergeRedactionRanges) {
- off64_t ranges[8] = { 35, 40,
- 1, 10,
- 32, 35,
- 15, 21, };
+ off64_t ranges[8] = {
+ 35, 40, 1, 10, 32, 35, 15, 21,
+ };
RedactionInfo info = RedactionInfo(4, ranges);
EXPECT_EQ(3, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Read request strictly contains all ranges: [0, 49]
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/50, /*off*/0);
- off64_t expected1[] = { 1, 10,
- 15, 21,
- 32, 40, };
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 50, /*off*/ 0);
+ off64_t expected1[] = {
+ 1, 10, 15, 21, 32, 40,
+ };
EXPECT_EQ(*(createRedactionRangeVector(3, expected1)), *overlapping_rr);
// Read request strictly contains a subset of the ranges: [15, 40]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
- off64_t expected2[] = { 15, 21,
- 32, 40, };
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
+ off64_t expected2[] = {
+ 15,
+ 21,
+ 32,
+ 40,
+ };
EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
// Read request intersects with a subset of the ranges" [16, 32]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/17, /*off*/16);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 17, /*off*/ 16);
EXPECT_EQ(*(createRedactionRangeVector(2, expected2)), *overlapping_rr);
}
@@ -175,27 +189,25 @@
* Test the case where the redaction ranges all merge into the first range
*/
TEST(RedactionInfoTest, testMergeAllRangesIntoTheFirstRange) {
- off64_t ranges[10] = { 1, 100,
- 2, 99,
- 3, 98,
- 4, 97,
- 3, 15, };
+ off64_t ranges[10] = {
+ 1, 100, 2, 99, 3, 98, 4, 97, 3, 15,
+ };
RedactionInfo info = RedactionInfo(5, ranges);
EXPECT_EQ(1, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Read request equals the range: [1, 100]
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/1);
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 1);
off64_t expected[] = {1, 100};
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
// Read request is contained in the range: [15, 40]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
// Read request that strictly contains all of the redaction ranges
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1000, /*off*/0);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
}
@@ -203,27 +215,25 @@
* Test the case where the redaction ranges all merge into the last range
*/
TEST(RedactionInfoTest, testMergeAllRangesIntoTheLastRange) {
- off64_t ranges[10] = { 4, 96,
- 3, 97,
- 2, 98,
- 1, 99,
- 0, 100, };
+ off64_t ranges[10] = {
+ 4, 96, 3, 97, 2, 98, 1, 99, 0, 100,
+ };
RedactionInfo info = RedactionInfo(5, ranges);
EXPECT_EQ(1, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Read request equals the range: [0, 100]
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/0);
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 0);
off64_t expected[] = {0, 100};
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
// Read request is contained in the range: [15, 40]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/26, /*off*/15);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 26, /*off*/ 15);
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
// Read request that strictly contains all of the redaction ranges
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/1000, /*off*/0);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 1000, /*off*/ 0);
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
}
@@ -231,40 +241,36 @@
* Test the case where the redaction ranges progressively merge
*/
TEST(RedactionInfoTest, testMergeAllRangesProgressively) {
- off64_t ranges[10] = { 1, 11,
- 2, 12,
- 3, 13,
- 4, 14,
- 5, 15, };
+ off64_t ranges[10] = {
+ 1, 11, 2, 12, 3, 13, 4, 14, 5, 15,
+ };
RedactionInfo info = RedactionInfo(5, ranges);
EXPECT_EQ(1, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Read request equals the range: [1, 15]
- auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/15, /*off*/1);
+ auto overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 15, /*off*/ 1);
off64_t expected[] = {1, 15};
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
// Read request is contained in the range: [2, 12]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/10, /*off*/2);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 10, /*off*/ 2);
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
// Read request that strictly contains all of the redaction ranges
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/100, /*off*/0);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 100, /*off*/ 0);
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
- off64_t reverse_rr[10] = { 5, 15,
- 4, 14,
- 3, 13,
- 2, 12,
- 1, 11, };
+ off64_t reverse_rr[10] = {
+ 5, 15, 4, 14, 3, 13, 2, 12, 1, 11,
+ };
RedactionInfo reverse_info = RedactionInfo(5, reverse_rr);
EXPECT_EQ(1, info.size());
EXPECT_EQ(true, info.isRedactionNeeded());
// Read request equals the range: [1, 15]
- overlapping_rr = info.getOverlappingRedactionRanges(/*size*/15, /*off*/1);
+ overlapping_rr = info.getOverlappingRedactionRanges(/*size*/ 15, /*off*/ 1);
EXPECT_EQ(*(createRedactionRangeVector(1, expected)), *overlapping_rr);
}
diff --git a/tests/jni/RedactionInfoTest.xml b/jni/RedactionInfoTest.xml
similarity index 100%
rename from tests/jni/RedactionInfoTest.xml
rename to jni/RedactionInfoTest.xml
diff --git a/jni/TEST_MAPPING b/jni/TEST_MAPPING
index be62f80..a871459 100644
--- a/jni/TEST_MAPPING
+++ b/jni/TEST_MAPPING
@@ -2,6 +2,9 @@
"presubmit": [
{
"name": "RedactionInfoTest"
+ },
+ {
+ "name": "fuse_node_test"
}
]
}
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index bc0bbc9..4085a3a 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -101,7 +101,9 @@
@Override
public Uri insert(Uri uri, ContentValues values) {
try {
- new File(values.getAsString(MediaColumns.DATA)).createNewFile();
+ final File file = new File(values.getAsString(MediaColumns.DATA));
+ file.getParentFile().mkdirs();
+ file.createNewFile();
} catch (IOException e) {
throw new IllegalStateException(e);
}
diff --git a/logging.sh b/logging.sh
index cf14af5..43f0d86 100755
--- a/logging.sh
+++ b/logging.sh
@@ -1,28 +1,29 @@
#!/bin/bash
level=$1
-uids=$(adb shell cat /data/system/packages.list |grep -Po "providers.media[a-z\.]* \K\d+")
if [ $level == "on" ] || [ $level == "extreme" ]
then
adb shell setprop log.tag.MediaProvider VERBOSE
adb shell setprop log.tag.ModernMediaScanner VERBOSE
+ adb shell setprop log.tag.FuseDaemon DEBUG
+ adb shell setprop log.tag.libfuse DEBUG
else
adb shell setprop log.tag.MediaProvider INFO
adb shell setprop log.tag.ModernMediaScanner INFO
+ adb shell setprop log.tag.FuseDaemon INFO
+ adb shell setprop log.tag.libfuse INFO
fi
if [ $level == "extreme" ]
then
- for uid in $uids;
- do adb shell setprop db.log.slow_query_threshold.$uid 0;
- done
- adb shell setprop db.log.bindargs 1
+ adb shell setprop log.tag.SQLiteQueryBuilder VERBOSE
+ adb shell setprop log.tag.FuseDaemon VERBOSE
+ adb shell setprop log.tag.libfuse VERBOSE
else
- for uid in $uids;
- do adb shell setprop db.log.slow_query_threshold.$uid 10000;
- done
- adb shell setprop db.log.bindargs 0
+ adb shell setprop log.tag.SQLiteQueryBuilder INFO
+ adb shell setprop log.tag.FuseDaemon INFO
+ adb shell setprop log.tag.libfuse INFO
fi
# Kill process to kick new settings into place
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index ccf5627..c579e80 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -70,6 +70,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
+import java.util.function.LongSupplier;
import java.util.regex.Matcher;
/**
@@ -78,16 +79,16 @@
* on demand, create and upgrade the schema, etc.
*/
public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable {
- // maximum number of cached external databases to keep
- private static final int MAX_EXTERNAL_DATABASES = 3;
-
- // Delete databases that have not been used in two months
- // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60)
- private static final long OBSOLETE_DATABASE_DB = 5184000000L;
-
static final String INTERNAL_DATABASE_NAME = "internal.db";
static final String EXTERNAL_DATABASE_NAME = "external.db";
+ /**
+ * Raw SQL clause that can be used to obtain the current generation, which
+ * is designed to be populated into {@link MediaColumns#GENERATION_ADDED} or
+ * {@link MediaColumns#GENERATION_MODIFIED}.
+ */
+ public static final String CURRENT_GENERATION_CLAUSE = "SELECT generation FROM local_metadata";
+
final Context mContext;
final String mName;
final int mVersion;
@@ -180,7 +181,7 @@
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.wtf(TAG, "Database operations must not happen on main thread", new Throwable());
}
- return super.getReadableDatabase();
+ return super.getWritableDatabase();
}
@Override
@@ -201,89 +202,6 @@
downgradeDatabase(db, oldV, newV);
}
- /**
- * For devices that have removable storage, we support keeping multiple databases
- * to allow users to switch between a number of cards.
- * On such devices, touch this particular database and garbage collect old databases.
- * An LRU cache system is used to clean up databases for old external
- * storage volumes.
- */
- @Override
- public void onOpen(SQLiteDatabase db) {
- if (mEarlyUpgrade) return; // Doing early upgrade.
- if (mInternal) return; // The internal database is kept separately.
-
- // the code below is only needed on devices with removable storage
- if (!Environment.isExternalStorageRemovable()) return;
-
- // touch the database file to show it is most recently used
- File file = new File(db.getPath());
- long now = System.currentTimeMillis();
- file.setLastModified(now);
-
- // delete least recently used databases if we are over the limit
- String[] databases = mContext.databaseList();
- // Don't delete wal auxiliary files(db-shm and db-wal) directly because db file may
- // not be deleted, and it will cause Disk I/O error when accessing this database.
- List<String> dbList = new ArrayList<String>();
- for (String database : databases) {
- if (database != null && database.endsWith(".db")) {
- dbList.add(database);
- }
- }
- databases = dbList.toArray(new String[0]);
- int count = databases.length;
- int limit = MAX_EXTERNAL_DATABASES;
-
- // delete external databases that have not been used in the past two months
- long twoMonthsAgo = now - OBSOLETE_DATABASE_DB;
- for (int i = 0; i < databases.length; i++) {
- File other = mContext.getDatabasePath(databases[i]);
- if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) {
- databases[i] = null;
- count--;
- if (file.equals(other)) {
- // reduce limit to account for the existence of the database we
- // are about to open, which we removed from the list.
- limit--;
- }
- } else {
- long time = other.lastModified();
- if (time < twoMonthsAgo) {
- if (LOGV) Log.v(TAG, "Deleting old database " + databases[i]);
- mContext.deleteDatabase(databases[i]);
- databases[i] = null;
- count--;
- }
- }
- }
-
- // delete least recently used databases until
- // we are no longer over the limit
- while (count > limit) {
- int lruIndex = -1;
- long lruTime = 0;
-
- for (int i = 0; i < databases.length; i++) {
- if (databases[i] != null) {
- long time = mContext.getDatabasePath(databases[i]).lastModified();
- if (lruTime == 0 || time < lruTime) {
- lruIndex = i;
- lruTime = time;
- }
- }
- }
-
- // delete least recently used database
- if (lruIndex != -1) {
- if (LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]);
- mContext.deleteDatabase(databases[lruIndex]);
- databases[lruIndex] = null;
- count--;
- }
- }
- }
-
@GuardedBy("mProjectionMapCache")
private final ArrayMap<Class<?>[], ArrayMap<String, String>>
mProjectionMapCache = new ArrayMap<>();
@@ -327,11 +245,16 @@
public void beginTransaction() {
getWritableDatabase().beginTransaction();
+ getWritableDatabase().execSQL("UPDATE local_metadata SET generation=generation+1;");
mNotifyChanges.set(new ArrayList<>());
}
public void setTransactionSuccessful() {
getWritableDatabase().setTransactionSuccessful();
+ }
+
+ public void endTransaction() {
+ getWritableDatabase().endTransaction();
final List<Uri> uris = mNotifyChanges.get();
if (uris != null) {
BackgroundThread.getExecutor().execute(() -> {
@@ -341,8 +264,26 @@
mNotifyChanges.remove();
}
- public void endTransaction() {
- getWritableDatabase().endTransaction();
+ /**
+ * Execute the given runnable inside a transaction. If the calling thread is
+ * not already in an active transaction, this method will wrap the given
+ * runnable inside a new transaction.
+ */
+ public long runWithTransaction(@NonNull LongSupplier s) {
+ if (mNotifyChanges.get() != null) {
+ // Already inside a transaction, so we can run directly
+ return s.getAsLong();
+ } else {
+ // Not inside a transaction, so we need to make one
+ beginTransaction();
+ try {
+ final long res = s.getAsLong();
+ setTransactionSuccessful();
+ return res;
+ } finally {
+ endTransaction();
+ }
+ }
}
/**
@@ -461,6 +402,9 @@
makePristineSchema(db);
+ db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)");
+ db.execSQL("INSERT INTO local_metadata VALUES (0)");
+
db.execSQL("CREATE TABLE android_metadata (locale TEXT)");
db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER,"
+ "kind INTEGER,width INTEGER,height INTEGER)");
@@ -498,7 +442,8 @@
+ "is_favorite INTEGER DEFAULT 0, num_tracks INTEGER DEFAULT NULL,"
+ "writer TEXT DEFAULT NULL, exposure_time TEXT DEFAULT NULL,"
+ "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
- + "scene_capture_type INTEGER DEFAULT NULL)");
+ + "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
+ + "generation_modified INTEGER DEFAULT 0)");
db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
if (!mInternal) {
@@ -681,7 +626,7 @@
db.execSQL("CREATE VIEW audio_artists AS SELECT "
+ " artist_id AS " + Audio.Artists._ID
- + ", artist AS " + Audio.Artists.ARTIST
+ + ", MIN(artist) AS " + Audio.Artists.ARTIST
+ ", artist_key AS " + Audio.Artists.ARTIST_KEY
+ ", COUNT(DISTINCT album_id) AS " + Audio.Artists.NUMBER_OF_ALBUMS
+ ", COUNT(DISTINCT _id) AS " + Audio.Artists.NUMBER_OF_TRACKS
@@ -692,7 +637,7 @@
db.execSQL("CREATE VIEW audio_albums AS SELECT "
+ " album_id AS " + Audio.Albums._ID
+ ", album_id AS " + Audio.Albums.ALBUM_ID
- + ", album AS " + Audio.Albums.ALBUM
+ + ", MIN(album) AS " + Audio.Albums.ALBUM
+ ", album_key AS " + Audio.Albums.ALBUM_KEY
+ ", artist_id AS " + Audio.Albums.ARTIST_ID
+ ", artist AS " + Audio.Albums.ARTIST
@@ -708,7 +653,7 @@
db.execSQL("CREATE VIEW audio_genres AS SELECT "
+ " genre_id AS " + Audio.Genres._ID
- + ", genre AS " + Audio.Genres.NAME
+ + ", MIN(genre) AS " + Audio.Genres.NAME
+ " FROM audio"
+ " WHERE volume_name IN " + filterVolumeNames
+ " GROUP BY genre_id");
@@ -905,6 +850,16 @@
db.execSQL("DELETE FROM log;");
}
+ private static void updateAddLocalMetadata(SQLiteDatabase db, boolean internal) {
+ db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)");
+ db.execSQL("INSERT INTO local_metadata VALUES (0)");
+ }
+
+ private static void updateAddGeneration(SQLiteDatabase db, boolean internal) {
+ db.execSQL("ALTER TABLE files ADD COLUMN generation_added INTEGER DEFAULT 0;");
+ db.execSQL("ALTER TABLE files ADD COLUMN generation_modified INTEGER DEFAULT 0;");
+ }
+
private static void recomputeDataValues(SQLiteDatabase db, boolean internal) {
try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA },
null, null, null, null, null, null)) {
@@ -933,7 +888,7 @@
static final int VERSION_O = 800;
static final int VERSION_P = 900;
static final int VERSION_Q = 1023;
- static final int VERSION_R = 1107;
+ static final int VERSION_R = 1109;
static final int VERSION_LATEST = VERSION_R;
/**
@@ -1055,6 +1010,12 @@
if (fromVersion < 1107) {
updateAddSceneCaptureType(db, internal);
}
+ if (fromVersion < 1108) {
+ updateAddLocalMetadata(db, internal);
+ }
+ if (fromVersion < 1109) {
+ updateAddGeneration(db, internal);
+ }
if (recomputeDataValues) {
recomputeDataValues(db, internal);
@@ -1114,6 +1075,16 @@
}
/**
+ * Return the current generation that will be populated into
+ * {@link MediaColumns#GENERATION_ADDED} or
+ * {@link MediaColumns#GENERATION_MODIFIED}.
+ */
+ public long getGeneration() {
+ return android.database.DatabaseUtils.longForQuery(getReadableDatabase(),
+ CURRENT_GENERATION_CLAUSE + ";", null);
+ }
+
+ /**
* Return total number of items tracked inside this database. This includes
* only real media items, and does not include directories.
*/
@@ -1126,12 +1097,8 @@
* only real media items, and does not include directories.
*/
private long getItemCount(SQLiteDatabase db) {
- try (Cursor c = db.query(false, "files", new String[] { "COUNT(_id)" },
- FileColumns.MIME_TYPE + " IS NOT NULL", null, null, null, null, null, null)) {
- if (c.moveToFirst()) {
- return c.getLong(0);
- }
- }
- return 0;
+ return android.database.DatabaseUtils.longForQuery(db,
+ "SELECT COUNT(_id) FROM files WHERE " + FileColumns.MIME_TYPE + " IS NOT NULL",
+ null);
}
}
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index d4f32f3..3942057 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -23,6 +23,7 @@
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.checkPermissionReadAudio;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
@@ -186,6 +187,7 @@
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;
private int hasPermission;
private int hasPermissionResolved;
@@ -233,6 +235,8 @@
return checkPermissionWriteVideo(context, pid, uid, getPackageName());
case PERMISSION_WRITE_IMAGES:
return checkPermissionWriteImages(context, pid, uid, getPackageName());
+ case PERMISSION_MANAGE_EXTERNAL_STORAGE:
+ return checkPermissionManageExternalStorage(context, pid, uid, packageName);
default:
return false;
}
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 65150c8..8576f2e 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -86,7 +86,7 @@
public class MediaDocumentsProvider extends DocumentsProvider {
private static final String TAG = "MediaDocumentsProvider";
- private static final String AUTHORITY = "com.android.providers.media.documents";
+ public static final String AUTHORITY = "com.android.providers.media.documents";
private static final String SUPPORTED_QUERY_ARGS = joinNewline(
DocumentsContract.QUERY_ARG_DISPLAY_NAME,
@@ -112,18 +112,18 @@
private static final String AUDIO_MIME_TYPES = joinNewline(
"audio/*", "application/ogg", "application/x-flac");
- private static final String TYPE_IMAGES_ROOT = "images_root";
- private static final String TYPE_IMAGES_BUCKET = "images_bucket";
- private static final String TYPE_IMAGE = "image";
+ static final String TYPE_IMAGES_ROOT = "images_root";
+ static final String TYPE_IMAGES_BUCKET = "images_bucket";
+ static final String TYPE_IMAGE = "image";
- private static final String TYPE_VIDEOS_ROOT = "videos_root";
- private static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
- private static final String TYPE_VIDEO = "video";
+ static final String TYPE_VIDEOS_ROOT = "videos_root";
+ static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
+ static final String TYPE_VIDEO = "video";
- private static final String TYPE_AUDIO_ROOT = "audio_root";
- private static final String TYPE_AUDIO = "audio";
- private static final String TYPE_ARTIST = "artist";
- private static final String TYPE_ALBUM = "album";
+ static final String TYPE_AUDIO_ROOT = "audio_root";
+ static final String TYPE_AUDIO = "audio";
+ static final String TYPE_ARTIST = "artist";
+ static final String TYPE_ALBUM = "album";
private static boolean sReturnedImagesEmpty = false;
private static boolean sReturnedVideosEmpty = false;
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 1f0c262..5e3fcf9 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -44,6 +44,7 @@
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
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_READ_AUDIO;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
@@ -535,7 +536,7 @@
* devices. We only do this once per volume so we don't annoy the user if
* deleted manually.
*/
- private void ensureDefaultFolders(String volumeName, DatabaseHelper helper, SQLiteDatabase db) {
+ private void ensureDefaultFolders(String volumeName, DatabaseHelper helper) {
try {
final File path = getVolumePath(volumeName);
final StorageVolume vol = mStorageManager.getStorageVolume(path);
@@ -553,7 +554,7 @@
final File folder = new File(vol.getDirectory(), folderName);
if (!folder.exists()) {
folder.mkdirs();
- insertDirectory(db, folder.getAbsolutePath());
+ insertDirectory(helper, folder.getAbsolutePath());
}
}
@@ -1071,8 +1072,8 @@
/**
* Updates database entry for given {@code path} with {@code values}
*/
- private boolean updateDatabaseForFuseRename(SQLiteDatabase db, String oldPath, String newPath,
- ContentValues values) {
+ private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
+ @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
final Uri uriOldPath = Files.getContentUriForPath(oldPath);
boolean allowHidden = isCallingPackageAllowedHidden();
final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
@@ -1082,7 +1083,7 @@
boolean retryUpdateWithReplace = false;
try {
- count = qbForUpdate.update(db, values, selection, new String[]{oldPath});
+ count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
} catch (SQLiteConstraintException e) {
Log.w(TAG, "Database update failed while renaming " + oldPath, e);
retryUpdateWithReplace = true;
@@ -1094,9 +1095,9 @@
final Uri uriNewPath = Files.getContentUriForPath(oldPath);
final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE,
matchUri(uriNewPath, allowHidden), uriNewPath, Bundle.EMPTY, null);
- if (qbForDelete.delete(db, selection, new String[] {newPath}) == 1) {
+ if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) {
Log.i(TAG, "Retrying database update after deleting conflicting entry");
- count = qbForUpdate.update(db, values, selection, new String[]{oldPath});
+ count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
} else {
return false;
}
@@ -1219,8 +1220,8 @@
ArrayList<String> fileList = new ArrayList<>();
final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE};
- try (Cursor c = qb.query(helper.getReadableDatabase(), projection, selection, null,
- null, null, null)) {
+ try (Cursor c = qb.query(helper, projection, selection, null,
+ null, null, null, null, null)) {
// Check if the calling package has write permission to all files in the given
// directory. If calling package has write permission to all files in the directory, the
// query with update uri should return same number of files as previous query.
@@ -1283,21 +1284,20 @@
return -OsConstants.EPERM;
}
- final SQLiteDatabase db;
+ final DatabaseHelper helper;
try {
- final DatabaseHelper helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
- db = helper.getWritableDatabase();
+ helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
} catch (VolumeNotFoundException e) {
throw new IllegalStateException("Volume not found while trying to update database for "
+ oldPath, e);
}
- db.beginTransaction();
+ helper.beginTransaction();
try {
for (String filePath : fileList) {
final String newFilePath = newPath + "/" + filePath;
final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath));
- if(!updateDatabaseForFuseRename(db, oldPath + "/" + filePath, newFilePath,
+ if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath,
getContentValuesForFuseRename(newFilePath, mimeType, mimeType))) {
Log.e(TAG, "Calling package doesn't have write permission to rename file.");
return -OsConstants.EPERM;
@@ -1307,12 +1307,12 @@
// Rename the directory in lower file system.
int errno = renameInLowerFs(oldPath, newPath);
if (errno == 0) {
- db.setTransactionSuccessful();
+ helper.setTransactionSuccessful();
} else {
return errno;
}
} finally {
- db.endTransaction();
+ helper.endTransaction();
}
// Process metadata in background thread.
postProcessMetadataForFuseRename(oldPath, newPath);
@@ -1343,19 +1343,18 @@
return -OsConstants.EPERM;
}
- final SQLiteDatabase db;
+ final DatabaseHelper helper;
try {
- final DatabaseHelper helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
- db = helper.getWritableDatabase();
+ helper = getDatabaseForUri(Files.getContentUriForPath(oldPath));
} catch (VolumeNotFoundException e) {
throw new IllegalStateException("Volume not found while trying to update database for"
+ oldPath + ". Rename failed due to database update error", e);
}
- db.beginTransaction();
+ helper.beginTransaction();
try {
final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
- if (!updateDatabaseForFuseRename(db, oldPath, newPath,
+ if (!updateDatabaseForFuseRename(helper, oldPath, newPath,
getContentValuesForFuseRename(newPath, oldMimeType, newMimeType))) {
Log.e(TAG, "Calling package doesn't have write permission to rename file.");
return -OsConstants.EPERM;
@@ -1364,12 +1363,12 @@
// Try renaming oldPath to newPath in lower file system.
int errno = renameInLowerFs(oldPath, newPath);
if (errno == 0) {
- db.setTransactionSuccessful();
+ helper.setTransactionSuccessful();
} else {
return errno;
}
} finally {
- db.endTransaction();
+ helper.endTransaction();
}
// Process metadata in background thread.
postProcessMetadataForFuseRename(oldPath, newPath);
@@ -1488,10 +1487,8 @@
final int table = matchUri(uri, allowHidden);
final DatabaseHelper helper;
- final SQLiteDatabase db;
try {
helper = getDatabaseForUri(uri);
- db = helper.getReadableDatabase();
} catch (VolumeNotFoundException e) {
return PackageManager.PERMISSION_DENIED;
}
@@ -1504,8 +1501,8 @@
}
final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null);
- try (Cursor c = qb.query(db,
- new String[] { BaseColumns._ID }, null, null, null, null, null)) {
+ try (Cursor c = qb.query(helper,
+ new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) {
if (c.getCount() == 1) {
return PackageManager.PERMISSION_GRANTED;
}
@@ -1574,54 +1571,8 @@
}
final DatabaseHelper helper = getDatabaseForUri(uri);
- final SQLiteDatabase db = helper.getReadableDatabase();
-
final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs,
honoredArgs::add);
- String filter = uri.getQueryParameter("filter");
- String [] keywords = null;
- if (filter != null) {
- filter = Uri.decode(filter).trim();
- if (!TextUtils.isEmpty(filter)) {
- String [] searchWords = filter.split(" ");
- keywords = new String[searchWords.length];
- for (int i = 0; i < searchWords.length; i++) {
- String key = MediaStore.Audio.keyFor(searchWords[i]);
- key = key.replace("\\", "\\\\");
- key = key.replace("%", "\\%");
- key = key.replace("_", "\\_");
- keywords[i] = key;
- }
- }
- }
-
- String keywordColumn = null;
- switch (table) {
- case AUDIO_MEDIA:
- case AUDIO_GENRES_ALL_MEMBERS:
- case AUDIO_GENRES_ID_MEMBERS:
- case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
- case AUDIO_PLAYLISTS_ID_MEMBERS:
- keywordColumn = MediaStore.Audio.Media.ARTIST_KEY +
- "||" + MediaStore.Audio.Media.ALBUM_KEY +
- "||" + MediaStore.Audio.Media.TITLE_KEY;
- break;
- case AUDIO_ARTISTS_ID_ALBUMS:
- case AUDIO_ALBUMS:
- keywordColumn = MediaStore.Audio.Media.ARTIST_KEY + "||"
- + MediaStore.Audio.Media.ALBUM_KEY;
- break;
- case AUDIO_ARTISTS:
- keywordColumn = MediaStore.Audio.Media.ARTIST_KEY;
- break;
- }
-
- if (keywordColumn != null) {
- for (int i = 0; keywords != null && i < keywords.length; i++) {
- appendWhereStandalone(qb, keywordColumn + " LIKE ? ESCAPE '\\'",
- "%" + keywords[i] + "%");
- }
- }
if (targetSdkVersion < Build.VERSION_CODES.R) {
// Some apps are abusing "ORDER BY" clauses to inject "LIMIT"
@@ -1687,8 +1638,8 @@
final String sortOrder = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER);
final String limit = queryArgs.getString(QUERY_ARG_SQL_LIMIT);
- final Cursor c = qb.query(db, projection,
- selection, selectionArgs, groupBy, having, sortOrder, limit, signal);
+ final Cursor c = qb.query(helper, projection, selection, selectionArgs, groupBy, having,
+ sortOrder, limit, signal);
if (c != null) {
// As a performance optimization, only configure notifications when
@@ -2124,10 +2075,8 @@
}
final DatabaseHelper helper;
- final SQLiteDatabase db;
try {
helper = getDatabaseForUri(uri);
- db = helper.getWritableDatabase();
} catch (VolumeNotFoundException e) {
return e.translateForUpdateDelete(targetSdkVersion);
}
@@ -2142,12 +2091,12 @@
}
}
- private long insertDirectory(SQLiteDatabase db, String path) {
+ private long insertDirectory(DatabaseHelper helper, String path) {
if (LOGV) Log.v(TAG, "inserting directory " + path);
ContentValues values = new ContentValues();
values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
values.put(FileColumns.DATA, path);
- values.put(FileColumns.PARENT, getParent(db, path));
+ values.put(FileColumns.PARENT, getParent(helper, path));
values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path));
values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
@@ -2157,11 +2106,12 @@
if (file.exists()) {
values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
}
+ final SQLiteDatabase db = helper.getWritableDatabase();
long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
return rowId;
}
- private long getParent(SQLiteDatabase db, String path) {
+ private long getParent(DatabaseHelper helper, String path) {
final String parentPath = new File(path).getParent();
if (Objects.equals("/", parentPath)) {
return -1;
@@ -2174,12 +2124,13 @@
}
final long id;
+ final SQLiteDatabase db = helper.getReadableDatabase();
try (Cursor c = db.query("files", new String[] { FileColumns._ID },
FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) {
if (c.moveToFirst()) {
id = c.getLong(0);
} else {
- id = insertDirectory(db, parentPath);
+ id = insertDirectory(helper, parentPath);
}
}
@@ -2325,8 +2276,8 @@
}
}
- private long insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull SQLiteDatabase db, int match,
- @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
+ private long insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
+ int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
int mediaType, boolean notify) {
boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA)
|| TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA));
@@ -2427,12 +2378,12 @@
Long parent = values.getAsLong(FileColumns.PARENT);
if (parent == null) {
if (path != null) {
- long parentId = getParent(db, path);
+ long parentId = getParent(helper, path);
values.put(FileColumns.PARENT, parentId);
}
}
- rowId = db.insert("files", FileColumns.DATE_MODIFIED, values);
+ rowId = qb.insert(helper, values);
}
if (format == MtpConstants.FORMAT_ASSOCIATION) {
synchronized (mDirectoryCache) {
@@ -2483,7 +2434,15 @@
@Nullable Bundle extras) {
Trace.beginSection("insert");
try {
- return insertInternal(uri, values, extras);
+ try {
+ return insertInternal(uri, values, extras);
+ } catch (SQLiteConstraintException e) {
+ if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
+ throw e;
+ } else {
+ return null;
+ }
+ }
} catch (FallbackException e) {
return e.translateForInsert(getCallingPackageTargetSdkVersion());
} finally {
@@ -2577,15 +2536,13 @@
Uri newUri = null;
final DatabaseHelper helper = getDatabaseForUri(uri);
- final SQLiteDatabase db = helper.getWritableDatabase();
-
final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
switch (match) {
case IMAGES_MEDIA: {
maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
final boolean isDownload = maybeMarkAsDownload(initialValues);
- rowId = insertFile(qb, db, match, uri, extras, initialValues,
+ rowId = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_IMAGE, true);
if (rowId > 0) {
MediaDocumentsProvider.onMediaStoreInsert(
@@ -2610,7 +2567,7 @@
ensureUniqueFileColumns(match, uri, extras, initialValues);
- rowId = qb.insert(db, initialValues);
+ rowId = qb.insert(helper, initialValues);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(Images.Thumbnails.
getContentUri(originalVolumeName), rowId);
@@ -2632,7 +2589,7 @@
ensureUniqueFileColumns(match, uri, extras, initialValues);
- rowId = qb.insert(db, initialValues);
+ rowId = qb.insert(helper, initialValues);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(Video.Thumbnails.
getContentUri(originalVolumeName), rowId);
@@ -2643,7 +2600,7 @@
case AUDIO_MEDIA: {
maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
final boolean isDownload = maybeMarkAsDownload(initialValues);
- rowId = insertFile(qb, db, match, uri, extras, initialValues,
+ rowId = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_AUDIO, true);
if (rowId > 0) {
MediaDocumentsProvider.onMediaStoreInsert(
@@ -2663,7 +2620,7 @@
final long audioId = Long.parseLong(uri.getPathSegments().get(2));
enforceCallingPermission(ContentUris.withAppendedId(
MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId),
- Bundle.EMPTY, true);
+ Bundle.EMPTY, false);
final long playlistId = initialValues
.getAsLong(MediaStore.Audio.Playlists.Members.PLAYLIST_ID);
enforceCallingPermission(ContentUris.withAppendedId(
@@ -2672,10 +2629,10 @@
ContentValues values = new ContentValues(initialValues);
values.put(Audio.Playlists.Members.AUDIO_ID, audioId);
- rowId = qb.insert(db, values);
+ rowId = qb.insert(helper, values);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(uri, rowId);
- updatePlaylistDateModifiedToNow(db, playlistId);
+ updatePlaylistDateModifiedToNow(helper, playlistId);
}
break;
}
@@ -2693,7 +2650,7 @@
final boolean isDownload = maybeMarkAsDownload(initialValues);
ContentValues values = new ContentValues(initialValues);
values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
- rowId = insertFile(qb, db, match, uri, extras, values,
+ rowId = insertFile(qb, helper, match, uri, extras, values,
FileColumns.MEDIA_TYPE_PLAYLIST, true);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(
@@ -2709,7 +2666,7 @@
.getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID);
enforceCallingPermission(ContentUris.withAppendedId(
MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId),
- Bundle.EMPTY, true);
+ Bundle.EMPTY, false);
final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
enforceCallingPermission(ContentUris.withAppendedId(
MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId),
@@ -2717,10 +2674,10 @@
ContentValues values = new ContentValues(initialValues);
values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId);
- rowId = qb.insert(db, values);
+ rowId = qb.insert(helper, values);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(uri, rowId);
- updatePlaylistDateModifiedToNow(db, playlistId);
+ updatePlaylistDateModifiedToNow(helper, playlistId);
}
break;
}
@@ -2728,7 +2685,7 @@
case VIDEO_MEDIA: {
maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
final boolean isDownload = maybeMarkAsDownload(initialValues);
- rowId = insertFile(qb, db, match, uri, extras, initialValues,
+ rowId = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_VIDEO, true);
if (rowId > 0) {
MediaDocumentsProvider.onMediaStoreInsert(
@@ -2746,7 +2703,7 @@
ensureUniqueFileColumns(match, uri, extras, initialValues);
- rowId = qb.insert(db, initialValues);
+ rowId = qb.insert(helper, initialValues);
if (rowId > 0) {
newUri = ContentUris.withAppendedId(uri, rowId);
}
@@ -2756,7 +2713,7 @@
case FILES: {
maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
final boolean isDownload = maybeMarkAsDownload(initialValues);
- rowId = insertFile(qb, db, match, uri, extras, initialValues,
+ rowId = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_NONE, true);
if (rowId > 0) {
MediaDocumentsProvider.onMediaStoreInsert(
@@ -2769,7 +2726,7 @@
case DOWNLOADS:
maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
initialValues.put(FileColumns.IS_DOWNLOAD, true);
- rowId = insertFile(qb, db, match, uri, extras, initialValues,
+ rowId = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_NONE, false);
if (rowId > 0) {
final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
@@ -2851,6 +2808,15 @@
qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs));
}
+ private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb,
+ @NonNull String[] columns, @Nullable String filter) {
+ if (TextUtils.isEmpty(filter)) return;
+ for (String filterWord : filter.split("\\s+")) {
+ appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'",
+ "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%");
+ }
+ }
+
private static boolean parseBoolean(String value) {
if (value == null) return false;
if ("1".equals(value)) return true;
@@ -2943,6 +2909,9 @@
if (matchTrashed == MATCH_DEFAULT) matchTrashed = MATCH_EXCLUDE;
if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE;
+ // Handle callers using legacy filtering
+ final String filter = uri.getQueryParameter("filter");
+
boolean includeAllVolumes = false;
switch (match) {
@@ -3025,6 +2994,9 @@
+ " IN " + sharedPackages
+ " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"));
}
+ appendWhereStandaloneFilter(qb, new String[] {
+ AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+ }, filter);
appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending);
appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed);
appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite);
@@ -3100,7 +3072,9 @@
} else {
throw new UnsupportedOperationException("Genres cannot be directly modified");
}
-
+ appendWhereStandaloneFilter(qb, new String[] {
+ AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+ }, filter);
if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
// We don't have a great way to filter parsed metadata by
// owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3162,6 +3136,9 @@
qb.setTables("audio_playlists_map");
qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class));
}
+ appendWhereStandaloneFilter(qb, new String[] {
+ AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+ }, filter);
if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
// We don't have a great way to filter parsed metadata by
// owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3198,6 +3175,9 @@
} else {
throw new UnsupportedOperationException("Albums cannot be directly modified");
}
+ appendWhereStandaloneFilter(qb, new String[] {
+ AudioColumns.ALBUM_KEY
+ }, filter);
if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
// We don't have a great way to filter parsed metadata by
// owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3215,6 +3195,9 @@
} else {
throw new UnsupportedOperationException("Artists cannot be directly modified");
}
+ appendWhereStandaloneFilter(qb, new String[] {
+ AudioColumns.ARTIST_KEY
+ }, filter);
if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
// We don't have a great way to filter parsed metadata by
// owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3232,6 +3215,9 @@
} else {
throw new UnsupportedOperationException("Albums cannot be directly modified");
}
+ appendWhereStandaloneFilter(qb, new String[] {
+ AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY
+ }, filter);
if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
// We don't have a great way to filter parsed metadata by
// owner, so callers need to hold READ_MEDIA_AUDIO
@@ -3329,6 +3315,9 @@
appendWhereStandalone(qb, TextUtils.join(" OR ", options));
}
+ appendWhereStandaloneFilter(qb, new String[] {
+ AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
+ }, filter);
appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending);
appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed);
appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite);
@@ -3492,11 +3481,9 @@
}
final DatabaseHelper helper = getDatabaseForUri(uri);
- final SQLiteDatabase db = helper.getWritableDatabase();
+ final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
{
- final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
-
// Give callers interacting with a specific media item a chance to
// escalate access if they don't already have it
switch (match) {
@@ -3518,8 +3505,8 @@
if (isFilesTable) {
String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
if (deleteparam == null || ! deleteparam.equals("false")) {
- Cursor c = qb.query(db, projection, userWhere, userWhereArgs,
- null, null, null, null);
+ Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
+ null, null, null, null, null);
String [] idvalue = new String[] { "" };
String [] playlistvalues = new String[] { "", "" };
try {
@@ -3563,6 +3550,7 @@
idvalue[0] = String.valueOf(id);
// for each playlist that the item appears in, move
// all the items behind it forward by one
+ final SQLiteDatabase db = helper.getWritableDatabase();
Cursor cc = db.query("audio_playlists_map",
sPlaylistIdPlayOrder,
"audio_id=?", idvalue, null, null, null);
@@ -3575,7 +3563,7 @@
" SET play_order=play_order-1" +
" WHERE playlist_id=? AND play_order>?",
playlistvalues);
- updatePlaylistDateModifiedToNow(db, playlistId);
+ updatePlaylistDateModifiedToNow(helper, playlistId);
}
db.delete("audio_playlists_map", "audio_id=?", idvalue);
} finally {
@@ -3608,8 +3596,8 @@
case VIDEO_THUMBNAILS_ID:
case VIDEO_THUMBNAILS:
// Delete the referenced files first.
- Cursor c = qb.query(db, sDataOnlyColumn, userWhere, userWhereArgs, null, null,
- null, null);
+ Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null,
+ null, null, null, null);
if (c != null) {
try {
while (c.moveToNext()) {
@@ -3619,18 +3607,18 @@
FileUtils.closeQuietly(c);
}
}
- count = deleteRecursive(qb, db, userWhere, userWhereArgs);
+ count = deleteRecursive(qb, helper, userWhere, userWhereArgs);
break;
case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
long playlistId = Long.parseLong(uri.getPathSegments().get(3));
- count = deleteRecursive(qb, db, userWhere, userWhereArgs);
+ count = deleteRecursive(qb, helper, userWhere, userWhereArgs);
if (count > 0) {
- updatePlaylistDateModifiedToNow(db, playlistId);
+ updatePlaylistDateModifiedToNow(helper, playlistId);
}
break;
default:
- count = deleteRecursive(qb, db, userWhere, userWhereArgs);
+ count = deleteRecursive(qb, helper, userWhere, userWhereArgs);
break;
}
@@ -3662,23 +3650,23 @@
* can be used to recursively delete all matching entries, since it only
* deletes parents when no references remaining.
*/
- private int deleteRecursive(SQLiteQueryBuilder qb, SQLiteDatabase db, String userWhere,
+ private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
String[] userWhereArgs) {
synchronized (mDirectoryCache) {
mDirectoryCache.clear();
- db.beginTransaction();
+ helper.beginTransaction();
try {
int n = 0;
int total = 0;
do {
- n = qb.delete(db, userWhere, userWhereArgs);
+ n = qb.delete(helper, userWhere, userWhereArgs);
total += n;
} while (n > 0);
- db.setTransactionSuccessful();
+ helper.setTransactionSuccessful();
return total;
} finally {
- db.endTransaction();
+ helper.endTransaction();
}
}
}
@@ -3750,6 +3738,21 @@
res.putString(Intent.EXTRA_TEXT, version);
return res;
}
+ case MediaStore.GET_GENERATION_CALL: {
+ final String volumeName = extras.getString(Intent.EXTRA_TEXT);
+
+ final long generation;
+ try {
+ generation = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName))
+ .getGeneration();
+ } catch (VolumeNotFoundException e) {
+ throw e.rethrowAsIllegalArgumentException();
+ }
+
+ final Bundle res = new Bundle();
+ res.putLong(Intent.EXTRA_INDEX, generation);
+ return res;
+ }
case MediaStore.GET_DOCUMENT_URI_CALL: {
final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
enforceCallingPermission(mediaUri, extras, false);
@@ -4134,8 +4137,6 @@
final int match = matchUri(uri, allowHidden);
final DatabaseHelper helper = getDatabaseForUri(uri);
- final SQLiteDatabase db = helper.getWritableDatabase();
-
final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null);
// Give callers interacting with a specific media item a chance to
@@ -4354,8 +4355,8 @@
if (triggerInvalidate || triggerScan) {
Trace.beginSection("snapshot");
final LocalCallingIdentity token = clearLocalCallingIdentity();
- try (Cursor c = qb.query(db, new String[] { FileColumns._ID },
- userWhere, userWhereArgs, null, null, null)) {
+ try (Cursor c = qb.query(helper, new String[] { FileColumns._ID },
+ userWhere, userWhereArgs, null, null, null, null, null)) {
while (c.moveToNext()) {
updatedIds.add(c.getLong(0));
}
@@ -4386,16 +4387,16 @@
case AUDIO_MEDIA_ID_PLAYLISTS_ID:
case AUDIO_PLAYLISTS_ID:
long playlistId = ContentUris.parseId(uri);
- count = qb.update(db, values, userWhere, userWhereArgs);
+ count = qb.update(helper, values, userWhere, userWhereArgs);
if (count > 0) {
- updatePlaylistDateModifiedToNow(db, playlistId);
+ updatePlaylistDateModifiedToNow(helper, playlistId);
}
break;
case AUDIO_PLAYLISTS_ID_MEMBERS:
long playlistIdMembers = Long.parseLong(uri.getPathSegments().get(3));
- count = qb.update(db, values, userWhere, userWhereArgs);
+ count = qb.update(helper, values, userWhere, userWhereArgs);
if (count > 0) {
- updatePlaylistDateModifiedToNow(db, playlistIdMembers);
+ updatePlaylistDateModifiedToNow(helper, playlistIdMembers);
}
break;
case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
@@ -4407,9 +4408,10 @@
List <String> segments = uri.getPathSegments();
long playlist = Long.parseLong(segments.get(3));
int oldpos = Integer.parseInt(segments.get(5));
- int rowsChanged = movePlaylistEntry(volumeName, helper, db, playlist, oldpos, newpos);
+ int rowsChanged = movePlaylistEntry(volumeName, helper,
+ playlist, oldpos, newpos);
if (rowsChanged > 0) {
- updatePlaylistDateModifiedToNow(db, playlist);
+ updatePlaylistDateModifiedToNow(helper, playlist);
}
return rowsChanged;
@@ -4419,7 +4421,7 @@
}
// fall through
default:
- count = qb.update(db, values, userWhere, userWhereArgs);
+ count = qb.update(helper, values, userWhere, userWhereArgs);
break;
}
@@ -4457,11 +4459,12 @@
return count;
}
- private int movePlaylistEntry(String volumeName, DatabaseHelper helper, SQLiteDatabase db,
+ private int movePlaylistEntry(String volumeName, DatabaseHelper helper,
long playlist, int from, int to) {
if (from == to) {
return 0;
}
+ final SQLiteDatabase db = helper.getWritableDatabase();
db.beginTransaction();
int numlines = 0;
Cursor c = null;
@@ -4515,19 +4518,18 @@
return numlines;
}
- private void updatePlaylistDateModifiedToNow(SQLiteDatabase database, long playlistId) {
+ private void updatePlaylistDateModifiedToNow(DatabaseHelper helper, long playlistId) {
ContentValues values = new ContentValues();
values.put(
FileColumns.DATE_MODIFIED,
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
);
- database.update(
- MediaStore.Files.TABLE,
- values,
- MediaStore.Files.FileColumns._ID + "=?",
- new String[]{String.valueOf(playlistId)}
- );
+ final Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Playlists.getContentUri(helper.mVolumeName), playlistId);
+ final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, AUDIO_PLAYLISTS_ID,
+ uri, Bundle.EMPTY, null);
+ qb.update(helper, values, null, null);
}
@Override
@@ -4971,10 +4973,15 @@
private boolean shouldBypassFuseRestrictions(boolean forWrite) {
boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
: isCallingPackageLegacyRead();
+ if (isRequestingLegacyStorage) {
+ return true;
+ }
- // TODO(b/137755945): We should let file managers bypass FUSE restrictions as well.
- // Remember to change the documentation above when this is addressed.
- return isRequestingLegacyStorage;
+ if (mCallingIdentity.get().hasPermission(PERMISSION_MANAGE_EXTERNAL_STORAGE)) {
+ return true;
+ }
+
+ return false;
}
/**
@@ -5033,6 +5040,16 @@
}
}
+ @Nullable
+ private String getAbsoluteSanitizedPath(String path) {
+ final String[] pathSegments = sanitizePath(path);
+ if (pathSegments.length == 0) {
+ return null;
+ }
+ return path = "/" + String.join("/",
+ Arrays.copyOfRange(pathSegments, 1, pathSegments.length));
+ }
+
/**
* Calculates the ranges that need to be redacted for the given file and user that wants to
* access the file.
@@ -5066,6 +5083,12 @@
return res;
}
+ // TODO(b/147741933): Quick fix. Add tests
+ path = getAbsoluteSanitizedPath(path);
+ if (path == null) {
+ throw new IOException("Invalid path " + path);
+ }
+
final Uri contentUri = Files.getContentUri(MediaStore.getVolumeName(new File(path)));
final String[] projection = new String[]{
MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID };
@@ -5197,6 +5220,13 @@
return -OsConstants.EACCES;
}
+ // TODO(b/147741933): Quick fix. Add tests
+ path = getAbsoluteSanitizedPath(path);
+ if (path == null) {
+ Log.e(TAG, "Invalid path " + path);
+ return -OsConstants.EPERM;
+ }
+
final Uri contentUri = Files.getContentUri(MediaStore.getVolumeName(new File(path)));
final String[] projection = new String[]{
MediaColumns._ID,
@@ -5677,10 +5707,8 @@
}
final DatabaseHelper helper;
- final SQLiteDatabase db;
try {
helper = getDatabaseForUri(uri);
- db = helper.getReadableDatabase();
} catch (VolumeNotFoundException e) {
throw e.rethrowAsIllegalArgumentException();
}
@@ -5691,7 +5719,8 @@
// First, check to see if caller has direct write access
if (forWrite) {
final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null);
- try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
+ try (Cursor c = qb.query(helper, new String[0],
+ null, null, null, null, null, null, null)) {
if (c.moveToFirst()) {
// Direct write access granted, yay!
return;
@@ -5713,7 +5742,8 @@
// Second, check to see if caller has direct read access
final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null);
- try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) {
+ try (Cursor c = qb.query(helper, new String[0],
+ null, null, null, null, null, null, null)) {
if (c.moveToFirst()) {
if (!forWrite) {
// Direct read access granted, yay!
@@ -5934,12 +5964,13 @@
}
final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build();
- getContext().getContentResolver().notifyChange(uri, null);
+ final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
+ ? mInternalDatabase : mExternalDatabase;
+ acceptWithExpansion(helper::notifyChange, uri);
if (LOGV) Log.v(TAG, "Attached volume: " + volume);
if (!MediaStore.VOLUME_INTERNAL.equals(volume)) {
BackgroundThread.getExecutor().execute(() -> {
- final DatabaseHelper helper = mExternalDatabase;
- ensureDefaultFolders(volume, helper, helper.getWritableDatabase());
+ ensureDefaultFolders(volume, helper);
});
}
return uri;
@@ -5971,7 +6002,9 @@
}
final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build();
- getContext().getContentResolver().notifyChange(uri, null);
+ final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
+ ? mInternalDatabase : mExternalDatabase;
+ acceptWithExpansion(helper::notifyChange, uri);
if (LOGV) Log.v(TAG, "Detached volume: " + volume);
}
diff --git a/src/com/android/providers/media/PrioritizedFutureTask.java b/src/com/android/providers/media/PrioritizedFutureTask.java
deleted file mode 100644
index 7c7fd33..0000000
--- a/src/com/android/providers/media/PrioritizedFutureTask.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright (C) 2018 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;
-
-import android.os.SystemClock;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.FutureTask;
-
-public class PrioritizedFutureTask<T> extends FutureTask<T>
- implements Comparable<PrioritizedFutureTask<T>> {
- static final int PRIORITY_LOW = 20;
- static final int PRIORITY_NORMAL = 10;
- static final int PRIORITY_HIGH = 5;
- static final int PRIORITY_CRITICAL = 0;
-
- final long requestTime;
- final int priority;
-
- public PrioritizedFutureTask(Callable<T> callable, int priority) {
- super(callable);
- this.requestTime = SystemClock.elapsedRealtime();
- this.priority = priority;
- }
-
- @Override
- public final int compareTo(PrioritizedFutureTask<T> other) {
- if (this.priority != other.priority) {
- return this.priority < other.priority ? -1 : 1;
- }
- if (this.requestTime != other.requestTime) {
- return this.requestTime < other.requestTime ? -1 : 1;
- }
- return 0;
- }
-}
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 58c453a..aef7376 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -86,6 +86,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.providers.media.util.DatabaseUtils;
import com.android.providers.media.util.ExifUtils;
import com.android.providers.media.util.FileUtils;
import com.android.providers.media.util.IsoInterface;
@@ -263,7 +264,7 @@
private final Uri mFilesUri;
private final CancellationSignal mSignal;
- private final long mStartCurrentTime;
+ private final long mStartGeneration;
private final boolean mSingleFile;
private final Set<Path> mAcquiredDirectoryLocks = new ArraySet<>();
private final ArrayList<ContentProviderOperation> mPending = new ArrayList<>();
@@ -291,7 +292,7 @@
mFilesUri = MediaStore.Files.getContentUri(mVolumeName);
mSignal = getOrCreateSignal(mVolumeName);
- mStartCurrentTime = System.currentTimeMillis();
+ mStartGeneration = MediaStore.getGeneration(mResolver, mVolumeName);
mSingleFile = mRoot.isFile();
Trace.endSection();
@@ -362,12 +363,12 @@
+ MtpConstants.FORMAT_UNDEFINED + ") != "
+ MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST;
final String dataClause = FileColumns.DATA + " LIKE ? ESCAPE '\\'";
- final String addedClause = "ifnull(" + FileColumns.DATE_ADDED + ",0) < "
- + mStartCurrentTime;
+ final String generationClause = FileColumns.GENERATION_ADDED + " <= "
+ + mStartGeneration;
final Bundle queryArgs = new Bundle();
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
- formatClause + " AND " + dataClause + " AND " + addedClause);
+ formatClause + " AND " + dataClause + " AND " + generationClause);
queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
new String[] { escapeForLike(mRoot.getAbsolutePath(), mSingleFile) });
queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
@@ -1263,19 +1264,12 @@
* Escape the given argument for use in a {@code LIKE} statement.
*/
static String escapeForLike(String arg, boolean singleFile) {
- final StringBuilder sb = new StringBuilder();
- for (int i = 0; i < arg.length(); i++) {
- final char c = arg.charAt(i);
- switch (c) {
- case '%': sb.append('\\');
- case '_': sb.append('\\');
- }
- sb.append(c);
+ final String escaped = DatabaseUtils.escapeForLike(arg);
+ if (singleFile) {
+ return escaped;
+ } else {
+ return escaped + "/%";
}
- if (!singleFile) {
- sb.append("/%");
- }
- return sb.toString();
}
static void logTroubleScanning(File file, Exception e) {
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index b5ad720..6fd35c7 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -445,43 +445,55 @@
return res;
}
- public static int executeSql(@NonNull SQLiteDatabase db, @NonNull String sql,
+ public static long executeInsert(@NonNull SQLiteDatabase db, @NonNull String sql,
@Nullable Object[] bindArgs) throws SQLException {
try (SQLiteStatement st = db.compileStatement(sql)) {
- if (bindArgs != null) {
- for (int i = 0; i < bindArgs.length; i++) {
- final Object bindArg = bindArgs[i];
- switch (DatabaseUtils.getTypeOfObject(bindArg)) {
- case Cursor.FIELD_TYPE_NULL:
- st.bindNull(i + 1);
- break;
- case Cursor.FIELD_TYPE_INTEGER:
- st.bindLong(i + 1, ((Number) bindArg).longValue());
- break;
- case Cursor.FIELD_TYPE_FLOAT:
- st.bindDouble(i + 1, ((Number) bindArg).doubleValue());
- break;
- case Cursor.FIELD_TYPE_BLOB:
- st.bindBlob(i + 1, (byte[]) bindArg);
- break;
- case Cursor.FIELD_TYPE_STRING:
- default:
- if (bindArg instanceof Boolean) {
- // Provide compatibility with legacy
- // applications which may pass Boolean values in
- // bind args.
- st.bindLong(i + 1, ((Boolean) bindArg).booleanValue() ? 1 : 0);
- } else {
- st.bindString(i + 1, bindArg.toString());
- }
- break;
- }
- }
- }
+ bindArgs(st, bindArgs);
+ return st.executeInsert();
+ }
+ }
+
+ public static int executeUpdateDelete(@NonNull SQLiteDatabase db, @NonNull String sql,
+ @Nullable Object[] bindArgs) throws SQLException {
+ try (SQLiteStatement st = db.compileStatement(sql)) {
+ bindArgs(st, bindArgs);
return st.executeUpdateDelete();
}
}
+ private static void bindArgs(@NonNull SQLiteStatement st, @Nullable Object[] bindArgs) {
+ if (bindArgs == null) return;
+
+ for (int i = 0; i < bindArgs.length; i++) {
+ final Object bindArg = bindArgs[i];
+ switch (getTypeOfObject(bindArg)) {
+ case Cursor.FIELD_TYPE_NULL:
+ st.bindNull(i + 1);
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ st.bindLong(i + 1, ((Number) bindArg).longValue());
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ st.bindDouble(i + 1, ((Number) bindArg).doubleValue());
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ st.bindBlob(i + 1, (byte[]) bindArg);
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ default:
+ if (bindArg instanceof Boolean) {
+ // Provide compatibility with legacy
+ // applications which may pass Boolean values in
+ // bind args.
+ st.bindLong(i + 1, ((Boolean) bindArg).booleanValue() ? 1 : 0);
+ } else {
+ st.bindString(i + 1, bindArg.toString());
+ }
+ break;
+ }
+ }
+ }
+
public static @NonNull String bindList(@NonNull Object... args) {
final StringBuilder sb = new StringBuilder();
sb.append('(');
@@ -494,4 +506,20 @@
sb.append(')');
return DatabaseUtils.bindSelection(sb.toString(), args);
}
+
+ /**
+ * Escape the given argument for use in a {@code LIKE} statement.
+ */
+ public static String escapeForLike(@NonNull String arg) {
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < arg.length(); i++) {
+ final char c = arg.charAt(i);
+ switch (c) {
+ case '%': sb.append('\\');
+ case '_': sb.append('\\');
+ }
+ sb.append(c);
+ }
+ return sb.toString();
+ }
}
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 732b0ce..0786c88 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -22,6 +22,7 @@
import static android.Manifest.permission.WRITE_MEDIA_STORAGE;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
+import static android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
import static android.app.AppOpsManager.OPSTR_READ_MEDIA_IMAGES;
@@ -32,6 +33,7 @@
import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.Context;
@@ -68,6 +70,11 @@
return context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED;
}
+ public static boolean checkPermissionManageExternalStorage(Context context, int pid, int uid,
+ String packageName) {
+ return hasAppOpPermission(context, pid, uid, packageName, OPSTR_MANAGE_EXTERNAL_STORAGE);
+ }
+
public static boolean checkPermissionWriteStorage(Context context,
int pid, int uid, String packageName) {
return checkPermissionAndAppOp(context, pid,
@@ -160,6 +167,23 @@
}
}
+ /**
+ * Checks if calling app is allowed the app-op. If its app-op mode is
+ * {@link AppOpsManager#MODE_DEFAULT} then it falls back checking the appropriate permission for
+ * the app-op. The permissions is retrieved from {@link AppOpsManager#opToPermission(String)}.
+ */
+ private static boolean hasAppOpPermission(@NonNull Context context, int pid, int uid,
+ @NonNull String packageName, @NonNull String op) {
+ final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+ final int mode = appOps.noteOpNoThrow(op, uid, packageName, null, null);
+ if (mode == AppOpsManager.MODE_DEFAULT) {
+ final String permission = AppOpsManager.opToPermission(op);
+ return permission != null
+ && context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
+ }
+ return mode == AppOpsManager.MODE_ALLOWED;
+ }
+
private static boolean noteAppOpAllowingLegacy(Context context,
int pid, int uid, String packageName, String op) {
final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
diff --git a/src/com/android/providers/media/util/RedactingFileDescriptor.java b/src/com/android/providers/media/util/RedactingFileDescriptor.java
index 09aef01..459c629 100644
--- a/src/com/android/providers/media/util/RedactingFileDescriptor.java
+++ b/src/com/android/providers/media/util/RedactingFileDescriptor.java
@@ -73,85 +73,6 @@
}
/** {@hide} */
- public static int translateModeStringToPosix(String mode) {
- // Sanity check for invalid chars
- for (int i = 0; i < mode.length(); i++) {
- switch (mode.charAt(i)) {
- case 'r':
- case 'w':
- case 't':
- case 'a':
- break;
- default:
- throw new IllegalArgumentException("Bad mode: " + mode);
- }
- }
-
- int res = 0;
- if (mode.startsWith("rw")) {
- res = O_RDWR | O_CREAT;
- } else if (mode.startsWith("w")) {
- res = O_WRONLY | O_CREAT;
- } else if (mode.startsWith("r")) {
- res = O_RDONLY;
- } else {
- throw new IllegalArgumentException("Bad mode: " + mode);
- }
- if (mode.indexOf('t') != -1) {
- res |= O_TRUNC;
- }
- if (mode.indexOf('a') != -1) {
- res |= O_APPEND;
- }
- return res;
- }
-
- /** {@hide} */
- public static String translateModePosixToString(int mode) {
- String res = "";
- if ((mode & O_ACCMODE) == O_RDWR) {
- res = "rw";
- } else if ((mode & O_ACCMODE) == O_WRONLY) {
- res = "w";
- } else if ((mode & O_ACCMODE) == O_RDONLY) {
- res = "r";
- } else {
- throw new IllegalArgumentException("Bad mode: " + mode);
- }
- if ((mode & O_TRUNC) == O_TRUNC) {
- res += "t";
- }
- if ((mode & O_APPEND) == O_APPEND) {
- res += "a";
- }
- return res;
- }
-
- /** {@hide} */
- public static int translateModePosixToPfd(int mode) {
- int res = 0;
- if ((mode & O_ACCMODE) == O_RDWR) {
- res = MODE_READ_WRITE;
- } else if ((mode & O_ACCMODE) == O_WRONLY) {
- res = MODE_WRITE_ONLY;
- } else if ((mode & O_ACCMODE) == O_RDONLY) {
- res = MODE_READ_ONLY;
- } else {
- throw new IllegalArgumentException("Bad mode: " + mode);
- }
- if ((mode & O_CREAT) == O_CREAT) {
- res |= MODE_CREATE;
- }
- if ((mode & O_TRUNC) == O_TRUNC) {
- res |= MODE_TRUNCATE;
- }
- if ((mode & O_APPEND) == O_APPEND) {
- res |= MODE_APPEND;
- }
- return res;
- }
-
- /** {@hide} */
public static int translateModePfdToPosix(int mode) {
int res = 0;
if ((mode & MODE_READ_WRITE) == MODE_READ_WRITE) {
@@ -175,23 +96,6 @@
return res;
}
- /** {@hide} */
- public static int translateModeAccessToPosix(int mode) {
- if (mode == F_OK) {
- // There's not an exact mapping, so we attempt a read-only open to
- // determine if a file exists
- return O_RDONLY;
- } else if ((mode & (R_OK | W_OK)) == (R_OK | W_OK)) {
- return O_RDWR;
- } else if ((mode & R_OK) == R_OK) {
- return O_RDONLY;
- } else if ((mode & W_OK) == W_OK) {
- return O_WRONLY;
- } else {
- throw new IllegalArgumentException("Bad mode: " + mode);
- }
- }
-
private RedactingFileDescriptor(
Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
throws IOException {
diff --git a/src/com/android/providers/media/util/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index d972c40..fac5112 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -25,10 +25,13 @@
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.provider.BaseColumns;
+import android.provider.MediaStore.MediaColumns;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
+import com.android.providers.media.DatabaseHelper;
+
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
@@ -387,80 +390,11 @@
s.append(' ');
}
- /**
- * Perform a query by combining all current settings and the
- * information passed into this method.
- *
- * @param db the database to query on
- * @param projectionIn A list of which columns to return. Passing
- * null will return all columns, which is discouraged to prevent
- * reading data from storage that isn't going to be used.
- * @param selection A filter declaring which rows to return,
- * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
- * itself). Passing null will return all rows for the given URL.
- * @param selectionArgs You may include ?s in selection, which
- * will be replaced by the values from selectionArgs, in order
- * that they appear in the selection. The values will be bound
- * as Strings.
- * @param groupBy A filter declaring how to group rows, formatted
- * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
- * itself). Passing null will cause the rows to not be grouped.
- * @param having A filter declare which row groups to include in
- * the cursor, if row grouping is being used, formatted as an
- * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
- * null will cause all row groups to be included, and is
- * required when row grouping is not being used.
- * @param sortOrder How to order the rows, formatted as an SQL
- * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
- * will use the default sort order, which may be unordered.
- * @return a cursor over the result set
- * @see android.content.ContentResolver#query(android.net.Uri, String[],
- * String, String[], String)
- */
- public Cursor query(SQLiteDatabase db, String[] projectionIn,
+ public Cursor query(DatabaseHelper helper, String[] projectionIn,
String selection, String[] selectionArgs, String groupBy,
- String having, String sortOrder) {
- return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder,
- null /* limit */, null /* cancellationSignal */);
- }
-
- /**
- * Perform a query by combining all current settings and the
- * information passed into this method.
- *
- * @param db the database to query on
- * @param projectionIn A list of which columns to return. Passing
- * null will return all columns, which is discouraged to prevent
- * reading data from storage that isn't going to be used.
- * @param selection A filter declaring which rows to return,
- * formatted as an SQL {@code WHERE} clause (excluding the {@code WHERE}
- * itself). Passing null will return all rows for the given URL.
- * @param selectionArgs You may include ?s in selection, which
- * will be replaced by the values from selectionArgs, in order
- * that they appear in the selection. The values will be bound
- * as Strings.
- * @param groupBy A filter declaring how to group rows, formatted
- * as an SQL {@code GROUP BY} clause (excluding the {@code GROUP BY}
- * itself). Passing null will cause the rows to not be grouped.
- * @param having A filter declare which row groups to include in
- * the cursor, if row grouping is being used, formatted as an
- * SQL {@code HAVING} clause (excluding the {@code HAVING} itself). Passing
- * null will cause all row groups to be included, and is
- * required when row grouping is not being used.
- * @param sortOrder How to order the rows, formatted as an SQL
- * {@code ORDER BY} clause (excluding the {@code ORDER BY} itself). Passing null
- * will use the default sort order, which may be unordered.
- * @param limit Limits the number of rows returned by the query,
- * formatted as {@code LIMIT} clause. Passing null denotes no {@code LIMIT} clause.
- * @return a cursor over the result set
- * @see android.content.ContentResolver#query(android.net.Uri, String[],
- * String, String[], String)
- */
- public Cursor query(SQLiteDatabase db, String[] projectionIn,
- String selection, String[] selectionArgs, String groupBy,
- String having, String sortOrder, String limit) {
- return query(db, projectionIn, selection, selectionArgs,
- groupBy, having, sortOrder, limit, null);
+ String having, String sortOrder, String limit, CancellationSignal cancellationSignal) {
+ return query(helper.getReadableDatabase(), projectionIn, selection, selectionArgs, groupBy,
+ having, sortOrder, limit, cancellationSignal);
}
/**
@@ -555,6 +489,14 @@
cancellationSignal); // will throw if query is invalid
}
+ public long insert(@NonNull DatabaseHelper helper, @NonNull ContentValues values) {
+ // We force wrap in a transaction to ensure that all mutations increment
+ // the generation counter
+ return (int) helper.runWithTransaction(() -> {
+ return insert(helper.getWritableDatabase(), values);
+ });
+ }
+
/**
* Perform an insert by combining all current settings and the
* information passed into this method.
@@ -587,7 +529,16 @@
Log.d(TAG, sql);
}
}
- return com.android.providers.media.util.DatabaseUtils.executeSql(db, sql, sqlArgs);
+ return com.android.providers.media.util.DatabaseUtils.executeInsert(db, sql, sqlArgs);
+ }
+
+ public int update(@NonNull DatabaseHelper helper, @NonNull ContentValues values,
+ @Nullable String selection, @Nullable String[] selectionArgs) {
+ // We force wrap in a transaction to ensure that all mutations increment
+ // the generation counter
+ return (int) helper.runWithTransaction(() -> {
+ return update(helper.getWritableDatabase(), values, selection, selectionArgs);
+ });
}
/**
@@ -664,7 +615,16 @@
Log.d(TAG, sql);
}
}
- return com.android.providers.media.util.DatabaseUtils.executeSql(db, sql, sqlArgs);
+ return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs);
+ }
+
+ public int delete(@NonNull DatabaseHelper helper, @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ // We force wrap in a transaction to ensure that all mutations increment
+ // the generation counter
+ return (int) helper.runWithTransaction(() -> {
+ return delete(helper.getWritableDatabase(), selection, selectionArgs);
+ });
}
/**
@@ -724,7 +684,7 @@
Log.d(TAG, sql);
}
}
- return com.android.providers.media.util.DatabaseUtils.executeSql(db, sql, sqlArgs);
+ return com.android.providers.media.util.DatabaseUtils.executeUpdateDelete(db, sql, sqlArgs);
}
private void enforceStrictColumns(@Nullable String[] projection) {
@@ -837,6 +797,12 @@
sql.append(SQLiteDatabase.findEditTable(mTables));
sql.append(" (");
+ final boolean hasGeneration = Objects.equals(mTables, "files");
+ if (hasGeneration) {
+ values.remove(MediaColumns.GENERATION_ADDED);
+ values.remove(MediaColumns.GENERATION_MODIFIED);
+ }
+
final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
.getValues(values);
for (int i = 0; i < rawValues.size(); i++) {
@@ -845,6 +811,12 @@
}
sql.append(rawValues.keyAt(i));
}
+ if (hasGeneration) {
+ sql.append(',');
+ sql.append(MediaColumns.GENERATION_ADDED);
+ sql.append(',');
+ sql.append(MediaColumns.GENERATION_MODIFIED);
+ }
sql.append(") VALUES (");
for (int i = 0; i < rawValues.size(); i++) {
if (i > 0) {
@@ -852,6 +824,16 @@
}
sql.append('?');
}
+ if (hasGeneration) {
+ sql.append(',');
+ sql.append('(');
+ sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
+ sql.append(')');
+ sql.append(',');
+ sql.append('(');
+ sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
+ sql.append(')');
+ }
sql.append(")");
return sql.toString();
}
@@ -867,6 +849,12 @@
sql.append(SQLiteDatabase.findEditTable(mTables));
sql.append(" SET ");
+ final boolean hasGeneration = Objects.equals(mTables, "files");
+ if (hasGeneration) {
+ values.remove(MediaColumns.GENERATION_ADDED);
+ values.remove(MediaColumns.GENERATION_MODIFIED);
+ }
+
final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
.getValues(values);
for (int i = 0; i < rawValues.size(); i++) {
@@ -876,6 +864,14 @@
sql.append(rawValues.keyAt(i));
sql.append("=?");
}
+ if (hasGeneration) {
+ sql.append(',');
+ sql.append(MediaColumns.GENERATION_MODIFIED);
+ sql.append('=');
+ sql.append('(');
+ sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
+ sql.append(')');
+ }
final String where = computeWhere(selection);
appendClause(sql, " WHERE ", where);
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 32e5fe3..04e2b5c 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -210,6 +210,7 @@
executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui);
// And force a scan to confirm upgraded data survives
+ MediaStore.waitForIdle(context.getContentResolver());
MediaStore.scanVolume(context.getContentResolver(),
MediaStore.getVolumeName(collectionUri));
diff --git a/tests/jni/Android.bp b/tests/jni/Android.bp
deleted file mode 100644
index 54a6642..0000000
--- a/tests/jni/Android.bp
+++ /dev/null
@@ -1,10 +0,0 @@
-cc_test {
- name: "RedactionInfoTest",
- test_suites: ["device-tests"],
- srcs: ["RedactionInfoTest.cpp"],
- cflags: ["-Wall", "-Werror"],
- shared_libs: [
- "libfuse_jni",
- ],
- test_config: "RedactionInfoTest.xml",
-}
\ No newline at end of file
diff --git a/tests/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 70c4a78..2f0c78f 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -31,7 +31,7 @@
manifest: "AndroidManifest.xml",
srcs: ["src/**/*.java"],
static_libs: ["androidx.test.rules", "truth-prebuilt", "tests-fusedaemon-lib"],
- test_suites: ["general-tests"],
+ test_suites: ["general-tests", "mts"],
sdk_version: "test_current",
java_resources: [
":TestAppA",
@@ -44,7 +44,7 @@
manifest: "legacy/AndroidManifest.xml",
srcs: ["legacy/src/**/*.java"],
static_libs: ["androidx.test.rules", "truth-prebuilt"],
- test_suites: ["general-tests"],
+ test_suites: ["general-tests", "mts"],
sdk_version: "29"
}
@@ -53,6 +53,6 @@
srcs: ["host/src/**/*.java"],
libs: ["tradefed"],
static_libs: ["testng"],
- test_suites: ["general-tests"],
+ test_suites: ["general-tests", "mts"],
test_config: "FuseDaemonHostTest.xml",
}
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index 391ef1c..46203dc 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
@@ -183,4 +183,9 @@
public void testRenameEmptyDirectory() throws Exception {
runDeviceTest("testRenameEmptyDirectory");
}
+
+ @Test
+ public void testManageExternalStorageBypassesMediaProviderRestrictions() throws Exception {
+ runDeviceTest("testManageExternalStorageBypassesMediaProviderRestrictions");
+ }
}
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
index b4202e5..f831dce 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
@@ -96,6 +96,14 @@
}
}
+ public static void adoptShellPermissionIdentity(String... permissions) {
+ sUiAutomation.adoptShellPermissionIdentity(permissions);
+ }
+
+ public static void dropShellPermissionIdentity() {
+ sUiAutomation.dropShellPermissionIdentity();
+ }
+
public static String executeShellCommand(String cmd) throws Exception {
try (FileInputStream output = new FileInputStream (sUiAutomation.executeShellCommand(cmd)
.getFileDescriptor())) {
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index 793aaf7..7cc1b20 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -25,9 +25,11 @@
import static com.android.tests.fused.lib.RedactionTestHelper.assertExifMetadataMismatch;
import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadata;
import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadataFromRawResource;
+import static com.android.tests.fused.lib.TestUtils.adoptShellPermissionIdentity;
import static com.android.tests.fused.lib.TestUtils.assertThrows;
import static com.android.tests.fused.lib.TestUtils.createFileAs;
import static com.android.tests.fused.lib.TestUtils.deleteFileAs;
+import static com.android.tests.fused.lib.TestUtils.dropShellPermissionIdentity;
import static com.android.tests.fused.lib.TestUtils.executeShellCommand;
import static com.android.tests.fused.lib.TestUtils.getFileMimeTypeFromDatabase;
import static com.android.tests.fused.lib.TestUtils.getFileRowIdFromDatabase;
@@ -40,6 +42,7 @@
import static org.junit.Assume.assumeTrue;
+import android.Manifest;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
@@ -1006,6 +1009,16 @@
}
}
+ @Test
+ public void testManageExternalStorageBypassesMediaProviderRestrictions() throws Exception {
+ adoptShellPermissionIdentity(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+ try {
+ assertCanCreateFile(new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME));
+ } finally {
+ dropShellPermissionIdentity();
+ }
+ }
+
private void deleteWithMediaProvider(String relativePath, String displayName) throws Exception {
String selection = MediaColumns.RELATIVE_PATH + " = ? AND "
+ MediaColumns.DISPLAY_NAME + " = ?";
diff --git a/tests/src/com/android/providers/media/DatabaseHelperTest.java b/tests/src/com/android/providers/media/DatabaseHelperTest.java
index eb1e133..a3936e5 100644
--- a/tests/src/com/android/providers/media/DatabaseHelperTest.java
+++ b/tests/src/com/android/providers/media/DatabaseHelperTest.java
@@ -154,6 +154,22 @@
}
@Test
+ public void testTransactions() throws Exception {
+ try (DatabaseHelper helper = new DatabaseHelperR(getContext(), TEST_CLEAN_DB)) {
+ helper.beginTransaction();
+ try {
+ helper.setTransactionSuccessful();
+ } finally {
+ helper.endTransaction();
+ }
+
+ helper.runWithTransaction(() -> {
+ return 0;
+ });
+ }
+ }
+
+ @Test
public void testRtoO() throws Exception {
assertDowngrade(DatabaseHelperR.class, DatabaseHelperO.class);
}
diff --git a/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
new file mode 100644
index 0000000..dc1767f
--- /dev/null
+++ b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaDocumentsProviderTest {
+ @Test
+ public void testSimple() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final Context isolatedContext = new IsolatedContext(context, "modern");
+ final ContentResolver resolver = isolatedContext.getContentResolver();
+
+ assertProbe(resolver, "root");
+
+ for (String root : new String[] {
+ MediaDocumentsProvider.TYPE_AUDIO_ROOT,
+ MediaDocumentsProvider.TYPE_VIDEOS_ROOT,
+ MediaDocumentsProvider.TYPE_IMAGES_ROOT,
+ }) {
+ assertProbe(resolver, "root", root, "search");
+
+ assertProbe(resolver, "document", root);
+ assertProbe(resolver, "document", root, "children");
+ }
+
+ for (String recent : new String[] {
+ MediaDocumentsProvider.TYPE_VIDEOS_ROOT,
+ MediaDocumentsProvider.TYPE_IMAGES_ROOT,
+ }) {
+ assertProbe(resolver, "root", recent, "recent");
+ }
+
+ for (String dir : new String[] {
+ MediaDocumentsProvider.TYPE_VIDEOS_BUCKET,
+ MediaDocumentsProvider.TYPE_IMAGES_BUCKET,
+ }) {
+ assertProbe(resolver, "document", dir, "children");
+ }
+
+ for (String item : new String[] {
+ MediaDocumentsProvider.TYPE_ARTIST,
+ MediaDocumentsProvider.TYPE_ALBUM,
+ MediaDocumentsProvider.TYPE_VIDEOS_BUCKET,
+ MediaDocumentsProvider.TYPE_IMAGES_BUCKET,
+
+ MediaDocumentsProvider.TYPE_AUDIO,
+ MediaDocumentsProvider.TYPE_VIDEO,
+ MediaDocumentsProvider.TYPE_IMAGE,
+ }) {
+ assertProbe(resolver, "document", item);
+ }
+ }
+
+ private static void assertProbe(ContentResolver resolver, String... paths) {
+ final Uri.Builder probe = Uri.parse("content://" + MediaDocumentsProvider.AUTHORITY)
+ .buildUpon();
+ for (String path : paths) {
+ probe.appendPath(path);
+ }
+ try (Cursor c = resolver.query(probe.build(), null, Bundle.EMPTY, null)) {
+ assertNotNull(Arrays.toString(paths), c);
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 842bc22..7473603 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -27,11 +27,13 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
+import android.os.CancellationSignal;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.MediaStore.Images.ImageColumns;
@@ -110,6 +112,23 @@
try (Cursor c = isolatedResolver.query(probe, null, null, null)) {
assertNotNull("probe", c);
}
+ try {
+ isolatedResolver.getType(probe);
+ } catch (IllegalStateException tolerated) {
+ }
+ }
+ }
+
+ @Test
+ public void testLocale() {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final Context isolatedContext = new IsolatedContext(context, "modern");
+ final ContentResolver isolatedResolver = isolatedContext.getContentResolver();
+
+ try (ContentProviderClient cpc = isolatedResolver
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ ((MediaProvider) cpc.getLocalContentProvider())
+ .onLocaleChanged();
}
}
@@ -269,6 +288,14 @@
}
@Test
+ public void testBuildData_Downloads() throws Exception {
+ final Uri uri = MediaStore.Downloads
+ .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ assertEndsWith("/Download/linux.iso",
+ buildFile(uri, null, "linux.iso", "application/x-iso9660-image"));
+ }
+
+ @Test
public void testGreylist() throws Exception {
assertFalse(isGreylistMatch(
"SELECT secret FROM other_table"));
diff --git a/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java b/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java
new file mode 100644
index 0000000..1c24b2e
--- /dev/null
+++ b/tests/src/com/android/providers/media/scan/LegacyMediaScannerTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.scan;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class LegacyMediaScannerTest {
+ @Test
+ public void testSimple() throws Exception {
+ final LegacyMediaScanner scanner = new LegacyMediaScanner(
+ InstrumentationRegistry.getTargetContext());
+ assertNotNull(scanner.getContext());
+
+ try {
+ scanner.scanDirectory(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+ } catch (UnsupportedOperationException expected) {
+ }
+ try {
+ scanner.scanFile(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+ } catch (UnsupportedOperationException expected) {
+ }
+ try {
+ scanner.onDetachVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ } catch (UnsupportedOperationException expected) {
+ }
+ }
+}
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index 4c72ca6..44d5cfb 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -43,6 +43,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.providers.media.MediaDocumentsProvider;
import com.android.providers.media.MediaProvider;
import com.android.providers.media.R;
import com.android.providers.media.util.FileUtils;
@@ -67,6 +68,7 @@
private final File mDir;
private final MockContentResolver mResolver;
private final MediaProvider mProvider;
+ private final MediaDocumentsProvider mDocumentsProvider;
public IsolatedContext(Context base, String tag) {
super(base);
@@ -80,8 +82,14 @@
.resolveContentProvider(MediaStore.AUTHORITY, 0);
mProvider = new MediaProvider();
mProvider.attachInfo(this, info);
-
mResolver.addProvider(MediaStore.AUTHORITY, mProvider);
+
+ final ProviderInfo documentsInfo = base.getPackageManager()
+ .resolveContentProvider(MediaDocumentsProvider.AUTHORITY, 0);
+ mDocumentsProvider = new MediaDocumentsProvider();
+ mDocumentsProvider.attachInfo(this, documentsInfo);
+ mResolver.addProvider(MediaDocumentsProvider.AUTHORITY, mDocumentsProvider);
+
mResolver.addProvider(Settings.AUTHORITY, new MockContentProvider() {
@Override
public Bundle call(String method, String request, Bundle args) {
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index f1ece3d..781f22a 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -35,7 +35,6 @@
import android.graphics.Bitmap;
import android.media.ExifInterface;
import android.net.Uri;
-import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
@@ -44,8 +43,8 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
import com.android.providers.media.R;
+import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
import com.android.providers.media.util.FileUtils;
import org.junit.After;
@@ -87,6 +86,11 @@
}
@Test
+ public void testSimple() throws Exception {
+ assertNotNull(mModern.getContext());
+ }
+
+ @Test
public void testOverrideMimeType() throws Exception {
assertFalse(parseOptionalMimeType("image/png", null).isPresent());
assertFalse(parseOptionalMimeType("image/png", "image").isPresent());
@@ -290,6 +294,36 @@
}
@Test
+ public void testFilter() throws Exception {
+ final File music = new File(mDir, "Music");
+ music.mkdirs();
+ stage(R.raw.test_audio, new File(music, "example.mp3"));
+ mModern.scanDirectory(mDir, REASON_UNKNOWN);
+
+ // Exact matches
+ assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ .buildUpon().appendQueryParameter("filter", "artist").build());
+ assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ .buildUpon().appendQueryParameter("filter", "album").build());
+ assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ .buildUpon().appendQueryParameter("filter", "title").build());
+
+ // Partial matches mid-string
+ assertQueryCount(1, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ .buildUpon().appendQueryParameter("filter", "ArT").build());
+
+ // Filter should only apply to narrow collection type
+ assertQueryCount(0, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI
+ .buildUpon().appendQueryParameter("filter", "title").build());
+
+ // Other unrelated search terms
+ assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ .buildUpon().appendQueryParameter("filter", "example").build());
+ assertQueryCount(0, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
+ .buildUpon().appendQueryParameter("filter", "チ").build());
+ }
+
+ @Test
public void testScan_Common() throws Exception {
final File file = new File(mDir, "red.jpg");
stage(R.raw.test_image, file);
diff --git a/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java b/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java
new file mode 100644
index 0000000..06f5ce8
--- /dev/null
+++ b/tests/src/com/android/providers/media/scan/NullMediaScannerTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.scan;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.provider.MediaStore;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+public class NullMediaScannerTest {
+ @Test
+ public void testSimple() throws Exception {
+ final NullMediaScanner scanner = new NullMediaScanner(
+ InstrumentationRegistry.getTargetContext());
+ assertNotNull(scanner.getContext());
+
+ scanner.scanDirectory(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+ scanner.scanFile(new File("/dev/null"), MediaScanner.REASON_UNKNOWN);
+
+ scanner.onDetachVolume(MediaStore.VOLUME_EXTERNAL_PRIMARY);
+ }
+}
diff --git a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
index ed84dcc..635b042 100644
--- a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
@@ -28,6 +28,7 @@
import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER;
import static android.content.ContentResolver.QUERY_SORT_DIRECTION_ASCENDING;
+import static android.database.DatabaseUtils.bindSelection;
import static com.android.providers.media.util.DatabaseUtils.maybeBalance;
import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveLimit;
@@ -58,6 +59,8 @@
private final Bundle args = new Bundle();
private final ArraySet<String> honored = new ArraySet<>();
+ private static final Object[] ARGS = { "baz", 4, null };
+
@Before
public void setUp() {
args.clear();
@@ -65,6 +68,41 @@
}
@Test
+ public void testBindSelection_none() throws Exception {
+ assertEquals(null,
+ bindSelection(null, ARGS));
+ assertEquals("",
+ bindSelection("", ARGS));
+ assertEquals("foo=bar",
+ bindSelection("foo=bar", ARGS));
+ }
+
+ @Test
+ public void testBindSelection_normal() throws Exception {
+ assertEquals("foo='baz'",
+ bindSelection("foo=?", ARGS));
+ assertEquals("foo='baz' AND bar=4",
+ bindSelection("foo=? AND bar=?", ARGS));
+ assertEquals("foo='baz' AND bar=4 AND meow=NULL",
+ bindSelection("foo=? AND bar=? AND meow=?", ARGS));
+ }
+
+ @Test
+ public void testBindSelection_whitespace() throws Exception {
+ assertEquals("BETWEEN 5 AND 10",
+ bindSelection("BETWEEN? AND ?", 5, 10));
+ assertEquals("IN 'foo'",
+ bindSelection("IN?", "foo"));
+ }
+
+ @Test
+ public void testBindSelection_indexed() throws Exception {
+ assertEquals("foo=10 AND bar=11 AND meow=1",
+ bindSelection("foo=?10 AND bar=? AND meow=?1",
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
+ }
+
+ @Test
public void testResolveQueryArgs_GroupBy() throws Exception {
args.putStringArray(QUERY_ARG_GROUP_COLUMNS, new String[] { "foo", "bar" });
args.putString(QUERY_ARG_SQL_GROUP_BY, "raw");
diff --git a/tests/src/com/android/providers/media/util/LongArrayTest.java b/tests/src/com/android/providers/media/util/LongArrayTest.java
new file mode 100644
index 0000000..f2e82f3
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/LongArrayTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class LongArrayTest {
+
+ @Test
+ public void testLongArray() {
+ LongArray a = new LongArray();
+ a.add(1);
+ a.add(2);
+ a.add(3);
+ verify(new long[]{1, 2, 3}, a);
+
+ LongArray b = LongArray.fromArray(new long[]{4, 5, 6, 7, 8}, 3);
+ a.addAll(b);
+ verify(new long[]{1, 2, 3, 4, 5, 6}, a);
+
+ a.resize(2);
+ verify(new long[]{1, 2}, a);
+
+ a.resize(8);
+ verify(new long[]{1, 2, 0, 0, 0, 0, 0, 0}, a);
+
+ a.set(5, 10);
+ verify(new long[]{1, 2, 0, 0, 0, 10, 0, 0}, a);
+
+ a.add(5, 20);
+ assertEquals(20, a.get(5));
+ assertEquals(5, a.indexOf(20));
+ verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0}, a);
+
+ assertEquals(-1, a.indexOf(99));
+
+ a.resize(15);
+ a.set(14, 30);
+ verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0, 0, 0, 0, 0, 0, 30}, a);
+
+ long[] backingArray = new long[]{1, 2, 3, 4};
+ a = LongArray.wrap(backingArray);
+ a.set(0, 10);
+ assertEquals(10, backingArray[0]);
+ backingArray[1] = 20;
+ backingArray[2] = 30;
+ verify(backingArray, a);
+ assertEquals(2, a.indexOf(30));
+
+ a.resize(2);
+ assertEquals(0, backingArray[2]);
+ assertEquals(0, backingArray[3]);
+
+ a.add(50);
+ verify(new long[]{10, 20, 50}, a);
+ }
+
+ public void verify(long[] expected, LongArray longArrays) {
+ assertEquals(expected.length, longArrays.size());
+ assertArrayEquals(expected, longArrays.toArray());
+ }
+}
diff --git a/tests/src/com/android/providers/media/util/MemoryTest.java b/tests/src/com/android/providers/media/util/MemoryTest.java
new file mode 100644
index 0000000..aa9511b
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/MemoryTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.util;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+public class MemoryTest {
+ private final byte[] buf = new byte[4];
+
+ @Test
+ public void testBigEndian() {
+ final int expected = 42;
+ Memory.pokeInt(buf, 0, expected, ByteOrder.BIG_ENDIAN);
+ final int actual = Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN);
+ assertEquals(expected, actual);
+ }
+
+ @Test
+ public void testLittleEndian() {
+ final int expected = 42;
+ Memory.pokeInt(buf, 0, expected, ByteOrder.LITTLE_ENDIAN);
+ final int actual = Memory.peekInt(buf, 0, ByteOrder.LITTLE_ENDIAN);
+ assertEquals(expected, actual);
+ }
+}
diff --git a/tests/src/com/android/providers/media/util/MetricsTest.java b/tests/src/com/android/providers/media/util/MetricsTest.java
new file mode 100644
index 0000000..6041734
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/MetricsTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.util;
+
+import android.provider.MediaStore;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.scan.MediaScanner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MetricsTest {
+
+ /**
+ * The best we can do for coverage is make sure we don't explode?
+ */
+ @Test
+ public void testSimple() throws Exception {
+ final String volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
+ final String packageName = "com.example";
+
+ Metrics.logScan(volumeName, MediaScanner.REASON_UNKNOWN, 42, 42, 42, 42, 42);
+ Metrics.logDeletion(volumeName, 42, packageName, 42);
+ Metrics.logPermissionGranted(volumeName, 42, packageName, 42);
+ Metrics.logPermissionDenied(volumeName, 42, packageName, 42);
+ Metrics.logSchemaChange(volumeName, 42, 42, 42, 42);
+ Metrics.logIdleMaintenance(volumeName, 42, 42, 42, 42);
+ }
+}
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
new file mode 100644
index 0000000..af7535f
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util;
+
+import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
+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.checkPermissionWriteAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteVideo;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PermissionUtilsTest {
+ /**
+ * The best we can do here is assert that we're granted the permissions that
+ * we expect to be holding.
+ */
+ @Test
+ public void testSelfPermissions() throws Exception {
+ final Context context = InstrumentationRegistry.getContext();
+ final int pid = android.os.Process.myPid();
+ final int uid = android.os.Process.myUid();
+ final String packageName = context.getPackageName();
+
+ assertTrue(checkPermissionSystem(context, pid, uid, packageName));
+ assertFalse(checkPermissionBackup(context, pid, uid));
+
+ assertTrue(checkPermissionReadStorage(context, pid, uid, packageName));
+ assertTrue(checkPermissionWriteStorage(context, pid, uid, packageName));
+
+ assertTrue(checkPermissionReadAudio(context, pid, uid, packageName));
+ assertFalse(checkPermissionWriteAudio(context, pid, uid, packageName));
+ assertTrue(checkPermissionReadVideo(context, pid, uid, packageName));
+ assertFalse(checkPermissionWriteVideo(context, pid, uid, packageName));
+ assertTrue(checkPermissionReadImages(context, pid, uid, packageName));
+ assertFalse(checkPermissionWriteImages(context, pid, uid, packageName));
+ }
+}
diff --git a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
index ea689e8..dce5063 100644
--- a/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
+++ b/tests/src/com/android/providers/media/util/SQLiteQueryBuilderTest.java
@@ -218,7 +218,7 @@
sqliteQueryBuilder.setTables("Employee");
Cursor cursor = sqliteQueryBuilder.query(mDatabase,
new String[] { "name", "sum(salary)" }, null, null,
- "name", "sum(salary)>1000", "name");
+ "name", "sum(salary)>1000", "name", null, null);
assertNotNull(cursor);
assertEquals(3, cursor.getCount());
@@ -239,7 +239,7 @@
cursor = sqliteQueryBuilder.query(mDatabase,
new String[] { "name", "sum(salary)" }, null, null,
"name", "sum(salary)>1000", "name", "2" // limit is 2
- );
+ , null);
assertNotNull(cursor);
assertEquals(2, cursor.getCount());
cursor.moveToFirst();
@@ -473,25 +473,25 @@
final SQLiteQueryBuilder qb = mStrictBuilder;
// Should normally only be able to see one row
- try (Cursor c = qb.query(mDatabase, null, null, null, null, null, null)) {
+ try (Cursor c = qb.query(mDatabase, null, null, null, null, null, null, null, null)) {
assertEquals(1, c.getCount());
}
// Trying sneaky queries should fail; even if they somehow succeed, we
// shouldn't get to see any other data.
- try (Cursor c = qb.query(mDatabase, null, "1=1", null, null, null, null)) {
+ try (Cursor c = qb.query(mDatabase, null, "1=1", null, null, null, null, null, null)) {
assertEquals(1, c.getCount());
} catch (Exception tolerated) {
}
- try (Cursor c = qb.query(mDatabase, null, "1=1 --", null, null, null, null)) {
+ try (Cursor c = qb.query(mDatabase, null, "1=1 --", null, null, null, null, null, null)) {
assertEquals(1, c.getCount());
} catch (Exception tolerated) {
}
- try (Cursor c = qb.query(mDatabase, null, "1=1) OR (1=1", null, null, null, null)) {
+ try (Cursor c = qb.query(mDatabase, null, "1=1) OR (1=1", null, null, null, null, null, null)) {
assertEquals(1, c.getCount());
} catch (Exception tolerated) {
}
- try (Cursor c = qb.query(mDatabase, null, "1=1)) OR ((1=1", null, null, null, null)) {
+ try (Cursor c = qb.query(mDatabase, null, "1=1)) OR ((1=1", null, null, null, null, null, null)) {
assertEquals(1, c.getCount());
} catch (Exception tolerated) {
}