package com.quran.labs.androidquran.presenter.translation; import android.content.Context; import android.support.annotation.VisibleForTesting; import android.util.SparseArray; import com.crashlytics.android.Crashlytics; import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.dao.translation.Translation; import com.quran.labs.androidquran.dao.translation.TranslationItem; import com.quran.labs.androidquran.dao.translation.TranslationList; import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.database.DatabaseHandler; import com.quran.labs.androidquran.database.TranslationsDBAdapter; import com.quran.labs.androidquran.presenter.Presenter; import com.quran.labs.androidquran.ui.TranslationManagerActivity; import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.labs.androidquran.util.QuranSettings; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.observers.DisposableMaybeObserver; import io.reactivex.schedulers.Schedulers; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; import okio.BufferedSink; import okio.Okio; import timber.log.Timber; @Singleton public class TranslationManagerPresenter implements Presenter<TranslationManagerActivity> { private static final String WEB_SERVICE_ENDPOINT = "data/translations.php?v=4"; private static final String CACHED_RESPONSE_FILE_NAME = "translations.v4.cache"; private final Context appContext; private final OkHttpClient okHttpClient; private final QuranSettings quranSettings; private final TranslationsDBAdapter translationsDBAdapter; @VisibleForTesting String host; private TranslationManagerActivity currentActivity; @Inject TranslationManagerPresenter(Context appContext, OkHttpClient okHttpClient, QuranSettings quranSettings, TranslationsDBAdapter dbAdapter) { this.host = Constants.HOST; this.appContext = appContext; this.okHttpClient = okHttpClient; this.quranSettings = quranSettings; this.translationsDBAdapter = dbAdapter; } public void checkForUpdates() { getTranslationsList(true); } public void getTranslationsList(boolean forceDownload) { Observable.concat( getCachedTranslationListObservable(forceDownload), getRemoteTranslationListObservable()) .filter(translationList -> translationList.translations != null) .firstElement() .filter(translationList -> !translationList.translations.isEmpty()) .map(translationList -> mergeWithServerTranslations(translationList.translations)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new DisposableMaybeObserver<List<TranslationItem>>() { @Override public void onSuccess(List<TranslationItem> translationItems) { if (currentActivity != null) { currentActivity.onTranslationsUpdated(translationItems); } // used for marking upgrades, irrespective of whether or not there is a bound activity boolean updatedTranslations = false; for (TranslationItem item : translationItems) { if (item.needsUpgrade()) { updatedTranslations = true; break; } } quranSettings.setHaveUpdatedTranslations(updatedTranslations); } @Override public void onError(Throwable e) { if (currentActivity != null) { currentActivity.onErrorDownloadTranslations(); } } @Override public void onComplete() { if (currentActivity != null) { currentActivity.onErrorDownloadTranslations(); } } }); } public void updateItem(final TranslationItem item) { Observable.fromCallable(() -> translationsDBAdapter.writeTranslationUpdates(Collections.singletonList(item)) ).subscribeOn(Schedulers.io()) .subscribe(); } Observable<TranslationList> getCachedTranslationListObservable(final boolean forceDownload) { return Observable.defer(() -> { boolean isCacheStale = System.currentTimeMillis() - quranSettings.getLastUpdatedTranslationDate() > Constants.MIN_TRANSLATION_REFRESH_TIME; if (forceDownload || isCacheStale) { return Observable.empty(); } try { File cachedFile = getCachedFile(); if (cachedFile.exists()) { Moshi moshi = new Moshi.Builder().build(); JsonAdapter<TranslationList> jsonAdapter = moshi.adapter(TranslationList.class); return Observable.just(jsonAdapter.fromJson(Okio.buffer(Okio.source(cachedFile)))); } } catch (Exception e) { Crashlytics.logException(e); } return Observable.empty(); }); } Observable<TranslationList> getRemoteTranslationListObservable() { return Observable.fromCallable(() -> { Request request = new Request.Builder() .url(host + WEB_SERVICE_ENDPOINT) .build(); Response response = okHttpClient.newCall(request).execute(); Moshi moshi = new Moshi.Builder().build(); JsonAdapter<TranslationList> jsonAdapter = moshi.adapter(TranslationList.class); ResponseBody responseBody = response.body(); TranslationList result = jsonAdapter.fromJson(responseBody.source()); responseBody.close(); return result; }).doOnNext(translationList -> { if (translationList.translations != null && !translationList.translations.isEmpty()) { writeTranslationList(translationList); } }); } void writeTranslationList(TranslationList list) { File cacheFile = getCachedFile(); try { File directory = cacheFile.getParentFile(); boolean directoryExists = directory.mkdirs() || directory.isDirectory(); if (directoryExists) { if (cacheFile.exists()) { cacheFile.delete(); } Moshi moshi = new Moshi.Builder().build(); JsonAdapter<TranslationList> jsonAdapter = moshi.adapter(TranslationList.class); BufferedSink sink = Okio.buffer(Okio.sink(cacheFile)); jsonAdapter.toJson(sink, list); sink.close(); quranSettings.setLastUpdatedTranslationDate(System.currentTimeMillis()); } } catch (Exception e) { cacheFile.delete(); Crashlytics.logException(e); } } private File getCachedFile() { String dir = QuranFileUtils.getQuranDatabaseDirectory(appContext); return new File(dir + File.separator + CACHED_RESPONSE_FILE_NAME); } private List<TranslationItem> mergeWithServerTranslations(List<Translation> serverTranslations) { List<TranslationItem> results = new ArrayList<>(serverTranslations.size()); SparseArray<LocalTranslation> localTranslations = translationsDBAdapter.getTranslationsHash(); String databaseDir = QuranFileUtils.getQuranDatabaseDirectory(appContext); List<TranslationItem> updates = new ArrayList<>(); for (int i = 0, count = serverTranslations.size(); i < count; i++) { Translation translation = serverTranslations.get(i); LocalTranslation local = localTranslations.get(translation.id); File dbFile = new File(databaseDir, translation.fileName); boolean exists = dbFile.exists(); TranslationItem item; if (exists) { int version = local == null ? getVersionFromDatabase(translation.fileName) : local.version; item = new TranslationItem(translation, version); } else { item = new TranslationItem(translation); } if (exists && !item.exists()) { // delete the file, it has been corrupted if (dbFile.delete()) { exists = false; } } if ((local == null && exists) || (local != null && !exists)) { updates.add(item); } else if (local != null && local.languageCode == null) { // older items don't have a language code updates.add(item); } results.add(item); } if (!updates.isEmpty()) { translationsDBAdapter.writeTranslationUpdates(updates); } return results; } private int getVersionFromDatabase(String filename) { try { DatabaseHandler handler = DatabaseHandler.getDatabaseHandler(appContext, filename); if (handler.validDatabase()) { return handler.getTextVersion(); } } catch (Exception e) { Timber.d(e, "exception opening database: %s", filename); } return 0; } @Override public void bind(TranslationManagerActivity activity) { currentActivity = activity; } @Override public void unbind(TranslationManagerActivity activity) { if (activity == currentActivity) { currentActivity = null; } } }