/*****************************************************************************
* 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 java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.Thread.State;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Stack;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.LibVlcException;
import org.videolan.libvlc.Media;
import org.videolan.vlc.gui.MainActivity;
import org.videolan.vlc.gui.audio.AudioBrowserFragment;
import org.videolan.vlc.gui.video.VideoGridFragment;
import android.content.Context;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
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<Media> mItemList;
private final ArrayList<Handler> mUpdateHandler;
private final ReadWriteLock mItemListLock;
private boolean isStopping = false;
private boolean mRestart = false;
private Context mRestartContext;
protected Thread mLoadingThread;
private MediaLibrary(Context context) {
mInstance = this;
mItemList = new ArrayList<Media>();
mUpdateHandler = new ArrayList<Handler>();
mItemListLock = new ReentrantReadWriteLock();
}
public void loadMediaItems(Context context, boolean restart) {
if (restart && isWorking()) {
/* do a clean restart if a scan is ongoing */
mRestart = true;
isStopping = true;
mRestartContext = context;
} else {
loadMediaItems(context);
}
}
public void loadMediaItems(Context context) {
if (mLoadingThread == null || mLoadingThread.getState() == State.TERMINATED) {
isStopping = false;
VideoGridFragment.actionScanStart(context.getApplicationContext());
mLoadingThread = new Thread(new GetMediaItemsRunnable(context.getApplicationContext()));
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 static MediaLibrary getInstance(Context context) {
if (mInstance == null)
mInstance = new MediaLibrary(context);
return mInstance;
}
public void addUpdateHandler(Handler handler) {
mUpdateHandler.add(handler);
}
public void removeUpdateHandler(Handler handler) {
mUpdateHandler.remove(handler);
}
public ArrayList<Media> getVideoItems() {
ArrayList<Media> videoItems = new ArrayList<Media>();
mItemListLock.readLock().lock();
for (int i = 0; i < mItemList.size(); i++) {
Media item = mItemList.get(i);
if (item != null && item.getType() == Media.TYPE_VIDEO) {
videoItems.add(item);
}
}
mItemListLock.readLock().unlock();
return videoItems;
}
public ArrayList<Media> getAudioItems() {
ArrayList<Media> audioItems = new ArrayList<Media>();
mItemListLock.readLock().lock();
for (int i = 0; i < mItemList.size(); i++) {
Media item = mItemList.get(i);
if (item.getType() == Media.TYPE_AUDIO) {
audioItems.add(item);
}
}
mItemListLock.readLock().unlock();
return audioItems;
}
public ArrayList<Media> getAudioItems(String name, String name2, int mode) {
ArrayList<Media> audioItems = new ArrayList<Media>();
mItemListLock.readLock().lock();
for (int i = 0; i < mItemList.size(); i++) {
Media item = mItemList.get(i);
if (item.getType() == Media.TYPE_AUDIO) {
boolean valid = false;
switch (mode) {
case AudioBrowserFragment.MODE_ARTIST:
valid = name.equals(item.getArtist()) && (name2 == null || name2.equals(item.getAlbum()));
break;
case AudioBrowserFragment.MODE_ALBUM:
valid = name.equals(item.getAlbum());
break;
case AudioBrowserFragment.MODE_GENRE:
valid = name.equals(item.getGenre()) && (name2 == null || name2.equals(item.getAlbum()));
break;
default:
break;
}
if (valid)
audioItems.add(item);
}
}
mItemListLock.readLock().unlock();
return audioItems;
}
public ArrayList<Media> getMediaItems() {
return mItemList;
}
public Media getMediaItem(String location) {
mItemListLock.readLock().lock();
for (int i = 0; i < mItemList.size(); i++) {
Media item = mItemList.get(i);
if (item.getLocation().equals(location)) {
mItemListLock.readLock().unlock();
return item;
}
}
mItemListLock.readLock().unlock();
return null;
}
public ArrayList<Media> getMediaItems(List<String> pathList) {
ArrayList<Media> items = new ArrayList<Media>();
for (int i = 0; i < pathList.size(); i++) {
Media 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>();
private Context mContext;
public GetMediaItemsRunnable(Context context) {
mContext = context;
}
@Override
public void run() {
LibVLC libVlcInstance;
try {
libVlcInstance = Util.getLibVlcInstance();
} catch (LibVlcException e1) {
Log.e(TAG, "ERROR: LibVLCException while trying to get instance");
return;
}
// Initialize variables
final MediaDatabase DBManager = MediaDatabase.getInstance(VLCApplication.getAppContext());
// show progressbar in footer
MainActivity.showProgressBar(mContext);
List<File> mediaDirs = DBManager.getMediaDirs();
if (mediaDirs.size() == 0) {
// Use all available storage directories as our default
String storageDirs[] = Util.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, Media> existingMedias = DBManager.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;
ArrayList<File> mediaToScan = new ArrayList<File>();
try {
// Count total files, and stack them
while (!directories.isEmpty()) {
File dir = directories.pop();
String dirPath = dir.getAbsolutePath();
File[] f = null;
// 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()) {
continue;
}
// Filter the extensions and the folders
try {
if ((f = dir.listFiles(mediaFileFilter)) != null) {
for (File file : f) {
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;
}
}
// Process the stacked items
for (File file : mediaToScan) {
String fileURI = LibVLC.PathToURI(file.getPath());
MainActivity.sendTextInfo(mContext, 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
Media m = new Media(libVlcInstance, fileURI);
mItemList.add(m);
// Add this item to database
MediaDatabase db = MediaDatabase.getInstance(VLCApplication.getAppContext());
db.addMedia(m);
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);
}
DBManager.removeMedias(existingMedias.keySet());
for (File file : DBManager.getMediaDirs())
if (!file.isDirectory())
DBManager.removeDir(file.getAbsolutePath());
}
// hide progressbar in footer
MainActivity.clearTextInfo(mContext);
MainActivity.hideProgressBar(mContext);
VideoGridFragment.actionScanStop(mContext);
if (mRestart) {
Log.d(TAG, "Restarting scan");
mRestart = false;
restartHandler.sendEmptyMessageDelayed(1, 200);
} else {
mRestartContext = null;
mContext = null;
}
}
}
};
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;
if (owner.mRestartContext != null)
owner.loadMediaItems(owner.mRestartContext);
else
Log.e(TAG, "Context lost in a black hole");
}
}
/**
* Filters all irrelevant files
*/
private class MediaItemFilter implements FileFilter {
@Override
public boolean accept(File f) {
boolean accepted = false;
if (!f.isHidden()) {
if (f.isDirectory() && !Media.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 = Media.AUDIO_EXTENSIONS.contains(fileExt) ||
Media.VIDEO_EXTENSIONS.contains(fileExt);
}
}
}
return accepted;
}
}
}