Merge "Sanitize paths before query"
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 9e281f7..41eb152 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1198,6 +1198,23 @@
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
@@ -1212,7 +1229,7 @@
* @see MediaStore#getGeneration(Context, String)
*/
@Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
- public static final String GENERATION = "generation";
+ public static final String GENERATION_MODIFIED = "generation_modified";
// =======================================
// ==== MediaMetadataRetriever values ====
@@ -3655,30 +3672,33 @@
* 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 changed since a previous point in
- * time. Generation numbers are monotonically increasing over time, and can
- * be safely arithmetically compared.
+ * 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_MODIFIED}, since those values may change
- * in unexpected ways when apps use {@link File#setLastModified(long)} or
- * when the system clock is set incorrectly.
+ * 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 opaque version string for.
- * Must be one of the values returned from
+ * @param volumeName specific volume to obtain an generation value for. Must
+ * be one of the values returned from
* {@link #getExternalVolumeNames(Context)}.
- * @see MediaColumns#GENERATION
+ * @see MediaColumns#GENERATION_ADDED
+ * @see MediaColumns#GENERATION_MODIFIED
*/
public static long getGeneration(@NonNull Context context, @NonNull String volumeName) {
- final ContentResolver resolver = context.getContentResolver();
- try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
- final Bundle in = new Bundle();
- in.putString(Intent.EXTRA_TEXT, volumeName);
- final Bundle out = client.call(GET_GENERATION_CALL, null, in);
- return out.getLong(Intent.EXTRA_INDEX);
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
+ 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);
}
/**
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 7b29b92..812e6b6 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;
@@ -995,7 +998,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);
@@ -1316,7 +1318,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);
@@ -1481,11 +1483,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/logging.sh b/logging.sh
index 3a11df2..43f0d86 100755
--- a/logging.sh
+++ b/logging.sh
@@ -6,16 +6,24 @@
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
adb shell setprop log.tag.SQLiteQueryBuilder VERBOSE
+ adb shell setprop log.tag.FuseDaemon VERBOSE
+ adb shell setprop log.tag.libfuse VERBOSE
else
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 53d7dc0..d146716 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -79,19 +79,13 @@
* 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}.
+ * 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";
@@ -208,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<>();
@@ -531,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, generation INTEGER DEFAULT 0)");
+ + "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) {
@@ -714,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
@@ -725,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
@@ -741,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");
@@ -938,11 +850,14 @@
db.execSQL("DELETE FROM log;");
}
- private static void updateAddGeneration(SQLiteDatabase db, boolean internal) {
+ 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)");
+ }
- db.execSQL("ALTER TABLE files ADD COLUMN generation INTEGER DEFAULT 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) {
@@ -973,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 = 1108;
+ static final int VERSION_R = 1109;
static final int VERSION_LATEST = VERSION_R;
/**
@@ -1096,6 +1011,9 @@
updateAddSceneCaptureType(db, internal);
}
if (fromVersion < 1108) {
+ updateAddLocalMetadata(db, internal);
+ }
+ if (fromVersion < 1109) {
updateAddGeneration(db, internal);
}
@@ -1158,7 +1076,8 @@
/**
* Return the current generation that will be populated into
- * {@link MediaColumns#GENERATION}.
+ * {@link MediaColumns#GENERATION_ADDED} or
+ * {@link MediaColumns#GENERATION_MODIFIED}.
*/
public long getGeneration() {
return android.database.DatabaseUtils.longForQuery(getReadableDatabase(),
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 47d0bbb..31da5f6 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;
@@ -1071,8 +1072,8 @@
/**
* Updates database entry for given {@code path} with {@code values}
*/
- private boolean updateDatabaseForFuseRename(DatabaseHelper helper, 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,
@@ -4984,10 +4985,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;
}
/**
@@ -5970,11 +5976,12 @@
}
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);
});
}
@@ -6007,7 +6014,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/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 58c453a..fc199da 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -263,7 +263,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 +291,7 @@
mFilesUri = MediaStore.Files.getContentUri(mVolumeName);
mSignal = getOrCreateSignal(mVolumeName);
- mStartCurrentTime = System.currentTimeMillis();
+ mStartGeneration = MediaStore.getGeneration(mResolver, mVolumeName);
mSingleFile = mRoot.isFile();
Trace.endSection();
@@ -362,12 +362,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,
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index de65ebe..3c18dcf 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -466,7 +466,7 @@
for (int i = 0; i < bindArgs.length; i++) {
final Object bindArg = bindArgs[i];
- switch (DatabaseUtils.getTypeOfObject(bindArg)) {
+ switch (getTypeOfObject(bindArg)) {
case Cursor.FIELD_TYPE_NULL:
st.bindNull(i + 1);
break;
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/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index 9c64ef6..fac5112 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -799,7 +799,8 @@
final boolean hasGeneration = Objects.equals(mTables, "files");
if (hasGeneration) {
- values.remove(MediaColumns.GENERATION);
+ values.remove(MediaColumns.GENERATION_ADDED);
+ values.remove(MediaColumns.GENERATION_MODIFIED);
}
final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
@@ -812,7 +813,9 @@
}
if (hasGeneration) {
sql.append(',');
- sql.append(MediaColumns.GENERATION);
+ sql.append(MediaColumns.GENERATION_ADDED);
+ sql.append(',');
+ sql.append(MediaColumns.GENERATION_MODIFIED);
}
sql.append(") VALUES (");
for (int i = 0; i < rawValues.size(); i++) {
@@ -826,6 +829,10 @@
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();
@@ -844,7 +851,8 @@
final boolean hasGeneration = Objects.equals(mTables, "files");
if (hasGeneration) {
- values.remove(MediaColumns.GENERATION);
+ values.remove(MediaColumns.GENERATION_ADDED);
+ values.remove(MediaColumns.GENERATION_MODIFIED);
}
final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
@@ -858,7 +866,7 @@
}
if (hasGeneration) {
sql.append(',');
- sql.append(MediaColumns.GENERATION);
+ sql.append(MediaColumns.GENERATION_MODIFIED);
sql.append('=');
sql.append('(');
sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
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/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/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..f7f1fd3 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;
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));
+ }
+}