/***************************************************************************** * MediaLibrary.java ***************************************************************************** * Copyright © 2011-2012 VLC authors and VideoLAN * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA. *****************************************************************************/ package org.videolan.vlc; import android.net.Uri; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.text.TextUtils; import android.util.Log; import org.videolan.libvlc.LibVLC; import org.videolan.libvlc.Media; import org.videolan.libvlc.util.AndroidUtil; import org.videolan.libvlc.util.Extensions; import org.videolan.vlc.gui.audio.AudioBrowserListAdapter; import org.videolan.vlc.interfaces.IBrowser; import org.videolan.vlc.util.AndroidDevices; import org.videolan.vlc.util.Util; import org.videolan.vlc.util.VLCInstance; import org.videolan.vlc.util.WeakHandler; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.lang.Thread.State; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Stack; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class MediaLibrary { public final static String TAG = "VLC/MediaLibrary"; public static final int MEDIA_ITEMS_UPDATED = 100; private static MediaLibrary mInstance; private final ArrayList<MediaWrapper> mItemList; private final ArrayList<Handler> mUpdateHandler; private final ReadWriteLock mItemListLock; private boolean isStopping = false; private boolean mRestart = false; protected Thread mLoadingThread; private WeakReference<IBrowser> mBrowser = null; public final static HashSet<String> FOLDER_BLACKLIST; static { final String[] folder_blacklist = { "/alarms", "/notifications", "/ringtones", "/media/alarms", "/media/notifications", "/media/ringtones", "/media/audio/alarms", "/media/audio/notifications", "/media/audio/ringtones", "/Android/data/" }; FOLDER_BLACKLIST = new HashSet<String>(); for (String item : folder_blacklist) FOLDER_BLACKLIST.add(AndroidDevices.EXTERNAL_PUBLIC_DIRECTORY + item); } private MediaLibrary() { mInstance = this; mItemList = new ArrayList<MediaWrapper>(); mUpdateHandler = new ArrayList<Handler>(); mItemListLock = new ReentrantReadWriteLock(); } public void loadMediaItems(boolean restart) { if (restart && isWorking()) { /* do a clean restart if a scan is ongoing */ mRestart = true; isStopping = true; } else { loadMediaItems(); } } public void loadMediaItems() { if (mLoadingThread == null || mLoadingThread.getState() == State.TERMINATED) { isStopping = false; Util.actionScanStart(); // 开启线程进行加载》GetMediaItemsRunnable() mLoadingThread = new Thread(new GetMediaItemsRunnable()); mLoadingThread.start(); } } public void stop() { isStopping = true; } public boolean isWorking() { if (mLoadingThread != null && mLoadingThread.isAlive() && mLoadingThread.getState() != State.TERMINATED && mLoadingThread.getState() != State.NEW) return true; return false; } public synchronized static MediaLibrary getInstance() { if (mInstance == null) mInstance = new MediaLibrary(); return mInstance; } public void addUpdateHandler(Handler handler) { mUpdateHandler.add(handler); } public void removeUpdateHandler(Handler handler) { mUpdateHandler.remove(handler); } public ArrayList<MediaWrapper> searchMedia(String query){ ArrayList<MediaWrapper> mediaList = new ArrayList<MediaWrapper>(); ArrayList<String> pathList = MediaDatabase.getInstance().searchMedia(query); if (!pathList.isEmpty()){ for (String path : pathList) { mediaList.add(getMediaItem(path)); } } return mediaList; } public ArrayList<MediaWrapper> getVideoItems() { ArrayList<MediaWrapper> videoItems = new ArrayList<MediaWrapper>(); mItemListLock.readLock().lock(); for (int i = 0; i < mItemList.size(); i++) { MediaWrapper item = mItemList.get(i); if (item != null && item.getType() == MediaWrapper.TYPE_VIDEO) { videoItems.add(item); } } mItemListLock.readLock().unlock(); return videoItems; } public ArrayList<MediaWrapper> getAudioItems() { ArrayList<MediaWrapper> audioItems = new ArrayList<MediaWrapper>(); mItemListLock.readLock().lock(); for (int i = 0; i < mItemList.size(); i++) { MediaWrapper item = mItemList.get(i); if (item.getType() == MediaWrapper.TYPE_AUDIO) { audioItems.add(item); } } mItemListLock.readLock().unlock(); return audioItems; } public ArrayList<MediaWrapper> getPlaylistFilesItems() { ArrayList<MediaWrapper> playlistItems = new ArrayList<MediaWrapper>(); mItemListLock.readLock().lock(); for (int i = 0; i < mItemList.size(); i++) { MediaWrapper item = mItemList.get(i); if (item.getType() == MediaWrapper.TYPE_PLAYLIST) { playlistItems.add(item); } } mItemListLock.readLock().unlock(); return playlistItems; } public ArrayList<AudioBrowserListAdapter.ListItem> getPlaylistDbItems() { ArrayList<AudioBrowserListAdapter.ListItem> playlistItems = new ArrayList<AudioBrowserListAdapter.ListItem>(); AudioBrowserListAdapter.ListItem playList; MediaDatabase db = MediaDatabase.getInstance(); String[] items, playlistNames = db.getPlaylists(); for (String playlistName : playlistNames){ items = db.playlistGetItems(playlistName); if (items == null) continue; playList = new AudioBrowserListAdapter.ListItem(playlistName, null, null, false); for (String track : items){ playList.mMediaList.add(new MediaWrapper(AndroidUtil.LocationToUri(track))); } playlistItems.add(playList); } return playlistItems; } public ArrayList<MediaWrapper> getMediaItems() { return mItemList; } public MediaWrapper getMediaItem(String location) { mItemListLock.readLock().lock(); for (int i = 0; i < mItemList.size(); i++) { MediaWrapper item = mItemList.get(i); if (item.getLocation().equals(location)) { mItemListLock.readLock().unlock(); return item; } } mItemListLock.readLock().unlock(); return null; } public ArrayList<MediaWrapper> getMediaItems(List<String> pathList) { ArrayList<MediaWrapper> items = new ArrayList<MediaWrapper>(); for (int i = 0; i < pathList.size(); i++) { MediaWrapper item = getMediaItem(pathList.get(i)); items.add(item); } return items; } private class GetMediaItemsRunnable implements Runnable { private final Stack<File> directories = new Stack<File>(); private final HashSet<String> directoriesScanned = new HashSet<String>(); public GetMediaItemsRunnable() { } @Override public void run() { LibVLC libVlcInstance = VLCInstance.get(); // Initialize variables final MediaDatabase mediaDatabase = MediaDatabase.getInstance(); // show progressbar in footer if (mBrowser != null && mBrowser.get() != null) mBrowser.get().showProgressBar(); List<File> mediaDirs = mediaDatabase.getMediaDirs(); if (mediaDirs.size() == 0) { // Use all available storage directories as our default String storageDirs[] = AndroidDevices.getMediaDirectories(); for (String dir: storageDirs) { File f = new File(dir); if (f.exists()) mediaDirs.add(f); } } directories.addAll(mediaDirs); // get all existing media items HashMap<String, MediaWrapper> existingMedias = mediaDatabase.getMedias(); // list of all added files HashSet<String> addedLocations = new HashSet<String>(); // clear all old items mItemListLock.writeLock().lock(); mItemList.clear(); mItemListLock.writeLock().unlock(); MediaItemFilter mediaFileFilter = new MediaItemFilter(); int count = 0; LinkedList<File> mediaToScan = new LinkedList<File>(); try { LinkedList<String> dirsToIgnore = new LinkedList<String>(); // Count total files, and stack them while (!directories.isEmpty()) { File dir = directories.pop(); String dirPath = dir.getAbsolutePath(); // Skip some system folders if (dirPath.startsWith("/proc/") || dirPath.startsWith("/sys/") || dirPath.startsWith("/dev/")) continue; // Do not scan again if same canonical path try { dirPath = dir.getCanonicalPath(); } catch (IOException e) { e.printStackTrace(); } if (directoriesScanned.contains(dirPath)) continue; else directoriesScanned.add(dirPath); // Do no scan media in .nomedia folders if (new File(dirPath + "/.nomedia").exists()) { dirsToIgnore.add("file://"+dirPath); continue; } // Filter the extensions and the folders try { String[] files = dir.list(); if (files != null) { for (String fileName : files) { File file = new File(dirPath, fileName); if (mediaFileFilter.accept(file)){ if (file.isFile()) mediaToScan.add(file); else if (file.isDirectory()) directories.push(file); } } } } catch (Exception e){ // listFiles can fail in OutOfMemoryError, go to the next folder continue; } if (isStopping) { Log.d(TAG, "Stopping scan"); return; } } //Remove ignored files HashSet<Uri> mediasToRemove = new HashSet<Uri>(); String path; outloop: for (Map.Entry<String, MediaWrapper> entry : existingMedias.entrySet()){ path = entry.getKey(); for (String dirPath : dirsToIgnore) { if (path.startsWith(dirPath)) { mediasToRemove.add(entry.getValue().getUri()); mItemList.remove(existingMedias.get(path)); continue outloop; } } } mediaDatabase.removeMedias(mediasToRemove); // Process the stacked items for (File file : mediaToScan) { String fileURI = AndroidUtil.FileToUri(file).toString(); if (mBrowser != null && mBrowser.get() != null) mBrowser.get().sendTextInfo(file.getName(), count, mediaToScan.size()); count++; if (existingMedias.containsKey(fileURI)) { /** * only add file if it is not already in the list. eg. if * user select an subfolder as well */ if (!addedLocations.contains(fileURI)) { mItemListLock.writeLock().lock(); // get existing media item from database mItemList.add(existingMedias.get(fileURI)); mItemListLock.writeLock().unlock(); addedLocations.add(fileURI); } } else { mItemListLock.writeLock().lock(); // create new media item final Media media = new Media(libVlcInstance, Uri.parse(fileURI)); media.parse(); /* skip files with .mod extension and no duration */ if ((media.getDuration() == 0 || (media.getTrackCount() != 0 && TextUtils.isEmpty(media.getTrack(0).codec))) && fileURI.endsWith(".mod")) { mItemListLock.writeLock().unlock(); media.release(); continue; } MediaWrapper mw = new MediaWrapper(media); media.release(); mw.setLastModified(file.lastModified()); mItemList.add(mw); // Add this item to database mediaDatabase.addMedia(mw); mItemListLock.writeLock().unlock(); } if (isStopping) { Log.d(TAG, "Stopping scan"); return; } } } finally { // update the video and audio activities for (int i = 0; i < mUpdateHandler.size(); i++) { Handler h = mUpdateHandler.get(i); h.sendEmptyMessage(MEDIA_ITEMS_UPDATED); } // remove old files & folders from database if storage is mounted if (!isStopping && Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { for (String fileURI : addedLocations) { existingMedias.remove(fileURI); } mediaDatabase.removeMediaWrappers(existingMedias.values()); /* * In case of file matching path of a folder from another removable storage */ for (File file : mediaDatabase.getMediaDirs()) if (!file.isDirectory()) mediaDatabase.removeDir(file.getAbsolutePath()); } // hide progressbar in footer if (mBrowser != null && mBrowser.get() != null) { mBrowser.get().clearTextInfo(); mBrowser.get().hideProgressBar(); } Util.actionScanStop(); if (mRestart) { Log.d(TAG, "Restarting scan"); mRestart = false; restartHandler.sendEmptyMessageDelayed(1, 200); } } } } private Handler restartHandler = new RestartHandler(this); private static class RestartHandler extends WeakHandler<MediaLibrary> { public RestartHandler(MediaLibrary owner) { super(owner); } @Override public void handleMessage(Message msg) { MediaLibrary owner = getOwner(); if(owner == null) return; owner.loadMediaItems(); } } /** * Filters all irrelevant files */ private static class MediaItemFilter implements FileFilter { @Override public boolean accept(File f) { boolean accepted = false; if (!f.isHidden()) { if (f.isDirectory() && !FOLDER_BLACKLIST.contains(f.getPath().toLowerCase(Locale.ENGLISH))) { accepted = true; } else { String fileName = f.getName().toLowerCase(Locale.ENGLISH); int dotIndex = fileName.lastIndexOf("."); if (dotIndex != -1) { String fileExt = fileName.substring(dotIndex); accepted = Extensions.AUDIO.contains(fileExt) || Extensions.VIDEO.contains(fileExt) || Extensions.PLAYLIST.contains(fileExt); } } } return accepted; } } public void setBrowser(IBrowser browser) { if (browser != null) mBrowser = new WeakReference<IBrowser>(browser); else mBrowser.clear(); } }