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