xref: /aosp_15_r20/cts/tests/tests/media/misc/src/android/media/misc/cts/MediaScannerTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2012 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 android.media.misc.cts;
18 
19 import android.app.UiAutomation;
20 import android.content.ComponentName;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.res.AssetFileDescriptor;
24 import android.database.Cursor;
25 import android.media.MediaMetadataRetriever;
26 import android.media.MediaScannerConnection;
27 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
28 import android.net.Uri;
29 import android.os.Build;
30 import android.os.Environment;
31 import android.os.IBinder;
32 import android.os.ParcelFileDescriptor;
33 import android.os.SystemClock;
34 import android.platform.test.annotations.AppModeFull;
35 import android.platform.test.annotations.Presubmit;
36 import android.platform.test.annotations.RequiresDevice;
37 import android.provider.MediaStore;
38 import android.provider.MediaStore.MediaColumns;
39 import android.test.AndroidTestCase;
40 import android.util.Log;
41 
42 import androidx.test.InstrumentationRegistry;
43 import androidx.test.filters.SmallTest;
44 
45 import com.android.compatibility.common.util.ApiLevelUtil;
46 import com.android.compatibility.common.util.FileCopyHelper;
47 import com.android.compatibility.common.util.FrameworkSpecificTest;
48 import com.android.compatibility.common.util.PollingCheck;
49 import com.android.compatibility.common.util.Preconditions;
50 
51 import java.io.BufferedReader;
52 import java.io.File;
53 import java.io.FileInputStream;
54 import java.io.FileNotFoundException;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.io.InputStreamReader;
58 import java.lang.reflect.Method;
59 import java.nio.charset.StandardCharsets;
60 
61 @Presubmit
62 @FrameworkSpecificTest
63 @SmallTest
64 @RequiresDevice
65 @AppModeFull(reason = "TODO: evaluate and port to instant")
66 public class MediaScannerTest extends AndroidTestCase {
67     private static final String MEDIA_TYPE = "audio/mpeg";
68     static final String mInpPrefix = WorkDir.getMediaDirString();
69     private File mMediaFile;
70     private static final int TIME_OUT = 10000;
71     private MockMediaScannerConnection mMediaScannerConnection;
72     private MockMediaScannerConnectionClient mMediaScannerConnectionClient;
73     private String mFileDir;
74 
75     @Override
setUp()76     protected void setUp() throws Exception {
77         super.setUp();
78         // prepare the media file.
79 
80         mFileDir = mContext.getExternalMediaDirs()[0].getAbsolutePath();
81 
82         cleanup();
83         String fileName = mFileDir + "/test" + System.currentTimeMillis() + ".mp3";
84         writeFile("testmp3.mp3", fileName);
85 
86         mMediaFile = new File(fileName);
87         assertTrue(mMediaFile.exists());
88     }
89 
getAssetFileDescriptorFor(final String res)90     protected AssetFileDescriptor getAssetFileDescriptorFor(final String res)
91             throws FileNotFoundException {
92         Preconditions.assertTestFileExists(mInpPrefix + res);
93         File inpFile = new File(mInpPrefix + res);
94         ParcelFileDescriptor parcelFD =
95                 ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY);
96         return new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize());
97     }
98 
writeFile(int resid, String path)99     private void writeFile(int resid, String path) throws IOException {
100         File out = new File(path);
101         File dir = out.getParentFile();
102         dir.mkdirs();
103         FileCopyHelper copier = new FileCopyHelper(mContext);
104         copier.copyToExternalStorage(resid, out);
105     }
106 
writeFile(final String res, String path)107     private void writeFile(final String res, String path) throws IOException {
108         File out = new File(path);
109         File dir = out.getParentFile();
110         dir.mkdirs();
111         FileCopyHelper copier = new FileCopyHelper(mContext);
112         copier.copyToExternalStorage(mInpPrefix + res, out);
113     }
114 
115     @Override
tearDown()116     protected void tearDown() throws Exception {
117         cleanup();
118         super.tearDown();
119     }
120 
cleanup()121     private void cleanup() {
122         if (mMediaFile != null) {
123             mMediaFile.delete();
124         }
125         if (mFileDir != null) {
126             String files[] = new File(mFileDir).list();
127             if (files != null) {
128                 for (String f: files) {
129                     new File(mFileDir + "/" + f).delete();
130                 }
131             }
132             new File(mFileDir).delete();
133         }
134 
135         if (mMediaScannerConnection != null) {
136             mMediaScannerConnection.disconnect();
137             mMediaScannerConnection = null;
138         }
139 
140         mContext.getContentResolver().delete(MediaStore.Audio.Media.getContentUri("external"),
141                 "_data like ?", new String[] { mFileDir + "%"});
142     }
143 
testLocalizeRingtoneTitles()144     public void testLocalizeRingtoneTitles() throws Exception {
145         mMediaScannerConnectionClient = new MockMediaScannerConnectionClient();
146         mMediaScannerConnection = new MockMediaScannerConnection(getContext(),
147             mMediaScannerConnectionClient);
148 
149         assertFalse(mMediaScannerConnection.isConnected());
150 
151         // start connection and wait until connected
152         mMediaScannerConnection.connect();
153         checkConnectionState(true);
154 
155         // Write unlocalizable audio file and scan to insert into database
156         final String unlocalizablePath = mFileDir + "/unlocalizable.mp3";
157         writeFile("testmp3.mp3", unlocalizablePath);
158         mMediaScannerConnection.scanFile(unlocalizablePath, null);
159         checkMediaScannerConnection();
160         final Uri media1Uri = mMediaScannerConnectionClient.mediaUri;
161 
162         // Ensure unlocalizable titles come back correctly
163         final ContentResolver res = mContext.getContentResolver();
164         final String unlocalizedTitle = "Chimey Phone";
165         Cursor c = res.query(media1Uri, new String[] { "title" }, null, null, null);
166         assertEquals(1, c.getCount());
167         c.moveToFirst();
168         assertEquals(unlocalizedTitle, c.getString(0));
169 
170         mMediaScannerConnectionClient.reset();
171 
172         // Write localizable audio file and scan to insert into database
173         final String localizablePath = mFileDir + "/localizable.mp3";
174         writeFile("testmp3_4.mp3", localizablePath);
175         mMediaScannerConnection.scanFile(localizablePath, null);
176         checkMediaScannerConnection();
177         final Uri media2Uri = mMediaScannerConnectionClient.mediaUri;
178 
179         // Ensure localized title comes back localized
180         final String localizedTitle = mContext.getString(R.string.test_localizable_title);
181         c = res.query(media2Uri, new String[] { "title" }, null, null, null);
182         assertEquals(1, c.getCount());
183         c.moveToFirst();
184         assertEquals(localizedTitle, c.getString(0));
185 
186         mMediaScannerConnection.disconnect();
187         c.close();
188     }
189 
testMediaScanner()190     public void testMediaScanner() throws InterruptedException, IOException {
191         mMediaScannerConnectionClient = new MockMediaScannerConnectionClient();
192         mMediaScannerConnection = new MockMediaScannerConnection(getContext(),
193                                     mMediaScannerConnectionClient);
194 
195         assertFalse(mMediaScannerConnection.isConnected());
196 
197         // start connection and wait until connected
198         mMediaScannerConnection.connect();
199         checkConnectionState(true);
200 
201         // start and wait for scan
202         mMediaScannerConnection.scanFile(mMediaFile.getAbsolutePath(), MEDIA_TYPE);
203         checkMediaScannerConnection();
204 
205         Uri insertUri = mMediaScannerConnectionClient.mediaUri;
206         long id = Long.valueOf(insertUri.getLastPathSegment());
207         ContentResolver res = mContext.getContentResolver();
208 
209         // check that the file ended up in the audio view
210         Cursor c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null,
211                 MediaColumns.DATA + "=?", new String[] { mMediaFile.getAbsolutePath() }, null);
212         assertEquals(1, c.getCount());
213         c.close();
214 
215         // add nomedia file and insert into database, file should no longer be in audio view
216         File nomedia = new File(mMediaFile.getParent() + "/.nomedia");
217         nomedia.createNewFile();
218         startMediaScanAndWait();
219 
220         // entry should not be in audio view anymore
221         c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null,
222                 MediaColumns.DATA + "=?", new String[] { mMediaFile.getAbsolutePath() }, null);
223         assertEquals(0, c.getCount());
224         c.close();
225 
226         // with nomedia file removed, do media scan and check that entry is in audio table again
227         nomedia.delete();
228         startMediaScanAndWait();
229 
230         // Give the 2nd stage scan that makes the unhidden files visible again
231         // a little more time
232         SystemClock.sleep(10000);
233         // entry should be in audio view again
234         c = res.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null,
235                 MediaColumns.DATA + "=?", new String[] { mMediaFile.getAbsolutePath() }, null);
236         assertEquals(1, c.getCount());
237         c.close();
238 
239         // ensure that we don't currently have playlists named ctsmediascanplaylist*
240         res.delete(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
241                 MediaStore.Audio.PlaylistsColumns.NAME + "=?",
242                 new String[] { "ctsmediascanplaylist1"});
243         res.delete(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI,
244                 MediaStore.Audio.PlaylistsColumns.NAME + "=?",
245                 new String[] { "ctsmediascanplaylist2"});
246         // delete the playlist file entries, if they exist
247         res.delete(MediaStore.Files.getContentUri("external"),
248                 MediaStore.Files.FileColumns.DATA + "=?",
249                 new String[] { mFileDir + "/ctsmediascanplaylist1.pls"});
250         res.delete(MediaStore.Files.getContentUri("external"),
251                 MediaStore.Files.FileColumns.DATA + "=?",
252                 new String[] { mFileDir + "/ctsmediascanplaylist2.m3u"});
253 
254         // write some more files
255         writeFile("testmp3.mp3", mFileDir + "/testmp3.mp3");
256         writeFile("testmp3_2.mp3", mFileDir + "/testmp3_2.mp3");
257         writeFile("playlist1.pls", mFileDir + "/ctsmediascanplaylist1.pls");
258         writeFile("playlist2.m3u", mFileDir + "/ctsmediascanplaylist2.m3u");
259 
260         startMediaScanAndWait();
261 
262         // verify that the two playlists were created correctly;
263         c = res.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null,
264                 MediaStore.Audio.PlaylistsColumns.NAME + "=?",
265                 new String[] { "ctsmediascanplaylist1"}, null);
266         assertEquals(1, c.getCount());
267         c.moveToFirst();
268         long playlistid = c.getLong(c.getColumnIndex(MediaStore.MediaColumns._ID));
269         c.close();
270 
271         c = res.query(MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid),
272                 null, null, null, MediaStore.Audio.Playlists.Members.PLAY_ORDER);
273         assertEquals(2, c.getCount());
274         c.moveToNext();
275         long song1a = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID));
276         c.moveToNext();
277         long song1b = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID));
278         c.close();
279         assertTrue("song id should not be 0", song1a != 0);
280         assertTrue("song id should not be 0", song1b != 0);
281         assertTrue("song ids should not be same", song1a != song1b);
282 
283         // 2nd playlist should have the same songs, in reverse order
284         c = res.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null,
285                 MediaStore.Audio.PlaylistsColumns.NAME + "=?",
286                 new String[] { "ctsmediascanplaylist2"}, null);
287         assertEquals(1, c.getCount());
288         c.moveToFirst();
289         playlistid = c.getLong(c.getColumnIndex(MediaStore.MediaColumns._ID));
290         c.close();
291 
292         c = res.query(MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid),
293                 null, null, null, MediaStore.Audio.Playlists.Members.PLAY_ORDER);
294         assertEquals(2, c.getCount());
295         c.moveToNext();
296         long song2a = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID));
297         c.moveToNext();
298         long song2b = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID));
299         c.close();
300         assertEquals("mismatched song ids", song1a, song2b);
301         assertEquals("mismatched song ids", song2a, song1b);
302 
303         mMediaScannerConnection.disconnect();
304 
305         checkConnectionState(false);
306     }
307 
testWildcardPaths()308     public void testWildcardPaths() throws Exception {
309         mMediaScannerConnectionClient = new MockMediaScannerConnectionClient();
310         mMediaScannerConnection = new MockMediaScannerConnection(getContext(),
311                                     mMediaScannerConnectionClient);
312 
313         assertFalse(mMediaScannerConnection.isConnected());
314 
315         // start connection and wait until connected
316         mMediaScannerConnection.connect();
317         checkConnectionState(true);
318 
319         long now = System.currentTimeMillis();
320         String dir1 = mFileDir + "/test-" + now;
321         String file1 = dir1 + "/test.mp3";
322         String dir2 = mFileDir + "/test_" + now;
323         String file2 = dir2 + "/test.mp3";
324         assertTrue(new File(dir1).mkdir());
325         writeFile("testmp3.mp3", file1);
326         mMediaScannerConnection.scanFile(file1, MEDIA_TYPE);
327         checkMediaScannerConnection();
328         Uri file1Uri = mMediaScannerConnectionClient.mediaUri;
329 
330         assertTrue(new File(dir2).mkdir());
331         writeFile("testmp3.mp3", file2);
332         mMediaScannerConnectionClient.reset();
333         mMediaScannerConnection.scanFile(file2, MEDIA_TYPE);
334         checkMediaScannerConnection();
335         Uri file2Uri = mMediaScannerConnectionClient.mediaUri;
336 
337         // if the URIs are the same, then the media scanner likely treated the _ character
338         // in the second path as a wildcard, and matched it with the first path
339         assertFalse(file1Uri.equals(file2Uri));
340 
341         // rewrite Uris to use the file scheme
342         long file1id = Long.valueOf(file1Uri.getLastPathSegment());
343         long file2id = Long.valueOf(file2Uri.getLastPathSegment());
344         file1Uri = MediaStore.Files.getContentUri("external", file1id);
345         file2Uri = MediaStore.Files.getContentUri("external", file2id);
346 
347         ContentResolver res = mContext.getContentResolver();
348         Cursor c = res.query(file1Uri, new String[] { "parent" }, null, null, null);
349         c.moveToFirst();
350         long parent1id = c.getLong(0);
351         c.close();
352         c = res.query(file2Uri, new String[] { "parent" }, null, null, null);
353         c.moveToFirst();
354         long parent2id = c.getLong(0);
355         c.close();
356         // if the parent ids are the same, then the media provider likely
357         // treated the _ character in the second path as a wildcard
358         assertTrue("same parent", parent1id != parent2id);
359 
360         // check the parent paths are correct
361 
362         assertEquals(dir1, getRawFile(MediaStore.Files.getContentUri("external", parent1id))
363                 .getAbsolutePath());
364         assertEquals(dir2, getRawFile(MediaStore.Files.getContentUri("external", parent2id))
365                 .getAbsolutePath());
366 
367         // clean up
368         new File(file1).delete();
369         new File(dir1).delete();
370         new File(file2).delete();
371         new File(dir2).delete();
372         res.delete(file1Uri, null, null);
373         res.delete(file2Uri, null, null);
374         res.delete(MediaStore.Files.getContentUri("external", parent1id), null, null);
375         res.delete(MediaStore.Files.getContentUri("external", parent2id), null, null);
376 
377         mMediaScannerConnection.disconnect();
378 
379         checkConnectionState(false);
380     }
381 
testCanonicalize()382     public void testCanonicalize() throws Exception {
383         mMediaScannerConnectionClient = new MockMediaScannerConnectionClient();
384         mMediaScannerConnection = new MockMediaScannerConnection(getContext(),
385                                     mMediaScannerConnectionClient);
386 
387         assertFalse(mMediaScannerConnection.isConnected());
388 
389         // start connection and wait until connected
390         mMediaScannerConnection.connect();
391         checkConnectionState(true);
392 
393         // test unlocalizable file
394         // testcanonicalize_mp3 has an ID3 title that is unique to this test.
395         // Do not use this clip for any other test and do not copy this to sdcard
396         // while running the test
397         canonicalizeTest(R.raw.testcanonicalize_mp3);
398 
399         mMediaScannerConnectionClient.reset();
400 
401         // test localizable file
402         // testcanonicalize_localizable_mp3 has an ID3 title that is unique to this test.
403         // Do not use this clip for any other test and do not copy this to sdcard
404         // while running the test
405         canonicalizeTest(R.raw.testcanonicalize_localizable_mp3);
406     }
407 
canonicalizeTest(int resId)408     private void canonicalizeTest(int resId) throws Exception {
409         // write file and scan to insert into database
410         String fileDir = mFileDir + "/canonicaltest-" + System.currentTimeMillis();
411         String fileName = fileDir + "/test.mp3";
412         writeFile(resId, fileName);
413         mMediaScannerConnection.scanFile(fileName, MEDIA_TYPE);
414         checkMediaScannerConnection();
415 
416         // check path and uri
417         Uri uri = mMediaScannerConnectionClient.mediaUri;
418         String path = mMediaScannerConnectionClient.mediaPath;
419         assertEquals(fileName, path);
420         assertNotNull(uri);
421 
422         // check canonicalization
423         ContentResolver res = mContext.getContentResolver();
424         Uri canonicalUri = res.canonicalize(uri);
425         assertNotNull(canonicalUri);
426         assertFalse(uri.equals(canonicalUri));
427         Uri uncanonicalizedUri = res.uncanonicalize(canonicalUri);
428         assertEquals(uri, uncanonicalizedUri);
429 
430         // remove the entry from the database
431         assertEquals(1, res.delete(uri, null, null));
432 
433         // write same file again and scan to insert into database
434         mMediaScannerConnectionClient.reset();
435         String fileName2 = fileDir + "/test2.mp3";
436         writeFile(resId, fileName2);
437         mMediaScannerConnection.scanFile(fileName2, MEDIA_TYPE);
438         checkMediaScannerConnection();
439 
440         // check path and uri
441         Uri uri2 = mMediaScannerConnectionClient.mediaUri;
442         String path2 = mMediaScannerConnectionClient.mediaPath;
443         assertEquals(fileName2, path2);
444         assertNotNull(uri2);
445 
446         // this should be a different entry in the database and not re-use the same database id
447         assertFalse(uri.equals(uri2));
448 
449         Uri canonicalUri2 = res.canonicalize(uri2);
450         assertNotNull(canonicalUri2);
451         assertFalse(uri2.equals(canonicalUri2));
452         Uri uncanonicalizedUri2 = res.uncanonicalize(canonicalUri2);
453         assertEquals(uri2, uncanonicalizedUri2);
454 
455         // uncanonicalize the original canonicalized uri, it should resolve to the new uri
456         Uri uncanonicalizedUri3 = res.uncanonicalize(canonicalUri);
457         assertEquals(uri2, uncanonicalizedUri3);
458 
459         assertEquals(1, res.delete(uri2, null, null));
460     }
461 
462     static class MediaScanEntry {
MediaScanEntry(String r, String[] t)463         MediaScanEntry(String r, String[] t) {
464             this.fileName = r;
465             this.tags = t;
466         }
467         final String fileName;
468         String[] tags;
469     }
470 
471     MediaScanEntry encodingtestfiles[] = {
472             new MediaScanEntry("gb18030_1.mp3",
473                     new String[] {"罗志祥", "2009年11月新歌", "罗志祥", "爱不单行(TV Version)", null} ),
474             new MediaScanEntry("gb18030_2.mp3",
475                     new String[] {"张杰", "明天过后", null, "明天过后", null} ),
476             new MediaScanEntry("gb18030_3.mp3",
477                     new String[] {"电视原声带", "格斗天王(限量精装版)(预购版)", null, "11.Open Arms.( cn808.net )", null} ),
478             new MediaScanEntry("gb18030_4.mp3",
479                     new String[] {"莫扎特", "黄金古典", "柏林爱乐乐团", "第25号交响曲", "莫扎特"} ),
480             new MediaScanEntry("gb18030_6.mp3",
481                     new String[] {"张韶涵", "潘朵拉", "張韶涵", "隐形的翅膀", "王雅君"} ),
482             new MediaScanEntry("gb18030_7.mp3", // this is actually utf-8
483                     new String[] {"五月天", "后青春期的诗", null, "突然好想你", null} ),
484             new MediaScanEntry("gb18030_8.mp3",
485                     new String[] {"周杰伦", "Jay", null, "反方向的钟", null} ),
486             new MediaScanEntry("big5_1.mp3",
487                     new String[] {"蘇永康", "So I Sing 08 Live", "蘇永康", "囍帖街", null} ),
488             new MediaScanEntry("big5_2.mp3",
489                     new String[] {"蘇永康", "So I Sing 08 Live", "蘇永康", "從不喜歡孤單一個 - 蘇永康/吳雨霏", null} ),
490             new MediaScanEntry("cp1251_v1.mp3",
491                     new String[] {"Екатерина Железнова", "Корабль игрушек", null, "Раз, два, три", null} ),
492             new MediaScanEntry("cp1251_v1v2.mp3",
493                     new String[] {"Мельница", "Перевал", null, "Королевна", null} ),
494             new MediaScanEntry("cp1251_3.mp3",
495                     new String[] {"Тату (tATu)", "200 По Встречной [Limited edi", null, "Я Сошла С Ума", null} ),
496             // The following 3 use cp1251 encoding, expanded to 16 bits and stored as utf16
497             new MediaScanEntry("cp1251_4.mp3",
498                     new String[] {"Александр Розенбаум", "Философия любви", null, "Разговор в гостинице (Как жить без веры)", "А.Розенбаум"} ),
499             new MediaScanEntry("cp1251_5.mp3",
500                     new String[] {"Александр Розенбаум", "Философия любви", null, "Четвертиночка", "А.Розенбаум"} ),
501             new MediaScanEntry("cp1251_6.mp3",
502                     new String[] {"Александр Розенбаум", "Философия ремесла", null, "Ну, вот...", "А.Розенбаум"} ),
503             new MediaScanEntry("cp1251_7.mp3",
504                     new String[] {"Вопли Видоплясова", "Хвилі Амура", null, "Або або", null} ),
505             new MediaScanEntry("cp1251_8.mp3",
506                     new String[] {"Вопли Видоплясова", "Хвилі Амура", null, "Таємнi сфери", null} ),
507             new MediaScanEntry("shiftjis1.mp3",
508                     new String[] {"", "", null, "中島敦「山月記」(第1回)", null} ),
509             new MediaScanEntry("shiftjis2.mp3",
510                     new String[] {"音人", "SoundEffects", null, "ファンファーレ", null} ),
511             new MediaScanEntry("shiftjis3.mp3",
512                     new String[] {"音人", "SoundEffects", null, "シンキングタイム", null} ),
513             new MediaScanEntry("shiftjis4.mp3",
514                     new String[] {"音人", "SoundEffects", null, "出題", null} ),
515             new MediaScanEntry("shiftjis5.mp3",
516                     new String[] {"音人", "SoundEffects", null, "時報", null} ),
517             new MediaScanEntry("shiftjis6.mp3",
518                     new String[] {"音人", "SoundEffects", null, "正解", null} ),
519             new MediaScanEntry("shiftjis7.mp3",
520                     new String[] {"音人", "SoundEffects", null, "残念", null} ),
521             new MediaScanEntry("shiftjis8.mp3",
522                     new String[] {"音人", "SoundEffects", null, "間違い", null} ),
523             new MediaScanEntry("iso88591_1.ogg",
524                     new String[] {"Mozart", "Best of Mozart", null, "Overtüre (Die Hochzeit des Figaro)", null} ),
525             new MediaScanEntry("iso88591_2.mp3", // actually UTF16, but only uses iso8859-1 chars
526                     new String[] {"Björk", "Telegram", "Björk", "Possibly Maybe (Lucy Mix)", null} ),
527             new MediaScanEntry("hebrew.mp3",
528                     new String[] {"אריק סיני", "", null, "לי ולך", null } ),
529             new MediaScanEntry("hebrew2.mp3",
530                     new String[] {"הפרוייקט של עידן רייכל", "Untitled - 11-11-02 (9)", null, "בואי", null } ),
531             new MediaScanEntry("iso88591_3.mp3",
532                     new String[] {"Mobilé", "Kartographie", null, "Zu Wenig", null }),
533             new MediaScanEntry("iso88591_4.mp3",
534                     new String[] {"Mobilé", "Kartographie", null, "Rotebeetesalat (Igel Stehlen)", null }),
535             new MediaScanEntry("iso88591_5.mp3",
536                     new String[] {"The Creatures", "Hai! [UK Bonus DVD] Disc 1", "The Creatures", "Imagoró", null }),
537             new MediaScanEntry("iso88591_6.mp3",
538                     new String[] {"¡Forward, Russia!", "Give Me a Wall", "Forward Russia", "Fifteen, Pt. 1", "Canning/Nicholls/Sarah Nicolls/Woodhead"}),
539             new MediaScanEntry("iso88591_7.mp3",
540                     new String[] {"Björk", "Homogenic", "Björk", "Jòga", "Björk/Sjòn"}),
541             // this one has a genre of "Indé" which confused the detector
542             new MediaScanEntry("iso88591_8.mp3",
543                     new String[] {"The Black Heart Procession", "3", null, "A Heart Like Mine", null}),
544             new MediaScanEntry("iso88591_9.mp3",
545                     new String[] {"DJ Tiësto", "Just Be", "DJ Tiësto", "Adagio For Strings", "Samuel Barber"}),
546             new MediaScanEntry("iso88591_10.mp3",
547                     new String[] {"Ratatat", "LP3", null, "Bruleé", null}),
548             new MediaScanEntry("iso88591_11.mp3",
549                     new String[] {"Sempé", "Le Petit Nicolas vol. 1", null, "Les Cow-Boys", null}),
550             new MediaScanEntry("iso88591_12.mp3",
551                     new String[] {"UUVVWWZ", "UUVVWWZ", null, "Neolaño", null}),
552             new MediaScanEntry("iso88591_13.mp3",
553                     new String[] {"Michael Bublé", "Crazy Love", "Michael Bublé", "Haven't Met You Yet", null}),
554             new MediaScanEntry("utf16_1.mp3",
555                     new String[] {"Shakira", "Latin Mix USA", "Shakira", "Estoy Aquí", null}),
556             // Tags are encoded in different charsets.
557             new MediaScanEntry("iso88591_utf8_mixed_1.mp3",
558                     new String[] {"刘昊霖/kidult.", "鱼干铺里", "刘昊霖/kidult.", "Colin Wine's Mailbox", null}),
559             new MediaScanEntry("iso88591_utf8_mixed_2.mp3",
560                     new String[] {"冰块先生/郭美孜", "hey jude", "冰块先生/郭美孜", "Hey Jude", null}),
561             new MediaScanEntry("iso88591_utf8_mixed_3.mp3",
562                     new String[] {"Toy王奕/Tizzy T/满舒克", "1993", "Toy王奕/Tizzy T/满舒克", "Me&Ma Bros", null}),
563             new MediaScanEntry("gb18030_utf8_mixed_1.mp3",
564                     new String[] {"张国荣", "钟情张国荣", null, "左右手", null}),
565             new MediaScanEntry("gb18030_utf8_mixed_2.mp3",
566                     new String[] {"纵贯线", "Live in Taipei 出发\\/终点站", null, "皇后大道东(Live)", null}),
567             new MediaScanEntry("gb18030_utf8_mixed_3.mp3",
568                     new String[] {"谭咏麟", "二十年白金畅销金曲全记录", null, "知心当玩偶", null})
569     };
570 
testEncodingDetection()571     public void testEncodingDetection() throws Exception {
572         for (int i = 0; i< encodingtestfiles.length; i++) {
573             MediaScanEntry entry = encodingtestfiles[i];
574             String path =  mFileDir + "/" + entry.fileName;
575             writeFile(entry.fileName, path);
576         }
577 
578         startMediaScanAndWait();
579 
580         String columns[] = {
581                 MediaStore.Audio.Media.ARTIST,
582                 MediaStore.Audio.Media.ALBUM,
583                 MediaStore.Audio.Media.ALBUM_ARTIST,
584                 MediaStore.Audio.Media.TITLE,
585                 MediaStore.Audio.Media.COMPOSER
586         };
587         ContentResolver res = mContext.getContentResolver();
588         for (int i = 0; i< encodingtestfiles.length; i++) {
589             MediaScanEntry entry = encodingtestfiles[i];
590             String path =  mFileDir + "/" + entry.fileName;
591             Cursor c = res.query(MediaStore.Audio.Media.getContentUri("external"), columns,
592                     MediaStore.Audio.Media.DATA + "=?", new String[] {path}, null);
593             assertNotNull("null cursor", c);
594             assertEquals("wrong number or results", 1, c.getCount());
595             assertTrue("failed to move cursor", c.moveToFirst());
596 
597             for (int j =0; j < 5; j++) {
598                 String expected = entry.tags[j];
599                 if ("".equals(expected)) {
600                     // empty entry in the table means an unset id3 tag that is filled in by
601                     // the media scanner, e.g. by using "<unknown>". Since this may be localized,
602                     // don't check it for any particular value.
603                     assertNotNull("unexpected null entry " + i + " field " + j + "(" + path + ")",
604                             c.getString(j));
605                 } else {
606                     assertEquals("mismatch on entry " + i + " field " + j + "(" + path + ")",
607                             expected, c.getString(j));
608                 }
609             }
610             // clean up
611             new File(path).delete();
612             res.delete(MediaStore.Audio.Media.getContentUri("external"),
613                     MediaStore.Audio.Media.DATA + "=?", new String[] {path});
614 
615             c.close();
616 
617             // also test with the MediaMetadataRetriever API
618             String[] actual;
619             try (MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever()) {
620                 AssetFileDescriptor afd = getAssetFileDescriptorFor(entry.fileName);
621                 metadataRetriever.setDataSource(afd.getFileDescriptor(),
622                         afd.getStartOffset(), afd.getDeclaredLength());
623 
624                 actual = new String[5];
625                 actual[0] = metadataRetriever.extractMetadata(
626                         MediaMetadataRetriever.METADATA_KEY_ARTIST);
627                 actual[1] = metadataRetriever.extractMetadata(
628                         MediaMetadataRetriever.METADATA_KEY_ALBUM);
629                 actual[2] = metadataRetriever.extractMetadata(
630                         MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
631                 actual[3] = metadataRetriever.extractMetadata(
632                         MediaMetadataRetriever.METADATA_KEY_TITLE);
633                 actual[4] = metadataRetriever.extractMetadata(
634                         MediaMetadataRetriever.METADATA_KEY_COMPOSER);
635             }
636 
637             for (int j = 0; j < 5; j++) {
638                 if ("".equals(entry.tags[j])) {
639                     // retriever doesn't insert "unknown artist" and such, it just returns null
640                     assertNull("retriever: unexpected non-null for entry " + i + " field " + j,
641                             actual[j]);
642                 } else {
643                     Log.i("@@@", "tags: @@" + entry.tags[j] + "@@" + actual[j] + "@@");
644                     assertEquals("retriever: mismatch on entry " + i + " field " + j,
645                             entry.tags[j], actual[j]);
646                 }
647             }
648         }
649     }
650 
scanVolume()651     private static void scanVolume() {
652         if (ApiLevelUtil.isAtLeast(Build.VERSION_CODES.R)) {
653             MediaStore.scanVolume(InstrumentationRegistry.getTargetContext().getContentResolver(),
654                     MediaStore.VOLUME_EXTERNAL_PRIMARY);
655         } else {
656             // on Q, scanVolume(Context, String path) should be used
657             try {
658                 Method scanVolumeMethod = MediaStore.class
659                     .getMethod("scanVolume", Context.class, File.class);
660                 scanVolumeMethod.invoke(null,
661                         InstrumentationRegistry.getTargetContext(),
662                         Environment.getExternalStorageDirectory());
663             } catch (Exception ex) {
664                 fail("could not find scanVolume method" + ex);
665             }
666         }
667     }
668 
startMediaScan()669     public static void startMediaScan() {
670         new Thread(() -> { scanVolume(); }).start();
671     }
672 
startMediaScanAndWait()673     public static void startMediaScanAndWait() {
674         scanVolume();
675     }
676 
checkMediaScannerConnection()677     private void checkMediaScannerConnection() {
678         new PollingCheck(TIME_OUT) {
679             protected boolean check() {
680                 return mMediaScannerConnectionClient.isOnMediaScannerConnectedCalled;
681             }
682         }.run();
683         new PollingCheck(TIME_OUT) {
684             protected boolean check() {
685                 return mMediaScannerConnectionClient.mediaPath != null;
686             }
687         }.run();
688     }
689 
checkConnectionState(final boolean expected)690     private void checkConnectionState(final boolean expected) {
691         new PollingCheck(TIME_OUT) {
692             protected boolean check() {
693                 return mMediaScannerConnection.isConnected() == expected;
694             }
695         }.run();
696     }
697 
698     class MockMediaScannerConnection extends MediaScannerConnection {
699 
700         public boolean mIsOnServiceConnectedCalled;
701         public boolean mIsOnServiceDisconnectedCalled;
MockMediaScannerConnection(Context context, MediaScannerConnectionClient client)702         public MockMediaScannerConnection(Context context, MediaScannerConnectionClient client) {
703             super(context, client);
704         }
705 
706         @Override
onServiceConnected(ComponentName className, IBinder service)707         public void onServiceConnected(ComponentName className, IBinder service) {
708             super.onServiceConnected(className, service);
709             mIsOnServiceConnectedCalled = true;
710         }
711 
712         @Override
onServiceDisconnected(ComponentName className)713         public void onServiceDisconnected(ComponentName className) {
714             super.onServiceDisconnected(className);
715             mIsOnServiceDisconnectedCalled = true;
716             // this is not called.
717         }
718     }
719 
720     class MockMediaScannerConnectionClient implements MediaScannerConnectionClient {
721 
722         public boolean isOnMediaScannerConnectedCalled;
723         public String mediaPath;
724         public Uri mediaUri;
onMediaScannerConnected()725         public void onMediaScannerConnected() {
726             isOnMediaScannerConnectedCalled = true;
727         }
728 
onScanCompleted(String path, Uri uri)729         public void onScanCompleted(String path, Uri uri) {
730             Log.v("MediaScannerTest", "onScanCompleted for " + path + " to " + uri);
731             mediaPath = path;
732             if (uri != null) {
733                 mediaUri = uri;
734             }
735         }
736 
reset()737         public void reset() {
738             mediaPath = null;
739             mediaUri = null;
740         }
741     }
742 
getRawFile(Uri uri)743     static File getRawFile(Uri uri) throws Exception {
744         final String res = executeShellCommand(
745                 "content query --uri " + uri
746                         + " --user " + getCurrentUser() + " --projection _data",
747                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
748         final int i = res.indexOf("_data=");
749         if (i >= 0) {
750             return new File(res.substring(i + 6));
751         } else {
752             throw new FileNotFoundException("Failed to find _data for " + uri + "; found " + res);
753         }
754     }
755 
executeShellCommand(String command)756     static String executeShellCommand(String command) throws IOException {
757         return executeShellCommand(command,
758                 InstrumentationRegistry.getInstrumentation().getUiAutomation());
759     }
760 
executeShellCommand(String command, UiAutomation uiAutomation)761     static String executeShellCommand(String command, UiAutomation uiAutomation)
762             throws IOException {
763         ParcelFileDescriptor pfd = uiAutomation.executeShellCommand(command.toString());
764         BufferedReader br = null;
765         try (InputStream in = new FileInputStream(pfd.getFileDescriptor());) {
766             br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
767             String str = null;
768             StringBuilder out = new StringBuilder();
769             while ((str = br.readLine()) != null) {
770                 out.append(str);
771             }
772             return out.toString();
773         } finally {
774             if (br != null) {
775                 br.close();
776             }
777         }
778     }
779 
getCurrentUser()780     private static int getCurrentUser() {
781         return android.os.Process.myUserHandle().getIdentifier();
782     }
783 }
784