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