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