1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media.scan; 18 19 import static android.provider.MediaStore.VOLUME_EXTERNAL; 20 21 import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN; 22 23 import static org.junit.Assert.assertEquals; 24 25 import android.Manifest; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.ContextWrapper; 29 import android.content.pm.ProviderInfo; 30 import android.database.Cursor; 31 import android.database.DatabaseUtils; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Environment; 35 import android.os.SystemClock; 36 import android.provider.BaseColumns; 37 import android.provider.DeviceConfig.OnPropertiesChangedListener; 38 import android.provider.MediaStore; 39 import android.provider.MediaStore.MediaColumns; 40 import android.provider.Settings; 41 import android.test.mock.MockContentProvider; 42 import android.test.mock.MockContentResolver; 43 import android.util.Log; 44 45 import androidx.test.InstrumentationRegistry; 46 import androidx.test.runner.AndroidJUnit4; 47 48 import com.android.providers.media.MediaDocumentsProvider; 49 import com.android.providers.media.MediaProvider; 50 import com.android.providers.media.R; 51 import com.android.providers.media.util.FileUtils; 52 53 import org.junit.Before; 54 import org.junit.Ignore; 55 import org.junit.Test; 56 import org.junit.runner.RunWith; 57 58 import java.io.File; 59 import java.io.FileOutputStream; 60 import java.io.IOException; 61 import java.io.InputStream; 62 import java.io.OutputStream; 63 import java.util.Arrays; 64 65 @RunWith(AndroidJUnit4.class) 66 public class MediaScannerTest { 67 private static final String TAG = "MediaScannerTest"; 68 69 public static class IsolatedContext extends ContextWrapper { 70 private final File mDir; 71 private final MockContentResolver mResolver; 72 private final MediaProvider mProvider; 73 private final MediaDocumentsProvider mDocumentsProvider; 74 IsolatedContext(Context base, String tag, boolean asFuseThread)75 public IsolatedContext(Context base, String tag, boolean asFuseThread) { 76 super(base); 77 mDir = new File(base.getFilesDir(), tag); 78 mDir.mkdirs(); 79 FileUtils.deleteContents(mDir); 80 81 mResolver = new MockContentResolver(this); 82 83 final ProviderInfo info = base.getPackageManager() 84 .resolveContentProvider(MediaStore.AUTHORITY, 0); 85 mProvider = new MediaProvider() { 86 @Override 87 public boolean isFuseThread() { 88 return asFuseThread; 89 } 90 91 @Override 92 public boolean getBooleanDeviceConfig(String key, boolean defaultValue) { 93 return defaultValue; 94 } 95 96 @Override 97 public String getStringDeviceConfig(String key, String defaultValue) { 98 return defaultValue; 99 } 100 101 @Override 102 public int getIntDeviceConfig(String key, int defaultValue) { 103 return defaultValue; 104 } 105 106 @Override 107 public void addOnPropertiesChangedListener(OnPropertiesChangedListener listener) { 108 // Ignore 109 } 110 }; 111 mProvider.attachInfo(this, info); 112 mResolver.addProvider(MediaStore.AUTHORITY, mProvider); 113 114 final ProviderInfo documentsInfo = base.getPackageManager() 115 .resolveContentProvider(MediaDocumentsProvider.AUTHORITY, 0); 116 mDocumentsProvider = new MediaDocumentsProvider(); 117 mDocumentsProvider.attachInfo(this, documentsInfo); 118 mResolver.addProvider(MediaDocumentsProvider.AUTHORITY, mDocumentsProvider); 119 120 mResolver.addProvider(Settings.AUTHORITY, new MockContentProvider() { 121 @Override 122 public Bundle call(String method, String request, Bundle args) { 123 return Bundle.EMPTY; 124 } 125 }); 126 127 MediaStore.waitForIdle(mResolver); 128 } 129 130 @Override getDatabasePath(String name)131 public File getDatabasePath(String name) { 132 return new File(mDir, name); 133 } 134 135 @Override getContentResolver()136 public ContentResolver getContentResolver() { 137 return mResolver; 138 } 139 } 140 141 private MediaScanner mLegacy; 142 private MediaScanner mModern; 143 144 @Before setUp()145 public void setUp() { 146 final Context context = InstrumentationRegistry.getTargetContext(); 147 InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( 148 Manifest.permission.INTERACT_ACROSS_USERS); 149 150 mLegacy = new LegacyMediaScanner( 151 new IsolatedContext(context, "legacy", /*asFuseThread*/ false)); 152 mModern = new ModernMediaScanner( 153 new IsolatedContext(context, "modern", /*asFuseThread*/ false)); 154 } 155 156 /** 157 * Ask both legacy and modern scanners to example sample files and assert 158 * the resulting database modifications are identical. 159 */ 160 @Test 161 @Ignore testCorrectness()162 public void testCorrectness() throws Exception { 163 final File dir = Environment.getExternalStorageDirectory(); 164 stage(R.raw.test_audio, new File(dir, "test.mp3")); 165 stage(R.raw.test_video, new File(dir, "test.mp4")); 166 stage(R.raw.test_image, new File(dir, "test.jpg")); 167 168 // Execute both scanners in isolation 169 scanDirectory(mLegacy, dir, "legacy"); 170 scanDirectory(mModern, dir, "modern"); 171 172 // Confirm that they both agree on scanned details 173 for (Uri uri : new Uri[] { 174 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 175 MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 176 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 177 }) { 178 final Context legacyContext = mLegacy.getContext(); 179 final Context modernContext = mModern.getContext(); 180 try (Cursor cl = legacyContext.getContentResolver().query(uri, null, null, null); 181 Cursor cm = modernContext.getContentResolver().query(uri, null, null, null)) { 182 try { 183 // Must have same count 184 assertEquals(cl.getCount(), cm.getCount()); 185 186 while (cl.moveToNext() && cm.moveToNext()) { 187 for (int i = 0; i < cl.getColumnCount(); i++) { 188 final String columnName = cl.getColumnName(i); 189 if (columnName.equals(MediaColumns._ID)) continue; 190 if (columnName.equals(MediaColumns.DATE_ADDED)) continue; 191 192 // Must have same name 193 assertEquals(cl.getColumnName(i), cm.getColumnName(i)); 194 // Must have same data types 195 assertEquals(columnName + " type", 196 cl.getType(i), cm.getType(i)); 197 // Must have same contents 198 assertEquals(columnName + " value", 199 cl.getString(i), cm.getString(i)); 200 } 201 } 202 } catch (AssertionError e) { 203 Log.d(TAG, "Legacy:"); 204 DatabaseUtils.dumpCursor(cl); 205 Log.d(TAG, "Modern:"); 206 DatabaseUtils.dumpCursor(cm); 207 throw e; 208 } 209 } 210 } 211 } 212 213 @Test 214 @Ignore testSpeed_Legacy()215 public void testSpeed_Legacy() throws Exception { 216 testSpeed(mLegacy); 217 } 218 219 @Test 220 @Ignore testSpeed_Modern()221 public void testSpeed_Modern() throws Exception { 222 testSpeed(mModern); 223 } 224 testSpeed(MediaScanner scanner)225 private void testSpeed(MediaScanner scanner) throws IOException { 226 final File scanDir = Environment.getExternalStorageDirectory(); 227 final File dir = new File(Environment.getExternalStorageDirectory(), 228 "test" + System.nanoTime()); 229 230 stage(dir, 4, 3); 231 scanDirectory(scanner, scanDir, "Initial"); 232 scanDirectory(scanner, scanDir, "No-op"); 233 234 FileUtils.deleteContents(dir); 235 dir.delete(); 236 scanDirectory(scanner, scanDir, "Clean"); 237 } 238 scanDirectory(MediaScanner scanner, File dir, String tag)239 private static void scanDirectory(MediaScanner scanner, File dir, String tag) { 240 final Context context = scanner.getContext(); 241 final long beforeTime = SystemClock.elapsedRealtime(); 242 final int[] beforeCounts = getCounts(context); 243 244 scanner.scanDirectory(dir, REASON_UNKNOWN); 245 246 final long deltaTime = SystemClock.elapsedRealtime() - beforeTime; 247 final int[] deltaCounts = subtract(getCounts(context), beforeCounts); 248 Log.i(TAG, "Scan " + tag + ": " + deltaTime + "ms " + Arrays.toString(deltaCounts)); 249 } 250 subtract(int[] a, int[] b)251 private static int[] subtract(int[] a, int[] b) { 252 final int[] c = new int[a.length]; 253 for (int i = 0; i < a.length; i++) { 254 c[i] = a[i] - b[i]; 255 } 256 return c; 257 } 258 getCounts(Context context)259 private static int[] getCounts(Context context) { 260 return new int[] { 261 getCount(context, MediaStore.Files.getContentUri(VOLUME_EXTERNAL)), 262 getCount(context, MediaStore.Audio.Media.getContentUri(VOLUME_EXTERNAL)), 263 getCount(context, MediaStore.Video.Media.getContentUri(VOLUME_EXTERNAL)), 264 getCount(context, MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL)), 265 }; 266 } 267 getCount(Context context, Uri uri)268 private static int getCount(Context context, Uri uri) { 269 try (Cursor c = context.getContentResolver().query(uri, 270 new String[] { BaseColumns._ID }, null, null)) { 271 return c.getCount(); 272 } 273 } 274 stage(File dir, int deep, int wide)275 private static void stage(File dir, int deep, int wide) throws IOException { 276 dir.mkdirs(); 277 278 if (deep > 0) { 279 stage(new File(dir, "dir" + System.nanoTime()), deep - 1, wide * 2); 280 } 281 282 for (int i = 0; i < wide; i++) { 283 stage(R.raw.test_image, new File(dir, System.nanoTime() + ".jpg")); 284 stage(R.raw.test_video, new File(dir, System.nanoTime() + ".mp4")); 285 } 286 } 287 stage(int resId, File file)288 public static File stage(int resId, File file) throws IOException { 289 final Context context = InstrumentationRegistry.getContext(); 290 try (InputStream source = context.getResources().openRawResource(resId); 291 OutputStream target = new FileOutputStream(file)) { 292 FileUtils.copy(source, target); 293 } 294 return file; 295 } 296 } 297