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