/* * Copyright (C) 2006 The Android Open Source Project * Copyright (C) 2013 YIXIA.COM * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.vov.vitamio; import android.content.ContentProviderClient; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.RemoteException; import android.text.TextUtils; import io.vov.vitamio.provider.MediaStore; import io.vov.vitamio.provider.MediaStore.Video; import io.vov.vitamio.utils.ContextUtils; import io.vov.vitamio.utils.FileUtils; import io.vov.vitamio.utils.Log; import java.io.File; import java.util.HashMap; import java.util.Iterator; public class MediaScanner { private static final String[] VIDEO_PROJECTION = new String[]{Video.Media._ID, Video.Media.DATA, Video.Media.DATE_MODIFIED,}; private static final int ID_VIDEO_COLUMN_INDEX = 0; private static final int PATH_VIDEO_COLUMN_INDEX = 1; private static final int DATE_MODIFIED_VIDEO_COLUMN_INDEX = 2; private Context mContext; private ContentProviderClient mProvider; private boolean mCaseInsensitivePaths; private HashMap<String, FileCacheEntry> mFileCache; private MyMediaScannerClient mClient = new MyMediaScannerClient(); public MediaScanner(Context ctx) { mContext = ctx; native_init(mClient); } private static native boolean loadFFmpeg_native(String ffmpegPath); private void initialize() { mCaseInsensitivePaths = true; } private void prescan(String filePath) throws RemoteException { mProvider = mContext.getContentResolver().acquireContentProviderClient(MediaStore.AUTHORITY); Cursor c = null; String where = null; String[] selectionArgs = null; if (mFileCache == null) mFileCache = new HashMap<String, FileCacheEntry>(); else mFileCache.clear(); try { if (filePath != null) { where = Video.Media.DATA + "=?"; selectionArgs = new String[]{filePath}; } c = mProvider.query(Video.Media.CONTENT_URI, VIDEO_PROJECTION, where, selectionArgs, null); if (c != null) { try { while (c.moveToNext()) { long rowId = c.getLong(ID_VIDEO_COLUMN_INDEX); String path = c.getString(PATH_VIDEO_COLUMN_INDEX); long lastModified = c.getLong(DATE_MODIFIED_VIDEO_COLUMN_INDEX); if (path.startsWith("/")) { File tempFile = new File(path); if (!TextUtils.isEmpty(filePath) && !tempFile.exists()) { mProvider.delete(Video.Media.CONTENT_URI, where, selectionArgs); return; } path = FileUtils.getCanonical(tempFile); String key = mCaseInsensitivePaths ? path.toLowerCase() : path; mFileCache.put(key, new FileCacheEntry(Video.Media.CONTENT_URI, rowId, path, lastModified)); } } } finally { c.close(); c = null; } } } finally { if (c != null) { c.close(); } } } ; private void postscan(String[] directories) throws RemoteException { Iterator<FileCacheEntry> iterator = mFileCache.values().iterator(); while (iterator.hasNext()) { FileCacheEntry entry = iterator.next(); String path = entry.mPath; if (!entry.mSeenInFileSystem) { if (inScanDirectory(path, directories) && !new File(path).exists()) { mProvider.delete(ContentUris.withAppendedId(entry.mTableUri, entry.mRowId), null, null); iterator.remove(); } } } mFileCache.clear(); mFileCache = null; mProvider.release(); mProvider = null; } private boolean inScanDirectory(String path, String[] directories) { for (int i = 0; i < directories.length; i++) { if (path.startsWith(directories[i])) return true; } return false; } public void scanDirectories(String[] directories) { try { long start = System.currentTimeMillis(); prescan(null); long prescan = System.currentTimeMillis(); for (int i = 0; i < directories.length; i++) { if (!TextUtils.isEmpty(directories[i])) { directories[i] = ContextUtils.fixLastSlash(directories[i]); processDirectory(directories[i], MediaFile.sFileExtensions); } } long scan = System.currentTimeMillis(); postscan(directories); long end = System.currentTimeMillis(); Log.d(" prescan time: %dms", prescan - start); Log.d(" scan time: %dms", scan - prescan); Log.d("postscan time: %dms", end - scan); Log.d(" total time: %dms", end - start); } catch (SQLException e) { Log.e("SQLException in MediaScanner.scan()", e); } catch (UnsupportedOperationException e) { Log.e("UnsupportedOperationException in MediaScanner.scan()", e); } catch (RemoteException e) { Log.e("RemoteException in MediaScanner.scan()", e); } } public Uri scanSingleFile(String path, String mimeType) { try { prescan(path); File file = new File(path); long lastModifiedSeconds = file.lastModified() / 1000; return mClient.doScanFile(path, lastModifiedSeconds, file.length(), true); } catch (RemoteException e) { Log.e("RemoteException in MediaScanner.scanFile()", e); return null; } } static { String LIB_ROOT = Vitamio.getLibraryPath(); Log.i("LIB ROOT: %s", LIB_ROOT); System.load(LIB_ROOT + "libstlport_shared.so"); System.load(LIB_ROOT + "libvscanner.so"); loadFFmpeg_native(LIB_ROOT + "libffmpeg.so"); } private native void processDirectory(String path, String extensions); private native boolean processFile(String path, String mimeType); private native final void native_init(MediaScannerClient client); public native void release(); private native final void native_finalize(); @Override protected void finalize() throws Throwable { try { native_finalize(); } finally { super.finalize(); } } private static class FileCacheEntry { Uri mTableUri; long mRowId; String mPath; long mLastModified; boolean mLastModifiedChanged; boolean mSeenInFileSystem; FileCacheEntry(Uri tableUri, long rowId, String path, long lastModified) { mTableUri = tableUri; mRowId = rowId; mPath = path; mLastModified = lastModified; mSeenInFileSystem = false; mLastModifiedChanged = false; } @Override public String toString() { return mPath; } } private class MyMediaScannerClient implements MediaScannerClient { private String mMimeType; private int mFileType; private String mPath; private long mLastModified; private long mFileSize; private String mTitle; private String mArtist; private String mAlbum; private String mLanguage; private long mDuration; private int mWidth; private int mHeight; public FileCacheEntry beginFile(String path, long lastModified, long fileSize) { int lastSlash = path.lastIndexOf('/'); if (lastSlash >= 0 && lastSlash + 2 < path.length()) { if (path.regionMatches(lastSlash + 1, "._", 0, 2)) return null; if (path.regionMatches(true, path.length() - 4, ".jpg", 0, 4)) { if (path.regionMatches(true, lastSlash + 1, "AlbumArt_{", 0, 10) || path.regionMatches(true, lastSlash + 1, "AlbumArt.", 0, 9)) { return null; } int length = path.length() - lastSlash - 1; if ((length == 17 && path.regionMatches(true, lastSlash + 1, "AlbumArtSmall", 0, 13)) || (length == 10 && path.regionMatches(true, lastSlash + 1, "Folder", 0, 6))) { return null; } } } MediaFile.MediaFileType mediaFileType = MediaFile.getFileType(path); if (mediaFileType != null) { mFileType = mediaFileType.fileType; mMimeType = mediaFileType.mimeType; } String key = FileUtils.getCanonical(new File(path)); if (mCaseInsensitivePaths) key = path.toLowerCase(); FileCacheEntry entry = mFileCache.get(key); if (entry == null) { entry = new FileCacheEntry(null, 0, path, 0); mFileCache.put(key, entry); } entry.mSeenInFileSystem = true; long delta = lastModified - entry.mLastModified; if (delta > 1 || delta < -1) { entry.mLastModified = lastModified; entry.mLastModifiedChanged = true; } mPath = path; mLastModified = lastModified; mFileSize = fileSize; mTitle = null; mDuration = 0; return entry; } public void scanFile(String path, long lastModified, long fileSize) { Log.i("scanFile: %s", path); doScanFile(path, lastModified, fileSize, false); } public Uri doScanFile(String path, long lastModified, long fileSize, boolean scanAlways) { Uri result = null; try { FileCacheEntry entry = beginFile(path, lastModified, fileSize); if (entry != null && (entry.mLastModifiedChanged || scanAlways)) { if (processFile(path, null)) { result = endFile(entry); } else { if (mCaseInsensitivePaths) mFileCache.remove(path.toLowerCase()); else mFileCache.remove(path); } } } catch (RemoteException e) { Log.e("RemoteException in MediaScanner.scanFile()", e); } return result; } private int parseSubstring(String s, int start, int defaultValue) { int length = s.length(); if (start == length) return defaultValue; char ch = s.charAt(start++); if (ch < '0' || ch > '9') return defaultValue; int result = ch - '0'; while (start < length) { ch = s.charAt(start++); if (ch < '0' || ch > '9') return result; result = result * 10 + (ch - '0'); } return result; } public void handleStringTag(String name, byte[] valueBytes, String valueEncoding) { String value; try { value = new String(valueBytes, valueEncoding); } catch (Exception e) { Log.e("handleStringTag", e); value = new String(valueBytes); } Log.i("%s : %s", name, value); if (name.equalsIgnoreCase("title")) { mTitle = value; } else if (name.equalsIgnoreCase("artist")) { mArtist = value.trim(); } else if (name.equalsIgnoreCase("albumartist")) { if (TextUtils.isEmpty(mArtist)) mArtist = value.trim(); } else if (name.equalsIgnoreCase("album")) { mAlbum = value.trim(); } else if (name.equalsIgnoreCase("language")) { mLanguage = value.trim(); } else if (name.equalsIgnoreCase("duration")) { mDuration = parseSubstring(value, 0, 0); } else if (name.equalsIgnoreCase("width")) { mWidth = parseSubstring(value, 0, 0); } else if (name.equalsIgnoreCase("height")) { mHeight = parseSubstring(value, 0, 0); } } public void setMimeType(String mimeType) { Log.i("setMimeType: %s", mimeType); mMimeType = mimeType; mFileType = MediaFile.getFileTypeForMimeType(mimeType); } private ContentValues toValues() { ContentValues map = new ContentValues(); map.put(MediaStore.MediaColumns.DATA, mPath); map.put(MediaStore.MediaColumns.DATE_MODIFIED, mLastModified); map.put(MediaStore.MediaColumns.SIZE, mFileSize); map.put(MediaStore.MediaColumns.MIME_TYPE, mMimeType); map.put(MediaStore.MediaColumns.TITLE, mTitle); if (MediaFile.isVideoFileType(mFileType)) { map.put(Video.Media.DURATION, mDuration); map.put(Video.Media.LANGUAGE, mLanguage); map.put(Video.Media.ALBUM, mAlbum); map.put(Video.Media.ARTIST, mArtist); map.put(Video.Media.WIDTH, mWidth); map.put(Video.Media.HEIGHT, mHeight); } return map; } private Uri endFile(FileCacheEntry entry) throws RemoteException { Uri tableUri; boolean isVideo = MediaFile.isVideoFileType(mFileType) && mWidth > 0 && mHeight > 0; if (isVideo) { tableUri = Video.Media.CONTENT_URI; } else { return null; } entry.mTableUri = tableUri; ContentValues values = toValues(); String title = values.getAsString(MediaStore.MediaColumns.TITLE); if (TextUtils.isEmpty(title)) { title = values.getAsString(MediaStore.MediaColumns.DATA); int lastSlash = title.lastIndexOf('/'); if (lastSlash >= 0) { lastSlash++; if (lastSlash < title.length()) title = title.substring(lastSlash); } int lastDot = title.lastIndexOf('.'); if (lastDot > 0) title = title.substring(0, lastDot); values.put(MediaStore.MediaColumns.TITLE, title); } long rowId = entry.mRowId; Uri result = null; if (rowId == 0) { result = mProvider.insert(tableUri, values); if (result != null) { rowId = ContentUris.parseId(result); entry.mRowId = rowId; } } else { result = ContentUris.withAppendedId(tableUri, rowId); mProvider.update(result, values, null, null); } return result; } public void addNoMediaFolder(String path) { ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DATA, ""); String[] pathSpec = new String[]{path + '%'}; try { mProvider.update(Video.Media.CONTENT_URI, values, MediaStore.MediaColumns.DATA + " LIKE ?", pathSpec); } catch (RemoteException e) { throw new RuntimeException(); } } } }