Merge "Fix ENAMETOOLONG issue when set trash or pending to file"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b58c930..e0b9983 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,6 +28,8 @@
<!-- Permissions required for reading device configs -->
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
+ <uses-permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/>
+
<application
android:name="com.android.providers.media.MediaApplication"
android:label="@string/app_label"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 3af81a2..74ff71e 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -35,6 +35,9 @@
},
{
"name": "fuse_node_test"
+ },
+ {
+ "name": "MediaProviderTranscodeTests"
}
],
"postsubmit": [
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index ffef8fb..6d8da53 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,4 @@
{
"name": "com.android.mediaprovider",
- "version": 300000000
+ "version": 309999900
}
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 4894a32..57ad915 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1741,6 +1741,44 @@
public static final int MEDIA_TYPE_DOCUMENT = 6;
/**
+ * Modifier of the database row
+ *
+ * Specifies the last modifying operation of the database row. This
+ * does not give any information on the package that modified the
+ * database row.
+ * Initially, this column will be populated by
+ * {@link ContentResolver}#insert and media scan operations. And,
+ * the column will be used to identify if the file was previously
+ * scanned.
+ * @hide
+ */
+ @Column(value = Cursor.FIELD_TYPE_INTEGER)
+ public static final String _MODIFIER = "_modifier";
+
+ /**
+ * Constant for the {@link #_MODIFIER} column indicating
+ * that the last modifier of the database row is FUSE operation.
+ * @hide
+ */
+ public static final int _MODIFIER_FUSE = 1;
+
+ /**
+ * Constant for the {@link #_MODIFIER} column indicating
+ * that the last modifier of the database row is explicit
+ * {@link ContentResolver} operation from app.
+ * @hide
+ */
+ public static final int _MODIFIER_CR = 2;
+
+ /**
+ * Constant for the {@link #_MODIFIER} column indicating
+ * that the last modifier of the database row is a media scan
+ * operation.
+ * @hide
+ */
+ public static final int _MODIFIER_MEDIA_SCAN = 3;
+
+ /**
* Status of the transcode file
*
* For apps that do not support modern media formats for video, we
diff --git a/jni/Android.bp b/jni/Android.bp
index 2223e5e..dfb2da3 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -60,6 +60,12 @@
"-google-runtime-int",
],
+ target: {
+ android32: {
+ cflags: ["-DNO_FUSE_PASSTHROUGH_32BIT"],
+ },
+ },
+
sdk_version: "current",
stl: "c++_static",
min_sdk_version: "30",
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index a1350bf..c5f4ab7 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -540,7 +540,13 @@
if (fuse->passthrough) {
if (conn->capable & FUSE_CAP_PASSTHROUGH) {
+#ifdef NO_FUSE_PASSTHROUGH_32BIT
+ // TODO(b/175151591) Linux compatibility layer for FUSE_DEV_IOC_PASSTHROUGH_OPEN
+ LOG(WARNING) << "Passthrough feature not supported on 32-bit userspace";
+ fuse->passthrough = false;
+#else
mask |= FUSE_CAP_PASSTHROUGH;
+#endif
} else {
LOG(WARNING) << "Passthrough feature not supported by the kernel";
fuse->passthrough = false;
diff --git a/res/raw/transcode_compat_manifest b/res/raw/transcode_compat_manifest
new file mode 100644
index 0000000..156a939
--- /dev/null
+++ b/res/raw/transcode_compat_manifest
@@ -0,0 +1,44 @@
+com.google.android.GoogleCamera,1
+com.google.android.apps.nbu.files,1
+com.instagram.android,1
+com.whatsapp,1
+com.google.android.apps.photos,1
+com.linecorp.b612.android,1
+im.thebot.messenger,1
+com.tencent.mm,1
+com.skype.raider,1
+com.facebook.katana,1
+com.facebook.orca,1
+com.imo.android.imous,1
+com.snapchat.android,1
+com.yy.biu,1
+com.google.android.talk,1
+com.twitter.android,1
+com.zhiliaoapp.musically,1
+com.nexstreaming.app.kinemasterfree,1
+com.magisto,1
+com.google.android.apps.tachyon,1
+com.kwai.bulldog,1
+com.funcamerastudio.videomaker,1
+com.avcrbt.funimate,1
+com.wondershare.filmorago,1
+com.vivashow.share.video.chat,1
+com.xvideostudio.videoeditor,1
+com.camerasideas.instashot,1
+com.gaana,1
+com.barfi.fun.videos,1
+com.hike.chat.stickers,1
+com.next.innovation.takatak,1
+com.stupeflix.replay,1
+com.sand.airdroid,1
+com.metago.astro,1
+pl.solidexplore2,1
+mobi.fileexplorer,1
+com.dewmobile.kuaiya.play,1
+com.amaze.filemanager,1
+in.mohalla.sharechat,1
+com.adobe.premiererush.videoeditor,1
+com.amazon.drive,1
+com.box.android,1
+com.microsoft.skydrive,1
+com.dropbox.android,1
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 859d2ea..0daeee6 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -806,7 +806,8 @@
+ "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
+ "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
+ "generation_modified INTEGER DEFAULT 0, xmp BLOB DEFAULT NULL,"
- + "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL)");
+ + "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL,"
+ + "_modifier INTEGER DEFAULT 0)");
db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
if (!mInternal) {
@@ -911,12 +912,13 @@
// When migrating pending or trashed files, we might need to
// rename them on disk to match new schema
if (volumePath != null) {
+ final String oldData = values.getAsString(MediaColumns.DATA);
FileUtils.computeDataFromValues(values, new File(volumePath),
/*isForFuse*/ false);
final String recomputedData = values.getAsString(MediaColumns.DATA);
- if (!Objects.equals(data, recomputedData)) {
+ if (!Objects.equals(oldData, recomputedData)) {
try {
- renameWithRetry(data, recomputedData);
+ renameWithRetry(oldData, recomputedData);
} catch (IOException e) {
// We only have one shot to migrate data, so log and
// keep marching forward
@@ -1495,6 +1497,10 @@
db.execSQL("UPDATE files SET date_modified=0 WHERE media_type=2;");
}
+ private static void updateAddModifier(SQLiteDatabase db, boolean internal) {
+ db.execSQL("ALTER TABLE files ADD COLUMN _modifier INTEGER DEFAULT 0;");
+ }
+
private static void recomputeDataValues(SQLiteDatabase db, boolean internal) {
try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA },
null, null, null, null, null, null)) {
@@ -1559,7 +1565,7 @@
static final int VERSION_R = 1115;
// Leave some gaps in database version tagging to allow R schema changes
// to go independent of S schema changes.
- static final int VERSION_S = 1201;
+ static final int VERSION_S = 1202;
static final int VERSION_LATEST = VERSION_S;
/**
@@ -1711,6 +1717,9 @@
if (fromVersion < 1201) {
updateAddVideoCodecType(db, internal);
}
+ if (fromVersion < 1202) {
+ updateAddModifier(db, internal);
+ }
// If this is the legacy database, it's not worth recomputing data
// values locally, since they'll be recomputed after the migration
@@ -1783,7 +1792,7 @@
* to retry several times before giving up.
* The retry logic is mainly added to avoid test flakiness.
*/
- private static String writeToPlaylistFileWithRetry(@NonNull File playlistFile,
+ private static void writeToPlaylistFileWithRetry(@NonNull File playlistFile,
@NonNull Playlist playlist) throws IOException {
final long start = SystemClock.elapsedRealtime();
while (true) {
@@ -1795,6 +1804,7 @@
playlistFile.getParentFile().mkdirs();
playlistFile.createNewFile();
playlist.write(playlistFile);
+ return;
} catch (IOException e) {
Log.i(TAG, "Failed to migrate playlist file, retrying " + e);
}
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index e238c8d..16d5620 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -143,6 +143,7 @@
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.Column;
+import android.provider.DeviceConfig;
import android.provider.MediaStore;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Audio.AudioColumns;
@@ -3442,6 +3443,15 @@
values.put(FileColumns.MEDIA_TYPE, mediaType);
}
+ if (isCallingPackageSelf() && values.containsKey(FileColumns._MODIFIER)) {
+ // We can't identify if the call is coming from media scan, hence
+ // we let ModernMediaScanner send FileColumns._MODIFIER value.
+ } else if (isFuseThread()) {
+ values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
+ } else {
+ values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR);
+ }
+
final long rowId;
{
if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
@@ -6155,11 +6165,6 @@
// This means either no playlist members match the query or VolumeNotFoundException
// was thrown. So we don't have anything to delete.
count = 0;
- } else if (indexes.length > 1 &&
- getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
- throw new FallbackException("Failed to update playlist",
- new IllegalStateException("Query matches more than one playlist member"),
- android.os.Build.VERSION_CODES.R);
} else {
count = playlist.removeMultiple(indexes);
}
@@ -6903,11 +6908,11 @@
boolean maybeHidden = !mNonHiddenPaths.containsKey(key);
if (maybeHidden) {
- File topNoMedia = FileUtils.getTopLevelNoMedia(new File(path));
- if (topNoMedia == null) {
+ File topNoMediaDir = FileUtils.getTopLevelNoMedia(new File(path));
+ if (topNoMediaDir == null) {
mNonHiddenPaths.put(key, 0);
} else {
- mMediaScanner.onDirectoryDirty(topNoMedia);
+ mMediaScanner.onDirectoryDirty(topNoMediaDir);
}
}
}
@@ -7775,6 +7780,28 @@
return FuseDaemon.native_is_fuse_thread();
}
+ @VisibleForTesting
+ public boolean getBooleanDeviceConfig(String key, boolean defaultValue) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, key,
+ defaultValue);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @VisibleForTesting
+ public String getStringDeviceConfig(String key, String defaultValue) {
+ final long token = Binder.clearCallingIdentity();
+ try {
+ return DeviceConfig.getString(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, key,
+ defaultValue);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
@Deprecated
private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) {
if (forWrite) {
diff --git a/src/com/android/providers/media/TranscodeHelper.java b/src/com/android/providers/media/TranscodeHelper.java
index fe602f0..18122e5 100644
--- a/src/com/android/providers/media/TranscodeHelper.java
+++ b/src/com/android/providers/media/TranscodeHelper.java
@@ -52,6 +52,7 @@
import android.os.Environment;
import android.os.Handler;
import android.os.SystemProperties;
+import android.os.UserHandle;
import android.provider.MediaStore;
import android.provider.MediaStore.Files.FileColumns;
import android.util.ArrayMap;
@@ -69,14 +70,15 @@
import com.android.providers.media.util.ForegroundThread;
import com.android.providers.media.util.SQLiteQueryBuilder;
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -102,6 +104,7 @@
private static final String TRANSCODE_DEFAULT_DEVICE_CONFIG_KEY = "transcode_default";
private static final String TRANSCODE_USER_CONTROL_SYS_PROP_KEY =
"persist.sys.fuse.transcode_user_control";
+ private static final String TRANSCODE_COMPAT_MANIFEST_KEY = "transcode_compat_manifest";
/**
* Force enable an app to support the HEVC media capability
@@ -169,16 +172,23 @@
private static final int TYPE_UPDATE = 2;
static final String DIRECTORY_CAMERA = "Camera";
+ private final Object mLock = new Object();
+ private final Context mContext;
private final MediaProvider mMediaProvider;
private final PackageManager mPackageManager;
private final MediaTranscodeManager mMediaTranscodeManager;
private final File mTranscodeDirectory;
- @GuardedBy("mTranscodingSessions")
+ @GuardedBy("mLock")
private final Map<String, TranscodingSession> mTranscodingSessions = new ArrayMap<>();
- @GuardedBy("mTranscodingSessions")
+ @GuardedBy("mLock")
private final SparseArray<CountDownLatch> mTranscodingLatches = new SparseArray<>();
private final TranscodeUiNotifier mTranscodingUiNotifier;
private final TranscodeMetrics mTranscodingMetrics;
+ @GuardedBy("mLock")
+ private final Map<String, Long> mAppCompatMediaCapabilities = new ArrayMap<>();
+
+ @GuardedBy("mLock")
+ private boolean mIsTranscodeEnabled;
private static final String[] TRANSCODE_CACHE_INFO_PROJECTION =
{FileColumns._ID, FileColumns._TRANSCODE_STATUS};
@@ -195,14 +205,18 @@
};
public TranscodeHelper(Context context, MediaProvider mediaProvider) {
+ mContext = context;
mPackageManager = context.getPackageManager();
mMediaTranscodeManager = context.getSystemService(MediaTranscodeManager.class);
mMediaProvider = mediaProvider;
- mTranscodeDirectory =
- FileUtils.buildPath(Environment.getExternalStorageDirectory(), DIRECTORY_TRANSCODE);
+ mTranscodeDirectory = new File("/storage/emulated/" + UserHandle.myUserId(),
+ DIRECTORY_TRANSCODE);
mTranscodeDirectory.mkdirs();
mTranscodingMetrics = new TranscodeMetrics();
mTranscodingUiNotifier = new TranscodeUiNotifier(context, mTranscodingMetrics);
+ mIsTranscodeEnabled = isTranscodeEnabled();
+
+ parseTranscodeCompatManifest();
}
/**
@@ -239,7 +253,7 @@
TranscodingSession session = null;
CountDownLatch latch = null;
- synchronized (mTranscodingSessions) {
+ synchronized (mLock) {
session = mTranscodingSessions.get(src);
if (session == null) {
latch = new CountDownLatch(1);
@@ -313,8 +327,10 @@
// TODO(b/173491972): Generalize to consider other file/app media capabilities beyond hevc
public boolean shouldTranscode(String path, int uid, Bundle bundle) {
- if (!getBooleanProperty(TRANSCODE_ENABLED_SYS_PROP_KEY,
- TRANSCODE_ENABLED_DEVICE_CONFIG_KEY)) {
+ boolean isTranscodeEnabled = isTranscodeEnabled();
+ updateConfigs(isTranscodeEnabled);
+
+ if (!isTranscodeEnabled) {
return false;
}
@@ -365,34 +381,23 @@
LocalCallingIdentity identity = mMediaProvider.getCachedCallingIdentityForTranscoding(uid);
final String[] callingPackages = identity.getSharedPackageNames();
- // Check allowPackages and manifest supported packages
- List<String> allowPackages = Arrays.asList(ALLOW_LIST);
+ // Check manifest supported packages and mAppCompatMediaCapabilities
+ // If we are here then the file supports HEVC, so we only check if the package is in the
+ // mAppCompatCapabilities. If it's there, we will respect that value.
for (String callingPackage : callingPackages) {
- if (allowPackages.contains(callingPackage)) {
- return false;
- } else if (checkManifestSupport(callingPackage, identity)) {
+ if (checkManifestSupport(callingPackage, identity)) {
return false;
}
- }
- // Check transcodePackages
- List<String> transcodePackages = Arrays.asList(
- SystemProperties.get("persist.sys.fuse.transcode_packages").split(","));
- for (String callingPackage : callingPackages) {
- if (transcodePackages.contains(callingPackage)) {
- return true;
+ synchronized (mLock) {
+ if (mAppCompatMediaCapabilities.containsKey(callingPackage)) {
+ return mAppCompatMediaCapabilities.get(callingPackage) == 0;
+ }
}
}
- // Check transcodeUids
- List<String> transcodeUids = Arrays.asList(
- SystemProperties.get("persist.sys.fuse.transcode_uids").split(","));
- if (transcodeUids.contains(String.valueOf(uid))) {
- return true;
- }
-
return getBooleanProperty(TRANSCODE_DEFAULT_SYS_PROP_KEY,
- TRANSCODE_DEFAULT_DEVICE_CONFIG_KEY);
+ TRANSCODE_DEFAULT_DEVICE_CONFIG_KEY, true /* defaultValue */);
}
public boolean supportsTranscode(String path) {
@@ -432,7 +437,6 @@
identity.setApplicationMediaCapabilitiesFlags(capabilitiesToFlags(capability));
return capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC);
} catch (NameNotFoundException | UnsupportedOperationException e) {
- Log.d(TAG, "No valid media capability defined for " + packageName, e);
return false;
}
}
@@ -461,18 +465,15 @@
return flags;
}
- private boolean getBooleanProperty(String sysPropKey, String deviceConfigKey) {
+ private boolean getBooleanProperty(String sysPropKey, String deviceConfigKey,
+ boolean defaultValue) {
// If the user wants to override the default, respect that; otherwise use the DeviceConfig
// which is filled with the values sent from server.
- // TODO(b/169327180): Until we figure out a nice way to fix tests for using DeviceConfig,
- // we are going to read the sysPropKey only. So, simply commenting out the code for now.
- // if (SystemProperties.getBoolean(TRANSCODE_USER_CONTROL_SYS_PROP_KEY, false)) {
- return SystemProperties.getBoolean(sysPropKey, false);
- // }
+ if (SystemProperties.getBoolean(TRANSCODE_USER_CONTROL_SYS_PROP_KEY, false)) {
+ return SystemProperties.getBoolean(sysPropKey, defaultValue);
+ }
- // return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
- // deviceConfigKey,
- // false);
+ return mMediaProvider.getBooleanDeviceConfig(deviceConfigKey, defaultValue);
}
private Pair<Long, Integer> getTranscodeCacheInfoFromDB(String path) {
@@ -485,7 +486,7 @@
}
public boolean isTranscodeFileCached(String path, String transcodePath) {
- if (SystemProperties.getBoolean("fuse.sys.disable_transcode_cache", false)) {
+ if (SystemProperties.getBoolean("sys.fuse.disable_transcode_cache", false)) {
// Caching is disabled. Hence, delete the cached transcode file.
return false;
}
@@ -604,7 +605,7 @@
private void finishTranscodingResult(int uid, String src, TranscodingSession session,
CountDownLatch latch) {
- synchronized (mTranscodingSessions) {
+ synchronized (mLock) {
latch.countDown();
session.cancel();
mTranscodingSessions.remove(src);
@@ -669,6 +670,128 @@
return qb.query(getDatabaseHelperForUri(uri), projection, extras, null);
}
+ private boolean isTranscodeEnabled() {
+ return getBooleanProperty(TRANSCODE_ENABLED_SYS_PROP_KEY,
+ TRANSCODE_ENABLED_DEVICE_CONFIG_KEY, true /* defaultValue */);
+ }
+
+ private void updateConfigs(boolean transcodeEnabled) {
+ synchronized (mLock) {
+ boolean isTranscodeEnabledChanged = transcodeEnabled != mIsTranscodeEnabled;
+ boolean isDebug = SystemProperties.getBoolean("sys.fuse.transcode_debug", false);
+
+ if (isTranscodeEnabledChanged || isDebug) {
+ Log.i(TAG, "Reloading transcode configs. transcodeEnabled: " + transcodeEnabled
+ + ". lastTranscodeEnabled: " + mIsTranscodeEnabled + ". isDebug: "
+ + isDebug);
+
+ mIsTranscodeEnabled = transcodeEnabled;
+ parseTranscodeCompatManifest();
+ }
+ }
+ }
+
+ private void parseTranscodeCompatManifest() {
+ synchronized (mLock) {
+ // Clear the transcode_compat manifest before parsing. If transcode is disabled,
+ // nothing will be parsed, effectively leaving the compat manifest empty.
+ mAppCompatMediaCapabilities.clear();
+ if (!mIsTranscodeEnabled) {
+ return;
+ }
+
+ if (!parseTranscodeCompatManifestFromDeviceConfigLocked()) {
+ Log.i(TAG, "Failed parsing transcode compat manifest from device config "
+ + "attempting resource...");
+ parseTranscodeCompatManifestFromResourceLocked();
+ }
+ }
+ }
+
+ /** @return {@code true} if the manifest was parsed successfully, {@code false} otherwise */
+ private boolean parseTranscodeCompatManifestFromDeviceConfigLocked() {
+ final String[] manifest = mMediaProvider.getStringDeviceConfig(
+ TRANSCODE_COMPAT_MANIFEST_KEY, "").split(",");
+
+ if (manifest.length == 0) {
+ Log.i(TAG, "Empty device config transcode compat manifest");
+ return false;
+ }
+ if ((manifest.length % 2) != 0) {
+ Log.w(TAG, "Uneven number of items in device config transcode compat manifest");
+ return false;
+ }
+
+ String packageName = "";
+ Long packageCompatValue;
+ int i = 0;
+ while (i < manifest.length - 1) {
+ try {
+ packageName = manifest[i++];
+ packageCompatValue = Long.valueOf(manifest[i++]);
+ synchronized (mLock) {
+ // Lock is already held, explicitly hold again to make error prone happy
+ mAppCompatMediaCapabilities.put(packageName, packageCompatValue);
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Failed to parse media capability from device config for package: "
+ + packageName, e);
+ }
+ }
+
+ synchronized (mLock) {
+ // Lock is already held, explicitly hold again to make error prone happy
+ int size = mAppCompatMediaCapabilities.size();
+ Log.i(TAG, "Parsed " + size + " packages from device config");
+ return size != 0;
+ }
+ }
+
+ /** @return {@code true} if the manifest was parsed successfully, {@code false} otherwise */
+ private boolean parseTranscodeCompatManifestFromResourceLocked() {
+ InputStream inputStream = mContext.getResources().openRawResource(
+ R.raw.transcode_compat_manifest);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+ try {
+ while(reader.ready()) {
+ String line = reader.readLine();
+ String packageName = "";
+ Long packageCompatValue;
+
+ if (line == null) {
+ Log.w(TAG, "Unexpected null line while parsing transcode compat manifest");
+ continue;
+ }
+
+ String[] lineValues = line.split(",");
+ if (lineValues.length != 2) {
+ Log.w(TAG, "Failed to read line while parsing transcode compat manifest");
+ continue;
+ }
+ try {
+ packageName = lineValues[0];
+ packageCompatValue = Long.valueOf(lineValues[1]);
+ synchronized (mLock) {
+ // Lock is already held, explicitly hold again to make error prone happy
+ mAppCompatMediaCapabilities.put(packageName, packageCompatValue);
+ }
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Failed to parse media capability from resource for package: "
+ + packageName, e);
+ }
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read transcode compat manifest", e);
+ }
+
+ synchronized (mLock) {
+ // Lock is already held, explicitly hold again to make error prone happy
+ int size = mAppCompatMediaCapabilities.size();
+ Log.i(TAG, "Parsed " + size + " packages from resource");
+ return size != 0;
+ }
+ }
+
private void logEvent(String event, @Nullable TranscodingSession session) {
Log.d(TAG, event + (session == null ? "" : session));
}
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index e408033..5151be9 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -150,12 +150,17 @@
// TODO: deprecate playlist editing
// TODO: deprecate PARENT column, since callers can't see directories
- @GuardedBy("sDateFormat")
- private static final SimpleDateFormat sDateFormat;
+ @GuardedBy("S_DATE_FORMAT")
+ private static final SimpleDateFormat S_DATE_FORMAT;
+ @GuardedBy("S_DATE_FORMAT_WITH_MILLIS")
+ private static final SimpleDateFormat S_DATE_FORMAT_WITH_MILLIS;
static {
- sDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
- sDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ S_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+ S_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+ S_DATE_FORMAT_WITH_MILLIS = new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS");
+ S_DATE_FORMAT_WITH_MILLIS.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private static final int BATCH_SIZE = 32;
@@ -163,7 +168,8 @@
// |excludeDirs * 2| < 1000 which is the max SQL expression size
// Because we add |excludeDir| and |excludeDir/| in the SQL expression to match dir and subdirs
// See SQLITE_MAX_EXPR_DEPTH in sqlite3.c
- private static final int MAX_EXCLUDE_DIRS = 450;
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final int MAX_EXCLUDE_DIRS = 450;
private static final Pattern PATTERN_VISIBLE = Pattern.compile(
"(?i)^/storage/[^/]+(?:/[0-9]+)?$");
@@ -338,6 +344,12 @@
* indicates that one or more of the current file's parents is a hidden directory.
*/
private int mHiddenDirCount;
+ /**
+ * Indicates if the nomedia directory tree is dirty. When a nomedia directory is dirty, we
+ * mark the top level nomedia as dirty. Hence if one of the sub directory in the nomedia
+ * directory is dirty, we consider the whole top level nomedia directory tree as dirty.
+ */
+ private boolean mIsDirectoryTreeDirty;
public Scan(File root, int reason, @Nullable String ownerPackage)
throws FileNotFoundException {
@@ -620,7 +632,13 @@
}
synchronized (mPendingCleanDirectories) {
- if (FileUtils.isDirectoryDirty(dir.toFile())) {
+ if (mIsDirectoryTreeDirty) {
+ // Directory tree is dirty, continue scanning subtree.
+ } else if (FileUtils.isDirectoryDirty(FileUtils.getTopLevelNoMedia(dir.toFile()))) {
+ // Track the directory dirty status for directory tree in mIsDirectoryDirty.
+ // This removes additional dirty state check for subdirectories of nomedia
+ // directory.
+ mIsDirectoryTreeDirty = true;
mPendingCleanDirectories.add(dir.toFile().getPath());
} else {
Log.d(TAG, "Skipping preVisitDirectory " + dir.toFile());
@@ -690,7 +708,7 @@
queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
final String[] projection = new String[] {FileColumns._ID, FileColumns.DATE_MODIFIED,
FileColumns.SIZE, FileColumns.MIME_TYPE, FileColumns.MEDIA_TYPE,
- FileColumns.IS_PENDING};
+ FileColumns.IS_PENDING, FileColumns._MODIFIER};
final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(realFile.getName());
// If IS_PENDING is set by FUSE, we should scan the file and update IS_PENDING to zero.
@@ -705,6 +723,8 @@
final String mimeType = c.getString(3);
final int mediaType = c.getInt(4);
isPendingFromFuse &= c.getInt(5) != 0;
+ final boolean isScanned =
+ c.getInt(6) == FileColumns._MODIFIER_MEDIA_SCAN;
// Remember visiting this existing item, even if we skipped
// due to it being unchanged; this is needed so we don't
@@ -722,7 +742,7 @@
mimeType.equalsIgnoreCase(actualMimeType);
final boolean sameMediaType = (actualMediaType == mediaType);
final boolean isSame = sameTime && sameSize && sameMediaType && sameMimeType
- && !isPendingFromFuse;
+ && !isPendingFromFuse && isScanned;
if (attrs.isDirectory() || isSame) {
if (LOGV) Log.v(TAG, "Skipping unchanged " + file);
return FileVisitResult.CONTINUE;
@@ -741,6 +761,7 @@
Trace.endSection();
}
if (op != null) {
+ op.withValue(FileColumns._MODIFIER, FileColumns._MODIFIER_MEDIA_SCAN);
// Add owner package name to new insertions when package name is provided.
if (op.build().isInsert() && !attrs.isDirectory() && mOwnerPackage != null) {
op.withValue(MediaColumns.OWNER_PACKAGE_NAME, mOwnerPackage);
@@ -777,10 +798,14 @@
// Now that we're finished scanning this directory, release lock to
// allow other parallel scans to proceed
releaseDirectoryLock(dir);
- synchronized (mPendingCleanDirectories) {
- if (mPendingCleanDirectories.remove(dir.toFile().getPath())) {
- // If |dir| is still clean, then persist
- FileUtils.setDirectoryDirty(dir.toFile(), false /* isDirty */);
+
+ if (mIsDirectoryTreeDirty) {
+ synchronized (mPendingCleanDirectories) {
+ if (mPendingCleanDirectories.remove(dir.toFile().getPath())) {
+ // If |dir| is still clean, then persist
+ FileUtils.setDirectoryDirty(dir.toFile(), false /* isDirty */);
+ mIsDirectoryTreeDirty = false;
+ }
}
}
return FileVisitResult.CONTINUE;
@@ -1441,15 +1466,31 @@
static @NonNull Optional<Long> parseOptionalDate(@Nullable String date) {
if (TextUtils.isEmpty(date)) return Optional.empty();
try {
- synchronized (sDateFormat) {
- final long value = sDateFormat.parse(date).getTime();
- return (value > 0) ? Optional.of(value) : Optional.empty();
+ synchronized (S_DATE_FORMAT_WITH_MILLIS) {
+ return parseDateWithFormat(date, S_DATE_FORMAT_WITH_MILLIS);
}
} catch (ParseException e) {
+ // Log and try without millis as well
+ Log.d(TAG, String.format(
+ "Parsing date with millis failed for [%s]. We will retry without millis",
+ date));
+ }
+ try {
+ synchronized (S_DATE_FORMAT) {
+ return parseDateWithFormat(date, S_DATE_FORMAT);
+ }
+ } catch (ParseException e) {
+ Log.d(TAG, String.format("Parsing date without millis failed for [%s]", date));
return Optional.empty();
}
}
+ private static Optional<Long> parseDateWithFormat(
+ @Nullable String date, SimpleDateFormat dateFormat) throws ParseException {
+ final long value = dateFormat.parse(date).getTime();
+ return (value > 0) ? Optional.of(value) : Optional.empty();
+ }
+
@VisibleForTesting
static @NonNull Optional<Integer> parseOptionalYear(@Nullable String value) {
final Optional<String> parsedValue = parseOptional(value);
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 7777ac3..0d6494b 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -1420,17 +1420,17 @@
* Returns {@code null} if there's no .nomedia in hierarchy
*/
public static File getTopLevelNoMedia(@NonNull File file) {
- File topNoMedia = null;
+ File topNoMediaDir = null;
File parent = file;
while (parent != null) {
File nomedia = new File(parent, ".nomedia");
if (nomedia.exists()) {
- topNoMedia = nomedia;
+ topNoMediaDir = parent;
}
parent = parent.getParentFile();
}
- return topNoMedia;
+ return topNoMediaDir;
}
}
diff --git a/tests/Android.bp b/tests/Android.bp
index cab61cf..8054a05 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -17,9 +17,11 @@
"main_res",
"res",
],
+
srcs: [
":framework-mediaprovider-sources",
":mediaprovider-sources",
+ ":mediaprovider-testutils",
"src/**/*.java",
],
@@ -50,3 +52,8 @@
],
},
}
+
+filegroup {
+ name: "mediaprovider-testutils",
+ srcs: ["utils/**/*.java"],
+}
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 26a0a0b..2b35634 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -10,6 +10,7 @@
srcs: [
"src/**/*.java",
+ ":mediaprovider-testutils",
],
libs: [
diff --git a/tests/client/src/com/android/providers/media/client/PerformanceTest.java b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
index 21cdb8b..867d459 100644
--- a/tests/client/src/com/android/providers/media/client/PerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
@@ -36,6 +36,8 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.providers.media.tests.utils.Timer;
+
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -281,6 +283,7 @@
renameFilesTimer.dumpResults();
deleteTimer.dumpResults();
}
+
private void doDirOperations(int size, Timer createTimer, Timer readTimer,
Timer renameDirTimer, Timer renameFilesTimer, Timer deleteTimer) throws Exception {
createTimer.start();
@@ -337,52 +340,6 @@
return new HashSet<>(uris);
}
- /**
- * Timer that can be started/stopped with nanosecond accuracy, and later
- * averaged based on the number of times it was cycled.
- */
- static class Timer {
- private final String name;
- private int count;
- private long duration;
- private long start;
-
- public Timer(String name) {
- this.name = name;
- }
-
- public void start() {
- if (start != 0) {
- throw new IllegalStateException();
- } else {
- start = SystemClock.elapsedRealtimeNanos();
- }
- }
-
- public void stop() {
- if (start == 0) {
- throw new IllegalStateException();
- } else {
- duration += (SystemClock.elapsedRealtimeNanos() - start);
- start = 0;
- count++;
- }
- }
-
- public long getAverageDurationMillis() {
- return TimeUnit.MILLISECONDS.convert(duration / count, TimeUnit.NANOSECONDS);
- }
-
- public void dumpResults() {
- final long duration = getAverageDurationMillis();
- Log.v(TAG, name + ": " + duration + "ms");
-
- final Bundle results = new Bundle();
- results.putLong(name, duration);
- InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
- }
- }
-
private static class Timers {
public final Timer actionInsert = new Timer("action_insert");
public final Timer actionUpdate = new Timer("action_update");
diff --git a/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java b/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
index 5df8423..c4ea8e8 100644
--- a/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
@@ -37,7 +37,7 @@
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
-import com.android.providers.media.client.PerformanceTest.Timer;
+import com.android.providers.media.tests.utils.Timer;
import org.junit.After;
import org.junit.Before;
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index 3d28820..d938180 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -85,6 +85,16 @@
public boolean isFuseThread() {
return asFuseThread;
}
+
+ @Override
+ public boolean getBooleanDeviceConfig(String key, boolean defaultValue) {
+ return defaultValue;
+ }
+
+ @Override
+ public String getStringDeviceConfig(String key, String defaultValue) {
+ return defaultValue;
+ }
};
mProvider.attachInfo(this, info);
mResolver.addProvider(MediaStore.AUTHORITY, mProvider);
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index 9b7f0d2..e7e890b 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -18,6 +18,7 @@
import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
import static com.android.providers.media.scan.MediaScannerTest.stage;
+import static com.android.providers.media.scan.ModernMediaScanner.MAX_EXCLUDE_DIRS;
import static com.android.providers.media.scan.ModernMediaScanner.shouldScanPathAndIsPathHidden;
import static com.android.providers.media.scan.ModernMediaScanner.isFileAlbumArt;
import static com.android.providers.media.scan.ModernMediaScanner.parseOptional;
@@ -36,6 +37,8 @@
import static com.android.providers.media.util.FileUtils.isDirectoryHidden;
import static com.android.providers.media.util.FileUtils.isFileHidden;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -68,6 +71,7 @@
import com.android.providers.media.R;
import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
+import com.android.providers.media.tests.utils.Timer;
import com.android.providers.media.util.FileUtils;
import com.google.common.io.ByteStreams;
@@ -90,6 +94,11 @@
// TODO: scan directory-vs-files and confirm identical results
private static final String TAG = "ModernMediaScannerTest";
+ /**
+ * Number of times we should repeat an operation to get an average/max.
+ */
+ private static final int COUNT_REPEAT = 5;
+
private File mDir;
private Context mIsolatedContext;
@@ -228,7 +237,12 @@
@Test
public void testParseOptionalDate() throws Exception {
- assertEquals(1577836800000L, (long) parseOptionalDate("20200101T000000").get());
+ assertThat(parseOptionalDate("20200101T000000")).isEqualTo(Optional.of(1577836800000L));
+ assertThat(parseOptionalDate("20200101T211205")).isEqualTo(Optional.of(1577913125000L));
+ assertThat(parseOptionalDate("20200101T211205.000Z"))
+ .isEqualTo(Optional.of(1577913125000L));
+ assertThat(parseOptionalDate("20200101T211205.123Z"))
+ .isEqualTo(Optional.of(1577913125123L));
}
@Test
@@ -731,12 +745,12 @@
@Test
public void testScan_Nomedia_Dir() throws Exception {
- final File red = new File(mDir, "red");
- final File blue = new File(mDir, "blue");
- red.mkdirs();
- blue.mkdirs();
- stage(R.raw.test_image, new File(red, "red.jpg"));
- stage(R.raw.test_image, new File(blue, "blue.jpg"));
+ final File redDir = new File(mDir, "red");
+ final File blueDir = new File(mDir, "blue");
+ redDir.mkdirs();
+ blueDir.mkdirs();
+ stage(R.raw.test_image, new File(redDir, "red.jpg"));
+ stage(R.raw.test_image, new File(blueDir, "blue.jpg"));
mModern.scanDirectory(mDir, REASON_UNKNOWN);
@@ -744,7 +758,7 @@
assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
// Hide one directory, rescan, and confirm hidden
- final File redNomedia = new File(red, ".nomedia");
+ final File redNomedia = new File(redDir, ".nomedia");
redNomedia.createNewFile();
mModern.scanDirectory(mDir, REASON_UNKNOWN);
assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
@@ -756,6 +770,35 @@
}
@Test
+ public void testScan_MaxExcludeNomediaDirs_DoesNotThrowException() throws Exception {
+ // Create MAX_EXCLUDE_DIRS + 50 nomedia dirs in mDir
+ // (Need to add 50 as MAX_EXCLUDE_DIRS is a safe limit;
+ // 499 would have been too close to the exception limit)
+ // Mark them as non-dirty so that they are excluded from scans
+ for (int i = 0 ; i < (MAX_EXCLUDE_DIRS + 50) ; i++) {
+ createCleanNomediaDir(mDir);
+ }
+
+ final File redDir = new File(mDir, "red");
+ redDir.mkdirs();
+ stage(R.raw.test_image, new File(redDir, "red.jpg"));
+
+ assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ mModern.scanDirectory(mDir, REASON_UNKNOWN);
+ assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ }
+
+ private void createCleanNomediaDir(File dir) throws Exception {
+ final File nomediaDir = new File(dir, "test_" + System.nanoTime());
+ nomediaDir.mkdirs();
+ final File nomedia = new File(nomediaDir, ".nomedia");
+ nomedia.createNewFile();
+
+ FileUtils.setDirectoryDirty(nomediaDir, false);
+ assertThat(FileUtils.isDirectoryDirty(nomediaDir)).isFalse();
+ }
+
+ @Test
public void testScan_Nomedia_File() throws Exception {
final File image = new File(mDir, "image.jpg");
final File nomedia = new File(mDir, ".nomedia");
@@ -1039,12 +1082,56 @@
mModern.scanDirectory(mDir, REASON_UNKNOWN);
- try (Cursor cursor = mIsolatedResolver
- .query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
- new String[] { MediaColumns.XMP }, null, null, null)) {
- assertEquals(1, cursor.getCount());
- cursor.moveToFirst();
- assertEquals(0, cursor.getBlob(0).length);
+ try (Cursor cursor = mIsolatedResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ new String[] { MediaColumns.XMP }, null, null, null)) {
+ assertEquals(1, cursor.getCount());
+ cursor.moveToFirst();
+ assertEquals(0, cursor.getBlob(0).length);
}
}
+
+ @Test
+ public void testNoOpScan_NoMediaDirs() throws Exception {
+ File nomedia = new File(mDir, ".nomedia");
+ assertThat(nomedia.createNewFile()).isTrue();
+ for (int i = 0; i < 100; i++) {
+ File file = new File(mDir, "file_" + System.nanoTime());
+ assertThat(file.createNewFile()).isTrue();
+ }
+ Timer firstDirScan = new Timer("firstDirScan");
+ firstDirScan.start();
+ // Time taken : preVisitDirectory + 100 visitFiles
+ mModern.scanDirectory(mDir, REASON_UNKNOWN);
+ firstDirScan.stop();
+ firstDirScan.dumpResults();
+
+ // Time taken : preVisitDirectory
+ Timer noOpDirScan = new Timer("noOpDirScan");
+ for (int i = 0 ; i < COUNT_REPEAT ; i++) {
+ noOpDirScan.start();
+ mModern.scanDirectory(mDir, REASON_UNKNOWN);
+ noOpDirScan.stop();
+ }
+ noOpDirScan.dumpResults();
+ assertThat(noOpDirScan.getMaxDurationMillis()).isLessThan(
+ firstDirScan.getMaxDurationMillis());
+
+ // renaming directory for non-M_E_S apps does a scan of the directory as well;
+ // so subsequent scans should be noOp as the directory is not dirty.
+ File renamedTestDir = new File(mIsolatedContext.getExternalMediaDirs()[0],
+ "renamed_test_" + System.nanoTime());
+ assertThat(mDir.renameTo(renamedTestDir)).isTrue();
+
+ Timer renamedDirScan = new Timer("renamedDirScan");
+ renamedDirScan.start();
+ // Time taken : preVisitDirectory
+ mModern.scanDirectory(renamedTestDir, REASON_UNKNOWN);
+ renamedDirScan.stop();
+ renamedDirScan.dumpResults();
+ assertThat(renamedDirScan.getMaxDurationMillis()).isLessThan(
+ firstDirScan.getMaxDurationMillis());
+
+ // This is essential for folder cleanup in tearDown
+ mDir = renamedTestDir;
+ }
}
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 35a6017..2e85421 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -730,7 +730,7 @@
File nomedia = new File(dirInDownload, ".nomedia");
assertTrue(nomedia.createNewFile());
- assertEquals(nomedia, FileUtils.getTopLevelNoMedia(new File(dirInDownload, "foo")));
+ assertEquals(dirInDownload, FileUtils.getTopLevelNoMedia(new File(dirInDownload, "foo")));
}
@Test
@@ -744,7 +744,7 @@
File nomedia = new File(dirInTopDirInDownload, ".nomedia");
assertTrue(nomedia.createNewFile());
- assertEquals(topNomedia,
+ assertEquals(topDirInDownload,
FileUtils.getTopLevelNoMedia(new File(dirInTopDirInDownload, "foo")));
}
diff --git a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
index 236f626..003f957 100644
--- a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
+++ b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
@@ -35,11 +35,11 @@
import android.media.ApplicationMediaCapabilities;
import android.media.MediaFormat;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
-import android.system.Os;
import androidx.test.runner.AndroidJUnit4;
@@ -50,6 +50,7 @@
import java.util.List;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
@@ -85,12 +86,15 @@
@Before
public void setUp() throws Exception {
+ // TODO(b/171789917): Cuttlefish doesn't support transcoding yet
+ Assume.assumeFalse(Build.MODEL.contains("Cuttlefish"));
+
TranscodeTestUtils.pollForExternalStorageState();
TranscodeTestUtils.grantPermission(getContext().getPackageName(),
Manifest.permission.READ_EXTERNAL_STORAGE);
TranscodeTestUtils.pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, true);
TranscodeTestUtils.enableSeamlessTranscoding();
- TranscodeTestUtils.disableTranscodingForAllUids();
+ TranscodeTestUtils.disableTranscodingForAllPackages();
}
@After
@@ -110,7 +114,7 @@
ParcelFileDescriptor pfdOriginal = open(modernFile, false);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
ParcelFileDescriptor pfdTranscoded = open(modernFile, false);
assertFileContent(modernFile, modernFile, pfdOriginal, pfdTranscoded, false);
@@ -139,7 +143,7 @@
}
ParcelFileDescriptor pfdOriginal1 = open(modernFile, false);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
for (File file : noTranscodeFiles) {
pfdOriginal1.seekTo(0);
@@ -164,7 +168,7 @@
try {
TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
ParcelFileDescriptor pfdTranscoded1 = open(modernFile, false);
ParcelFileDescriptor pfdTranscoded2 = open(modernFile, false);
@@ -186,7 +190,7 @@
ParcelFileDescriptor pfdOriginal = open(uri, false, null /* bundle */);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
ParcelFileDescriptor pfdTranscoded = open(uri, false, null /* bundle */);
@@ -217,7 +221,7 @@
ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
for (int i = 0; i < noTranscodeUris.size(); i++) {
pfdOriginal1.seekTo(0);
@@ -244,7 +248,7 @@
try {
Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
ParcelFileDescriptor pfdTranscoded1 = open(uri, false, null /* bundle */);
ParcelFileDescriptor pfdTranscoded2 = open(uri, false, null /* bundle */);
@@ -265,12 +269,12 @@
try {
TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTrue(modernFile.delete());
assertFalse(modernFile.exists());
- TranscodeTestUtils.disableTranscodingForAllUids();
+ TranscodeTestUtils.disableTranscodingForAllPackages();
assertFalse(modernFile.exists());
} finally {
@@ -289,13 +293,13 @@
try {
TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTrue(modernFile.renameTo(destFile));
assertTrue(destFile.exists());
assertFalse(modernFile.exists());
- TranscodeTestUtils.disableTranscodingForAllUids();
+ TranscodeTestUtils.disableTranscodingForAllPackages();
assertTrue(destFile.exists());
assertFalse(modernFile.exists());
@@ -317,7 +321,7 @@
assertTranscode(modernFile, false);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTranscode(modernFile, true);
} finally {
@@ -334,7 +338,7 @@
File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
try {
TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTranscode(modernFile, true);
assertTranscode(modernFile, false);
@@ -353,7 +357,7 @@
File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
try {
Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTranscode(uri, true);
assertTranscode(uri, false);
@@ -373,7 +377,7 @@
File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
try {
Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTranscode(uri, true);
assertTranscode(modernFile, false);
@@ -392,7 +396,7 @@
File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
try {
Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTranscode(modernFile, true);
assertTranscode(uri, false);
@@ -411,7 +415,7 @@
File destFile = new File(DIR_CAMERA, "renamed_" + HEVC_FILE_NAME);
try {
TranscodeTestUtils.stageHEVCVideoFile(modernFile);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
assertTranscode(modernFile, true);
@@ -432,7 +436,7 @@
ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
Bundle bundle = new Bundle();
bundle.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, true);
@@ -452,7 +456,7 @@
ParcelFileDescriptor pfdOriginal = open(uri, false, null /* bundle */);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
Bundle bundle = new Bundle();
bundle.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, false);
@@ -472,7 +476,7 @@
ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
Bundle bundle = new Bundle();
ApplicationMediaCapabilities capabilities =
@@ -495,7 +499,7 @@
ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
Bundle bundle = new Bundle();
ApplicationMediaCapabilities capabilities =
@@ -518,7 +522,7 @@
ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
- TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+ TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
Bundle bundle = new Bundle();
ApplicationMediaCapabilities capabilities =
diff --git a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java
index b618b8c..682dd1b 100644
--- a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java
+++ b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java
@@ -108,24 +108,25 @@
}
public static void enableSeamlessTranscoding() throws Exception {
+ // This is required so that MediaProvider handles device config changes
+ executeShellCommand("setprop sys.fuse.transcode_debug true");
+ // This is required so that setprop changes take precedence over device_config changes
executeShellCommand("setprop persist.sys.fuse.transcode_user_control true");
executeShellCommand("setprop persist.sys.fuse.transcode_enabled true");
+ executeShellCommand("setprop persist.sys.fuse.transcode_default false");
}
public static void disableSeamlessTranscoding() throws Exception {
+ executeShellCommand("setprop sys.fuse.transcode_debug false");
executeShellCommand("setprop persist.sys.fuse.transcode_user_control true");
executeShellCommand("setprop persist.sys.fuse.transcode_enabled false");
+ executeShellCommand("setprop persist.sys.fuse.transcode_default false");
+ disableTranscodingForAllPackages();
}
- public static void enableTranscodingForUid(int uid) throws IOException {
- final String command = "setprop persist.sys.fuse.transcode_uids "
- + String.valueOf(uid);
- executeShellCommand(command);
- }
-
- public static void enableTranscodingForPackage(String packageName) throws IOException {
- final String command = "setprop persist.sys.fuse.transcode_packages " + packageName;
- executeShellCommand(command);
+ public static void enableTranscodingForPackage(String packageName) throws Exception {
+ executeShellCommand("device_config put storage_native_boot transcode_compat_manifest "
+ + packageName + ",0");
}
public static void forceEnableAppCompatHevc(String packageName) throws IOException {
@@ -143,11 +144,8 @@
executeShellCommand(command);
}
- public static void disableTranscodingForAllUids() throws IOException {
- String command = "setprop persist.sys.fuse.transcode_uids -1";
- executeShellCommand(command);
- command = "setprop persist.sys.fuse.transcode_packages -1";
- executeShellCommand(command);
+ public static void disableTranscodingForAllPackages() throws IOException {
+ executeShellCommand("device_config delete storage_native_boot transcode_compat_manifest");
}
/**
diff --git a/tests/utils/src/com/android/providers/media/tests/utils/Timer.java b/tests/utils/src/com/android/providers/media/tests/utils/Timer.java
new file mode 100644
index 0000000..6d8caca
--- /dev/null
+++ b/tests/utils/src/com/android/providers/media/tests/utils/Timer.java
@@ -0,0 +1,82 @@
+/**
+ * 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.tests.utils;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * General Timer function for MediaProvider tests.
+ */
+public class Timer {
+ private static final String TAG = "Timer";
+
+ private final String name;
+ private int count;
+ private long duration;
+ private long start;
+ private long maxRunDuration = 0;
+
+ public Timer(String name) {
+ this.name = name;
+ }
+
+ public void start() {
+ if (start != 0) {
+ throw new IllegalStateException();
+ } else {
+ start = SystemClock.elapsedRealtimeNanos();
+ }
+ }
+
+ public void stop() {
+ long currentRunDuration = 0;
+ if (start == 0) {
+ throw new IllegalStateException();
+ } else {
+ currentRunDuration = (SystemClock.elapsedRealtimeNanos() - start);
+ maxRunDuration = (currentRunDuration > maxRunDuration) ? currentRunDuration :
+ maxRunDuration;
+ duration += currentRunDuration;
+ start = 0;
+ count++;
+ }
+ }
+
+ public long getAverageDurationMillis() {
+ return TimeUnit.MILLISECONDS.convert(duration / count, TimeUnit.NANOSECONDS);
+ }
+
+ public long getMaxDurationMillis() {
+ return TimeUnit.MILLISECONDS.convert(maxRunDuration, TimeUnit.NANOSECONDS);
+ }
+
+ public void dumpResults() {
+ final long duration = getAverageDurationMillis();
+ Log.v(TAG, name + ": " + duration + "ms");
+
+ final Bundle results = new Bundle();
+ results.putLong(name, duration);
+ InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
+ }
+}
diff --git a/transcode.sh b/transcode.sh
new file mode 100644
index 0000000..b36eccc
--- /dev/null
+++ b/transcode.sh
@@ -0,0 +1,17 @@
+# For extracting a transcode_compat_manifest from a csv file in the format
+# package_name,hevc_support,slow_motion_support,hdr_10_support,hdr_10_plus_support,hdr_hlg_support,hdr_dolby_vision_support,hevc_support_shift,slow_motion_support_shift,hdr_10_support_shift,hd_10_plus_support_shift,hdr_hlg_support_shift,hdr_dolby_vision_support_shift,media_capability
+# com.foo,1,0,0,0,0,0,1,0,0,0,0,0,1
+# ....
+function transcode_compat_manifest() {
+ # Cat file
+ # Remove CLRF (DOS format)
+ # Remove first line (header)
+ # Extract first and last columns in each line
+ # For device_config convert new lines (\n) to comma(,)
+ # For device_config remove trailing comma(,)
+ case "$1" in
+ -r) cat $2 | tr -d '\r' | sed 1d | awk -F "," '{new_var=$1","$NF; print new_var}';;
+ -d) cat $2 | tr -d '\r' | sed 1d | awk -F "," '{new_var=$1","$NF; print new_var}' | tr '\n' ',' | sed 's/,$//g';;
+ *) "Enter '-d' for device_config, '-r' for resource";;
+ esac
+}