package com.quran.labs.androidquran.util; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Environment; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.quran.labs.androidquran.BuildConfig; import com.quran.labs.androidquran.common.Response; import com.quran.labs.androidquran.data.QuranDataProvider; import com.quran.labs.androidquran.data.QuranFileConstants; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Locale; import okhttp3.Call; import okhttp3.OkHttpClient; import okhttp3.Request; import timber.log.Timber; import static com.quran.labs.androidquran.data.Constants.PAGES_LAST; public class QuranFileUtils { // server urls private static final String IMG_BASE_URL = QuranFileConstants.IMG_BASE_URL; private static final String IMG_ZIP_BASE_URL = QuranFileConstants.IMG_ZIP_BASE_URL; private static final String PATCH_ZIP_BASE_URL = QuranFileConstants.PATCH_ZIP_BASE_URL; private static final String DATABASE_BASE_URL = QuranFileConstants.DATABASE_BASE_URL; private static final String AYAHINFO_BASE_URL = QuranFileConstants.AYAHINFO_BASE_URL; private static final String AUDIO_DB_BASE_URL = QuranFileConstants.AUDIO_DB_BASE_URL; // local paths private static final String QURAN_BASE = QuranFileConstants.QURAN_BASE; private static final String DATABASE_DIRECTORY = QuranFileConstants.DATABASE_DIRECTORY; private static final String AUDIO_DIRECTORY = QuranFileConstants.AUDIO_DIRECTORY; private static final String AYAHINFO_DIRECTORY = QuranFileConstants.AYAHINFO_DIRECTORY; private static final String IMAGES_DIRECTORY = QuranFileConstants.IMAGES_DIRECTORY; // check if the images with the given width param have a version // that we specify (ex if version is 3, check for a .v3 file). public static boolean isVersion(Context context, String widthParam, int version) { String quranDirectory = getQuranImagesDirectory(context, widthParam); Timber.d("isVersion: checking if version %d exists for width %s at %s", version, widthParam, quranDirectory); if (quranDirectory == null) { return false; } // version 1 or below are true as long as you have images if (version <= 1) { return true; } // check the version code try { File vFile = new File(quranDirectory + File.separator + ".v" + version); return vFile.exists(); } catch (Exception e) { Timber.e(e, "isVersion: exception while checking version file"); return false; } } public static String getPotentialFallbackDirectory(Context context) { final String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { if (haveAllImages(context, "_1920")) { return "1920"; } else if (haveAllImages(context, "_1280")) { return "1280"; } else if (haveAllImages(context, "_1024")) { return "1024"; } else { return ""; } } return null; } public static boolean haveAllImages(Context context, String widthParam) { String quranDirectory = getQuranImagesDirectory(context, widthParam); Timber.d("haveAllImages: for width %s, directory is: %s", widthParam, quranDirectory); if (quranDirectory == null) { return false; } String state = Environment.getExternalStorageState(); if (state.equals(Environment.MEDIA_MOUNTED)) { File dir = new File(quranDirectory + File.separator); if (dir.isDirectory()) { Timber.d("haveAllImages: media state is mounted and directory exists"); String[] fileList = dir.list(); if (fileList == null) { Timber.d("haveAllImages: null fileList, checking page by page..."); for (int i = 1; i <= PAGES_LAST; i++) { if (!new File(dir, getPageFileName(i)).exists()) { Timber.d("haveAllImages: couldn't find page %d", i); return false; } } } else if (fileList.length < PAGES_LAST) { // ideally, we should loop for each page and ensure // all pages are there, but this will do for now. Timber.d("haveAllImages: found %d files instead of 604.", fileList.length); return false; } return true; } else { Timber.d("haveAllImages: couldn't find the directory, so making it instead"); QuranFileUtils.makeQuranDirectory(context); if (!IMAGES_DIRECTORY.isEmpty()) { QuranFileUtils.makeQuranImagesDirectory(context); } } } return false; } public static String getPageFileName(int p) { NumberFormat nf = NumberFormat.getInstance(Locale.US); nf.setMinimumIntegerDigits(3); return "page" + nf.format(p) + ".png"; } private static boolean isSDCardMounted() { String state = Environment.getExternalStorageState(); return state.equals(Environment.MEDIA_MOUNTED); } @NonNull public static Response getImageFromSD(Context context, String widthParam, String filename) { String location; if (widthParam != null) { location = getQuranImagesDirectory(context, widthParam); } else { location = getQuranImagesDirectory(context); } if (location == null) { return new Response(Response.ERROR_SD_CARD_NOT_FOUND); } BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ALPHA_8; final Bitmap bitmap = BitmapFactory.decodeFile(location + File.separator + filename, options); return bitmap == null ? new Response(Response.ERROR_FILE_NOT_FOUND) : new Response(bitmap); } private static boolean writeNoMediaFile(String parentDir) { File f = new File(parentDir + "/.nomedia"); if (f.exists()) { return true; } try { return f.createNewFile(); } catch (IOException e) { return false; } } public static boolean makeQuranDirectory(Context context) { String path = getQuranImagesDirectory(context); if (path == null) { return false; } File directory = new File(path); if (directory.exists() && directory.isDirectory()) { return writeNoMediaFile(path); } else { return directory.mkdirs() && writeNoMediaFile(path); } } private static boolean makeQuranImagesDirectory(Context context) { return makeDirectory(getQuranImagesDirectory(context)); } private static boolean makeDirectory(String path) { if (path == null) { return false; } File directory = new File(path); return directory.exists() && directory.isDirectory() || directory.mkdirs(); } private static boolean makeQuranDatabaseDirectory(Context context) { return makeDirectory(getQuranDatabaseDirectory(context)); } private static boolean makeQuranAyahDatabaseDirectory(Context context) { return makeQuranDatabaseDirectory(context) && makeDirectory(getQuranAyahDatabaseDirectory(context)); } public static Response getImageFromWeb(OkHttpClient okHttpClient, Context context, String filename) { return getImageFromWeb(okHttpClient, context, filename, false); } @NonNull private static Response getImageFromWeb(OkHttpClient okHttpClient, Context context, String filename, boolean isRetry) { QuranScreenInfo instance = QuranScreenInfo.getInstance(); if (instance == null) { instance = QuranScreenInfo.getOrMakeInstance(context); } String urlString = IMG_BASE_URL + "width" + instance.getWidthParam() + File.separator + filename; Timber.d("want to download: %s", urlString); final Request request = new Request.Builder() .url(urlString) .build(); final Call call = okHttpClient.newCall(request); InputStream stream = null; try { final okhttp3.Response response = call.execute(); if (response.isSuccessful()) { stream = response.body().byteStream(); final Bitmap bitmap = decodeBitmapStream(stream); if (bitmap != null) { String path = getQuranImagesDirectory(context); int warning = Response.WARN_SD_CARD_NOT_FOUND; if (path != null && QuranFileUtils.makeQuranDirectory(context)) { path += File.separator + filename; warning = tryToSaveBitmap(bitmap, path) ? 0 : Response.WARN_COULD_NOT_SAVE_FILE; } return new Response(bitmap, warning); } } } catch (IOException ioe) { Timber.e(ioe, "exception downloading file"); } finally { closeQuietly(stream); } return isRetry ? new Response(Response.ERROR_DOWNLOADING_ERROR) : getImageFromWeb(okHttpClient, context, filename, true); } private static Bitmap decodeBitmapStream(InputStream is) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ALPHA_8; return BitmapFactory.decodeStream(is, null, options); } public static void closeQuietly(Closeable closeable) { if (closeable != null) { try { closeable.close(); } catch (Exception e) { // no op } } } private static boolean tryToSaveBitmap(Bitmap bitmap, String savePath) { FileOutputStream output = null; try { output = new FileOutputStream(savePath); return bitmap.compress(Bitmap.CompressFormat.PNG, 100, output); } catch (IOException ioe) { // do nothing } finally { try { if (output != null) { output.flush(); output.close(); } } catch (Exception e) { // ignore... } } return false; } @Nullable public static String getQuranBaseDirectory(Context context) { String basePath = QuranSettings.getInstance(context).getAppCustomLocation(); if (!isSDCardMounted()) { // if our best guess suggests that we won't have access to the data due to the sdcard not // being mounted, then set the base path to null for now. if (basePath == null || basePath.equals( Environment.getExternalStorageDirectory().getAbsolutePath()) || (basePath.contains(BuildConfig.APPLICATION_ID) && context.getExternalFilesDir(null) == null)) { basePath = null; } } if (basePath != null) { if (!basePath.endsWith(File.separator)) { basePath += File.separator; } return basePath + QURAN_BASE; } return null; } /** * Returns the app used space in megabytes */ public static int getAppUsedSpace(Context context) { final String baseDirectory = getQuranBaseDirectory(context); if (baseDirectory == null) { return -1; } File base = new File(baseDirectory); ArrayList<File> files = new ArrayList<>(); files.add(base); long size = 0; while (!files.isEmpty()) { File f = files.remove(0); if (f.isDirectory()) { File[] subFiles = f.listFiles(); if (subFiles != null) { Collections.addAll(files, subFiles); } } else { size += f.length(); } } return (int) (size / (long) (1024 * 1024)); } public static String getQuranDatabaseDirectory(Context context) { String base = getQuranBaseDirectory(context); return (base == null) ? null : base + DATABASE_DIRECTORY; } public static String getQuranAyahDatabaseDirectory(Context context) { String base = getQuranBaseDirectory(context); return base == null ? null : base + File.separator + AYAHINFO_DIRECTORY; } @Nullable public static String getQuranAudioDirectory(Context context){ String path = getQuranBaseDirectory(context); if (path == null) { return null; } path += AUDIO_DIRECTORY; File dir = new File(path); if (!dir.exists() && !dir.mkdirs()) { return null; } writeNoMediaFile(path); return path + File.separator; } public static String getQuranImagesBaseDirectory(Context context) { String s = QuranFileUtils.getQuranBaseDirectory(context); return s == null ? null : s + IMAGES_DIRECTORY; } private static String getQuranImagesDirectory(Context context) { QuranScreenInfo qsi = QuranScreenInfo.getInstance(); if (qsi == null) { return null; } return getQuranImagesDirectory(context, qsi.getWidthParam()); } private static String getQuranImagesDirectory(Context context, String widthParam) { String base = getQuranBaseDirectory(context); return (base == null) ? null : base + (IMAGES_DIRECTORY.isEmpty() ? "" : IMAGES_DIRECTORY + File.separator) + "width" + widthParam; } public static String getZipFileUrl() { QuranScreenInfo qsi = QuranScreenInfo.getInstance(); if (qsi == null) { return null; } return getZipFileUrl(qsi.getWidthParam()); } public static String getZipFileUrl(String widthParam) { String url = IMG_ZIP_BASE_URL; url += "images" + widthParam + ".zip"; return url; } public static String getPatchFileUrl(String widthParam, int toVersion) { return PATCH_ZIP_BASE_URL + toVersion + "/patch" + widthParam + "_v" + toVersion + ".zip"; } private static String getAyaPositionFileName() { QuranScreenInfo qsi = QuranScreenInfo.getInstance(); if (qsi == null) { return null; } return getAyaPositionFileName(qsi.getWidthParam()); } public static String getAyaPositionFileName(String widthParam) { return "ayahinfo" + widthParam + ".db"; } public static String getAyaPositionFileUrl() { QuranScreenInfo qsi = QuranScreenInfo.getInstance(); if (qsi == null) { return null; } return getAyaPositionFileUrl(qsi.getWidthParam()); } public static String getAyaPositionFileUrl(String widthParam) { return AYAHINFO_BASE_URL + "ayahinfo" + widthParam + ".zip"; } static String getGaplessDatabaseRootUrl() { QuranScreenInfo qsi = QuranScreenInfo.getInstance(); if (qsi == null) { return null; } return AUDIO_DB_BASE_URL; } public static boolean haveAyaPositionFile(Context context) { String base = QuranFileUtils.getQuranAyahDatabaseDirectory(context); if (base == null) { QuranFileUtils.makeQuranAyahDatabaseDirectory(context); } String filename = QuranFileUtils.getAyaPositionFileName(); if (filename != null) { String ayaPositionDb = base + File.separator + filename; File f = new File(ayaPositionDb); return f.exists(); } return false; } public static boolean hasTranslation(Context context, String fileName) { String path = getQuranDatabaseDirectory(context); if (path != null) { path += File.separator + fileName; return new File(path).exists(); } return false; } public static boolean removeTranslation(Context context, String fileName) { String path = getQuranDatabaseDirectory(context); if (path != null) { path += File.separator + fileName; File f = new File(path); return f.delete(); } return false; } public static boolean hasArabicSearchDatabase(Context context) { if (hasTranslation(context, QuranDataProvider.QURAN_ARABIC_DATABASE)) { return true; } else if (!DATABASE_DIRECTORY.equals(AYAHINFO_DIRECTORY)){ // non-hafs flavors copy their ayahinfo and arabic search database in a subdirectory, // so we copy back the arabic database into the translations directory where it can // be shared across all flavors of quran android final File ayahInfoFile = new File(getQuranAyahDatabaseDirectory(context), QuranDataProvider.QURAN_ARABIC_DATABASE); final String baseDir = getQuranDatabaseDirectory(context); if (ayahInfoFile.exists() && baseDir != null) { final File base = new File(baseDir); final File translationsFile = new File(base, QuranDataProvider.QURAN_ARABIC_DATABASE); if (base.mkdir()) { try { copyFile(ayahInfoFile, translationsFile); return true; } catch (IOException ioe) { if (!translationsFile.delete()) { Timber.e("Error deleting translations file"); } } } } } return false; } public static String getArabicSearchDatabaseUrl() { return DATABASE_BASE_URL + QuranDataProvider.QURAN_ARABIC_DATABASE; } public static boolean moveAppFiles(Context context, String newLocation) { if (QuranSettings.getInstance(context).getAppCustomLocation().equals(newLocation)) { return true; } final String baseDir = getQuranBaseDirectory(context); if (baseDir == null) { return false; } File currentDirectory = new File(baseDir); File newDirectory = new File(newLocation, QURAN_BASE); if (!currentDirectory.exists()) { // No files to copy, so change the app directory directly return true; } else if (newDirectory.exists() || newDirectory.mkdirs()) { try { copyFileOrDirectory(currentDirectory, newDirectory); deleteFileOrDirectory(currentDirectory); return true; } catch (IOException e) { Timber.e(e, "error moving app files"); } } return false; } private static void deleteFileOrDirectory(File file) { if (file.isDirectory()) { File[] subFiles = file.listFiles(); // subFiles is null on some devices, despite this being a directory int length = subFiles == null ? 0 : subFiles.length; for (int i = 0; i < length; i++) { File sf = subFiles[i]; if (sf.isFile()) { if (!sf.delete()) { Timber.e("Error deleting %s", sf.getPath()); } } else { deleteFileOrDirectory(sf); } } } if (!file.delete()) { Timber.e("Error deleting %s", file.getPath()); } } private static void copyFileOrDirectory(File source, File destination) throws IOException { if (source.isDirectory()) { if (!destination.exists() && !destination.mkdirs()) { return; } File[] files = source.listFiles(); for (File f : files) { copyFileOrDirectory(f, new File(destination, f.getName())); } } else { copyFile(source, destination); } } private static void copyFile(File source, File destination) throws IOException { InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination); byte[] buffer = new byte[1024]; int length; while ((length = in.read(buffer)) > 0) { out.write(buffer, 0, length); } out.flush(); out.close(); in.close(); } }