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