• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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