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