MediaScanner.java revision 843ef36f7b96cc19ea7d2996b7c8661b41ec3452
1/* 2 * Copyright (C) 2007 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 17package android.media; 18 19import android.content.ContentValues; 20import android.content.Context; 21import android.content.IContentProvider; 22import android.content.ContentUris; 23import android.database.Cursor; 24import android.database.SQLException; 25import android.graphics.BitmapFactory; 26import android.net.Uri; 27import android.os.Process; 28import android.os.RemoteException; 29import android.os.SystemProperties; 30import android.provider.MediaStore; 31import android.provider.Settings; 32import android.provider.MediaStore.Audio; 33import android.provider.MediaStore.Images; 34import android.provider.MediaStore.Video; 35import android.provider.MediaStore.Audio.Genres; 36import android.provider.MediaStore.Audio.Playlists; 37import android.sax.Element; 38import android.sax.ElementListener; 39import android.sax.RootElement; 40import android.text.TextUtils; 41import android.util.Config; 42import android.util.Log; 43import android.util.Xml; 44 45import org.xml.sax.Attributes; 46import org.xml.sax.ContentHandler; 47import org.xml.sax.SAXException; 48 49import java.io.*; 50import java.util.ArrayList; 51import java.util.HashMap; 52import java.util.HashSet; 53import java.util.Iterator; 54 55/** 56 * Internal service that no-one should use directly. 57 * 58 * {@hide} 59 */ 60public class MediaScanner 61{ 62 static { 63 System.loadLibrary("media_jni"); 64 } 65 66 private final static String TAG = "MediaScanner"; 67 68 private static final String[] AUDIO_PROJECTION = new String[] { 69 Audio.Media._ID, // 0 70 Audio.Media.DATA, // 1 71 Audio.Media.DATE_MODIFIED, // 2 72 }; 73 74 private static final int ID_AUDIO_COLUMN_INDEX = 0; 75 private static final int PATH_AUDIO_COLUMN_INDEX = 1; 76 private static final int DATE_MODIFIED_AUDIO_COLUMN_INDEX = 2; 77 78 private static final String[] VIDEO_PROJECTION = new String[] { 79 Video.Media._ID, // 0 80 Video.Media.DATA, // 1 81 Video.Media.DATE_MODIFIED, // 2 82 }; 83 84 private static final int ID_VIDEO_COLUMN_INDEX = 0; 85 private static final int PATH_VIDEO_COLUMN_INDEX = 1; 86 private static final int DATE_MODIFIED_VIDEO_COLUMN_INDEX = 2; 87 88 private static final String[] IMAGES_PROJECTION = new String[] { 89 Images.Media._ID, // 0 90 Images.Media.DATA, // 1 91 Images.Media.DATE_MODIFIED, // 2 92 }; 93 94 private static final int ID_IMAGES_COLUMN_INDEX = 0; 95 private static final int PATH_IMAGES_COLUMN_INDEX = 1; 96 private static final int DATE_MODIFIED_IMAGES_COLUMN_INDEX = 2; 97 98 private static final String[] PLAYLISTS_PROJECTION = new String[] { 99 Audio.Playlists._ID, // 0 100 Audio.Playlists.DATA, // 1 101 Audio.Playlists.DATE_MODIFIED, // 2 102 }; 103 104 private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] { 105 Audio.Playlists.Members.PLAYLIST_ID, // 0 106 }; 107 108 private static final int ID_PLAYLISTS_COLUMN_INDEX = 0; 109 private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1; 110 private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2; 111 112 private static final String[] GENRE_LOOKUP_PROJECTION = new String[] { 113 Audio.Genres._ID, // 0 114 Audio.Genres.NAME, // 1 115 }; 116 117 private static final String RINGTONES_DIR = "/ringtones/"; 118 private static final String NOTIFICATIONS_DIR = "/notifications/"; 119 private static final String ALARMS_DIR = "/alarms/"; 120 private static final String MUSIC_DIR = "/music/"; 121 private static final String PODCAST_DIR = "/podcasts/"; 122 123 private static final String[] ID3_GENRES = { 124 // ID3v1 Genres 125 "Blues", 126 "Classic Rock", 127 "Country", 128 "Dance", 129 "Disco", 130 "Funk", 131 "Grunge", 132 "Hip-Hop", 133 "Jazz", 134 "Metal", 135 "New Age", 136 "Oldies", 137 "Other", 138 "Pop", 139 "R&B", 140 "Rap", 141 "Reggae", 142 "Rock", 143 "Techno", 144 "Industrial", 145 "Alternative", 146 "Ska", 147 "Death Metal", 148 "Pranks", 149 "Soundtrack", 150 "Euro-Techno", 151 "Ambient", 152 "Trip-Hop", 153 "Vocal", 154 "Jazz+Funk", 155 "Fusion", 156 "Trance", 157 "Classical", 158 "Instrumental", 159 "Acid", 160 "House", 161 "Game", 162 "Sound Clip", 163 "Gospel", 164 "Noise", 165 "AlternRock", 166 "Bass", 167 "Soul", 168 "Punk", 169 "Space", 170 "Meditative", 171 "Instrumental Pop", 172 "Instrumental Rock", 173 "Ethnic", 174 "Gothic", 175 "Darkwave", 176 "Techno-Industrial", 177 "Electronic", 178 "Pop-Folk", 179 "Eurodance", 180 "Dream", 181 "Southern Rock", 182 "Comedy", 183 "Cult", 184 "Gangsta", 185 "Top 40", 186 "Christian Rap", 187 "Pop/Funk", 188 "Jungle", 189 "Native American", 190 "Cabaret", 191 "New Wave", 192 "Psychadelic", 193 "Rave", 194 "Showtunes", 195 "Trailer", 196 "Lo-Fi", 197 "Tribal", 198 "Acid Punk", 199 "Acid Jazz", 200 "Polka", 201 "Retro", 202 "Musical", 203 "Rock & Roll", 204 "Hard Rock", 205 // The following genres are Winamp extensions 206 "Folk", 207 "Folk-Rock", 208 "National Folk", 209 "Swing", 210 "Fast Fusion", 211 "Bebob", 212 "Latin", 213 "Revival", 214 "Celtic", 215 "Bluegrass", 216 "Avantgarde", 217 "Gothic Rock", 218 "Progressive Rock", 219 "Psychedelic Rock", 220 "Symphonic Rock", 221 "Slow Rock", 222 "Big Band", 223 "Chorus", 224 "Easy Listening", 225 "Acoustic", 226 "Humour", 227 "Speech", 228 "Chanson", 229 "Opera", 230 "Chamber Music", 231 "Sonata", 232 "Symphony", 233 "Booty Bass", 234 "Primus", 235 "Porn Groove", 236 "Satire", 237 "Slow Jam", 238 "Club", 239 "Tango", 240 "Samba", 241 "Folklore", 242 "Ballad", 243 "Power Ballad", 244 "Rhythmic Soul", 245 "Freestyle", 246 "Duet", 247 "Punk Rock", 248 "Drum Solo", 249 "A capella", 250 "Euro-House", 251 "Dance Hall" 252 }; 253 254 private int mNativeContext; 255 private Context mContext; 256 private IContentProvider mMediaProvider; 257 private Uri mAudioUri; 258 private Uri mVideoUri; 259 private Uri mImagesUri; 260 private Uri mThumbsUri; 261 private Uri mGenresUri; 262 private Uri mPlaylistsUri; 263 private boolean mProcessPlaylists, mProcessGenres; 264 265 // used when scanning the image database so we know whether we have to prune 266 // old thumbnail files 267 private int mOriginalCount; 268 /** Whether the scanner has set a default sound for the ringer ringtone. */ 269 private boolean mDefaultRingtoneSet; 270 /** Whether the scanner has set a default sound for the notification ringtone. */ 271 private boolean mDefaultNotificationSet; 272 /** The filename for the default sound for the ringer ringtone. */ 273 private String mDefaultRingtoneFilename; 274 /** The filename for the default sound for the notification ringtone. */ 275 private String mDefaultNotificationFilename; 276 /** 277 * The prefix for system properties that define the default sound for 278 * ringtones. Concatenate the name of the setting from Settings 279 * to get the full system property. 280 */ 281 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; 282 283 // set to true if file path comparisons should be case insensitive. 284 // this should be set when scanning files on a case insensitive file system. 285 private boolean mCaseInsensitivePaths; 286 287 private BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); 288 289 private static class FileCacheEntry { 290 Uri mTableUri; 291 long mRowId; 292 String mPath; 293 long mLastModified; 294 boolean mSeenInFileSystem; 295 boolean mLastModifiedChanged; 296 297 FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) { 298 mTableUri = tableUri; 299 mRowId = rowId; 300 mPath = path; 301 mLastModified = lastModified; 302 mSeenInFileSystem = false; 303 mLastModifiedChanged = false; 304 } 305 306 @Override 307 public String toString() { 308 return mPath; 309 } 310 } 311 312 // hashes file path to FileCacheEntry. 313 // path should be lower case if mCaseInsensitivePaths is true 314 private HashMap<String, FileCacheEntry> mFileCache; 315 316 private ArrayList<FileCacheEntry> mPlayLists; 317 private HashMap<String, Uri> mGenreCache; 318 319 320 public MediaScanner(Context c) { 321 native_setup(); 322 mContext = c; 323 mBitmapOptions.inSampleSize = 1; 324 mBitmapOptions.inJustDecodeBounds = true; 325 326 setDefaultRingtoneFileNames(); 327 } 328 329 private void setDefaultRingtoneFileNames() { 330 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 331 + Settings.System.RINGTONE); 332 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 333 + Settings.System.NOTIFICATION_SOUND); 334 } 335 336 private MyMediaScannerClient mClient = new MyMediaScannerClient(); 337 338 private class MyMediaScannerClient implements MediaScannerClient { 339 340 private String mArtist; 341 private String mAlbumArtist; // use this if mArtist is missing 342 private String mAlbum; 343 private String mTitle; 344 private String mComposer; 345 private String mGenre; 346 private String mMimeType; 347 private int mFileType; 348 private int mTrack; 349 private int mYear; 350 private int mDuration; 351 private String mPath; 352 private long mLastModified; 353 private long mFileSize; 354 355 public FileCacheEntry beginFile(String path, String mimeType, long lastModified, long fileSize) { 356 357 // special case certain file names 358 // I use regionMatches() instead of substring() below 359 // to avoid memory allocation 360 int lastSlash = path.lastIndexOf('/'); 361 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 362 // ignore those ._* files created by MacOS 363 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 364 return null; 365 } 366 367 // ignore album art files created by Windows Media Player: 368 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg and AlbumArt_{...}_Small.jpg 369 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 370 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 371 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 372 return null; 373 } 374 int length = path.length() - lastSlash - 1; 375 if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 376 (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 377 return null; 378 } 379 } 380 } 381 382 mMimeType = null; 383 // try mimeType first, if it is specified 384 if (mimeType != null) { 385 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 386 if (mFileType != 0) { 387 mMimeType = mimeType; 388 } 389 } 390 mFileSize = fileSize; 391 392 // if mimeType was not specified, compute file type based on file extension. 393 if (mMimeType == null) { 394 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 395 if (mediaFileType != null) { 396 mFileType = mediaFileType.fileType; 397 mMimeType = mediaFileType.mimeType; 398 } 399 } 400 401 String key = path; 402 if (mCaseInsensitivePaths) { 403 key = path.toLowerCase(); 404 } 405 FileCacheEntry entry = mFileCache.get(key); 406 if (entry == null) { 407 entry = new FileCacheEntry(null, 0, path, 0); 408 mFileCache.put(key, entry); 409 } 410 entry.mSeenInFileSystem = true; 411 412 // add some slack to avoid a rounding error 413 long delta = lastModified - entry.mLastModified; 414 if (delta > 1 || delta < -1) { 415 entry.mLastModified = lastModified; 416 entry.mLastModifiedChanged = true; 417 } 418 419 if (mProcessPlaylists && MediaFile.isPlayListFileType(mFileType)) { 420 mPlayLists.add(entry); 421 // we don't process playlists in the main scan, so return null 422 return null; 423 } 424 425 // clear all the metadata 426 mArtist = null; 427 mAlbumArtist = null; 428 mAlbum = null; 429 mTitle = null; 430 mComposer = null; 431 mGenre = null; 432 mTrack = 0; 433 mYear = 0; 434 mDuration = 0; 435 mPath = path; 436 mLastModified = lastModified; 437 438 return entry; 439 } 440 441 public void scanFile(String path, long lastModified, long fileSize) { 442 doScanFile(path, null, lastModified, fileSize, false); 443 } 444 445 public void scanFile(String path, String mimeType, long lastModified, long fileSize) { 446 doScanFile(path, mimeType, lastModified, fileSize, false); 447 } 448 449 public Uri doScanFile(String path, String mimeType, long lastModified, long fileSize, boolean scanAlways) { 450 Uri result = null; 451// long t1 = System.currentTimeMillis(); 452 try { 453 FileCacheEntry entry = beginFile(path, mimeType, lastModified, fileSize); 454 // rescan for metadata if file was modified since last scan 455 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 456 String lowpath = path.toLowerCase(); 457 boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0); 458 boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 459 boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0); 460 boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0); 461 boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) || 462 (!ringtones && !notifications && !alarms && !podcasts); 463 464 if (mFileType == MediaFile.FILE_TYPE_MP3 || 465 mFileType == MediaFile.FILE_TYPE_MP4 || 466 mFileType == MediaFile.FILE_TYPE_M4A || 467 mFileType == MediaFile.FILE_TYPE_3GPP || 468 mFileType == MediaFile.FILE_TYPE_3GPP2 || 469 mFileType == MediaFile.FILE_TYPE_OGG || 470 mFileType == MediaFile.FILE_TYPE_MID || 471 mFileType == MediaFile.FILE_TYPE_WMA) { 472 // we only extract metadata from MP3, M4A, OGG, MID and WMA files. 473 // check MP4 files, to determine if they contain only audio. 474 processFile(path, mimeType, this); 475 } else if (MediaFile.isImageFileType(mFileType)) { 476 // we used to compute the width and height but it's not worth it 477 } 478 479 result = endFile(entry, ringtones, notifications, alarms, music, podcasts); 480 } 481 } catch (RemoteException e) { 482 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 483 } 484// long t2 = System.currentTimeMillis(); 485// Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 486 return result; 487 } 488 489 private int parseSubstring(String s, int start, int defaultValue) { 490 int length = s.length(); 491 if (start == length) return defaultValue; 492 493 char ch = s.charAt(start++); 494 // return defaultValue if we have no integer at all 495 if (ch < '0' || ch > '9') return defaultValue; 496 497 int result = ch - '0'; 498 while (start < length) { 499 ch = s.charAt(start++); 500 if (ch < '0' || ch > '9') return result; 501 result = result * 10 + (ch - '0'); 502 } 503 504 return result; 505 } 506 507 public void handleStringTag(String name, String value) { 508 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 509 mTitle = value.trim(); 510 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 511 mArtist = value.trim(); 512 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")) { 513 mAlbumArtist = value.trim(); 514 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 515 mAlbum = value.trim(); 516 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 517 mComposer = value.trim(); 518 } else if (name.equalsIgnoreCase("genre") || name.startsWith("genre;")) { 519 // handle numeric genres, which PV sometimes encodes like "(20)" 520 if (value.length() > 0) { 521 int genreCode = -1; 522 char ch = value.charAt(0); 523 if (ch == '(') { 524 genreCode = parseSubstring(value, 1, -1); 525 } else if (ch >= '0' && ch <= '9') { 526 genreCode = parseSubstring(value, 0, -1); 527 } 528 if (genreCode >= 0 && genreCode < ID3_GENRES.length) { 529 value = ID3_GENRES[genreCode]; 530 } 531 } 532 mGenre = value; 533 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 534 mYear = parseSubstring(value, 0, 0); 535 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 536 // track number might be of the form "2/12" 537 // we just read the number before the slash 538 int num = parseSubstring(value, 0, 0); 539 mTrack = (mTrack / 1000) * 1000 + num; 540 } else if (name.equalsIgnoreCase("discnumber") || 541 name.equals("set") || name.startsWith("set;")) { 542 // set number might be of the form "1/3" 543 // we just read the number before the slash 544 int num = parseSubstring(value, 0, 0); 545 mTrack = (num * 1000) + (mTrack % 1000); 546 } else if (name.equalsIgnoreCase("duration")) { 547 mDuration = parseSubstring(value, 0, 0); 548 } 549 } 550 551 public void setMimeType(String mimeType) { 552 mMimeType = mimeType; 553 mFileType = MediaFile.getFileTypeForMimeType(mimeType); 554 } 555 556 /** 557 * Formats the data into a values array suitable for use with the Media 558 * Content Provider. 559 * 560 * @return a map of values 561 */ 562 private ContentValues toValues() { 563 ContentValues map = new ContentValues(); 564 565 map.put(MediaStore.MediaColumns.DATA, mPath); 566 map.put(MediaStore.MediaColumns.TITLE, mTitle); 567 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 568 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 569 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 570 571 if (MediaFile.isVideoFileType(mFileType)) { 572 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 573 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 574 map.put(Video.Media.DURATION, mDuration); 575 // FIXME - add RESOLUTION 576 } else if (MediaFile.isImageFileType(mFileType)) { 577 // FIXME - add DESCRIPTION 578 // map.put(field, value); 579 } else if (MediaFile.isAudioFileType(mFileType)) { 580 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0 ? mArtist : MediaFile.UNKNOWN_STRING)); 581 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 ? mAlbum : MediaFile.UNKNOWN_STRING)); 582 map.put(Audio.Media.COMPOSER, mComposer); 583 if (mYear != 0) { 584 map.put(Audio.Media.YEAR, mYear); 585 } 586 map.put(Audio.Media.TRACK, mTrack); 587 map.put(Audio.Media.DURATION, mDuration); 588 } 589 return map; 590 } 591 592 private Uri endFile(FileCacheEntry entry, boolean ringtones, boolean notifications, 593 boolean alarms, boolean music, boolean podcasts) 594 throws RemoteException { 595 // update database 596 Uri tableUri; 597 boolean isAudio = MediaFile.isAudioFileType(mFileType); 598 boolean isVideo = MediaFile.isVideoFileType(mFileType); 599 boolean isImage = MediaFile.isImageFileType(mFileType); 600 if (isVideo) { 601 tableUri = mVideoUri; 602 } else if (isImage) { 603 tableUri = mImagesUri; 604 } else if (isAudio) { 605 tableUri = mAudioUri; 606 } else { 607 // don't add file to database if not audio, video or image 608 return null; 609 } 610 entry.mTableUri = tableUri; 611 612 // use album artist if artist is missing 613 if (mArtist == null || mArtist.length() == 0) { 614 mArtist = mAlbumArtist; 615 } 616 617 ContentValues values = toValues(); 618 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 619 if (TextUtils.isEmpty(title)) { 620 title = values.getAsString(MediaStore.MediaColumns.DATA); 621 // extract file name after last slash 622 int lastSlash = title.lastIndexOf('/'); 623 if (lastSlash >= 0) { 624 lastSlash++; 625 if (lastSlash < title.length()) { 626 title = title.substring(lastSlash); 627 } 628 } 629 // truncate the file extension (if any) 630 int lastDot = title.lastIndexOf('.'); 631 if (lastDot > 0) { 632 title = title.substring(0, lastDot); 633 } 634 values.put(MediaStore.MediaColumns.TITLE, title); 635 } 636 if (isAudio) { 637 values.put(Audio.Media.IS_RINGTONE, ringtones); 638 values.put(Audio.Media.IS_NOTIFICATION, notifications); 639 values.put(Audio.Media.IS_ALARM, alarms); 640 values.put(Audio.Media.IS_MUSIC, music); 641 values.put(Audio.Media.IS_PODCAST, podcasts); 642 } else if (isImage) { 643 // nothing right now 644 } 645 646 Uri result = null; 647 long rowId = entry.mRowId; 648 if (rowId == 0) { 649 // new file, insert it 650 result = mMediaProvider.insert(tableUri, values); 651 if (result != null) { 652 rowId = ContentUris.parseId(result); 653 entry.mRowId = rowId; 654 } 655 } else { 656 // updated file 657 result = ContentUris.withAppendedId(tableUri, rowId); 658 mMediaProvider.update(result, values, null, null); 659 } 660 if (mProcessGenres && mGenre != null) { 661 String genre = mGenre; 662 Uri uri = mGenreCache.get(genre); 663 if (uri == null) { 664 Cursor cursor = null; 665 try { 666 // see if the genre already exists 667 cursor = mMediaProvider.query( 668 mGenresUri, 669 GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 670 new String[] { genre }, null); 671 if (cursor == null || cursor.getCount() == 0) { 672 // genre does not exist, so create the genre in the genre table 673 values.clear(); 674 values.put(MediaStore.Audio.Genres.NAME, genre); 675 uri = mMediaProvider.insert(mGenresUri, values); 676 } else { 677 // genre already exists, so compute its Uri 678 cursor.moveToNext(); 679 uri = ContentUris.withAppendedId(mGenresUri, cursor.getLong(0)); 680 } 681 if (uri != null) { 682 uri = Uri.withAppendedPath(uri, Genres.Members.CONTENT_DIRECTORY); 683 mGenreCache.put(genre, uri); 684 } 685 } finally { 686 // release the cursor if it exists 687 if (cursor != null) { 688 cursor.close(); 689 } 690 } 691 } 692 693 if (uri != null) { 694 // add entry to audio_genre_map 695 values.clear(); 696 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 697 mMediaProvider.insert(uri, values); 698 } 699 } 700 701 if (notifications && !mDefaultNotificationSet) { 702 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 703 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 704 setSettingIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 705 mDefaultNotificationSet = true; 706 } 707 } else if (ringtones && !mDefaultRingtoneSet) { 708 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 709 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 710 setSettingIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 711 mDefaultRingtoneSet = true; 712 } 713 } 714 715 return result; 716 } 717 718 private boolean doesPathHaveFilename(String path, String filename) { 719 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 720 int filenameLength = filename.length(); 721 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 722 pathFilenameStart + filenameLength == path.length(); 723 } 724 725 private void setSettingIfNotSet(String settingName, Uri uri, long rowId) { 726 727 String existingSettingValue = Settings.System.getString(mContext.getContentResolver(), 728 settingName); 729 730 if (TextUtils.isEmpty(existingSettingValue)) { 731 // Set the setting to the given URI 732 Settings.System.putString(mContext.getContentResolver(), settingName, 733 ContentUris.withAppendedId(uri, rowId).toString()); 734 } 735 } 736 737 }; // end of anonymous MediaScannerClient instance 738 739 private void prescan(String filePath) throws RemoteException { 740 Cursor c = null; 741 String where = null; 742 String[] selectionArgs = null; 743 744 if (mFileCache == null) { 745 mFileCache = new HashMap<String, FileCacheEntry>(); 746 } else { 747 mFileCache.clear(); 748 } 749 if (mPlayLists == null) { 750 mPlayLists = new ArrayList<FileCacheEntry>(); 751 } else { 752 mPlayLists.clear(); 753 } 754 755 // Build the list of files from the content provider 756 try { 757 // Read existing files from the audio table 758 if (filePath != null) { 759 where = MediaStore.Audio.Media.DATA + "=?"; 760 selectionArgs = new String[] { filePath }; 761 } 762 c = mMediaProvider.query(mAudioUri, AUDIO_PROJECTION, where, selectionArgs, null); 763 764 if (c != null) { 765 try { 766 while (c.moveToNext()) { 767 long rowId = c.getLong(ID_AUDIO_COLUMN_INDEX); 768 String path = c.getString(PATH_AUDIO_COLUMN_INDEX); 769 long lastModified = c.getLong(DATE_MODIFIED_AUDIO_COLUMN_INDEX); 770 771 String key = path; 772 if (mCaseInsensitivePaths) { 773 key = path.toLowerCase(); 774 } 775 mFileCache.put(key, new FileCacheEntry(mAudioUri, rowId, path, 776 lastModified)); 777 } 778 } finally { 779 c.close(); 780 c = null; 781 } 782 } 783 784 // Read existing files from the video table 785 if (filePath != null) { 786 where = MediaStore.Video.Media.DATA + "=?"; 787 } else { 788 where = null; 789 } 790 c = mMediaProvider.query(mVideoUri, VIDEO_PROJECTION, where, selectionArgs, null); 791 792 if (c != null) { 793 try { 794 while (c.moveToNext()) { 795 long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); 796 String path = c.getString(PATH_VIDEO_COLUMN_INDEX); 797 long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); 798 799 String key = path; 800 if (mCaseInsensitivePaths) { 801 key = path.toLowerCase(); 802 } 803 mFileCache.put(key, new FileCacheEntry(mVideoUri, rowId, path, 804 lastModified)); 805 } 806 } finally { 807 c.close(); 808 c = null; 809 } 810 } 811 812 // Read existing files from the images table 813 if (filePath != null) { 814 where = MediaStore.Images.Media.DATA + "=?"; 815 } else { 816 where = null; 817 } 818 mOriginalCount = 0; 819 c = mMediaProvider.query(mImagesUri, IMAGES_PROJECTION, where, selectionArgs, null); 820 821 if (c != null) { 822 try { 823 mOriginalCount = c.getCount(); 824 while (c.moveToNext()) { 825 long rowId = c.getLong(ID_IMAGES_COLUMN_INDEX); 826 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 827 long lastModified = c.getLong(DATE_MODIFIED_IMAGES_COLUMN_INDEX); 828 829 String key = path; 830 if (mCaseInsensitivePaths) { 831 key = path.toLowerCase(); 832 } 833 mFileCache.put(key, new FileCacheEntry(mImagesUri, rowId, path, 834 lastModified)); 835 } 836 } finally { 837 c.close(); 838 c = null; 839 } 840 } 841 842 if (mProcessPlaylists) { 843 // Read existing files from the playlists table 844 if (filePath != null) { 845 where = MediaStore.Audio.Playlists.DATA + "=?"; 846 } else { 847 where = null; 848 } 849 c = mMediaProvider.query(mPlaylistsUri, PLAYLISTS_PROJECTION, where, selectionArgs, null); 850 851 if (c != null) { 852 try { 853 while (c.moveToNext()) { 854 String path = c.getString(PATH_IMAGES_COLUMN_INDEX); 855 856 if (path != null && path.length() > 0) { 857 long rowId = c.getLong(ID_PLAYLISTS_COLUMN_INDEX); 858 long lastModified = c.getLong(DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX); 859 860 String key = path; 861 if (mCaseInsensitivePaths) { 862 key = path.toLowerCase(); 863 } 864 mFileCache.put(key, new FileCacheEntry(mPlaylistsUri, rowId, path, 865 lastModified)); 866 } 867 } 868 } finally { 869 c.close(); 870 c = null; 871 } 872 } 873 } 874 } 875 finally { 876 if (c != null) { 877 c.close(); 878 } 879 } 880 } 881 882 private boolean inScanDirectory(String path, String[] directories) { 883 for (int i = 0; i < directories.length; i++) { 884 if (path.startsWith(directories[i])) { 885 return true; 886 } 887 } 888 return false; 889 } 890 891 private void pruneDeadThumbnailFiles() { 892 HashSet<String> existingFiles = new HashSet<String>(); 893 String directory = "/sdcard/DCIM/.thumbnails"; 894 String [] files = (new File(directory)).list(); 895 if (files == null) 896 files = new String[0]; 897 898 for (int i = 0; i < files.length; i++) { 899 String fullPathString = directory + "/" + files[i]; 900 existingFiles.add(fullPathString); 901 } 902 903 try { 904 Cursor c = mMediaProvider.query( 905 mThumbsUri, 906 new String [] { "_data" }, 907 null, 908 null, 909 null); 910 Log.v(TAG, "pruneDeadThumbnailFiles... " + c); 911 if (c != null && c.moveToFirst()) { 912 do { 913 String fullPathString = c.getString(0); 914 existingFiles.remove(fullPathString); 915 } while (c.moveToNext()); 916 } 917 918 for (String fileToDelete : existingFiles) { 919 if (Config.LOGV) 920 Log.v(TAG, "fileToDelete is " + fileToDelete); 921 try { 922 (new File(fileToDelete)).delete(); 923 } catch (SecurityException ex) { 924 } 925 } 926 927 Log.v(TAG, "/pruneDeadThumbnailFiles... " + c); 928 if (c != null) { 929 c.close(); 930 } 931 } catch (RemoteException e) { 932 // We will soon be killed... 933 } 934 } 935 936 private void postscan(String[] directories) throws RemoteException { 937 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 938 939 while (iterator.hasNext()) { 940 FileCacheEntry entry = iterator.next(); 941 String path = entry.mPath; 942 943 // remove database entries for files that no longer exist. 944 boolean fileMissing = false; 945 946 if (!entry.mSeenInFileSystem) { 947 if (inScanDirectory(path, directories)) { 948 // we didn't see this file in the scan directory. 949 fileMissing = true; 950 } else { 951 // the file is outside of our scan directory, 952 // so we need to check for file existence here. 953 File testFile = new File(path); 954 if (!testFile.exists()) { 955 fileMissing = true; 956 } 957 } 958 } 959 960 if (fileMissing) { 961 // do not delete missing playlists, since they may have been modified by the user. 962 // the user can delete them in the media player instead. 963 // instead, clear the path and lastModified fields in the row 964 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 965 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 966 967 if (MediaFile.isPlayListFileType(fileType)) { 968 ContentValues values = new ContentValues(); 969 values.put(MediaStore.Audio.Playlists.DATA, ""); 970 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, 0); 971 mMediaProvider.update(ContentUris.withAppendedId(mPlaylistsUri, entry.mRowId), values, null, null); 972 } else { 973 mMediaProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); 974 iterator.remove(); 975 } 976 } 977 } 978 979 // handle playlists last, after we know what media files are on the storage. 980 if (mProcessPlaylists) { 981 processPlayLists(); 982 } 983 984 if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external"))) 985 pruneDeadThumbnailFiles(); 986 987 // allow GC to clean up 988 mGenreCache = null; 989 mPlayLists = null; 990 mFileCache = null; 991 mMediaProvider = null; 992 } 993 994 private void initialize(String volumeName) { 995 mMediaProvider = mContext.getContentResolver().acquireProvider("media"); 996 997 mAudioUri = Audio.Media.getContentUri(volumeName); 998 mVideoUri = Video.Media.getContentUri(volumeName); 999 mImagesUri = Images.Media.getContentUri(volumeName); 1000 mThumbsUri = Images.Thumbnails.getContentUri(volumeName); 1001 1002 if (!volumeName.equals("internal")) { 1003 // we only support playlists on external media 1004 mProcessPlaylists = true; 1005 mProcessGenres = true; 1006 mGenreCache = new HashMap<String, Uri>(); 1007 mGenresUri = Genres.getContentUri(volumeName); 1008 mPlaylistsUri = Playlists.getContentUri(volumeName); 1009 // assuming external storage is FAT (case insensitive), except on the simulator. 1010 if ( Process.supportsProcesses()) { 1011 mCaseInsensitivePaths = true; 1012 } 1013 } 1014 } 1015 1016 public void scanDirectories(String[] directories, String volumeName) { 1017 try { 1018 long start = System.currentTimeMillis(); 1019 initialize(volumeName); 1020 prescan(null); 1021 long prescan = System.currentTimeMillis(); 1022 1023 for (int i = 0; i < directories.length; i++) { 1024 processDirectory(directories[i], MediaFile.sFileExtensions, mClient); 1025 } 1026 long scan = System.currentTimeMillis(); 1027 postscan(directories); 1028 long end = System.currentTimeMillis(); 1029 1030 if (Config.LOGD) { 1031 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1032 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1033 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1034 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1035 } 1036 } catch (SQLException e) { 1037 // this might happen if the SD card is removed while the media scanner is running 1038 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1039 } catch (UnsupportedOperationException e) { 1040 // this might happen if the SD card is removed while the media scanner is running 1041 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1042 } catch (RemoteException e) { 1043 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1044 } 1045 } 1046 1047 // this function is used to scan a single file 1048 public Uri scanSingleFile(String path, String volumeName, String mimeType) { 1049 try { 1050 initialize(volumeName); 1051 prescan(path); 1052 1053 File file = new File(path); 1054 // always scan the file, so we can return the content://media Uri for existing files 1055 return mClient.doScanFile(path, mimeType, file.lastModified(), file.length(), true); 1056 } catch (RemoteException e) { 1057 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1058 return null; 1059 } 1060 } 1061 1062 // returns the number of matching file/directory names, starting from the right 1063 private int matchPaths(String path1, String path2) { 1064 int result = 0; 1065 int end1 = path1.length(); 1066 int end2 = path2.length(); 1067 1068 while (end1 > 0 && end2 > 0) { 1069 int slash1 = path1.lastIndexOf('/', end1 - 1); 1070 int slash2 = path2.lastIndexOf('/', end2 - 1); 1071 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1072 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1073 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1074 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1075 if (start1 < 0) start1 = 0; else start1++; 1076 if (start2 < 0) start2 = 0; else start2++; 1077 int length = end1 - start1; 1078 if (end2 - start2 != length) break; 1079 if (path1.regionMatches(true, start1, path2, start2, length)) { 1080 result++; 1081 end1 = start1 - 1; 1082 end2 = start2 - 1; 1083 } else break; 1084 } 1085 1086 return result; 1087 } 1088 1089 private boolean addPlayListEntry(String entry, String playListDirectory, 1090 Uri uri, ContentValues values, int index) { 1091 1092 // watch for trailing whitespace 1093 int entryLength = entry.length(); 1094 while (entryLength > 0 && Character.isWhitespace(entry.charAt(entryLength - 1))) entryLength--; 1095 // path should be longer than 3 characters. 1096 // avoid index out of bounds errors below by returning here. 1097 if (entryLength < 3) return false; 1098 if (entryLength < entry.length()) entry = entry.substring(0, entryLength); 1099 1100 // does entry appear to be an absolute path? 1101 // look for Unix or DOS absolute paths 1102 char ch1 = entry.charAt(0); 1103 boolean fullPath = (ch1 == '/' || 1104 (Character.isLetter(ch1) && entry.charAt(1) == ':' && entry.charAt(2) == '\\')); 1105 // if we have a relative path, combine entry with playListDirectory 1106 if (!fullPath) 1107 entry = playListDirectory + entry; 1108 1109 //FIXME - should we look for "../" within the path? 1110 1111 // best matching MediaFile for the play list entry 1112 FileCacheEntry bestMatch = null; 1113 1114 // number of rightmost file/directory names for bestMatch 1115 int bestMatchLength = 0; 1116 1117 Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); 1118 while (iterator.hasNext()) { 1119 FileCacheEntry cacheEntry = iterator.next(); 1120 String path = cacheEntry.mPath; 1121 1122 if (path.equalsIgnoreCase(entry)) { 1123 bestMatch = cacheEntry; 1124 break; // don't bother continuing search 1125 } 1126 1127 int matchLength = matchPaths(path, entry); 1128 if (matchLength > bestMatchLength) { 1129 bestMatch = cacheEntry; 1130 bestMatchLength = matchLength; 1131 } 1132 } 1133 1134 if (bestMatch == null) { 1135 return false; 1136 } 1137 1138 try { 1139 // OK, now we need to add this to the database 1140 values.clear(); 1141 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1142 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(bestMatch.mRowId)); 1143 mMediaProvider.insert(uri, values); 1144 } catch (RemoteException e) { 1145 Log.e(TAG, "RemoteException in MediaScanner.addPlayListEntry()", e); 1146 return false; 1147 } 1148 1149 return true; 1150 } 1151 1152 private void processM3uPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1153 BufferedReader reader = null; 1154 try { 1155 File f = new File(path); 1156 if (f.exists()) { 1157 reader = new BufferedReader( 1158 new InputStreamReader(new FileInputStream(f)), 8192); 1159 String line = reader.readLine(); 1160 int index = 0; 1161 while (line != null) { 1162 // ignore comment lines, which begin with '#' 1163 if (line.length() > 0 && line.charAt(0) != '#') { 1164 values.clear(); 1165 if (addPlayListEntry(line, playListDirectory, uri, values, index)) 1166 index++; 1167 } 1168 line = reader.readLine(); 1169 } 1170 } 1171 } catch (IOException e) { 1172 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1173 } finally { 1174 try { 1175 if (reader != null) 1176 reader.close(); 1177 } catch (IOException e) { 1178 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1179 } 1180 } 1181 } 1182 1183 private void processPlsPlayList(String path, String playListDirectory, Uri uri, ContentValues values) { 1184 BufferedReader reader = null; 1185 try { 1186 File f = new File(path); 1187 if (f.exists()) { 1188 reader = new BufferedReader( 1189 new InputStreamReader(new FileInputStream(f)), 8192); 1190 String line = reader.readLine(); 1191 int index = 0; 1192 while (line != null) { 1193 // ignore comment lines, which begin with '#' 1194 if (line.startsWith("File")) { 1195 int equals = line.indexOf('='); 1196 if (equals > 0) { 1197 values.clear(); 1198 if (addPlayListEntry(line.substring(equals + 1), playListDirectory, uri, values, index)) 1199 index++; 1200 } 1201 } 1202 line = reader.readLine(); 1203 } 1204 } 1205 } catch (IOException e) { 1206 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1207 } finally { 1208 try { 1209 if (reader != null) 1210 reader.close(); 1211 } catch (IOException e) { 1212 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1213 } 1214 } 1215 } 1216 1217 class WplHandler implements ElementListener { 1218 1219 final ContentHandler handler; 1220 String playListDirectory; 1221 Uri uri; 1222 ContentValues values = new ContentValues(); 1223 int index = 0; 1224 1225 public WplHandler(String playListDirectory, Uri uri) { 1226 this.playListDirectory = playListDirectory; 1227 this.uri = uri; 1228 1229 RootElement root = new RootElement("smil"); 1230 Element body = root.getChild("body"); 1231 Element seq = body.getChild("seq"); 1232 Element media = seq.getChild("media"); 1233 media.setElementListener(this); 1234 1235 this.handler = root.getContentHandler(); 1236 } 1237 1238 public void start(Attributes attributes) { 1239 String path = attributes.getValue("", "src"); 1240 if (path != null) { 1241 values.clear(); 1242 if (addPlayListEntry(path, playListDirectory, uri, values, index)) { 1243 index++; 1244 } 1245 } 1246 } 1247 1248 public void end() { 1249 } 1250 1251 ContentHandler getContentHandler() { 1252 return handler; 1253 } 1254 } 1255 1256 private void processWplPlayList(String path, String playListDirectory, Uri uri) { 1257 FileInputStream fis = null; 1258 try { 1259 File f = new File(path); 1260 if (f.exists()) { 1261 fis = new FileInputStream(f); 1262 1263 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), new WplHandler(playListDirectory, uri).getContentHandler()); 1264 } 1265 } catch (SAXException e) { 1266 e.printStackTrace(); 1267 } catch (IOException e) { 1268 e.printStackTrace(); 1269 } finally { 1270 try { 1271 if (fis != null) 1272 fis.close(); 1273 } catch (IOException e) { 1274 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1275 } 1276 } 1277 } 1278 1279 private void processPlayLists() throws RemoteException { 1280 Iterator<FileCacheEntry> iterator = mPlayLists.iterator(); 1281 while (iterator.hasNext()) { 1282 FileCacheEntry entry = iterator.next(); 1283 String path = entry.mPath; 1284 1285 // only process playlist files if they are new or have been modified since the last scan 1286 if (entry.mLastModifiedChanged) { 1287 ContentValues values = new ContentValues(); 1288 int lastSlash = path.lastIndexOf('/'); 1289 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1290 Uri uri, membersUri; 1291 long rowId = entry.mRowId; 1292 if (rowId == 0) { 1293 // Create a new playlist 1294 1295 int lastDot = path.lastIndexOf('.'); 1296 String name = (lastDot < 0 ? path.substring(lastSlash + 1) : path.substring(lastSlash + 1, lastDot)); 1297 values.put(MediaStore.Audio.Playlists.NAME, name); 1298 values.put(MediaStore.Audio.Playlists.DATA, path); 1299 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1300 uri = mMediaProvider.insert(mPlaylistsUri, values); 1301 rowId = ContentUris.parseId(uri); 1302 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1303 } else { 1304 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1305 1306 // update lastModified value of existing playlist 1307 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1308 mMediaProvider.update(uri, values, null, null); 1309 1310 // delete members of existing playlist 1311 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1312 mMediaProvider.delete(membersUri, null, null); 1313 } 1314 1315 String playListDirectory = path.substring(0, lastSlash + 1); 1316 MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); 1317 int fileType = (mediaFileType == null ? 0 : mediaFileType.fileType); 1318 1319 if (fileType == MediaFile.FILE_TYPE_M3U) 1320 processM3uPlayList(path, playListDirectory, membersUri, values); 1321 else if (fileType == MediaFile.FILE_TYPE_PLS) 1322 processPlsPlayList(path, playListDirectory, membersUri, values); 1323 else if (fileType == MediaFile.FILE_TYPE_WPL) 1324 processWplPlayList(path, playListDirectory, membersUri); 1325 1326 Cursor cursor = mMediaProvider.query(membersUri, PLAYLIST_MEMBERS_PROJECTION, null, 1327 null, null); 1328 try { 1329 if (cursor == null || cursor.getCount() == 0) { 1330 Log.d(TAG, "playlist is empty - deleting"); 1331 mMediaProvider.delete(uri, null, null); 1332 } 1333 } finally { 1334 if (cursor != null) cursor.close(); 1335 } 1336 } 1337 } 1338 } 1339 1340 private native void processDirectory(String path, String extensions, MediaScannerClient client); 1341 private native void processFile(String path, String mimeType, MediaScannerClient client); 1342 public native void setLocale(String locale); 1343 1344 public native byte[] extractAlbumArt(FileDescriptor fd); 1345 1346 private native final void native_setup(); 1347 private native final void native_finalize(); 1348 @Override 1349 protected void finalize() { 1350 mContext.getContentResolver().releaseProvider(mMediaProvider); 1351 native_finalize(); 1352 } 1353} 1354