/*
* Copyright (C) 2017 Team Gateship-One
* (Hendrik Borghorst & Frederik Luetkes)
*
* The AUTHORS.md file contains a detailed contributors list:
* <https://github.com/gateship-one/odyssey/blob/master/AUTHORS.md>
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*
*/
package org.gateshipone.odyssey.utils;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v4.content.ContextCompat;
import org.gateshipone.odyssey.models.FileModel;
import org.gateshipone.odyssey.models.TrackModel;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
public class FileExplorerHelper {
private static FileExplorerHelper mInstance = null;
private HashMap<String, TrackModel> mTrackHash;
private FileExplorerHelper() {
mTrackHash = new HashMap<>();
}
public static synchronized FileExplorerHelper getInstance() {
if (mInstance == null) {
mInstance = new FileExplorerHelper();
}
return mInstance;
}
/**
* return the list of available storage volumes
*/
public List<String> getStorageVolumes(Context context) {
// create storage volume list
ArrayList<String> storagePathList = new ArrayList<>();
File[] storageList = ContextCompat.getExternalFilesDirs(context, null);
for (File storageFile : storageList) {
if (null != storageFile) {
storagePathList.add(storageFile.getAbsolutePath().replaceAll("/Android/data/" + context.getPackageName() + "/files", ""));
}
}
storagePathList.add("/");
return storagePathList;
}
/**
* create a TrackModel for the given File
* if no entry in the mediadb is found a dummy TrackModel will be created
*/
public TrackModel getTrackModelForFile(Context context, FileModel file) {
TrackModel track;
String urlString = file.getURLString();
// use pre built hash to lookup the file
track = mTrackHash.get(urlString);
if (null == track) {
// lookup the current file in the media db
String whereVal[] = {urlString};
String where = MediaStore.Audio.Media.DATA + "=?";
Cursor cursor = PermissionHelper.query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MusicLibraryHelper.projectionTracks, where, whereVal, MediaStore.Audio.Media.TRACK);
if (cursor != null) {
if (cursor.moveToFirst()) {
String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
int no = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.TRACK));
String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
String url = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
String albumKey = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_KEY));
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID));
track = new TrackModel(title, artist, album, albumKey, duration, no, url, id);
}
cursor.close();
}
}
if (track == null) {
// no entry in the media db was found so create a custom track
try {
// try to read the file metadata
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(context, Uri.parse(FormatHelper.encodeFileURI(urlString)));
String title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
if (title == null) {
title = file.getName();
}
String durationString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
long duration = 0;
if (durationString != null) {
try {
duration = Long.valueOf(durationString);
} catch (NumberFormatException e) {
duration = 0;
}
}
String noString = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER);
int no = -1;
if (noString != null) {
try {
if (noString.contains("/")) {
// if string has the format (trackNumber / numberOfTracks)
String[] components = noString.split("/");
if (components.length > 0) {
no = Integer.valueOf(components[0]);
}
} else {
no = Integer.valueOf(noString);
}
} catch (NumberFormatException e) {
no = -1;
}
}
String artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
String album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM);
String albumKey = "" + ((artist == null ? "" : artist) + (album == null ? "" : album)).hashCode();
track = new TrackModel(title, artist, album, albumKey, duration, no, urlString, -1);
} catch (Exception e) {
String albumKey = "" + file.getName().hashCode();
track = new TrackModel(file.getName(), "", "", albumKey, 0, -1, urlString, -1);
}
}
return track;
}
/**
* return a list of TrackModels created for the given folder
* this excludes all subfolders
*/
public List<TrackModel> getTrackModelsForFolder(Context context, FileModel folder) {
List<TrackModel> tracks = new ArrayList<>();
// get all tracks from the mediadb related to the current folder and store the tracks in a hashmap
mTrackHash.clear();
String urlString = folder.getURLString();
String whereVal[] = {urlString + "%"};
String where = MediaStore.Audio.Media.DATA + " LIKE ?";
Cursor cursor = PermissionHelper.query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MusicLibraryHelper.projectionTracks, where, whereVal, MediaStore.Audio.Media.TRACK);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
int no = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.TRACK));
String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
String url = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
String albumKey = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_KEY));
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID));
TrackModel track = new TrackModel(title, artist, album, albumKey, duration, no, url, id);
mTrackHash.put(url, track);
} while (cursor.moveToNext());
}
cursor.close();
}
List<FileModel> files = PermissionHelper.getFilesForDirectory(context, folder);
for (FileModel file : files) {
if (file.isFile()) {
// file is not a directory so create a trackmodel for the file
tracks.add(getTrackModelForFile(context, file));
}
}
// clear the hash
mTrackHash.clear();
return tracks;
}
/**
* Generates a list of {@link FileModel} objects that are either in the Android DB and not on the FS
* or that are on the FS but not in the Android DB.
*
* @param context Context used for DB query
* @param basePath Path of files to check
* @return List of files that need to be scanned
*/
public List<FileModel> getMissingDBFiles(Context context, FileModel basePath) {
List<FileModel> filesFS = new ArrayList<>();
List<FileModel> filesDB = new ArrayList<>();
String whereVal[] = {basePath.getPath() + "%"};
String where = MediaStore.Audio.Media.DATA + " LIKE ?";
Cursor cursor = PermissionHelper.query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MusicLibraryHelper.projectionTracks, where, whereVal, MediaStore.Audio.Media.TRACK);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String url = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
filesDB.add(new FileModel(url));
} while (cursor.moveToNext());
}
cursor.close();
}
getFilesRecursive(context, basePath, filesFS);
return generateFileListDiff(filesDB, filesFS);
}
/**
* Helper method to create a list of {@link FileModel} objects that are part of one list but not
* of the other.
*
* @param list1 First list of {@link FileModel} objects.
* @param list2 Second list of {@link FileModel} objects.
* @return List of {@link FileModel} that are only part of one of the two given lists.
*/
private List<FileModel> generateFileListDiff(List<FileModel> list1, List<FileModel> list2) {
List<FileModel> filesDiff = new ArrayList<>();
// Sort lists so that an easy compare is possible because of given order.
Collections.sort(list1);
Collections.sort(list2);
// Create the difference between the to lists
ListIterator<FileModel> dbIterartor = list1.listIterator();
ListIterator<FileModel> fsIterartor = list2.listIterator();
// Get the first list elements (if any available)
FileModel list1Model = null;
if (dbIterartor.hasNext()) {
list1Model = dbIterartor.next();
}
FileModel list2Model = null;
if (fsIterartor.hasNext()) {
list2Model = fsIterartor.next();
}
while (dbIterartor.hasNext() || fsIterartor.hasNext()) {
int compareVal = 0;
// Check if files are available and compare them if both are available
if (list1Model != null && list2Model != null) {
compareVal = list1Model.compareTo(list2Model);
} else if (list1Model != null) {
// No model from the list2 available, make sure remaining list1 models are added
compareVal = -1;
} else if (list2Model != null) {
// No model from the list1 available, make sure remaining list2 models are added
compareVal = 1;
}
if (compareVal == 0) {
// Both are equal, move to next elements on both lists
if (dbIterartor.hasNext()) {
list1Model = dbIterartor.next();
} else {
list1Model = null;
}
if (fsIterartor.hasNext()) {
list2Model = fsIterartor.next();
} else {
list2Model = null;
}
} else if (compareVal < 0) {
// list1Model is less, move it forward and add current file
filesDiff.add(list1Model);
if (dbIterartor.hasNext()) {
list1Model = dbIterartor.next();
} else {
list1Model = null;
}
} else {
// list2Model is less, move it forward and add current file
filesDiff.add(list2Model);
if (fsIterartor.hasNext()) {
list2Model = fsIterartor.next();
} else {
list2Model = null;
}
}
}
return filesDiff;
}
/**
* Helper method to create a list of of {@link FileModel} objects that are files for the given folder.
* This method will be called recursively for all subfolders.
*
* @param context The current android context.
* @param folder The current folder.
* @param files List of {@link FileModel} objects that are files and in the folder.
*/
private void getFilesRecursive(Context context, FileModel folder, List<FileModel> files) {
if (folder.isFile()) {
// file is not a directory so add the filemodel to the list
files.add(folder);
} else {
List<FileModel> filesTmp = PermissionHelper.getFilesForDirectory(context, folder);
for (FileModel file : filesTmp) {
// call method for all files found in this folder
getFilesRecursive(context, file, files);
}
}
}
/**
* return a list of TrackModels created for the given folder
* this includes all subfolders
*/
public List<TrackModel> getTrackModelsForFolderAndSubFolders(Context context, FileModel folder) {
List<TrackModel> tracks = new ArrayList<>();
// get all tracks from the mediadb related to the current folder and store the tracks in a hashmap
mTrackHash.clear();
String urlString = folder.getURLString();
String whereVal[] = {urlString + "%"};
String where = MediaStore.Audio.Media.DATA + " LIKE ?";
Cursor cursor = PermissionHelper.query(context, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MusicLibraryHelper.projectionTracks, where, whereVal, MediaStore.Audio.Media.TRACK);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
int no = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.TRACK));
String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
String url = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
String albumKey = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_KEY));
long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID));
TrackModel track = new TrackModel(title, artist, album, albumKey, duration, no, url, id);
mTrackHash.put(url, track);
} while (cursor.moveToNext());
}
cursor.close();
}
// check current folder and subfolders for music files
getTrackModelsForFolderAndSubFolders(context, folder, tracks);
// clear the hash
mTrackHash.clear();
return tracks;
}
/**
* add TrackModel objects for the current folder and all subfolders to the tracklist
*/
private void getTrackModelsForFolderAndSubFolders(Context context, FileModel folder, List<TrackModel> tracks) {
if (folder.isFile()) {
// file is not a directory so create a trackmodel for the file
tracks.add(getTrackModelForFile(context, folder));
} else {
List<FileModel> files = PermissionHelper.getFilesForDirectory(context, folder);
for (FileModel file : files) {
// call method for all files found in this folder
getTrackModelsForFolderAndSubFolders(context, file, tracks);
}
}
}
}