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.cts; 18 19 import com.android.cts.media.R; 20 21 import android.content.ComponentName; 22 import android.content.ContentResolver; 23 import android.content.ContentUris; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.cts.util.PollingCheck; 29 import android.database.Cursor; 30 import android.media.MediaScannerConnection; 31 import android.media.MediaScannerConnection.MediaScannerConnectionClient; 32 import android.mtp.MtpConstants; 33 import android.net.Uri; 34 import android.os.Bundle; 35 import android.os.Environment; 36 import android.os.IBinder; 37 import android.os.SystemClock; 38 import android.provider.MediaStore; 39 import android.provider.cts.FileCopyHelper; 40 import android.test.AndroidTestCase; 41 import android.util.Log; 42 43 import java.io.File; 44 import java.io.IOException; 45 46 public class MediaScannerTest extends AndroidTestCase { 47 48 private static final String MEDIA_TYPE = "audio/mpeg"; 49 private File mMediaFile; 50 private static final int TIME_OUT = 10000; 51 private MockMediaScannerConnection mMediaScannerConnection; 52 private MockMediaScannerConnectionClient mMediaScannerConnectionClient; 53 private String mFileDir; 54 55 @Override 56 protected void setUp() throws Exception { 57 super.setUp(); 58 // prepare the media file. 59 60 mFileDir = Environment.getExternalStorageDirectory() + "/" + getClass().getCanonicalName(); 61 cleanup(); 62 String fileName = mFileDir + "/test" + System.currentTimeMillis() + ".mp3"; 63 writeFile(R.raw.testmp3, fileName); 64 65 mMediaFile = new File(fileName); 66 assertTrue(mMediaFile.exists()); 67 } 68 69 private void writeFile(int resid, String path) throws IOException { 70 File out = new File(path); 71 File dir = out.getParentFile(); 72 dir.mkdirs(); 73 FileCopyHelper copier = new FileCopyHelper(mContext); 74 copier.copyToExternalStorage(resid, out); 75 } 76 77 @Override 78 protected void tearDown() throws Exception { 79 super.tearDown(); 80 } 81 82 private void cleanup() { 83 if (mMediaFile != null) { 84 mMediaFile.delete(); 85 } 86 if (mFileDir != null) { 87 new File(mFileDir + "/testmp3.mp3").delete(); 88 new File(mFileDir + "/testmp3_2.mp3").delete(); 89 new File(mFileDir + "/ctsmediascanplaylist1.pls").delete(); 90 new File(mFileDir + "/ctsmediascanplaylist2.m3u").delete(); 91 new File(mFileDir).delete(); 92 } 93 94 if (mMediaScannerConnection != null) { 95 mMediaScannerConnection.disconnect(); 96 mMediaScannerConnection = null; 97 } 98 99 mContext.getContentResolver().delete(MediaStore.Audio.Media.getContentUri("external"), 100 "_data like ?", new String[] { mFileDir + "%"}); 101 } 102 103 public void testMediaScanner() throws InterruptedException, IOException { 104 mMediaScannerConnectionClient = new MockMediaScannerConnectionClient(); 105 mMediaScannerConnection = new MockMediaScannerConnection(getContext(), 106 mMediaScannerConnectionClient); 107 108 assertFalse(mMediaScannerConnection.isConnected()); 109 110 // start connection and wait until connected 111 mMediaScannerConnection.connect(); 112 checkConnectionState(true); 113 114 // start and wait for scan 115 mMediaScannerConnection.scanFile(mMediaFile.getAbsolutePath(), MEDIA_TYPE); 116 checkMediaScannerConnection(); 117 118 Uri insertUri = mMediaScannerConnectionClient.mediaUri; 119 long id = Long.valueOf(insertUri.getLastPathSegment()); 120 ContentResolver res = mContext.getContentResolver(); 121 122 // check that the file ended up in the audio view 123 Uri audiouri = ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id); 124 Cursor c = res.query(audiouri, null, null, null, null); 125 assertEquals(1, c.getCount()); 126 c.close(); 127 128 // add nomedia file and insert into database, file should no longer be in audio view 129 File nomedia = new File(mMediaFile.getParent() + "/.nomedia"); 130 nomedia.createNewFile(); 131 ContentValues values = new ContentValues(); 132 values.put(MediaStore.MediaColumns.DATA, nomedia.getAbsolutePath()); 133 values.put(MediaStore.Files.FileColumns.FORMAT, MtpConstants.FORMAT_UNDEFINED); 134 Uri nomediauri = res.insert(MediaStore.Files.getContentUri("external"), values); 135 // clean up nomedia file 136 nomedia.delete(); 137 138 // entry should not be in audio view anymore 139 c = res.query(audiouri, null, null, null, null); 140 assertEquals(0, c.getCount()); 141 c.close(); 142 143 // with nomedia file removed, do media scan and check that entry is in audio table again 144 startMediaScanAndWait(); 145 146 // Give the 2nd stage scan that makes the unhidden files visible again 147 // a little more time 148 SystemClock.sleep(10000); 149 // entry should be in audio view again 150 c = res.query(audiouri, null, null, null, null); 151 assertEquals(1, c.getCount()); 152 c.close(); 153 154 // ensure that we don't currently have playlists named ctsmediascanplaylist* 155 res.delete(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, 156 MediaStore.Audio.PlaylistsColumns.NAME + "=?", 157 new String[] { "ctsmediascanplaylist1"}); 158 res.delete(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, 159 MediaStore.Audio.PlaylistsColumns.NAME + "=?", 160 new String[] { "ctsmediascanplaylist2"}); 161 // delete the playlist file entries, if they exist 162 res.delete(MediaStore.Files.getContentUri("external"), 163 MediaStore.Files.FileColumns.DATA + "=?", 164 new String[] { mFileDir + "/ctsmediascanplaylist1.pls"}); 165 res.delete(MediaStore.Files.getContentUri("external"), 166 MediaStore.Files.FileColumns.DATA + "=?", 167 new String[] { mFileDir + "/ctsmediascanplaylist2.m3u"}); 168 169 // write some more files 170 writeFile(R.raw.testmp3, mFileDir + "/testmp3.mp3"); 171 writeFile(R.raw.testmp3_2, mFileDir + "/testmp3_2.mp3"); 172 writeFile(R.raw.playlist1, mFileDir + "/ctsmediascanplaylist1.pls"); 173 writeFile(R.raw.playlist2, mFileDir + "/ctsmediascanplaylist2.m3u"); 174 175 startMediaScanAndWait(); 176 177 // verify that the two playlists were created correctly; 178 c = res.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null, 179 MediaStore.Audio.PlaylistsColumns.NAME + "=?", 180 new String[] { "ctsmediascanplaylist1"}, null); 181 assertEquals(1, c.getCount()); 182 c.moveToFirst(); 183 long playlistid = c.getLong(c.getColumnIndex(MediaStore.MediaColumns._ID)); 184 c.close(); 185 186 c = res.query(MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid), 187 null, null, null, MediaStore.Audio.Playlists.Members.PLAY_ORDER); 188 assertEquals(2, c.getCount()); 189 c.moveToNext(); 190 long song1a = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID)); 191 c.moveToNext(); 192 long song1b = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID)); 193 c.close(); 194 assertTrue("song id should not be 0", song1a != 0); 195 assertTrue("song id should not be 0", song1b != 0); 196 assertTrue("song ids should not be same", song1a != song1b); 197 198 // 2nd playlist should have the same songs, in reverse order 199 c = res.query(MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI, null, 200 MediaStore.Audio.PlaylistsColumns.NAME + "=?", 201 new String[] { "ctsmediascanplaylist2"}, null); 202 assertEquals(1, c.getCount()); 203 c.moveToFirst(); 204 playlistid = c.getLong(c.getColumnIndex(MediaStore.MediaColumns._ID)); 205 c.close(); 206 207 c = res.query(MediaStore.Audio.Playlists.Members.getContentUri("external", playlistid), 208 null, null, null, MediaStore.Audio.Playlists.Members.PLAY_ORDER); 209 assertEquals(2, c.getCount()); 210 c.moveToNext(); 211 long song2a = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID)); 212 c.moveToNext(); 213 long song2b = c.getLong(c.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID)); 214 c.close(); 215 assertEquals("mismatched song ids", song1a, song2b); 216 assertEquals("mismatched song ids", song2a, song1b); 217 218 mMediaScannerConnection.disconnect(); 219 220 checkConnectionState(false); 221 } 222 223 public void testWildcardPaths() throws InterruptedException, IOException { 224 mMediaScannerConnectionClient = new MockMediaScannerConnectionClient(); 225 mMediaScannerConnection = new MockMediaScannerConnection(getContext(), 226 mMediaScannerConnectionClient); 227 228 assertFalse(mMediaScannerConnection.isConnected()); 229 230 // start connection and wait until connected 231 mMediaScannerConnection.connect(); 232 checkConnectionState(true); 233 234 long now = System.currentTimeMillis(); 235 String dir1 = mFileDir + "/test-" + now; 236 String file1 = dir1 + "/test.mp3"; 237 String dir2 = mFileDir + "/test_" + now; 238 String file2 = dir2 + "/test.mp3"; 239 assertTrue(new File(dir1).mkdir()); 240 writeFile(R.raw.testmp3, file1); 241 mMediaScannerConnection.scanFile(file1, MEDIA_TYPE); 242 checkMediaScannerConnection(); 243 Uri file1Uri = mMediaScannerConnectionClient.mediaUri; 244 245 assertTrue(new File(dir2).mkdir()); 246 writeFile(R.raw.testmp3, file2); 247 mMediaScannerConnectionClient.reset(); 248 mMediaScannerConnection.scanFile(file2, MEDIA_TYPE); 249 checkMediaScannerConnection(); 250 Uri file2Uri = mMediaScannerConnectionClient.mediaUri; 251 252 // if the URIs are the same, then the media scanner likely treated the _ character 253 // in the second path as a wildcard, and matched it with the first path 254 assertFalse(file1Uri.equals(file2Uri)); 255 256 // rewrite Uris to use the file scheme 257 long file1id = Long.valueOf(file1Uri.getLastPathSegment()); 258 long file2id = Long.valueOf(file2Uri.getLastPathSegment()); 259 file1Uri = MediaStore.Files.getContentUri("external", file1id); 260 file2Uri = MediaStore.Files.getContentUri("external", file2id); 261 262 ContentResolver res = mContext.getContentResolver(); 263 Cursor c = res.query(file1Uri, new String[] { "parent" }, null, null, null); 264 c.moveToFirst(); 265 long parent1id = c.getLong(0); 266 c.close(); 267 c = res.query(file2Uri, new String[] { "parent" }, null, null, null); 268 c.moveToFirst(); 269 long parent2id = c.getLong(0); 270 c.close(); 271 // if the parent ids are the same, then the media provider likely 272 // treated the _ character in the second path as a wildcard 273 assertTrue("same parent", parent1id != parent2id); 274 275 // check the parent paths are correct 276 c = res.query(MediaStore.Files.getContentUri("external", parent1id), 277 new String[] { "_data" }, null, null, null); 278 c.moveToFirst(); 279 assertEquals(dir1, c.getString(0)); 280 c.close(); 281 282 c = res.query(MediaStore.Files.getContentUri("external", parent2id), 283 new String[] { "_data" }, null, null, null); 284 c.moveToFirst(); 285 assertEquals(dir2, c.getString(0)); 286 c.close(); 287 288 // clean up 289 new File(file1).delete(); 290 new File(dir1).delete(); 291 new File(file2).delete(); 292 new File(dir2).delete(); 293 res.delete(file1Uri, null, null); 294 res.delete(file2Uri, null, null); 295 res.delete(MediaStore.Files.getContentUri("external", parent1id), null, null); 296 res.delete(MediaStore.Files.getContentUri("external", parent2id), null, null); 297 298 mMediaScannerConnection.disconnect(); 299 300 checkConnectionState(false); 301 } 302 303 public void testCanonicalize() throws Exception { 304 mMediaScannerConnectionClient = new MockMediaScannerConnectionClient(); 305 mMediaScannerConnection = new MockMediaScannerConnection(getContext(), 306 mMediaScannerConnectionClient); 307 308 assertFalse(mMediaScannerConnection.isConnected()); 309 310 // start connection and wait until connected 311 mMediaScannerConnection.connect(); 312 checkConnectionState(true); 313 314 // write file and scan to insert into database 315 String fileDir = Environment.getExternalStorageDirectory() + "/" 316 + getClass().getCanonicalName() + "/canonicaltest-" + System.currentTimeMillis(); 317 String fileName = fileDir + "/test.mp3"; 318 writeFile(R.raw.testmp3, fileName); 319 mMediaScannerConnection.scanFile(fileName, MEDIA_TYPE); 320 checkMediaScannerConnection(); 321 322 // check path and uri 323 Uri uri = mMediaScannerConnectionClient.mediaUri; 324 String path = mMediaScannerConnectionClient.mediaPath; 325 assertEquals(fileName, path); 326 assertNotNull(uri); 327 328 // check canonicalization 329 ContentResolver res = mContext.getContentResolver(); 330 Uri canonicalUri = res.canonicalize(uri); 331 assertNotNull(canonicalUri); 332 assertFalse(uri.equals(canonicalUri)); 333 Uri uncanonicalizedUri = res.uncanonicalize(canonicalUri); 334 assertEquals(uri, uncanonicalizedUri); 335 336 // remove the entry from the database 337 assertEquals(1, res.delete(uri, null, null)); 338 assertTrue(new File(path).delete()); 339 340 // write same file again and scan to insert into database 341 mMediaScannerConnectionClient.reset(); 342 String fileName2 = fileDir + "/test2.mp3"; 343 writeFile(R.raw.testmp3, fileName2); 344 mMediaScannerConnection.scanFile(fileName2, MEDIA_TYPE); 345 checkMediaScannerConnection(); 346 347 // check path and uri 348 Uri uri2 = mMediaScannerConnectionClient.mediaUri; 349 String path2 = mMediaScannerConnectionClient.mediaPath; 350 assertEquals(fileName2, path2); 351 assertNotNull(uri2); 352 353 // this should be a different entry in the database and not re-use the same database id 354 assertFalse(uri.equals(uri2)); 355 356 Uri canonicalUri2 = res.canonicalize(uri2); 357 assertNotNull(canonicalUri2); 358 assertFalse(uri2.equals(canonicalUri2)); 359 Uri uncanonicalizedUri2 = res.uncanonicalize(canonicalUri2); 360 assertEquals(uri2, uncanonicalizedUri2); 361 362 // uncanonicalize the original canonicalized uri, it should resolve to the new uri 363 Uri uncanonicalizedUri3 = res.uncanonicalize(canonicalUri); 364 assertEquals(uri2, uncanonicalizedUri3); 365 366 assertEquals(1, res.delete(uri2, null, null)); 367 assertTrue(new File(path2).delete()); 368 } 369 370 private void startMediaScanAndWait() throws InterruptedException { 371 ScannerNotificationReceiver finishedReceiver = new ScannerNotificationReceiver( 372 Intent.ACTION_MEDIA_SCANNER_FINISHED); 373 IntentFilter finishedIntentFilter = new IntentFilter(Intent.ACTION_MEDIA_SCANNER_FINISHED); 374 finishedIntentFilter.addDataScheme("file"); 375 mContext.registerReceiver(finishedReceiver, finishedIntentFilter); 376 377 Bundle args = new Bundle(); 378 args.putString("volume", "external"); 379 Intent i = new Intent("android.media.IMediaScannerService").putExtras(args); 380 i.setClassName("com.android.providers.media", 381 "com.android.providers.media.MediaScannerService"); 382 mContext.startService(i); 383 384 finishedReceiver.waitForBroadcast(); 385 mContext.unregisterReceiver(finishedReceiver); 386 } 387 388 private void checkMediaScannerConnection() { 389 new PollingCheck(TIME_OUT) { 390 protected boolean check() { 391 return mMediaScannerConnectionClient.isOnMediaScannerConnectedCalled; 392 } 393 }.run(); 394 new PollingCheck(TIME_OUT) { 395 protected boolean check() { 396 return mMediaScannerConnectionClient.mediaPath != null; 397 } 398 }.run(); 399 } 400 401 private void checkConnectionState(final boolean expected) { 402 new PollingCheck(TIME_OUT) { 403 protected boolean check() { 404 return mMediaScannerConnection.isConnected() == expected; 405 } 406 }.run(); 407 } 408 409 class MockMediaScannerConnection extends MediaScannerConnection { 410 411 public boolean mIsOnServiceConnectedCalled; 412 public boolean mIsOnServiceDisconnectedCalled; 413 public MockMediaScannerConnection(Context context, MediaScannerConnectionClient client) { 414 super(context, client); 415 } 416 417 @Override 418 public void onServiceConnected(ComponentName className, IBinder service) { 419 super.onServiceConnected(className, service); 420 mIsOnServiceConnectedCalled = true; 421 } 422 423 @Override 424 public void onServiceDisconnected(ComponentName className) { 425 super.onServiceDisconnected(className); 426 mIsOnServiceDisconnectedCalled = true; 427 // this is not called. 428 } 429 } 430 431 class MockMediaScannerConnectionClient implements MediaScannerConnectionClient { 432 433 public boolean isOnMediaScannerConnectedCalled; 434 public String mediaPath; 435 public Uri mediaUri; 436 public void onMediaScannerConnected() { 437 isOnMediaScannerConnectedCalled = true; 438 } 439 440 public void onScanCompleted(String path, Uri uri) { 441 mediaPath = path; 442 if (uri != null) { 443 mediaUri = uri; 444 } 445 } 446 447 public void reset() { 448 mediaPath = null; 449 mediaUri = null; 450 } 451 } 452 453 } 454