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