package com.quran.labs.androidquran; import com.crashlytics.android.answers.Answers; import com.crashlytics.android.answers.CustomEvent; import com.quran.labs.androidquran.data.QuranFileConstants; import com.quran.labs.androidquran.service.QuranDownloadService; import com.quran.labs.androidquran.service.util.DefaultDownloadReceiver; import com.quran.labs.androidquran.service.util.PermissionUtil; import com.quran.labs.androidquran.service.util.QuranDownloadNotifier; import com.quran.labs.androidquran.service.util.ServiceIntentHelper; import com.quran.labs.androidquran.ui.QuranActivity; import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.labs.androidquran.util.QuranScreenInfo; import com.quran.labs.androidquran.util.QuranSettings; import android.Manifest; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.widget.Toast; import java.io.File; import timber.log.Timber; public class QuranDataActivity extends Activity implements DefaultDownloadReceiver.SimpleDownloadListener, ActivityCompat.OnRequestPermissionsResultCallback { public static final String PAGES_DOWNLOAD_KEY = "PAGES_DOWNLOAD_KEY"; private static final int LATEST_IMAGE_VERSION = QuranFileConstants.IMAGES_VERSION; private static final int REQUEST_WRITE_TO_SDCARD_PERMISSIONS = 1; private boolean mIsPaused = false; private AsyncTask<Void, Void, Boolean> mCheckPagesTask; private QuranSettings mQuranSettings; private AlertDialog mErrorDialog = null; private AlertDialog mPromptForDownloadDialog = null; private AlertDialog mPermissionsDialog; private DefaultDownloadReceiver mDownloadReceiver = null; private boolean mNeedPortraitImages = false; private boolean mNeedLandscapeImages = false; private boolean mTaskIsRunning; private String mPatchUrl; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); QuranScreenInfo.getOrMakeInstance(this); mQuranSettings = QuranSettings.getInstance(this); mQuranSettings.upgradePreferences(); } @Override protected void onResume() { super.onResume(); mIsPaused = false; mDownloadReceiver = new DefaultDownloadReceiver(this, QuranDownloadService.DOWNLOAD_TYPE_PAGES); mDownloadReceiver.setCanCancelDownload(true); String action = QuranDownloadNotifier.ProgressIntent.INTENT_NAME; LocalBroadcastManager.getInstance(this).registerReceiver( mDownloadReceiver, new IntentFilter(action)); mDownloadReceiver.setListener(this); checkPermissions(); } @Override protected void onPause() { mIsPaused = true; if (mDownloadReceiver != null) { mDownloadReceiver.setListener(null); LocalBroadcastManager.getInstance(this). unregisterReceiver(mDownloadReceiver); mDownloadReceiver = null; } if (mPromptForDownloadDialog != null) { mPromptForDownloadDialog.dismiss(); mPromptForDownloadDialog = null; } if (mErrorDialog != null) { mErrorDialog.dismiss(); mErrorDialog = null; } super.onPause(); } private void checkPermissions() { final String path = mQuranSettings.getAppCustomLocation(); final File fallbackFile = getExternalFilesDir(null); boolean usesExternalFileDir = path != null && path.contains("com.quran"); if (path == null || usesExternalFileDir && fallbackFile == null) { // suggests that we're on m+ and getExternalFilesDir returned null at some point runListView(); return; } boolean needsPermission = !usesExternalFileDir || !path.equals(fallbackFile.getAbsolutePath()); if (needsPermission && !PermissionUtil.haveWriteExternalStoragePermission(this)) { // request permission if (PermissionUtil.canRequestWriteExternalStoragePermission(this)) { Answers.getInstance().logCustom(new CustomEvent("storagePermissionRationaleShown")); //show permission rationale dialog mPermissionsDialog = new AlertDialog.Builder(this) .setMessage(R.string.storage_permission_rationale) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); mPermissionsDialog = null; Answers.getInstance().logCustom( new CustomEvent("storagePermissionRationaleAccepted")); // request permissions requestExternalSdcardPermission(); } }) .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // dismiss the dialog dialog.dismiss(); mPermissionsDialog = null; Answers.getInstance().logCustom( new CustomEvent("storagePermissionRationaleDenied")); // fall back if we can if (fallbackFile != null) { mQuranSettings.setAppCustomLocation(fallbackFile.getAbsolutePath()); checkPages(); } else { // set to null so we can try again next launch mQuranSettings.setAppCustomLocation(null); runListView(); } } }) .create(); mPermissionsDialog.show(); } else { // fall back if we can if (fallbackFile != null) { mQuranSettings.setAppCustomLocation(fallbackFile.getAbsolutePath()); checkPages(); } else { // set to null so we can try again next launch mQuranSettings.setAppCustomLocation(null); runListView(); } } } else { checkPages(); } } private void checkPages() { if (mTaskIsRunning) { return; } mTaskIsRunning = true; // check whether or not we need to download mCheckPagesTask = new CheckPagesAsyncTask(this); mCheckPagesTask.execute(); } private void requestExternalSdcardPermission() { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE }, REQUEST_WRITE_TO_SDCARD_PERMISSIONS); mQuranSettings.setSdcardPermissionsDialogPresented(); } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == REQUEST_WRITE_TO_SDCARD_PERMISSIONS) { if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { /** * taking a risk here. on nexus 6, the permission is granted automatically. on the emulator, * a restart is required. on reddit, someone with a nexus 9 said they also didn't need to * restart for the permission to take effect. * * going to assume that it just works to avoid meh code (a check to see if i can actually * write, and a PendingIntent plus System.exit to restart the app otherwise). logging to * know if we should actually have that code or not. * * also see: * http://stackoverflow.com/questions/32471888/ */ Answers.getInstance().logCustom(new CustomEvent("storagePermissionGranted")); if (!canWriteSdcardAfterPermissions()) { Answers.getInstance().logCustom(new CustomEvent("storagePermissionNeedsRestart")); Toast.makeText(this, R.string.storage_permission_please_restart, Toast.LENGTH_LONG).show(); } checkPages(); } else { Answers.getInstance().logCustom(new CustomEvent("storagePermissionDenied")); final File fallbackFile = getExternalFilesDir(null); if (fallbackFile != null) { mQuranSettings.setAppCustomLocation(fallbackFile.getAbsolutePath()); checkPages(); } else { // set to null so we can try again next launch mQuranSettings.setAppCustomLocation(null); runListView(); } } } } private boolean canWriteSdcardAfterPermissions() { String location = QuranFileUtils.getQuranBaseDirectory(this); if (location != null) { try { if (new File(location).exists() || QuranFileUtils.makeQuranDirectory(this)) { File f = new File(location, "" + System.currentTimeMillis()); if (f.createNewFile()) { f.delete(); return true; } } } catch (Exception e) { // no op } } return false; } @Override public void handleDownloadSuccess() { mQuranSettings.removeShouldFetchPages(); runListView(); } @Override public void handleDownloadFailure(int errId) { if (mErrorDialog != null && mErrorDialog.isShowing()) { return; } showFatalErrorDialog(errId); } private void showFatalErrorDialog(int errorId) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(errorId); builder.setCancelable(false); builder.setPositiveButton(R.string.download_retry, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); mErrorDialog = null; removeErrorPreferences(); downloadQuranImages(true); } }); builder.setNegativeButton(R.string.download_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); mErrorDialog = null; removeErrorPreferences(); mQuranSettings.setShouldFetchPages(false); runListView(); } }); mErrorDialog = builder.create(); mErrorDialog.show(); } private void removeErrorPreferences() { mQuranSettings.clearLastDownloadError(); } class CheckPagesAsyncTask extends AsyncTask<Void, Void, Boolean> { private final Context mAppContext; private String mPatchParam; private boolean mStorageNotAvailable; public CheckPagesAsyncTask(Context context) { mAppContext = context.getApplicationContext(); } @Override protected Boolean doInBackground(Void... params) { final String baseDir = QuranFileUtils.getQuranBaseDirectory(mAppContext); if (baseDir == null) { mStorageNotAvailable = true; return false; } final QuranScreenInfo qsi = QuranScreenInfo.getInstance(); if (!mQuranSettings.haveDefaultImagesDirectory()) { /* previously, we would send any screen widths greater than 1280 * to get 1920 images. this was problematic for various reasons, * including: * a. a texture limit for the maximum size of a bitmap that could * be loaded, which the 1920x3106 images exceeded on devices * with the minimum 2048 height capacity. * b. slow to switch pages due to the massive size of the gl * texture loaded by android. * * consequently, in this new version, we make anything above 1024 * fallback to a 1260 bucket (height of 2038). this works around * both problems (much faster page flipping now too) with a very * minor loss in quality. * * this code checks and sees, if the user already has a complete * folder of images - 1920, then 1280, then 1024 - and in any of * those cases, sets that in the pref so we load those instead of * the new 1260 images. */ final String fallback = QuranFileUtils.getPotentialFallbackDirectory(mAppContext); if (fallback != null) { mQuranSettings.setDefaultImagesDirectory(fallback); qsi.setOverrideParam(fallback); } } final String width = qsi.getWidthParam(); if (qsi.isDualPageMode(mAppContext)) { final String tabletWidth = qsi.getTabletWidthParam(); boolean haveLandscape = QuranFileUtils.haveAllImages(mAppContext, tabletWidth); boolean havePortrait = QuranFileUtils.haveAllImages(mAppContext, width); mNeedPortraitImages = !havePortrait; mNeedLandscapeImages = !haveLandscape; Timber.d("checkPages: have portrait images: %s, have landscape images: %s", havePortrait ? "yes" : "no", haveLandscape ? "yes" : "no"); if (haveLandscape && havePortrait) { // if we have the images, see if we need a patch set or not if (!QuranFileUtils.isVersion(mAppContext, width, LATEST_IMAGE_VERSION) || !QuranFileUtils.isVersion(mAppContext, tabletWidth, LATEST_IMAGE_VERSION)) { if (!width.equals(tabletWidth)) { mPatchParam = width + tabletWidth; } else { mPatchParam = width; } } } return haveLandscape && havePortrait; } else { boolean haveAll = QuranFileUtils.haveAllImages(mAppContext, QuranScreenInfo.getInstance().getWidthParam()); Timber.d("checkPages: have all images: %s", haveAll ? "yes" : "no"); mNeedPortraitImages = !haveAll; mNeedLandscapeImages = false; if (haveAll && !QuranFileUtils.isVersion(mAppContext, width, LATEST_IMAGE_VERSION)) { mPatchParam = width; } return haveAll; } } @Override protected void onPostExecute(@NonNull Boolean result) { mCheckPagesTask = null; mPatchUrl = null; mTaskIsRunning = false; if (mIsPaused) { return; } if (!result) { if (mStorageNotAvailable) { // no storage mounted, nothing we can do... runListView(); return; } String lastErrorItem = mQuranSettings.getLastDownloadItemWithError(); Timber.d("checkPages: need to download pages... lastError: %s", lastErrorItem); if (PAGES_DOWNLOAD_KEY.equals(lastErrorItem)) { int lastError = mQuranSettings.getLastDownloadErrorCode(); int errorId = ServiceIntentHelper .getErrorResourceFromErrorCode(lastError, false); showFatalErrorDialog(errorId); } else if (mQuranSettings.shouldFetchPages()) { downloadQuranImages(false); } else { promptForDownload(); } } else { if (!TextUtils.isEmpty(mPatchParam)) { Timber.d("checkPages: have pages, but need patch %s", mPatchParam); mPatchUrl = QuranFileUtils.getPatchFileUrl(mPatchParam, LATEST_IMAGE_VERSION); promptForDownload(); return; } runListView(); } } } /** * this method asks the service to download quran images. * * there are two possible cases - the first is one in which we are not * sure if a download is going on or not (ie we just came in the app, * the files aren't all there, so we want to start downloading). in * this case, we start the download only if we didn't receive any * broadcasts before starting it. * * in the second case, we know what we are doing (either because the user * just clicked "download" for the first time or the user asked to retry * after an error), then we pass the force parameter, which asks the * service to just restart the download irrespective of anything else. * * @param force whether to force the download to restart or not */ private void downloadQuranImages(boolean force) { // if any broadcasts were received, then we are already downloading // so unless we know what we are doing (via force), don't ask the // service to restart the download if (mDownloadReceiver != null && mDownloadReceiver.didReceiveBroadcast() && !force) { return; } if (mIsPaused) { return; } QuranScreenInfo qsi = QuranScreenInfo.getInstance(); String url; if (mNeedPortraitImages && !mNeedLandscapeImages) { // phone (and tablet when upgrading on some devices, ex n10) url = QuranFileUtils.getZipFileUrl(); } else if (mNeedLandscapeImages && !mNeedPortraitImages) { // tablet (when upgrading from pre-tablet on some devices, ex n7). url = QuranFileUtils.getZipFileUrl(qsi.getTabletWidthParam()); } else { // new tablet installation - if both image sets are the same // size, then just get the correct one only if (qsi.getTabletWidthParam().equals(qsi.getWidthParam())) { url = QuranFileUtils.getZipFileUrl(); } else { // otherwise download one zip with both image sets String widthParam = qsi.getWidthParam() + qsi.getTabletWidthParam(); url = QuranFileUtils.getZipFileUrl(widthParam); } } // if we have a patch url, just use that if (!TextUtils.isEmpty(mPatchUrl)) { url = mPatchUrl; } String destination = QuranFileUtils.getQuranImagesBaseDirectory(QuranDataActivity.this); // start service Intent intent = ServiceIntentHelper.getDownloadIntent(this, url, destination, getString(R.string.app_name), PAGES_DOWNLOAD_KEY, QuranDownloadService.DOWNLOAD_TYPE_PAGES); if (!force) { // handle race condition in which we missed the error preference and // the broadcast - if so, just rebroadcast errors so we handle them intent.putExtra(QuranDownloadService.EXTRA_REPEAT_LAST_ERROR, true); } startService(intent); } private void promptForDownload() { int message = R.string.downloadPrompt; if (QuranScreenInfo.getInstance().isDualPageMode(this) && (mNeedPortraitImages != mNeedLandscapeImages)) { message = R.string.downloadTabletPrompt; } if (!TextUtils.isEmpty(mPatchUrl)) { // patch message if applicable message = R.string.downloadImportantPrompt; } AlertDialog.Builder dialog = new AlertDialog.Builder(this); dialog.setMessage(message); dialog.setCancelable(false); dialog.setPositiveButton(R.string.downloadPrompt_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); mPromptForDownloadDialog = null; mQuranSettings.setShouldFetchPages(true); downloadQuranImages(true); } }); dialog.setNegativeButton(R.string.downloadPrompt_no, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.dismiss(); mPromptForDownloadDialog = null; runListView(); } }); mPromptForDownloadDialog = dialog.create(); mPromptForDownloadDialog.setTitle(R.string.downloadPrompt_title); mPromptForDownloadDialog.show(); } protected void runListView() { Intent i = new Intent(this, QuranActivity.class); i.putExtra(QuranActivity.EXTRA_SHOW_TRANSLATION_UPGRADE, mQuranSettings.haveUpdatedTranslations()); startActivity(i); finish(); } }