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