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