package me.devsaki.hentoid.util;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import me.devsaki.hentoid.BuildConfig;
import me.devsaki.hentoid.HentoidApp;
import me.devsaki.hentoid.R;
import me.devsaki.hentoid.database.domains.Content;
import me.devsaki.hentoid.enums.Site;
import static android.os.Environment.MEDIA_MOUNTED;
import static android.os.Environment.getExternalStorageDirectory;
import static android.os.Environment.getExternalStorageState;
/**
* Created by avluis on 08/05/2016.
* File related utility class
*/
public class FileHelper {
// Note that many devices will report true (there are no guarantees of this being 'external')
public static final boolean isSDPresent = getExternalStorageState().equals(MEDIA_MOUNTED);
private static final String TAG = LogHelper.makeLogTag(FileHelper.class);
private static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".provider.FileProvider";
public static void saveUri(Uri uri) {
LogHelper.d(TAG, "Saving Uri: " + uri);
SharedPreferences prefs = HentoidApp.getSharedPrefs();
SharedPreferences.Editor editor = prefs.edit();
editor.putString(ConstsPrefs.PREF_SD_STORAGE_URI, uri.toString()).apply();
}
public static void clearUri() {
SharedPreferences prefs = HentoidApp.getSharedPrefs();
SharedPreferences.Editor editor = prefs.edit();
editor.putString(ConstsPrefs.PREF_SD_STORAGE_URI, "").apply();
}
static String getStringUri() {
SharedPreferences prefs = HentoidApp.getSharedPrefs();
return prefs.getString(ConstsPrefs.PREF_SD_STORAGE_URI, null);
}
public static boolean isSAF() {
return getStringUri() != null && !getStringUri().equals("");
}
/**
* Determine if a file is on external sd card. (Kitkat+)
*
* @param file The file.
* @return true if on external sd card.
*/
public static boolean isOnExtSdCard(final File file) {
return getExtSdCardFolder(file) != null;
}
/**
* Determine the main folder of the external SD card containing the given file. (Kitkat+)
*
* @param file The file.
* @return The main folder of the external SD card containing this file,
* if the file is on an SD card. Otherwise, null is returned.
*/
public static String getExtSdCardFolder(final File file) {
String[] extSdPaths = getExtSdCardPaths();
try {
for (String extSdPath : extSdPaths) {
if (file.getCanonicalPath().startsWith(extSdPath)) {
return extSdPath;
}
}
} catch (IOException e) {
return null;
}
return null;
}
/**
* Get a list of external SD card paths. (Kitkat+)
*
* @return A list of external SD card paths.
*/
public static String[] getExtSdCardPaths() {
Context cxt = HentoidApp.getAppContext();
List<String> paths = new ArrayList<>();
for (File file : ContextCompat.getExternalFilesDirs(cxt, "external")) {
if (file != null && !file.equals(cxt.getExternalFilesDir("external"))) {
int index = file.getAbsolutePath().lastIndexOf("/Android/data");
if (index < 0) {
LogHelper.w(TAG, "Unexpected external file dir: " + file.getAbsolutePath());
} else {
String path = file.getAbsolutePath().substring(0, index);
try {
path = new File(path).getCanonicalPath();
} catch (IOException e) {
// Keep non-canonical path.
}
paths.add(path);
}
}
}
return paths.toArray(new String[paths.size()]);
}
/**
* Check is a file is writable.
* Detects write issues on external SD card.
*
* @param file The file.
* @return true if the file is writable.
*/
public static boolean isWritable(@NonNull final File file) {
boolean isExisting = file.exists();
try {
FileOutputStream output = new FileOutputStream(file, true);
try {
output.close();
} catch (IOException e) {
// do nothing.
}
} catch (FileNotFoundException e) {
if (!file.isDirectory()) {
return false;
}
}
boolean result = file.canWrite();
// Ensure that file is not created during this process.
if (!isExisting) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
return result;
}
/**
* Checks if file could be read or created
*
* @param file - The file (as a String).
* @return true if file's is writable.
*/
public static boolean isReadable(@NonNull final String file) {
return isReadable(new File(file));
}
/**
* Checks if file could be read or created
*
* @param file - The file.
* @return true if file's is writable.
*/
public static boolean isReadable(@NonNull final File file) {
if (!file.isFile()) {
LogHelper.d(TAG, "isReadable(): Not a File");
return false;
}
return file.exists() && file.canRead();
}
/**
* Method ensures file creation from stream.
*
* @param stream - OutputStream
* @return true if all OK.
*/
public static boolean sync(@NonNull final OutputStream stream) {
return (stream instanceof FileOutputStream) && FileUtil.sync((FileOutputStream) stream);
}
/**
* Get OutputStream from file.
*
* @param target The file.
* @return FileOutputStream.
*/
public static OutputStream getOutputStream(@NonNull final File target) {
return FileUtil.getOutputStream(target);
}
/**
* Create a file.
*
* @param file The file to be created.
* @return true if creation was successful.
*/
public static boolean createFile(@NonNull File file) throws IOException {
return FileUtil.makeFile(file);
}
/**
* Create a folder.
*
* @param file The folder to be created.
* @return true if creation was successful.
*/
public static boolean createDirectory(@NonNull File file) {
return FileUtil.makeDir(file);
}
/**
* Delete a file.
*
* @param target The file.
* @return true if deleted successfully.
*/
public static boolean removeFile(File target) {
return FileUtil.deleteFile(target);
}
/**
* Delete files in a target directory.
*
* @param target The folder.
* @return true if cleaned successfully.
*/
public static boolean cleanDirectory(@NonNull File target) {
// Delete directory -- create directory
return FileUtil.deleteDir(target) && FileUtil.makeDir(target);
}
public static boolean validateFolder(String folder) {
return validateFolder(folder, false);
}
public static boolean validateFolder(String folder, boolean notify) {
Context cxt = HentoidApp.getAppContext();
SharedPreferences prefs = HentoidApp.getSharedPrefs();
SharedPreferences.Editor editor = prefs.edit();
// Validate folder
File file = new File(folder);
if (!file.exists() && !file.isDirectory() && !FileUtil.makeDir(file)) {
if (notify) {
Helper.toast(cxt, R.string.error_creating_folder);
}
return false;
}
File nomedia = new File(folder, ".nomedia");
boolean hasPermission;
// Clean up (if any) nomedia file
try {
if (nomedia.exists()) {
boolean deleted = FileUtil.deleteFile(nomedia);
if (deleted) {
LogHelper.d(TAG, ".nomedia file deleted");
}
}
// Re-create nomedia file to confirm write permissions
hasPermission = FileUtil.makeFile(nomedia);
} catch (IOException e) {
hasPermission = false;
LogHelper.e(TAG, e, "We couldn't confirm write permissions to this location: ");
}
if (!hasPermission) {
if (notify) {
Helper.toast(cxt, R.string.error_write_permission);
}
return false;
}
editor.putString(Consts.SETTINGS_FOLDER, folder);
boolean directorySaved = editor.commit();
if (!directorySaved) {
if (notify) {
Helper.toast(cxt, R.string.error_creating_folder);
}
return false;
}
return true;
}
public static boolean createNoMedia() {
String settingDir = getRoot();
File noMedia = new File(settingDir, ".nomedia");
try {
if (FileUtil.makeFile(noMedia)) {
Helper.toast(R.string.nomedia_file_created);
} else {
LogHelper.d(TAG, ".nomedia file already exists.");
}
} catch (IOException io) {
if (!isReadable(noMedia)) {
LogHelper.e(TAG, io, "Failed to create file.");
Helper.toast(R.string.error_creating_nomedia_file);
return false;
} else {
Helper.toast(R.string.nomedia_file_created);
}
}
return true;
}
// Run method in background thread
public static void removeContent(Context cxt, Content content) {
File dir = getContentDownloadDir(cxt, content);
if (FileUtil.deleteDir(dir)) {
LogHelper.d(TAG, "Directory " + dir + " removed.");
} else {
LogHelper.d(TAG, "Failed to delete directory: " + dir);
}
}
public static File getContentDownloadDir(Context cxt, Content content) {
File file;
String settingDir = getRoot();
String folderDir = content.getSite().getFolder() + content.getUniqueSiteId();
if (settingDir.isEmpty()) {
return getDefaultDir(cxt, folderDir);
}
file = new File(settingDir, folderDir);
if (!file.exists() && !FileUtil.makeDir(file)) {
file = new File(settingDir + folderDir);
if (!file.exists()) {
FileUtil.makeDir(file);
}
}
return file;
}
public static File getDefaultDir(Context cxt, String dir) {
File file;
try {
file = new File(getExternalStorageDirectory() + "/"
+ Consts.DEFAULT_LOCAL_DIRECTORY + "/" + dir);
} catch (Exception e) {
file = cxt.getDir("", Context.MODE_PRIVATE);
file = new File(file, "/" + Consts.DEFAULT_LOCAL_DIRECTORY);
}
if (!file.exists() && !FileUtil.makeDir(file)) {
file = cxt.getDir("", Context.MODE_PRIVATE);
file = new File(file, "/" + Consts.DEFAULT_LOCAL_DIRECTORY + "/" + dir);
if (!file.exists()) {
FileUtil.makeDir(file);
}
}
return file;
}
public static File getSiteDownloadDir(Context cxt, Site site) {
File file;
String settingDir = getRoot();
String folderDir = site.getFolder();
if (settingDir.isEmpty()) {
return getDefaultDir(cxt, folderDir);
}
file = new File(settingDir, folderDir);
if (!file.exists() && !FileUtil.makeDir(file)) {
file = new File(settingDir + folderDir);
if (!file.exists()) {
FileUtil.makeDir(file);
}
}
return file;
}
// Method is used by onBindViewHolder(), speed is key
public static String getThumb(Context cxt, Content content) {
File dir = getContentDownloadDir(cxt, content);
String coverUrl = content.getCoverImageUrl();
if (isSAF() && getExtSdCardFolder(new File(getRoot())) == null) {
LogHelper.d(TAG, "File not found!! Returning online resource.");
return coverUrl;
}
String thumbExt = coverUrl.substring(coverUrl.length() - 3);
String thumb;
switch (thumbExt) {
case "jpg":
case "png":
case "gif":
thumb = new File(dir, "thumb" + "." + thumbExt).getAbsolutePath();
// Some thumbs from nhentai were saved as jpg instead of png
// Follow through to scan the directory instead
// TODO: Rename the file instead
if (!content.getSite().equals(Site.NHENTAI)) {
break;
}
default:
File[] fileList = dir.listFiles(
pathname -> {
return pathname.getName().contains("thumb");
}
);
thumb = fileList.length > 0 ? fileList[0].getAbsolutePath() : coverUrl;
break;
}
return thumb;
}
public static void openContent(final Context cxt, Content content) {
LogHelper.d(TAG, "Opening: " + content.getTitle() + " from: " +
getContentDownloadDir(cxt, content));
if (isSAF() && getExtSdCardFolder(new File(getRoot())) == null) {
LogHelper.d(TAG, "File not found!! Exiting method.");
Helper.toast(R.string.sd_access_error);
return;
}
Helper.toast("Opening: " + content.getTitle());
SharedPreferences sp = HentoidApp.getSharedPrefs();
File dir = getContentDownloadDir(cxt, content);
File imageFile = null;
File[] files = dir.listFiles();
Arrays.sort(files);
for (File file : files) {
String filename = file.getName();
if (filename.endsWith(".jpg") ||
filename.endsWith(".png") ||
filename.endsWith(".gif")) {
imageFile = file;
break;
}
}
if (imageFile == null) {
String message = cxt.getString(R.string.image_file_not_found)
.replace("@dir", dir.getAbsolutePath());
Helper.toast(cxt, message);
} else {
int readContentPreference = Integer.parseInt(
sp.getString(
ConstsPrefs.PREF_READ_CONTENT_LISTS,
ConstsPrefs.PREF_READ_CONTENT_DEFAULT + ""));
if (readContentPreference == ConstsPrefs.PREF_READ_CONTENT_ASK) {
final File file = imageFile;
AlertDialog.Builder builder = new AlertDialog.Builder(cxt);
builder.setMessage(R.string.select_the_action)
.setPositiveButton(R.string.open_default_image_viewer,
(dialog, id) -> openFile(cxt, file))
.setNegativeButton(R.string.open_perfect_viewer,
(dialog, id) -> openPerfectViewer(cxt, file)).create().show();
} else if (readContentPreference == ConstsPrefs.PREF_READ_CONTENT_PERFECT_VIEWER) {
openPerfectViewer(cxt, imageFile);
}
}
}
private static void openFile(Context cxt, File aFile) {
Intent myIntent = new Intent(Intent.ACTION_VIEW);
File file = new File(aFile.getAbsolutePath());
String extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(file).toString());
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
myIntent.setDataAndType(Uri.fromFile(file), mimeType);
cxt.startActivity(myIntent);
}
private static void openPerfectViewer(Context cxt, File firstImage) {
try {
Intent intent = cxt
.getPackageManager()
.getLaunchIntentForPackage("com.rookiestudio.perfectviewer");
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(firstImage), "image/*");
cxt.startActivity(intent);
} catch (Exception e) {
Helper.toast(cxt, R.string.error_open_perfect_viewer);
}
}
public static void archiveContent(final Context cxt, Content content) {
LogHelper.d(TAG, "Building file list for: " + content.getTitle());
// Build list of files
File dir = getContentDownloadDir(cxt, content);
File[] files = dir.listFiles();
Arrays.sort(files);
ArrayList<File> fileList = new ArrayList<>();
for (File file : files) {
String filename = file.getName();
if (filename.endsWith(".json") || filename.contains("thumb")) {
break;
}
fileList.add(file);
}
// Create folder to share from
File sharedDir = new File(cxt.getExternalCacheDir() + "/shared");
if (FileUtil.makeDir(sharedDir)) {
LogHelper.d(TAG, "Shared folder created.");
}
// Clean directory (in case of previous job)
if (cleanDirectory(sharedDir)) {
LogHelper.d(TAG, "Shared folder cleaned up.");
}
// Build destination file
File dest = new File(cxt.getExternalCacheDir() + "/shared/" +
content.getTitle()
.replaceAll("[\\?\\\\/:|<>\\*]", " ") //filter ? \ / : | < > *
.replaceAll("\\s+", "_") // white space as underscores
+ ".zip");
LogHelper.d(TAG, "Destination file: " + dest);
// Convert ArrayList to Array
File[] fileArray = fileList.toArray(new File[fileList.size()]);
// Compress files
new AsyncUnzip(cxt, dest).execute(fileArray, dest);
}
public static String getRoot() {
SharedPreferences prefs = HentoidApp.getSharedPrefs();
return prefs.getString(Consts.SETTINGS_FOLDER, "");
}
private static class AsyncUnzip extends ZipUtil.ZipTask {
final Context cxt;
final File dest;
AsyncUnzip(Context cxt, File dest) {
this.cxt = cxt;
this.dest = dest;
}
@Override
protected void onPostExecute(Boolean aBoolean) {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
// Hentoid is FileProvider ready!!
sendIntent.putExtra(Intent.EXTRA_STREAM,
FileProvider.getUriForFile(cxt, AUTHORITY, dest));
sendIntent.setType(MimeTypes.getMimeType(dest));
cxt.startActivity(sendIntent);
}
}
}