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.os.UserHandle; 37 import android.provider.BaseColumns; 38 import android.provider.DeviceConfig.OnPropertiesChangedListener; 39 import android.provider.MediaStore; 40 import android.provider.MediaStore.MediaColumns; 41 import android.provider.Settings; 42 import android.test.mock.MockContentProvider; 43 import android.test.mock.MockContentResolver; 44 import android.util.Log; 45 46 import androidx.test.InstrumentationRegistry; 47 import androidx.test.runner.AndroidJUnit4; 48 49 import com.android.providers.media.DatabaseHelper; 50 import com.android.providers.media.MediaDocumentsProvider; 51 import com.android.providers.media.MediaProvider; 52 import com.android.providers.media.PickerUriResolver; 53 import com.android.providers.media.R; 54 import com.android.providers.media.photopicker.PhotoPickerProvider; 55 import com.android.providers.media.photopicker.PickerSyncController; 56 import com.android.providers.media.util.FileUtils; 57 58 import org.junit.Before; 59 import org.junit.Ignore; 60 import org.junit.Test; 61 import org.junit.runner.RunWith; 62 63 import java.io.File; 64 import java.io.FileOutputStream; 65 import java.io.IOException; 66 import java.io.InputStream; 67 import java.io.OutputStream; 68 import java.util.Arrays; 69 70 @RunWith(AndroidJUnit4.class) 71 public class MediaScannerTest { 72 private static final String TAG = "MediaScannerTest"; 73 74 public static class IsolatedContext extends ContextWrapper { 75 private final File mDir; 76 private final MockContentResolver mResolver; 77 private final MediaProvider mProvider; 78 private final MediaDocumentsProvider mDocumentsProvider; 79 private final PhotoPickerProvider mPhotoPickerProvider; 80 private final UserHandle mUserHandle; 81 IsolatedContext(Context base, String tag, boolean asFuseThread)82 public IsolatedContext(Context base, String tag, boolean asFuseThread) { 83 this(base, tag, asFuseThread, base.getUser()); 84 } 85 IsolatedContext(Context base, String tag, boolean asFuseThread, UserHandle userHandle)86 public IsolatedContext(Context base, String tag, boolean asFuseThread, 87 UserHandle userHandle) { 88 super(base); 89 mDir = new File(base.getFilesDir(), tag); 90 mDir.mkdirs(); 91 FileUtils.deleteContents(mDir); 92 93 mResolver = new MockContentResolver(this); 94 mUserHandle = userHandle; 95 96 final ProviderInfo info = base.getPackageManager() 97 .resolveContentProvider(MediaStore.AUTHORITY, 0); 98 mProvider = new MediaProvider() { 99 @Override 100 public boolean isFuseThread() { 101 return asFuseThread; 102 } 103 104 @Override 105 public boolean getBooleanDeviceConfig(String key, boolean defaultValue) { 106 return defaultValue; 107 } 108 109 @Override 110 public String getStringDeviceConfig(String key, String defaultValue) { 111 return defaultValue; 112 } 113 114 @Override 115 public int getIntDeviceConfig(String key, int defaultValue) { 116 return defaultValue; 117 } 118 119 @Override 120 public int getIntDeviceConfig(String namespace, String key, int defaultValue) { 121 return 0; 122 } 123 124 @Override 125 public void addOnPropertiesChangedListener(OnPropertiesChangedListener listener) { 126 // Ignore 127 } 128 129 @Override 130 protected void updateNextRowIdXattr(DatabaseHelper helper, long id) { 131 // Ignoring this as test app would not have access to update xattr. 132 } 133 }; 134 mProvider.attachInfo(this, info); 135 mResolver.addProvider(MediaStore.AUTHORITY, mProvider); 136 137 final ProviderInfo documentsInfo = base.getPackageManager() 138 .resolveContentProvider(MediaDocumentsProvider.AUTHORITY, 0); 139 mDocumentsProvider = new MediaDocumentsProvider(); 140 mDocumentsProvider.attachInfo(this, documentsInfo); 141 mResolver.addProvider(MediaDocumentsProvider.AUTHORITY, mDocumentsProvider); 142 143 mResolver.addProvider(Settings.AUTHORITY, new MockContentProvider() { 144 @Override 145 public Bundle call(String method, String request, Bundle args) { 146 return Bundle.EMPTY; 147 } 148 }); 149 150 final ProviderInfo photoPickerProviderInfo = base.getPackageManager() 151 .resolveContentProvider(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, 152 0); 153 mPhotoPickerProvider = new PhotoPickerProvider(); 154 mPhotoPickerProvider.attachInfo(this, photoPickerProviderInfo); 155 mResolver.addProvider(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, 156 mPhotoPickerProvider); 157 158 MediaStore.waitForIdle(mResolver); 159 } 160 161 @Override getDatabasePath(String name)162 public File getDatabasePath(String name) { 163 return new File(mDir, name); 164 } 165 166 @Override getContentResolver()167 public ContentResolver getContentResolver() { 168 return mResolver; 169 } 170 171 @Override getUser()172 public UserHandle getUser() { 173 return mUserHandle; 174 } 175 setPickerUriResolver(PickerUriResolver resolver)176 public void setPickerUriResolver(PickerUriResolver resolver) { 177 mProvider.setUriResolver(resolver); 178 } 179 } 180 181 private MediaScanner mLegacy; 182 private MediaScanner mModern; 183 184 @Before setUp()185 public void setUp() { 186 final Context context = InstrumentationRegistry.getTargetContext(); 187 InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity( 188 Manifest.permission.INTERACT_ACROSS_USERS); 189 190 mLegacy = new LegacyMediaScanner( 191 new IsolatedContext(context, "legacy", /*asFuseThread*/ false)); 192 mModern = new ModernMediaScanner( 193 new IsolatedContext(context, "modern", /*asFuseThread*/ false)); 194 } 195 196 /** 197 * Ask both legacy and modern scanners to example sample files and assert 198 * the resulting database modifications are identical. 199 */ 200 @Test 201 @Ignore testCorrectness()202 public void testCorrectness() throws Exception { 203 final File dir = Environment.getExternalStorageDirectory(); 204 stage(R.raw.test_audio, new File(dir, "test.mp3")); 205 stage(R.raw.test_video, new File(dir, "test.mp4")); 206 stage(R.raw.test_image, new File(dir, "test.jpg")); 207 208 // Execute both scanners in isolation 209 scanDirectory(mLegacy, dir, "legacy"); 210 scanDirectory(mModern, dir, "modern"); 211 212 // Confirm that they both agree on scanned details 213 for (Uri uri : new Uri[] { 214 MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 215 MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 216 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 217 }) { 218 final Context legacyContext = mLegacy.getContext(); 219 final Context modernContext = mModern.getContext(); 220 try (Cursor cl = legacyContext.getContentResolver().query(uri, null, null, null); 221 Cursor cm = modernContext.getContentResolver().query(uri, null, null, null)) { 222 try { 223 // Must have same count 224 assertEquals(cl.getCount(), cm.getCount()); 225 226 while (cl.moveToNext() && cm.moveToNext()) { 227 for (int i = 0; i < cl.getColumnCount(); i++) { 228 final String columnName = cl.getColumnName(i); 229 if (columnName.equals(MediaColumns._ID)) continue; 230 if (columnName.equals(MediaColumns.DATE_ADDED)) continue; 231 232 // Must have same name 233 assertEquals(cl.getColumnName(i), cm.getColumnName(i)); 234 // Must have same data types 235 assertEquals(columnName + " type", 236 cl.getType(i), cm.getType(i)); 237 // Must have same contents 238 assertEquals(columnName + " value", 239 cl.getString(i), cm.getString(i)); 240 } 241 } 242 } catch (AssertionError e) { 243 Log.d(TAG, "Legacy:"); 244 DatabaseUtils.dumpCursor(cl); 245 Log.d(TAG, "Modern:"); 246 DatabaseUtils.dumpCursor(cm); 247 throw e; 248 } 249 } 250 } 251 } 252 253 @Test 254 @Ignore testSpeed_Legacy()255 public void testSpeed_Legacy() throws Exception { 256 testSpeed(mLegacy); 257 } 258 259 @Test 260 @Ignore testSpeed_Modern()261 public void testSpeed_Modern() throws Exception { 262 testSpeed(mModern); 263 } 264 testSpeed(MediaScanner scanner)265 private void testSpeed(MediaScanner scanner) throws IOException { 266 final File scanDir = Environment.getExternalStorageDirectory(); 267 final File dir = new File(Environment.getExternalStorageDirectory(), 268 "test" + System.nanoTime()); 269 270 stage(dir, 4, 3); 271 scanDirectory(scanner, scanDir, "Initial"); 272 scanDirectory(scanner, scanDir, "No-op"); 273 274 FileUtils.deleteContents(dir); 275 dir.delete(); 276 scanDirectory(scanner, scanDir, "Clean"); 277 } 278 scanDirectory(MediaScanner scanner, File dir, String tag)279 private static void scanDirectory(MediaScanner scanner, File dir, String tag) { 280 final Context context = scanner.getContext(); 281 final long beforeTime = SystemClock.elapsedRealtime(); 282 final int[] beforeCounts = getCounts(context); 283 284 scanner.scanDirectory(dir, REASON_UNKNOWN); 285 286 final long deltaTime = SystemClock.elapsedRealtime() - beforeTime; 287 final int[] deltaCounts = subtract(getCounts(context), beforeCounts); 288 Log.i(TAG, "Scan " + tag + ": " + deltaTime + "ms " + Arrays.toString(deltaCounts)); 289 } 290 subtract(int[] a, int[] b)291 private static int[] subtract(int[] a, int[] b) { 292 final int[] c = new int[a.length]; 293 for (int i = 0; i < a.length; i++) { 294 c[i] = a[i] - b[i]; 295 } 296 return c; 297 } 298 getCounts(Context context)299 private static int[] getCounts(Context context) { 300 return new int[] { 301 getCount(context, MediaStore.Files.getContentUri(VOLUME_EXTERNAL)), 302 getCount(context, MediaStore.Audio.Media.getContentUri(VOLUME_EXTERNAL)), 303 getCount(context, MediaStore.Video.Media.getContentUri(VOLUME_EXTERNAL)), 304 getCount(context, MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL)), 305 }; 306 } 307 getCount(Context context, Uri uri)308 private static int getCount(Context context, Uri uri) { 309 try (Cursor c = context.getContentResolver().query(uri, 310 new String[] { BaseColumns._ID }, null, null)) { 311 return c.getCount(); 312 } 313 } 314 stage(File dir, int deep, int wide)315 private static void stage(File dir, int deep, int wide) throws IOException { 316 dir.mkdirs(); 317 318 if (deep > 0) { 319 stage(new File(dir, "dir" + System.nanoTime()), deep - 1, wide * 2); 320 } 321 322 for (int i = 0; i < wide; i++) { 323 stage(R.raw.test_image, new File(dir, System.nanoTime() + ".jpg")); 324 stage(R.raw.test_video, new File(dir, System.nanoTime() + ".mp4")); 325 } 326 } 327 stage(int resId, File file)328 public static File stage(int resId, File file) throws IOException { 329 final Context context = InstrumentationRegistry.getContext(); 330 try (InputStream source = context.getResources().openRawResource(resId); 331 OutputStream target = new FileOutputStream(file)) { 332 FileUtils.copy(source, target); 333 } 334 return file; 335 } 336 } 337