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