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