diff --git a/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslation.kt b/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslation.kt index 99c2e1366b..d541bf54f5 100644 --- a/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslation.kt +++ b/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslation.kt @@ -9,7 +9,8 @@ data class LocalTranslation( val url: String = "", val languageCode: String? = "", val version: Int = 1, - val minimumVersion: Int = 2) { + val minimumVersion: Int = 2, + val displayOrder: Int = -1) { fun getTranslatorName(): String { return when { diff --git a/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslationDisplaySort.kt b/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslationDisplaySort.kt new file mode 100644 index 0000000000..888836241f --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslationDisplaySort.kt @@ -0,0 +1,7 @@ +package com.quran.labs.androidquran.common + +class LocalTranslationDisplaySort : Comparator { + override fun compare(p0: LocalTranslation, p1: LocalTranslation): Int { + return p0.displayOrder.compareTo(p1.displayOrder); + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/common/TranslationMetadata.kt b/app/src/main/java/com/quran/labs/androidquran/common/TranslationMetadata.kt index 48ea1cdd84..307a8d0893 100644 --- a/app/src/main/java/com/quran/labs/androidquran/common/TranslationMetadata.kt +++ b/app/src/main/java/com/quran/labs/androidquran/common/TranslationMetadata.kt @@ -5,4 +5,5 @@ import com.quran.data.model.SuraAyah data class TranslationMetadata(val sura: Int, val ayah: Int, val text: CharSequence, + val localTranslationId: Int? = null, val link: SuraAyah? = null) diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/translation/Translation.kt b/app/src/main/java/com/quran/labs/androidquran/dao/translation/Translation.kt index bb155f5755..1d01051d1a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/dao/translation/Translation.kt +++ b/app/src/main/java/com/quran/labs/androidquran/dao/translation/Translation.kt @@ -14,7 +14,8 @@ data class Translation(val id: Int, val saveTo: String, val languageCode: String, val translator: String? = "", - @Json(name = "translatorForeign") val translatorNameLocalized: String? = "") { + @Json(name = "translatorForeign") val translatorNameLocalized: String? = "", + val displayOrder: Int = -1) { fun withSchema(schema: Int) = copy(minimumVersion = schema) } diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItem.kt b/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItem.kt index dffe644e97..b5ce16c63a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItem.kt +++ b/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItem.kt @@ -1,7 +1,8 @@ package com.quran.labs.androidquran.dao.translation data class TranslationItem @JvmOverloads constructor(val translation: Translation, - val localVersion: Int = 0) : TranslationRowData { + val localVersion: Int = 0, + val displayOrder: Int = -1) : TranslationRowData { override fun isSeparator() = false @@ -16,4 +17,6 @@ data class TranslationItem @JvmOverloads constructor(val translation: Translatio fun withTranslationRemoved() = this.copy(localVersion = 0) fun withTranslationVersion(version: Int) = this.copy(localVersion = version) + + fun withDisplayOrder(newDisplayOrder: Int) = this.copy(displayOrder = newDisplayOrder) } diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItemDisplaySort.kt b/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItemDisplaySort.kt new file mode 100644 index 0000000000..52ab77c149 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItemDisplaySort.kt @@ -0,0 +1,7 @@ +package com.quran.labs.androidquran.dao.translation + +class TranslationItemDisplaySort : Comparator { + override fun compare(p0: TranslationItem, p1: TranslationItem): Int { + return p0.displayOrder.compareTo(p1.displayOrder); + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.java b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.java index dc298f0f9a..a992af6e41 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.java +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.java @@ -72,23 +72,27 @@ public List getTranslations() { null, null, null, null, null, TranslationsTable.ID + " ASC"); if (cursor != null) { - while (cursor.moveToNext()) { - int id = cursor.getInt(0); - String name = cursor.getString(1); - String translator = cursor.getString(2); - String translatorForeign = cursor.getString(3); - String filename = cursor.getString(4); - String url = cursor.getString(5); - String languageCode = cursor.getString(6); - int version = cursor.getInt(7); - int minimumVersion = cursor.getInt(8); - - if (quranFileUtils.hasTranslation(context, filename)) { - items.add(new LocalTranslation(id, filename, name, translator, - translatorForeign, url, languageCode, version, minimumVersion)); + try { + while (cursor.moveToNext()) { + int id = cursor.getInt(0); + String name = cursor.getString(1); + String translator = cursor.getString(2); + String translatorForeign = cursor.getString(3); + String filename = cursor.getString(4); + String url = cursor.getString(5); + String languageCode = cursor.getString(6); + int version = cursor.getInt(7); + int minimumVersion = cursor.getInt(8); + int displayOrder = cursor.getInt(9); + + if (quranFileUtils.hasTranslation(context, filename)) { + items.add(new LocalTranslation(id, filename, name, translator, + translatorForeign, url, languageCode, version, minimumVersion, displayOrder)); + } } + } finally { + cursor.close(); } - cursor.close(); } items = Collections.unmodifiableList(items); if (items.size() > 0) { @@ -105,11 +109,35 @@ public void deleteTranslationByFile(String filename) { public boolean writeTranslationUpdates(List updates) { boolean result = true; db.beginTransaction(); + try { for (int i = 0, updatesSize = updates.size(); i < updatesSize; i++) { TranslationItem item = updates.get(i); if (item.exists()) { + int displayOrder = 0; + final Translation translation = item.getTranslation(); + + if (item.getDisplayOrder() > -1) { + displayOrder = item.getDisplayOrder(); + } else { + // get next highest display order + Cursor cursor = db.query( + TranslationsTable.TABLE_NAME, + new String[] {TranslationsTable.DISPLAY_ORDER}, + null, null, null, null, + TranslationsTable.DISPLAY_ORDER + " DESC", + "1" + ); + try { + if (cursor != null && cursor.moveToFirst()) { + displayOrder = cursor.getInt(0) + 1; + } + } finally { + if (cursor !=null) cursor.close(); + } + } + ContentValues values = new ContentValues(); values.put(TranslationsTable.ID, translation.getId()); values.put(TranslationsTable.NAME, translation.getDisplayName()); @@ -121,6 +149,7 @@ public boolean writeTranslationUpdates(List updates) { values.put(TranslationsTable.LANGUAGE_CODE, translation.getLanguageCode()); values.put(TranslationsTable.VERSION, item.getLocalVersion()); values.put(TranslationsTable.MINIMUM_REQUIRED_VERSION, translation.getMinimumVersion()); + values.put(TranslationsTable.DISPLAY_ORDER, displayOrder); db.replace(TranslationsTable.TABLE_NAME, null, values); } else { diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.java b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.java index c4d612d9a2..4d27b7b263 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.java +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.java @@ -1,6 +1,8 @@ package com.quran.labs.androidquran.database; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -11,7 +13,7 @@ class TranslationsDBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "translations.db"; - private static final int DB_VERSION = 4; + private static final int DB_VERSION = 5; private static final String CREATE_TRANSLATIONS_TABLE = "CREATE TABLE " + TranslationsTable.TABLE_NAME + "(" + TranslationsTable.ID + " integer primary key, " + @@ -22,7 +24,9 @@ class TranslationsDBHelper extends SQLiteOpenHelper { TranslationsTable.URL + " varchar, " + TranslationsTable.LANGUAGE_CODE + " varchar, " + TranslationsTable.VERSION + " integer not null default 0," + - TranslationsTable.MINIMUM_REQUIRED_VERSION + " integer not null default 0);"; + TranslationsTable.MINIMUM_REQUIRED_VERSION + " integer not null default 0, " + + TranslationsTable.DISPLAY_ORDER + " integer not null default -1 " + + ");"; @Inject TranslationsDBHelper(Context context) { @@ -69,6 +73,41 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.endTransaction(); } } + if (oldVersion < 5) { + // Add display order column and add arbitrary order to existing translations + db.beginTransaction(); + try { + db.execSQL("ALTER TABLE " + + TranslationsTable.TABLE_NAME + + " ADD COLUMN " + + TranslationsTable.DISPLAY_ORDER + + " integer not null default -1" + ); + Cursor translations = db.query( + TranslationsTable.TABLE_NAME, new String[] { TranslationsTable.ID }, null, null, null, null, null + ); + try { + if (translations != null && translations.moveToFirst()) { + for (int i = 0; i < translations.getCount(); i++) { + ContentValues values = new ContentValues(); + values.put(TranslationsTable.DISPLAY_ORDER, i); + db.update( + TranslationsTable.TABLE_NAME, + values, + TranslationsTable.ID + " = ?", + new String[] { String.valueOf(translations.getInt(0)) } + ); + translations.moveToNext(); + } + } + } finally { + if (translations != null) translations.close(); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } } static class TranslationsTable { @@ -82,5 +121,6 @@ static class TranslationsTable { static final String LANGUAGE_CODE = "languageCode"; static final String VERSION = "version"; static final String MINIMUM_REQUIRED_VERSION = "minimumRequiredVersion"; + static final String DISPLAY_ORDER = "userDisplayOrder"; } } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt index 1d990e626c..14e7cec422 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenter.kt @@ -99,14 +99,16 @@ internal open class BaseTranslationPresenter internal constructor( if (element != null) { // replace with "" when a translation doesn't load to keep translations aligned val ayahTranslations = quranTexts.mapIndexed { index: Int, quranText: QuranText? -> - val translationMinVersion = translationInfo.getOrNull(index)?.minimumVersion ?: 0 + val translation = translationInfo.getOrNull(index); + val translationMinVersion = translation?.minimumVersion ?: 0 + val translationId = translation?.id ?: -1; val shouldProcess = translationMinVersion >= TranslationUtil.MINIMUM_PROCESSING_VERSION val text = quranText ?: QuranText(element.sura, element.ayah, "") if (shouldProcess) { - translationUtil.parseTranslationText(text) + translationUtil.parseTranslationText(text, translationId) } else { - TranslationMetadata(element.sura, element.ayah, text.text) + TranslationMetadata(element.sura, element.ayah, text.text, translationId) } } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java index 648c4945eb..e489c83b82 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java @@ -248,7 +248,7 @@ private List mergeWithServerTranslations(List serv override = new TranslationItem(translation.withSchema(versions.second), versions.first); } } else { - item = new TranslationItem(translation, local.getVersion()); + item = new TranslationItem(translation, local.getVersion(), local.getDisplayOrder()); } } else { item = new TranslationItem(translation); diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java index b29518ec92..3a51f9af9f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/PagerActivity.java @@ -37,6 +37,7 @@ import com.quran.labs.androidquran.QuranPreferenceActivity; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.SearchActivity; +import com.quran.labs.androidquran.common.LocalTranslationDisplaySort; import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.common.audio.QariItem; import com.quran.labs.androidquran.di.component.activity.PagerActivityComponent; @@ -86,6 +87,8 @@ import com.quran.labs.androidquran.widgets.SlidingUpPanelLayout; import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -1354,10 +1357,13 @@ private void requestTranslationsList() { .subscribeWith(new DisposableSingleObserver>() { @Override public void onSuccess(List translationList) { - int items = translationList.size(); + final List sortedTranslations = new ArrayList<>(translationList); + Collections.sort(sortedTranslations, new LocalTranslationDisplaySort()); + + int items = sortedTranslations.size(); String[] titles = new String[items]; for (int i = 0; i < items; i++) { - LocalTranslation item = translationList.get(i); + LocalTranslation item = sortedTranslations.get(i); if (!TextUtils.isEmpty(item.getTranslatorForeign())) { titles[i] = item.getTranslatorForeign(); } else if (!TextUtils.isEmpty(item.getTranslator())) { @@ -1371,16 +1377,16 @@ public void onSuccess(List translationList) { if (currentActiveTranslations.isEmpty() && items > 0) { currentActiveTranslations = new HashSet<>(); for (int i = 0; i < items; i++) { - currentActiveTranslations.add(translationList.get(i).getFilename()); + currentActiveTranslations.add(sortedTranslations.get(i).getFilename()); } } activeTranslations = currentActiveTranslations; if (translationsSpinnerAdapter != null) { - translationsSpinnerAdapter.updateItems(titles, translationList, activeTranslations); + translationsSpinnerAdapter.updateItems(titles, sortedTranslations, activeTranslations); } translationItems = titles; - translations = translationList; + translations = sortedTranslations; if (showingTranslation) { // Since translation items have changed, need to diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java index ce23c5de96..d8427ed1b8 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java @@ -6,6 +6,8 @@ import android.content.IntentFilter; import android.os.Bundle; import android.util.SparseIntArray; +import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import com.quran.labs.androidquran.QuranApplication; @@ -13,6 +15,7 @@ import com.quran.labs.androidquran.dao.translation.Translation; import com.quran.labs.androidquran.dao.translation.TranslationHeader; import com.quran.labs.androidquran.dao.translation.TranslationItem; +import com.quran.labs.androidquran.dao.translation.TranslationItemDisplaySort; import com.quran.labs.androidquran.dao.translation.TranslationRowData; import com.quran.labs.androidquran.database.DatabaseHandler; import com.quran.labs.androidquran.presenter.translation.TranslationManagerPresenter; @@ -20,12 +23,15 @@ import com.quran.labs.androidquran.service.util.DefaultDownloadReceiver; import com.quran.labs.androidquran.service.util.QuranDownloadNotifier; import com.quran.labs.androidquran.service.util.ServiceIntentHelper; +import com.quran.labs.androidquran.ui.adapter.DownloadedItemActionListener; +import com.quran.labs.androidquran.ui.adapter.DownloadedMenuActionListener; import com.quran.labs.androidquran.ui.adapter.TranslationsAdapter; import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.labs.androidquran.util.QuranSettings; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -33,6 +39,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.ActionMode; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -41,7 +48,7 @@ import timber.log.Timber; public class TranslationManagerActivity extends QuranActionBarActivity - implements DefaultDownloadReceiver.SimpleDownloadListener { + implements DefaultDownloadReceiver.SimpleDownloadListener, DownloadedMenuActionListener { public static final String TRANSLATION_DOWNLOAD_KEY = "TRANSLATION_DOWNLOAD_KEY"; private static final String UPGRADING_EXTENSION = ".old"; @@ -57,6 +64,12 @@ public class TranslationManagerActivity extends QuranActionBarActivity private Disposable onClickDownloadDisposable; private Disposable onClickRemoveDisposable; + private Disposable onClickRankUpDisposable; + private Disposable onClickRankDownDisposable; + + private ActionMode actionMode; + private TranslationSelectionListener selectionListener; + private DownloadedItemActionListener downloadedItemActionListener; @Inject TranslationManagerPresenter presenter; @Inject QuranFileUtils quranFileUtils; @@ -77,6 +90,7 @@ public void onCreate(Bundle savedInstanceState) { adapter = new TranslationsAdapter(this); translationRecycler.setAdapter(adapter); + selectionListener = new TranslationSelectionListener(adapter); databaseDirectory = quranFileUtils.getQuranDatabaseDirectory(this); @@ -89,6 +103,8 @@ public void onCreate(Bundle savedInstanceState) { quranSettings = QuranSettings.getInstance(this); onClickDownloadDisposable = adapter.getOnClickDownloadSubject().subscribe(this::downloadItem); onClickRemoveDisposable = adapter.getOnClickRemoveSubject().subscribe(this::removeItem); + onClickRankUpDisposable = adapter.getOnClickRankUpSubject().subscribe(this::rankUpItem); + onClickRankDownDisposable = adapter.getOnClickRankDownSubject().subscribe(this::rankDownItem); translationSwipeRefresh.setOnRefreshListener(this::onRefresh); presenter.bind(this); @@ -112,6 +128,8 @@ protected void onDestroy() { presenter.unbind(this); onClickDownloadDisposable.dispose(); onClickRemoveDisposable.dispose(); + onClickRankUpDisposable.dispose(); + onClickRankDownDisposable.dispose(); super.onDestroy(); } @@ -140,8 +158,13 @@ public void handleDownloadSuccess() { } } + List sortedItems = sortedDownloadedItems(); + int lastDisplayOrder = sortedItems.get(sortedItems.size() - 1).getDisplayOrder(); TranslationItem updated = downloadingItem.withTranslationVersion( - downloadingItem.getTranslation().getCurrentVersion()); + downloadingItem.getTranslation().getCurrentVersion() + ).withDisplayOrder( + lastDisplayOrder + 1 + ); updateTranslationItem(updated); // update active translations and add this item to it @@ -228,9 +251,12 @@ private void generateListItems() { TranslationHeader hdr = new TranslationHeader(getString(R.string.downloaded_translations)); result.add(hdr); + Collections.sort(downloaded, new TranslationItemDisplaySort ()); + boolean needsUpgrade = false; for (TranslationItem item : downloaded) { result.add(item); + Timber.i("aaa translation " + item.name () + " order " + item.getDisplayOrder ()); needsUpgrade = needsUpgrade || item.needsUpgrade(); } @@ -330,6 +356,86 @@ private void removeItem(final TranslationRowData translationRowData) { builder.show(); } + private List sortedDownloadedItems() { + final ArrayList result = new ArrayList<>(); + for (TranslationItem item : allItems) { + if (item.exists()) result.add(item); + } + Collections.sort(result, new TranslationItemDisplaySort ()); + return result; + } + + private void rankDownItem(TranslationRowData targetRow) { + TranslationItem targetItem = (TranslationItem) targetRow; + List sortedDownloads = sortedDownloadedItems(); + if (sortedDownloads.indexOf(targetItem) + 1 < sortedDownloads.size()) { // ignore last item in list + TranslationItem updatedItem = targetItem.withDisplayOrder(targetItem.getDisplayOrder() + 1); + ArrayList toUpdate = new ArrayList<>(); + toUpdate.add(updatedItem); + TranslationItem swapItem = null; + for(TranslationItem translationItem : sortedDownloads){ + if (translationItem.getDisplayOrder() == updatedItem.getDisplayOrder()) { + swapItem = translationItem; + break; + } + } + if (swapItem != null) { // swap item order + if (swapItem.getDisplayOrder() > 0) { + toUpdate.add(swapItem.withDisplayOrder(swapItem.getDisplayOrder() - 1)); + } + } else { // shift item order for higher items + for (TranslationItem translationItem : sortedDownloads) { + if (translationItem.getDisplayOrder() > -1 && translationItem.getDisplayOrder() < updatedItem.getDisplayOrder()) { + toUpdate.add(translationItem.withDisplayOrder(translationItem.getDisplayOrder() + 1)); + } + } + } + if (!toUpdate.isEmpty()) { + if (selectionListener != null) selectionListener.handleSelection(updatedItem); + for(TranslationItem toUpdateItem : toUpdate){ + updateTranslationItem(toUpdateItem); + } + generateListItems(); + } + } + } + + private void rankUpItem(TranslationRowData targetRow) { + TranslationItem targetItem = (TranslationItem) targetRow; + List sortedDownloads = sortedDownloadedItems(); + if (sortedDownloads.indexOf(targetItem) > 0) { // ignore first item in list + ArrayList toUpdate = new ArrayList<>(); + int updatedDisplayOrder = targetItem.getDisplayOrder(); + TranslationItem updatedItem = null; + if (updatedDisplayOrder > 0) { + updatedItem = targetItem.withDisplayOrder(--updatedDisplayOrder); + toUpdate.add(updatedItem); + } + TranslationItem swapItem = null; + for (TranslationItem translationItem : sortedDownloads) { + if (translationItem.getDisplayOrder() == updatedDisplayOrder) { + swapItem = translationItem; + } + } + if (swapItem != null) { // swap item order + toUpdate.add(swapItem.withDisplayOrder(swapItem.getDisplayOrder() + 1)); + } else { // shift item order for lower items + for (TranslationItem translationItem : sortedDownloads) { + if (translationItem.getDisplayOrder() > updatedDisplayOrder) { + toUpdate.add(translationItem.withDisplayOrder(translationItem.getDisplayOrder() - 1)); + } + } + } + if (!toUpdate.isEmpty()) { + if (selectionListener != null && updatedItem != null) selectionListener.handleSelection(updatedItem); + for (TranslationItem toUpdateItem : toUpdate) { + updateTranslationItem(toUpdateItem); + } + generateListItems(); + } + } + } + private boolean removeTranslation(String fileName) { String path = quranFileUtils.getQuranDatabaseDirectory(TranslationManagerActivity.this); if (path != null) { @@ -340,4 +446,86 @@ private boolean removeTranslation(String fileName) { return false; } + @Override + public void startMenuAction(TranslationItem item, DownloadedItemActionListener aDownloadedItemActionListener) { + downloadedItemActionListener = aDownloadedItemActionListener; + if (actionMode != null) { + actionMode.finish(); + selectionListener.clearSelection(); + } else { + selectionListener.handleSelection(item); + actionMode = startSupportActionMode(new ModeCallback()); + } + } + + @Override + public void finishMenuAction() { + if (actionMode != null) { + actionMode.finish(); + } + selectionListener.clearSelection(); + downloadedItemActionListener = null; + } + + class TranslationSelectionListener { + private final TranslationsAdapter adapter; + + TranslationSelectionListener(TranslationsAdapter anAdapter) { + adapter = anAdapter; + } + + void handleSelection(TranslationItem item) { + adapter.setSelectedItem(item); + } + + void clearSelection() { + adapter.setSelectedItem(null); + } + } + + private class ModeCallback implements ActionMode.Callback { + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.downloaded_translation_menu, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch(item.getItemId()) { + case R.id.dtm_delete: + if (downloadedItemActionListener != null) downloadedItemActionListener.handleDeleteItemAction(); + endAction(); + break; + case R.id.dtm_move_up: + if (downloadedItemActionListener != null) downloadedItemActionListener.handleRankUpItemAction(); + break; + case R.id.dtm_move_down: + if (downloadedItemActionListener != null) downloadedItemActionListener.handleRankDownItemAction(); + break; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + if (mode == actionMode) { + selectionListener.clearSelection(); + actionMode = null; + } + } + + private void endAction() { + if (actionMode != null) { + selectionListener.clearSelection(); + actionMode.finish(); + } + } + } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/adapter/DownloadedItemActionListener.java b/app/src/main/java/com/quran/labs/androidquran/ui/adapter/DownloadedItemActionListener.java new file mode 100644 index 0000000000..9b97cdbd9e --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/adapter/DownloadedItemActionListener.java @@ -0,0 +1,7 @@ +package com.quran.labs.androidquran.ui.adapter; + +public interface DownloadedItemActionListener { + void handleDeleteItemAction(); + void handleRankUpItemAction(); + void handleRankDownItemAction(); +} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/adapter/DownloadedMenuActionListener.java b/app/src/main/java/com/quran/labs/androidquran/ui/adapter/DownloadedMenuActionListener.java new file mode 100644 index 0000000000..8fe54cff03 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/adapter/DownloadedMenuActionListener.java @@ -0,0 +1,8 @@ +package com.quran.labs.androidquran.ui.adapter; + +import com.quran.labs.androidquran.dao.translation.TranslationItem; + +public interface DownloadedMenuActionListener { + void startMenuAction(TranslationItem item, DownloadedItemActionListener downloadedItemActionListener); + void finishMenuAction(); +} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/adapter/TranslationsAdapter.java b/app/src/main/java/com/quran/labs/androidquran/ui/adapter/TranslationsAdapter.java index 7c44c099e4..5ab56d42d7 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/adapter/TranslationsAdapter.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/adapter/TranslationsAdapter.java @@ -1,6 +1,5 @@ package com.quran.labs.androidquran.ui.adapter; -import android.content.Context; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -26,12 +25,19 @@ public class TranslationsAdapter extends RecyclerView.Adapter onClickDownloadSubject = UnicastSubject.create(); private final UnicastSubject onClickRemoveSubject = UnicastSubject.create(); + private final UnicastSubject onClickRankUpSubject = UnicastSubject.create(); + private final UnicastSubject onClickRankDownSubject = UnicastSubject.create(); + + private final DownloadedMenuActionListener downloadedMenuActionListener; + private final DownloadedItemActionListener downloadedItemActionListener; private List translations = new ArrayList<>(); - private Context context; - public TranslationsAdapter(Context context) { - this.context = context; + private TranslationItem selectedItem; + + public TranslationsAdapter(DownloadedMenuActionListener aDownloadedMenuActionListener) { + this.downloadedMenuActionListener = aDownloadedMenuActionListener; + this.downloadedItemActionListener = new DownloadedItemActionListenerImpl(); } @NotNull @@ -47,6 +53,10 @@ public void onBindViewHolder(@NotNull TranslationViewHolder holder, int position switch (holder.getItemViewType()) { case R.layout.translation_row: TranslationItem item = (TranslationItem) rowItem; + holder.setItem(item); + holder.itemView.setActivated( + (selectedItem != null) && (item.getTranslation().getId() == selectedItem.getTranslation().getId()) + ); holder.getTranslationTitle().setText(item.name()); if (TextUtils.isEmpty(item.getTranslation().getTranslatorNameLocalized())) { holder.getTranslationInfo().setText(item.getTranslation().getTranslator()); @@ -58,6 +68,8 @@ public void onBindViewHolder(@NotNull TranslationViewHolder holder, int position ImageView rightImage = holder.getRightImage(); if (item.exists()) { + rightImage.setVisibility(View.GONE); + holder.itemView.setOnLongClickListener(holder.actionMenuListener); if (item.needsUpgrade()) { leftImage.setImageResource(R.drawable.ic_download); leftImage.setVisibility(View.VISIBLE); @@ -65,10 +77,6 @@ public void onBindViewHolder(@NotNull TranslationViewHolder holder, int position } else { leftImage.setVisibility(View.GONE); } - rightImage.setImageResource(R.drawable.ic_cancel); - rightImage.setOnClickListener(holder.removeListener); - rightImage.setVisibility(View.VISIBLE); - rightImage.setContentDescription(context.getString(R.string.remove_button)); } else { leftImage.setVisibility(View.GONE); rightImage.setImageResource(R.drawable.ic_download); @@ -80,6 +88,7 @@ public void onBindViewHolder(@NotNull TranslationViewHolder holder, int position } break; case R.layout.translation_sep: + holder.itemView.setActivated(false); holder.getSeparatorText().setText(rowItem.name()); break; } @@ -104,6 +113,14 @@ public Observable getOnClickRemoveSubject() { return onClickRemoveSubject.hide(); } + public Observable getOnClickRankUpSubject() { + return onClickRankUpSubject.hide(); + } + + public Observable getOnClickRankDownSubject() { + return onClickRankDownSubject.hide(); + } + public void setTranslations(List data) { this.translations = data; } @@ -112,6 +129,25 @@ public List getTranslations() { return translations; } + public void setSelectedItem ( TranslationItem selectedItem ) { + this.selectedItem = selectedItem; + notifyDataSetChanged(); + } + + class DownloadedItemActionListenerImpl implements DownloadedItemActionListener { + public void handleDeleteItemAction() { + onClickRemoveSubject.onNext(selectedItem); + } + + public void handleRankUpItemAction() { + onClickRankUpSubject.onNext(selectedItem); + } + + public void handleRankDownItemAction() { + onClickRankDownSubject.onNext(selectedItem); + } + } + class TranslationViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { @Nullable TextView translationTitle; @@ -119,6 +155,7 @@ class TranslationViewHolder extends RecyclerView.ViewHolder implements View.OnCl @Nullable ImageView leftImage; @Nullable ImageView rightImage; @Nullable TextView separatorText; + @Nullable TranslationItem item; TranslationViewHolder(View itemView, int viewType) { super(itemView); @@ -132,6 +169,10 @@ class TranslationViewHolder extends RecyclerView.ViewHolder implements View.OnCl } } + void setItem ( @Nullable TranslationItem item ) { + this.item = item; + } + TextView getSeparatorText() { return separatorText; } @@ -152,17 +193,15 @@ ImageView getRightImage() { return rightImage; } - final View.OnClickListener removeListener = v -> { - TranslationItem item = (TranslationItem) translations.get(getAdapterPosition()); - onClickRemoveSubject.onNext(item); + final View.OnLongClickListener actionMenuListener = v -> { + downloadedMenuActionListener.startMenuAction(item, downloadedItemActionListener); + return true; }; @Override public void onClick(View v) { - TranslationItem item = (TranslationItem) translations.get(getAdapterPosition()); - if (item.exists() && !item.needsUpgrade()) { - onClickRemoveSubject.onNext(item); - } else { + downloadedMenuActionListener.finishMenuAction(); + if (!item.exists() || item.needsUpgrade()) { onClickDownloadSubject.onNext(item); } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationView.java b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationView.java index bc86d38e38..e720d4c6be 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationView.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationView.java @@ -9,6 +9,7 @@ import com.quran.labs.androidquran.BuildConfig; import com.quran.labs.androidquran.R; +import com.quran.labs.androidquran.common.LocalTranslationDisplaySort; import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.common.TranslationMetadata; @@ -19,6 +20,7 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import androidx.annotation.NonNull; @@ -107,22 +109,23 @@ public void setVerses(@NonNull QuranDisplayData quranDisplayData, rows.add(new TranslationViewRow(TranslationViewRow.Type.QURAN_TEXT, verse)); } - // added this to guard against a crash that happened when verse.texts was empty - int verseTexts = verse.texts.size(); - for (int j = 0; j < translations.length; j++) { - final TranslationMetadata metadata = verseTexts > j ? verse.texts.get(j) : null; + final LocalTranslation[] sortedTranslations = Arrays.copyOf(this.translations, this.translations.length); + Arrays.sort(sortedTranslations, new LocalTranslationDisplaySort()); + + for (int j = 0; j < sortedTranslations.length; j++) { + final TranslationMetadata metadata = findText(verse.texts, sortedTranslations[j].getId()); CharSequence text = metadata != null ? metadata.getText() : ""; if (!TextUtils.isEmpty(text)) { if (wantTranslationHeaders) { rows.add( new TranslationViewRow(TranslationViewRow.Type.TRANSLATOR, verse, - translations[j].getTranslatorName())); + sortedTranslations[j].getTranslatorName())); } rows.add(new TranslationViewRow( TranslationViewRow.Type.TRANSLATION_TEXT, verse, text, j, metadata == null ? null : metadata.getLink(), - "ar".equals(translations[j].getLanguageCode()))); + "ar".equals(sortedTranslations[j].getLanguageCode()))); } } @@ -179,6 +182,15 @@ public void onClick(View v) { } } + private TranslationMetadata findText(List texts, Integer translationId) { + for (TranslationMetadata text : texts) { + if (translationId.equals(text.getLocalTranslationId())) { + return text; + } + } + return null; + } + /** * This method updates the toolbar position when an ayah is selected * This method is called from the onScroll listener, and as thus must make sure not to ask diff --git a/app/src/main/java/com/quran/labs/androidquran/util/TranslationUtil.kt b/app/src/main/java/com/quran/labs/androidquran/util/TranslationUtil.kt index 90798b8a58..9ee51ac8ec 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/TranslationUtil.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/TranslationUtil.kt @@ -14,7 +14,7 @@ open class TranslationUtil(@ColorInt private val color: Int, private val quranInfo: QuranInfo ) { - open fun parseTranslationText(quranText: QuranText): TranslationMetadata { + open fun parseTranslationText(quranText: QuranText, translationId: Int): TranslationMetadata { val text = quranText.text val hyperlinkId = getHyperlinkAyahId(quranText) @@ -39,7 +39,7 @@ open class TranslationUtil(@ColorInt private val color: Int, val range = it.range spannable.setSpan(span, range.start, range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } - return TranslationMetadata(quranText.sura, quranText.ayah, spannable, suraAyah) + return TranslationMetadata(quranText.sura, quranText.ayah, spannable, translationId, suraAyah) } companion object { diff --git a/app/src/main/res/drawable-hdpi/arrow_circle_down.png b/app/src/main/res/drawable-hdpi/arrow_circle_down.png new file mode 100755 index 0000000000..15ac3d5340 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/arrow_circle_down.png differ diff --git a/app/src/main/res/drawable-hdpi/arrow_circle_up.png b/app/src/main/res/drawable-hdpi/arrow_circle_up.png new file mode 100755 index 0000000000..68be2f51a6 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/arrow_circle_up.png differ diff --git a/app/src/main/res/drawable-mdpi/arrow_circle_down.png b/app/src/main/res/drawable-mdpi/arrow_circle_down.png new file mode 100755 index 0000000000..2bb16c38c0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/arrow_circle_down.png differ diff --git a/app/src/main/res/drawable-mdpi/arrow_circle_up.png b/app/src/main/res/drawable-mdpi/arrow_circle_up.png new file mode 100755 index 0000000000..6848e0a82a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/arrow_circle_up.png differ diff --git a/app/src/main/res/drawable-xhdpi/arrow_circle_down.png b/app/src/main/res/drawable-xhdpi/arrow_circle_down.png new file mode 100755 index 0000000000..fd6a27c9a6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/arrow_circle_down.png differ diff --git a/app/src/main/res/drawable-xhdpi/arrow_circle_up.png b/app/src/main/res/drawable-xhdpi/arrow_circle_up.png new file mode 100755 index 0000000000..a4a1ee111a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/arrow_circle_up.png differ diff --git a/app/src/main/res/drawable-xxhdpi/arrow_circle_down.png b/app/src/main/res/drawable-xxhdpi/arrow_circle_down.png new file mode 100755 index 0000000000..b41fb39a05 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/arrow_circle_down.png differ diff --git a/app/src/main/res/drawable-xxhdpi/arrow_circle_up.png b/app/src/main/res/drawable-xxhdpi/arrow_circle_up.png new file mode 100755 index 0000000000..1378ef5243 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/arrow_circle_up.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/arrow_circle_down.png b/app/src/main/res/drawable-xxxhdpi/arrow_circle_down.png new file mode 100755 index 0000000000..92509da09d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/arrow_circle_down.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/arrow_circle_up.png b/app/src/main/res/drawable-xxxhdpi/arrow_circle_up.png new file mode 100755 index 0000000000..22433dc5e1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/arrow_circle_up.png differ diff --git a/app/src/main/res/drawable/translation_row_background.xml b/app/src/main/res/drawable/translation_row_background.xml new file mode 100644 index 0000000000..a9ee399f8d --- /dev/null +++ b/app/src/main/res/drawable/translation_row_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/layout/translation_row.xml b/app/src/main/res/layout/translation_row.xml index b2272cf409..2877f06e5a 100644 --- a/app/src/main/res/layout/translation_row.xml +++ b/app/src/main/res/layout/translation_row.xml @@ -8,7 +8,7 @@ android:paddingTop="4dp" android:paddingBottom="4dp" android:orientation="horizontal" - android:background="?attr/selectableItemBackground" + android:background="@drawable/translation_row_background" > + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 03f0b13134..1709cff7f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -237,6 +237,10 @@ Error exporting data Data exported to %1$s + Move Up + Move Down + Remove Translation + quranandroid+logs@gmail.com Warning Due to Android limitations, if you choose to place Quran diff --git a/app/src/test/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenterTest.kt b/app/src/test/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenterTest.kt index 95d7845d18..bd226b55e2 100644 --- a/app/src/test/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenterTest.kt +++ b/app/src/test/java/com/quran/labs/androidquran/presenter/translation/BaseTranslationPresenterTest.kt @@ -33,8 +33,8 @@ class BaseTranslationPresenterTest { MadaniDataSource() ) ) { - override fun parseTranslationText(quranText: QuranText): TranslationMetadata { - return TranslationMetadata(quranText.sura, quranText.ayah, quranText.text) + override fun parseTranslationText(quranText: QuranText, translationId: Int): TranslationMetadata { + return TranslationMetadata(quranText.sura, quranText.ayah, quranText.text, translationId) } }, QuranInfo(MadaniDataSource()) @@ -48,7 +48,7 @@ class BaseTranslationPresenterTest { init { put("one.db", LocalTranslation(1, "one.db", "One", "First", null, "", null, 1, 2)) put("two.db", LocalTranslation(2, "two.db", "Two", "Second", null, "", null, 1, 2)) - put("three.db", LocalTranslation(2, "three.db", "Three", "Third", null, "", null, 1, 2)) + put("three.db", LocalTranslation(3, "three.db", "Three", "Third", null, "", null, 1, 2)) } } @@ -74,7 +74,8 @@ class BaseTranslationPresenterTest { val verseRange = VerseRange(1, 1, 1, 1, 1) val arabic = listOf(QuranText(1, 1, "first ayah")) val info = presenter.combineAyahData(verseRange, arabic, - listOf(listOf(QuranText(1, 1, "translation"))), emptyArray()) + listOf(listOf(QuranText(1, 1, "translation"))), + arrayOf(LocalTranslation(1, "one.db", "One", "First", null, "", null, 1, 2))) assertThat(info).hasSize(1) val first = info[0] @@ -83,6 +84,7 @@ class BaseTranslationPresenterTest { assertThat(first.texts).hasSize(1) assertThat(first.arabicText).isEqualTo("first ayah") assertThat(first.texts[0].text).isEqualTo("translation") + assertThat(first.texts[0].localTranslationId).isEqualTo(1) } @Test