Skip to content

Commit

Permalink
Reimplement media scanner in managed code.
Browse files Browse the repository at this point in the history
The platform media scanner has served faithfully for many years, but
it's become tedious to test and maintain, mainly due to the way it
weaves obscurely between managed and native code.

This modern reimplementation is bug-faithful to the legacy scanner,
with tests to confirm that media is scanned identically.  Future CLs
will flesh out the remaining features and add additional tests to
confirm behaviors around hiding/showing directories.

Eventually this will also extract XMP metadata from images.

Current benchmarks show legacy performance:
    Scan Initial: 5175ms [220, 1, 94, 94]
    Scan No-op: 333ms [0, 0, 0, 0]
    Scan Clean: 111ms [-191, 0, -93, -93]

Compared with similar modern performance:
    Scan Initial: 5822ms [207, 1, 94, 94]
    Scan No-op: 331ms [0, 0, 0, 0]
    Scan Clean: 170ms [-191, 0, -93, -93]

Bug: 120791890, 122263824, 120862852
Test: atest com.android.providers.media.MediaScannerTest
Change-Id: I938b18f7c6cd5309a6b9c4eb97e635d6151b4ead
  • Loading branch information
jsharkey committed Feb 6, 2019
1 parent 8aca4c7 commit 10b4d8d
Show file tree
Hide file tree
Showing 12 changed files with 970 additions and 43 deletions.
4 changes: 4 additions & 0 deletions Android.bp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ android_app {
"src/**/*.java",
],

optimize: {
proguard_flags_files: ["proguard.flags"],
},

platform_apis: true,

certificate: "media",
Expand Down
7 changes: 7 additions & 0 deletions proguard.flags
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-keep class com.android.providers.media.scan.MediaScanner {
*;
}

-keep class * implements com.android.providers.media.scan.MediaScanner {
*;
}
37 changes: 12 additions & 25 deletions src/com/android/providers/media/MediaProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
import android.graphics.drawable.Icon;
import android.media.ExifInterface;
import android.media.MediaFile;
import android.media.MediaScanner;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.media.ThumbnailUtils;
Expand Down Expand Up @@ -125,6 +124,7 @@
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.providers.media.scan.MediaScanner;

import libcore.io.IoUtils;
import libcore.net.MimeUtils;
Expand Down Expand Up @@ -2131,7 +2131,7 @@ private long insertFile(DatabaseHelper helper, int match, Uri uri, ContentValues
// to use that exact type, so don't override it based on mimetype
if (!values.containsKey(FileColumns.MEDIA_TYPE) &&
mediaType == FileColumns.MEDIA_TYPE_NONE &&
!MediaScanner.isNoMediaPath(path)) {
!android.media.MediaScanner.isNoMediaPath(path)) {
if (MediaFile.isAudioMimeType(mimeType)) {
mediaType = FileColumns.MEDIA_TYPE_AUDIO;
} else if (MediaFile.isVideoMimeType(mimeType)) {
Expand Down Expand Up @@ -2764,7 +2764,7 @@ public void run() {
private void hidePath(String volumeName, DatabaseHelper helper, SQLiteDatabase db,
String path) {
// a new nomedia path was added, so clear the media paths
MediaScanner.clearMediaPathCache(true /* media */, false /* nomedia */);
android.media.MediaScanner.clearMediaPathCache(true /* media */, false /* nomedia */);
File nomedia = new File(path);
String hiddenroot = nomedia.isDirectory() ? path : nomedia.getParent();

Expand Down Expand Up @@ -2806,7 +2806,7 @@ private void hidePath(String volumeName, DatabaseHelper helper, SQLiteDatabase d
*/
private void processRemovedNoMediaPath(final String path) {
// a nomedia path was removed, so clear the nomedia paths
MediaScanner.clearMediaPathCache(false /* media */, true /* nomedia */);
android.media.MediaScanner.clearMediaPathCache(false /* media */, true /* nomedia */);

final String volumeName = MediaStore.getVolumeName(new File(path));
final Uri uri = MediaStore.Files.getContentUri(volumeName);
Expand Down Expand Up @@ -3423,11 +3423,12 @@ public int delete(Uri uri, String userWhere, String[] userWhereArgs) {

if (INTERNAL_VOLUME.equals(mMediaScannerVolume)) {
// persist current build fingerprint as fingerprint for system (internal) sound scan
final SharedPreferences scanSettings =
getContext().getSharedPreferences(MediaScanner.SCANNED_BUILD_PREFS_NAME,
Context.MODE_PRIVATE);
final SharedPreferences scanSettings = getContext().getSharedPreferences(
android.media.MediaScanner.SCANNED_BUILD_PREFS_NAME,
Context.MODE_PRIVATE);
final SharedPreferences.Editor editor = scanSettings.edit();
editor.putString(MediaScanner.LAST_INTERNAL_SCAN_FINGERPRINT, Build.FINGERPRINT);
editor.putString(android.media.MediaScanner.LAST_INTERNAL_SCAN_FINGERPRINT,
Build.FINGERPRINT);
editor.apply();
}
mMediaScannerVolume = null;
Expand Down Expand Up @@ -3657,12 +3658,8 @@ public Bundle call(String method, String arg, Bundle extras) {
final Bundle res = new Bundle();
switch (method) {
case MediaStore.SCAN_FILE_CALL:
final String path = systemFile.getAbsolutePath();
final String ext = path.substring(path.lastIndexOf('.') + 1);
final String mimeType = MimeUtils.guessMimeTypeFromExtension(ext);
res.putParcelable(Intent.EXTRA_STREAM,
MediaService.onScanFile(getContext(), Uri.fromFile(systemFile),
mimeType));
MediaScanner.instance(getContext()).scanFile(systemFile));
break;
case MediaStore.SCAN_VOLUME_CALL:
MediaService.onScanVolume(getContext(), Uri.fromFile(systemFile));
Expand Down Expand Up @@ -4385,11 +4382,7 @@ public int update(Uri uri, ContentValues initialValues, String userWhere,
try (Cursor c = queryForSingleItem(uri,
new String[] { FileColumns.DATA }, null, null, null)) {
final String data = c.getString(0);
try (MediaScanner scanner = new MediaScanner(getContext(), volumeName)) {
final String ext = data.substring(data.lastIndexOf('.') + 1);
scanner.scanSingleFile(data,
MimeUtils.guessMimeTypeFromExtension(ext));
}
MediaScanner.instance(getContext()).scanFile(new File(data));
} catch (Exception e) {
Log.w(TAG, "Failed to update metadata for " + uri, e);
} finally {
Expand Down Expand Up @@ -4715,13 +4708,7 @@ && parseBoolean(uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL))) {
update(uri, values, null, null);
break;
default:
final String volumeName = MediaStore.getVolumeName(uri);
final String data = file.getAbsolutePath();
try (MediaScanner scanner = new MediaScanner(getContext(), volumeName)) {
final String ext = data.substring(data.lastIndexOf('.') + 1);
scanner.scanSingleFile(data,
MimeUtils.guessMimeTypeFromExtension(ext));
}
MediaScanner.instance(getContext()).scanFile(file);
break;
}
} catch (Exception e2) {
Expand Down
2 changes: 1 addition & 1 deletion src/com/android/providers/media/MediaScannerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void requestScanFile(String path, String mimeType,
.translateAppToSystem(new File(path).getCanonicalFile(),
callingPid, callingUid);
res = MediaService.onScanFile(MediaScannerService.this,
Uri.fromFile(systemFile), mimeType);
Uri.fromFile(systemFile));
Log.d(TAG, "Scanned " + path + " as " + systemFile + " for " + res);
} catch (Exception e) {
Log.w(TAG, "Failed to scan " + path, e);
Expand Down
28 changes: 11 additions & 17 deletions src/com/android/providers/media/MediaService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.media.MediaScanner;
import android.net.Uri;
import android.os.Environment;
import android.os.PowerManager;
import android.os.Trace;
import android.provider.MediaStore;
import android.util.Log;

import com.android.providers.media.scan.MediaScanner;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

public class MediaService extends IntentService {
public MediaService() {
Expand Down Expand Up @@ -77,7 +78,7 @@ protected void onHandleIntent(Intent intent) {
break;
}
case Intent.ACTION_MEDIA_SCANNER_SCAN_FILE: {
onScanFile(this, intent.getData(), intent.getType());
onScanFile(this, intent.getData());
break;
}
default: {
Expand Down Expand Up @@ -137,8 +138,8 @@ public static void onScanVolume(Context context, Uri uri) throws IOException {
context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
}

try (MediaScanner scanner = new MediaScanner(context, volumeName)) {
scanner.scanDirectories(resolveDirectories(volumeName));
for (File dir : resolveDirectories(volumeName)) {
MediaScanner.instance(context).scanDirectory(dir);
}

resolver.delete(scanUri, null, null);
Expand All @@ -150,20 +151,13 @@ public static void onScanVolume(Context context, Uri uri) throws IOException {
}
}

public static Uri onScanFile(Context context, Uri uri, String mimeType) throws IOException {
public static Uri onScanFile(Context context, Uri uri) throws IOException {
final File file = new File(uri.getPath()).getCanonicalFile();
final String volumeName = MediaStore.getVolumeName(file);

try (MediaScanner scanner = new MediaScanner(context, volumeName)) {
return scanner.scanSingleFile(file.getAbsolutePath(), mimeType);
}
return MediaScanner.instance(context).scanFile(file);
}

private static String[] resolveDirectories(String volumeName) throws FileNotFoundException {
final ArrayList<String> res = new ArrayList<>();
for (File dir : MediaStore.getVolumeScanPaths(volumeName)) {
res.add(dir.getAbsolutePath());
}
return res.toArray(new String[res.size()]);
private static Collection<File> resolveDirectories(String volumeName)
throws FileNotFoundException {
return MediaStore.getVolumeScanPaths(volumeName);
}
}
69 changes: 69 additions & 0 deletions src/com/android/providers/media/scan/LegacyMediaScanner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.providers.media.scan;

import android.content.Context;
import android.net.Uri;
import android.os.Trace;
import android.provider.MediaStore;

import libcore.net.MimeUtils;

import java.io.File;

public class LegacyMediaScanner implements MediaScanner {
private final Context mContext;

public LegacyMediaScanner(Context context) {
mContext = context;
}

@Override
public Context getContext() {
return mContext;
}

@Override
public void scanDirectory(File file) {
final String path = file.getAbsolutePath();
final String volumeName = MediaStore.getVolumeName(file);

Trace.traceBegin(Trace.TRACE_TAG_DATABASE, "scanDirectory");
try (android.media.MediaScanner scanner =
new android.media.MediaScanner(mContext, volumeName)) {
scanner.scanDirectories(new String[] { path });
} finally {
Trace.traceEnd(Trace.TRACE_TAG_DATABASE);
}
}

@Override
public Uri scanFile(File file) {
final String path = file.getAbsolutePath();
final String volumeName = MediaStore.getVolumeName(file);

Trace.traceBegin(Trace.TRACE_TAG_DATABASE, "scanFile");
try (android.media.MediaScanner scanner =
new android.media.MediaScanner(mContext, volumeName)) {
final String ext = path.substring(path.lastIndexOf('.') + 1);
return scanner.scanSingleFile(path,
MimeUtils.guessMimeTypeFromExtension(ext));
} finally {
Trace.traceEnd(Trace.TRACE_TAG_DATABASE);
}
}
}
32 changes: 32 additions & 0 deletions src/com/android/providers/media/scan/MediaScanner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.providers.media.scan;

import android.content.Context;
import android.net.Uri;

import java.io.File;

public interface MediaScanner {
public Context getContext();
public void scanDirectory(File file);
public Uri scanFile(File file);

public static MediaScanner instance(Context context) {
return new LegacyMediaScanner(context);
}
}
Loading

0 comments on commit 10b4d8d

Please sign in to comment.