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