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