Merge "Always close FUSE file descriptor when we're exiting the thread." into rvc-dev
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 27e61ec..84b2b10 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -57,7 +57,6 @@
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.text.TextUtils;
-import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -609,6 +608,15 @@
public static final String QUERY_ARG_RELATED_URI = "android:query-arg-related-uri";
/**
+ * Flag that can be used to enable movement of media items on disk through
+ * {@link ContentResolver#update} calls. This is typically true for
+ * third-party apps, but false for system components.
+ *
+ * @hide
+ */
+ public static final String QUERY_ARG_ALLOW_MOVEMENT = "android:query-arg-allow-movement";
+
+ /**
* Specify how {@link MediaColumns#IS_PENDING} items should be filtered when
* performing a {@link MediaStore} operation.
* <p>
@@ -719,7 +727,7 @@
/** @hide */
@Deprecated
public static boolean getIncludePending(@NonNull Uri uri) {
- return parseBoolean(uri.getQueryParameter(MediaStore.PARAM_INCLUDE_PENDING));
+ return uri.getBooleanQueryParameter(MediaStore.PARAM_INCLUDE_PENDING, false);
}
/**
@@ -749,7 +757,7 @@
* @see MediaStore#setRequireOriginal(Uri)
*/
public static boolean getRequireOriginal(@NonNull Uri uri) {
- return parseBoolean(uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL));
+ return uri.getBooleanQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL, false);
}
/**
@@ -896,11 +904,8 @@
final ContentValues values = new ContentValues();
if (value) {
values.put(MediaColumns.IS_TRASHED, 1);
- values.put(MediaColumns.DATE_EXPIRES,
- (System.currentTimeMillis() + DateUtils.WEEK_IN_MILLIS) / 1000);
} else {
values.put(MediaColumns.IS_TRASHED, 0);
- values.putNull(MediaColumns.DATE_EXPIRES);
}
return createRequest(resolver, CREATE_TRASH_REQUEST_CALL, uris, values);
}
@@ -1103,9 +1108,17 @@
* The time the media item should be considered expired. Typically only
* meaningful in the context of {@link #IS_PENDING} or
* {@link #IS_TRASHED}.
+ * <p>
+ * The value stored in this column is automatically calculated when
+ * {@link #IS_PENDING} or {@link #IS_TRASHED} is changed. The default
+ * pending expiration is typically 7 days, and the default trashed
+ * expiration is typically 30 days.
+ * <p>
+ * Expired media items are automatically deleted once their expiration
+ * time has passed, typically during during the next device idle period.
*/
@CurrentTimeSecondsLong
- @Column(Cursor.FIELD_TYPE_INTEGER)
+ @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
public static final String DATE_EXPIRES = "date_expires";
/**
@@ -2058,7 +2071,6 @@
values.put(MediaColumns.MIME_TYPE, "image/jpeg");
values.put(MediaColumns.DATE_ADDED, now / 1000);
values.put(MediaColumns.DATE_MODIFIED, now / 1000);
- values.put(MediaColumns.DATE_EXPIRES, (now + DateUtils.DAY_IN_MILLIS) / 1000);
values.put(MediaColumns.IS_PENDING, 1);
final Uri uri = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
@@ -2070,7 +2082,6 @@
// Everything went well above, publish it!
values.clear();
values.put(MediaColumns.IS_PENDING, 0);
- values.putNull(MediaColumns.DATE_EXPIRES);
cr.update(uri, values, null, null);
return uri.toString();
} catch (Exception e) {
@@ -3732,13 +3743,6 @@
return volumeName;
}
- private static boolean parseBoolean(@Nullable String value) {
- if (value == null) return false;
- if ("1".equals(value)) return true;
- if ("true".equalsIgnoreCase(value)) return true;
- return false;
- }
-
/**
* Uri for querying the state of the media scanner.
*/
diff --git a/deploy.sh b/deploy.sh
index 22524c0..e6edcce 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -8,6 +8,7 @@
adb remount
adb sync
adb shell umount /apex/com.android.mediaprovider*
+adb shell setprop apexd.status '""'
adb shell setprop ctl.restart apexd
adb shell rm -rf /system/priv-app/MediaProvider
adb shell rm -rf /system/priv-app/MediaProviderGoogle
diff --git a/jni/Android.bp b/jni/Android.bp
index 0ff937b..265f7fb 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -65,7 +65,14 @@
cc_test {
name: "fuse_node_test",
- test_suites: ["device-tests"],
+ test_suites: ["device-tests", "mts"],
+
+ compile_multilib: "both",
+ multilib: {
+ lib32: { suffix: "32", },
+ lib64: { suffix: "64", },
+ },
+
srcs: [
"node_test.cpp",
"node.cpp",
@@ -91,7 +98,14 @@
cc_test {
name: "RedactionInfoTest",
- test_suites: ["device-tests"],
+ test_suites: ["device-tests", "mts"],
+
+ compile_multilib: "both",
+ multilib: {
+ lib32: { suffix: "32", },
+ lib64: { suffix: "64", },
+ },
+
srcs: [
"RedactionInfoTest.cpp",
"RedactionInfo.cpp",
@@ -111,4 +125,4 @@
sdk_version: "current",
stl: "c++_static",
-}
\ No newline at end of file
+}
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 01837bf..7f95186 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -96,10 +96,18 @@
#define FUSE_UNKNOWN_INO 0xffffffff
+// Stolen from: android_filesystem_config.h
+#define AID_APP_START 10000
+
constexpr size_t MAX_READ_SIZE = 128 * 1024;
// Stolen from: UserHandle#getUserId
constexpr int PER_USER_RANGE = 100000;
+// Regex copied from FileUtils.java in MediaProvider, but without media directory.
+const std::regex PATTERN_OWNED_PATH(
+ "^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb|sandbox)/([^/]+)(/?.*)?",
+ std::regex_constants::icase);
+
/*
* In order to avoid double caching with fuse, call fadvise on the file handles
* in the underlying file system. However, if this is done on every read/write,
@@ -240,12 +248,29 @@
// because fuse_lowlevel_ops documents that the root inode is always one
// (see FUSE_ROOT_ID in fuse_lowlevel.h). There are no particular requirements
// on any of the other inodes in the FS.
- inline node* FromInode(__u64 inode) {
+ // Also, if ctx is not nullptr, it will also check if the path can be accessible
+ // by the caller, and return nullptr if it cannot access.
+ inline node* FromInode(__u64 inode, const struct fuse_ctx* ctx) {
if (inode == FUSE_ROOT_ID) {
return root;
}
- return node::FromInode(inode, &tracker);
+ node* result = node::FromInode(inode, &tracker);
+ if (ctx == nullptr || ctx->uid < AID_APP_START) {
+ return result;
+ }
+
+ string path = result->BuildPath();
+ std::smatch match;
+ if (std::regex_match(path, match, PATTERN_OWNED_PATH)) {
+ std::string const & pkg = match[1];
+ if (!mp->IsUidForPackage(pkg, ctx->uid)) {
+ PLOG(WARNING) << "Invalid file access from " << pkg << ": " << path;
+ return nullptr;
+ }
+ }
+
+ return result;
}
inline __u64 ToInode(node* node) const {
@@ -346,6 +371,13 @@
return reinterpret_cast<struct fuse*>(fuse_req_userdata(req));
}
+static bool is_package_owned_path(const string& path, const string& fuse_path) {
+ if (path.rfind(fuse_path, 0) != 0) {
+ return false;
+ }
+ return std::regex_match(path, PATTERN_OWNED_PATH);
+}
+
static node* make_node_entry(fuse_req_t req, node* parent, const string& name, const string& path,
struct fuse_entry_param* e, int* error_code) {
struct fuse* fuse = get_fuse(req);
@@ -370,8 +402,10 @@
// reuse inode numbers.
e->generation = 0;
e->ino = fuse->ToInode(node);
- e->entry_timeout = std::numeric_limits<double>::max();
- e->attr_timeout = std::numeric_limits<double>::max();
+ e->entry_timeout = is_package_owned_path(path, fuse->path) ?
+ 0 : std::numeric_limits<double>::max();
+ e->attr_timeout = is_package_owned_path(path, fuse->path) ?
+ 0 : std::numeric_limits<double>::max();
return node;
}
@@ -415,7 +449,11 @@
struct fuse_entry_param* e, int* error_code) {
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* parent_node = fuse->FromInode(parent);
+ node* parent_node = fuse->FromInode(parent, ctx);
+ if (!parent_node) {
+ *error_code = ENOENT;
+ return nullptr;
+ }
string parent_path = parent_node->BuildPath();
string child_path = parent_path + "/" + name;
@@ -445,7 +483,8 @@
}
static void do_forget(struct fuse* fuse, fuse_ino_t ino, uint64_t nlookup) {
- node* node = fuse->FromInode(ino);
+ // Always allow to forget so we put ctx as nullptr.
+ node* node = fuse->FromInode(ino, nullptr);
TRACE_NODE(node);
if (node) {
// This is a narrowing conversion from an unsigned 64bit to a 32bit value. For
@@ -482,18 +521,21 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* node = fuse->FromInode(ino);
+ node* node = fuse->FromInode(ino, ctx);
+ if (!node) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
string path = node->BuildPath();
TRACE_NODE(node);
- if (!node) fuse_reply_err(req, ENOENT);
-
struct stat s;
memset(&s, 0, sizeof(s));
if (lstat(path.c_str(), &s) < 0) {
fuse_reply_err(req, errno);
} else {
- fuse_reply_attr(req, &s, std::numeric_limits<double>::max());
+ fuse_reply_attr(req, &s, is_package_owned_path(path, fuse->path) ?
+ 0 : std::numeric_limits<double>::max());
}
}
@@ -505,16 +547,15 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* node = fuse->FromInode(ino);
- string path = node->BuildPath();
- struct timespec times[2];
-
- TRACE_NODE(node);
-
+ node* node = fuse->FromInode(ino, ctx);
if (!node) {
fuse_reply_err(req, ENOENT);
return;
}
+ string path = node->BuildPath();
+ struct timespec times[2];
+
+ TRACE_NODE(node);
/* XXX: incomplete implementation on purpose.
* chmod/chown should NEVER be implemented.*/
@@ -559,12 +600,13 @@
}
lstat(path.c_str(), attr);
- fuse_reply_attr(req, attr, std::numeric_limits<double>::max());
+ fuse_reply_attr(req, attr, is_package_owned_path(path, fuse->path) ?
+ 0 : std::numeric_limits<double>::max());
}
static void pf_canonical_path(fuse_req_t req, fuse_ino_t ino)
{
- node* node = get_fuse(req)->FromInode(ino);
+ node* node = get_fuse(req)->FromInode(ino, fuse_req_ctx(req));
if (node) {
// TODO(b/147482155): Check that uid has access to |path| and its contents
@@ -582,15 +624,15 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* parent_node = fuse->FromInode(parent);
- string parent_path = parent_node->BuildPath();
-
- TRACE_NODE(parent_node);
-
+ node* parent_node = fuse->FromInode(parent, ctx);
if (!parent_node) {
fuse_reply_err(req, ENOENT);
return;
}
+ string parent_path = parent_node->BuildPath();
+
+ TRACE_NODE(parent_node);
+
const string child_path = parent_path + "/" + name;
mode = (mode & (~0777)) | 0664;
@@ -616,7 +658,11 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* parent_node = fuse->FromInode(parent);
+ node* parent_node = fuse->FromInode(parent, ctx);
+ if (!parent_node) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
const string parent_path = parent_node->BuildPath();
TRACE_NODE(parent_node);
@@ -649,7 +695,11 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* parent_node = fuse->FromInode(parent);
+ node* parent_node = fuse->FromInode(parent, ctx);
+ if (!parent_node) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
const string parent_path = parent_node->BuildPath();
TRACE_NODE(parent_node);
@@ -675,7 +725,11 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* parent_node = fuse->FromInode(parent);
+ node* parent_node = fuse->FromInode(parent, ctx);
+ if (!parent_node) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
const string parent_path = parent_node->BuildPath();
TRACE_NODE(parent_node);
@@ -718,9 +772,11 @@
return EINVAL;
}
- node* old_parent_node = fuse->FromInode(parent);
+ node* old_parent_node = fuse->FromInode(parent, ctx);
+ if (!old_parent_node) return ENOENT;
const string old_parent_path = old_parent_node->BuildPath();
- node* new_parent_node = fuse->FromInode(new_parent);
+ node* new_parent_node = fuse->FromInode(new_parent, ctx);
+ if (!new_parent_node) return ENOENT;
const string new_parent_path = new_parent_node->BuildPath();
if (!old_parent_node || !new_parent_node) {
@@ -793,15 +849,14 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* node = fuse->FromInode(ino);
- const string path = node->BuildPath();
-
- TRACE_NODE(node) << (is_requesting_write(fi->flags) ? "write" : "read");
-
+ node* node = fuse->FromInode(ino, ctx);
if (!node) {
fuse_reply_err(req, ENOENT);
return;
}
+ const string path = node->BuildPath();
+
+ TRACE_NODE(node) << (is_requesting_write(fi->flags) ? "write" : "read");
if (fi->flags & O_DIRECT) {
fi->flags &= ~O_DIRECT;
@@ -1042,7 +1097,7 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
- node* node = fuse->FromInode(ino);
+ node* node = fuse->FromInode(ino, fuse_req_ctx(req));
handle* h = reinterpret_cast<handle*>(fi->fh);
TRACE_NODE(node);
@@ -1088,15 +1143,14 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* node = fuse->FromInode(ino);
- const string path = node->BuildPath();
-
- TRACE_NODE(node);
-
+ node* node = fuse->FromInode(ino, ctx);
if (!node) {
fuse_reply_err(req, ENOENT);
return;
}
+ const string path = node->BuildPath();
+
+ TRACE_NODE(node);
int status = fuse->mp->IsOpendirAllowed(path, ctx->uid);
if (status) {
@@ -1135,7 +1189,11 @@
struct fuse_entry_param e;
size_t entry_size = 0;
- node* node = fuse->FromInode(ino);
+ node* node = fuse->FromInode(ino, fuse_req_ctx(req));
+ if (!node) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
const string path = node->BuildPath();
TRACE_NODE(node);
@@ -1223,7 +1281,7 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
- node* node = fuse->FromInode(ino);
+ node* node = fuse->FromInode(ino, fuse_req_ctx(req));
dirhandle* h = reinterpret_cast<dirhandle*>(fi->fh);
TRACE_NODE(node);
if (node) {
@@ -1271,7 +1329,11 @@
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* node = fuse->FromInode(ino);
+ node* node = fuse->FromInode(ino, ctx);
+ if (!node) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
const string path = node->BuildPath();
TRACE_NODE(node);
@@ -1287,7 +1349,11 @@
ATRACE_CALL();
struct fuse* fuse = get_fuse(req);
const struct fuse_ctx* ctx = fuse_req_ctx(req);
- node* parent_node = fuse->FromInode(parent);
+ node* parent_node = fuse->FromInode(parent, ctx);
+ if (!parent_node) {
+ fuse_reply_err(req, ENOENT);
+ return;
+ }
const string parent_path = parent_node->BuildPath();
TRACE_NODE(parent_node);
diff --git a/jni/MediaProviderWrapper.cpp b/jni/MediaProviderWrapper.cpp
index 999396a..361b738 100644
--- a/jni/MediaProviderWrapper.cpp
+++ b/jni/MediaProviderWrapper.cpp
@@ -146,6 +146,18 @@
return res;
}
+bool isUidForPackageInternal(JNIEnv* env, jobject media_provider_object,
+ jmethodID mid_is_uid_for_package, const string& pkg, uid_t uid) {
+ ScopedLocalRef<jstring> j_pkg(env, env->NewStringUTF(pkg.c_str()));
+ bool res = env->CallBooleanMethod(media_provider_object, mid_is_uid_for_package, j_pkg.get(),
+ uid);
+
+ if (CheckForJniException(env)) {
+ return false;
+ }
+ return res;
+}
+
std::vector<std::shared_ptr<DirectoryEntry>> getFilesInDirectoryInternal(
JNIEnv* env, jobject media_provider_object, jmethodID mid_get_files_in_dir, uid_t uid,
const string& path) {
@@ -252,7 +264,8 @@
/*is_static*/ false);
mid_rename_ = CacheMethod(env, "rename", "(Ljava/lang/String;Ljava/lang/String;I)I",
/*is_static*/ false);
-
+ mid_is_uid_for_package_ = CacheMethod(env, "isUidForPackage", "(Ljava/lang/String;I)Z",
+ /*is_static*/ false);
}
MediaProviderWrapper::~MediaProviderWrapper() {
@@ -368,6 +381,15 @@
return isOpendirAllowedInternal(env, media_provider_object_, mid_is_opendir_allowed_, path, uid);
}
+bool MediaProviderWrapper::IsUidForPackage(const string& pkg, uid_t uid) {
+ if (shouldBypassMediaProvider(uid)) {
+ return true;
+ }
+
+ JNIEnv* env = MaybeAttachCurrentThread();
+ return isUidForPackageInternal(env, media_provider_object_, mid_is_uid_for_package_, pkg, uid);
+}
+
int MediaProviderWrapper::Rename(const string& old_path, const string& new_path, uid_t uid) {
if (shouldBypassMediaProvider(uid)) {
int res = rename(old_path.c_str(), new_path.c_str());
diff --git a/jni/MediaProviderWrapper.h b/jni/MediaProviderWrapper.h
index 016696f..ff160ea 100644
--- a/jni/MediaProviderWrapper.h
+++ b/jni/MediaProviderWrapper.h
@@ -136,6 +136,15 @@
int IsOpendirAllowed(const std::string& path, uid_t uid);
/**
+ * Determines if the given package name matches its uid.
+ *
+ * @param pkg the package name of the app
+ * @param uid UID of the app
+ * @return true if it matches, otherwise return false.
+ */
+ bool IsUidForPackage(const std::string& pkg, uid_t uid);
+
+ /**
* Renames a file or directory to new path.
*
* @param old_path path of the file or directory to be renamed.
@@ -169,6 +178,7 @@
jmethodID mid_is_opendir_allowed_;
jmethodID mid_get_files_in_dir_;
jmethodID mid_rename_;
+ jmethodID mid_is_uid_for_package_;
/**
* Auxiliary for caching MediaProvider methods.
diff --git a/legacy/src/com/android/providers/media/LegacyMediaProvider.java b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
index a505ee1..6e152d4 100644
--- a/legacy/src/com/android/providers/media/LegacyMediaProvider.java
+++ b/legacy/src/com/android/providers/media/LegacyMediaProvider.java
@@ -20,15 +20,19 @@
import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
+import android.content.OperationApplicationException;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
+import android.util.ArraySet;
import androidx.annotation.NonNull;
@@ -38,7 +42,9 @@
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Objects;
+import java.util.Set;
/**
* Very limited subset of {@link MediaProvider} which only surfaces
@@ -106,13 +112,41 @@
}
@Override
- public Uri insert(Uri uri, ContentValues values) {
+ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+ throws OperationApplicationException {
+ // Open transactions on databases for requested volumes
+ final Set<DatabaseHelper> transactions = new ArraySet<>();
try {
- final File file = new File(values.getAsString(MediaColumns.DATA));
- file.getParentFile().mkdirs();
- file.createNewFile();
- } catch (IOException e) {
- throw new IllegalStateException(e);
+ for (ContentProviderOperation op : operations) {
+ final DatabaseHelper helper = getDatabaseForUri(op.getUri());
+ if (!transactions.contains(helper)) {
+ helper.beginTransaction();
+ transactions.add(helper);
+ }
+ }
+
+ final ContentProviderResult[] result = super.applyBatch(operations);
+ for (DatabaseHelper helper : transactions) {
+ helper.setTransactionSuccessful();
+ }
+ return result;
+ } finally {
+ for (DatabaseHelper helper : transactions) {
+ helper.endTransaction();
+ }
+ }
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ if (!uri.getBooleanQueryParameter("silent", false)) {
+ try {
+ final File file = new File(values.getAsString(MediaColumns.DATA));
+ file.getParentFile().mkdirs();
+ file.createNewFile();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
}
final DatabaseHelper helper = getDatabaseForUri(uri);
diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml
index 47be764..2d36456 100644
--- a/res/values-gu/strings.xml
+++ b/res/values-gu/strings.xml
@@ -23,7 +23,7 @@
<string name="unknown" msgid="2059049215682829375">"અજાણ"</string>
<string name="root_images" msgid="5861633549189045666">"છબીઓ"</string>
<string name="root_videos" msgid="8792703517064649453">"વિડિઓઝ"</string>
- <string name="root_audio" msgid="3505830755201326018">"ઑડિઓ"</string>
+ <string name="root_audio" msgid="3505830755201326018">"ઑડિયો"</string>
<string name="root_documents" msgid="3829103301363849237">"દસ્તાવેજો"</string>
<string name="permission_required" msgid="1460820436132943754">"આ આઇટમમાં ફેરફાર કરવા માટે અથવા તેને ડિલીટ કરવા માટે પરવાનગી હોવી જરૂરી છે."</string>
<string name="permission_required_action" msgid="706370952366113539">"આગળ વધો"</string>
diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml
index 09eac9f..80999fa 100644
--- a/res/values-pl/strings.xml
+++ b/res/values-pl/strings.xml
@@ -21,7 +21,7 @@
<string name="app_label" msgid="9035307001052716210">"Przechowywanie multimediów"</string>
<string name="artist_label" msgid="8105600993099120273">"Wykonawca"</string>
<string name="unknown" msgid="2059049215682829375">"Nieznany"</string>
- <string name="root_images" msgid="5861633549189045666">"Grafika"</string>
+ <string name="root_images" msgid="5861633549189045666">"Obrazy"</string>
<string name="root_videos" msgid="8792703517064649453">"Filmy"</string>
<string name="root_audio" msgid="3505830755201326018">"Dźwięk"</string>
<string name="root_documents" msgid="3829103301363849237">"Dokumenty"</string>
diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml
index 51a5d6c..77cc361 100644
--- a/res/values-sv/strings.xml
+++ b/res/values-sv/strings.xml
@@ -22,7 +22,7 @@
<string name="artist_label" msgid="8105600993099120273">"Artist"</string>
<string name="unknown" msgid="2059049215682829375">"Okänd"</string>
<string name="root_images" msgid="5861633549189045666">"Bilder"</string>
- <string name="root_videos" msgid="8792703517064649453">"Videoklipp"</string>
+ <string name="root_videos" msgid="8792703517064649453">"Videor"</string>
<string name="root_audio" msgid="3505830755201326018">"Ljud"</string>
<string name="root_documents" msgid="3829103301363849237">"Dokument"</string>
<string name="permission_required" msgid="1460820436132943754">"Behörighet krävs för att ändra eller radera objektet."</string>
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 55ec897..a61075b 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -69,7 +69,6 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
@@ -112,6 +111,13 @@
long mScanStopTime;
/**
+ * Flag indicating that this database should invoke
+ * {@link #migrateFromLegacy} to migrate from a legacy database, typically
+ * only set when this database is starting from scratch.
+ */
+ boolean mMigrateFromLegacy;
+
+ /**
* Lock used to guard against deadlocks in SQLite; the write lock is used to
* guard any schema changes, and the read lock is used for all other
* database operations.
@@ -135,7 +141,7 @@
public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
long oldId, int oldMediaType, boolean oldIsDownload,
long newId, int newMediaType, boolean newIsDownload,
- String ownerPackage, String oldPath);
+ String oldOwnerPackage, String newOwnerPackage, String oldPath);
public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
int mediaType, boolean isDownload, String ownerPackage, String path);
}
@@ -242,7 +248,7 @@
db.setCustomScalarFunction("_INSERT", (arg) -> {
if (arg != null && mFilesListener != null
&& !mSchemaLock.isWriteLockedByCurrentThread()) {
- final String[] split = arg.split(":");
+ final String[] split = arg.split(":", 4);
final String volumeName = split[0];
final long id = Long.parseLong(split[1]);
final int mediaType = Integer.parseInt(split[2]);
@@ -261,7 +267,7 @@
db.setCustomScalarFunction("_UPDATE", (arg) -> {
if (arg != null && mFilesListener != null
&& !mSchemaLock.isWriteLockedByCurrentThread()) {
- final String[] split = arg.split(":");
+ final String[] split = arg.split(":", 10);
final String volumeName = split[0];
final long oldId = Long.parseLong(split[1]);
final int oldMediaType = Integer.parseInt(split[2]);
@@ -269,15 +275,15 @@
final long newId = Long.parseLong(split[4]);
final int newMediaType = Integer.parseInt(split[5]);
final boolean newIsDownload = Integer.parseInt(split[6]) != 0;
- final String ownerPackage = split[7];
- // Path can include ':', assume rest of split[8..length] is path.
- final String oldPath = String.join(":", Arrays.copyOfRange(split, 8, split.length));
+ final String oldOwnerPackage = split[7];
+ final String newOwnerPackage = split[8];
+ final String oldPath = split[9];
Trace.beginSection("_UPDATE");
try {
mFilesListener.onUpdate(DatabaseHelper.this, volumeName, oldId,
oldMediaType, oldIsDownload, newId, newMediaType, newIsDownload,
- ownerPackage, oldPath);
+ oldOwnerPackage, newOwnerPackage, oldPath);
} finally {
Trace.endSection();
}
@@ -287,14 +293,13 @@
db.setCustomScalarFunction("_DELETE", (arg) -> {
if (arg != null && mFilesListener != null
&& !mSchemaLock.isWriteLockedByCurrentThread()) {
- final String[] split = arg.split(":");
+ final String[] split = arg.split(":", 6);
final String volumeName = split[0];
final long id = Long.parseLong(split[1]);
final int mediaType = Integer.parseInt(split[2]);
final boolean isDownload = Integer.parseInt(split[3]) != 0;
final String ownerPackage = split[4];
- // Path can include ':', assume rest of split[5..length] is path.
- final String path = String.join(":", Arrays.copyOfRange(split, 5, split.length));
+ final String path = split[5];
Trace.beginSection("_DELETE");
try {
@@ -352,6 +357,22 @@
}
}
+ @Override
+ public void onOpen(final SQLiteDatabase db) {
+ Log.v(TAG, "onOpen() for " + mName);
+ if (mMigrateFromLegacy) {
+ // Clear flag, since we should only attempt once
+ mMigrateFromLegacy = false;
+
+ mSchemaLock.writeLock().lock();
+ try {
+ migrateFromLegacy(db);
+ } finally {
+ mSchemaLock.writeLock().unlock();
+ }
+ }
+ }
+
@GuardedBy("mProjectionMapCache")
private final ArrayMap<Class<?>, ArrayMap<String, String>>
mProjectionMapCache = new ArrayMap<>();
@@ -759,7 +780,7 @@
// Since this code is used by both the legacy and modern providers, we
// only want to migrate when we're running as the modern provider
if (!mLegacyProvider) {
- migrateFromLegacy(db);
+ mMigrateFromLegacy = true;
}
}
@@ -788,8 +809,8 @@
extras.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_INCLUDE);
extras.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
- db.execSQL("SAVEPOINT before_migrate");
- Log.d(TAG, "Starting migration from legacy provider for " + mName);
+ db.beginTransaction();
+ Log.d(TAG, "Starting migration from legacy provider");
mMigrationListener.onStarted(client, mVolumeName);
try (Cursor c = client.query(queryUri, sMigrateColumns.toArray(new String[0]),
extras, null)) {
@@ -808,17 +829,19 @@
// When migrating pending or trashed files, we might need to
// rename them on disk to match new schema
- FileUtils.computeDataFromValues(values,
- new File(FileUtils.extractVolumePath(data)));
- final String recomputedData = values.getAsString(MediaColumns.DATA);
- if (!Objects.equals(data, recomputedData)) {
- try {
- Os.rename(data, recomputedData);
- } catch (ErrnoException e) {
- // We only have one shot to migrate data, so log and
- // keep marching forward
- Log.w(TAG, "Failed to rename " + values + "; continuing");
- FileUtils.computeValuesFromData(values);
+ final String volumePath = FileUtils.extractVolumePath(data);
+ if (volumePath != null) {
+ FileUtils.computeDataFromValues(values, new File(volumePath));
+ final String recomputedData = values.getAsString(MediaColumns.DATA);
+ if (!Objects.equals(data, recomputedData)) {
+ try {
+ Os.rename(data, recomputedData);
+ } catch (ErrnoException e) {
+ // We only have one shot to migrate data, so log and
+ // keep marching forward
+ Log.w(TAG, "Failed to rename " + values + "; continuing");
+ FileUtils.computeValuesFromData(values);
+ }
}
}
@@ -827,18 +850,28 @@
// keep marching forward
Log.w(TAG, "Failed to insert " + values + "; continuing");
}
+
+ // To avoid SQLITE_NOMEM errors, we need to periodically
+ // flush the current transaction and start another one
+ if ((c.getPosition() % 1_000) == 0) {
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ db.beginTransaction();
+ }
}
- db.execSQL("RELEASE before_migrate");
- Log.d(TAG, "Finished migration from legacy provider for " + mName);
- mMigrationListener.onFinished(client, mVolumeName);
+ Log.d(TAG, "Finished migration from legacy provider");
} catch (Exception e) {
// We have to guard ourselves against any weird behavior of the
// legacy provider by trying to catch everything
- db.execSQL("ROLLBACK TO before_migrate");
Log.wtf(TAG, "Failed migration from legacy provider", e);
- mMigrationListener.onFinished(client, mVolumeName);
}
+
+ // We tried our best above to migrate everything we could, and we
+ // only have one possible shot, so mark everything successful
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ mMigrationListener.onFinished(client, mVolumeName);
}
}
@@ -989,7 +1022,8 @@
final String updateArg =
"old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
+ "||':'||new._id||':'||new.media_type||':'||new.is_download"
- + "||':'||ifnull(old.owner_package_name,'null')||':'||old._data";
+ + "||':'||ifnull(old.owner_package_name,'null')"
+ + "||':'||ifnull(new.owner_package_name,'null')||':'||old._data";
final String deleteArg =
"old.volume_name||':'||old._id||':'||old.media_type||':'||old.is_download"
+ "||':'||ifnull(old.owner_package_name,'null')||':'||old._data";
@@ -1247,7 +1281,7 @@
static final int VERSION_O = 800;
static final int VERSION_P = 900;
static final int VERSION_Q = 1023;
- static final int VERSION_R = 1113;
+ static final int VERSION_R = 1114;
static final int VERSION_LATEST = VERSION_R;
/**
@@ -1387,6 +1421,9 @@
if (fromVersion < 1113) {
// Empty version bump to ensure triggers are recreated
}
+ if (fromVersion < 1114) {
+ // Empty version bump to ensure triggers are recreated
+ }
if (recomputeDataValues) {
recomputeDataValues(db, internal);
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 3fcf718..4d2f2d5 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -60,6 +60,7 @@
import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
import static com.android.providers.media.util.FileUtils.extractVolumeName;
import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
+import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
import static com.android.providers.media.util.FileUtils.isDownload;
import static com.android.providers.media.util.FileUtils.sanitizePath;
import static com.android.providers.media.util.Logging.LOGV;
@@ -119,6 +120,7 @@
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.OnCloseListener;
+import android.os.Process;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
@@ -564,9 +566,10 @@
public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
long oldId, int oldMediaType, boolean oldIsDownload,
long newId, int newMediaType, boolean newIsDownload,
- String ownerPackage, String oldPath) {
+ String oldOwnerPackage, String newOwnerPackage, String oldPath) {
final boolean isDownload = oldIsDownload || newIsDownload;
- handleUpdatedRowForFuse(oldPath, ownerPackage, oldId, newId);
+ handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId);
+ handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage);
acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload);
if (newMediaType != oldMediaType) {
@@ -1276,6 +1279,13 @@
return new String[] {""};
}
+ // Do not allow apps to list Android/data or Android/obb dirs. Installer and
+ // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs
+ // are mounted to lowerfs directly.
+ if (isDataOrObbPath(path)) {
+ return new String[] {""};
+ }
+
if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
return new String[] {"/"};
}
@@ -1342,6 +1352,55 @@
mimeType.startsWith(supportedPrimaryMimeType));
}
+ /**
+ * Removes owner package for the renamed path if the calling package doesn't own the db row
+ *
+ * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the
+ * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath
+ * from accessing renamed file.
+ * @return {@code true} if
+ * <ul>
+ * <li> there is no corresponding database row for given {@code path}
+ * <li> shared calling package is the owner of the database row
+ * <li> owner package name is already set to 'null'
+ * <li> updating owner package name to 'null' was successful.
+ * </ul>
+ * Returns {@code false} otherwise.
+ */
+ private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper,
+ @NonNull String path) {
+
+ final Uri uri = Files.getContentUriForPath(path);
+ final int match = matchUri(uri, isCallingPackageAllowedHidden());
+ final String ownerPackageName;
+ final String selection = MediaColumns.DATA + " =? AND "
+ + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'";
+ final String[] selectionArgs = new String[] {path};
+
+ final SQLiteQueryBuilder qbForQuery =
+ getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null);
+ try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME},
+ selection, selectionArgs, null, null, null, null, null)) {
+ if (!c.moveToFirst()) {
+ // We don't need to remove owner_package from db row if path doesn't exist in
+ // database or owner_package is already set to 'null'
+ return true;
+ }
+ ownerPackageName = c.getString(0);
+ if (isCallingIdentitySharedPackageName(ownerPackageName)) {
+ // We don't need to remove owner_package from db row if calling package is the owner
+ // of the database row
+ return true;
+ }
+ }
+
+ final SQLiteQueryBuilder qbForUpdate =
+ getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null);
+ ContentValues values = new ContentValues();
+ values.put(FileColumns.OWNER_PACKAGE_NAME, "null");
+ return qbForUpdate.update(helper, values, selection, selectionArgs) == 1;
+ }
+
private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
@NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
@@ -1623,26 +1682,34 @@
if (!isMimeTypeSupportedInPath(newPath, newMimeType)) {
return OsConstants.EPERM;
}
- return renameFileUncheckedForFuse(oldPath, newPath);
+ return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ;
}
private int renameFileUncheckedForFuse(String oldPath, String newPath) {
- final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
+ return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ;
+ }
+
+ private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) {
final DatabaseHelper helper;
try {
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);
+ throw new IllegalStateException("Failed to update database row with " + oldPath, e);
}
helper.beginTransaction();
try {
+ final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
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;
+ if (!bypassRestrictions) {
+ Log.e(TAG, "Calling package doesn't have write permission to rename file.");
+ return OsConstants.EPERM;
+ } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
+ Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
+ return OsConstants.EPERM;
+ }
}
// Try renaming oldPath to newPath in lower file system.
@@ -2378,7 +2445,7 @@
values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
- values.put(FileColumns.IS_DOWNLOAD, isDownload(path));
+ values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0);
File file = new File(path);
if (file.exists()) {
values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
@@ -2679,12 +2746,13 @@
try {
return qb.insert(helper, values);
} catch (SQLiteConstraintException e) {
- final long rowId = getIdIfPathExistsForCallingPackage(qb, helper, path);
+ SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path);
+ final long rowId = getIdIfPathExistsForCallingPackage(qbForUpsert, helper, path);
// Apps sometimes create a file via direct path and then insert it into
// MediaStore via ContentResolver. The former should create a database entry,
// so we have to treat the latter as an upsert.
// TODO(b/149917493) Perform all INSERT operations as UPSERT.
- if (rowId != -1 && qb.update(helper, values, "_id=?",
+ if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?",
new String[]{Long.toString(rowId)}) == 1) {
return rowId;
}
@@ -2715,6 +2783,28 @@
return -1;
}
+ /**
+ * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns
+ * check to allow upsert to update any column with Files uri.
+ */
+ private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) {
+ final Uri uri = Files.getContentUriForPath(path);
+ final boolean allowHidden = isCallingPackageAllowedHidden();
+ // When Fuse inserts a file to database it doesn't set is_download column. When app tries
+ // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't
+ // find a row ID with is_download=1. Use Files uri to query & update any existing row
+ // irrespective of is_download=1.
+ SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri,
+ Bundle.EMPTY, null);
+
+ // We won't be able to update columns that are not part of projection map of Files table. We
+ // have already checked strict columns in previous insert operation which failed with
+ // exception. Any malicious column usage would have got caught in insert operation, hence we
+ // can safely disable strict column check for upsert.
+ qb.setStrictColumns(false);
+ return qb;
+ }
+
private void maybePut(@NonNull ContentValues values, @NonNull String key,
@Nullable String value) {
if (value != null) {
@@ -2725,7 +2815,7 @@
private boolean maybeMarkAsDownload(@NonNull ContentValues values) {
final String path = values.getAsString(MediaColumns.DATA);
if (path != null && isDownload(path)) {
- values.put(FileColumns.IS_DOWNLOAD, true);
+ values.put(FileColumns.IS_DOWNLOAD, 1);
return true;
}
return false;
@@ -2835,6 +2925,9 @@
// IDs are forever; nobody should be editing them
initialValues.remove(MediaColumns._ID);
+ // Expiration times are hard-coded; let's derive them
+ FileUtils.computeDateExpires(initialValues);
+
// Ignore or augment incoming raw filesystem paths
for (String column : sDataColumns.keySet()) {
if (!initialValues.containsKey(column)) continue;
@@ -3038,7 +3131,7 @@
case DOWNLOADS:
maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
- initialValues.put(FileColumns.IS_DOWNLOAD, true);
+ initialValues.put(FileColumns.IS_DOWNLOAD, 1);
rowId = insertFile(qb, helper, match, uri, extras, initialValues,
FileColumns.MEDIA_TYPE_NONE, false);
if (rowId > 0) {
@@ -3122,14 +3215,6 @@
}
}
- @VisibleForTesting
- static boolean parseBoolean(String value) {
- if (value == null) return false;
- if ("1".equals(value)) return true;
- if ("true".equalsIgnoreCase(value)) return true;
- return false;
- }
-
@Deprecated
private String getSharedPackages(String callingPackage) {
final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames();
@@ -3178,7 +3263,7 @@
}
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
- if (parseBoolean(uri.getQueryParameter("distinct"))) {
+ if (uri.getBooleanQueryParameter("distinct", false)) {
qb.setDistinct(true);
}
qb.setStrict(true);
@@ -3186,11 +3271,8 @@
// When caller is system, such as the media scanner, we're willing
// to let them access any columns they want
} else {
- // TODO: re-enable regardless of target SDK in b/146518586
- if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
- qb.setStrictColumns(true);
- qb.setStrictGrammar(true);
- }
+ qb.setStrictColumns(true);
+ qb.setStrictGrammar(true);
}
final String callingPackage = getCallingPackageOrSelf();
@@ -3925,19 +4007,19 @@
*/
private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
String[] userWhereArgs) {
- synchronized (mDirectoryCache) {
- mDirectoryCache.clear();
+ return (int) helper.runWithTransaction((db) -> {
+ synchronized (mDirectoryCache) {
+ mDirectoryCache.clear();
+ }
- return (int) helper.runWithTransaction((db) -> {
- int n = 0;
- int total = 0;
- do {
- n = qb.delete(helper, userWhere, userWhereArgs);
- total += n;
- } while (n > 0);
- return total;
- });
- }
+ int n = 0;
+ int total = 0;
+ do {
+ n = qb.delete(helper, userWhere, userWhereArgs);
+ total += n;
+ } while (n > 0);
+ return total;
+ });
}
@Override
@@ -4158,8 +4240,7 @@
break;
case MediaStore.CREATE_TRASH_REQUEST_CALL:
allowedColumns = Arrays.asList(
- MediaColumns.IS_TRASHED,
- MediaColumns.DATE_EXPIRES);
+ MediaColumns.IS_TRASHED);
break;
default:
allowedColumns = Arrays.asList();
@@ -4291,7 +4372,7 @@
// doesn't exist we fall through to create it below
final File thumbFile = getThumbnailFile(uri);
try {
- return ParcelFileDescriptor.open(thumbFile,
+ return FileUtils.openSafely(thumbFile,
ParcelFileDescriptor.MODE_READ_ONLY);
} catch (FileNotFoundException ignored) {
}
@@ -4314,9 +4395,9 @@
// once for remote reading. Both FDs point at the same
// underlying inode on disk, so they're stable across renames
// to avoid race conditions between threads.
- thumbWrite = ParcelFileDescriptor.open(thumbTempFile,
+ thumbWrite = FileUtils.openSafely(thumbTempFile,
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
- thumbRead = ParcelFileDescriptor.open(thumbTempFile,
+ thumbRead = FileUtils.openSafely(thumbTempFile,
ParcelFileDescriptor.MODE_READ_ONLY);
final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
@@ -4490,7 +4571,7 @@
final Uri playlistUri = ContentUris.withAppendedId(
MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
- if (parseBoolean(uri.getQueryParameter("move"))) {
+ if (uri.getBooleanQueryParameter("move", false)) {
// Convert explicit request into query; sigh, moveItem()
// uses zero-based indexing instead of one-based indexing
final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1;
@@ -4543,6 +4624,9 @@
// IDs are forever; nobody should be editing them
initialValues.remove(MediaColumns._ID);
+ // Expiration times are hard-coded; let's derive them
+ FileUtils.computeDateExpires(initialValues);
+
// Ignore or augment incoming raw filesystem paths
for (String column : sDataColumns.keySet()) {
if (!initialValues.containsKey(column)) continue;
@@ -4644,16 +4728,14 @@
break;
}
- // TODO: remove this as part of fixing b/151768142
- final boolean isCallingPackageSystem = isCallingPackageSystem()
- && !"com.android.systemui".equals(getCallingPackageOrSelf());
-
// If we're touching columns that would change placement of a file,
// blend in current values and recalculate path
+ final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
+ !isCallingPackageSystem());
if (containsAny(initialValues.keySet(), sPlacementColumns)
&& !initialValues.containsKey(MediaColumns.DATA)
- && !isCallingPackageSystem
- && !isThumbnail) {
+ && !isThumbnail
+ && allowMovement) {
Trace.beginSection("movement");
// We only support movement under well-defined collections
@@ -4734,15 +4816,6 @@
// Make sure any updated paths look sane
assertFileColumnsSane(match, uri, initialValues);
- // if the media type is being changed, check if it's being changed from image or video
- // to something else
- if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
- final int newMediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
-
- // If we're changing media types, invalidate any thumbnails
- triggerInvalidate = true;
- }
-
if (initialValues.containsKey(FileColumns.DATA)) {
// If we're changing paths, invalidate any thumbnails
triggerInvalidate = true;
@@ -4767,11 +4840,7 @@
final ContentValues values = new ContentValues(initialValues);
switch (match) {
- case AUDIO_MEDIA_ID: {
- computeAudioLocalizedValues(values);
- computeAudioKeyValues(values);
- // fall-through
- }
+ case AUDIO_MEDIA_ID:
case AUDIO_PLAYLISTS_ID:
case VIDEO_MEDIA_ID:
case IMAGES_MEDIA_ID:
@@ -4782,6 +4851,17 @@
}
}
+ if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
+ final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
+ switch (mediaType) {
+ case FileColumns.MEDIA_TYPE_AUDIO: {
+ computeAudioLocalizedValues(values);
+ computeAudioKeyValues(values);
+ break;
+ }
+ }
+ }
+
count = qb.update(helper, values, userWhere, userWhereArgs);
// If the caller tried (and failed) to update metadata, the file on disk
@@ -5208,6 +5288,16 @@
mCallingIdentity.get().addDeletedRowId(path, rowId);
}
+ private void handleOwnerPackageNameChange(@NonNull String oldPath,
+ @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) {
+ if (Objects.equals(oldOwnerPackage, newOwnerPackage)) {
+ return;
+ }
+ // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old
+ // owner from gaining access to replaced file.
+ invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath);
+ }
+
/**
* Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
*/
@@ -5416,7 +5506,7 @@
// If fuse is enabled, we can provide an fd that points to the fuse
// file system and handle redaction in the fuse handler when the caller reads.
Log.i(TAG, "Redacting with new FUSE for " + filePath);
- pfd = ParcelFileDescriptor.open(getFuseFile(file), modeBits);
+ pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
} else {
// TODO(b/135341978): Remove this and associated code
// when fuse is on by default.
@@ -5434,7 +5524,7 @@
daemon = getFuseDaemonForFile(file);
} catch (FileNotFoundException ignored) {
}
- ParcelFileDescriptor lowerFsFd = ParcelFileDescriptor.open(file, modeBits);
+ ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits);
boolean forRead = (modeBits & ParcelFileDescriptor.MODE_READ_ONLY) != 0;
boolean shouldOpenWithFuse = daemon != null
&& daemon.shouldOpenWithFuse(filePath, forRead, lowerFsFd.getFd());
@@ -5445,7 +5535,7 @@
// resulting from cache inconsistencies between the upper and lower
// filesystem caches
Log.w(TAG, "Using FUSE for " + filePath);
- pfd = ParcelFileDescriptor.open(getFuseFile(file), modeBits);
+ pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
try {
lowerFsFd.close();
} catch (IOException e) {
@@ -6210,6 +6300,17 @@
}
}
+ @Keep
+ public boolean isUidForPackageForFuse(@NonNull String packageName, int uid) {
+ final LocalCallingIdentity token =
+ clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+ try {
+ return isCallingIdentitySharedPackageName(packageName);
+ } finally {
+ restoreLocalCallingIdentity(token);
+ }
+ }
+
private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
// System internals can work with all media
if (isCallingPackageSystem()) {
@@ -6822,7 +6923,6 @@
sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
- sMutableColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
diff --git a/src/com/android/providers/media/PermissionActivity.java b/src/com/android/providers/media/PermissionActivity.java
index 0c398dd..0696ef5 100644
--- a/src/com/android/providers/media/PermissionActivity.java
+++ b/src/com/android/providers/media/PermissionActivity.java
@@ -20,6 +20,7 @@
import static com.android.providers.media.MediaProvider.IMAGES_MEDIA_ID;
import static com.android.providers.media.MediaProvider.VIDEO_MEDIA_ID;
import static com.android.providers.media.MediaProvider.collectUris;
+import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
import static com.android.providers.media.util.Logging.TAG;
import android.app.Activity;
@@ -198,6 +199,7 @@
for (Uri uri : uris) {
ops.add(ContentProviderOperation.newUpdate(uri)
.withValues(values)
+ .withExtra(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, true)
.withExceptionAllowed(true)
.build());
}
@@ -290,10 +292,10 @@
case MediaStore.CREATE_WRITE_REQUEST_CALL:
return VERB_WRITE;
case MediaStore.CREATE_TRASH_REQUEST_CALL:
- return (values.getAsInteger(MediaColumns.IS_TRASHED) != 0)
+ return getAsBoolean(values, MediaColumns.IS_TRASHED, false)
? VERB_TRASH : VERB_UNTRASH;
case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
- return (values.getAsInteger(MediaColumns.IS_FAVORITE) != 0)
+ return getAsBoolean(values, MediaColumns.IS_FAVORITE, false)
? VERB_FAVORITE : VERB_UNFAVORITE;
case MediaStore.CREATE_DELETE_REQUEST_CALL:
return VERB_DELETE;
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index ef33b04..a5ab700 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -48,7 +48,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -532,10 +534,27 @@
return sb.toString();
}
+ public static boolean parseBoolean(@Nullable Object value, boolean def) {
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ } else if (value instanceof Number) {
+ return ((Number) value).intValue() != 0;
+ } else if (value instanceof String) {
+ final String stringValue = ((String) value).toLowerCase(Locale.ROOT);
+ return (!"false".equals(stringValue) && !"0".equals(stringValue));
+ } else {
+ return def;
+ }
+ }
+
+ public static boolean getAsBoolean(@NonNull Bundle extras,
+ @NonNull String key, boolean def) {
+ return parseBoolean(extras.get(key), def);
+ }
+
public static boolean getAsBoolean(@NonNull ContentValues values,
@NonNull String key, boolean def) {
- final Integer value = values.getAsInteger(key);
- return (value != null) ? (value != 0) : def;
+ return parseBoolean(values.get(key), def);
}
public static long getAsLong(@NonNull ContentValues values,
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index ee5d7e1..e711d0c 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -25,16 +25,21 @@
import static android.system.OsConstants.F_OK;
import static android.system.OsConstants.O_ACCMODE;
import static android.system.OsConstants.O_APPEND;
+import static android.system.OsConstants.O_CLOEXEC;
import static android.system.OsConstants.O_CREAT;
+import static android.system.OsConstants.O_NOFOLLOW;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_RDWR;
import static android.system.OsConstants.O_TRUNC;
import static android.system.OsConstants.O_WRONLY;
import static android.system.OsConstants.R_OK;
+import static android.system.OsConstants.S_IRWXG;
+import static android.system.OsConstants.S_IRWXU;
import static android.system.OsConstants.W_OK;
import static com.android.providers.media.util.DatabaseUtils.getAsBoolean;
import static com.android.providers.media.util.DatabaseUtils.getAsLong;
+import static com.android.providers.media.util.DatabaseUtils.parseBoolean;
import static com.android.providers.media.util.Logging.TAG;
import android.content.ClipDescription;
@@ -42,9 +47,13 @@
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
+import android.os.ParcelFileDescriptor;
import android.os.storage.StorageManager;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
@@ -55,6 +64,7 @@
import androidx.annotation.VisibleForTesting;
import java.io.File;
+import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -79,10 +89,39 @@
import java.util.regex.Pattern;
public class FileUtils {
+ /**
+ * Drop-in replacement for {@link ParcelFileDescriptor#open(File, int)}
+ * which adds security features like {@link OsConstants#O_CLOEXEC} and
+ * {@link OsConstants#O_NOFOLLOW}.
+ */
+ public static @NonNull ParcelFileDescriptor openSafely(@NonNull File file, int pfdFlags)
+ throws FileNotFoundException {
+ final int posixFlags = translateModePfdToPosix(pfdFlags) | O_CLOEXEC | O_NOFOLLOW;
+ try {
+ final FileDescriptor fd = Os.open(file.getAbsolutePath(), posixFlags,
+ S_IRWXU | S_IRWXG);
+ try {
+ return ParcelFileDescriptor.dup(fd);
+ } finally {
+ closeQuietly(fd);
+ }
+ } catch (IOException | ErrnoException e) {
+ throw new FileNotFoundException(e.getMessage());
+ }
+ }
+
public static void closeQuietly(@Nullable AutoCloseable closeable) {
android.os.FileUtils.closeQuietly(closeable);
}
+ public static void closeQuietly(@Nullable FileDescriptor fd) {
+ if (fd == null) return;
+ try {
+ Os.close(fd);
+ } catch (ErrnoException ignored) {
+ }
+ }
+
public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
return android.os.FileUtils.copy(in, out);
}
@@ -804,13 +843,13 @@
* Default duration that {@link MediaColumns#IS_PENDING} items should be
* preserved for until automatically cleaned by {@link #runIdleMaintenance}.
*/
- public static final long DEFAULT_DURATION_PENDING = DateUtils.WEEK_IN_MILLIS;
+ public static final long DEFAULT_DURATION_PENDING = 7 * DateUtils.DAY_IN_MILLIS;
/**
* Default duration that {@link MediaColumns#IS_TRASHED} items should be
* preserved for until automatically cleaned by {@link #runIdleMaintenance}.
*/
- public static final long DEFAULT_DURATION_TRASHED = DateUtils.WEEK_IN_MILLIS;
+ public static final long DEFAULT_DURATION_TRASHED = 30 * DateUtils.DAY_IN_MILLIS;
public static boolean isDownload(@NonNull String path) {
return PATTERN_DOWNLOADS_FILE.matcher(path).matches();
@@ -831,7 +870,13 @@
* and which captures the package name as the first group.
*/
public static final Pattern PATTERN_OWNED_PATH = Pattern.compile(
- "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/.*)?");
+ "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)(/?.*)?");
+
+ /**
+ * Regex that matches Android/obb or Android/data path.
+ */
+ public static final Pattern PATTERN_DATA_OR_OBB_PATH = Pattern.compile(
+ "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|obb)/?$");
/**
* Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it
@@ -920,6 +965,15 @@
}
/**
+ * Returns true if relative path is Android/data or Android/obb path.
+ */
+ public static boolean isDataOrObbPath(String path) {
+ if (path == null) return false;
+ final Matcher m = PATTERN_DATA_OR_OBB_PATH.matcher(path);
+ return m.matches();
+ }
+
+ /**
* Returns the name of the top level directory, or null if the path doesn't go through the
* external storage directory.
*/
@@ -933,6 +987,36 @@
}
/**
+ * Compute the value of {@link MediaColumns#DATE_EXPIRES} based on other
+ * columns being modified by this operation.
+ */
+ public static void computeDateExpires(@NonNull ContentValues values) {
+ // External apps have no ability to change this field
+ values.remove(MediaColumns.DATE_EXPIRES);
+
+ // Only define the field when this modification is actually adjusting
+ // one of the flags that should influence the expiration
+ final Object pending = values.get(MediaColumns.IS_PENDING);
+ if (pending != null) {
+ if (parseBoolean(pending, false)) {
+ values.put(MediaColumns.DATE_EXPIRES,
+ (System.currentTimeMillis() + DEFAULT_DURATION_PENDING) / 1000);
+ } else {
+ values.putNull(MediaColumns.DATE_EXPIRES);
+ }
+ }
+ final Object trashed = values.get(MediaColumns.IS_TRASHED);
+ if (trashed != null) {
+ if (parseBoolean(trashed, false)) {
+ values.put(MediaColumns.DATE_EXPIRES,
+ (System.currentTimeMillis() + DEFAULT_DURATION_TRASHED) / 1000);
+ } else {
+ values.putNull(MediaColumns.DATE_EXPIRES);
+ }
+ }
+ }
+
+ /**
* Compute several scattered {@link MediaColumns} values from
* {@link MediaColumns#DATA}. This method performs no enforcement of
* argument validity.
@@ -941,7 +1025,6 @@
// Worst case we have to assume no bucket details
values.remove(MediaColumns.VOLUME_NAME);
values.remove(MediaColumns.RELATIVE_PATH);
- values.remove(MediaColumns.IS_DOWNLOAD);
values.remove(MediaColumns.IS_PENDING);
values.remove(MediaColumns.IS_TRASHED);
values.remove(MediaColumns.DATE_EXPIRES);
@@ -957,8 +1040,6 @@
values.put(MediaColumns.VOLUME_NAME, extractVolumeName(data));
values.put(MediaColumns.RELATIVE_PATH, extractRelativePath(data));
- values.put(MediaColumns.IS_DOWNLOAD, isDownload(data));
-
final String displayName = extractDisplayName(data);
final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(displayName);
if (matcher.matches()) {
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index c076914..387ced0 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -35,16 +35,22 @@
import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.Context;
+import android.provider.MediaStore;
public class PermissionUtils {
// Callers must hold both the old and new permissions, so that we can
// handle obscure cases like when an app targets Q but was installed on
// a device that was originally running on P before being upgraded to Q.
+ private static volatile int sLegacyMediaProviderUid = -1;
+
public static boolean checkPermissionSystem(Context context,
int pid, int uid, String packageName) {
+ // Apps sharing legacy MediaProvider's uid like DownloadProvider and MTP are treated as
+ // system.
return uid == android.os.Process.SYSTEM_UID || uid == android.os.Process.myUid()
- || uid == android.os.Process.SHELL_UID || uid == android.os.Process.ROOT_UID;
+ || uid == android.os.Process.SHELL_UID || uid == android.os.Process.ROOT_UID
+ || isLegacyMediaProvider(context, uid);
}
public static boolean checkPermissionBackup(Context context, int pid, int uid) {
@@ -209,4 +215,15 @@
throw new IllegalStateException(op + " has unknown mode " + mode);
}
}
+
+ private static boolean isLegacyMediaProvider(Context context, int uid) {
+ if (sLegacyMediaProviderUid == -1) {
+ // Uid stays constant while legacy Media Provider stays installed. Cache legacy
+ // MediaProvider's uid for the first time.
+ sLegacyMediaProviderUid = context.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0)
+ .applicationInfo.uid;
+ }
+ return (uid == sLegacyMediaProviderUid);
+ }
}
diff --git a/src/com/android/providers/media/util/RedactingFileDescriptor.java b/src/com/android/providers/media/util/RedactingFileDescriptor.java
index a0bec59..bf2524f 100644
--- a/src/com/android/providers/media/util/RedactingFileDescriptor.java
+++ b/src/com/android/providers/media/util/RedactingFileDescriptor.java
@@ -47,15 +47,10 @@
private volatile long[] mRedactRanges;
private volatile long[] mFreeOffsets;
- private FileDescriptor mInner = null;
+ private ParcelFileDescriptor mInner = null;
private ParcelFileDescriptor mOuter = null;
- public static void closeQuietly(FileDescriptor fd) {
- try {
- Os.close(fd);
- } catch (ErrnoException ignored) {
- }
- }
+ private FileDescriptor mInnerFd = null;
private RedactingFileDescriptor(
Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
@@ -64,17 +59,13 @@
mFreeOffsets = freeOffsets;
try {
- try {
- mInner = Os.open(file.getAbsolutePath(),
- FileUtils.translateModePfdToPosix(mode), 0);
- mOuter = context.getSystemService(StorageManager.class)
- .openProxyFileDescriptor(mode, mCallback,
- new Handler(Looper.getMainLooper()));
- } catch (ErrnoException e) {
- throw e.rethrowAsIOException();
- }
+ mInner = FileUtils.openSafely(file, mode);
+ mInnerFd = mInner.getFileDescriptor();
+ mOuter = context.getSystemService(StorageManager.class)
+ .openProxyFileDescriptor(mode, mCallback,
+ new Handler(Looper.getMainLooper()));
} catch (IOException e) {
- closeQuietly(mInner);
+ FileUtils.closeQuietly(mInner);
FileUtils.closeQuietly(mOuter);
throw e;
}
@@ -158,7 +149,7 @@
private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
@Override
public long onGetSize() throws ErrnoException {
- return Os.fstat(mInner).st_size;
+ return Os.fstat(mInnerFd).st_size;
}
@Override
@@ -166,7 +157,7 @@
int n = 0;
while (n < size) {
try {
- final int res = Os.pread(mInner, data, n, size - n, offset + n);
+ final int res = Os.pread(mInnerFd, data, n, size - n, offset + n);
if (res == 0) {
break;
} else {
@@ -203,7 +194,7 @@
int n = 0;
while (n < size) {
try {
- final int res = Os.pwrite(mInner, data, n, size - n, offset + n);
+ final int res = Os.pwrite(mInnerFd, data, n, size - n, offset + n);
if (res == 0) {
break;
} else {
@@ -222,12 +213,12 @@
@Override
public void onFsync() throws ErrnoException {
- Os.fsync(mInner);
+ Os.fsync(mInnerFd);
}
@Override
public void onRelease() {
- closeQuietly(mInner);
+ FileUtils.closeQuietly(mInner);
}
};
}
diff --git a/tests/Android.bp b/tests/Android.bp
index e79f576..ee5143d 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -9,6 +9,7 @@
"device-tests",
"mts",
],
+ compile_multilib: "both",
manifest: "AndroidManifest.xml",
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 65b45cd..0d29bbf 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -4,6 +4,7 @@
"device-tests",
"mts",
],
+ compile_multilib: "both",
manifest: "AndroidManifest.xml",
@@ -20,6 +21,7 @@
static_libs: [
"androidx.test.rules",
"mockito-target",
+ "truth-prebuilt",
],
certificate: "media",
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 fa2de15..71f79a1 100644
--- a/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
+++ b/tests/client/src/com/android/providers/media/client/LegacyProviderMigrationTest.java
@@ -24,6 +24,7 @@
import android.app.UiAutomation;
import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
@@ -49,6 +50,8 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.google.common.truth.Truth;
+
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
@@ -61,6 +64,7 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
@@ -80,6 +84,11 @@
private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
private static final long POLLING_SLEEP_MILLIS = 100;
+ /**
+ * Number of media items to insert for {@link #testLegacy_Extreme()}.
+ */
+ private static final int EXTREME_COUNT = 10_000;
+
private Uri mExternalAudio;
private Uri mExternalVideo;
private Uri mExternalImages;
@@ -182,6 +191,67 @@
doLegacy(mExternalDownloads, values);
}
+ /**
+ * Verify that a legacy database with thousands of media entries can be
+ * successfully migrated.
+ */
+ @Test
+ public void testLegacy_Extreme() throws Exception {
+ final Context context = InstrumentationRegistry.getTargetContext();
+ final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+
+ final ProviderInfo legacyProvider = context.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY_LEGACY, 0);
+ final ProviderInfo modernProvider = context.getPackageManager()
+ .resolveContentProvider(MediaStore.AUTHORITY, 0);
+
+ // Only continue if we have both providers to test against
+ Assume.assumeNotNull(legacyProvider);
+ Assume.assumeNotNull(modernProvider);
+
+ // Wait until everything calms down
+ MediaStore.waitForIdle(context.getContentResolver());
+
+ // Clear data on the legacy provider so that we create a database
+ executeShellCommand("pm clear " + legacyProvider.applicationInfo.packageName, ui);
+
+ // Create thousands of items in the legacy provider
+ try (ContentProviderClient legacy = context.getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY_LEGACY)) {
+ // We're purposefully "silent" to avoid creating the raw file on
+ // disk, since otherwise this test would take several minutes
+ final Uri insertTarget = rewriteToLegacy(
+ mExternalImages.buildUpon().appendQueryParameter("silent", "true").build());
+
+ final ArrayList<ContentProviderOperation> ops = new ArrayList<>();
+ for (int i = 0; i < EXTREME_COUNT; i++) {
+ ops.add(ContentProviderOperation.newInsert(insertTarget)
+ .withValues(generateValues(FileColumns.MEDIA_TYPE_IMAGE, "image/png",
+ Environment.DIRECTORY_PICTURES))
+ .build());
+
+ if ((ops.size() > 1_000) || (i == (EXTREME_COUNT - 1))) {
+ Log.v(TAG, "Inserting items...");
+ legacy.applyBatch(MediaStore.AUTHORITY_LEGACY, ops);
+ ops.clear();
+ }
+ }
+ }
+
+ // Clear data on the modern provider so that the initial scan recovers
+ // metadata from the legacy provider
+ executeShellCommand("pm clear " + modernProvider.applicationInfo.packageName, ui);
+ pollForExternalStorageState();
+
+ // Confirm that details from legacy provider have migrated
+ try (ContentProviderClient modern = context.getContentResolver()
+ .acquireContentProviderClient(MediaStore.AUTHORITY)) {
+ try (Cursor cursor = modern.query(mExternalImages, null, null, null)) {
+ Truth.assertThat(cursor.getCount()).isAtLeast(EXTREME_COUNT);
+ }
+ }
+ }
+
private void doLegacy(Uri collectionUri, ContentValues values) throws Exception {
final Context context = InstrumentationRegistry.getTargetContext();
final UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation();
diff --git a/tests/jni/FuseDaemonTest/Android.bp b/tests/jni/FuseDaemonTest/Android.bp
index 1208e1d..e3e92b0 100644
--- a/tests/jni/FuseDaemonTest/Android.bp
+++ b/tests/jni/FuseDaemonTest/Android.bp
@@ -47,6 +47,7 @@
manifest: "AndroidManifest.xml",
srcs: ["src/**/*.java"],
static_libs: ["androidx.test.rules", "truth-prebuilt", "tests-fusedaemon-lib"],
+ compile_multilib: "both",
test_suites: ["general-tests", "mts"],
sdk_version: "test_current",
java_resources: [
@@ -61,6 +62,7 @@
manifest: "legacy/AndroidManifest.xml",
srcs: ["legacy/src/**/*.java"],
static_libs: ["androidx.test.rules", "truth-prebuilt", "tests-fusedaemon-lib"],
+ compile_multilib: "both",
test_suites: ["general-tests", "mts"],
sdk_version: "test_current",
target_sdk_version: "29",
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
index 074231b..29986bc 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/ReaddirTestHelper.java
@@ -18,6 +18,7 @@
import java.io.File;
import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Files;
@@ -69,14 +70,15 @@
public static ArrayList<String> readDirectory(String directoryPath, Filter<Path> filter) {
ArrayList<String> directoryEntries = new ArrayList<String>();
File dir = new File(directoryPath);
+
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir.toPath(),
filter)) {
for (Path de: directoryStream) {
directoryEntries.add(de.getFileName().toString());
}
- } catch (IOException x) {
+ } catch (IOException | DirectoryIteratorException x) {
Log.e(TAG, "IOException occurred while readding directory entries from " +
- directoryPath);
+ directoryPath, x);
}
return directoryEntries;
}
diff --git a/tests/src/com/android/providers/media/MediaProviderTest.java b/tests/src/com/android/providers/media/MediaProviderTest.java
index 0f0ed64..6e9fda9 100644
--- a/tests/src/com/android/providers/media/MediaProviderTest.java
+++ b/tests/src/com/android/providers/media/MediaProviderTest.java
@@ -741,18 +741,6 @@
}
@Test
- public void testParseBoolean() throws Exception {
- assertTrue(MediaProvider.parseBoolean("TRUE"));
- assertTrue(MediaProvider.parseBoolean("true"));
- assertTrue(MediaProvider.parseBoolean("1"));
-
- assertFalse(MediaProvider.parseBoolean("FALSE"));
- assertFalse(MediaProvider.parseBoolean("false"));
- assertFalse(MediaProvider.parseBoolean("0"));
- assertFalse(MediaProvider.parseBoolean(null));
- }
-
- @Test
public void testIsDownload() throws Exception {
assertTrue(isDownload("/storage/emulated/0/Download/colors.png"));
assertTrue(isDownload("/storage/emulated/0/Download/test.pdf"));
diff --git a/tests/src/com/android/providers/media/PermissionActivityTest.java b/tests/src/com/android/providers/media/PermissionActivityTest.java
index e751a90..7bbce62 100644
--- a/tests/src/com/android/providers/media/PermissionActivityTest.java
+++ b/tests/src/com/android/providers/media/PermissionActivityTest.java
@@ -19,15 +19,22 @@
import android.app.Instrumentation;
import android.content.ClipData;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
import android.provider.MediaStore;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.providers.media.scan.MediaScannerTest;
+
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.io.File;
+
/**
* We already have solid coverage of this logic in {@code CtsProviderTestCases},
* but the coverage system currently doesn't measure that, so we add the bare
@@ -45,11 +52,18 @@
activity.startActivityForResult(createIntent(), 42);
}
- private static Intent createIntent() {
+ private static Intent createIntent() throws Exception {
+ final Context context = InstrumentationRegistry.getContext();
+
+ final File dir = Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
+ final File file = MediaScannerTest.stage(R.raw.test_image,
+ new File(dir, "test" + System.nanoTime() + ".jpg"));
+ final Uri uri = MediaStore.scanFile(context.getContentResolver(), file);
+
final Intent intent = new Intent(MediaStore.CREATE_WRITE_REQUEST_CALL, null,
- InstrumentationRegistry.getContext(), PermissionActivity.class);
- intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("",
- MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, 42)));
+ context, PermissionActivity.class);
+ intent.putExtra(MediaStore.EXTRA_CLIP_DATA, ClipData.newRawUri("", uri));
intent.putExtra(MediaStore.EXTRA_CONTENT_VALUES, new ContentValues());
return intent;
}
diff --git a/tests/src/com/android/providers/media/scan/DrmTest.java b/tests/src/com/android/providers/media/scan/DrmTest.java
index 038a93e..c7ed122 100644
--- a/tests/src/com/android/providers/media/scan/DrmTest.java
+++ b/tests/src/com/android/providers/media/scan/DrmTest.java
@@ -97,6 +97,7 @@
@Test
public void testForwardLock_Audio() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("audio/mpeg", R.raw.test_audio, (values) -> {
assertEquals(1_045L, (long) values.getAsLong(FileColumns.DURATION));
assertEquals(FileColumns.MEDIA_TYPE_AUDIO,
@@ -106,6 +107,7 @@
@Test
public void testForwardLock_Video() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("video/mp4", R.raw.test_video, (values) -> {
assertEquals(40_000L, (long) values.getAsLong(FileColumns.DURATION));
assertEquals(FileColumns.MEDIA_TYPE_VIDEO,
@@ -115,6 +117,7 @@
@Test
public void testForwardLock_Image() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("image/jpeg", R.raw.test_image, (values) -> {
// ExifInterface currently doesn't know how to scan DRM images, so
// the best we can do is verify the base test metadata
@@ -125,6 +128,7 @@
@Test
public void testForwardLock_Binary() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
doForwardLock("application/octet-stream", R.raw.test_image, null);
}
@@ -134,6 +138,8 @@
*/
@Test
public void testForwardLock_130680734() throws Exception {
+ Assume.assumeTrue(isForwardLockSupported());
+
final ContentValues values = new ContentValues();
values.put(MediaColumns.DISPLAY_NAME, "temp" + System.nanoTime() + ".fl");
values.put(MediaColumns.MIME_TYPE, MIME_FORWARD_LOCKED);
@@ -185,8 +191,6 @@
private void doForwardLock(String mimeType, int resId,
@Nullable Consumer<ContentValues> verifier) throws Exception {
- Assume.assumeTrue(isForwardLockSupported());
-
InputStream dmStream = createDmStream(mimeType, resId);
File flPath = new File(mContext.getExternalMediaDirs()[0],
diff --git a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
index 3a0ea75..d4ff968 100644
--- a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
@@ -32,6 +32,7 @@
import static android.database.DatabaseUtils.escapeForLike;
import static com.android.providers.media.util.DatabaseUtils.maybeBalance;
+import static com.android.providers.media.util.DatabaseUtils.parseBoolean;
import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveLimit;
import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveSortOrder;
import static com.android.providers.media.util.DatabaseUtils.resolveQueryArgs;
@@ -368,6 +369,24 @@
escapeForLike("/path/to/fi%le.bin"));
}
+ @Test
+ public void testParseBoolean() throws Exception {
+ assertTrue(parseBoolean("TRUE", false));
+ assertTrue(parseBoolean("true", false));
+ assertTrue(parseBoolean("1", false));
+ assertTrue(parseBoolean(1, false));
+ assertTrue(parseBoolean(true, false));
+
+ assertFalse(parseBoolean("FALSE", true));
+ assertFalse(parseBoolean("false", true));
+ assertFalse(parseBoolean("0", true));
+ assertFalse(parseBoolean(0, true));
+ assertFalse(parseBoolean(false, true));
+
+ assertFalse(parseBoolean(null, false));
+ assertTrue(parseBoolean(null, true));
+ }
+
private static Pair<String, String> recoverAbusiveGroupBy(
Pair<String, String> selectionAndGroupBy) {
final Bundle queryArgs = new Bundle();
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 35f789e..da8ddce 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -51,6 +51,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -61,6 +62,9 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.google.common.collect.Range;
+import com.google.common.truth.Truth;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -548,6 +552,63 @@
assertEquals("/", values.get(MediaColumns.RELATIVE_PATH));
}
+ @Test
+ public void testComputeDateExpires_None() throws Exception {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+ FileUtils.computeDateExpires(values);
+ assertFalse(values.containsKey(MediaColumns.DATE_EXPIRES));
+ }
+
+ @Test
+ public void testComputeDateExpires_Pending_Set() throws Exception {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.IS_PENDING, 1);
+ values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+ FileUtils.computeDateExpires(values);
+ final long target = (System.currentTimeMillis()
+ + FileUtils.DEFAULT_DURATION_PENDING) / 1_000;
+ Truth.assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES))
+ .isIn(Range.closed(target - 5, target + 5));
+ }
+
+ @Test
+ public void testComputeDateExpires_Pending_Clear() throws Exception {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.IS_PENDING, 0);
+ values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+ FileUtils.computeDateExpires(values);
+ assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES));
+ assertNull(values.get(MediaColumns.DATE_EXPIRES));
+ }
+
+ @Test
+ public void testComputeDateExpires_Trashed_Set() throws Exception {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.IS_TRASHED, 1);
+ values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+ FileUtils.computeDateExpires(values);
+ final long target = (System.currentTimeMillis()
+ + FileUtils.DEFAULT_DURATION_TRASHED) / 1_000;
+ Truth.assertThat(values.getAsLong(MediaColumns.DATE_EXPIRES))
+ .isIn(Range.closed(target - 5, target + 5));
+ }
+
+ @Test
+ public void testComputeDateExpires_Trashed_Clear() throws Exception {
+ final ContentValues values = new ContentValues();
+ values.put(MediaColumns.IS_TRASHED, 0);
+ values.put(MediaColumns.DATE_EXPIRES, 1577836800L);
+
+ FileUtils.computeDateExpires(values);
+ assertTrue(values.containsKey(MediaColumns.DATE_EXPIRES));
+ assertNull(values.get(MediaColumns.DATE_EXPIRES));
+ }
+
private static File touch(File dir, String name) throws IOException {
final File res = new File(dir, name);
res.createNewFile();