package com.simplecity.amp_library.utils; import android.annotation.SuppressLint; import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import android.os.Environment; import android.support.annotation.WorkerThread; import android.text.TextUtils; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import com.simplecity.amp_library.model.BaseFileObject; import com.simplecity.amp_library.model.FileObject; import com.simplecity.amp_library.model.Song; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import rx.Observable; import rx.schedulers.Schedulers; public class FileHelper { private final static String TAG = "FileHelper"; /** * The root directory */ public static final String ROOT_DIRECTORY = "/"; /** * The parent directory */ public static final String PARENT_DIRECTORY = ".."; /** * The current directory */ public static final String CURRENT_DIRECTORY = "."; /** * Method that check if a file is a symbolic link. * * @param file File to check * @return boolean If file is a symbolic link * @throws IOException If real file couldn't be checked */ public static boolean isSymlink(File file) throws IOException { if (file == null) { return false; } String absPath = file.getAbsolutePath(); String canonPath = file.getCanonicalPath(); return !(TextUtils.isEmpty(absPath) || TextUtils.isEmpty(canonPath)) && absPath.compareTo(canonPath) != 0; } /** * Method that resolves a symbolic link to the real file or directory. * * @param file File to check * @return File The real file or directory * @throws IOException If real file couldn't be resolved */ public static File resolveSymlink(File file) throws IOException { return file.getCanonicalFile(); } /** * Returns the name of a string, excluding the extension * * @param name the name (path) of the file * @return the name of the file, excluding the extension */ public static String getName(String name) { String ext = getExtension(name); if (ext == null) { return name; } return name.substring(0, name.length() - ext.length() - 1); } /** * Returns the extension of the file * * @param name the File to retrieve the extension from * @return String the extension of the file */ public static String getExtension(String name) { final char dot = '.'; int pos = name.lastIndexOf(dot); if (pos == -1 || pos == 0) { // Hidden files don't have extensions return null; } return name.substring(pos + 1); } /** * Returns true if the folder is the root directory * * @param folder The folder to check * @return true if the folder is the root directory */ public static boolean isRootDirectory(File folder) { return folder.getPath().compareTo(FileHelper.ROOT_DIRECTORY) == 0; } /** * Returns true if this OldFileObject can has read & write access * * @param file the File to check * @return boolean true if this OldFileObject can has read & write access */ public static boolean canReadWrite(File file) { return file.canRead() && file.canWrite(); } /** * Resolves the /storage/emulated/legacy paths to * their true folder path representations. Required * for Nexii and other devices with no SD card. * * @return The true, resolved file path to the input path. */ @SuppressLint("SdCardPath") public static String getPath(File file) { if (file == null) { return null; } String filePath = file.getAbsolutePath(); try { if (isSymlink(file)) { file = resolveSymlink(file); filePath = file.getAbsolutePath(); } } catch (IOException ignored) { } if (!TextUtils.isEmpty(filePath) && filePath.equals("/storage/emulated/0") || filePath.equals("/storage/emulated/0/") || filePath.equals("/storage/emulated/legacy") || filePath.equals("/storage/emulated/legacy/") || filePath.equals("/storage/sdcard0") || filePath.equals("/storage/sdcard0/") || filePath.equals("/sdcard") || filePath.equals("/sdcard/") || filePath.equals("/mnt/sdcard") || filePath.equals("/mnt/sdcard/")) { filePath = Environment.getExternalStorageDirectory().toString(); } return filePath; } /** * Gets a formatted, human readable file size String * * @param size long, the size of the file in bytes * @return String a formatted, human readable file size */ public static String getHumanReadableSize(long size) { if (size <= 0) { return "0"; } final String[] units = new String[]{"B", "KB", "MB", "GB", "TB"}; int digitGroups = (int) (Math.log10(size) / Math.log10(1024)); return new DecimalFormat("#,##0.#").format(size / Math.pow(1024, digitGroups)) + " " + units[digitGroups]; } public static long getDuration(Context context, BaseFileObject baseFileObject) { int duration = 0; if (baseFileObject != null && !TextUtils.isEmpty(baseFileObject.path)) { Uri uri = Uri.parse(baseFileObject.path); if (uri != null) { MediaPlayer mediaPlayer = MediaPlayer.create(context, uri); if (mediaPlayer != null) { duration = mediaPlayer.getDuration(); mediaPlayer.reset(); mediaPlayer.release(); } } } return duration; } /** * Recursively collects all the song id's for the given directory and * all of its sub-directories. Must be called Asynchronously. * * @param file the File to retrieve the song Id's from * @param recursive whether to recursively check the sub-directories for song Id's * @return long[] a list of the songId's for the given fileObject's directory & sub-directories */ public static Observable<List<Song>> getSongList(final File file, final boolean recursive, final boolean inSameDir) { return Observable.fromCallable(() -> walk(file, new ArrayList<>(), recursive, inSameDir)).flatMap(filePaths -> DataManager.getInstance().getSongsRelay() .map(songs -> Stream.of(songs) .filter(song -> filePaths.contains(song.path)) .collect(Collectors.toList())) .subscribeOn(Schedulers.io())); } /** * Gets the song for a given file * * @param file the file to retrieve the song Id's from * @return long[] a list of the songId's for the given fileObject's directory & sub-directories */ public static Observable<Song> getSong(File file) { return DataManager.getInstance().getSongsRelay() .map(songs -> Stream.of(songs) .filter(song -> song.path.contains(FileHelper.getPath(file))) .findFirst() .orElse(null)); } /** * Recursively 'walks' the files subdirectories, gathering a list of paths. * * @param root the root file to walk * @param paths the paths will be added to this List * @param recursive whether to recursively walk subdirectories * @param inSameDir whether files in the same dir as root should be included * @return a List of paths */ @WorkerThread private static List<String> walk(File root, final List<String> paths, final boolean recursive, final boolean inSameDir) { if (inSameDir) { root = root.getParentFile(); } if (!root.isDirectory()) { paths.add(root.getAbsolutePath()); return paths; } File[] list = root.listFiles(getAudioFilter()); if (list != null) { for (File f : list) { if (f.isDirectory()) { if (recursive) { walk(f, paths, true, false); } } else { paths.add(f.getAbsolutePath()); } } } return paths; } /** * Delete a File recursively. * * @param file the File to delete * @return true if the deletion was successful */ public static boolean deleteFile(File file) { return DeleteRecursive(file); } /** * Recursively delete a File * * @param fileOrDirectory the file or directory to delete * @return true id the deletion was successful */ private static boolean DeleteRecursive(File fileOrDirectory) { if (fileOrDirectory == null) { return false; } else if (fileOrDirectory.isDirectory()) { File[] fileList = fileOrDirectory.listFiles(); if (fileList != null) { for (File child : fileList) DeleteRecursive(child); } } return fileOrDirectory.delete(); } /** * Renames an {@link FileObject} to the passed in newName * * @param context Context * @param baseFileObject the FileObject representation of the file to rename * @param newName the new name of the file * @return */ public static boolean renameFile(Context context, BaseFileObject baseFileObject, String newName) { if (newName == null) { return false; } if (baseFileObject instanceof FileObject) { String ext = ((FileObject) baseFileObject).extension; if (ext == null) { ext = ""; } newName = newName + "." + ext; } File file = new File(baseFileObject.path); File newFile = new File(baseFileObject.getParent(), newName); if (file.renameTo(newFile)) { baseFileObject.name = FileHelper.getName(newFile.getName()); CustomMediaScanner.scanFiles(Collections.singletonList(file.getPath()), null); return true; } return false; } /** * An array of accepted/supported audio extensions. */ public static String[] sExtensions = new String[]{ "mp3", "3gp", "mp4", "m4a", "aac", "ts", "flac", "mid", "xmf", "mxmf", "midi", "rtttl", "rtx", "ota", "imy", "ogg", "mkv", "wav" }; /** * An {@link FileFilter} which only accepts directories & supported audio filetypes, based on extension */ public static FileFilter getAudioFilter() { return file -> { if (!file.isHidden() && file.canRead()) { if (file.isDirectory()) { return true; } else { String ext = getExtension(file.getName()); for (String allowedExtension : sExtensions) { if (!TextUtils.isEmpty(ext)) { if (allowedExtension.equalsIgnoreCase(ext)) { return true; } } } } } return false; }; } }