blob: f8c27e6a676e16b94fca28e6323defc7ff9190b4 [file] [log] [blame]
/*
* 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 static android.provider.MediaStore.VOLUME_EXTERNAL;
import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
import static org.junit.Assert.assertEquals;
import android.Manifest;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.BaseColumns;
import android.provider.DeviceConfig.OnPropertiesChangedListener;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.test.mock.MockContentProvider;
import android.test.mock.MockContentResolver;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.providers.media.DatabaseHelper;
import com.android.providers.media.MediaDocumentsProvider;
import com.android.providers.media.MediaProvider;
import com.android.providers.media.PickerUriResolver;
import com.android.providers.media.R;
import com.android.providers.media.photopicker.PhotoPickerProvider;
import com.android.providers.media.photopicker.PickerSyncController;
import com.android.providers.media.util.FileUtils;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
public class MediaScannerTest {
private static final String TAG = "MediaScannerTest";
public static class IsolatedContext extends ContextWrapper {
private final File mDir;
private final MockContentResolver mResolver;
private final MediaProvider mProvider;
private final MediaDocumentsProvider mDocumentsProvider;
private final PhotoPickerProvider mPhotoPickerProvider;
private final UserHandle mUserHandle;
public IsolatedContext(Context base, String tag, boolean asFuseThread) {
this(base, tag, asFuseThread, base.getUser());
}
public IsolatedContext(Context base, String tag, boolean asFuseThread,
UserHandle userHandle) {
super(base);
mDir = new File(base.getFilesDir(), tag);
mDir.mkdirs();
FileUtils.deleteContents(mDir);
mResolver = new MockContentResolver(this);
mUserHandle = userHandle;
final ProviderInfo info = base.getPackageManager()
.resolveContentProvider(MediaStore.AUTHORITY, 0);
mProvider = new MediaProvider() {
@Override
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;
}
@Override
public int getIntDeviceConfig(String key, int defaultValue) {
return defaultValue;
}
@Override
public int getIntDeviceConfig(String namespace, String key, int defaultValue) {
return 0;
}
@Override
public void addOnPropertiesChangedListener(OnPropertiesChangedListener listener) {
// Ignore
}
@Override
protected void updateNextRowIdXattr(DatabaseHelper helper, long id) {
// Ignoring this as test app would not have access to update xattr.
}
};
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) {
return Bundle.EMPTY;
}
});
final ProviderInfo photoPickerProviderInfo = base.getPackageManager()
.resolveContentProvider(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY,
0);
mPhotoPickerProvider = new PhotoPickerProvider();
mPhotoPickerProvider.attachInfo(this, photoPickerProviderInfo);
mResolver.addProvider(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY,
mPhotoPickerProvider);
MediaStore.waitForIdle(mResolver);
}
@Override
public File getDatabasePath(String name) {
return new File(mDir, name);
}
@Override
public ContentResolver getContentResolver() {
return mResolver;
}
@Override
public UserHandle getUser() {
return mUserHandle;
}
public void setPickerUriResolver(PickerUriResolver resolver) {
mProvider.setUriResolver(resolver);
}
}
private MediaScanner mLegacy;
private MediaScanner mModern;
@Before
public void setUp() {
final Context context = InstrumentationRegistry.getTargetContext();
InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
Manifest.permission.INTERACT_ACROSS_USERS);
mLegacy = new LegacyMediaScanner(
new IsolatedContext(context, "legacy", /*asFuseThread*/ false));
mModern = new ModernMediaScanner(
new IsolatedContext(context, "modern", /*asFuseThread*/ false));
}
/**
* Ask both legacy and modern scanners to example sample files and assert
* the resulting database modifications are identical.
*/
@Test
@Ignore
public void testCorrectness() throws Exception {
final File dir = Environment.getExternalStorageDirectory();
stage(R.raw.test_audio, new File(dir, "test.mp3"));
stage(R.raw.test_video, new File(dir, "test.mp4"));
stage(R.raw.test_image, new File(dir, "test.jpg"));
// Execute both scanners in isolation
scanDirectory(mLegacy, dir, "legacy");
scanDirectory(mModern, dir, "modern");
// Confirm that they both agree on scanned details
for (Uri uri : new Uri[] {
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
}) {
final Context legacyContext = mLegacy.getContext();
final Context modernContext = mModern.getContext();
try (Cursor cl = legacyContext.getContentResolver().query(uri, null, null, null);
Cursor cm = modernContext.getContentResolver().query(uri, null, null, null)) {
try {
// Must have same count
assertEquals(cl.getCount(), cm.getCount());
while (cl.moveToNext() && cm.moveToNext()) {
for (int i = 0; i < cl.getColumnCount(); i++) {
final String columnName = cl.getColumnName(i);
if (columnName.equals(MediaColumns._ID)) continue;
if (columnName.equals(MediaColumns.DATE_ADDED)) continue;
// Must have same name
assertEquals(cl.getColumnName(i), cm.getColumnName(i));
// Must have same data types
assertEquals(columnName + " type",
cl.getType(i), cm.getType(i));
// Must have same contents
assertEquals(columnName + " value",
cl.getString(i), cm.getString(i));
}
}
} catch (AssertionError e) {
Log.d(TAG, "Legacy:");
DatabaseUtils.dumpCursor(cl);
Log.d(TAG, "Modern:");
DatabaseUtils.dumpCursor(cm);
throw e;
}
}
}
}
@Test
@Ignore
public void testSpeed_Legacy() throws Exception {
testSpeed(mLegacy);
}
@Test
@Ignore
public void testSpeed_Modern() throws Exception {
testSpeed(mModern);
}
private void testSpeed(MediaScanner scanner) throws IOException {
final File scanDir = Environment.getExternalStorageDirectory();
final File dir = new File(Environment.getExternalStorageDirectory(),
"test" + System.nanoTime());
stage(dir, 4, 3);
scanDirectory(scanner, scanDir, "Initial");
scanDirectory(scanner, scanDir, "No-op");
FileUtils.deleteContents(dir);
dir.delete();
scanDirectory(scanner, scanDir, "Clean");
}
private static void scanDirectory(MediaScanner scanner, File dir, String tag) {
final Context context = scanner.getContext();
final long beforeTime = SystemClock.elapsedRealtime();
final int[] beforeCounts = getCounts(context);
scanner.scanDirectory(dir, REASON_UNKNOWN);
final long deltaTime = SystemClock.elapsedRealtime() - beforeTime;
final int[] deltaCounts = subtract(getCounts(context), beforeCounts);
Log.i(TAG, "Scan " + tag + ": " + deltaTime + "ms " + Arrays.toString(deltaCounts));
}
private static int[] subtract(int[] a, int[] b) {
final int[] c = new int[a.length];
for (int i = 0; i < a.length; i++) {
c[i] = a[i] - b[i];
}
return c;
}
private static int[] getCounts(Context context) {
return new int[] {
getCount(context, MediaStore.Files.getContentUri(VOLUME_EXTERNAL)),
getCount(context, MediaStore.Audio.Media.getContentUri(VOLUME_EXTERNAL)),
getCount(context, MediaStore.Video.Media.getContentUri(VOLUME_EXTERNAL)),
getCount(context, MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL)),
};
}
private static int getCount(Context context, Uri uri) {
try (Cursor c = context.getContentResolver().query(uri,
new String[] { BaseColumns._ID }, null, null)) {
return c.getCount();
}
}
private static void stage(File dir, int deep, int wide) throws IOException {
dir.mkdirs();
if (deep > 0) {
stage(new File(dir, "dir" + System.nanoTime()), deep - 1, wide * 2);
}
for (int i = 0; i < wide; i++) {
stage(R.raw.test_image, new File(dir, System.nanoTime() + ".jpg"));
stage(R.raw.test_video, new File(dir, System.nanoTime() + ".mp4"));
}
}
public static File stage(int resId, File file) throws IOException {
final Context context = InstrumentationRegistry.getContext();
try (InputStream source = context.getResources().openRawResource(resId);
OutputStream target = new FileOutputStream(file)) {
FileUtils.copy(source, target);
}
return file;
}
}