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 17 package android.media; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.content.ContentProviderClient; 21 import android.content.ContentResolver; 22 import android.content.ContentUris; 23 import android.content.ContentValues; 24 import android.content.Context; 25 import android.content.SharedPreferences; 26 import android.database.Cursor; 27 import android.database.SQLException; 28 import android.drm.DrmManagerClient; 29 import android.graphics.BitmapFactory; 30 import android.mtp.MtpConstants; 31 import android.net.Uri; 32 import android.os.Build; 33 import android.os.Environment; 34 import android.os.RemoteException; 35 import android.os.SystemProperties; 36 import android.provider.MediaStore; 37 import android.provider.MediaStore.Audio; 38 import android.provider.MediaStore.Audio.Playlists; 39 import android.provider.MediaStore.Files; 40 import android.provider.MediaStore.Files.FileColumns; 41 import android.provider.MediaStore.Images; 42 import android.provider.MediaStore.Video; 43 import android.provider.Settings; 44 import android.provider.Settings.SettingNotFoundException; 45 import android.sax.Element; 46 import android.sax.ElementListener; 47 import android.sax.RootElement; 48 import android.system.ErrnoException; 49 import android.system.Os; 50 import android.text.TextUtils; 51 import android.util.Log; 52 import android.util.Xml; 53 54 import dalvik.system.CloseGuard; 55 56 import org.xml.sax.Attributes; 57 import org.xml.sax.ContentHandler; 58 import org.xml.sax.SAXException; 59 60 import java.io.BufferedReader; 61 import java.io.File; 62 import java.io.FileDescriptor; 63 import java.io.FileInputStream; 64 import java.io.IOException; 65 import java.io.InputStreamReader; 66 import java.text.ParseException; 67 import java.text.SimpleDateFormat; 68 import java.util.ArrayList; 69 import java.util.HashMap; 70 import java.util.Iterator; 71 import java.util.Locale; 72 import java.util.TimeZone; 73 import java.util.concurrent.atomic.AtomicBoolean; 74 75 /** 76 * Internal service helper that no-one should use directly. 77 * 78 * The way the scan currently works is: 79 * - The Java MediaScannerService creates a MediaScanner (this class), and calls 80 * MediaScanner.scanDirectories on it. 81 * - scanDirectories() calls the native processDirectory() for each of the specified directories. 82 * - the processDirectory() JNI method wraps the provided mediascanner client in a native 83 * 'MyMediaScannerClient' class, then calls processDirectory() on the native MediaScanner 84 * object (which got created when the Java MediaScanner was created). 85 * - native MediaScanner.processDirectory() calls 86 * doProcessDirectory(), which recurses over the folder, and calls 87 * native MyMediaScannerClient.scanFile() for every file whose extension matches. 88 * - native MyMediaScannerClient.scanFile() calls back on Java MediaScannerClient.scanFile, 89 * which calls doScanFile, which after some setup calls back down to native code, calling 90 * MediaScanner.processFile(). 91 * - MediaScanner.processFile() calls one of several methods, depending on the type of the 92 * file: parseMP3, parseMP4, parseMidi, parseOgg or parseWMA. 93 * - each of these methods gets metadata key/value pairs from the file, and repeatedly 94 * calls native MyMediaScannerClient.handleStringTag, which calls back up to its Java 95 * counterparts in this file. 96 * - Java handleStringTag() gathers the key/value pairs that it's interested in. 97 * - once processFile returns and we're back in Java code in doScanFile(), it calls 98 * Java MyMediaScannerClient.endFile(), which takes all the data that's been 99 * gathered and inserts an entry in to the database. 100 * 101 * In summary: 102 * Java MediaScannerService calls 103 * Java MediaScanner scanDirectories, which calls 104 * Java MediaScanner processDirectory (native method), which calls 105 * native MediaScanner processDirectory, which calls 106 * native MyMediaScannerClient scanFile, which calls 107 * Java MyMediaScannerClient scanFile, which calls 108 * Java MediaScannerClient doScanFile, which calls 109 * Java MediaScanner processFile (native method), which calls 110 * native MediaScanner processFile, which calls 111 * native parseMP3, parseMP4, parseMidi, parseOgg or parseWMA, which calls 112 * native MyMediaScanner handleStringTag, which calls 113 * Java MyMediaScanner handleStringTag. 114 * Once MediaScanner processFile returns, an entry is inserted in to the database. 115 * 116 * The MediaScanner class is not thread-safe, so it should only be used in a single threaded manner. 117 * 118 * {@hide} 119 * 120 * @deprecated this media scanner has served faithfully for many years, but it's 121 * become tedious to test and maintain, mainly due to the way it 122 * weaves obscurely between managed and native code. It's been 123 * replaced by {@code ModernMediaScanner} in the 124 * {@code MediaProvider} package. 125 */ 126 @Deprecated 127 public class MediaScanner implements AutoCloseable { 128 static { 129 System.loadLibrary("media_jni"); native_init()130 native_init(); 131 } 132 133 private final static String TAG = "MediaScanner"; 134 135 @UnsupportedAppUsage 136 private static final String[] FILES_PRESCAN_PROJECTION = new String[] { 137 Files.FileColumns._ID, // 0 138 Files.FileColumns.DATA, // 1 139 Files.FileColumns.FORMAT, // 2 140 Files.FileColumns.DATE_MODIFIED, // 3 141 Files.FileColumns.MEDIA_TYPE, // 4 142 }; 143 144 private static final String[] ID_PROJECTION = new String[] { 145 Files.FileColumns._ID, 146 }; 147 148 private static final int FILES_PRESCAN_ID_COLUMN_INDEX = 0; 149 private static final int FILES_PRESCAN_PATH_COLUMN_INDEX = 1; 150 private static final int FILES_PRESCAN_FORMAT_COLUMN_INDEX = 2; 151 private static final int FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX = 3; 152 private static final int FILES_PRESCAN_MEDIA_TYPE_COLUMN_INDEX = 4; 153 154 private static final String[] PLAYLIST_MEMBERS_PROJECTION = new String[] { 155 Audio.Playlists.Members.PLAYLIST_ID, // 0 156 }; 157 158 private static final int ID_PLAYLISTS_COLUMN_INDEX = 0; 159 private static final int PATH_PLAYLISTS_COLUMN_INDEX = 1; 160 private static final int DATE_MODIFIED_PLAYLISTS_COLUMN_INDEX = 2; 161 162 private static final String RINGTONES_DIR = "/ringtones/"; 163 private static final String NOTIFICATIONS_DIR = "/notifications/"; 164 private static final String ALARMS_DIR = "/alarms/"; 165 private static final String MUSIC_DIR = "/music/"; 166 private static final String PODCASTS_DIR = "/podcasts/"; 167 private static final String AUDIOBOOKS_DIR = "/audiobooks/"; 168 169 public static final String SCANNED_BUILD_PREFS_NAME = "MediaScanBuild"; 170 public static final String LAST_INTERNAL_SCAN_FINGERPRINT = "lastScanFingerprint"; 171 private static final String SYSTEM_SOUNDS_DIR = Environment.getRootDirectory() + "/media/audio"; 172 private static final String OEM_SOUNDS_DIR = Environment.getOemDirectory() + "/media/audio"; 173 private static final String PRODUCT_SOUNDS_DIR = Environment.getProductDirectory() + "/media/audio"; 174 private static String sLastInternalScanFingerprint; 175 176 private static final String[] ID3_GENRES = { 177 // ID3v1 Genres 178 "Blues", 179 "Classic Rock", 180 "Country", 181 "Dance", 182 "Disco", 183 "Funk", 184 "Grunge", 185 "Hip-Hop", 186 "Jazz", 187 "Metal", 188 "New Age", 189 "Oldies", 190 "Other", 191 "Pop", 192 "R&B", 193 "Rap", 194 "Reggae", 195 "Rock", 196 "Techno", 197 "Industrial", 198 "Alternative", 199 "Ska", 200 "Death Metal", 201 "Pranks", 202 "Soundtrack", 203 "Euro-Techno", 204 "Ambient", 205 "Trip-Hop", 206 "Vocal", 207 "Jazz+Funk", 208 "Fusion", 209 "Trance", 210 "Classical", 211 "Instrumental", 212 "Acid", 213 "House", 214 "Game", 215 "Sound Clip", 216 "Gospel", 217 "Noise", 218 "AlternRock", 219 "Bass", 220 "Soul", 221 "Punk", 222 "Space", 223 "Meditative", 224 "Instrumental Pop", 225 "Instrumental Rock", 226 "Ethnic", 227 "Gothic", 228 "Darkwave", 229 "Techno-Industrial", 230 "Electronic", 231 "Pop-Folk", 232 "Eurodance", 233 "Dream", 234 "Southern Rock", 235 "Comedy", 236 "Cult", 237 "Gangsta", 238 "Top 40", 239 "Christian Rap", 240 "Pop/Funk", 241 "Jungle", 242 "Native American", 243 "Cabaret", 244 "New Wave", 245 "Psychadelic", 246 "Rave", 247 "Showtunes", 248 "Trailer", 249 "Lo-Fi", 250 "Tribal", 251 "Acid Punk", 252 "Acid Jazz", 253 "Polka", 254 "Retro", 255 "Musical", 256 "Rock & Roll", 257 "Hard Rock", 258 // The following genres are Winamp extensions 259 "Folk", 260 "Folk-Rock", 261 "National Folk", 262 "Swing", 263 "Fast Fusion", 264 "Bebob", 265 "Latin", 266 "Revival", 267 "Celtic", 268 "Bluegrass", 269 "Avantgarde", 270 "Gothic Rock", 271 "Progressive Rock", 272 "Psychedelic Rock", 273 "Symphonic Rock", 274 "Slow Rock", 275 "Big Band", 276 "Chorus", 277 "Easy Listening", 278 "Acoustic", 279 "Humour", 280 "Speech", 281 "Chanson", 282 "Opera", 283 "Chamber Music", 284 "Sonata", 285 "Symphony", 286 "Booty Bass", 287 "Primus", 288 "Porn Groove", 289 "Satire", 290 "Slow Jam", 291 "Club", 292 "Tango", 293 "Samba", 294 "Folklore", 295 "Ballad", 296 "Power Ballad", 297 "Rhythmic Soul", 298 "Freestyle", 299 "Duet", 300 "Punk Rock", 301 "Drum Solo", 302 "A capella", 303 "Euro-House", 304 "Dance Hall", 305 // The following ones seem to be fairly widely supported as well 306 "Goa", 307 "Drum & Bass", 308 "Club-House", 309 "Hardcore", 310 "Terror", 311 "Indie", 312 "Britpop", 313 null, 314 "Polsk Punk", 315 "Beat", 316 "Christian Gangsta", 317 "Heavy Metal", 318 "Black Metal", 319 "Crossover", 320 "Contemporary Christian", 321 "Christian Rock", 322 "Merengue", 323 "Salsa", 324 "Thrash Metal", 325 "Anime", 326 "JPop", 327 "Synthpop", 328 // 148 and up don't seem to have been defined yet. 329 }; 330 331 private long mNativeContext; 332 @UnsupportedAppUsage 333 private final Context mContext; 334 @UnsupportedAppUsage 335 private final String mPackageName; 336 private final String mVolumeName; 337 private final ContentProviderClient mMediaProvider; 338 @UnsupportedAppUsage 339 private final Uri mAudioUri; 340 private final Uri mVideoUri; 341 private final Uri mImagesUri; 342 private final Uri mPlaylistsUri; 343 @UnsupportedAppUsage 344 private final Uri mFilesUri; 345 private final Uri mFilesFullUri; 346 private final boolean mProcessPlaylists; 347 private final boolean mProcessGenres; 348 private int mMtpObjectHandle; 349 350 private final AtomicBoolean mClosed = new AtomicBoolean(); 351 private final CloseGuard mCloseGuard = CloseGuard.get(); 352 353 /** whether to use bulk inserts or individual inserts for each item */ 354 private static final boolean ENABLE_BULK_INSERTS = true; 355 356 // used when scanning the image database so we know whether we have to prune 357 // old thumbnail files 358 private int mOriginalCount; 359 /** Whether the scanner has set a default sound for the ringer ringtone. */ 360 private boolean mDefaultRingtoneSet; 361 /** Whether the scanner has set a default sound for the notification ringtone. */ 362 private boolean mDefaultNotificationSet; 363 /** Whether the scanner has set a default sound for the alarm ringtone. */ 364 private boolean mDefaultAlarmSet; 365 /** The filename for the default sound for the ringer ringtone. */ 366 @UnsupportedAppUsage 367 private String mDefaultRingtoneFilename; 368 /** The filename for the default sound for the notification ringtone. */ 369 @UnsupportedAppUsage 370 private String mDefaultNotificationFilename; 371 /** The filename for the default sound for the alarm ringtone. */ 372 @UnsupportedAppUsage 373 private String mDefaultAlarmAlertFilename; 374 /** 375 * The prefix for system properties that define the default sound for 376 * ringtones. Concatenate the name of the setting from Settings 377 * to get the full system property. 378 */ 379 private static final String DEFAULT_RINGTONE_PROPERTY_PREFIX = "ro.config."; 380 381 private final BitmapFactory.Options mBitmapOptions = new BitmapFactory.Options(); 382 383 private static class FileEntry { 384 @UnsupportedAppUsage 385 long mRowId; 386 String mPath; 387 long mLastModified; 388 int mFormat; 389 int mMediaType; 390 @UnsupportedAppUsage 391 boolean mLastModifiedChanged; 392 393 /** @deprecated kept intact for lame apps using reflection */ 394 @Deprecated 395 @UnsupportedAppUsage FileEntry(long rowId, String path, long lastModified, int format)396 FileEntry(long rowId, String path, long lastModified, int format) { 397 this(rowId, path, lastModified, format, FileColumns.MEDIA_TYPE_NONE); 398 } 399 FileEntry(long rowId, String path, long lastModified, int format, int mediaType)400 FileEntry(long rowId, String path, long lastModified, int format, int mediaType) { 401 mRowId = rowId; 402 mPath = path; 403 mLastModified = lastModified; 404 mFormat = format; 405 mMediaType = mediaType; 406 mLastModifiedChanged = false; 407 } 408 409 @Override toString()410 public String toString() { 411 return mPath + " mRowId: " + mRowId; 412 } 413 } 414 415 private static class PlaylistEntry { 416 String path; 417 long bestmatchid; 418 int bestmatchlevel; 419 } 420 421 private final ArrayList<PlaylistEntry> mPlaylistEntries = new ArrayList<>(); 422 private final ArrayList<FileEntry> mPlayLists = new ArrayList<>(); 423 424 @UnsupportedAppUsage 425 private MediaInserter mMediaInserter; 426 427 private DrmManagerClient mDrmManagerClient = null; 428 429 @UnsupportedAppUsage MediaScanner(Context c, String volumeName)430 public MediaScanner(Context c, String volumeName) { 431 native_setup(); 432 mContext = c; 433 mPackageName = c.getPackageName(); 434 mVolumeName = volumeName; 435 436 mBitmapOptions.inSampleSize = 1; 437 mBitmapOptions.inJustDecodeBounds = true; 438 439 setDefaultRingtoneFileNames(); 440 441 mMediaProvider = mContext.getContentResolver() 442 .acquireContentProviderClient(MediaStore.AUTHORITY); 443 444 if (sLastInternalScanFingerprint == null) { 445 final SharedPreferences scanSettings = 446 mContext.getSharedPreferences(SCANNED_BUILD_PREFS_NAME, Context.MODE_PRIVATE); 447 sLastInternalScanFingerprint = 448 scanSettings.getString(LAST_INTERNAL_SCAN_FINGERPRINT, new String()); 449 } 450 451 mAudioUri = Audio.Media.getContentUri(volumeName); 452 mVideoUri = Video.Media.getContentUri(volumeName); 453 mImagesUri = Images.Media.getContentUri(volumeName); 454 mFilesUri = Files.getContentUri(volumeName); 455 456 Uri filesFullUri = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build(); 457 filesFullUri = MediaStore.setIncludePending(filesFullUri); 458 filesFullUri = MediaStore.setIncludeTrashed(filesFullUri); 459 mFilesFullUri = filesFullUri; 460 461 if (!volumeName.equals("internal")) { 462 // we only support playlists on external media 463 mProcessPlaylists = true; 464 mProcessGenres = true; 465 mPlaylistsUri = Playlists.getContentUri(volumeName); 466 } else { 467 mProcessPlaylists = false; 468 mProcessGenres = false; 469 mPlaylistsUri = null; 470 } 471 472 final Locale locale = mContext.getResources().getConfiguration().locale; 473 if (locale != null) { 474 String language = locale.getLanguage(); 475 String country = locale.getCountry(); 476 if (language != null) { 477 if (country != null) { 478 setLocale(language + "_" + country); 479 } else { 480 setLocale(language); 481 } 482 } 483 } 484 485 mCloseGuard.open("close"); 486 } 487 setDefaultRingtoneFileNames()488 private void setDefaultRingtoneFileNames() { 489 mDefaultRingtoneFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 490 + Settings.System.RINGTONE); 491 mDefaultNotificationFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 492 + Settings.System.NOTIFICATION_SOUND); 493 mDefaultAlarmAlertFilename = SystemProperties.get(DEFAULT_RINGTONE_PROPERTY_PREFIX 494 + Settings.System.ALARM_ALERT); 495 } 496 497 @UnsupportedAppUsage 498 private final MyMediaScannerClient mClient = new MyMediaScannerClient(); 499 500 @UnsupportedAppUsage isDrmEnabled()501 private boolean isDrmEnabled() { 502 String prop = SystemProperties.get("drm.service.enabled"); 503 return prop != null && prop.equals("true"); 504 } 505 506 private class MyMediaScannerClient implements MediaScannerClient { 507 508 private final SimpleDateFormat mDateFormatter; 509 510 private String mArtist; 511 private String mAlbumArtist; // use this if mArtist is missing 512 private String mAlbum; 513 private String mTitle; 514 private String mComposer; 515 private String mGenre; 516 @UnsupportedAppUsage 517 private String mMimeType; 518 /** @deprecated file types no longer exist */ 519 @Deprecated 520 @UnsupportedAppUsage 521 private int mFileType; 522 private int mTrack; 523 private int mYear; 524 private int mDuration; 525 @UnsupportedAppUsage 526 private String mPath; 527 private long mDate; 528 private long mLastModified; 529 private long mFileSize; 530 private String mWriter; 531 private int mCompilation; 532 @UnsupportedAppUsage 533 private boolean mIsDrm; 534 @UnsupportedAppUsage 535 private boolean mNoMedia; // flag to suppress file from appearing in media tables 536 private boolean mScanSuccess; 537 private int mWidth; 538 private int mHeight; 539 private int mColorStandard; 540 private int mColorTransfer; 541 private int mColorRange; 542 MyMediaScannerClient()543 public MyMediaScannerClient() { 544 mDateFormatter = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); 545 mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); 546 } 547 548 @UnsupportedAppUsage beginFile(String path, String mimeType, long lastModified, long fileSize, boolean isDirectory, boolean noMedia)549 public FileEntry beginFile(String path, String mimeType, long lastModified, 550 long fileSize, boolean isDirectory, boolean noMedia) { 551 mMimeType = mimeType; 552 mFileSize = fileSize; 553 mIsDrm = false; 554 mScanSuccess = true; 555 556 if (!isDirectory) { 557 if (!noMedia && isNoMediaFile(path)) { 558 noMedia = true; 559 } 560 mNoMedia = noMedia; 561 562 // if mimeType was not specified, compute file type based on file extension. 563 if (mMimeType == null) { 564 mMimeType = MediaFile.getMimeTypeForFile(path); 565 } 566 567 if (isDrmEnabled() && MediaFile.isDrmMimeType(mMimeType)) { 568 getMimeTypeFromDrm(path); 569 } 570 } 571 572 FileEntry entry = makeEntryFor(path); 573 // add some slack to avoid a rounding error 574 long delta = (entry != null) ? (lastModified - entry.mLastModified) : 0; 575 boolean wasModified = delta > 1 || delta < -1; 576 if (entry == null || wasModified) { 577 if (wasModified) { 578 entry.mLastModified = lastModified; 579 } else { 580 entry = new FileEntry(0, path, lastModified, 581 (isDirectory ? MtpConstants.FORMAT_ASSOCIATION : 0), 582 FileColumns.MEDIA_TYPE_NONE); 583 } 584 entry.mLastModifiedChanged = true; 585 } 586 587 if (mProcessPlaylists && MediaFile.isPlayListMimeType(mMimeType)) { 588 mPlayLists.add(entry); 589 // we don't process playlists in the main scan, so return null 590 return null; 591 } 592 593 // clear all the metadata 594 mArtist = null; 595 mAlbumArtist = null; 596 mAlbum = null; 597 mTitle = null; 598 mComposer = null; 599 mGenre = null; 600 mTrack = 0; 601 mYear = 0; 602 mDuration = 0; 603 mPath = path; 604 mDate = 0; 605 mLastModified = lastModified; 606 mWriter = null; 607 mCompilation = 0; 608 mWidth = 0; 609 mHeight = 0; 610 mColorStandard = -1; 611 mColorTransfer = -1; 612 mColorRange = -1; 613 614 return entry; 615 } 616 617 @Override 618 @UnsupportedAppUsage 619 public void scanFile(String path, long lastModified, long fileSize, 620 boolean isDirectory, boolean noMedia) { 621 // This is the callback funtion from native codes. 622 // Log.v(TAG, "scanFile: "+path); 623 doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia); 624 } 625 626 @UnsupportedAppUsage 627 public Uri doScanFile(String path, String mimeType, long lastModified, 628 long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) { 629 Uri result = null; 630 // long t1 = System.currentTimeMillis(); 631 try { 632 FileEntry entry = beginFile(path, mimeType, lastModified, 633 fileSize, isDirectory, noMedia); 634 635 if (entry == null) { 636 return null; 637 } 638 639 // if this file was just inserted via mtp, set the rowid to zero 640 // (even though it already exists in the database), to trigger 641 // the correct code path for updating its entry 642 if (mMtpObjectHandle != 0) { 643 entry.mRowId = 0; 644 } 645 646 if (entry.mPath != null) { 647 if (((!mDefaultNotificationSet && 648 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) 649 || (!mDefaultRingtoneSet && 650 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) 651 || (!mDefaultAlarmSet && 652 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)))) { 653 Log.w(TAG, "forcing rescan of " + entry.mPath + 654 "since ringtone setting didn't finish"); 655 scanAlways = true; 656 } else if (isSystemSoundWithMetadata(entry.mPath) 657 && !Build.FINGERPRINT.equals(sLastInternalScanFingerprint)) { 658 // file is located on the system partition where the date cannot be trusted: 659 // rescan if the build fingerprint has changed since the last scan. 660 Log.i(TAG, "forcing rescan of " + entry.mPath 661 + " since build fingerprint changed"); 662 scanAlways = true; 663 } 664 } 665 666 // rescan for metadata if file was modified since last scan 667 if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { 668 if (noMedia) { 669 result = endFile(entry, false, false, false, false, false, false); 670 } else { 671 boolean isaudio = MediaFile.isAudioMimeType(mMimeType); 672 boolean isvideo = MediaFile.isVideoMimeType(mMimeType); 673 boolean isimage = MediaFile.isImageMimeType(mMimeType); 674 675 if (isaudio || isvideo || isimage) { 676 path = Environment.maybeTranslateEmulatedPathToInternal(new File(path)) 677 .getAbsolutePath(); 678 } 679 680 // we only extract metadata for audio and video files 681 if (isaudio || isvideo) { 682 mScanSuccess = processFile(path, mimeType, this); 683 } 684 685 if (isimage) { 686 mScanSuccess = processImageFile(path); 687 } 688 689 String lowpath = path.toLowerCase(Locale.ROOT); 690 boolean ringtones = mScanSuccess && (lowpath.indexOf(RINGTONES_DIR) > 0); 691 boolean notifications = mScanSuccess && 692 (lowpath.indexOf(NOTIFICATIONS_DIR) > 0); 693 boolean alarms = mScanSuccess && (lowpath.indexOf(ALARMS_DIR) > 0); 694 boolean podcasts = mScanSuccess && (lowpath.indexOf(PODCASTS_DIR) > 0); 695 boolean audiobooks = mScanSuccess && (lowpath.indexOf(AUDIOBOOKS_DIR) > 0); 696 boolean music = mScanSuccess && ((lowpath.indexOf(MUSIC_DIR) > 0) || 697 (!ringtones && !notifications && !alarms && !podcasts && !audiobooks)); 698 699 result = endFile(entry, ringtones, notifications, alarms, podcasts, 700 audiobooks, music); 701 } 702 } 703 } catch (RemoteException e) { 704 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 705 } 706 // long t2 = System.currentTimeMillis(); 707 // Log.v(TAG, "scanFile: " + path + " took " + (t2-t1)); 708 return result; 709 } 710 711 private long parseDate(String date) { 712 try { 713 return mDateFormatter.parse(date).getTime(); 714 } catch (ParseException e) { 715 return 0; 716 } 717 } 718 719 private int parseSubstring(String s, int start, int defaultValue) { 720 int length = s.length(); 721 if (start == length) return defaultValue; 722 723 char ch = s.charAt(start++); 724 // return defaultValue if we have no integer at all 725 if (ch < '0' || ch > '9') return defaultValue; 726 727 int result = ch - '0'; 728 while (start < length) { 729 ch = s.charAt(start++); 730 if (ch < '0' || ch > '9') return result; 731 result = result * 10 + (ch - '0'); 732 } 733 734 return result; 735 } 736 737 @UnsupportedAppUsage 738 public void handleStringTag(String name, String value) { 739 if (name.equalsIgnoreCase("title") || name.startsWith("title;")) { 740 // Don't trim() here, to preserve the special \001 character 741 // used to force sorting. The media provider will trim() before 742 // inserting the title in to the database. 743 mTitle = value; 744 } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) { 745 mArtist = value.trim(); 746 } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;") 747 || name.equalsIgnoreCase("band") || name.startsWith("band;")) { 748 mAlbumArtist = value.trim(); 749 } else if (name.equalsIgnoreCase("album") || name.startsWith("album;")) { 750 mAlbum = value.trim(); 751 } else if (name.equalsIgnoreCase("composer") || name.startsWith("composer;")) { 752 mComposer = value.trim(); 753 } else if (mProcessGenres && 754 (name.equalsIgnoreCase("genre") || name.startsWith("genre;"))) { 755 mGenre = getGenreName(value); 756 } else if (name.equalsIgnoreCase("year") || name.startsWith("year;")) { 757 mYear = parseSubstring(value, 0, 0); 758 } else if (name.equalsIgnoreCase("tracknumber") || name.startsWith("tracknumber;")) { 759 // track number might be of the form "2/12" 760 // we just read the number before the slash 761 int num = parseSubstring(value, 0, 0); 762 mTrack = (mTrack / 1000) * 1000 + num; 763 } else if (name.equalsIgnoreCase("discnumber") || 764 name.equals("set") || name.startsWith("set;")) { 765 // set number might be of the form "1/3" 766 // we just read the number before the slash 767 int num = parseSubstring(value, 0, 0); 768 mTrack = (num * 1000) + (mTrack % 1000); 769 } else if (name.equalsIgnoreCase("duration")) { 770 mDuration = parseSubstring(value, 0, 0); 771 } else if (name.equalsIgnoreCase("writer") || name.startsWith("writer;")) { 772 mWriter = value.trim(); 773 } else if (name.equalsIgnoreCase("compilation")) { 774 mCompilation = parseSubstring(value, 0, 0); 775 } else if (name.equalsIgnoreCase("isdrm")) { 776 mIsDrm = (parseSubstring(value, 0, 0) == 1); 777 } else if (name.equalsIgnoreCase("date")) { 778 mDate = parseDate(value); 779 } else if (name.equalsIgnoreCase("width")) { 780 mWidth = parseSubstring(value, 0, 0); 781 } else if (name.equalsIgnoreCase("height")) { 782 mHeight = parseSubstring(value, 0, 0); 783 } else if (name.equalsIgnoreCase("colorstandard")) { 784 mColorStandard = parseSubstring(value, 0, -1); 785 } else if (name.equalsIgnoreCase("colortransfer")) { 786 mColorTransfer = parseSubstring(value, 0, -1); 787 } else if (name.equalsIgnoreCase("colorrange")) { 788 mColorRange = parseSubstring(value, 0, -1); 789 } else { 790 //Log.v(TAG, "unknown tag: " + name + " (" + mProcessGenres + ")"); 791 } 792 } 793 794 private boolean convertGenreCode(String input, String expected) { 795 String output = getGenreName(input); 796 if (output.equals(expected)) { 797 return true; 798 } else { 799 Log.d(TAG, "'" + input + "' -> '" + output + "', expected '" + expected + "'"); 800 return false; 801 } 802 } 803 private void testGenreNameConverter() { 804 convertGenreCode("2", "Country"); 805 convertGenreCode("(2)", "Country"); 806 convertGenreCode("(2", "(2"); 807 convertGenreCode("2 Foo", "Country"); 808 convertGenreCode("(2) Foo", "Country"); 809 convertGenreCode("(2 Foo", "(2 Foo"); 810 convertGenreCode("2Foo", "2Foo"); 811 convertGenreCode("(2)Foo", "Country"); 812 convertGenreCode("200 Foo", "Foo"); 813 convertGenreCode("(200) Foo", "Foo"); 814 convertGenreCode("200Foo", "200Foo"); 815 convertGenreCode("(200)Foo", "Foo"); 816 convertGenreCode("200)Foo", "200)Foo"); 817 convertGenreCode("200) Foo", "200) Foo"); 818 } 819 820 public String getGenreName(String genreTagValue) { 821 822 if (genreTagValue == null) { 823 return null; 824 } 825 final int length = genreTagValue.length(); 826 827 if (length > 0) { 828 boolean parenthesized = false; 829 StringBuffer number = new StringBuffer(); 830 int i = 0; 831 for (; i < length; ++i) { 832 char c = genreTagValue.charAt(i); 833 if (i == 0 && c == '(') { 834 parenthesized = true; 835 } else if (Character.isDigit(c)) { 836 number.append(c); 837 } else { 838 break; 839 } 840 } 841 char charAfterNumber = i < length ? genreTagValue.charAt(i) : ' '; 842 if ((parenthesized && charAfterNumber == ')') 843 || !parenthesized && Character.isWhitespace(charAfterNumber)) { 844 try { 845 short genreIndex = Short.parseShort(number.toString()); 846 if (genreIndex >= 0) { 847 if (genreIndex < ID3_GENRES.length && ID3_GENRES[genreIndex] != null) { 848 return ID3_GENRES[genreIndex]; 849 } else if (genreIndex == 0xFF) { 850 return null; 851 } else if (genreIndex < 0xFF && (i + 1) < length) { 852 // genre is valid but unknown, 853 // if there is a string after the value we take it 854 if (parenthesized && charAfterNumber == ')') { 855 i++; 856 } 857 String ret = genreTagValue.substring(i).trim(); 858 if (ret.length() != 0) { 859 return ret; 860 } 861 } else { 862 // else return the number, without parentheses 863 return number.toString(); 864 } 865 } 866 } catch (NumberFormatException e) { 867 } 868 } 869 } 870 871 return genreTagValue; 872 } 873 874 private boolean processImageFile(String path) { 875 try { 876 mBitmapOptions.outWidth = 0; 877 mBitmapOptions.outHeight = 0; 878 BitmapFactory.decodeFile(path, mBitmapOptions); 879 mWidth = mBitmapOptions.outWidth; 880 mHeight = mBitmapOptions.outHeight; 881 return mWidth > 0 && mHeight > 0; 882 } catch (Throwable th) { 883 // ignore; 884 } 885 return false; 886 } 887 888 @UnsupportedAppUsage 889 public void setMimeType(String mimeType) { 890 if ("audio/mp4".equals(mMimeType) && 891 mimeType.startsWith("video")) { 892 // for feature parity with Donut, we force m4a files to keep the 893 // audio/mp4 mimetype, even if they are really "enhanced podcasts" 894 // with a video track 895 return; 896 } 897 mMimeType = mimeType; 898 } 899 900 /** 901 * Formats the data into a values array suitable for use with the Media 902 * Content Provider. 903 * 904 * @return a map of values 905 */ 906 @UnsupportedAppUsage 907 private ContentValues toValues() { 908 ContentValues map = new ContentValues(); 909 910 map.put(MediaStore.MediaColumns.DATA, mPath); 911 map.put(MediaStore.MediaColumns.TITLE, mTitle); 912 map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); 913 map.put(MediaStore.MediaColumns.SIZE, mFileSize); 914 map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); 915 map.put(MediaStore.MediaColumns.IS_DRM, mIsDrm); 916 map.putNull(MediaStore.MediaColumns.HASH); 917 918 String resolution = null; 919 if (mWidth > 0 && mHeight > 0) { 920 map.put(MediaStore.MediaColumns.WIDTH, mWidth); 921 map.put(MediaStore.MediaColumns.HEIGHT, mHeight); 922 resolution = mWidth + "x" + mHeight; 923 } 924 925 if (!mNoMedia) { 926 if (MediaFile.isVideoMimeType(mMimeType)) { 927 map.put(Video.Media.ARTIST, (mArtist != null && mArtist.length() > 0 928 ? mArtist : MediaStore.UNKNOWN_STRING)); 929 map.put(Video.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0 930 ? mAlbum : MediaStore.UNKNOWN_STRING)); 931 map.put(Video.Media.DURATION, mDuration); 932 if (resolution != null) { 933 map.put(Video.Media.RESOLUTION, resolution); 934 } 935 if (mColorStandard >= 0) { 936 map.put(Video.Media.COLOR_STANDARD, mColorStandard); 937 } 938 if (mColorTransfer >= 0) { 939 map.put(Video.Media.COLOR_TRANSFER, mColorTransfer); 940 } 941 if (mColorRange >= 0) { 942 map.put(Video.Media.COLOR_RANGE, mColorRange); 943 } 944 if (mDate > 0) { 945 map.put(Video.Media.DATE_TAKEN, mDate); 946 } 947 } else if (MediaFile.isImageMimeType(mMimeType)) { 948 // FIXME - add DESCRIPTION 949 } else if (MediaFile.isAudioMimeType(mMimeType)) { 950 map.put(Audio.Media.ARTIST, (mArtist != null && mArtist.length() > 0) ? 951 mArtist : MediaStore.UNKNOWN_STRING); 952 map.put(Audio.Media.ALBUM_ARTIST, (mAlbumArtist != null && 953 mAlbumArtist.length() > 0) ? mAlbumArtist : null); 954 map.put(Audio.Media.ALBUM, (mAlbum != null && mAlbum.length() > 0) ? 955 mAlbum : MediaStore.UNKNOWN_STRING); 956 map.put(Audio.Media.COMPOSER, mComposer); 957 map.put(Audio.Media.GENRE, mGenre); 958 if (mYear != 0) { 959 map.put(Audio.Media.YEAR, mYear); 960 } 961 map.put(Audio.Media.TRACK, mTrack); 962 map.put(Audio.Media.DURATION, mDuration); 963 map.put(Audio.Media.COMPILATION, mCompilation); 964 } 965 } 966 return map; 967 } 968 969 @UnsupportedAppUsage 970 private Uri endFile(FileEntry entry, boolean ringtones, boolean notifications, 971 boolean alarms, boolean podcasts, boolean audiobooks, boolean music) 972 throws RemoteException { 973 // update database 974 975 // use album artist if artist is missing 976 if (mArtist == null || mArtist.length() == 0) { 977 mArtist = mAlbumArtist; 978 } 979 980 ContentValues values = toValues(); 981 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 982 if (title == null || TextUtils.isEmpty(title.trim())) { 983 title = MediaFile.getFileTitle(values.getAsString(MediaStore.MediaColumns.DATA)); 984 values.put(MediaStore.MediaColumns.TITLE, title); 985 } 986 String album = values.getAsString(Audio.Media.ALBUM); 987 if (MediaStore.UNKNOWN_STRING.equals(album)) { 988 album = values.getAsString(MediaStore.MediaColumns.DATA); 989 // extract last path segment before file name 990 int lastSlash = album.lastIndexOf('/'); 991 if (lastSlash >= 0) { 992 int previousSlash = 0; 993 while (true) { 994 int idx = album.indexOf('/', previousSlash + 1); 995 if (idx < 0 || idx >= lastSlash) { 996 break; 997 } 998 previousSlash = idx; 999 } 1000 if (previousSlash != 0) { 1001 album = album.substring(previousSlash + 1, lastSlash); 1002 values.put(Audio.Media.ALBUM, album); 1003 } 1004 } 1005 } 1006 long rowId = entry.mRowId; 1007 if (MediaFile.isAudioMimeType(mMimeType) && (rowId == 0 || mMtpObjectHandle != 0)) { 1008 // Only set these for new entries. For existing entries, they 1009 // may have been modified later, and we want to keep the current 1010 // values so that custom ringtones still show up in the ringtone 1011 // picker. 1012 values.put(Audio.Media.IS_RINGTONE, ringtones); 1013 values.put(Audio.Media.IS_NOTIFICATION, notifications); 1014 values.put(Audio.Media.IS_ALARM, alarms); 1015 values.put(Audio.Media.IS_MUSIC, music); 1016 values.put(Audio.Media.IS_PODCAST, podcasts); 1017 values.put(Audio.Media.IS_AUDIOBOOK, audiobooks); 1018 } else if (MediaFile.isExifMimeType(mMimeType) && !mNoMedia) { 1019 ExifInterface exif = null; 1020 try { 1021 exif = new ExifInterface(entry.mPath); 1022 } catch (Exception ex) { 1023 // exif is null 1024 } 1025 if (exif != null) { 1026 long time = exif.getGpsDateTime(); 1027 if (time != -1) { 1028 values.put(Images.Media.DATE_TAKEN, time); 1029 } else { 1030 // If no time zone information is available, we should consider using 1031 // EXIF local time as taken time if the difference between file time 1032 // and EXIF local time is not less than 1 Day, otherwise MediaProvider 1033 // will use file time as taken time. 1034 time = exif.getDateTime(); 1035 if (time != -1 && Math.abs(mLastModified * 1000 - time) >= 86400000) { 1036 values.put(Images.Media.DATE_TAKEN, time); 1037 } 1038 } 1039 1040 int orientation = exif.getAttributeInt( 1041 ExifInterface.TAG_ORIENTATION, -1); 1042 if (orientation != -1) { 1043 // We only recognize a subset of orientation tag values. 1044 int degree; 1045 switch(orientation) { 1046 case ExifInterface.ORIENTATION_ROTATE_90: 1047 degree = 90; 1048 break; 1049 case ExifInterface.ORIENTATION_ROTATE_180: 1050 degree = 180; 1051 break; 1052 case ExifInterface.ORIENTATION_ROTATE_270: 1053 degree = 270; 1054 break; 1055 default: 1056 degree = 0; 1057 break; 1058 } 1059 values.put(Images.Media.ORIENTATION, degree); 1060 } 1061 } 1062 } 1063 1064 Uri tableUri = mFilesUri; 1065 int mediaType = FileColumns.MEDIA_TYPE_NONE; 1066 MediaInserter inserter = mMediaInserter; 1067 if (!mNoMedia) { 1068 if (MediaFile.isVideoMimeType(mMimeType)) { 1069 tableUri = mVideoUri; 1070 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 1071 } else if (MediaFile.isImageMimeType(mMimeType)) { 1072 tableUri = mImagesUri; 1073 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 1074 } else if (MediaFile.isAudioMimeType(mMimeType)) { 1075 tableUri = mAudioUri; 1076 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 1077 } else if (MediaFile.isPlayListMimeType(mMimeType)) { 1078 tableUri = mPlaylistsUri; 1079 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 1080 } 1081 } 1082 Uri result = null; 1083 boolean needToSetSettings = false; 1084 // Setting a flag in order not to use bulk insert for the file related with 1085 // notifications, ringtones, and alarms, because the rowId of the inserted file is 1086 // needed. 1087 if (notifications && !mDefaultNotificationSet) { 1088 if (TextUtils.isEmpty(mDefaultNotificationFilename) || 1089 doesPathHaveFilename(entry.mPath, mDefaultNotificationFilename)) { 1090 needToSetSettings = true; 1091 } 1092 } else if (ringtones && !mDefaultRingtoneSet) { 1093 if (TextUtils.isEmpty(mDefaultRingtoneFilename) || 1094 doesPathHaveFilename(entry.mPath, mDefaultRingtoneFilename)) { 1095 needToSetSettings = true; 1096 } 1097 } else if (alarms && !mDefaultAlarmSet) { 1098 if (TextUtils.isEmpty(mDefaultAlarmAlertFilename) || 1099 doesPathHaveFilename(entry.mPath, mDefaultAlarmAlertFilename)) { 1100 needToSetSettings = true; 1101 } 1102 } 1103 1104 if (rowId == 0) { 1105 if (mMtpObjectHandle != 0) { 1106 values.put(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, mMtpObjectHandle); 1107 } 1108 if (tableUri == mFilesUri) { 1109 int format = entry.mFormat; 1110 if (format == 0) { 1111 format = MediaFile.getFormatCode(entry.mPath, mMimeType); 1112 } 1113 values.put(Files.FileColumns.FORMAT, format); 1114 } 1115 // New file, insert it. 1116 // Directories need to be inserted before the files they contain, so they 1117 // get priority when bulk inserting. 1118 // If the rowId of the inserted file is needed, it gets inserted immediately, 1119 // bypassing the bulk inserter. 1120 if (inserter == null || needToSetSettings) { 1121 if (inserter != null) { 1122 inserter.flushAll(); 1123 } 1124 result = mMediaProvider.insert(tableUri, values); 1125 } else if (entry.mFormat == MtpConstants.FORMAT_ASSOCIATION) { 1126 inserter.insertwithPriority(tableUri, values); 1127 } else { 1128 inserter.insert(tableUri, values); 1129 } 1130 1131 if (result != null) { 1132 rowId = ContentUris.parseId(result); 1133 entry.mRowId = rowId; 1134 } 1135 } else { 1136 // updated file 1137 result = ContentUris.withAppendedId(tableUri, rowId); 1138 // path should never change, and we want to avoid replacing mixed cased paths 1139 // with squashed lower case paths 1140 values.remove(MediaStore.MediaColumns.DATA); 1141 1142 if (!mNoMedia) { 1143 // Changing media type must be done as separate update 1144 if (mediaType != entry.mMediaType) { 1145 final ContentValues mediaTypeValues = new ContentValues(); 1146 mediaTypeValues.put(FileColumns.MEDIA_TYPE, mediaType); 1147 mMediaProvider.update(ContentUris.withAppendedId(mFilesUri, rowId), 1148 mediaTypeValues, null, null); 1149 } 1150 } 1151 1152 mMediaProvider.update(result, values, null, null); 1153 } 1154 1155 if(needToSetSettings) { 1156 if (notifications) { 1157 setRingtoneIfNotSet(Settings.System.NOTIFICATION_SOUND, tableUri, rowId); 1158 mDefaultNotificationSet = true; 1159 } else if (ringtones) { 1160 setRingtoneIfNotSet(Settings.System.RINGTONE, tableUri, rowId); 1161 mDefaultRingtoneSet = true; 1162 } else if (alarms) { 1163 setRingtoneIfNotSet(Settings.System.ALARM_ALERT, tableUri, rowId); 1164 mDefaultAlarmSet = true; 1165 } 1166 } 1167 1168 return result; 1169 } 1170 1171 private boolean doesPathHaveFilename(String path, String filename) { 1172 int pathFilenameStart = path.lastIndexOf(File.separatorChar) + 1; 1173 int filenameLength = filename.length(); 1174 return path.regionMatches(pathFilenameStart, filename, 0, filenameLength) && 1175 pathFilenameStart + filenameLength == path.length(); 1176 } 1177 1178 private void setRingtoneIfNotSet(String settingName, Uri uri, long rowId) { 1179 if (wasRingtoneAlreadySet(settingName)) { 1180 return; 1181 } 1182 1183 ContentResolver cr = mContext.getContentResolver(); 1184 String existingSettingValue = Settings.System.getString(cr, settingName); 1185 if (TextUtils.isEmpty(existingSettingValue)) { 1186 final Uri settingUri = Settings.System.getUriFor(settingName); 1187 final Uri ringtoneUri = ContentUris.withAppendedId(uri, rowId); 1188 RingtoneManager.setActualDefaultRingtoneUri(mContext, 1189 RingtoneManager.getDefaultType(settingUri), ringtoneUri); 1190 } 1191 Settings.System.putInt(cr, settingSetIndicatorName(settingName), 1); 1192 } 1193 1194 /** @deprecated file types no longer exist */ 1195 @Deprecated 1196 @UnsupportedAppUsage 1197 private int getFileTypeFromDrm(String path) { 1198 return 0; 1199 } 1200 1201 private void getMimeTypeFromDrm(String path) { 1202 mMimeType = null; 1203 1204 if (mDrmManagerClient == null) { 1205 mDrmManagerClient = new DrmManagerClient(mContext); 1206 } 1207 1208 if (mDrmManagerClient.canHandle(path, null)) { 1209 mIsDrm = true; 1210 mMimeType = mDrmManagerClient.getOriginalMimeType(path); 1211 } 1212 1213 if (mMimeType == null) { 1214 mMimeType = ContentResolver.MIME_TYPE_DEFAULT; 1215 } 1216 } 1217 1218 }; // end of anonymous MediaScannerClient instance 1219 1220 private static boolean isSystemSoundWithMetadata(String path) { 1221 if (path.startsWith(SYSTEM_SOUNDS_DIR + ALARMS_DIR) 1222 || path.startsWith(SYSTEM_SOUNDS_DIR + RINGTONES_DIR) 1223 || path.startsWith(SYSTEM_SOUNDS_DIR + NOTIFICATIONS_DIR) 1224 || path.startsWith(OEM_SOUNDS_DIR + ALARMS_DIR) 1225 || path.startsWith(OEM_SOUNDS_DIR + RINGTONES_DIR) 1226 || path.startsWith(OEM_SOUNDS_DIR + NOTIFICATIONS_DIR) 1227 || path.startsWith(PRODUCT_SOUNDS_DIR + ALARMS_DIR) 1228 || path.startsWith(PRODUCT_SOUNDS_DIR + RINGTONES_DIR) 1229 || path.startsWith(PRODUCT_SOUNDS_DIR + NOTIFICATIONS_DIR)) { 1230 return true; 1231 } 1232 return false; 1233 } 1234 1235 private String settingSetIndicatorName(String base) { 1236 return base + "_set"; 1237 } 1238 1239 private boolean wasRingtoneAlreadySet(String name) { 1240 ContentResolver cr = mContext.getContentResolver(); 1241 String indicatorName = settingSetIndicatorName(name); 1242 try { 1243 return Settings.System.getInt(cr, indicatorName) != 0; 1244 } catch (SettingNotFoundException e) { 1245 return false; 1246 } 1247 } 1248 1249 @UnsupportedAppUsage 1250 private void prescan(String filePath, boolean prescanFiles) throws RemoteException { 1251 Cursor c = null; 1252 String where = null; 1253 String[] selectionArgs = null; 1254 1255 mPlayLists.clear(); 1256 1257 if (filePath != null) { 1258 // query for only one file 1259 where = MediaStore.Files.FileColumns._ID + ">?" + 1260 " AND " + Files.FileColumns.DATA + "=?"; 1261 selectionArgs = new String[] { "", filePath }; 1262 } else { 1263 where = MediaStore.Files.FileColumns._ID + ">?"; 1264 selectionArgs = new String[] { "" }; 1265 } 1266 1267 mDefaultRingtoneSet = wasRingtoneAlreadySet(Settings.System.RINGTONE); 1268 mDefaultNotificationSet = wasRingtoneAlreadySet(Settings.System.NOTIFICATION_SOUND); 1269 mDefaultAlarmSet = wasRingtoneAlreadySet(Settings.System.ALARM_ALERT); 1270 1271 // Tell the provider to not delete the file. 1272 // If the file is truly gone the delete is unnecessary, and we want to avoid 1273 // accidentally deleting files that are really there (this may happen if the 1274 // filesystem is mounted and unmounted while the scanner is running). 1275 Uri.Builder builder = mFilesUri.buildUpon(); 1276 builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false"); 1277 MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, builder.build()); 1278 1279 // Build the list of files from the content provider 1280 try { 1281 if (prescanFiles) { 1282 // First read existing files from the files table. 1283 // Because we'll be deleting entries for missing files as we go, 1284 // we need to query the database in small batches, to avoid problems 1285 // with CursorWindow positioning. 1286 long lastId = Long.MIN_VALUE; 1287 Uri limitUri = mFilesUri.buildUpon() 1288 .appendQueryParameter(MediaStore.PARAM_LIMIT, "1000").build(); 1289 1290 while (true) { 1291 selectionArgs[0] = "" + lastId; 1292 if (c != null) { 1293 c.close(); 1294 c = null; 1295 } 1296 c = mMediaProvider.query(limitUri, FILES_PRESCAN_PROJECTION, 1297 where, selectionArgs, MediaStore.Files.FileColumns._ID, null); 1298 if (c == null) { 1299 break; 1300 } 1301 1302 int num = c.getCount(); 1303 1304 if (num == 0) { 1305 break; 1306 } 1307 while (c.moveToNext()) { 1308 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1309 String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1310 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1311 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1312 lastId = rowId; 1313 1314 // Only consider entries with absolute path names. 1315 // This allows storing URIs in the database without the 1316 // media scanner removing them. 1317 if (path != null && path.startsWith("/")) { 1318 boolean exists = false; 1319 try { 1320 exists = Os.access(path, android.system.OsConstants.F_OK); 1321 } catch (ErrnoException e1) { 1322 } 1323 if (!exists && !MtpConstants.isAbstractObject(format)) { 1324 // do not delete missing playlists, since they may have been 1325 // modified by the user. 1326 // The user can delete them in the media player instead. 1327 // instead, clear the path and lastModified fields in the row 1328 String mimeType = MediaFile.getMimeTypeForFile(path); 1329 if (!MediaFile.isPlayListMimeType(mimeType)) { 1330 deleter.delete(rowId); 1331 if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 1332 deleter.flush(); 1333 String parent = new File(path).getParent(); 1334 mMediaProvider.call(MediaStore.UNHIDE_CALL, parent, null); 1335 } 1336 } 1337 } 1338 } 1339 } 1340 } 1341 } 1342 } 1343 finally { 1344 if (c != null) { 1345 c.close(); 1346 } 1347 deleter.flush(); 1348 } 1349 1350 // compute original size of images 1351 mOriginalCount = 0; 1352 c = mMediaProvider.query(mImagesUri, ID_PROJECTION, null, null, null, null); 1353 if (c != null) { 1354 mOriginalCount = c.getCount(); 1355 c.close(); 1356 } 1357 } 1358 1359 static class MediaBulkDeleter { 1360 StringBuilder whereClause = new StringBuilder(); 1361 ArrayList<String> whereArgs = new ArrayList<String>(100); 1362 final ContentProviderClient mProvider; 1363 final Uri mBaseUri; 1364 1365 public MediaBulkDeleter(ContentProviderClient provider, Uri baseUri) { 1366 mProvider = provider; 1367 mBaseUri = baseUri; 1368 } 1369 1370 public void delete(long id) throws RemoteException { 1371 if (whereClause.length() != 0) { 1372 whereClause.append(","); 1373 } 1374 whereClause.append("?"); 1375 whereArgs.add("" + id); 1376 if (whereArgs.size() > 100) { 1377 flush(); 1378 } 1379 } 1380 public void flush() throws RemoteException { 1381 int size = whereArgs.size(); 1382 if (size > 0) { 1383 String [] foo = new String [size]; 1384 foo = whereArgs.toArray(foo); 1385 int numrows = mProvider.delete(mBaseUri, 1386 MediaStore.MediaColumns._ID + " IN (" + 1387 whereClause.toString() + ")", foo); 1388 //Log.i("@@@@@@@@@", "rows deleted: " + numrows); 1389 whereClause.setLength(0); 1390 whereArgs.clear(); 1391 } 1392 } 1393 } 1394 1395 @UnsupportedAppUsage 1396 private void postscan(final String[] directories) throws RemoteException { 1397 1398 // handle playlists last, after we know what media files are on the storage. 1399 if (mProcessPlaylists) { 1400 processPlayLists(); 1401 } 1402 1403 // allow GC to clean up 1404 mPlayLists.clear(); 1405 } 1406 1407 private void releaseResources() { 1408 // release the DrmManagerClient resources 1409 if (mDrmManagerClient != null) { 1410 mDrmManagerClient.close(); 1411 mDrmManagerClient = null; 1412 } 1413 } 1414 1415 public void scanDirectories(String[] directories) { 1416 try { 1417 long start = System.currentTimeMillis(); 1418 prescan(null, true); 1419 long prescan = System.currentTimeMillis(); 1420 1421 if (ENABLE_BULK_INSERTS) { 1422 // create MediaInserter for bulk inserts 1423 mMediaInserter = new MediaInserter(mMediaProvider, 500); 1424 } 1425 1426 for (int i = 0; i < directories.length; i++) { 1427 processDirectory(directories[i], mClient); 1428 } 1429 1430 if (ENABLE_BULK_INSERTS) { 1431 // flush remaining inserts 1432 mMediaInserter.flushAll(); 1433 mMediaInserter = null; 1434 } 1435 1436 long scan = System.currentTimeMillis(); 1437 postscan(directories); 1438 long end = System.currentTimeMillis(); 1439 1440 if (false) { 1441 Log.d(TAG, " prescan time: " + (prescan - start) + "ms\n"); 1442 Log.d(TAG, " scan time: " + (scan - prescan) + "ms\n"); 1443 Log.d(TAG, "postscan time: " + (end - scan) + "ms\n"); 1444 Log.d(TAG, " total time: " + (end - start) + "ms\n"); 1445 } 1446 } catch (SQLException e) { 1447 // this might happen if the SD card is removed while the media scanner is running 1448 Log.e(TAG, "SQLException in MediaScanner.scan()", e); 1449 } catch (UnsupportedOperationException e) { 1450 // this might happen if the SD card is removed while the media scanner is running 1451 Log.e(TAG, "UnsupportedOperationException in MediaScanner.scan()", e); 1452 } catch (RemoteException e) { 1453 Log.e(TAG, "RemoteException in MediaScanner.scan()", e); 1454 } finally { 1455 releaseResources(); 1456 } 1457 } 1458 1459 // this function is used to scan a single file 1460 @UnsupportedAppUsage 1461 public Uri scanSingleFile(String path, String mimeType) { 1462 try { 1463 prescan(path, true); 1464 1465 File file = new File(path); 1466 if (!file.exists() || !file.canRead()) { 1467 return null; 1468 } 1469 1470 // lastModified is in milliseconds on Files. 1471 long lastModifiedSeconds = file.lastModified() / 1000; 1472 1473 // always scan the file, so we can return the content://media Uri for existing files 1474 return mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1475 false, true, MediaScanner.isNoMediaPath(path)); 1476 } catch (RemoteException e) { 1477 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1478 return null; 1479 } finally { 1480 releaseResources(); 1481 } 1482 } 1483 1484 private static boolean isNoMediaFile(String path) { 1485 File file = new File(path); 1486 if (file.isDirectory()) return false; 1487 1488 // special case certain file names 1489 // I use regionMatches() instead of substring() below 1490 // to avoid memory allocation 1491 int lastSlash = path.lastIndexOf('/'); 1492 if (lastSlash >= 0 && lastSlash + 2 < path.length()) { 1493 // ignore those ._* files created by MacOS 1494 if (path.regionMatches(lastSlash + 1, "._", 0, 2)) { 1495 return true; 1496 } 1497 1498 // ignore album art files created by Windows Media Player: 1499 // Folder.jpg, AlbumArtSmall.jpg, AlbumArt_{...}_Large.jpg 1500 // and AlbumArt_{...}_Small.jpg 1501 if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { 1502 if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || 1503 path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { 1504 return true; 1505 } 1506 int length = path.length() - lastSlash - 1; 1507 if ((length == 17 && path.regionMatches( 1508 true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || 1509 (length == 10 1510 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { 1511 return true; 1512 } 1513 } 1514 } 1515 return false; 1516 } 1517 1518 private static HashMap<String,String> mNoMediaPaths = new HashMap<String,String>(); 1519 private static HashMap<String,String> mMediaPaths = new HashMap<String,String>(); 1520 1521 /* MediaProvider calls this when a .nomedia file is added or removed */ 1522 public static void clearMediaPathCache(boolean clearMediaPaths, boolean clearNoMediaPaths) { 1523 synchronized (MediaScanner.class) { 1524 if (clearMediaPaths) { 1525 mMediaPaths.clear(); 1526 } 1527 if (clearNoMediaPaths) { 1528 mNoMediaPaths.clear(); 1529 } 1530 } 1531 } 1532 1533 @UnsupportedAppUsage 1534 public static boolean isNoMediaPath(String path) { 1535 if (path == null) { 1536 return false; 1537 } 1538 // return true if file or any parent directory has name starting with a dot 1539 if (path.indexOf("/.") >= 0) { 1540 return true; 1541 } 1542 1543 int firstSlash = path.lastIndexOf('/'); 1544 if (firstSlash <= 0) { 1545 return false; 1546 } 1547 String parent = path.substring(0, firstSlash); 1548 1549 synchronized (MediaScanner.class) { 1550 if (mNoMediaPaths.containsKey(parent)) { 1551 return true; 1552 } else if (!mMediaPaths.containsKey(parent)) { 1553 // check to see if any parent directories have a ".nomedia" file 1554 // start from 1 so we don't bother checking in the root directory 1555 int offset = 1; 1556 while (offset >= 0) { 1557 int slashIndex = path.indexOf('/', offset); 1558 if (slashIndex > offset) { 1559 slashIndex++; // move past slash 1560 File file = new File(path.substring(0, slashIndex) + ".nomedia"); 1561 if (file.exists()) { 1562 // we have a .nomedia in one of the parent directories 1563 mNoMediaPaths.put(parent, ""); 1564 return true; 1565 } 1566 } 1567 offset = slashIndex; 1568 } 1569 mMediaPaths.put(parent, ""); 1570 } 1571 } 1572 1573 return isNoMediaFile(path); 1574 } 1575 1576 public void scanMtpFile(String path, int objectHandle, int format) { 1577 String mimeType = MediaFile.getMimeType(path, format); 1578 File file = new File(path); 1579 long lastModifiedSeconds = file.lastModified() / 1000; 1580 1581 if (!MediaFile.isAudioMimeType(mimeType) && !MediaFile.isVideoMimeType(mimeType) && 1582 !MediaFile.isImageMimeType(mimeType) && !MediaFile.isPlayListMimeType(mimeType) && 1583 !MediaFile.isDrmMimeType(mimeType)) { 1584 1585 // no need to use the media scanner, but we need to update last modified and file size 1586 ContentValues values = new ContentValues(); 1587 values.put(Files.FileColumns.SIZE, file.length()); 1588 values.put(Files.FileColumns.DATE_MODIFIED, lastModifiedSeconds); 1589 try { 1590 String[] whereArgs = new String[] { Integer.toString(objectHandle) }; 1591 mMediaProvider.update(Files.getMtpObjectsUri(mVolumeName), values, 1592 "_id=?", whereArgs); 1593 } catch (RemoteException e) { 1594 Log.e(TAG, "RemoteException in scanMtpFile", e); 1595 } 1596 return; 1597 } 1598 1599 mMtpObjectHandle = objectHandle; 1600 Cursor fileList = null; 1601 try { 1602 if (MediaFile.isPlayListMimeType(mimeType)) { 1603 // build file cache so we can look up tracks in the playlist 1604 prescan(null, true); 1605 1606 FileEntry entry = makeEntryFor(path); 1607 if (entry != null) { 1608 fileList = mMediaProvider.query(mFilesUri, 1609 FILES_PRESCAN_PROJECTION, null, null, null, null); 1610 processPlayList(entry, fileList); 1611 } 1612 } else { 1613 // MTP will create a file entry for us so we don't want to do it in prescan 1614 prescan(path, false); 1615 1616 // always scan the file, so we can return the content://media Uri for existing files 1617 mClient.doScanFile(path, mimeType, lastModifiedSeconds, file.length(), 1618 (format == MtpConstants.FORMAT_ASSOCIATION), true, isNoMediaPath(path)); 1619 } 1620 } catch (RemoteException e) { 1621 Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e); 1622 } finally { 1623 mMtpObjectHandle = 0; 1624 if (fileList != null) { 1625 fileList.close(); 1626 } 1627 releaseResources(); 1628 } 1629 } 1630 1631 @UnsupportedAppUsage 1632 FileEntry makeEntryFor(String path) { 1633 String where; 1634 String[] selectionArgs; 1635 1636 Cursor c = null; 1637 try { 1638 where = Files.FileColumns.DATA + "=?"; 1639 selectionArgs = new String[] { path }; 1640 c = mMediaProvider.query(mFilesFullUri, FILES_PRESCAN_PROJECTION, 1641 where, selectionArgs, null, null); 1642 if (c != null && c.moveToFirst()) { 1643 long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1644 long lastModified = c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX); 1645 int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX); 1646 int mediaType = c.getInt(FILES_PRESCAN_MEDIA_TYPE_COLUMN_INDEX); 1647 return new FileEntry(rowId, path, lastModified, format, mediaType); 1648 } 1649 } catch (RemoteException e) { 1650 } finally { 1651 if (c != null) { 1652 c.close(); 1653 } 1654 } 1655 return null; 1656 } 1657 1658 // returns the number of matching file/directory names, starting from the right 1659 private int matchPaths(String path1, String path2) { 1660 int result = 0; 1661 int end1 = path1.length(); 1662 int end2 = path2.length(); 1663 1664 while (end1 > 0 && end2 > 0) { 1665 int slash1 = path1.lastIndexOf('/', end1 - 1); 1666 int slash2 = path2.lastIndexOf('/', end2 - 1); 1667 int backSlash1 = path1.lastIndexOf('\\', end1 - 1); 1668 int backSlash2 = path2.lastIndexOf('\\', end2 - 1); 1669 int start1 = (slash1 > backSlash1 ? slash1 : backSlash1); 1670 int start2 = (slash2 > backSlash2 ? slash2 : backSlash2); 1671 if (start1 < 0) start1 = 0; else start1++; 1672 if (start2 < 0) start2 = 0; else start2++; 1673 int length = end1 - start1; 1674 if (end2 - start2 != length) break; 1675 if (path1.regionMatches(true, start1, path2, start2, length)) { 1676 result++; 1677 end1 = start1 - 1; 1678 end2 = start2 - 1; 1679 } else break; 1680 } 1681 1682 return result; 1683 } 1684 1685 private boolean matchEntries(long rowId, String data) { 1686 1687 int len = mPlaylistEntries.size(); 1688 boolean done = true; 1689 for (int i = 0; i < len; i++) { 1690 PlaylistEntry entry = mPlaylistEntries.get(i); 1691 if (entry.bestmatchlevel == Integer.MAX_VALUE) { 1692 continue; // this entry has been matched already 1693 } 1694 done = false; 1695 if (data.equalsIgnoreCase(entry.path)) { 1696 entry.bestmatchid = rowId; 1697 entry.bestmatchlevel = Integer.MAX_VALUE; 1698 continue; // no need for path matching 1699 } 1700 1701 int matchLength = matchPaths(data, entry.path); 1702 if (matchLength > entry.bestmatchlevel) { 1703 entry.bestmatchid = rowId; 1704 entry.bestmatchlevel = matchLength; 1705 } 1706 } 1707 return done; 1708 } 1709 1710 private void cachePlaylistEntry(String line, String playListDirectory) { 1711 PlaylistEntry entry = new PlaylistEntry(); 1712 // watch for trailing whitespace 1713 int entryLength = line.length(); 1714 while (entryLength > 0 && Character.isWhitespace(line.charAt(entryLength - 1))) entryLength--; 1715 // path should be longer than 3 characters. 1716 // avoid index out of bounds errors below by returning here. 1717 if (entryLength < 3) return; 1718 if (entryLength < line.length()) line = line.substring(0, entryLength); 1719 1720 // does entry appear to be an absolute path? 1721 // look for Unix or DOS absolute paths 1722 char ch1 = line.charAt(0); 1723 boolean fullPath = (ch1 == '/' || 1724 (Character.isLetter(ch1) && line.charAt(1) == ':' && line.charAt(2) == '\\')); 1725 // if we have a relative path, combine entry with playListDirectory 1726 if (!fullPath) 1727 line = playListDirectory + line; 1728 entry.path = line; 1729 //FIXME - should we look for "../" within the path? 1730 1731 mPlaylistEntries.add(entry); 1732 } 1733 1734 private void processCachedPlaylist(Cursor fileList, ContentValues values, Uri playlistUri) { 1735 fileList.moveToPosition(-1); 1736 while (fileList.moveToNext()) { 1737 long rowId = fileList.getLong(FILES_PRESCAN_ID_COLUMN_INDEX); 1738 String data = fileList.getString(FILES_PRESCAN_PATH_COLUMN_INDEX); 1739 if (matchEntries(rowId, data)) { 1740 break; 1741 } 1742 } 1743 1744 int len = mPlaylistEntries.size(); 1745 int index = 0; 1746 for (int i = 0; i < len; i++) { 1747 PlaylistEntry entry = mPlaylistEntries.get(i); 1748 if (entry.bestmatchlevel > 0) { 1749 try { 1750 values.clear(); 1751 values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, Integer.valueOf(index)); 1752 values.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, Long.valueOf(entry.bestmatchid)); 1753 mMediaProvider.insert(playlistUri, values); 1754 index++; 1755 } catch (RemoteException e) { 1756 Log.e(TAG, "RemoteException in MediaScanner.processCachedPlaylist()", e); 1757 return; 1758 } 1759 } 1760 } 1761 mPlaylistEntries.clear(); 1762 } 1763 1764 private void processM3uPlayList(String path, String playListDirectory, Uri uri, 1765 ContentValues values, Cursor fileList) { 1766 BufferedReader reader = null; 1767 try { 1768 File f = new File(path); 1769 if (f.exists()) { 1770 reader = new BufferedReader( 1771 new InputStreamReader(new FileInputStream(f)), 8192); 1772 String line = reader.readLine(); 1773 mPlaylistEntries.clear(); 1774 while (line != null) { 1775 // ignore comment lines, which begin with '#' 1776 if (line.length() > 0 && line.charAt(0) != '#') { 1777 cachePlaylistEntry(line, playListDirectory); 1778 } 1779 line = reader.readLine(); 1780 } 1781 1782 processCachedPlaylist(fileList, values, uri); 1783 } 1784 } catch (IOException e) { 1785 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1786 } finally { 1787 try { 1788 if (reader != null) 1789 reader.close(); 1790 } catch (IOException e) { 1791 Log.e(TAG, "IOException in MediaScanner.processM3uPlayList()", e); 1792 } 1793 } 1794 } 1795 1796 private void processPlsPlayList(String path, String playListDirectory, Uri uri, 1797 ContentValues values, Cursor fileList) { 1798 BufferedReader reader = null; 1799 try { 1800 File f = new File(path); 1801 if (f.exists()) { 1802 reader = new BufferedReader( 1803 new InputStreamReader(new FileInputStream(f)), 8192); 1804 String line = reader.readLine(); 1805 mPlaylistEntries.clear(); 1806 while (line != null) { 1807 // ignore comment lines, which begin with '#' 1808 if (line.startsWith("File")) { 1809 int equals = line.indexOf('='); 1810 if (equals > 0) { 1811 cachePlaylistEntry(line.substring(equals + 1), playListDirectory); 1812 } 1813 } 1814 line = reader.readLine(); 1815 } 1816 1817 processCachedPlaylist(fileList, values, uri); 1818 } 1819 } catch (IOException e) { 1820 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1821 } finally { 1822 try { 1823 if (reader != null) 1824 reader.close(); 1825 } catch (IOException e) { 1826 Log.e(TAG, "IOException in MediaScanner.processPlsPlayList()", e); 1827 } 1828 } 1829 } 1830 1831 class WplHandler implements ElementListener { 1832 1833 final ContentHandler handler; 1834 String playListDirectory; 1835 1836 public WplHandler(String playListDirectory, Uri uri, Cursor fileList) { 1837 this.playListDirectory = playListDirectory; 1838 1839 RootElement root = new RootElement("smil"); 1840 Element body = root.getChild("body"); 1841 Element seq = body.getChild("seq"); 1842 Element media = seq.getChild("media"); 1843 media.setElementListener(this); 1844 1845 this.handler = root.getContentHandler(); 1846 } 1847 1848 @Override 1849 public void start(Attributes attributes) { 1850 String path = attributes.getValue("", "src"); 1851 if (path != null) { 1852 cachePlaylistEntry(path, playListDirectory); 1853 } 1854 } 1855 1856 @Override 1857 public void end() { 1858 } 1859 1860 ContentHandler getContentHandler() { 1861 return handler; 1862 } 1863 } 1864 1865 private void processWplPlayList(String path, String playListDirectory, Uri uri, 1866 ContentValues values, Cursor fileList) { 1867 FileInputStream fis = null; 1868 try { 1869 File f = new File(path); 1870 if (f.exists()) { 1871 fis = new FileInputStream(f); 1872 1873 mPlaylistEntries.clear(); 1874 Xml.parse(fis, Xml.findEncodingByName("UTF-8"), 1875 new WplHandler(playListDirectory, uri, fileList).getContentHandler()); 1876 1877 processCachedPlaylist(fileList, values, uri); 1878 } 1879 } catch (SAXException e) { 1880 e.printStackTrace(); 1881 } catch (IOException e) { 1882 e.printStackTrace(); 1883 } finally { 1884 try { 1885 if (fis != null) 1886 fis.close(); 1887 } catch (IOException e) { 1888 Log.e(TAG, "IOException in MediaScanner.processWplPlayList()", e); 1889 } 1890 } 1891 } 1892 1893 private void processPlayList(FileEntry entry, Cursor fileList) throws RemoteException { 1894 String path = entry.mPath; 1895 ContentValues values = new ContentValues(); 1896 int lastSlash = path.lastIndexOf('/'); 1897 if (lastSlash < 0) throw new IllegalArgumentException("bad path " + path); 1898 Uri uri, membersUri; 1899 long rowId = entry.mRowId; 1900 1901 // make sure we have a name 1902 String name = values.getAsString(MediaStore.Audio.Playlists.NAME); 1903 if (name == null) { 1904 name = values.getAsString(MediaStore.MediaColumns.TITLE); 1905 if (name == null) { 1906 // extract name from file name 1907 int lastDot = path.lastIndexOf('.'); 1908 name = (lastDot < 0 ? path.substring(lastSlash + 1) 1909 : path.substring(lastSlash + 1, lastDot)); 1910 } 1911 } 1912 1913 values.put(MediaStore.Audio.Playlists.NAME, name); 1914 values.put(MediaStore.Audio.Playlists.DATE_MODIFIED, entry.mLastModified); 1915 1916 if (rowId == 0) { 1917 values.put(MediaStore.Audio.Playlists.DATA, path); 1918 uri = mMediaProvider.insert(mPlaylistsUri, values); 1919 rowId = ContentUris.parseId(uri); 1920 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1921 } else { 1922 uri = ContentUris.withAppendedId(mPlaylistsUri, rowId); 1923 mMediaProvider.update(uri, values, null, null); 1924 1925 // delete members of existing playlist 1926 membersUri = Uri.withAppendedPath(uri, Playlists.Members.CONTENT_DIRECTORY); 1927 mMediaProvider.delete(membersUri, null, null); 1928 } 1929 1930 String playListDirectory = path.substring(0, lastSlash + 1); 1931 String mimeType = MediaFile.getMimeTypeForFile(path); 1932 switch (mimeType) { 1933 case "application/vnd.ms-wpl": 1934 processWplPlayList(path, playListDirectory, membersUri, values, fileList); 1935 break; 1936 case "audio/x-mpegurl": 1937 processM3uPlayList(path, playListDirectory, membersUri, values, fileList); 1938 break; 1939 case "audio/x-scpls": 1940 processPlsPlayList(path, playListDirectory, membersUri, values, fileList); 1941 break; 1942 } 1943 } 1944 1945 private void processPlayLists() throws RemoteException { 1946 Iterator<FileEntry> iterator = mPlayLists.iterator(); 1947 Cursor fileList = null; 1948 try { 1949 // use the files uri and projection because we need the format column, 1950 // but restrict the query to just audio files 1951 fileList = mMediaProvider.query(mFilesUri, FILES_PRESCAN_PROJECTION, 1952 "media_type=2", null, null, null); 1953 while (iterator.hasNext()) { 1954 FileEntry entry = iterator.next(); 1955 // only process playlist files if they are new or have been modified since the last scan 1956 if (entry.mLastModifiedChanged) { 1957 processPlayList(entry, fileList); 1958 } 1959 } 1960 } catch (RemoteException e1) { 1961 } finally { 1962 if (fileList != null) { 1963 fileList.close(); 1964 } 1965 } 1966 } 1967 1968 private native void processDirectory(String path, MediaScannerClient client); 1969 private native boolean processFile(String path, String mimeType, MediaScannerClient client); 1970 @UnsupportedAppUsage 1971 private native void setLocale(String locale); 1972 1973 public native byte[] extractAlbumArt(FileDescriptor fd); 1974 1975 private static native final void native_init(); 1976 private native final void native_setup(); 1977 private native final void native_finalize(); 1978 1979 @Override 1980 public void close() { 1981 mCloseGuard.close(); 1982 if (mClosed.compareAndSet(false, true)) { 1983 mMediaProvider.close(); 1984 native_finalize(); 1985 } 1986 } 1987 1988 @Override 1989 protected void finalize() throws Throwable { 1990 try { 1991 if (mCloseGuard != null) { 1992 mCloseGuard.warnIfOpen(); 1993 } 1994 1995 close(); 1996 } finally { 1997 super.finalize(); 1998 } 1999 } 2000 } 2001