From 4e0e54936b5552377df80fb7cab8e1fbe65386c0 Mon Sep 17 00:00:00 2001 From: MahmouMabrok Date: Thu, 7 Jan 2021 22:12:46 +0200 Subject: [PATCH 001/258] feat: add speedup & down to sound player UI --- .../androidquran/service/AudioService.java | 90 ++++-- .../labs/androidquran/ui/PagerActivity.java | 15 + .../androidquran/view/AudioStatusBar.java | 303 ++++++++++-------- app/src/main/res/drawable/ic_neg_1_24.xml | 10 + app/src/main/res/drawable/ic_plus_1_24.xml | 10 + 5 files changed, 268 insertions(+), 160 deletions(-) create mode 100644 app/src/main/res/drawable/ic_neg_1_24.xml create mode 100644 app/src/main/res/drawable/ic_plus_1_24.xml diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.java b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.java index ac39521bb9..b5f1f4c694 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.java +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.java @@ -1,8 +1,8 @@ -/* +/* * This code is based on the RandomMusicPlayer example from * the Android Open Source Project samples. It has been modified * for use in Quran Android. - * + * * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,6 +41,7 @@ import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnPreparedListener; +import android.media.PlaybackParams; import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.AsyncTask; @@ -55,19 +56,21 @@ import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.SparseIntArray; + import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.media.app.NotificationCompat.MediaStyle; import androidx.media.session.MediaButtonReceiver; + import com.quran.data.core.QuranInfo; +import com.quran.data.model.SuraAyah; import com.quran.labs.androidquran.QuranApplication; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.dao.audio.AudioPlaybackInfo; import com.quran.labs.androidquran.dao.audio.AudioRequest; import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.data.QuranDisplayData; -import com.quran.data.model.SuraAyah; import com.quran.labs.androidquran.database.DatabaseUtils; import com.quran.labs.androidquran.database.SuraTimingDatabaseHandler; import com.quran.labs.androidquran.extension.SuraAyahExtensionKt; @@ -78,12 +81,15 @@ import com.quran.labs.androidquran.ui.PagerActivity; import com.quran.labs.androidquran.util.AudioUtils; import com.quran.labs.androidquran.util.NotificationChannelUtil; -import io.reactivex.Maybe; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; + import java.io.File; import java.io.IOException; + import javax.inject.Inject; + +import io.reactivex.Maybe; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import timber.log.Timber; /** @@ -104,6 +110,8 @@ public class AudioService extends Service implements OnCompletionListener, public static final String ACTION_PAUSE = "com.quran.labs.androidquran.action.PAUSE"; public static final String ACTION_STOP = "com.quran.labs.androidquran.action.STOP"; public static final String ACTION_SKIP = "com.quran.labs.androidquran.action.SKIP"; + public static final String ACTION_INCREASE_SPEAD = "com.quran.labs.androidquran.action.INCREASE_SPEAD"; + public static final String ACTION_DECREASE_SPEAD = "com.quran.labs.androidquran.action.DECREASE_SPEAD"; public static final String ACTION_REWIND = "com.quran.labs.androidquran.action.REWIND"; public static final String ACTION_CONNECT = "com.quran.labs.androidquran.action.CONNECT"; public static final String ACTION_UPDATE_REPEAT = "com.quran.labs.androidquran.action.UPDATE_REPEAT"; @@ -210,9 +218,12 @@ private enum AudioFocus { private AsyncTask timingTask = null; private final CompositeDisposable compositeDisposable = new CompositeDisposable(); - @Inject QuranInfo quranInfo; - @Inject QuranDisplayData quranDisplayData; - @Inject AudioUtils audioUtils; + @Inject + QuranInfo quranInfo; + @Inject + QuranDisplayData quranDisplayData; + @Inject + AudioUtils audioUtils; private static final int MSG_INCOMING = 1; private static final int MSG_START_AUDIO = 2; @@ -319,8 +330,8 @@ public void onCreate() { compositeDisposable.add( Maybe.fromCallable(this::generateNotificationIcon) - .subscribeOn(Schedulers.io()) - .subscribe(bitmap -> notificationIcon = bitmap)); + .subscribeOn(Schedulers.io()) + .subscribe(bitmap -> notificationIcon = bitmap)); } private class MediaSessionCallback extends MediaSessionCompat.Callback { @@ -432,6 +443,10 @@ private void handleIntent(Intent intent) { processStopRequest(); } else if (ACTION_REWIND.equals(action)) { processRewindRequest(); + } else if (ACTION_INCREASE_SPEAD.equals(action)) { + processIncreasePlayback(); + } else if (ACTION_DECREASE_SPEAD.equals(action)) { + processDecreasePlayback(); } else if (ACTION_UPDATE_REPEAT.equals(action)) { final AudioRequest playInfo = intent.getParcelableExtra(EXTRA_PLAY_INFO); if (playInfo != null && audioQueue != null) { @@ -736,6 +751,19 @@ private void processRewindRequest() { } } + private void processIncreasePlayback() { + if (State.Playing == state) { + increasePlayback(); + } + } + + private void processDecreasePlayback() { + if (State.Playing == state) { + decreasePlayback(); + } + } + + private void processSkipRequest() { if (audioRequest == null) { return; @@ -928,6 +956,22 @@ private void configAndStartMediaPlayer(boolean canSeek) { } } + private void increasePlayback() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PlaybackParams params = player.getPlaybackParams(); + params.setSpeed(params.getSpeed() + 0.15f); + player.setPlaybackParams(params); + } + } + + private void decreasePlayback() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PlaybackParams params = player.getPlaybackParams(); + params.setSpeed(params.getSpeed() - 0.15f); + player.setPlaybackParams(params); + } + } + private void tryToGetAudioFocus() { if (audioFocus != AudioFocus.Focused && audioFocusHelper != null && audioFocusHelper.requestFocus()) { @@ -1065,12 +1109,12 @@ private void setState(int state) { builder.setState(state, position, 1.0f); builder.setActions( PlaybackStateCompat.ACTION_PLAY | - PlaybackStateCompat.ACTION_STOP | - PlaybackStateCompat.ACTION_REWIND | - PlaybackStateCompat.ACTION_FAST_FORWARD | - PlaybackStateCompat.ACTION_PAUSE | - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | - PlaybackStateCompat.ACTION_SKIP_TO_NEXT); + PlaybackStateCompat.ACTION_STOP | + PlaybackStateCompat.ACTION_REWIND | + PlaybackStateCompat.ACTION_FAST_FORWARD | + PlaybackStateCompat.ACTION_PAUSE | + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | + PlaybackStateCompat.ACTION_SKIP_TO_NEXT); mediaSession.setPlaybackState(builder.build()); } @@ -1083,7 +1127,9 @@ public void onSeekComplete(MediaPlayer mediaPlayer) { serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200); } - /** Called when media player is done playing current file. */ + /** + * Called when media player is done playing current file. + */ @Override public void onCompletion(MediaPlayer player) { // The media player finished playing the current file, so @@ -1112,7 +1158,9 @@ public void onCompletion(MediaPlayer player) { } } - /** Called when media player is done preparing. */ + /** + * Called when media player is done preparing. + */ @Override public void onPrepared(MediaPlayer player) { Timber.d("okay, prepared!"); @@ -1145,7 +1193,9 @@ public void onPrepared(MediaPlayer player) { configAndStartMediaPlayer(); } - /** Updates the notification. */ + /** + * Updates the notification. + */ void updateNotification() { notificationBuilder.setContentText(getTitle()); if (!didSetNotificationIconOnNotificationBuilder && notificationIcon != null) { 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 cac29b10b6..290e067a49 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 @@ -1574,6 +1574,21 @@ public void onPausePressed() { audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE); } + @Override + public void onUpPressed() { + startService(audioUtils.getAudioIntent( + this, AudioService.ACTION_INCREASE_SPEAD)); + // audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE); + } + + @Override + public void onDownPressed() { + startService(audioUtils.getAudioIntent( + this, AudioService.ACTION_DECREASE_SPEAD)); + // audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE); + } + + @Override public void onNextPressed() { startService(audioUtils.getAudioIntent(this, diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index c761848770..56d9093299 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -20,6 +20,12 @@ import android.widget.ProgressBar; import android.widget.TextView; +import androidx.annotation.DrawableRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.core.view.ViewCompat; + import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.common.audio.QariItem; import com.quran.labs.androidquran.data.Constants; @@ -28,12 +34,6 @@ import java.util.List; -import androidx.annotation.DrawableRes; -import androidx.annotation.LayoutRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.core.view.ViewCompat; - public class AudioStatusBar extends LeftToRightLinearLayout { public static final int STOPPED_MODE = 1; @@ -43,44 +43,85 @@ public class AudioStatusBar extends LeftToRightLinearLayout { public static final int PAUSED_MODE = 5; public static final int PROMPT_DOWNLOAD_MODE = 6; - private Context context; + private final Context context; private int currentMode; - private int buttonWidth; - private int separatorWidth; - private int separatorSpacing; - private int textFontSize; - private int textFullFontSize; - private int spinnerPadding; + private final int buttonWidth; + private final int separatorWidth; + private final int separatorSpacing; + private final int textFontSize; + private final int textFullFontSize; + private final int spinnerPadding; private QariAdapter adapter; private int currentQari; private int currentRepeat = 0; - @DrawableRes private int itemBackground; - private boolean isRtl; + private final boolean isRtl; + private final SharedPreferences sharedPreferences; private boolean isDualPageMode; private boolean hasErrorText; private boolean haveCriticalError = false; - private SharedPreferences sharedPreferences; + private final int[] repeatValues = {0, 1, 2, 3, -1}; private QuranSpinner spinner; private TextView progressText; private ProgressBar progressBar; private RepeatButton repeatButton; private AudioBarListener audioBarListener; + @DrawableRes + private int itemBackground; + OnClickListener onClickListener = new OnClickListener() { + @Override + public void onClick(View view) { + if (audioBarListener != null) { + int tag = (Integer) view.getTag(); + switch (tag) { + case R.drawable.ic_play: + audioBarListener.onPlayPressed(); + break; + case R.drawable.ic_stop: + audioBarListener.onStopPressed(); + break; - private int[] repeatValues = {0, 1, 2, 3, -1}; + case R.drawable.ic_plus_1_24: + audioBarListener.onUpPressed(); + break; + + case R.drawable.ic_neg_1_24: + audioBarListener.onDownPressed(); + break; - public interface AudioBarListener { - void onPlayPressed(); - void onPausePressed(); - void onNextPressed(); - void onPreviousPressed(); - void onStopPressed(); - void onCancelPressed(boolean stopDownload); - void setRepeatCount(int repeatCount); - void onAcceptPressed(); - void onAudioSettingsPressed(); - } + + case R.drawable.ic_pause: + audioBarListener.onPausePressed(); + break; + case R.drawable.ic_next: + audioBarListener.onNextPressed(); + break; + case R.drawable.ic_previous: + audioBarListener.onPreviousPressed(); + break; + case R.drawable.ic_repeat: + incrementRepeat(); + audioBarListener.setRepeatCount(repeatValues[currentRepeat]); + break; + case R.drawable.ic_cancel: + if (haveCriticalError) { + haveCriticalError = false; + switchMode(STOPPED_MODE); + } else { + audioBarListener.onCancelPressed(currentMode == DOWNLOADING_MODE); + } + break; + case R.drawable.ic_accept: + audioBarListener.onAcceptPressed(); + break; + case R.drawable.ic_action_settings: + audioBarListener.onAudioSettingsPressed(); + break; + } + } + } + }; public AudioStatusBar(Context context) { this(context, null); @@ -164,11 +205,7 @@ public void switchMode(int mode) { showPromptForDownloadMode(); } else if (mode == DOWNLOADING_MODE || mode == LOADING_MODE) { showProgress(mode); - } else if (mode == PLAYING_MODE) { - showPlayingMode(false); - } else { - showPlayingMode(true); - } + } else showPlayingMode(mode != PLAYING_MODE); } @NonNull @@ -228,60 +265,32 @@ private void showStoppedMode() { } } - private static class QariAdapter extends BaseAdapter { - @NonNull LayoutInflater inflater; - @NonNull private final List items; - @LayoutRes private final int layoutViewId; - @LayoutRes private final int dropDownViewId; - - QariAdapter(@NonNull Context context, - @NonNull List items, - @LayoutRes int layoutViewId, - @LayoutRes int dropDownViewId) { - this.items = items; - this.layoutViewId = layoutViewId; - this.dropDownViewId = dropDownViewId; - inflater = LayoutInflater.from(context); - } - - @Override - public int getCount() { - return items.size(); - } - - @Override - public QariItem getItem(int position) { - return items.get(position); - } + private void showPlayingMode(boolean isPaused) { + removeAllViews(); - @Override - public long getItemId(int position) { - return position; - } + final boolean withWeight = !isDualPageMode; - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return getViewInternal(position, convertView, parent, layoutViewId); + int button; + if (isPaused) { + button = R.drawable.ic_play; + currentMode = PAUSED_MODE; + } else { + button = R.drawable.ic_pause; + currentMode = PLAYING_MODE; } - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - return getViewInternal(position, convertView, parent, dropDownViewId); - } + addButton(R.drawable.ic_stop, withWeight); + addButton(R.drawable.ic_previous, withWeight); + addButton(button, withWeight); + addButton(R.drawable.ic_next, withWeight); + addButton(R.drawable.ic_plus_1_24, withWeight); + addButton(R.drawable.ic_neg_1_24, withWeight); - private View getViewInternal(int position, View convertView, - ViewGroup parent, @LayoutRes int resource) { - TextView textView; - if (convertView == null) { - textView = (TextView) inflater.inflate(resource, parent, false); - } else { - textView = (TextView) convertView; - } + repeatButton = new RepeatButton(context); + addButton(repeatButton, R.drawable.ic_repeat, withWeight); + updateRepeatButtonText(); - QariItem item = getItem(position); - textView.setText(item.getName()); - return textView; - } + addButton(R.drawable.ic_action_settings, withWeight); } private void addSpinner() { @@ -399,30 +408,28 @@ private void addDownloadProgress(@StringRes int text) { addView(ll, lp); } - private void showPlayingMode(boolean isPaused) { - removeAllViews(); + public interface AudioBarListener { + void onPlayPressed(); - final boolean withWeight = !isDualPageMode; + void onPausePressed(); - int button; - if (isPaused) { - button = R.drawable.ic_play; - currentMode = PAUSED_MODE; - } else { - button = R.drawable.ic_pause; - currentMode = PLAYING_MODE; - } + void onNextPressed(); - addButton(R.drawable.ic_stop, withWeight); - addButton(R.drawable.ic_previous, withWeight); - addButton(button, withWeight); - addButton(R.drawable.ic_next, withWeight); + void onUpPressed(); - repeatButton = new RepeatButton(context); - addButton(repeatButton, R.drawable.ic_repeat, withWeight); - updateRepeatButtonText(); + void onDownPressed(); - addButton(R.drawable.ic_action_settings, withWeight); + void onPreviousPressed(); + + void onStopPressed(); + + void onCancelPressed(boolean stopDownload); + + void setRepeatCount(int repeatCount); + + void onAcceptPressed(); + + void onAudioSettingsPressed(); } private void addButton(int imageId, boolean withWeight) { @@ -498,47 +505,63 @@ public void setAudioBarListener(AudioBarListener listener) { audioBarListener = listener; } - OnClickListener onClickListener = new OnClickListener() { + private static class QariAdapter extends BaseAdapter { + @NonNull + private final List items; + @LayoutRes + private final int layoutViewId; + @LayoutRes + private final int dropDownViewId; + @NonNull + LayoutInflater inflater; + + QariAdapter(@NonNull Context context, + @NonNull List items, + @LayoutRes int layoutViewId, + @LayoutRes int dropDownViewId) { + this.items = items; + this.layoutViewId = layoutViewId; + this.dropDownViewId = dropDownViewId; + inflater = LayoutInflater.from(context); + } + @Override - public void onClick(View view) { - if (audioBarListener != null) { - int tag = (Integer) view.getTag(); - switch (tag) { - case R.drawable.ic_play: - audioBarListener.onPlayPressed(); - break; - case R.drawable.ic_stop: - audioBarListener.onStopPressed(); - break; - case R.drawable.ic_pause: - audioBarListener.onPausePressed(); - break; - case R.drawable.ic_next: - audioBarListener.onNextPressed(); - break; - case R.drawable.ic_previous: - audioBarListener.onPreviousPressed(); - break; - case R.drawable.ic_repeat: - incrementRepeat(); - audioBarListener.setRepeatCount(repeatValues[currentRepeat]); - break; - case R.drawable.ic_cancel: - if (haveCriticalError) { - haveCriticalError = false; - switchMode(STOPPED_MODE); - } else { - audioBarListener.onCancelPressed(currentMode == DOWNLOADING_MODE); - } - break; - case R.drawable.ic_accept: - audioBarListener.onAcceptPressed(); - break; - case R.drawable.ic_action_settings: - audioBarListener.onAudioSettingsPressed(); - break; - } + public int getCount() { + return items.size(); + } + + @Override + public QariItem getItem(int position) { + return items.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getViewInternal(position, convertView, parent, layoutViewId); + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + return getViewInternal(position, convertView, parent, dropDownViewId); + } + + private View getViewInternal(int position, View convertView, + ViewGroup parent, @LayoutRes int resource) { + TextView textView; + if (convertView == null) { + textView = (TextView) inflater.inflate(resource, parent, false); + } else { + textView = (TextView) convertView; } + + QariItem item = getItem(position); + textView.setText(item.getName()); + return textView; } - }; + } } diff --git a/app/src/main/res/drawable/ic_neg_1_24.xml b/app/src/main/res/drawable/ic_neg_1_24.xml new file mode 100644 index 0000000000..c9830ca293 --- /dev/null +++ b/app/src/main/res/drawable/ic_neg_1_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_plus_1_24.xml b/app/src/main/res/drawable/ic_plus_1_24.xml new file mode 100644 index 0000000000..0e2de79573 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_1_24.xml @@ -0,0 +1,10 @@ + + + From a56124247a17cb1668c7fa678a9acbe71f9508c5 Mon Sep 17 00:00:00 2001 From: Abdelouali Benkheil Date: Sat, 8 Apr 2023 03:02:00 +0200 Subject: [PATCH 002/258] Updated Dutch Translations --- app/src/main/res/values-nl/strings.xml | 519 +++++++++++------- app/src/main/res/values-nl/sura_names.xml | 236 ++++++++ .../audio/src/main/res/values-nl/readers.xml | 77 +++ 3 files changed, 630 insertions(+), 202 deletions(-) create mode 100644 app/src/main/res/values-nl/sura_names.xml create mode 100644 common/audio/src/main/res/values-nl/readers.xml diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 4fa7dab4ec..9ff5ddeb5f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,22 +1,22 @@ - - + Koran - Download vereiste bestanden? + Benodigde bestanden downloaden? Om Koran Android goed te laten werken, -         moeten we een aantal bestanden downloaden. Als u dit niet doet, werkt de app -         mogelijk niet betrouwbaar en zal een internetverbinding vereist zijn voor het lezen. -         Wilt u de benodigde bestanden nu downloaden? - - We hebben onlangs betere afbeeldingen toegevoegd voor Tablets. Wil je deze afbeeldingen nu toevoegen? - Er is een kleine maar belangrijke update voor Koran afbeeldingen die je op je apparaat hebt staat. Wil je deze patch nu downloaden? - + moeten we enkele bestanden downloaden. Doe je dit nu niet, dan is de app + werkt mogelijk niet betrouwbaar en vereist een internetverbinding om te lezen. + Wilt u de benodigde bestanden nu downloaden? + + We hebben onlangs verbeterde afbeeldingen voor tablets + toegevoegd. Wilt u deze afbeeldingen nu downloaden? + Er is een kleine maar belangrijke update voor de + koranafbeeldingen die je op je apparaat hebt staan. Wilt u deze patch nu downloaden? + Ja Nee - Wacht tot de bestanden zijn gedownload (CV ondersteund). + Downloaden… + Wacht tot de bestanden zijn gedownload (hervatting wordt + ondersteund). Verwerken… Over Ons Andere apps @@ -25,63 +25,131 @@ Yasser Baghouli (zaterdag@outlook.com) Bladwijzers Pagina Bladwijzers Ayah Bladwijzers - Sorteren + Sorteer Datum Toegevoegd - Locatie in Koran - Groepeer via Label - Toon Vertalingen + Locatie in de Koran + Groeperen op tags + @string/recent_pages + Datum tonen + Toon vertaling Toon Koran - Ondersteuning - Zoek - Verkrijg Vertalingen - Ga + Hulp + Zoeken + Ontvang vertalingen + @string/gotoPage Ga naar pagina + Even geduld aub… - Koran voor Android is een gratis Koran Applicatie. Vergeet a.u.b. de ontwikkelaars niet in uw gebeden. - Gegevens bronnen + Koran voor Android is een gratis Koran-applicatie. Vergeet de bijdragers niet in uw gebeden. + Data bronnen Afbeeldingen - De Koran Afbeeldingen zijn gebaseerd op de lettertypen van de Koning Fahd Koran Printing Complex - De koran beelden voor het Naskh app werden genomen (met toestemming) van SHL Info Systems - Qaloon afbeeldingen gebruikt met toestemming van Nous Memes Editions Et Diffusion (Tunisia) - Gapless mp3 Quran recitations + De koranafbeeldingen zijn gebaseerd op de lettertypen van het + King Fahd Quran Printing Complex + + https://github.com/quran/quran.com-images + De koranafbeeldingen voor de Naskh-app zijn (met toestemming) + afkomstig van SHL Info Systems + http://www.shlinfosystems.com + Qaloon-afbeeldingen worden gebruikt met toestemming van Nous + Memes Editions Et Diffusion (Tunesië) + QuranicAudio + Gapless mp3 koranrecitaties + https://quranicaudio.com Electronic Moshaf Project - The King Saud University Electronic Moshaf Project was the app\'s source of Arabic Tafaseer and translations for various languages - Vertalingen van vele talen - Noorehira lettertype en Mufti Taqi Vertaling - Open Source Projecten + Het King Saud University Electronic Moshaf Project is de + app's bron van Arabische Tafaseer en vertalingen voor verschillende talen. Het is ook de bron + voor de recitatie van Dr. Ayman Suwaid. + https://quran.ksu.edu.sa + Al-Bāḥith al-Qur’ānī (tafsir.app) + Uthmani-tekst en Arabische Tafseer + https://tafsir.app + Noble Quran Encyclopedia (QuranEnc) + Vertalingen voor vele talen + https://quranenc.com + Tanzil + Vertalingen voor enkele talen + http://tanzil.net + Noorhidayat + Noorehira-lettertype en Mufti Taqi-vertaling + http://www.noorehidayat.org + Open Source Projects + AndroidX + + https://developer.android.com/jetpack/androidx + Dagger 2 + https://google.github.io/dagger + dnsjava + http://dnsjava.org + Material Components for Android + + https://github.com/material-components/material-components-android + Timber + https://github.com/JakeWharton/timber + Kotlin + https://kotlinlang.org + Number Picker + + https://github.com/ShawnLin013/NumberPicker + OkHttp + https://github.com/square/okhttp + RxJava + https://github.com/ReactiveX/RxJava + RxAndroid + https://github.com/ReactiveX/RxAndroid + Moshi + https://github.com/square/moshi + AndroidSlidingUpPanel + + https://github.com/umano/AndroidSlidingUpPanel Andere Bijdragers - Een lijst van personen die hebben bijgedragen aan de ontwikkeling van Koran voor Android + Een lijst met mensen die hebben bijgedragen aan de + ontwikkeling van Koran voor Android + + https://github.com/quran/quran_android/blob/master/CONTRIBUTORS.md Veel Gestelde Vragen - <b>Hoe kan ik de audio afspelen?</b> - <br/>Open even welke Koran pagina. Tik op het scherm eenmaal. Aan de onderkant, zult u -een afspeelknop zien en wat tekst met de naam van de Qari. Klik op de naam van de qari - om een andere qari te selecteren. Klik op afspelen om te downloaden en de soera van de huidige pagina af te spelen. - <br/> - <br/><b>Hoe kan ik vertalingen tonen?</b> - <br/>Open even welke Koran pagina. Tik op het scherm eenmaal. Aan de bovenkant, zal je een aardbol zien. Klik hier op en maak een keuze van vertaling. - <br/> - Als je/u over geen enkele vertaling beschikt, wordt u naar een venster geleid waar u vertalingen kunt downloaden. - Kies en download een vertaling. Keer dan terug en raak het aardbolicoontje opnieuw aan om de vertaling te bekijken. - <br/> - <br/><b>Hoe voeg ik een pagina toe als bladwijzer?</b> - <br/>Open eender welke Koran-pagina. Raak het scherm één keer aan. Rechtsboven zal je nu een bladwijzer-icoontje zien. Raak het icoontje aan om de pagina toe te voegen als bladwijzer. (het icoontje wordt dan strak wit). Raak het icoontje opnieuw aan om de bladwijzer te verwijderen. - <br/> - <br/><b>Hoe vergroot ik de tekst (/ Hoe zoom ik in?)</b> - <br/>Voor de Arabischtalige pagina\'s: Hou je smartphone horizontaal (/in landscape?). De tekst zal dan vanzelf groter worden. -Voor de vertaalde pagina\'s (/voor vertalingen): Ga naar instellingen en wijzig de tekstgrootte naar uw voorkeur. - <br/> - <br/><b>Hoe deel ik een aya?</b> - <br/>Als je op een Arabischtalige pagina bent: Hou de (/het?) gewenste aya (/vers?) ingedrukt totdat er een menu verschijnt waarop je kan kiezen om als bladwijzer toe te voegen, te taggen, de aya (/het vers?) te kopiëren of delen naar het clipboard, de vertaling te bekijken, of te luisteren naar de recitatie van de aya (/het vers?)<br/> - <br/><b>De lettertypes: Malayam, Tamil, Bengali en Urdu werken niet!</b> - <br/>Helaas ondersteunen Android versies ouder dan 4.0 deze lettertypes niet en is er weinig dat wij hieraan kunnen doen. - - Zoeken in de Koran + <b>Hoe speel ik audio af?</b> + <br/>Open een willekeurige pagina van de Koran. Tik eenmaal op het scherm. Onderaan zie je + een afspeelknop en wat tekst met de naam van een qari. Klik op de naam van de qari om een andere + qari te kiezen. Klik op afspelen om de huidige pagina of sura te downloaden en af te spelen. + <br/> + <br/><b>Hoe bekijk ik de vertaling?</b> + <br/>Open een willekeurige pagina van de Koran. Tik eenmaal op het scherm. Bovenaan zie je + een wereldbolpictogram (of, als je het niet ziet, klik op een pictogram met drie vierkante + stippen) - klik hierop en kies vertaling om de vertaling te bekijken. + <br/> + Als je geen vertalingen hebt gedownload, brengt het je naar een scherm waar je vertalingen kunt + downloaden. Kies en download een vertaling, keer dan terug en tik opnieuw op het + wereldbolpictogram om de vertaling te bekijken. + <br/> + <br/><b>Hoe maak ik een bladwijzer van een pagina?</b> + <br/>Open een willekeurige pagina van de Koran. Tik eenmaal op het scherm. Rechtsboven zie je + een bladwijzerpictogram. Tik op het bladwijzerpictogram om de pagina als bladwijzer toe te + voegen (de kleur wordt wit). Tik opnieuw op het bladwijzerpictogram om de bladwijzer te + verwijderen. + <br/> + <br/><b>Hoe maak ik de tekst groter?</b> + <br/>Houd je telefoon in landschapsmodus voor de Arabische pagina's. Dit maakt de tekst + groter. Voor vertalingen ga je naar de instellingen en stel je de grootte van de vertalingstekst + in. + <br/> + <br/><b>Hoe deel ik een ayah?</b> + <br/>Terwijl je op een willekeurige Arabische pagina bent, druk en houd je op een ayah om een + menu te krijgen waar je kunt kiezen om de ayah als bladwijzer te markeren, taggen, delen of + kopiëren naar het klembord, de vertaling te bekijken, of naar de recitatie te luisteren. + <br/> + <br/><b>Malayalam/Tamil/Bengali/Urdu-lettertypen werken niet!</b> + <br/>Helaas ondersteunen Android-versies vóór 4.0 deze lettertypen niet en er is weinig dat + we hieraan kunnen doen. + + Zoek in de Koran Verzen van de Koran + Volledige resultaten + Doorzoek de hele mushaf @@ -90,97 +158,141 @@ Voor de vertaalde pagina\'s (/voor vertalingen): Ga naar instellingen en wijzig - Koran wil uw toestemming om zijn gegevens op te slaan op -         uw externe opslag. Koran zal werken zonder deze toestemming, maar als je de app deinstalleerd zullen alle gedownloade pagina\'s, audio en gegevens verwijderd worden. Verleen je Koran toestemming? - - Gelieve de app opnieuw opstarten om de veranderingen uit te voeren. + Koran wil toestemming om zijn gegevens op externe + opslag op te slaan. Koran werkt zonder deze toestemming, maar als u gegevens wist of de app + verwijdert, worden alle gedownloade pagina's, audio en gegevens verwijderd. Wilt u Koran + toestemming geven? + Start de app opnieuw op zodat de wijzigingen van + kracht worden. - Gevonden in Soera %1$s: %2$d (Pagina %3$d) - Geen resultaten gevonden voor \"%s\" - Je hebt \"Arabisch Zoeken\" niet gedownload. Download het en probeer uw zoekopdracht opnieuw. - Verkrijg "Arabisch Zoeken" Database + Gevonden in Soera %1$s: %2$d (pagina %3$d) + Geen resultaten gevonden voor "%s" + U heeft het Arabische zoekpakket niet gedownload. + Download het alstublieft en probeer uw zoekopdracht opnieuw. + Arabische zoekdatabase verkrijgen - Volumetoets navigatie - Navigeren tussen pagina\'s met behulp van de volumetoetsen - Voorkeuren voor Lezen - Voorkeuren voor Vertalingen - Beeldscherminstellingen - Arabisch-modus (الوضع العربي) - Gebruik Arabisch voor de applicatie-interface + Volume-toets navigatie + Blader tussen pagina's met de volumetoetsen + Leesvoorkeuren + Vertaalvoorkeuren + Weergave-instellingen + Arabische modus (الوضع العربي) + Gebruik Arabisch voor de toepassingsinterface Nieuwe achtergrond - Vergrendel schermoriëntatie - Gebruik vaste oriëntatie-modus - Aanpassen aan de huidige oriëntatie mode - Landscape modus - Gebruik altijd landscape modus - Gebruik altijd portret-modus - Nacht modus - Gebruik een donkere achtergrond en een lichte lettertype - Tekst helderheid - Helederheid van de tekst wanneer de nacht modus zijn ingeschakeld. - Achtergrond helderheid - Helderheid van de pagina wanneer de nachtmodus actief is - Toon pagina info - Overlay page number, sura name, and juz\' number while reading - Weergeef marker popups - Er komt een popup bij het bereiken van juz\', hizb, etc. - Markeer bladwijzers - Markeer opgeslagen ayah verzen - Vertaling tekstgrootte - Vertaling - Downloaden en beheren van vertalingen - Ayah voor vertaling + Schermoriëntatie vergrendelen + Gebruik de modus voor vaste oriëntatie + Aanpasbare oriëntatiemodus + Liggende modus + Gebruik altijd de liggende modus + Gebruik altijd de staande modus + Nachtmodus + Gebruik een donkere achtergrond en lichte lettertypen + Teksthelderheid + Helderheid van de tekst wanneer de + nachtmodus actief is + Achtergrondhelderheid + Helderheid van de pagina wanneer de + nachtmodus actief is + Paginagegevens tonen + Overlay van paginanummer, soera naam en juz' nummer + tijdens het lezen + Popup-markeringen tonen + Popup tonen bij het bereiken van juz', hizb, enz. + Bladwijzers markeren + Gemarkeerde ayah's markeren tijdens het lezen + Tekstgrootte van vertalingen + Vertalingen + Vertalingen downloaden en beheren + Ayah vóór vertaling Toon ayah in het Arabisch boven de vertaling - Dyslexia friendly font - Display translations in dyslexia friendly font - Download Opties - Streaming - Audio streamen indien mogelijk - Hoeveelheid te downloaden - Preferred download amount for non-gapless audio - Beheer en download Koran audio + Dyslexie-vriendelijk lettertype + Vertalingen weergeven in een dyslexie-vriendelijk + lettertype + Downloadopties + Streamen + Audio streamen in plaats van downloaden + Downloadhoeveelheid + Voorkeurshoeveelheid voor het downloaden van + niet-gapless audio + @string/audio_manager + Koran audio beheren en downloaden Gedownload Beschikbaar om te downloaden OK - Koran gegevens bibliotheek - Kies de locatie voor het opslaan van Koran gegevens. - Verstuur logbestanden - Verstuur debug logbestanden naar de ontwikkelaar - Interne geheugen - Externe SD kaart %1$d - Huidige gegevens geheugen is - Berekenen van App Grootte + Koran gegevensmap + Kies waar Koran-bestanden moeten worden opgeslagen + Logs verzenden + Debug logs naar de ontwikkelaar verzenden + Intern geheugen + Externe opslag %1$d + Huidige gegevensgrootte is + App-grootte berekenen App-bestanden kopiëren - Kan app bestanden niet verplaatsen - Onvoldoende ruimte om app bestanden te verplaatsen + Fout bij het verplaatsen van app-bestanden + Onvoldoende ruimte om app-bestanden te + verplaatsen %1$d MB - Tablet Mode - In landscape, worden twee pagina\'s naast elkaar weergegeven. - In landscape, zal slechts één pagina verschijnen. + @string/download_amount_in_megabytes + Dubbele Paginamodus + In landschapmodus worden twee pagina's naast elkaar + weergegeven. + In landschapmodus wordt slechts één pagina + weergegeven. Geavanceerde opties + Bladwijzers importeren/exporteren, + Koran-gegevensdirectory instellen, enzovoort. + Importeren + Importeer bladwijzers en tags Exporteren - Exporteer een kopie van bladwijzers en labels + Exporteer een kopie van bladwijzers en tags + Exporteren naar CSV + Exporteer een kopie van bladwijzers en tags naar CSV + Paginatype (experimenteel) + Selecteer het type leespagina's + Toon de vertaling van de naam van de soera + Vertaalde naam van soera + Voorbeeld - Kan geen back-upbestand lezen wegens bevoegdheden fouten. - Ongeldige backup-bestand (of niet in staat om back-up bestand te lezen). - Importeer Gegevens - Als je dit bestand importeer zal het al je bladwijzers vervangen met %1$d bladwijzer(s) en %2$d label(s). Importeren? - Import Successvol - Fout bij het exporteren van gegevens. - Gegevens geëxporteerd naar %1$s. + @string/prefs_translations + Meer vertalingen + Kan back-upbestand niet lezen vanwege + toestemmingsfout. + Ongeldig back-upbestand (of kan back-upbestand niet lezen). + Gegevens importeren + Als je dit bestand importeert, worden alle bladwijzers + vervangen door %1$d bladwijzer(s) en %2$d tag(s). Importeren? + Importeren voltooid + Fout bij exporteren van gegevens + Gegevens geëxporteerd naar %1$s + + Omhoog verplaatsen + Omlaag verplaatsen + Vertaling verwijderen + quranandroid+logs@gmail.com Waarschuwing - Als gevolg van Android beperkingen, als je ervoor kiest om Koran gegevens te plaatsen -         op uw externe SD-kaart en deze later verwijderd, zal je al deze gegevens opnieuw moeten downloaden. Ben jij -         zeker dat je de externe SD-kaart wilt gebruiken? - Gelieve toestemming te verlenen in programma-instellingen. + Vanwege beperkingen in Android, als u ervoor kiest om + Koran-gegevens op uw externe SD-kaart te plaatsen en later Koran Android verwijdert of gegevens + wist, worden alle pagina's en audio van Koran Android verwijderd en moet u deze opnieuw + downloaden. Weet u zeker dat u de externe SD-kaart wilt gebruiken? + Vanwege wijzigingen in Android om de privacy van gebruikers + te vergroten, kan het kopiëren van bestanden buiten de app-mappen voorkomen dat Koran in + toekomstige versies van Android toegang heeft tot zijn gegevens. Weet u zeker dat u deze locatie + wilt gebruiken? + Verleen toestemming in de applicatie-instellingen + + المزيد… + De tafseer van deze ayah is inbegrepen bij de tafseer van ayah + %d (Klik om uit te vouwen). + - Update Beschikbaar - Er is een update beschikbaar voor een aantal van uw vertalingen. Bezoek het scherm vertalingen nu? + Update beschikbaar + Er is een update beschikbaar voor sommige van uw + vertalingen. Wilt u nu naar het vertalingenscherm gaan? Ja Later @@ -200,44 +312,49 @@ Voor de vertaalde pagina\'s (/voor vertalingen): Ga naar instellingen en wijzig Laden… - annuleren… + Annuleren… Juz\' %1$s + , Pagina %1$s, Juz\' %2$s Vereiste Bestanden Vereiste Bestanden + - Kon geen SD-kaart vinden. Gelieve te dit te controleren en opnieuw te proberen. - Download Succesvol - Download Verwerken… - Download mislukt - Er is onvoeldoende schijfruimte om te downloaden - Er is een netwerk fout opgetreden tijdens het downloaden - Er is een toestemming fout opgetreden tijdens het downloaden - Downloaded file was corrupted - Corrupted file, attempting to re-download - Network error, trying to resume… - Download canceled - Je maak geen gebruik van Wifi. Toch gegevens downloaden? + SD-kaart niet gevonden. Monteer de kaart en probeer opnieuw. + Download succesvol + Download verwerken… + Downloaden mislukt + Niet genoeg schijfruimte om te downloaden + Downloaden mislukt vanwege netwerkfout + Downloaden mislukt vanwege machtigingsfout + Gedownloade bestand was beschadigd + Beschadigd bestand, opnieuw downloaden wordt + geprobeerd + Netwerkfout, proberen te hervatten… + Download geannuleerd + U bent niet verbonden met Wi-Fi. Data toch downloaden? - Processing file %1$d / %2$d - Probeer opnieuw + Bestand verwerken %1$d / %2$d + Opnieuw Proberen Annuleren - We moeten één of twee kleine bestanden downloaden om het delen en vertaling te ondersteunen. Download nu? + We moeten een of twee kleine bestanden downloaden om delen en + vertalen te ondersteunen. Download nu? Vertaling Verwijderen? - Bent u zeker dat u %1$s wilt verwijderen? - Kan geen lijst met vertalingen downloaden. Probeer het later opnieuw. - Zoek Gegevens - Je heb geen enkele vertaling/tafseer gedownload - Ontvang Vertalingen + Weet u zeker dat u de %1$s wilt verwijderen? + Kan de lijst met vertalingen niet downloaden. + Probeer het later opnieuw. + Data Zoeken + U heeft nog geen vertalingen/tafaseer gedownload. + Vertalingen Ophalen - Als uw vraag hierboven niet wordt beantwoord, kunt u een e-mail sturen naar quranandroid@gmail.com voor ondersteuning. -         Houd er rekening mee dat we veel e-mails ontvangen. Wij zijn daarom niet instaat om deze allemaal te beantwoorden. - + Als uw vraag hierboven niet wordt beantwoord, kunt u een e-mail sturen + naar quranandroid@gmail.com voor ondersteuning. Houd er rekening mee dat we veel e-mails + ontvangen, dus we kunnen ze mogelijk niet allemaal beantwoorden. Makki @@ -248,15 +365,17 @@ Voor de vertaalde pagina\'s (/voor vertalingen): Ga naar instellingen en wijzig - Speel enkel de bovenstaande verzen af - Speel een reeks verzen af: + @string/from + @string/to + Speel alleen bovenstaande verzen + Speel een set van verzen af: Speel elke vers af: - Pas toe - Pas toe en Speel af + Toepassen + Toepassen en Afspelen Vorige Afspelen - Pauze - Stop + Pauzeren + Onderbreken Volgende @@ -266,59 +385,55 @@ Voor de vertaalde pagina\'s (/voor vertalingen): Ga naar instellingen en wijzig lus + Begin met afspelen vanaf: + Begin van Pagina + Ayah Gekopieerd - Label Bladwijzer - Label Verwijderen - Label Bewerken - Nieuwe Label - Niet Gelabeld + Tag Bladwijzer + Verwijder Tag + Bewerk Tag + Nieuwe Tag + Niet Getaged - een item verwijderd + Eén item verwijderd %d items verwijderd - Ongedaan Maken + Ongedaan maken + + Recente pagina's + + @string/menu_jump_last_page + @string/recent_pages + - Label + Tags Naam + Tagnaam mag niet leeg zijn! + Tagnaam bestaat al! - - Geen Favorieten + + Geen bladwijzers - - Toon Datum - Wacht alstublieft... - Uthmani tekst en Arabische Tafaseer - Vertalingen voor vele talen - Volledige resultaten - Doorzoek de hele mushaf - Importeren/exporteren van bladwijzers, instellen van de Quran-gegevensmap, enz. - Importeren - Bladwijzers en tags importeren - Paginatype (experimenteel) - Selecteer het type leespagina - Toon de vertaling van de naam van de soera - Surah vertaalde naam - Voorbeeld - Meer vertalingen - Naar boven - Naar beneden - Vertaling verwijderen - Als gevolg van Android veranderingen om de privacy van gebruikers te verhogen, kan het kopiëren van bestanden buiten de app directories ervoor zorgen dat Quran voor Android geen toegang meer heeft tot zijn gegevens in toekomstige versies van Android. Weet u zeker dat u dit pad wilt gebruiken? - De tafseer van deze ayah is opgenomen bij de tafseer van ayah %d (Klik om uit te breiden). - Start het afspelen vanaf: - Begin van pagina - Recente pagina\'s - Tag namen mogen niet leeg zijn! - Tag naam bestaat al! + Koran Audiobestanden Update - Verschillende Quran audio bestanden zijn bijgewerkt. Quran voor Android heeft uw kopie van deze bestanden verwijderd, zodat de nieuwste versies kunnen worden gedownload wanneer u ze de volgende keer afspeelt. - Dubbele pagina voorkeuren - Koran en vertaling in duale modus - In de dubbele pagina modus met vertalingen, wordt de Koran pagina en de vertaling getoond - Koran downloads - Koran recitatie - + Verschillende koran-audiobestanden zijn bijgewerkt. Koran heeft + uw exemplaar van deze bestanden verwijderd, zodat de nieuwste versies kunnen worden gedownload + de volgende keer dat u ze afspeelt. + + + %1$d:%2$d + + + Voorkeuren voor dubbele pagina's + Koran en vertaling in dubbele modus + In dubbele paginamodus met vertalingen, + koranpagina en vertaling wordt weergegeven + + + Koran Downloads + Koran Recitaties + \ No newline at end of file diff --git a/app/src/main/res/values-nl/sura_names.xml b/app/src/main/res/values-nl/sura_names.xml new file mode 100644 index 0000000000..eccc67d46a --- /dev/null +++ b/app/src/main/res/values-nl/sura_names.xml @@ -0,0 +1,236 @@ + + + + Al-Fātihah + Al-Baqarah + Āli-ʿImrān + An-Nisāʾ + Al-Māʾidah + Al-Anʿām + Al-Aʿrāf + Al-Anfāl + At-Tawbah + Yūnus + Hūd + Yūsuf + Ar-Raʿd + Ibrāhīm + Al-Ḥijr + An-Naḥl + Al-Isrāʾ + Al-Kahf + Maryam + Ṭā-Hā + Al-Anbiyāʾ + Al-Ḥajj + Al-Muʾminūn + An-Nūr + Al-Furqān + Ash-Shuʿarāʾ + An-Naml + Al-Qaṣaṣ + Al-ʿAnkabūt + Ar-Rūm + Luqmān + As-Sajdah + Al-Aḥzāb + Sabaʾ + Fāṭir + Yā-Sīn + Aṣ-Ṣāffāt + Ṣād + Az-Zumar + Ghāfir + Fuṣṣilat + Ash-Shūrā + Az-Zukhruf + Ad-Dukhān + Al-Jāthiyah + Al-Aḥqāf + Muḥammad + Al-Fatḥ + Al-Ḥujurāt + Qāf + Adh-Dhāriyāt + Aṭ-Ṭūr + An-Najm + Al-Qamar + Ar-Raḥmān + Al-Wāqiʿah + Al-Ḥadīd + Al-Mujādilah + Al-Ḥashr + Al-Mumtaḥanah + Aṣ-Ṣaff + Al-Jumuʿah + Al-Munāfiqūn + At-Taghābun + Aṭ-Ṭalāq + At-Taḥrīm + Al-Mulk + Al-Qalam + Al-Ḥāqqah + Al-Maʿārij + Nūḥ + Al-Jinn + Al-Muzzammil + Al-Muddaththir + Al-Qiyāmah + Al-Insān + Al-Mursalāt + An-Nabaʾ + An-Nāziʿāt + ʿAbasa + At-Takwīr + Al-Infiṭār + Al-Muṭaffifīn + Al-Inshiqāq + Al-Burūj + Aṭ-Ṭāriq + Al-Aʿlā + Al-Ghāshiyah + Al-Fajr + Al-Balad + Ash-Shams + Al-Layl + Aḍ-Ḍuḥā + Ash-Sharḥ + At-Tīn + Al-ʿAlaq + Al-Qadr + Al-Bayyinah + Az-Zalzalah + Al-ʿĀdiyāt + Al-Qāriʿah + At-Takāthur + Al-ʿAṣr + Al-Humazah + Al-Fīl + Quraysh + Al-Māʿūn + Al-Kawthar + Al-Kāfirūn + An-Naṣr + Al-Masad + Al-Ikhlāṣ + Al-Falaq + An-Nās + + + + De Opening + De Koe + De Familie van Imran + De Vrouwen + De Tafel + Het Vee + De Kantelen + De Buit + Het Berouw + De Profeet Jonas + De Profeet Hūd + De Profeet Jozef + De Donder + Abraham + Al-Hidjr + De Bijen + De Nachtreis + De Grot + Maryam + Ta-Ha + De Profeten + De Bedevaart + De Gelovigen + Het Licht + Het Reddend Onderscheidingsmiddel + De Dichters + De Mieren + Het Verhaal + De Spin + De Romeinen + Luqmān + De Eerbiedige Neerbuiging + De Partijen + Saba + De Engelen + Ya Sin + De Zich Opstellenden + De Arabische Letter "Saad" + De Drommen + Hij Die Vergeeft + Zij Zijn Uiteengezet + Het Beraad + Pracht en Praal + De Rook + De Neergeknielden + De Zandduinen + Mohammed صَلَّى ٱللَّهُ عَلَيۡهِ وَسَلَّمَ + Het Succes + De Binnenste Vertrekken + De Arabische Letter "Qaf" + De Schiftende Winden + De Berg + De Sterren + De Maan + De Weldadige, de Weldoener + Het Onvermijdelijke + IJzer + De Vrouw die pleitte + Exodus, Ballingschap + De Vrouw die ondervraagd werd + De Gelederen + Vrijdag + De Hypocrieten + Wederzijdse Winst en Verlies + De Scheiding + Verbod + Heerschappij + De Pen + De Realiteit + De Stijgende Trap(pen) + (de profeet) Nuh + De Djinns + Degene Gekleed in een Mantel + Degene Omwikkeld met Gewaden + De Wederopstanding + De Mensheid + De Afgezanten + De Aankondiging + Zij die (de ziel) wegrukken (Engelen) + Hij Fronste + Het Rollen (of Opvouwen, Omverwerpen) + De Splijting + De Oplichters + De Scheuring + De Sterrenstelsels + De Heldere Ster ("Nachtkomer") + De Hoogste + De Overweldiging + De Dageraad + De Stad + De Zon + De Nacht + De Voormiddag + Soelaas + De Vijg + De Klonter + Het Noodlot + Het (duidelijke) Bewijs + De Aardbeving + De Renners (Paarden) + Dag des Oordeels + De Vergaring (van goederen) + De Namiddag + De Roddelaar, Lasteraar + De Olifant + De Qoeraisj + Kleine Vriendelijkheden, Liefdadigheid + De (Rivier/Bron van) Overvloed + De Ongelovigen + Goddelijke Hulp + Doorns, Palmvezels + Oprechtheid + De Dageraad + De Mensheid + + diff --git a/common/audio/src/main/res/values-nl/readers.xml b/common/audio/src/main/res/values-nl/readers.xml new file mode 100644 index 0000000000..9a2f172487 --- /dev/null +++ b/common/audio/src/main/res/values-nl/readers.xml @@ -0,0 +1,77 @@ + + + Minshawi Murattal + Husary + Abdurrahman As-Sudais + Nasser Al Qatami + Abd Al-Basit (pauzes) + Abd Al-Basit Mujawwad (pauzes) + Abdullah Basfar (pauzes) + Abdurrahmaan As-Sudais (pauzes) + Abu Bakr Ash-Shatri (pauzes) + Mishary Al-Afasy (pauzes) + Saad Al-Ghamdi (pauzes) + Ibrahim Walk (Engelse vertaling) + Hani Ar-Rifai (pauzes) + Husary Mujawwad (pauzes) + Ali Al-Hudhaify (pauzes) + Maher Al Muaiqly (pauzes) + Minshawy Mujawwad (pauzes) + Minshawy Mujawwad + Mohammad al Tablaway (pauzes) + Muhammad Ayyoub (pauzes) + Muhammad Jibreel (pauzes) + Saood Ash-Shuraym (pauzes) + Yasser Ad-Dussary (pauzes) + Abd Al-Basit + Abd Al-Basit Mujawwad + Aziz Alili + Salah Budair + Saood Ash-Shuraym + Yasser Ad-Dussary + Mohammad al Tablawy + Sahl Yaseen + Abu Bakr Ash-Shatri + Ahmad Nauina + Akram Al-Alaqmi + Ali Hajjaj Alsouasi + Saad Al-Ghamdi + Bandar Baleela + Mahmoud Ali al Bana + Abdulrahman Al-Shahat + Abdur-Rashid Sufi + Mostafa Ismaeel + Husary Broadcast + Abdulaziz Az-Zahrani + Ayman Suwaid (pauzes) + Mishary Al-Afasy + Hani Ar-Rifai + Muhammad Ayyoub + Mishari & Ibrahim Walk (Engels) + Muhammad Jibreel + Abdullah Basfar + Ibrahim Walk (Engelse vertaling) + Mishari Al-Afasy (Californië) + Ahmed ibn Ali al-Ajamy + Ali Jaber + Maher Al Muaiqly (Haramain) + Abdullah Al-Juhany + Abdul Mohsen Al-Qasim + Fares Abbad + Khalifa Taniji + Abdullah Matroud + Salah Bukhatir + Ali Al-Hudhaify + Khaled Al-Muhanna + Husary (Mujawwad) + Husar (Muallim) + Ibrahim Al-Akhdar + Maher Al Muaiqly + Yasser Salama (hadr) + Khalid Al Qahtani + Abdullah Al-Asmari (Arabisch commentaar) + Abdul Rahman Aloosi + Alzain Muhammad Ahmad + Muhammad Rashad Al-Scherif + Wadee\' Al-Yamani + From a7e38b52cc169bc964a75319b38b7da0a4b50f67 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 8 Apr 2023 12:16:28 +0400 Subject: [PATCH 003/258] Escape apostrophes --- app/src/main/res/values-nl/strings.xml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 9ff5ddeb5f..1b20e1b288 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,5 +1,5 @@ - + Koran Benodigde bestanden downloaden? Om Koran Android goed te laten werken, @@ -58,7 +58,7 @@ https://quranicaudio.com Electronic Moshaf Project Het King Saud University Electronic Moshaf Project is de - app's bron van Arabische Tafaseer en vertalingen voor verschillende talen. Het is ook de bron + app\'s bron van Arabische Tafaseer en vertalingen voor verschillende talen. Het is ook de bron voor de recitatie van Dr. Ayman Suwaid. https://quran.ksu.edu.sa Al-Bāḥith al-Qur’ānī (tafsir.app) @@ -174,7 +174,7 @@ Volume-toets navigatie - Blader tussen pagina's met de volumetoetsen + Blader tussen pagina\'s met de volumetoetsen Leesvoorkeuren Vertaalvoorkeuren Weergave-instellingen @@ -196,12 +196,12 @@ Helderheid van de pagina wanneer de nachtmodus actief is Paginagegevens tonen - Overlay van paginanummer, soera naam en juz' nummer + Overlay van paginanummer, soera naam en juz\' nummer tijdens het lezen Popup-markeringen tonen - Popup tonen bij het bereiken van juz', hizb, enz. + Popup tonen bij het bereiken van juz\', hizb, enz. Bladwijzers markeren - Gemarkeerde ayah's markeren tijdens het lezen + Gemarkeerde ayah\'s markeren tijdens het lezen Tekstgrootte van vertalingen Vertalingen Vertalingen downloaden en beheren @@ -236,7 +236,7 @@ %1$d MB @string/download_amount_in_megabytes Dubbele Paginamodus - In landschapmodus worden twee pagina's naast elkaar + In landschapmodus worden twee pagina\'s naast elkaar weergegeven. In landschapmodus wordt slechts één pagina weergegeven. @@ -250,7 +250,7 @@ Exporteren naar CSV Exporteer een kopie van bladwijzers en tags naar CSV Paginatype (experimenteel) - Selecteer het type leespagina's + Selecteer het type leespagina\'s Toon de vertaling van de naam van de soera Vertaalde naam van soera Voorbeeld @@ -403,7 +403,7 @@ Ongedaan maken - Recente pagina's + Recente pagina\'s @string/menu_jump_last_page @string/recent_pages @@ -428,7 +428,7 @@ %1$d:%2$d - Voorkeuren voor dubbele pagina's + Voorkeuren voor dubbele pagina\'s Koran en vertaling in dubbele modus In dubbele paginamodus met vertalingen, koranpagina en vertaling wordt weergegeven @@ -436,4 +436,4 @@ Koran Downloads Koran Recitaties - \ No newline at end of file + From e1a20efbf870b4fc4b27fb8824db3909d03a77e3 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 8 Apr 2023 14:28:07 +0400 Subject: [PATCH 004/258] Additional fixes to strings --- app/src/main/res/values-nl/strings.xml | 6 +++--- app/src/main/res/values-uk/strings.xml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1b20e1b288..21d7f00266 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -133,7 +133,7 @@ verwijderen. <br/> <br/><b>Hoe maak ik de tekst groter?</b> - <br/>Houd je telefoon in landschapsmodus voor de Arabische pagina's. Dit maakt de tekst + <br/>Houd je telefoon in landschapsmodus voor de Arabische pagina\'s. Dit maakt de tekst groter. Voor vertalingen ga je naar de instellingen en stel je de grootte van de vertalingstekst in. <br/> @@ -160,7 +160,7 @@ Koran wil toestemming om zijn gegevens op externe opslag op te slaan. Koran werkt zonder deze toestemming, maar als u gegevens wist of de app - verwijdert, worden alle gedownloade pagina's, audio en gegevens verwijderd. Wilt u Koran + verwijdert, worden alle gedownloade pagina\'s, audio en gegevens verwijderd. Wilt u Koran toestemming geven? Start de app opnieuw op zodat de wijzigingen van kracht worden. @@ -275,7 +275,7 @@ Waarschuwing Vanwege beperkingen in Android, als u ervoor kiest om Koran-gegevens op uw externe SD-kaart te plaatsen en later Koran Android verwijdert of gegevens - wist, worden alle pagina's en audio van Koran Android verwijderd en moet u deze opnieuw + wist, worden alle pagina\'s en audio van Koran Android verwijderd en moet u deze opnieuw downloaden. Weet u zeker dat u de externe SD-kaart wilt gebruiken? Vanwege wijzigingen in Android om de privacy van gebruikers te vergroten, kan het kopiëren van bestanden buiten de app-mappen voorkomen dat Koran in diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f051400537..57147b3681 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -170,7 +170,7 @@ Не вдалося прочитати файл резервної копії через помилку дозволів. Неприпустимий файл резервної копії (або не вдалося прочитати файл резервної копії). Імпорт даних - Якщо ви імпортуєте цей файл, він замінить усі ваші закладки закладками % 1$d і тегами %2$d. Імпорт? + Якщо ви імпортуєте цей файл, він замінить усі ваші закладки закладками %1$d і тегами %2$d. Імпорт? Імпорт успішний Помилка експорту даних Дані експортуються до %1$s From 9d348edc9f5d5f005bedb2ad988000e1fbe2973d Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 8 Apr 2023 14:54:04 +0400 Subject: [PATCH 005/258] Update to Kotlin 1.8.20 Also update Compose compiler, compose-bom, and Anvil. --- .../src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt | 2 +- .../quran/labs/androidquran/buildutil/DependenciesCommon.kt | 2 +- build.gradle | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 26e22f4854..9f6d205e8a 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -18,7 +18,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin { extensions.configure { applyAndroidCommon() buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.4.3" + composeOptions.kotlinCompilerExtensionVersion = "1.4.4-dev-k1.8.20-f6ae19e64ff" } applyKotlinCommon() diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt index 9bc38751ae..168f636a71 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt @@ -6,6 +6,6 @@ import org.gradle.kotlin.dsl.dependencies fun Project.applyBoms() { dependencies { add("implementation", platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) - add("implementation", platform("androidx.compose:compose-bom:2023.01.00")) + add("implementation", platform("androidx.compose:compose-bom:2023.04.00")) } } diff --git a/build.gradle b/build.gradle index 01e68fed06..0c62f4c793 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlinVersion = '1.8.10' + kotlinVersion = '1.8.20' coroutinesVersion = '1.6.4' daggerVersion = '2.45' @@ -18,7 +18,7 @@ buildscript { materialComponentsVersion = '1.8.0' coreKtxVersion = '1.9.0' - anvilVersion = '2.4.4' + anvilVersion = '2.4.5' moshiVersion = '1.14.0' okioVersion = '3.3.0' retrofitVersion = '2.9.0' From e1ea757cad9a6d05b040b60d5480556c682c7e89 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 8 Apr 2023 19:17:34 +0000 Subject: [PATCH 006/258] Update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.8.20 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 2e49747fd7..0883a2c7fd 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:7.4.2") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") } gradlePlugin { From 687a2feee5f150c73804bdef04565e17a55f0c80 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 17:34:48 +0000 Subject: [PATCH 007/258] Update dependency org.robolectric:robolectric to v4.10 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0c62f4c793..bad54777c0 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { mockitoVersion = '5.2.0' truthVersion = '1.1.3' espressoVersion = '3.5.1' - robolectricVersion = '4.9.2' + robolectricVersion = '4.10' androidxJunitExtVersion = '1.1.5' deps = [ From adb52800f0e3fad834af4032a9320ac836d9a843 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 22:00:10 +0000 Subject: [PATCH 008/258] Update dependency org.mockito:mockito-core to v5.3.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0c62f4c793..0c15ad78c2 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ buildscript { // testing junitVersion = '4.13.2' - mockitoVersion = '5.2.0' + mockitoVersion = '5.3.0' truthVersion = '1.1.3' espressoVersion = '3.5.1' robolectricVersion = '4.9.2' From 5dccb6d62c570fb600ace8367fb83162a1bbc9da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:38:30 +0000 Subject: [PATCH 009/258] Update dependency gradle to v8.1 --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 61608 -> 62076 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 ++++--- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index bdc9a83b1e..0c85a1f751 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710deaf9f98673a68957ea02138b60d0a..c1962a79e29d3e0ab67b14947c167a862655af9b 100644 GIT binary patch delta 8979 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 F{TFmrHLU;u delta 8469 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 Date: Thu, 13 Apr 2023 20:55:49 +0000 Subject: [PATCH 010/258] Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.5 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0c62f4c793..94c63ea544 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:7.4.2" - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.4" + classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.5" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.0.1" classpath "com.google.gms:google-services:4.3.15" From f7c61287bcdb874fc903ce8282457ae1d215a15b Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 15 Apr 2023 18:00:03 +0400 Subject: [PATCH 011/258] Update Android Gradle Plugin to 8.0.0 --- .github/workflows/build.yml | 6 ++++++ app/build.gradle | 6 ++++-- build-logic/convention/build.gradle.kts | 2 +- build.gradle | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca400154ac..30deb30d84 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,6 +58,12 @@ jobs: - name: Checkout the code uses: actions/checkout@v3 + - name: Setup JDK + uses: actions/setup-java@v3 + with: + distribution: zulu + java-version: 17 + - name: Setup Gradle uses: gradle/gradle-build-action@v2 diff --git a/app/build.gradle b/app/build.gradle index 40b3844ded..b9be3ecd52 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -21,6 +21,8 @@ android { testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" } + buildFeatures.buildConfig = true + signingConfigs { release { storeFile file(STORE_FILE) @@ -30,7 +32,7 @@ android { } } - flavorDimensions "pageType" + flavorDimensions = ["pageType"] productFlavors { madani { applicationId "com.quran.labs.androidquran" @@ -58,7 +60,7 @@ android { } } - applicationVariants.all { variant -> + applicationVariants.configureEach { variant -> resValue "string", "authority", applicationId + '.data.QuranDataProvider' resValue "string", "file_authority", applicationId + '.fileprovider' if (applicationId.endsWith("debug")) { diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 0883a2c7fd..f911d2881b 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:7.4.2") + compileOnly("com.android.tools.build:gradle:8.0.0") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") } diff --git a/build.gradle b/build.gradle index f2920d7d15..039d790bca 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ buildscript { } dependencies { - classpath "com.android.tools.build:gradle:7.4.2" + classpath 'com.android.tools.build:gradle:8.0.0' classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.5" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.0.1" From 4a1afda99d739c7371fa4cef315ff6ade02899ac Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 15 Apr 2023 18:30:41 +0400 Subject: [PATCH 012/258] Do not crash when building without Firebase The Gradle flag for disabling Firebase was causing a crash due to us explicitly depending on Firebase for Crashlytics in app. This patch fixes it by making it behind our own CrashReporter interface and returning the real one if we're building with Firebase, or the noop one otherwise. --- app/build.gradle | 1 - .../quran/labs/androidquran/util/RecordingLogTree.kt | 5 ++--- .../main/java/com/quran/analytics/CrashReporter.kt | 6 ++++++ .../com/quran/analytics/provider/AnalyticsModule.kt | 3 +++ .../quran/analytics/provider/NoopCrashReporter.kt | 12 ++++++++++++ .../quran/analytics/provider/SystemCrashReporter.kt | 9 +++++++++ 6 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 common/analytics/src/main/java/com/quran/analytics/CrashReporter.kt create mode 100644 feature/analytics-noop/src/main/java/com/quran/analytics/provider/NoopCrashReporter.kt create mode 100644 feature/analytics-noop/src/main/java/com/quran/analytics/provider/SystemCrashReporter.kt diff --git a/app/build.gradle b/app/build.gradle index 40b3844ded..b93c29383f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -169,7 +169,6 @@ dependencies { implementation "dev.chrisbanes.insetter:insetter:0.6.1" implementation 'com.jakewharton.timber:timber:5.0.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' - implementation 'com.google.firebase:firebase-crashlytics:18.3.6' testImplementation "junit:junit:${junitVersion}" testImplementation "com.google.truth:truth:${truthVersion}" diff --git a/app/src/main/java/com/quran/labs/androidquran/util/RecordingLogTree.kt b/app/src/main/java/com/quran/labs/androidquran/util/RecordingLogTree.kt index 5c60a4675b..1964f062fe 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/RecordingLogTree.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/RecordingLogTree.kt @@ -1,9 +1,8 @@ package com.quran.labs.androidquran.util import android.util.Log -import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.quran.analytics.provider.SystemCrashReporter import timber.log.Timber -import java.lang.StringBuilder import java.util.ArrayDeque import java.util.Deque @@ -30,7 +29,7 @@ class RecordingLogTree : Timber.Tree() { } } - private val crashlytics = FirebaseCrashlytics.getInstance() + private val crashlytics = SystemCrashReporter.crashReporter() // Adding one to the initial size accounts for the add before remove. private val buffer: Deque = ArrayDeque(BUFFER_SIZE + 1) diff --git a/common/analytics/src/main/java/com/quran/analytics/CrashReporter.kt b/common/analytics/src/main/java/com/quran/analytics/CrashReporter.kt new file mode 100644 index 0000000000..e04bd592e7 --- /dev/null +++ b/common/analytics/src/main/java/com/quran/analytics/CrashReporter.kt @@ -0,0 +1,6 @@ +package com.quran.analytics + +interface CrashReporter { + fun log(message: String) + fun recordException(throwable: Throwable) +} diff --git a/feature/analytics-noop/src/main/java/com/quran/analytics/provider/AnalyticsModule.kt b/feature/analytics-noop/src/main/java/com/quran/analytics/provider/AnalyticsModule.kt index 6b70df9ab2..b87af45fa9 100644 --- a/feature/analytics-noop/src/main/java/com/quran/analytics/provider/AnalyticsModule.kt +++ b/feature/analytics-noop/src/main/java/com/quran/analytics/provider/AnalyticsModule.kt @@ -1,11 +1,14 @@ package com.quran.analytics.provider import com.quran.analytics.AnalyticsProvider +import com.quran.analytics.CrashReporter import dagger.Binds import dagger.Module @Module interface AnalyticsModule { + @Binds + fun provideCrashReporter(noopCrashReporter: NoopCrashReporter): CrashReporter @Binds fun provideAnalyticsProvider(noopAnalyticsProvider: NoopAnalyticsProvider): AnalyticsProvider diff --git a/feature/analytics-noop/src/main/java/com/quran/analytics/provider/NoopCrashReporter.kt b/feature/analytics-noop/src/main/java/com/quran/analytics/provider/NoopCrashReporter.kt new file mode 100644 index 0000000000..a8ffb0f3ac --- /dev/null +++ b/feature/analytics-noop/src/main/java/com/quran/analytics/provider/NoopCrashReporter.kt @@ -0,0 +1,12 @@ +package com.quran.analytics.provider + +import com.quran.analytics.CrashReporter +import javax.inject.Inject + +class NoopCrashReporter @Inject constructor() : CrashReporter { + override fun log(message: String) { + } + + override fun recordException(throwable: Throwable) { + } +} diff --git a/feature/analytics-noop/src/main/java/com/quran/analytics/provider/SystemCrashReporter.kt b/feature/analytics-noop/src/main/java/com/quran/analytics/provider/SystemCrashReporter.kt new file mode 100644 index 0000000000..cef09fdd56 --- /dev/null +++ b/feature/analytics-noop/src/main/java/com/quran/analytics/provider/SystemCrashReporter.kt @@ -0,0 +1,9 @@ +package com.quran.analytics.provider + +import com.quran.analytics.CrashReporter + +object SystemCrashReporter { + private val noopCrashReporter by lazy { NoopCrashReporter() } + + fun crashReporter(): CrashReporter = noopCrashReporter +} From 05935424fe576ede7cb8cbeead55273c675cfaac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Apr 2023 22:12:09 +0000 Subject: [PATCH 013/258] Update dependency androidx.activity:activity-compose to v1.7.1 --- feature/downloadmanager/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/downloadmanager/build.gradle b/feature/downloadmanager/build.gradle index 3f55642f89..97d12ffed6 100644 --- a/feature/downloadmanager/build.gradle +++ b/feature/downloadmanager/build.gradle @@ -27,7 +27,7 @@ dependencies { implementation project(path: ':common:ui:core') implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" - implementation "androidx.activity:activity-compose:1.7.0" + implementation "androidx.activity:activity-compose:1.7.1" // dagger implementation deps.dagger.runtime From 9ff257be39d0d4ecd733eb0ff4474733037293b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Apr 2023 22:12:13 +0000 Subject: [PATCH 014/258] Update dependency androidx.fragment:fragment-ktx to v1.5.7 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a28e7f0017..748ce8181a 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { androidxMediaVersion = '1.6.0' androidxAnnotationVersion = '1.6.0' - androidxFragmentVersion = '1.5.6' + androidxFragmentVersion = '1.5.7' androidxPreferencesVersion = '1.2.0' androidxAppcompatVersion = '1.6.1' androidxLocalBroadcastVersion = '1.1.0' From 0e01582284ffebe9399b01b67d645fe05c433fea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:17:57 +0000 Subject: [PATCH 015/258] Update dependency gradle to v8.1.1 --- build-logic/gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index 0c85a1f751..37aef8d3f0 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c85a1f751..37aef8d3f0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 499310bd27efd53175cb8792b2402044079b0c0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 21 Apr 2023 21:04:28 +0000 Subject: [PATCH 016/258] Update dependency org.mockito:mockito-core to v5.3.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a28e7f0017..a3b4bf33a9 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ buildscript { // testing junitVersion = '4.13.2' - mockitoVersion = '5.3.0' + mockitoVersion = '5.3.1' truthVersion = '1.1.3' espressoVersion = '3.5.1' robolectricVersion = '4.10' From 8ca6f0d37945fd84c17ea46db4403e3e5caf7ce6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 22 Apr 2023 01:58:30 +0000 Subject: [PATCH 017/258] Update dependency net.ltgt.gradle:gradle-errorprone-plugin to v3.1.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a28e7f0017..52e9f05cf1 100644 --- a/build.gradle +++ b/build.gradle @@ -57,7 +57,7 @@ buildscript { classpath 'com.android.tools.build:gradle:8.0.0' classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.5" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.0.1" + classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" classpath "com.google.gms:google-services:4.3.15" classpath "app.cash.sqldelight:gradle-plugin:$sqldelightVersion" classpath "com.squareup.anvil:gradle-plugin:$anvilVersion" From 3e6c7181803f9eedbdc3621ef8482ae5e5976b6b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 17:37:47 +0000 Subject: [PATCH 018/258] Update dependency com.android.tools.build:gradle to v8.0.1 --- build-logic/convention/build.gradle.kts | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index f911d2881b..4ac935dcfe 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.0.0") + compileOnly("com.android.tools.build:gradle:8.0.1") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") } diff --git a/build.gradle b/build.gradle index 8be3981fd0..b802d41358 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.0.0' + classpath 'com.android.tools.build:gradle:8.0.1' classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.5" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" From 4bfd76b40b4a971b7d86f385f2c8163db154d8a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 21:22:08 +0000 Subject: [PATCH 019/258] Update dependency org.robolectric:robolectric to v4.10.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b802d41358..25f2fb83be 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { mockitoVersion = '5.3.1' truthVersion = '1.1.3' espressoVersion = '3.5.1' - robolectricVersion = '4.10' + robolectricVersion = '4.10.2' androidxJunitExtVersion = '1.1.5' deps = [ From e04984ae59845b64455c2aa70330f68a3d1c2292 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 21:22:14 +0000 Subject: [PATCH 020/258] Update dependency com.google.android.material:material to v1.9.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b802d41358..df1e6ee335 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ buildscript { androidxRecyclerViewVersion = '1.3.0' supportSqliteVersion = '2.1.0' workManagerVersion = '2.8.1' - materialComponentsVersion = '1.8.0' + materialComponentsVersion = '1.9.0' coreKtxVersion = '1.9.0' anvilVersion = '2.4.5' From f7cbdc60c9125a9d8acbf494cffd29f03ca79bdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 5 May 2023 16:23:50 +0000 Subject: [PATCH 021/258] Update coroutinesVersion to v1.7.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b802d41358..590c0feafc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlinVersion = '1.8.20' - coroutinesVersion = '1.6.4' + coroutinesVersion = '1.7.0' daggerVersion = '2.45' From 81f6c58b5f00180965f7b042a5782494f2a6f62e Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 6 May 2023 02:29:14 +0400 Subject: [PATCH 022/258] Bump Kotlin to 1.8.21 --- .../src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 9f6d205e8a..3b5f73d77f 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -18,7 +18,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin { extensions.configure { applyAndroidCommon() buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.4.4-dev-k1.8.20-f6ae19e64ff" + composeOptions.kotlinCompilerExtensionVersion = "1.4.7" } applyKotlinCommon() diff --git a/build.gradle b/build.gradle index 3c567e8ac3..dddcc345ab 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlinVersion = '1.8.20' + kotlinVersion = '1.8.21' coroutinesVersion = '1.7.0' daggerVersion = '2.45' From 38ed8e5ee031e82a8debe50762930e70df66ee1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 May 2023 18:34:11 +0000 Subject: [PATCH 023/258] Update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.8.21 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 4ac935dcfe..fc4c68fb98 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:8.0.1") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21") } gradlePlugin { From c69eb71e18e2aa570e419f109a1044f45d495065 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 23:28:45 +0000 Subject: [PATCH 024/258] Update dependency com.google.errorprone:error_prone_core to v2.19.0 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c5c73f36f5..a6072e8399 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,7 @@ dependencies { testImplementation "androidx.test.espresso:espresso-core:${espressoVersion}" testImplementation "androidx.test.espresso:espresso-intents:${espressoVersion}" - errorprone 'com.google.errorprone:error_prone_core:2.18.0' + errorprone 'com.google.errorprone:error_prone_core:2.19.0' // Number Picker implementation 'io.github.ShawnLin013:number-picker:2.4.13' From b53fa1d40151b4c89e3fe50f6dd8f4b9bc139ead Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 May 2023 21:04:43 +0000 Subject: [PATCH 025/258] Update dependency com.google.errorprone:error_prone_core to v2.19.1 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a6072e8399..2ef4a31de8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,7 @@ dependencies { testImplementation "androidx.test.espresso:espresso-core:${espressoVersion}" testImplementation "androidx.test.espresso:espresso-intents:${espressoVersion}" - errorprone 'com.google.errorprone:error_prone_core:2.19.0' + errorprone 'com.google.errorprone:error_prone_core:2.19.1' // Number Picker implementation 'io.github.ShawnLin013:number-picker:2.4.13' From b3849630963b74a8445a0f446e0803d9c8fa3157 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 17:46:52 +0000 Subject: [PATCH 026/258] Update dependency org.jetbrains.kotlinx:kotlinx-coroutines-android to v1.7.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d7e283c7db..a5d462010c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlinVersion = '1.8.21' - coroutinesVersion = '1.7.0' + coroutinesVersion = '1.7.1' daggerVersion = '2.45' From 18b13c167d584e2f6ade9c24cbcb1c4ca5e8c238 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 17:46:58 +0000 Subject: [PATCH 027/258] Update moshiVersion to v1.15.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d7e283c7db..f0f2e5f80b 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { coreKtxVersion = '1.9.0' anvilVersion = '2.4.5' - moshiVersion = '1.14.0' + moshiVersion = '1.15.0' okioVersion = '3.3.0' retrofitVersion = '2.9.0' sqldelightVersion = '2.0.0-alpha05' From ddc0e95064845c75ef8188a7a266447f48ec9a7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 May 2023 22:21:04 +0000 Subject: [PATCH 028/258] Update daggerVersion to v2.46.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d7e283c7db..7cbca3d35c 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { kotlinVersion = '1.8.21' coroutinesVersion = '1.7.0' - daggerVersion = '2.45' + daggerVersion = '2.46.1' androidxMediaVersion = '1.6.0' androidxAnnotationVersion = '1.6.0' From c523674ead6f4ac45afe32f5da67814f93da09ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 17 May 2023 23:03:28 +0000 Subject: [PATCH 029/258] Update dependency org.robolectric:robolectric to v4.10.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1dc54a2cf2..583ac99e43 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { mockitoVersion = '5.3.1' truthVersion = '1.1.3' espressoVersion = '3.5.1' - robolectricVersion = '4.10.2' + robolectricVersion = '4.10.3' androidxJunitExtVersion = '1.1.5' deps = [ From 237f1cd82902f6e6d6e72a4a71f48496fd6bb206 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 May 2023 03:39:52 +0000 Subject: [PATCH 030/258] Update dependency com.squareup.leakcanary:leakcanary-android to v2.11 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2ef4a31de8..8506a6b671 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,7 +170,7 @@ dependencies { implementation "dev.chrisbanes.insetter:insetter:0.6.1" implementation 'com.jakewharton.timber:timber:5.0.1' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11' testImplementation "junit:junit:${junitVersion}" testImplementation "com.google.truth:truth:${truthVersion}" From cb9b74ea1fd6e5da64fa35ecaa0fe0dde74c6b44 Mon Sep 17 00:00:00 2001 From: Doozy <98877504+DoozyDoz@users.noreply.github.com> Date: Sun, 21 May 2023 13:40:50 +0300 Subject: [PATCH 031/258] added ellipsis to juz' and bookmarks --- .../com/quran/labs/androidquran/ui/fragment/JuzListFragment.kt | 3 ++- .../quran/labs/androidquran/ui/helpers/QuranRowFactory.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/JuzListFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/JuzListFragment.kt index 6581ac9820..e384de197c 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/JuzListFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/JuzListFragment.kt @@ -170,8 +170,9 @@ class JuzListFragment : Fragment() { R.string.sura_ayah_notification_str, quranDisplayData.getSuraName(activity, pos.sura, false), pos.ayah ) + val juzTextWithEllipsis = quarters[i] + "..." val builder = Builder() - .withText(quarters[i]) + .withText(juzTextWithEllipsis) .withMetadata(metadata) .withPage(page) .withJuzType(ENTRY_TYPES[i % 4]) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranRowFactory.java b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranRowFactory.java index af031a8eeb..cafe69d663 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranRowFactory.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranRowFactory.java @@ -76,7 +76,7 @@ public QuranRow fromBookmark(Context context, Bookmark bookmark, Long tagId) { title = quranDisplayData.getAyahString(bookmark.getSura(), bookmark.getAyah(), context); metadata = quranDisplayData.getPageSubtitle(context, bookmark.getPage()); } else { - title = ayahText; + title = ayahText + "..."; metadata = quranDisplayData.getAyahMetadata(bookmark.getSura(), bookmark.getAyah(), bookmark.getPage(), context); } From ed508a7d5aa8d102597436c69ed68b1399467cb9 Mon Sep 17 00:00:00 2001 From: Bach Nguyen Date: Wed, 24 May 2023 11:55:21 +0800 Subject: [PATCH 032/258] Vietnamese Language localization --- app/src/main/res/values-vi/strings.xml | 350 ++++++++++++++++++ app/src/main/res/values-vi/sura_names.xml | 238 ++++++++++++ .../audio/src/main/res/values-vi/readers.xml | 77 ++++ .../src/main/res/values-vi/strings.xml | 12 + .../core/src/main/res/values-vi/strings.xml | 7 + .../src/main/res/values-vi/strings.xml | 32 ++ .../src/main/res/values-vi/strings.xml | 10 + .../madani/src/main/res/values-vi/strings.xml | 5 + 8 files changed, 731 insertions(+) create mode 100644 app/src/main/res/values-vi/strings.xml create mode 100644 app/src/main/res/values-vi/sura_names.xml create mode 100644 common/audio/src/main/res/values-vi/readers.xml create mode 100644 common/toolbar/src/main/res/values-vi/strings.xml create mode 100644 common/ui/core/src/main/res/values-vi/strings.xml create mode 100644 feature/downloadmanager/src/main/res/values-vi/strings.xml create mode 100644 feature/qarilist/src/main/res/values-vi/strings.xml create mode 100644 pages/madani/src/main/res/values-vi/strings.xml diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..2df98f4e9c --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,350 @@ + + + Kinh Quran + Tải xuống tệp cần thiết? + Để Kinh Quran Android hoạt động bình thường, + chúng tôi cần tải xuống một số tệp. Nếu bạn không làm điều này bây giờ, ứng dụng + có thể không hoạt động đáng tin cậy và sẽ yêu cầu kết nối Internet để đọc. + Bạn có muốn tải xuống các tệp cần thiết ngay bây giờ không? + + Gần đây chúng tôi đã thêm hình ảnh cải tiến cho + máy tính bảng. Bạn có muốn tải xuống những hình ảnh này ngay bây giờ không? + Có một cập nhật nhỏ nhưng quan trọng + cho những hình ảnh Kinh Quran bạn có trên thiết bị của mình. Bạn có muốn tải bản vá này + bây giờ không? + + + Không + Đang tải… + Vui lòng đợi tệp tải xuống (hỗ trợ tạm dừng). + Đang xử lý… + Về Chúng tôi + Ứng dụng khác + Cài đặt + Trang cuối + Đánh dấu + Dấu Trang + Dấu Ayah + Sắp xếp + Ngày Thêm + Vị trí ở Quran + Nhóm theo Thẻ + Hiện Ngày + Hiện Bản dịch + Hiện Quran + Trợ giúp + Tìm kiếm + Tải Bản dịch + @string/gotoPage + Đến trang + Xin đợi… + + + Kinh Quran cho Android là một ứng dụng Kinh Quran miễn phí. Xin đừng quên những người đóng góp trong lời cầu nguyện của bạn. + Nguồn Dữ liệu + Ảnh + Các hình ảnh Kinh Quran dựa trên các phông chữ từ King Fahd Quran Printing Complex + Hình ảnh Kinh Quran cho ứng dụng Naskh được lấy (với sự cho phép) từ SHL Info Systems + Hình ảnh Qaloon được sử dụng với sự cho phép của Nous Memes Editions Et Diffusion (Tunisia) + Các bản xướng đọc gapless mp3 Quran + Electronic Moshaf Project + King Saud University Electronic Moshaf Project là nguồn Tafaseer Ả Rập cho app và các bản dịch cho nhiều ngôn ngữ khác nhau. Nó cũng là nguồn cho bản đọc của Tiến sĩ Ayman Suwaid. + Văn bản Uthmani and Tafaseer Ả Rập + Bản dịch cho nhiều ngôn ngữ + Bản dịch cho một vài ngôn ngữ + Phông chữ Noorehira và Bản dịch Mufti Taqi + Dự án Mã nguồn Mở + Khác + Những người đóng góp + Danh sách những người đã đóng góp vào sự phát triển của Kinh Quran cho Android + + + (FAQ) Câu hỏi thường gặp + + <b>Làm cách nào để nghe Quran?</b> + <br/>Mở bất kỳ trang Kinh Quran nào. Chạm vào màn hình một lần. Ở phía dưới, bạn sẽ nhận thấy + một nút phát và một danh sách tên các qari. Nhấp vào tên của qari để + chọn một qari khác. Nhấp vào phát để tải xuống và phát trang hoặc sura hiện tại. + <br/> + <br/><b>Làm cách nào để xem bản dịch?</b> + <br/>Mở bất kỳ trang Kinh Quran nào. Chạm vào màn hình một lần. Ở trên cùng, bạn sẽ thấy một + biểu tượng quả địa cầu (hoặc, nếu bạn không nhìn thấy nó, hãy nhấp vào biểu tượng có ba chấm tròn) - nhấn + nó và chọn bản dịch để xem bản dịch. + <br/> + Nếu bạn không có bất kỳ bản dịch nào được tải xuống, nó sẽ đưa + bạn đến một màn hình nơi bạn có thể tải xuống các bản dịch. Chọn và tải xuống bản dịch, + sau đó quay lại và nhấn lại vào biểu tượng quả địa cầu để xem bản dịch. + <br/> + <br/><b>Làm cách nào để đánh dấu trang?</b> + <br/>Mở bất kỳ trang Kinh Quran nào. Chạm vào màn hình một lần. Ở trên cùng bên phải, bạn sẽ thấy một + biểu tượng dấu trang. Nhấn vào biểu tượng dấu trang để đánh dấu trang (màu sẽ chuyển sang màu trắng đặc). Nhấn vào biểu tượng dấu trang + một lần nữa để xóa dấu trang. + <br/> + <br/><b>Làm cách nào để làm cho văn bản lớn hơn?</b> + <br/>Đối với các trang tiếng Ả Rập, hãy giữ điện thoại của bạn ở chế độ nằm ngang. Điều này làm cho văn bản lớn hơn. + Đối với bản dịch, hãy đi tới cài đặt và tăng cỡ chữ văn bản dịch. + <br/> + <br/><b>Làm cách nào để chia sẻ ayah?</b> + <br/>Khi đang ở bất kỳ trang tiếng Ả Rập nào, hãy nhấn và giữ bất kỳ ayah nào để có menu nơi bạn có thể + chọn đánh dấu, gắn thẻ, chia sẻ hoặc sao chép ayah đó vào khay nhớ tạm, xem bản dịch, + hoặc nghe bản đọc của nó.<br/> + <br/><b>Phông Malayalam/Tamil/Bengali/Urdu không dùng được!</b> + <br/>Rất tiếc, các phiên bản Android trước 4.0 không hỗ trợ các phông chữ này và + chúng tôi không giúp được gì nhiều. + + Tìm trong Quran + Những câu Kinh Quran + Kết quả đầy đủ + Tìm kiếm cả mushaf + + + + Một kết quả cho \"%1$s\": + %2$d kết quả cho \"%1$s\": + + + + Kinh Quran muốn bạn cho phép lưu trữ dữ liệu của nó trên + lưu trữ ngoài. Kinh Quran sẽ vẫn hoạt động mà không cần quyền này, nhưng nếu bạn xóa dữ liệu hoặc + gỡ cài đặt ứng dụng, tất cả các trang, âm thanh và dữ liệu đã tải xuống sẽ bị xóa. Cấp quyền + cho kinh Quran? + Vui lòng khởi động lại ứng dụng để điều này có hiệu lực. + + + Tìm thấy ở Surah %1$s: %2$d (page %3$d) + Không kết quả cho \"%s\" + Bạn chưa tải xuống gói tìm kiếm tiếng Ả Rập. Vui lòng tải xuống và thử tìm kiếm lại. + Tải Gói dữ liệu Tìm kiếm tiếng Ả Rập + + + Điều hướng phím âm lượng + Điều hướng giữa các trang bằng phím âm lượng + Tùy chọn Đọc + Tùy chọn Bản dịch + Cài đặt Hiển thị + Chế độ Ả Rập (الوضع العربي) + Dùng tiếng Ả Rập cho giao diện ứng dụng + Màu nền mới + Khóa hướng màn hình + Khóa một hướng cố định + Tự chuyển hướng cùng thiết bị + Hướng ngang + Luôn dùng chế độ nằm ngang + Luôn dùng chế độ nằm dọc + Chế độ Tối + Dùng nền tối và phông chữ sáng + Độ sáng của chữ + Độ sáng của chữ khi chế độ tối được kích hoạt + Độ sáng nền + Độ sáng của trang khi chế độ tối được kích hoạt + Hiển thị thông tin trang + Hiện số trang, tên surah và số juz\' trong khi đọc + Hiện hộp thoại + Hiển thị hộp thoại khi tới juz\', hizb, v.v kế tiếp + Tô sáng các đánh dấu + Tô sáng ayahs đã được đánh dấu trong khi đọc + Cỡ chữ bản dịch + Bản dịch + Tải xuống và quản lý bản dịch + Ayah trước bản dịch + Hiển thị ayah tiếng Ả Rập phía trên bản dịch + Phông chữ cho chứng khó đọc + Hiển thị bản dịch bằng phông chữ thân thiện với người mắc chứng khó đọc + Tùy chọn Tải xuống + Phát trực tuyến + Phát âm thanh trực tuyến thay vì tải xuống + Số lượng tải xuống + Số lượng tải xuống ưu tiên cho âm thanh non-gapless + Quản lý và tải xuống bản đọc Kinh Quran + Đã tải + Có sẵn để tải xuống + OK + Nơi Kinh Quran lưu dữ liệu + Chọn nơi lưu trữ các tệp Kinh Quran + Gửi logs + Gửi debug logs cho lập trình viên + Bộ nhớ trong + Bộ nhớ ngoài %1$d + Kích cỡ dữ liệu hiện tại là + Tính Kích thước App + Sao chép tệp App + Lỗi di chuyển tệp của app + Không đủ dung lượng để di chuyển tệp app + %1$d MB + Chế độ Trang Kép + Ở chế độ ngang, hai trang sẽ xuất hiện cạnh nhau + Ở chế độ ngang, chỉ một trang sẽ xuất hiện + Tùy chọn Nâng cao + Nhập/xuất đánh dấu, đặt thư mục dữ liệu Kinh Quran, v.v. + Nhập + Nhập đánh dấu và thẻ + Xuất + Xuất bản sao của đánh dấu và thẻ + Xuất CSV + Xuất bản sao của đánh dấu và thẻ dạng CSV + Loại Trang (thử nghiệm) + Chọn loại trang đọc + Hiển thị bản dịch tên của các surah + Dịch tên Surah + Xem trước + + Bản dịch khác + Không thể đọc tệp sao lưu do lỗi quyền. + Tệp sao lưu không hợp lệ (hoặc không thể đọc tệp sao lưu). + Nhập Data + Nếu bạn nhập tệp này, nó sẽ thay thế tất cả đánh dấu của bạn bằng %1$d đánh dấu và %2$d thẻ. Nhập? + Nhập thành công + Lỗi xuất dữ liệu + Dữ liệu được xuất sang %1$s + + Di Lên + Di Xuống + Xóa Bản dịch + + Cảnh báo + Do những hạn chế của Android, nếu bạn chọn đặt dữ liệu Kinh Quran + trên thẻ nhớ SD ngoài và sau đó gỡ cài đặt hoặc xóa dữ liệu của Kinh Quran Android, tất cả các trang và âm thanh + của Kinh Quran Android sẽ bị xóa và bạn sẽ phải tải xuống lại. Bạn có + chắc chắn muốn sử dụng thẻ nhớ SD ngoài? + Do những thay đổi của Android để tăng quyền riêng tư của người dùng, việc sao chép + các tệp bên ngoài thư mục của ứng dụng có thể sẽ ngăn Kinh Quran truy cập dữ liệu của nó trong các phiên bản sau + của Android. Bạn có chắc chắn muốn sử dụng đường dẫn này không? + Vui lòng cấp quyền trong cài đặt ứng dụng + + + + Tafseer của ayah này đã có trong tafseer của ayah %d (Nhấp để mở rộng). + + + Có cập nhật mới + Một bản cập nhật có sẵn cho một số bản dịch của bạn. Truy cập trang bản dịch ngay bây giờ? + + Để sau + + + ¼ + ½ + ¾ + Hizb + Juz\' + Trang + Surahs + Ayah %1$d + %1$s - Ayah %2$s, Juz\' %3$s + Surah %1$s + Surah %1$s, Ayah %2$d + Surah %1$s: %2$d + + Đang tải… + + Đang hủy… + Juz\' %1$s + Trang %1$s, Juz\' %2$s + Tệp cần thiết + Tệp cần thiết + + + Không tìm thấy thẻ SD. Vui lòng gắn nó và thử lại. + Tải xuống thành công + Xử lý tải xuống… + Không tải xuống được + Không đủ dung lượng đĩa để tải xuống + Không tải được do lỗi mạng + Không thể tải xuống do lỗi quyền + Tệp đã tải bị hỏng + Tệp bị hỏng, đang cố tải lại + Lỗi mạng, đang cố gắng tải tiếp… + Đã hủy tải xuống + Bạn đang không dùng Wi-Fi. Vẫn tải xuống dữ liệu? + + + Đang xử lý tập tin %1$d / %2$d + Thử lại + Hủy + Chúng tôi cần tải xuống một hoặc hai tệp nhỏ để hỗ trợ chia sẻ và dịch thuật. Tải ngay? + + + Xóa Bản dịch? + Bạn có chắc chắn muốn xóa %1$s? + Không thể tải xuống danh sách các bản dịch. Vui lòng thử lại sau. + Dữ liệu Tìm kiếm + Bạn chưa tải xuống bản dịch/tafaseer nào. + Tải Bản dịch + + + Nếu câu hỏi của bạn không được trả lời ở trên, bạn có thể gửi email quranandroid@gmail.com để được hỗ trợ. + Xin lưu ý rằng chúng tôi nhận được rất nhiều email, vì vậy chúng tôi không thể trả lời tất cả chúng. + + + Makki + Madani + + Một câu + %s câu + + + + Chỉ phát những câu trên + Lần phát cả bộ câu: + Lần phát từng câu: + Áp dụng + Áp dụng và Phát + Trước + Phát + Nghỉ + Dừng + Tiếp + + + 1 lần + 2 lần + 3 lần + lặp + + + Bắt đầu phát từ: + Đầu Trang + + + Đã sao chép Ayah + + + Đánh dấu Thẻ + Xóa Thẻ + Sửa Thẻ + Thẻ Mới + Chưa được gắn thẻ + + Đã xóa một mục + Đã xóa %d mục + + Hoàn tác + + Vừa Đọc + + + Thẻ + Tên + Tên thẻ không được để trống! + Tên thẻ đã tồn tại! + + + Không có Đánh dấu + + + Cập nhật tệp âm thanh Kinh Quran + Một số tệp âm thanh Kinh Quran đã được cập nhật. Kinh Quran đã loại bỏ + bản sao của các tệp này để có thể tải xuống các phiên bản mới nhất khi bạn phát lần sau. + + + + + + Tùy chọn Trang Kép + Kinh Quran và bản dịch ở chế độ kép + Ở chế độ trang kép có bản dịch, cả trang Kinh Quran và bản dịch được hiển thị + + + Quran Tải xuống + Quran Xướng đọc + diff --git a/app/src/main/res/values-vi/sura_names.xml b/app/src/main/res/values-vi/sura_names.xml new file mode 100644 index 0000000000..b067b980be --- /dev/null +++ b/app/src/main/res/values-vi/sura_names.xml @@ -0,0 +1,238 @@ + + + + Al-Fātihah + Al-Baqarah + Āli-ʿImrān + An-Nisāʾ + Al-Māʾidah + Al-Anʿām + Al-Aʿrāf + Al-Anfāl + At-Tawbah + Yūnus + Hūd + Yūsuf + Ar-Raʿd + Ibrāhīm + Al-Ḥijr + An-Naḥl + Al-Isrāʾ + Al-Kahf + Maryam + Ṭā-Hā + Al-Anbiyāʾ + Al-Ḥajj + Al-Muʾminūn + An-Nūr + Al-Furqān + Ash-Shuʿarāʾ + An-Naml + Al-Qaṣaṣ + Al-ʿAnkabūt + Ar-Rūm + Luqmān + As-Sajdah + Al-Aḥzāb + Sabaʾ + Fāṭir + Yā-Sīn + Aṣ-Ṣāffāt + Ṣād + Az-Zumar + Ghāfir + Fuṣṣilat + Ash-Shūrā + Az-Zukhruf + Ad-Dukhān + Al-Jāthiyah + Al-Aḥqāf + Muḥammad + Al-Fatḥ + Al-Ḥujurāt + Qāf + Adh-Dhāriyāt + Aṭ-Ṭūr + An-Najm + Al-Qamar + Ar-Raḥmān + Al-Wāqiʿah + Al-Ḥadīd + Al-Mujādilah + Al-Ḥashr + Al-Mumtaḥanah + Aṣ-Ṣaff + Al-Jumuʿah + Al-Munāfiqūn + At-Taghābun + Aṭ-Ṭalāq + At-Taḥrīm + Al-Mulk + Al-Qalam + Al-Ḥāqqah + Al-Maʿārij + Nūḥ + Al-Jinn + Al-Muzzammil + Al-Muddaththir + Al-Qiyāmah + Al-Insān + Al-Mursalāt + An-Nabaʾ + An-Nāziʿāt + ʿAbasa + At-Takwīr + Al-Infiṭār + Al-Muṭaffifīn + Al-Inshiqāq + Al-Burūj + Aṭ-Ṭāriq + Al-Aʿlā + Al-Ghāshiyah + Al-Fajr + Al-Balad + Ash-Shams + Al-Layl + Aḍ-Ḍuḥā + Ash-Sharḥ + At-Tīn + Al-ʿAlaq + Al-Qadr + Al-Bayyinah + Az-Zalzalah + Al-ʿĀdiyāt + Al-Qāriʿah + At-Takāthur + Al-ʿAṣr + Al-Humazah + Al-Fīl + Quraysh + Al-Māʿūn + Al-Kawthar + Al-Kāfirūn + An-Naṣr + Al-Masad + Al-Ikhlāṣ + Al-Falaq + An-Nās + + + + + Khai Đề + Con Bò Cái + Gia Đình Imrant + Người Phụ Nữ + Bàn Thức Ăn + Súc Vật + Các Gò Cao + Chiến Lợi Phẩm + Sám Hối + Nabi Jonah (Giô-na) + Nabi Hūd + Nabi Joseph (Giô-sép) + Sấm Sét + Nabi Abraham (Áp-ra-ham) + Vùng Núi Hijr + Con Ong + Đi Trong Đêm + Cái Hang + Maryam (Maria) + Ta-Ha + Các Vị Nabi + Hành Hương Hajj + Người Có Đức Tin + Ánh Sáng + Tiêu Chuẩn Phân Biệt + Các Thi Sĩ + Con Kiến + Những Câu Chuyện + Con Nhện + La Mã + Hiền Nhân Luqmān + Phủ Phục + Liên Quân + Vương Quốc Sheba + Đấng Tạo Hoá + Ya Sin + Những Thiên Thần Đứng Thẳng Hàng + Chữ "Sad" + Những Nhóm Người + Đấng Tha Thứ + Được Giải Thích Chi Tiết + Tham Khảo + Đồ Trang Trí Bằng Vàng + Khói Mờ + Quì Gối + Cồn Cát + Thiên Sứ Muhammad + Thắng Lợi + Nội Phòng + Chữ "Qaf" + Những Cơn Gió Thổi Bụi + Núi Tur + Tinh Tú + Mặt Trăng + Đấng Độ Lượng + Biến Cố Giờ Tận Thế + Sắt + Người Phụ Nữ Khiếu Nại + Cuộc Tập Họp + Cô Ấy Cần Được Kiểm Tra + Hàng Ngũ + Thứ Sáu + Kẻ Đạo Đức Giả + Người Thắng Kẻ Thua + Ly Dị + Cấm Đoán + Quyền Thống Trị + Cây Viết + Sự Thật + Đường Lên Trời + Nabi Noah (Nô-ê) + Loài Jinn + Người Cuộn Mình Trong Chiếc Áo + Người Phủ Áo Choàng Lên Mình + Ngày Phục Sinh + Loài Người + Các Luồng Gió Liên Tục + Các Nguồn Tin + Giật Mạnh + Y Cau Mày + Sự Cuốn Xếp + Phân Tách + Những Kẻ Giân Lận + Vỡ Nứt + Chòm Sao “Burūj” + Sao Mai + Đấng Tối Cao + Biến Cố Trần Ngập + Rạng Đông + Vùng Đất Al-Haram + Mặt Trời + Ban Đêm + Giờ Dhuha + Mở Rộng + Cây Sung + Cục Máu + Đêm Định Mệnh + Bằng Chứng Rõ Ràng + Động Đất + Những Con Chiến Mã + Giờ Kinh Hoàng + Tranh Tiền Của Và Con Cái + Thời Gian + Kẻ Vu Khống + Con Voi + Bộ Tộc Quraish + Vật Dụng Cần Thiết + Sông Kawthar + Kẻ Vô Đức Tin + Phù Trợ + Sợi Dây Được Xe Từ Xơ + Thành Tâm + Bình Minh + Nhân Loại + + diff --git a/common/audio/src/main/res/values-vi/readers.xml b/common/audio/src/main/res/values-vi/readers.xml new file mode 100644 index 0000000000..6dba363b34 --- /dev/null +++ b/common/audio/src/main/res/values-vi/readers.xml @@ -0,0 +1,77 @@ + + + Minshawi Murattal + Husary + Abdurrahman As-Sudais + Nasser Al Qatami + Abd Al-Basit (gapped) + Abd Al-Basit Mujawwad (gapped) + Abdullah Basfar (gapped) + Abdurrahmaan As-Sudais (gapped) + Abu Bakr Ash-Shatri (gapped) + Mishary Al-Afasy (gapped) + Saad Al-Ghamdi (gapped) + Ibrahim Walk (tiếng Anh) + Hani Ar-Rifai (gapped) + Husary Mujawwad (gapped) + Ali Al-Hudhaify (gapped) + Maher Al Muaiqly (gapped) + Minshawy Mujawwad (gapped) + Minshawy Mujawwad + Mohammad al Tablaway (gapped) + Muhammad Ayyoub (gapped) + Muhammad Jibreel (gapped) + Saood Ash-Shuraym (gapped) + Yasser Ad-Dussary (gapped) + Abd Al-Basit + Abd Al-Basit Mujawwad + Aziz Alili + Salah Budair + Saood Ash-Shuraym + Yasser Ad-Dussary + Mohammad al Tablawy + Sahl Yaseen + Abu Bakr Ash-Shatri + Ahmad Nauina + Akram Al-Alaqmi + Ali Hajjaj Alsouasi + Saad Al-Ghamdi + Bandar Baleela + Mahmoud Ali al Bana + Abdulrahman Al-Shahat + Abdur-Rashid Sufi + Mostafa Ismaeel + Husary Broadcast + Abdulaziz Az-Zahrani + Dr. Ayman Suwaid (gapped) + Mishary Al-Afasy + Hani Ar-Rifai + Muhammad Ayyoub + Mishari và Ibrahim Walk (tiếng Anh) + Muhammad Jibreel + Abdullah Basfar + Ibrahim Walk (tiếng Anh) + Mishari Al-Afasy (California) + Ahmed ibn Ali al-Ajamy + Ali Jaber + Maher Al Muaiqly (Haramain) + Abdullah al Juhany + AbdulMuhsin al Qasim + Fares Abbad + Khalifa Taniji + Abdullah Matroud + Salah Bukhatir + Ali Al-Hudhaify + Khaled Al-Muhanna (gapped) + Husary (Mujawwad) + Husary (Muallim) + Ibrahim Al-Akhdar + Maher Al Muaiqly + Yasser Salama (hadr) + Khalid Al-Qahtani + Abdullah Al-Asmari (Bình luận t. Ả Rập) + Abdulrahman Aloosi + Alzain Mohammad Ahmad + Muhammad Rashad Al-Shereef + Wadee\' Al-Yamani + diff --git a/common/toolbar/src/main/res/values-vi/strings.xml b/common/toolbar/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..d65164e353 --- /dev/null +++ b/common/toolbar/src/main/res/values-vi/strings.xml @@ -0,0 +1,12 @@ + + + Đánh dấu Ayah này + Gắn thẻ Ayah này + Chia sẻ Link Ayah + Chia sẻ văn bản Ayah + Bản dịch/Tafseer của Ayah + Phát từ Đây + Đọc từ đây + Ấn nút tìm để tìm một câu kinh + Sao chép Ayah + diff --git a/common/ui/core/src/main/res/values-vi/strings.xml b/common/ui/core/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..74ab382cf6 --- /dev/null +++ b/common/ui/core/src/main/res/values-vi/strings.xml @@ -0,0 +1,7 @@ + + + Xóa + Hủy + Từ + Đến + diff --git a/feature/downloadmanager/src/main/res/values-vi/strings.xml b/feature/downloadmanager/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..ecb7dcb48e --- /dev/null +++ b/feature/downloadmanager/src/main/res/values-vi/strings.xml @@ -0,0 +1,32 @@ + + + + 1 surah đã tải + %1$d surahs đã tải + + + + Quản lý Âm thanh + Xóa Surah? + Bạn có muốn xóa %1$s? + Bạn có chắc là muốn xóa? + + Xóa surah thành công + Xóa %1$d surahs thành công + + + Đã xảy ra lỗi khi xóa surah + Đã xảy ra lỗi khi xóa %1$d surahs + + Tải surah + Xóa surah + Tải tất cả + Tải xuống lựa chọn + Xóa lựa chọn + + Đang tải xuống… + %s MB + Đã tải xuống %1$s / %2$s + Đã tải xuống %1$s / %2$s of surah %3$d + Đang tải xuống surah %1$d ayah %2$d + diff --git a/feature/qarilist/src/main/res/values-vi/strings.xml b/feature/qarilist/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..568f75b59b --- /dev/null +++ b/feature/qarilist/src/main/res/values-vi/strings.xml @@ -0,0 +1,10 @@ + + + Chọn một Qari + Có sẵn để Phát + Qaris với Tải xuống + Gapless (Liền mạch) + Gapped (Ngắt quãng) + Đã chọn + Bỏ qua + diff --git a/pages/madani/src/main/res/values-vi/strings.xml b/pages/madani/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000000..6f09eccf07 --- /dev/null +++ b/pages/madani/src/main/res/values-vi/strings.xml @@ -0,0 +1,5 @@ + + + Madani Mushaf cổ điển + Bản kinh Madani cổ điển. + From bd9970b367f5416fe8062dcc90b9cb32a2791cb0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 May 2023 20:40:23 +0000 Subject: [PATCH 033/258] Update dependency androidx.activity:activity-compose to v1.7.2 --- feature/downloadmanager/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/downloadmanager/build.gradle b/feature/downloadmanager/build.gradle index 97d12ffed6..0b878e78f4 100644 --- a/feature/downloadmanager/build.gradle +++ b/feature/downloadmanager/build.gradle @@ -27,7 +27,7 @@ dependencies { implementation project(path: ':common:ui:core') implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" - implementation "androidx.activity:activity-compose:1.7.1" + implementation "androidx.activity:activity-compose:1.7.2" // dagger implementation deps.dagger.runtime From 87f3a7d051ea5fb6ac768b937c5434997e89689d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 May 2023 01:59:54 +0000 Subject: [PATCH 034/258] Update dependency com.squareup.anvil:gradle-plugin to v2.4.6 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 583ac99e43..125c80c33e 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { materialComponentsVersion = '1.9.0' coreKtxVersion = '1.9.0' - anvilVersion = '2.4.5' + anvilVersion = '2.4.6' moshiVersion = '1.15.0' okioVersion = '3.3.0' retrofitVersion = '2.9.0' From 86a55f91c28d144b71af818f46c8122cb6d490d3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 16:58:38 +0000 Subject: [PATCH 035/258] Update dependency com.google.truth:truth to v1.1.4 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 583ac99e43..55174d7001 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ buildscript { // testing junitVersion = '4.13.2' mockitoVersion = '5.3.1' - truthVersion = '1.1.3' + truthVersion = '1.1.4' espressoVersion = '3.5.1' robolectricVersion = '4.10.3' androidxJunitExtVersion = '1.1.5' From 4a1c6c58517b416e27041d7b54ef811fd4d45434 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 16:58:43 +0000 Subject: [PATCH 036/258] Update sqldelightVersion to v2.0.0-rc01 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 583ac99e43..5a4f83ef86 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ buildscript { moshiVersion = '1.15.0' okioVersion = '3.3.0' retrofitVersion = '2.9.0' - sqldelightVersion = '2.0.0-alpha05' + sqldelightVersion = '2.0.0-rc01' // testing junitVersion = '4.13.2' From a68575b4c22d0d71e8bfcc50fb56678df21d7856 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 23:02:31 +0000 Subject: [PATCH 037/258] Update dependency com.android.tools.build:gradle to v8.0.2 --- build-logic/convention/build.gradle.kts | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index fc4c68fb98..0aa470d336 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.0.1") + compileOnly("com.android.tools.build:gradle:8.0.2") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21") } diff --git a/build.gradle b/build.gradle index 0e873454db..998749300d 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.0.1' + classpath 'com.android.tools.build:gradle:8.0.2' classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.5" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" From 343b70ad897b5e97003012dc354ff0af7bf4acfa Mon Sep 17 00:00:00 2001 From: KuRa KuRd Date: Sat, 3 Jun 2023 19:37:37 +0300 Subject: [PATCH 038/258] Update CONTRIBUTORS.md Update Kurdish language translator to the correct username at GitHub. Note: I'm the translator :) --- CONTRIBUTORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 647ffbb993..a801cccb31 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -41,7 +41,7 @@ Quran Android Contributors * Turkish by Mehmed Mahmudoglu. * Turkish updates by [Shuhrat Dehkanov](http://github.com/ozbek). * Russian by Rinat [Ринат Валеев](https://github.com/Valey). -* Kurdish by [Goran Gharib Karim](https://github.com/GorranKurd). +* Kurdish by [Goran Gharib](https://github.com/GoRaN909). * French by Yasser [yasserkad](http://github.com/yasserkad). * French updates by [Abdullah ibn Nadjo](https://github.com/abdullahibnnadjo). * French updates 1441 Ramadan 13 (06/05/2020) [Saïd B](https://github.com/sbou88). From 4dfc1067162bd665acddd77f19c4441d683ec947 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 19:04:29 +0000 Subject: [PATCH 039/258] Update dependency androidx.fragment:fragment-ktx to v1.6.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1ce500f994..421dec09a9 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { androidxMediaVersion = '1.6.0' androidxAnnotationVersion = '1.6.0' - androidxFragmentVersion = '1.5.7' + androidxFragmentVersion = '1.6.0' androidxPreferencesVersion = '1.2.0' androidxAppcompatVersion = '1.6.1' androidxLocalBroadcastVersion = '1.1.0' From d025367e373d9ecc40e67acf6616078e561f0490 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 8 Jun 2023 02:44:47 +0400 Subject: [PATCH 040/258] Target Java 17 and update Compose BOM --- .../com/quran/labs/androidquran/buildutil/AndroidCommon.kt | 6 +++--- .../quran/labs/androidquran/buildutil/DependenciesCommon.kt | 2 +- .../com/quran/labs/androidquran/buildutil/KotlinCommon.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt index 9447bd32cd..8f0474b735 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt @@ -10,11 +10,11 @@ fun CommonExtension<*, *, *, *>.applyAndroidCommon() { defaultConfig.minSdk = 21 compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } (this as ExtensionAware).extensions.configure("kotlinOptions") { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } } diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt index 168f636a71..1141a18e67 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt @@ -6,6 +6,6 @@ import org.gradle.kotlin.dsl.dependencies fun Project.applyBoms() { dependencies { add("implementation", platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) - add("implementation", platform("androidx.compose:compose-bom:2023.04.00")) + add("implementation", platform("androidx.compose:compose-bom:2023.06.00")) } } diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt index df192ebd4e..920ce681ad 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt @@ -6,6 +6,6 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension fun Project.applyKotlinCommon() { extensions.configure { - jvmToolchain(11) + jvmToolchain(17) } } From c008ea8d14489aa4a53d6efa3ffc904018076678 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 15 Jun 2023 00:32:59 +0400 Subject: [PATCH 041/258] Change PageContentType to a sealed class This allows passing more details to types that need it. --- .../labs/androidquran/presenter/data/QuranDataPresenter.kt | 2 +- .../src/main/java/com/quran/data/source/PageContentType.kt | 5 +++-- .../data/src/main/java/com/quran/data/source/PageProvider.kt | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt index 3904cb0338..d7010cba3d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt @@ -115,7 +115,7 @@ class QuranDataPresenter @Inject internal constructor( fun imagesVersion() = quranPageProvider.getImageVersion() - fun canProceedWithoutDownload() = quranPageProvider.getPageContentType() == PageContentType.IMAGE + fun canProceedWithoutDownload() = quranPageProvider.getPageContentType() == PageContentType.Image fun fallbackToImageType() { val fallbackType = quranPageProvider.getFallbackPageType() diff --git a/common/data/src/main/java/com/quran/data/source/PageContentType.kt b/common/data/src/main/java/com/quran/data/source/PageContentType.kt index 93300b45bb..9601eeaf16 100644 --- a/common/data/src/main/java/com/quran/data/source/PageContentType.kt +++ b/common/data/src/main/java/com/quran/data/source/PageContentType.kt @@ -1,5 +1,6 @@ package com.quran.data.source -enum class PageContentType { - IMAGE, LINE +sealed class PageContentType { + object Image : PageContentType() + data class Line(val ratio: Float, val allowOverlapOfLines: Boolean): PageContentType() } diff --git a/common/data/src/main/java/com/quran/data/source/PageProvider.kt b/common/data/src/main/java/com/quran/data/source/PageProvider.kt index c40ea35d8b..616eb42b18 100644 --- a/common/data/src/main/java/com/quran/data/source/PageProvider.kt +++ b/common/data/src/main/java/com/quran/data/source/PageProvider.kt @@ -26,7 +26,7 @@ interface PageProvider { @StringRes fun getPreviewTitle(): Int @StringRes fun getPreviewDescription(): Int - fun getPageContentType(): PageContentType = PageContentType.IMAGE + fun getPageContentType(): PageContentType = PageContentType.Image fun getFallbackPageType(): String? = null fun getQaris(): List fun pageType(): String = "" From 326fa437bd5cd52d6404296b7297dfd34dd3e708 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jun 2023 20:33:15 +0000 Subject: [PATCH 042/258] Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.6 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 421dec09a9..e99009df93 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.0.2' - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.5" + classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.6" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" classpath "com.google.gms:google-services:4.3.15" From 079500337ce11414a7c7a7ff9a157cd5924d383b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jun 2023 18:15:36 +0000 Subject: [PATCH 043/258] Update dependency com.google.errorprone:error_prone_core to v2.20.0 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 8506a6b671..483753c6bc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -181,7 +181,7 @@ dependencies { testImplementation "androidx.test.espresso:espresso-core:${espressoVersion}" testImplementation "androidx.test.espresso:espresso-intents:${espressoVersion}" - errorprone 'com.google.errorprone:error_prone_core:2.19.1' + errorprone 'com.google.errorprone:error_prone_core:2.20.0' // Number Picker implementation 'io.github.ShawnLin013:number-picker:2.4.13' From aa425c089138ef192a6d3e88185b040b69a33ccf Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 18 Jun 2023 00:56:35 +0400 Subject: [PATCH 044/258] Make footnotes collapsed by default Add logic to collapse all footnotes by default, and allow clicking a footnote to expand it. --- .../ui/helpers/ExpandFootnoteSpan.kt | 14 +++++ .../ui/translation/TranslationAdapter.kt | 57 +++++++++++++++---- .../ui/translation/TranslationViewRow.kt | 29 ++++++++++ 3 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/helpers/ExpandFootnoteSpan.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/ExpandFootnoteSpan.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/ExpandFootnoteSpan.kt new file mode 100644 index 0000000000..4580966292 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/ExpandFootnoteSpan.kt @@ -0,0 +1,14 @@ +package com.quran.labs.androidquran.ui.helpers + +import android.text.style.ClickableSpan +import android.view.View + +class ExpandFootnoteSpan( + private val number: Int, + private val expander: ((View, Int) -> Unit) +) : ClickableSpan() { + + override fun onClick(widget: View) { + expander(widget, number) + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt index 9f3675090f..b404b50cdd 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt @@ -2,14 +2,13 @@ package com.quran.labs.androidquran.ui.translation import android.content.Context import android.graphics.Color -import android.text.Spannable import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned import android.text.method.LinkMovementMethod import android.text.style.ForegroundColorSpan import android.text.style.RelativeSizeSpan -import android.text.style.TextAppearanceSpan +import android.text.style.SuperscriptSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -24,11 +23,13 @@ import com.quran.data.model.highlight.HighlightType import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils +import com.quran.labs.androidquran.ui.helpers.ExpandFootnoteSpan import com.quran.labs.androidquran.ui.helpers.ExpandTafseerSpan import com.quran.labs.androidquran.ui.helpers.HighlightTypes import com.quran.labs.androidquran.ui.helpers.UthmaniSpan import com.quran.labs.androidquran.ui.util.TypefaceManager import com.quran.labs.androidquran.util.QuranSettings +import com.quran.labs.androidquran.util.QuranUtils import com.quran.labs.androidquran.view.AyahNumberView import com.quran.labs.androidquran.view.DividerView import kotlin.math.ln1p @@ -61,6 +62,7 @@ internal class TranslationAdapter( private val expandedTafseerAyahs = mutableSetOf>() private val expandedHyperlinks = mutableSetOf>() + private val expandedFootnotes = mutableMapOf>() private val defaultClickListener = View.OnClickListener { this.handleClick(it) } private val defaultLongClickListener = View.OnLongClickListener { this.selectVerseRows(it) } @@ -340,18 +342,22 @@ internal class TranslationAdapter( holder.text.setOnClickListener(expandHyperlinkClickListener) } - val spannable = SpannableString(row.data) + val spannableBuilder = SpannableStringBuilder(row.data) + row.ayat.forEach { range -> val span = ForegroundColorSpan(inlineAyahColor) - spannable.setSpan(span, range.first, range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannableBuilder.setSpan(span, range.first, range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } - row.footnotes.forEach { range -> - val span = RelativeSizeSpan(0.7f) - val colorSpan = ForegroundColorSpan(footnoteColor) - spannable.setSpan(span, range.first, range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - spannable.setSpan(colorSpan, range.first, range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - } + val expandedFootnotes = expandedFootnotes[row.ayahInfo] ?: emptyList() + val spannable = SpannableString( + row.footnoteCognizantText( + spannableBuilder, + expandedFootnotes, + ::collapsedFootnoteSpan, + ::expandedFootnote + ) + ) when { row.link != null && !expandHyperlink -> getAyahLink(row.link) @@ -420,6 +426,37 @@ internal class TranslationAdapter( updateHighlight(row, holder) } + private fun collapsedFootnoteSpan(number: Int): SpannableString { + val text = QuranUtils.getLocalizedNumber(context, number) + val spannable = SpannableString(text) + spannable.setSpan(SuperscriptSpan(), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(RelativeSizeSpan(0.7f), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(ExpandFootnoteSpan(number, ::expandFootnote), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable + } + + private fun expandedFootnote( + spannableStringBuilder: SpannableStringBuilder, + start: Int, + end: Int + ): SpannableStringBuilder { + val span = RelativeSizeSpan(0.7f) + val colorSpan = ForegroundColorSpan(footnoteColor) + spannableStringBuilder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannableStringBuilder.setSpan(colorSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannableStringBuilder + } + + private fun expandFootnote(view: View, number: Int) { + val position = recyclerView.getChildAdapterPosition(view) + if (position != RecyclerView.NO_POSITION) { + val data = data[position] + val expanded = expandedFootnotes[data.ayahInfo] ?: listOf() + expandedFootnotes[data.ayahInfo] = expanded + number + notifyItemChanged(position) + } + } + private fun getAyahLink(link: SuraAyah): CharSequence { return context.getString(R.string.see_tafseer_of_verse, link.ayah) } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt index cce87049eb..d1c94637f2 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt @@ -1,5 +1,7 @@ package com.quran.labs.androidquran.ui.translation +import android.text.SpannableString +import android.text.SpannableStringBuilder import androidx.annotation.IntDef import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.common.QuranAyahInfo @@ -15,6 +17,33 @@ internal class TranslationViewRow @JvmOverloads constructor( val ayat: List = emptyList(), val footnotes: List = emptyList() ) { + + fun footnoteCognizantText( + spannableStringBuilder: SpannableStringBuilder, + expandedFootnotes: List, + collapsedFootnoteSpannableStyler: ((Int) -> SpannableString), + expandedFootnoteSpannableStyler: ((SpannableStringBuilder, Int, Int) -> SpannableStringBuilder) + ): CharSequence { + val data = data + return if (data != null) { + val ranges = footnotes.sortedByDescending { it.last } + ranges.foldIndexed(spannableStringBuilder) { index, builder, range -> + val number = ranges.size - index + if (number !in expandedFootnotes) { + builder.replace( + range.first, + range.last + 1, + collapsedFootnoteSpannableStyler(number) + ) + } else { + expandedFootnoteSpannableStyler(builder, range.first, range.last + 1) + } + } + } else { + "" + } + } + @IntDef( Type.BASMALLAH, Type.SURA_HEADER, From a55584f32931723f2928fb725e8de437ae1baf5f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Jun 2023 11:17:19 +0000 Subject: [PATCH 045/258] Update dependency org.mockito:mockito-core to v5.4.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e99009df93..6403de9434 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ buildscript { // testing junitVersion = '4.13.2' - mockitoVersion = '5.3.1' + mockitoVersion = '5.4.0' truthVersion = '1.1.4' espressoVersion = '3.5.1' robolectricVersion = '4.10.3' From d8682534138c10c3bcdb5dacfd5ef6795e4de985 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 18 Jun 2023 15:44:48 +0400 Subject: [PATCH 046/258] Kotlin-ify InlineTranslationView --- .../view/InlineTranslationView.java | 153 ------------------ .../view/InlineTranslationView.kt | 141 ++++++++++++++++ 2 files changed, 141 insertions(+), 153 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.java b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.java deleted file mode 100644 index 5fdb8953a1..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.quran.labs.androidquran.view; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.Typeface; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.style.StyleSpan; -import android.util.AttributeSet; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.StyleRes; - -import com.quran.labs.androidquran.R; -import com.quran.labs.androidquran.common.LocalTranslation; -import com.quran.labs.androidquran.common.QuranAyahInfo; -import com.quran.labs.androidquran.common.TranslationMetadata; -import com.quran.labs.androidquran.util.QuranSettings; - -import java.util.List; - -public class InlineTranslationView extends ScrollView { - private Context context; - private Resources resources; - private int leftRightMargin; - private int topBottomMargin; - @StyleRes private int textStyle; - private int fontSize; - private int footerSpacerHeight; - - private LocalTranslation[] translations; - private List ayat; - - private LinearLayout linearLayout; - - public InlineTranslationView(Context context) { - this(context, null); - } - - public InlineTranslationView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public InlineTranslationView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - init(context); - } - - private void init(Context context) { - this.context = context; - - setFillViewport(true); - linearLayout = new LinearLayout(context); - linearLayout.setOrientation(LinearLayout.VERTICAL); - addView(linearLayout, ScrollView.LayoutParams.MATCH_PARENT, - ScrollView.LayoutParams.WRAP_CONTENT); - - resources = getResources(); - leftRightMargin = resources.getDimensionPixelSize(R.dimen.translation_left_right_margin); - topBottomMargin = resources.getDimensionPixelSize(R.dimen.translation_top_bottom_margin); - footerSpacerHeight = resources.getDimensionPixelSize(R.dimen.translation_footer_spacer); - initResources(); - } - - private void initResources() { - QuranSettings settings = QuranSettings.getInstance(context); - fontSize = settings.getTranslationTextSize(); - textStyle = R.style.TranslationText; - } - - public void refresh() { - if (ayat != null && translations != null) { - initResources(); - setAyahs(translations, ayat); - } - } - - public void setAyahs(LocalTranslation[] translations, List ayat) { - linearLayout.removeAllViews(); - if (ayat.size() > 0 && ayat.get(0).texts.size() > 0) { - this.ayat = ayat; - this.translations = translations; - - for (int i = 0, ayatSize = ayat.size(); i < ayatSize; i++) { - addTextForAyah(translations, ayat.get(i)); - } - addFooterSpacer(); - this.scrollTo(0, 0); - } - } - - private void addFooterSpacer() { - final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LayoutParams.MATCH_PARENT, footerSpacerHeight); - final View view = new View(context); - linearLayout.addView(view, params); - } - - private void addTextForAyah(LocalTranslation[] translations, QuranAyahInfo ayah) { - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - params.setMargins(leftRightMargin, topBottomMargin, leftRightMargin, topBottomMargin); - - final int suraNumber = ayah.sura; - final int ayahNumber = ayah.ayah; - TextView ayahHeader = new TextView(context); - ayahHeader.setTextColor(Color.WHITE); - ayahHeader.setTextSize(fontSize); - ayahHeader.setTypeface(null, Typeface.BOLD); - ayahHeader.setText(resources.getString(R.string.sura_ayah, suraNumber, ayahNumber)); - linearLayout.addView(ayahHeader, params); - - TextView ayahView = new TextView(context); - ayahView.setTextAppearance(context, textStyle); - ayahView.setTextColor(Color.WHITE); - ayahView.setTextSize(fontSize); - - // translation - boolean showHeader = translations.length > 1; - SpannableStringBuilder builder = new SpannableStringBuilder(); - for (int i = 0; i < translations.length; i++) { - final TranslationMetadata translationMetadata = ayah.texts.get(i); - final CharSequence translationText = translationMetadata.getText(); - if (!TextUtils.isEmpty(translationText)) { - if (showHeader) { - if (i > 0) { - builder.append("\n\n"); - } - int start = builder.length(); - builder.append(translations[i].getTranslatorName()); - builder.setSpan(new StyleSpan(Typeface.BOLD), - start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - builder.append("\n\n"); - } - - // irrespective of whether it's a link or not, show the text - builder.append(translationText); - } - } - ayahView.append(builder); - - params = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); - params.setMargins(leftRightMargin, topBottomMargin, leftRightMargin, topBottomMargin); - ayahView.setTextIsSelectable(true); - linearLayout.addView(ayahView, params); - } - -} diff --git a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt new file mode 100644 index 0000000000..f0edb9ee97 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt @@ -0,0 +1,141 @@ +package com.quran.labs.androidquran.view + +import android.content.Context +import android.graphics.Color +import android.graphics.Typeface +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextUtils +import android.text.style.StyleSpan +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView +import androidx.annotation.StyleRes +import com.quran.labs.androidquran.R +import com.quran.labs.androidquran.common.LocalTranslation +import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.util.QuranSettings + +class InlineTranslationView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : ScrollView(context, attrs, defStyle) { + private var leftRightMargin = 0 + private var topBottomMargin = 0 + + @StyleRes + private var textStyle = 0 + private var fontSize = 0 + private var footerSpacerHeight = 0 + + private lateinit var linearLayout: LinearLayout + + private var ayat: List? = null + private var translations: Array? = null + + init { + init(context) + } + + private fun init(context: Context) { + isFillViewport = true + linearLayout = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + } + addView(linearLayout, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + leftRightMargin = resources.getDimensionPixelSize(R.dimen.translation_left_right_margin) + topBottomMargin = resources.getDimensionPixelSize(R.dimen.translation_top_bottom_margin) + footerSpacerHeight = resources.getDimensionPixelSize(R.dimen.translation_footer_spacer) + initResources() + } + + private fun initResources() { + val settings = QuranSettings.getInstance(context) + fontSize = settings.translationTextSize + textStyle = R.style.TranslationText + } + + fun refresh() { + val ayat = ayat + val translations = translations + if (ayat != null && translations != null) { + initResources() + setAyahs(translations, ayat) + } + } + + fun setAyahs(translations: Array, ayat: List) { + linearLayout.removeAllViews() + if (ayat.isNotEmpty() && ayat[0].texts.size > 0) { + this.ayat = ayat + this.translations = translations + var i = 0 + val ayatSize = ayat.size + while (i < ayatSize) { + addTextForAyah(translations, ayat[i]) + i++ + } + addFooterSpacer() + scrollTo(0, 0) + } + } + + private fun addFooterSpacer() { + val params = LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, footerSpacerHeight + ) + val view = View(context) + linearLayout.addView(view, params) + } + + private fun addTextForAyah(translations: Array, ayah: QuranAyahInfo) { + var params = LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT + ) + params.setMargins(leftRightMargin, topBottomMargin, leftRightMargin, topBottomMargin) + val suraNumber = ayah.sura + val ayahNumber = ayah.ayah + val ayahHeader = TextView(context) + ayahHeader.setTextColor(Color.WHITE) + ayahHeader.textSize = fontSize.toFloat() + ayahHeader.setTypeface(null, Typeface.BOLD) + ayahHeader.text = context.resources.getString(R.string.sura_ayah, suraNumber, ayahNumber) + linearLayout.addView(ayahHeader, params) + val ayahView = TextView(context) + ayahView.setTextAppearance(context, textStyle) + ayahView.setTextColor(Color.WHITE) + ayahView.textSize = fontSize.toFloat() + + // translation + val showHeader = translations.size > 1 + val builder = SpannableStringBuilder() + for (i in translations.indices) { + val (_, _, translationText) = ayah.texts[i] + if (!TextUtils.isEmpty(translationText)) { + if (showHeader) { + if (i > 0) { + builder.append("\n\n") + } + val start = builder.length + builder.append(translations[i].getTranslatorName()) + builder.setSpan( + StyleSpan(Typeface.BOLD), + start, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + builder.append("\n\n") + } + + // irrespective of whether it's a link or not, show the text + builder.append(translationText) + } + } + ayahView.append(builder) + params = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + params.setMargins(leftRightMargin, topBottomMargin, leftRightMargin, topBottomMargin) + ayahView.setTextIsSelectable(true) + linearLayout.addView(ayahView, params) + } +} From f597d80d65489bcff8e6359f73b4ea2473ef881c Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 18 Jun 2023 16:45:41 +0400 Subject: [PATCH 047/258] Support coloring of ayat in inline translation This fixes #2249. --- .../common/TranslationMetadata.kt | 22 +++++++++- .../ui/helpers/TranslationFootnoteHelper.kt | 34 ++++++++++++++ .../ui/translation/TranslationViewRow.kt | 29 +++++------- .../view/InlineTranslationView.kt | 44 ++++++++++++++++++- 4 files changed, 108 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/helpers/TranslationFootnoteHelper.kt 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 9a7bdfcddf..8ebb582b89 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 @@ -1,6 +1,9 @@ package com.quran.labs.androidquran.common +import android.text.SpannableString +import android.text.SpannableStringBuilder import com.quran.data.model.SuraAyah +import com.quran.labs.androidquran.ui.helpers.TranslationFootnoteHelper data class TranslationMetadata( val sura: Int, @@ -11,4 +14,21 @@ data class TranslationMetadata( val linkPageNumber: Int? = null, val ayat: List = emptyList(), val footnotes: List = emptyList() -) +) { + + fun footnoteCognizantText( + spannableStringBuilder: SpannableStringBuilder, + expandedFootnotes: List, + collapsedFootnoteSpannableStyler: ((Int) -> SpannableString), + expandedFootnoteSpannableStyler: ((SpannableStringBuilder, Int, Int) -> SpannableStringBuilder) + ): CharSequence { + return TranslationFootnoteHelper.footnoteCognizantText( + text, + footnotes, + spannableStringBuilder, + expandedFootnotes, + collapsedFootnoteSpannableStyler, + expandedFootnoteSpannableStyler + ) + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/TranslationFootnoteHelper.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/TranslationFootnoteHelper.kt new file mode 100644 index 0000000000..f1bae49d0c --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/TranslationFootnoteHelper.kt @@ -0,0 +1,34 @@ +package com.quran.labs.androidquran.ui.helpers + +import android.text.SpannableString +import android.text.SpannableStringBuilder + +object TranslationFootnoteHelper { + + fun footnoteCognizantText( + data: CharSequence?, + footnotes: List, + spannableStringBuilder: SpannableStringBuilder, + expandedFootnotes: List, + collapsedFootnoteSpannableStyler: ((Int) -> SpannableString), + expandedFootnoteSpannableStyler: ((SpannableStringBuilder, Int, Int) -> SpannableStringBuilder) + ): CharSequence { + return if (data != null) { + val ranges = footnotes.sortedByDescending { it.last } + ranges.foldIndexed(spannableStringBuilder) { index, builder, range -> + val number = ranges.size - index + if (number !in expandedFootnotes) { + builder.replace( + range.first, + range.last + 1, + collapsedFootnoteSpannableStyler(number) + ) + } else { + expandedFootnoteSpannableStyler(builder, range.first, range.last + 1) + } + } + } else { + "" + } + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt index d1c94637f2..055fd7ad02 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt @@ -5,6 +5,7 @@ import android.text.SpannableStringBuilder import androidx.annotation.IntDef import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.ui.helpers.TranslationFootnoteHelper internal class TranslationViewRow @JvmOverloads constructor( @field:Type val type: Int, @@ -15,7 +16,7 @@ internal class TranslationViewRow @JvmOverloads constructor( val linkPage: Int? = null, val isArabic: Boolean = false, val ayat: List = emptyList(), - val footnotes: List = emptyList() + private val footnotes: List = emptyList() ) { fun footnoteCognizantText( @@ -24,24 +25,14 @@ internal class TranslationViewRow @JvmOverloads constructor( collapsedFootnoteSpannableStyler: ((Int) -> SpannableString), expandedFootnoteSpannableStyler: ((SpannableStringBuilder, Int, Int) -> SpannableStringBuilder) ): CharSequence { - val data = data - return if (data != null) { - val ranges = footnotes.sortedByDescending { it.last } - ranges.foldIndexed(spannableStringBuilder) { index, builder, range -> - val number = ranges.size - index - if (number !in expandedFootnotes) { - builder.replace( - range.first, - range.last + 1, - collapsedFootnoteSpannableStyler(number) - ) - } else { - expandedFootnoteSpannableStyler(builder, range.first, range.last + 1) - } - } - } else { - "" - } + return TranslationFootnoteHelper.footnoteCognizantText( + data, + footnotes, + spannableStringBuilder, + expandedFootnotes, + collapsedFootnoteSpannableStyler, + expandedFootnoteSpannableStyler + ) } @IntDef( diff --git a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt index f0edb9ee97..ea30f212ff 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt @@ -3,9 +3,11 @@ package com.quran.labs.androidquran.view import android.content.Context import android.graphics.Color import android.graphics.Typeface +import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextUtils +import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.util.AttributeSet import android.view.View @@ -13,9 +15,11 @@ import android.widget.LinearLayout import android.widget.ScrollView import android.widget.TextView import androidx.annotation.StyleRes +import androidx.core.content.ContextCompat import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.common.TranslationMetadata import com.quran.labs.androidquran.util.QuranSettings class InlineTranslationView @JvmOverloads constructor( @@ -30,6 +34,7 @@ class InlineTranslationView @JvmOverloads constructor( private var textStyle = 0 private var fontSize = 0 private var footerSpacerHeight = 0 + private var inlineAyahColor: Int = 0 private lateinit var linearLayout: LinearLayout @@ -56,6 +61,7 @@ class InlineTranslationView @JvmOverloads constructor( val settings = QuranSettings.getInstance(context) fontSize = settings.translationTextSize textStyle = R.style.TranslationText + inlineAyahColor = ContextCompat.getColor(context, R.color.translation_translator_color) } fun refresh() { @@ -129,7 +135,7 @@ class InlineTranslationView @JvmOverloads constructor( } // irrespective of whether it's a link or not, show the text - builder.append(translationText) + builder.append(stylize(ayah.texts[i], translationText)) } } ayahView.append(builder) @@ -138,4 +144,40 @@ class InlineTranslationView @JvmOverloads constructor( ayahView.setTextIsSelectable(true) linearLayout.addView(ayahView, params) } + + private fun collapsedFootnoteSpan(number: Int): SpannableString { + return SpannableString("") + } + + private fun expandedFootnote( + spannableStringBuilder: SpannableStringBuilder, + start: Int, + end: Int + ): SpannableStringBuilder { + return spannableStringBuilder + } + + private fun stylize( + metadata: TranslationMetadata, + translationText: String + ): CharSequence { + val spannableStringBuilder = SpannableStringBuilder(translationText) + + metadata.ayat.forEach { range -> + val span = ForegroundColorSpan(inlineAyahColor) + spannableStringBuilder.setSpan( + span, + range.first, + range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + return metadata.footnoteCognizantText( + spannableStringBuilder, + listOf(), + ::collapsedFootnoteSpan, + ::expandedFootnote + ) + } } From 180b2de7e01e12b7ce8e6bbe40ce349fa77be7e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 18 Jun 2023 16:32:07 +0000 Subject: [PATCH 048/258] Update dependency com.google.truth:truth to v1.1.5 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 6403de9434..b251893232 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ buildscript { // testing junitVersion = '4.13.2' mockitoVersion = '5.4.0' - truthVersion = '1.1.4' + truthVersion = '1.1.5' espressoVersion = '3.5.1' robolectricVersion = '4.10.3' androidxJunitExtVersion = '1.1.5' From 3eb1054b89528b660d842551dd6f7afd01093bd7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Jun 2023 13:27:46 +0000 Subject: [PATCH 049/258] Update coroutinesVersion to v1.7.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b251893232..f5cfb9b03f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlinVersion = '1.8.21' - coroutinesVersion = '1.7.1' + coroutinesVersion = '1.7.2' daggerVersion = '2.46.1' From 3ca833b352fc0115791dfb4a23501793b0cd4e19 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 29 Jun 2023 17:39:55 +0400 Subject: [PATCH 050/258] Update Kotlin to 1.8.22 --- build-logic/convention/build.gradle.kts | 2 +- .../src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt | 2 +- build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 0aa470d336..9f03a63ec0 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:8.0.2") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22") } gradlePlugin { diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 3b5f73d77f..fc13f5e240 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -18,7 +18,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin { extensions.configure { applyAndroidCommon() buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.4.7" + composeOptions.kotlinCompilerExtensionVersion = "1.4.8" } applyKotlinCommon() diff --git a/build.gradle b/build.gradle index f5cfb9b03f..a3838815e8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlinVersion = '1.8.21' + kotlinVersion = '1.8.22' coroutinesVersion = '1.7.2' daggerVersion = '2.46.1' From 1f728cc909cef191b239767d357eaa97f7cfd066 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Jun 2023 17:33:14 +0000 Subject: [PATCH 051/258] Update sqldelightVersion to v2.0.0-rc02 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a3838815e8..2c089fd351 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ buildscript { moshiVersion = '1.15.0' okioVersion = '3.3.0' retrofitVersion = '2.9.0' - sqldelightVersion = '2.0.0-rc01' + sqldelightVersion = '2.0.0-rc02' // testing junitVersion = '4.13.2' From 42013daec732a6742bc8513725c7d0353df15503 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Jun 2023 22:45:24 +0000 Subject: [PATCH 052/258] Update dependency com.squareup.leakcanary:leakcanary-android to v2.12 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 483753c6bc..f558b2f51d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,7 +170,7 @@ dependencies { implementation "dev.chrisbanes.insetter:insetter:0.6.1" implementation 'com.jakewharton.timber:timber:5.0.1' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.11' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' testImplementation "junit:junit:${junitVersion}" testImplementation "com.google.truth:truth:${truthVersion}" From 42d50004cb561e6934dae9f05fbd42c73d461a96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 Jun 2023 19:08:37 +0000 Subject: [PATCH 053/258] Update dependency gradle to v8.2 --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 62076 -> 63375 bytes gradle/wrapper/gradle-wrapper.properties | 3 ++- gradlew | 5 ++++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f0..a363877439 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c1962a79e29d3e0ab67b14947c167a862655af9b..033e24c4cdf41af1ab109bc7f253b2b887023340 100644 GIT binary patch delta 16170 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 delta 14892 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d3f0..62f495dfed 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index aeb74cbb43..fcb6fca147 100755 --- a/gradlew +++ b/gradlew @@ -130,10 +130,13 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. From 90cbfc744839a66710b9def05e333770de2ba7df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 7 Jul 2023 15:13:29 +0000 Subject: [PATCH 054/258] Update okioVersion to v3.4.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2c089fd351..72589c9157 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { anvilVersion = '2.4.6' moshiVersion = '1.15.0' - okioVersion = '3.3.0' + okioVersion = '3.4.0' retrofitVersion = '2.9.0' sqldelightVersion = '2.0.0-rc02' From a6504605335ef5570a6033baa1f2ff582979ade9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:25:56 +0000 Subject: [PATCH 055/258] Update dependency gradle to v8.2.1 --- build-logic/gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index a363877439..17a8ddce2d 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 62f495dfed..9f4197d5f4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 495557afc86dd62bf0398bbccc63dc9e8edd54d4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 19:26:56 +0000 Subject: [PATCH 056/258] Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.7 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 72589c9157..96d1196a01 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.0.2' - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.6" + classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.7" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" classpath "com.google.gms:google-services:4.3.15" From b1dd9580a5fe0c0766249c7d93f956460cda60da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Jul 2023 01:43:08 +0000 Subject: [PATCH 057/258] Update daggerVersion to v2.47 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 72589c9157..d53e045a79 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { kotlinVersion = '1.8.22' coroutinesVersion = '1.7.2' - daggerVersion = '2.46.1' + daggerVersion = '2.47' androidxMediaVersion = '1.6.0' androidxAnnotationVersion = '1.6.0' From e591457373edd98085f4830e0fd5fd1a6f2c1c4c Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 15 Jul 2023 23:41:40 +0400 Subject: [PATCH 058/258] Update Kotlin to 1.9.0 Combine lint options for all Android projects to share Lint exclusions. --- app/build.gradle | 6 ------ .../main/kotlin/AndroidApplicationConventionPlugin.kt | 2 +- .../main/kotlin/AndroidLibraryComposeConventionPlugin.kt | 6 ++++-- .../src/main/kotlin/AndroidLibraryConventionPlugin.kt | 2 +- .../quran/labs/androidquran/buildutil/AndroidCommon.kt | 9 ++++++++- build.gradle | 2 +- common/ui/core/build.gradle | 1 - feature/downloadmanager/build.gradle | 1 - feature/qarilist/build.gradle | 1 - gradle.properties | 3 +++ app/lint.xml => lint.xml | 0 11 files changed, 18 insertions(+), 15 deletions(-) rename app/lint.xml => lint.xml (100%) diff --git a/app/build.gradle b/app/build.gradle index f558b2f51d..2b52fcc209 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,12 +86,6 @@ android { } } - lint { - checkReleaseBuilds true - enable 'Interoperability' - lintConfig file('lint.xml') - } - packagingOptions { resources { excludes += ['META-INF/*.kotlin_module', 'META-INF/DEPENDENCIES', 'META-INF/INDEX.LIST'] diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 2c842d673c..d93bae5ccb 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -17,7 +17,7 @@ class AndroidApplicationConventionPlugin : Plugin { } extensions.configure { - applyAndroidCommon() + applyAndroidCommon(target) defaultConfig.targetSdk = 32 } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index fc13f5e240..2e5cccd5dc 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -13,12 +13,14 @@ class AndroidLibraryComposeConventionPlugin : Plugin { with(target) { with(pluginManager) { apply("com.android.library") + apply("org.jetbrains.kotlin.android") } extensions.configure { - applyAndroidCommon() + applyAndroidCommon(target) + defaultConfig.targetSdk = 32 buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.4.8" + composeOptions.kotlinCompilerExtensionVersion = "1.5.0-dev-k1.9.0-6a60475e07f" } applyKotlinCommon() diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 1ba14fe76d..853767b1ef 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -16,7 +16,7 @@ class AndroidLibraryConventionPlugin : Plugin { } extensions.configure { - applyAndroidCommon() + applyAndroidCommon(target) defaultConfig.targetSdk = 32 } diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt index 8f0474b735..ecb47b38e9 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt @@ -2,10 +2,11 @@ package com.quran.labs.androidquran.buildutil import com.android.build.api.dsl.CommonExtension import org.gradle.api.JavaVersion +import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions -fun CommonExtension<*, *, *, *>.applyAndroidCommon() { +fun CommonExtension<*, *, *, *>.applyAndroidCommon(project: Project) { compileSdk = 33 defaultConfig.minSdk = 21 @@ -17,4 +18,10 @@ fun CommonExtension<*, *, *, *>.applyAndroidCommon() { (this as ExtensionAware).extensions.configure("kotlinOptions") { jvmTarget = JavaVersion.VERSION_17.toString() } + + lint { + checkReleaseBuilds = true + enable.add("Interoperability") + lintConfig = project.rootProject.file("lint.xml") + } } diff --git a/build.gradle b/build.gradle index fba89c2ed3..9102beaead 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlinVersion = '1.8.22' + kotlinVersion = '1.9.0' coroutinesVersion = '1.7.2' daggerVersion = '2.47' diff --git a/common/ui/core/build.gradle b/common/ui/core/build.gradle index a546adeca9..f881415c9c 100644 --- a/common/ui/core/build.gradle +++ b/common/ui/core/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'quran.android.library.android' id 'quran.android.library.compose' } diff --git a/feature/downloadmanager/build.gradle b/feature/downloadmanager/build.gradle index 0b878e78f4..07dfb7327d 100644 --- a/feature/downloadmanager/build.gradle +++ b/feature/downloadmanager/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'quran.android.library.android' id 'quran.android.library.compose' id 'com.squareup.anvil' } diff --git a/feature/qarilist/build.gradle b/feature/qarilist/build.gradle index c7140f2caa..b3b788217a 100644 --- a/feature/qarilist/build.gradle +++ b/feature/qarilist/build.gradle @@ -1,5 +1,4 @@ plugins { - id 'quran.android.library.android' id 'quran.android.library.compose' id 'com.squareup.anvil' } diff --git a/gradle.properties b/gradle.properties index 6f9754e719..2c43635e7a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,6 @@ android.enableResourceOptimizations=true android.uniquePackageNames=true android.enableAppCompileTimeRClass=true android.experimental.enableNewResourceShrinker=true + +# work around https://issuetracker.google.com/issues/185418482 +android.experimental.lint.version=8.2.0-alpha11 diff --git a/app/lint.xml b/lint.xml similarity index 100% rename from app/lint.xml rename to lint.xml From 1b30fef9d51628b4b302b1b540b3892b17580d03 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 16 Jul 2023 01:49:33 +0400 Subject: [PATCH 059/258] Configure Dagger for projects --- .../labs/androidquran/buildutil/KotlinCommon.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt index 920ce681ad..ca64d2babc 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/KotlinCommon.kt @@ -3,9 +3,21 @@ package com.quran.labs.androidquran.buildutil import org.gradle.api.Project import org.gradle.kotlin.dsl.configure import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KaptExtension fun Project.applyKotlinCommon() { extensions.configure { jvmToolchain(17) } + + pluginManager.withPlugin("org.jetbrains.kotlin.kapt") { + extensions.configure { + arguments { + arg("dagger.ignoreProvisionKeyWildcards", "enabled") + arg("dagger.experimentalDaggerErrorMessages", "enabled") + arg("dagger.warnIfInjectionFactoryNotGeneratedUpstream", "enabled") + arg("dagger.fastInit", "enabled") + } + } + } } From d92ecae813e1c16caabf58297f86acc5e0fe8639 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Jul 2023 20:20:33 +0000 Subject: [PATCH 060/258] Update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.9.0 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 9f03a63ec0..f74073192b 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:8.0.2") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") } gradlePlugin { From 61c9d4fa1ba8d9126db7965e7edd3bfc5675e79b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 25 Jul 2023 19:48:54 +0000 Subject: [PATCH 061/258] Update coroutinesVersion to v1.7.3 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 9102beaead..5b5b4fb8f9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { kotlinVersion = '1.9.0' - coroutinesVersion = '1.7.2' + coroutinesVersion = '1.7.3' daggerVersion = '2.47' From 6bff8c5aca0f419a780688280494e12039ddf0f5 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Wed, 26 Jul 2023 13:04:39 +0500 Subject: [PATCH 062/258] Bump AGP to 8.1.0 --- build-logic/convention/build.gradle.kts | 2 +- .../com/quran/labs/androidquran/buildutil/AndroidCommon.kt | 2 +- build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index f74073192b..d6eb084bc5 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.0.2") + compileOnly("com.android.tools.build:gradle:8.1.0") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") } diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt index ecb47b38e9..2a166539f9 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt @@ -6,7 +6,7 @@ import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions -fun CommonExtension<*, *, *, *>.applyAndroidCommon(project: Project) { +fun CommonExtension<*, *, *, *, *>.applyAndroidCommon(project: Project) { compileSdk = 33 defaultConfig.minSdk = 21 diff --git a/build.gradle b/build.gradle index 5b5b4fb8f9..57c98358a6 100644 --- a/build.gradle +++ b/build.gradle @@ -54,7 +54,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.1.0' classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.7" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" From c2850dfc3b52f0685b5c407a3efe1c5fe5f0e2e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 19:45:21 +0000 Subject: [PATCH 063/258] Update dependency androidx.fragment:fragment-ktx to v1.6.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 57c98358a6..054ba6c57c 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { androidxMediaVersion = '1.6.0' androidxAnnotationVersion = '1.6.0' - androidxFragmentVersion = '1.6.0' + androidxFragmentVersion = '1.6.1' androidxPreferencesVersion = '1.2.0' androidxAppcompatVersion = '1.6.1' androidxLocalBroadcastVersion = '1.1.0' From 8a3e9eeeb588d03155af08d87635b2455434217c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 19:45:25 +0000 Subject: [PATCH 064/258] Update dependency androidx.recyclerview:recyclerview to v1.3.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 57c98358a6..bc3691ee82 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { androidxAppcompatVersion = '1.6.1' androidxLocalBroadcastVersion = '1.1.0' androidxSwipeRefreshVersion = '1.1.0' - androidxRecyclerViewVersion = '1.3.0' + androidxRecyclerViewVersion = '1.3.1' supportSqliteVersion = '2.1.0' workManagerVersion = '2.8.1' materialComponentsVersion = '1.9.0' From 56d29dbc7a134a92cbb12a166baeba59d80a4fe2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:52:04 +0000 Subject: [PATCH 065/258] Update sqldelightVersion to v2.0.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 57c98358a6..a8ce7136ff 100644 --- a/build.gradle +++ b/build.gradle @@ -22,7 +22,7 @@ buildscript { moshiVersion = '1.15.0' okioVersion = '3.4.0' retrofitVersion = '2.9.0' - sqldelightVersion = '2.0.0-rc02' + sqldelightVersion = '2.0.0' // testing junitVersion = '4.13.2' From 7b5ab2152c25cec5661a38040575cd9f46e7ae67 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 22:43:13 +0000 Subject: [PATCH 066/258] Update dependency com.google.errorprone:error_prone_core to v2.21.0 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 2b52fcc209..669c2e996a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,7 +175,7 @@ dependencies { testImplementation "androidx.test.espresso:espresso-core:${espressoVersion}" testImplementation "androidx.test.espresso:espresso-intents:${espressoVersion}" - errorprone 'com.google.errorprone:error_prone_core:2.20.0' + errorprone 'com.google.errorprone:error_prone_core:2.21.0' // Number Picker implementation 'io.github.ShawnLin013:number-picker:2.4.13' From 0f427f5cf1c0ddf9578306e5bf011724427659ff Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Wed, 2 Aug 2023 10:24:11 +0500 Subject: [PATCH 067/258] Bump Anvil to 2.4.7 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 09080d82b7..cba09b04d3 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { materialComponentsVersion = '1.9.0' coreKtxVersion = '1.9.0' - anvilVersion = '2.4.6' + anvilVersion = '2.4.7' moshiVersion = '1.15.0' okioVersion = '3.4.0' retrofitVersion = '2.9.0' From 62c7a3ed6bf7586661678c341671e525f742a35d Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Wed, 2 Aug 2023 14:13:49 +0500 Subject: [PATCH 068/258] Support Qaloon Arabic font and minor cleanup --- .../labs/androidquran/database/DatabaseHandler.kt | 14 -------------- .../labs/androidquran/ui/util/TypefaceManager.kt | 2 ++ .../labs/androidquran/data/QuranFileConstants.kt | 4 ++-- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt b/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt index ccb254ea9b..eac9ab3099 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt @@ -117,10 +117,6 @@ class DatabaseHandler private constructor( fun validDatabase(): Boolean = database?.isOpen ?: false - private fun getVerses(sura: Int, minAyah: Int, maxAyah: Int): Cursor? { - return getVerses(sura, minAyah, maxAyah, VERSE_TABLE) - } - private fun getProperty(column: String): Int { var value = 1 if (!validDatabase()) return value @@ -284,16 +280,6 @@ class DatabaseHandler private constructor( ) } - /** - * @deprecated use {@link #getVerses(VerseRange, int)} instead - * @param sura the sura - * @param ayah the ayah - * @return the result - */ - fun getVerse(sura: Int, ayah: Int): Cursor? { - return getVerses(sura, ayah, ayah) - } - fun getVersesByIds(ids: List): Cursor? { val builder = StringBuilder() for (i in ids.indices) { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/util/TypefaceManager.kt b/app/src/main/java/com/quran/labs/androidquran/ui/util/TypefaceManager.kt index 1d17fa7c8a..9a0b2adc56 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/util/TypefaceManager.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/util/TypefaceManager.kt @@ -9,6 +9,7 @@ object TypefaceManager { const val TYPE_UTHMANI_HAFS = 1 const val TYPE_NOOR_HAYAH = 2 const val TYPE_UTHMANIC_WARSH = 3 + const val TYPE_UTHMANIC_QALOON = 4 private var typeface: Typeface? = null private var arabicTafseerTypeface: Typeface? = null @@ -21,6 +22,7 @@ object TypefaceManager { val fontName = when (QuranFileConstants.FONT_TYPE) { TYPE_NOOR_HAYAH -> "noorehira.ttf" TYPE_UTHMANIC_WARSH -> "uthmanic_warsh_ver09.ttf" + TYPE_UTHMANIC_QALOON -> "uthmanic_qaloon_ver21.ttf" else -> "uthmanic_hafs_ver12.otf" } val instance = Typeface.createFromAsset(context.assets, fontName) diff --git a/app/src/qaloon/java/com/quran/labs/androidquran/data/QuranFileConstants.kt b/app/src/qaloon/java/com/quran/labs/androidquran/data/QuranFileConstants.kt index 131930217a..51494f0cd6 100644 --- a/app/src/qaloon/java/com/quran/labs/androidquran/data/QuranFileConstants.kt +++ b/app/src/qaloon/java/com/quran/labs/androidquran/data/QuranFileConstants.kt @@ -5,10 +5,10 @@ import com.quran.labs.androidquran.database.DatabaseHandler object QuranFileConstants { // server urls - const val FONT_TYPE = TypefaceManager.TYPE_UTHMANI_HAFS + const val FONT_TYPE = TypefaceManager.TYPE_UTHMANIC_QALOON // arabic database - const val ARABIC_DATABASE = "quran.ar.db" + const val ARABIC_DATABASE = "quran.ar.qaloon.db" const val ARABIC_SHARE_TABLE = DatabaseHandler.ARABIC_TEXT_TABLE const val ARABIC_SHARE_TEXT_HAS_BASMALLAH = true const val FETCH_QUARTER_NAMES_FROM_DATABASE = false From 3018b4fa893db49da3d5855efc810581e01ab32c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 14:30:28 +0000 Subject: [PATCH 069/258] Update okioVersion to v3.5.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cba09b04d3..fe0a1dcbbb 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ buildscript { anvilVersion = '2.4.7' moshiVersion = '1.15.0' - okioVersion = '3.4.0' + okioVersion = '3.5.0' retrofitVersion = '2.9.0' sqldelightVersion = '2.0.0' From 2cdd1399b1f1942a0b668d3f2e179bf4e3760dd7 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 3 Aug 2023 01:13:09 +0500 Subject: [PATCH 070/258] Allow removing a certain page from bookmarks In some cases, when we swap out pages, we might get rid of extra pages that are no longer there. This allows removing bookmarks for such pages. This is really meant for pages like 612 (the du3a page) in Naskh, in preparation for replacing this set with the King Fahd images, which do not have this page. --- .../database/BookmarksDBAdapter.kt | 19 +++++++++++++++++++ .../androidquran/database/BookmarksDaoImpl.kt | 12 ++++++++++++ .../java/com/quran/data/dao/BookmarksDao.kt | 2 ++ 3 files changed, 33 insertions(+) diff --git a/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDBAdapter.kt index 35876d9aa4..5cb9596233 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDBAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDBAdapter.kt @@ -54,6 +54,17 @@ class BookmarksDBAdapter @Inject constructor(bookmarksDatabase: BookmarksDatabas } } + fun removeRecentsForPage(page: Int) { + lastPageQueries.transaction { + val lastPages = lastPageQueries.getLastPages().executeAsList() + val lastPagesWithoutPage = lastPages.filter { it.page != page } + if (lastPages.size != lastPagesWithoutPage.size) { + lastPageQueries.removeLastPages() + lastPagesWithoutPage.forEach { addRecentPage(it.page) } + } + } + } + fun replaceRecentRangeWithPage(deleteRangeStart: Int, deleteRangeEnd: Int, page: Int) { val maxPages = Constants.MAX_RECENT_PAGES.toLong() lastPageQueries.replaceRangeWithPage(deleteRangeStart, deleteRangeEnd, page, maxPages) @@ -105,6 +116,14 @@ class BookmarksDBAdapter @Inject constructor(bookmarksDatabase: BookmarksDatabas } } + fun removeBookmarksForPage(page: Int) { + val bookmarkId = bookmarkQueries.getBookmarkIdForPage(page).executeAsOneOrNull() + if (bookmarkId != null) { + bookmarkTagQueries.deleteByBookmarkIds(listOf(bookmarkId)) + bookmarkQueries.deleteByIds(listOf(bookmarkId)) + } + } + fun addBookmarkIfNotExists(sura: Int, ayah: Int, page: Int): Long { var bookmarkId = getBookmarkId(sura, ayah, page) if (bookmarkId < 0) { diff --git a/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDaoImpl.kt b/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDaoImpl.kt index b6ddd9eaaf..9a026b909b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDaoImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/BookmarksDaoImpl.kt @@ -23,6 +23,12 @@ class BookmarksDaoImpl @Inject constructor( } } + override suspend fun removeBookmarksForPage(page: Int) { + withContext(Dispatchers.IO) { + bookmarksDBAdapter.removeBookmarksForPage(page) + } + } + override suspend fun recentPages(): List { return withContext(Dispatchers.IO) { bookmarksDBAdapter.getRecentPages() @@ -40,4 +46,10 @@ class BookmarksDaoImpl @Inject constructor( bookmarksDBAdapter.removeRecentPages() } } + + override suspend fun removeRecentsForPage(page: Int) { + withContext(Dispatchers.IO) { + bookmarksDBAdapter.removeRecentsForPage(page) + } + } } diff --git a/common/data/src/main/java/com/quran/data/dao/BookmarksDao.kt b/common/data/src/main/java/com/quran/data/dao/BookmarksDao.kt index 6c2da4dd39..3a8d6365fe 100644 --- a/common/data/src/main/java/com/quran/data/dao/BookmarksDao.kt +++ b/common/data/src/main/java/com/quran/data/dao/BookmarksDao.kt @@ -6,9 +6,11 @@ import com.quran.data.model.bookmark.RecentPage interface BookmarksDao { suspend fun bookmarks(): List suspend fun replaceBookmarks(bookmarks: List) + suspend fun removeBookmarksForPage(page: Int) // recent pages suspend fun recentPages(): List suspend fun removeRecentPages() suspend fun replaceRecentPages(pages: List) + suspend fun removeRecentsForPage(page: Int) } From d65c68e3a4bdae57aeee303684fbfd865e6a60a2 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 3 Aug 2023 16:48:48 +0500 Subject: [PATCH 071/258] Support recursively copying from assets In order to support bundled line by line images, support recursively copying data from assets, and increase the limit on the number of files. --- .../labs/androidquran/util/QuranFileUtils.kt | 24 +++++++++++++++++++ .../labs/androidquran/util/ZipUtils.java | 2 +- .../com/quran/data/core/QuranFileManager.kt | 3 +++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/util/QuranFileUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/QuranFileUtils.kt index df22224704..572f545fb3 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/QuranFileUtils.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/QuranFileUtils.kt @@ -250,6 +250,30 @@ class QuranFileUtils @Inject constructor( copyFromAssets(assetsPath, filename, actualDestination) } + override fun copyFromAssetsRelativeRecursive( + assetsPath: String, + directory: String, + destination: String + ) { + val destinationPath = File(getQuranBaseDirectory(appContext) + destination) + val directoryDestinationPath = File(destinationPath, directory) + if (!directoryDestinationPath.exists()) { + directoryDestinationPath.mkdirs() + } + + val assets = appContext.assets + val files = assets.list(assetsPath) ?: emptyArray() + val destinationDirectory = "$destination${File.separator}$directory" + files.forEach { + val path = "$assetsPath${File.separator}$it" + if (assets.list(path)?.isNotEmpty() == true) { + copyFromAssetsRelativeRecursive(path, it, destinationDirectory) + } else { + copyFromAssetsRelative(path, it, destinationDirectory) + } + } + } + @WorkerThread override fun removeOldArabicDatabase(): Boolean { val databaseQuranArabicDatabase = File( diff --git a/app/src/main/java/com/quran/labs/androidquran/util/ZipUtils.java b/app/src/main/java/com/quran/labs/androidquran/util/ZipUtils.java index 743482c836..3e1b6adc04 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/ZipUtils.java +++ b/app/src/main/java/com/quran/labs/androidquran/util/ZipUtils.java @@ -15,7 +15,7 @@ public class ZipUtils { private static final int BUFFER_SIZE = 512; - private static final int MAX_FILES = 10000; // Max number of files + private static final int MAX_FILES = 12000; // Max number of files @VisibleForTesting static int MAX_UNZIPPED_SIZE = 0x1f400000; // Max size of unzipped data, 500MB diff --git a/common/data/src/main/java/com/quran/data/core/QuranFileManager.kt b/common/data/src/main/java/com/quran/data/core/QuranFileManager.kt index 9aab1f4f0a..fc6d61d5d5 100644 --- a/common/data/src/main/java/com/quran/data/core/QuranFileManager.kt +++ b/common/data/src/main/java/com/quran/data/core/QuranFileManager.kt @@ -16,6 +16,9 @@ interface QuranFileManager { @WorkerThread fun copyFromAssetsRelative(assetsPath: String, filename: String, destination: String) + @WorkerThread + fun copyFromAssetsRelativeRecursive(assetsPath: String, directory: String, destination: String) + @WorkerThread fun removeOldArabicDatabase(): Boolean From f54a8c86748fb786c9fbd88f51e2da9a04db7bb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:44:36 +0000 Subject: [PATCH 072/258] Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.8 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index fe0a1dcbbb..b3ea0460ed 100644 --- a/build.gradle +++ b/build.gradle @@ -55,7 +55,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.1.0' - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.7" + classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.8" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" classpath "com.google.gms:google-services:4.3.15" From 6923d86123efaa346cc8b961934b1c0cb01d4390 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 00:30:56 +0000 Subject: [PATCH 073/258] Update dependency com.google.errorprone:error_prone_core to v2.21.1 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 669c2e996a..ed996514cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,7 +175,7 @@ dependencies { testImplementation "androidx.test.espresso:espresso-core:${espressoVersion}" testImplementation "androidx.test.espresso:espresso-intents:${espressoVersion}" - errorprone 'com.google.errorprone:error_prone_core:2.21.0' + errorprone 'com.google.errorprone:error_prone_core:2.21.1' // Number Picker implementation 'io.github.ShawnLin013:number-picker:2.4.13' From 937181a6a7d54af2516751595bb000160d2ede96 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 5 Aug 2023 23:20:58 +0500 Subject: [PATCH 074/258] Replace extras component with existing reading one --- .../di/component/fragment/QuranPageComponent.kt | 4 ++-- .../com/quran/labs/androidquran/ui/PagerActivity.java | 8 ++++---- .../java/com/quran/mobile/di/QuranReadingPageComponent.kt | 3 +++ .../quran/mobile/di/QuranReadingPageComponentProvider.kt | 5 +++++ .../data/page/provider/di/QuranPageExtrasComponent.kt | 3 --- .../page/provider/di/QuranPageExtrasComponentProvider.kt | 5 ----- 6 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponent.kt create mode 100644 common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponentProvider.kt delete mode 100644 pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponent.kt delete mode 100644 pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponentProvider.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt b/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt index 248dfdeed3..3332566703 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt @@ -2,17 +2,17 @@ package com.quran.labs.androidquran.di.component.fragment import com.quran.data.di.QuranPageScope import com.quran.data.di.QuranReadingPageScope -import com.quran.data.page.provider.di.QuranPageExtrasComponent import com.quran.labs.androidquran.di.module.fragment.QuranPageModule import com.quran.labs.androidquran.ui.fragment.QuranPageFragment import com.quran.labs.androidquran.ui.fragment.TabletFragment import com.quran.labs.androidquran.ui.fragment.TranslationFragment +import com.quran.mobile.di.QuranReadingPageComponent import com.squareup.anvil.annotations.MergeSubcomponent import dagger.Subcomponent @QuranPageScope @MergeSubcomponent(QuranReadingPageScope::class, modules = [QuranPageModule::class]) -interface QuranPageComponent: QuranPageExtrasComponent { +interface QuranPageComponent: QuranReadingPageComponent { fun inject(quranPageFragment: QuranPageFragment) fun inject(tabletFragment: TabletFragment) fun inject(translationFragment: TranslationFragment) 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 eb4698b524..652d1562c6 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 @@ -57,8 +57,6 @@ import com.quran.data.model.selection.AyahSelectionKt; import com.quran.data.model.selection.SelectionIndicator; import com.quran.data.model.selection.SelectionIndicatorKt; -import com.quran.data.page.provider.di.QuranPageExtrasComponent; -import com.quran.data.page.provider.di.QuranPageExtrasComponentProvider; import com.quran.labs.androidquran.BuildConfig; import com.quran.labs.androidquran.HelpActivity; import com.quran.labs.androidquran.QuranApplication; @@ -119,6 +117,8 @@ import com.quran.mobile.di.AyahActionFragmentProvider; import com.quran.mobile.di.QuranReadingActivityComponent; import com.quran.mobile.di.QuranReadingActivityComponentProvider; +import com.quran.mobile.di.QuranReadingPageComponent; +import com.quran.mobile.di.QuranReadingPageComponentProvider; import com.quran.mobile.feature.qarilist.QariListWrapper; import com.quran.mobile.feature.qarilist.di.QariListWrapperInjector; import com.quran.page.common.factory.PageViewFactoryProvider; @@ -161,7 +161,7 @@ public class PagerActivity extends AppCompatActivity implements AyahSelectedListener, JumpDestination, QuranReadingActivityComponentProvider, - QuranPageExtrasComponentProvider, + QuranReadingPageComponentProvider, AyahToolBarInjector, QariListWrapperInjector, ActivityCompat.OnRequestPermissionsResultCallback { @@ -826,7 +826,7 @@ public QuranReadingActivityComponent provideQuranReadingActivityComponent() { @NonNull @Override - public QuranPageExtrasComponent provideQuranPageExtrasComponent(@NonNull int... pages) { + public QuranReadingPageComponent provideQuranReadingPageComponent(@NonNull int... pages) { return getPagerActivityComponent() .quranPageComponentBuilder() .withQuranPageModule(new QuranPageModule(pages)) diff --git a/common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponent.kt b/common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponent.kt new file mode 100644 index 0000000000..e9a0fa3476 --- /dev/null +++ b/common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponent.kt @@ -0,0 +1,3 @@ +package com.quran.mobile.di + +interface QuranReadingPageComponent diff --git a/common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponentProvider.kt b/common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponentProvider.kt new file mode 100644 index 0000000000..a547e8fa07 --- /dev/null +++ b/common/di/src/main/java/com/quran/mobile/di/QuranReadingPageComponentProvider.kt @@ -0,0 +1,5 @@ +package com.quran.mobile.di + +interface QuranReadingPageComponentProvider { + fun provideQuranReadingPageComponent(vararg pages: Int): QuranReadingPageComponent +} diff --git a/pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponent.kt b/pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponent.kt deleted file mode 100644 index c2ad3fefe6..0000000000 --- a/pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.quran.data.page.provider.di - -interface QuranPageExtrasComponent diff --git a/pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponentProvider.kt b/pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponentProvider.kt deleted file mode 100644 index 38df98ee55..0000000000 --- a/pages/madani/src/main/java/com/quran/data/page/provider/di/QuranPageExtrasComponentProvider.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.quran.data.page.provider.di - -interface QuranPageExtrasComponentProvider { - fun provideQuranPageExtrasComponent(vararg pages: Int): QuranPageExtrasComponent -} From 437a1bbcb3f5a986c2a7bf65f44ed6ec7ed75611 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 6 Aug 2023 13:26:02 +0500 Subject: [PATCH 075/258] Support displaying manzil in the header --- .../quran/labs/androidquran/data/QuranDisplayData.kt | 9 +++++++++ .../quran/ayahtracker/AyahImageTrackerItem.kt | 3 ++- .../androidquran/ui/helpers/QuranDisplayHelper.java | 3 ++- .../quran/labs/androidquran/util/QuranPageInfoImpl.kt | 4 ++++ .../labs/androidquran/view/HighlightingImageView.java | 6 ++++-- app/src/main/res/values-ar/strings.xml | 3 ++- app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values/strings.xml | 3 ++- .../src/main/java/com/quran/data/core/QuranInfo.kt | 10 ++++++++++ .../src/main/java/com/quran/data/core/QuranPageInfo.kt | 1 + .../com/quran/data/pageinfo/common/MadaniDataSource.kt | 2 ++ .../main/java/com/quran/data/source/QuranDataSource.kt | 1 + 12 files changed, 39 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt b/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt index 4d63480edd..5fcd33589b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt +++ b/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt @@ -89,6 +89,15 @@ class QuranDisplayData @Inject constructor(private val quranInfo: QuranInfo): Qu QuranUtils.getLocalizedNumber(context, quranInfo.getJuzForDisplayFromPage(page))) } + fun getManzilForPage(context: Context, page: Int): String { + val manzil = quranInfo.manzilForPage(page) + return if (manzil > 0) { + context.getString(R.string.comma) + ' ' + context.getString(R.string.manzil_description, QuranUtils.getLocalizedNumber(context, manzil)) + } else { + "" + } + } + fun getSuraAyahString(context: Context, sura: Int, ayah: Int): String { return getSuraAyahString(context, sura, ayah, R.string.sura_ayah_notification_str) } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahImageTrackerItem.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahImageTrackerItem.kt index 4d4c7fdf87..153feb83e3 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahImageTrackerItem.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahImageTrackerItem.kt @@ -40,7 +40,8 @@ open class AyahImageTrackerItem @JvmOverloads constructor( val juzText = quranDisplayData.getJuzDisplayStringForPage(context, page) val pageText = QuranUtils.getLocalizedNumber(context, page) val rub3Text = QuranDisplayHelper.displayRub3(context, quranInfo, page) - ayahView.setOverlayText(context, suraText, juzText, pageText, rub3Text) + val manzilText = quranDisplayData.getManzilForPage(context, page) + ayahView.setOverlayText(suraText, juzText, pageText, rub3Text, manzilText) } ayahView.setPageData(pageCoordinates, imageDrawHelpers) } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranDisplayHelper.java b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranDisplayHelper.java index 7dd94e30d8..a182315d3e 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranDisplayHelper.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranDisplayHelper.java @@ -96,7 +96,8 @@ public static String displayRub3(Context context, QuranInfo quranInfo, int page) } int hizb = (rub3 / 4) + 1; StringBuilder sb = new StringBuilder(); - sb.append(context.getString(R.string.comma_with_spaces)); + sb.append(context.getString(R.string.comma)); + sb.append(' '); int remainder = rub3 % 4; if (remainder == 1) { sb.append(context.getString(R.string.quran_rob3)).append(' '); diff --git a/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt index 510a5a732f..ec34a7e192 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt @@ -31,4 +31,8 @@ class QuranPageInfoImpl constructor( override fun pageForSuraAyah(sura: Int, ayah: Int): Int { return quranInfo.getPageFromSuraAyah(sura, ayah) } + + override fun manzilForPage(page: Int): String { + return quranDisplayData.getManzilForPage(context, page) + } } diff --git a/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java b/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java index c6b5834f28..6f48e83e65 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java @@ -385,9 +385,10 @@ private static class OverlayParams { String juzText = null; String pageText = null; String rub3Text = null; + String manzilText = null; } - public void setOverlayText(Context context, String suraText, String juzText, String pageText, String rub3Text) { + public void setOverlayText(String suraText, String juzText, String pageText, String rub3Text, String manzilText) { // Calculate page bounding rect from ayahinfo db if (pageBounds == null) { return; @@ -398,6 +399,7 @@ public void setOverlayText(Context context, String suraText, String juzText, Str overlayParams.juzText = juzText; overlayParams.pageText = pageText; overlayParams.rub3Text = rub3Text; + overlayParams.manzilText = manzilText; overlayParams.paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); overlayParams.paint.setTextSize(fontSize); // if (juzText.contains("ج")) { @@ -475,7 +477,7 @@ private void overlayText(Canvas canvas, Matrix matrix) { overlayParams.paint); // Merge the current rub3 text with the juz' text overlayParams.paint.setTextAlign(Align.RIGHT); - canvas.drawText(overlayParams.juzText + overlayParams.rub3Text, + canvas.drawText(overlayParams.juzText + overlayParams.rub3Text + overlayParams.manzilText, (getWidth() - overlayParams.offsetX) - horizontalSafeOffset, overlayParams.topBaseline + topSafeOffset, overlayParams.paint); diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index df3e71ce71..753d62a2cd 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -156,6 +156,7 @@ جاري إلغاء اﻷمر… الجزء %1$s + منزل %1$s صفحة %1$s، جزء %2$s جاري تحميل الملفات المطلوبة جاري تحميل الملفات المطلوبة @@ -320,6 +321,6 @@ مكان غير مجلد التطبيق الافتراضي قد يؤدي إلى عدم تمكن التطبيق من الوصول لملفاته في إصدارات أندرويد القادمة، هل ترغب في نقل البيانات على أية حال؟ - " ، " + "،" diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 21d7f00266..826c26f02f 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -314,7 +314,6 @@ Annuleren… Juz\' %1$s - , Pagina %1$s, Juz\' %2$s Vereiste Bestanden Vereiste Bestanden diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fca7f16f62..7f6ab199b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -283,7 +283,8 @@ Canceling… Juz\' %1$s - , + Manzil %1$s + , Page %1$s, Juz\' %2$s Required Files Required Files diff --git a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt index e8a332807c..2aecb96b4c 100644 --- a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt +++ b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt @@ -19,6 +19,7 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { private val pageRub3Start = quranDataSource.quarterStartByPage private val suraNumAyahs = quranDataSource.numberOfAyahsForSuraArray private val suraIsMakki = quranDataSource.isMakkiBySuraArray + private val manazil = quranDataSource.manzilPageArray val quarters = quranDataSource.quartersArray val numberOfPages = quranDataSource.numberOfPages @@ -32,6 +33,15 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { return suraPageStart[sura - 1] } + fun manzilForPage(page: Int): Int { + val manzil = manazil.indexOfFirst { it > page } + return if (manzil == -1 && manazil.isNotEmpty() && page >= manazil.last()) { + manazil.size + } else { + manzil + } + } + fun getSuraNumberFromPage(page: Int): Int { var sura = -1 for (i in 0 until NUMBER_OF_SURAS) { diff --git a/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt b/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt index 024fd1f7ed..97d5261a05 100644 --- a/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt +++ b/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt @@ -6,4 +6,5 @@ interface QuranPageInfo { fun displayRub3(page: Int): String fun localizedPage(page: Int): String fun pageForSuraAyah(sura: Int, ayah: Int): Int + fun manzilForPage(page: Int): String } diff --git a/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt b/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt index 125b31ec39..5557588ca5 100644 --- a/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt +++ b/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt @@ -225,4 +225,6 @@ open class MadaniDataSource : QuranDataSource { /* hizb 58 */ SuraAyah(72, 1), SuraAyah(73, 20), SuraAyah(75, 1), SuraAyah(76, 19), /* hizb 59 */ SuraAyah(78, 1), SuraAyah(80, 1), SuraAyah(82, 1), SuraAyah(84, 1), /* hizb 60 */ SuraAyah(87, 1), SuraAyah(90, 1), SuraAyah(94, 1), SuraAyah(100, 9)) + + override val manzilPageArray: Array = emptyArray() } diff --git a/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt b/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt index 70a1853321..7b9d2086be 100644 --- a/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt +++ b/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt @@ -13,4 +13,5 @@ interface QuranDataSource { val isMakkiBySuraArray: BooleanArray val quarterStartByPage: IntArray val quartersArray: Array + val manzilPageArray: Array } From d765513cf0dcd480a331a8f35672810b98d85a66 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 6 Aug 2023 17:02:01 +0500 Subject: [PATCH 076/258] Show progress bar while doing local upgrades --- .../labs/androidquran/QuranDataActivity.kt | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt index d851ee3473..85e83c7de2 100644 --- a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt @@ -158,8 +158,7 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio errorDialog?.dismiss() errorDialog = null - updateDialog?.dismiss() - updateDialog = null + hideMigrationDialog() scope.cancel() super.onPause() @@ -238,12 +237,23 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio permissionsDialog.show() } - private fun migrateFromTo(destination: String) { - val migrationDialog = AlertDialog.Builder(this) + private fun showMigrationDialog() { + if (updateDialog == null) { + val migrationDialog = AlertDialog.Builder(this) .setView(R.layout.migration_upgrade) .create() - updateDialog = migrationDialog - migrationDialog.show() + updateDialog = migrationDialog + migrationDialog.show() + } + } + + private fun hideMigrationDialog() { + updateDialog?.dismiss() + updateDialog = null + } + + private fun migrateFromTo(destination: String) { + showMigrationDialog() scope.launch { withContext(Dispatchers.IO) { @@ -257,6 +267,7 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio } private fun checkPages() { + showMigrationDialog() quranDataPresenter.checkPages() } @@ -382,15 +393,13 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio } fun onStorageNotAvailable() { - updateDialog?.dismiss() - updateDialog = null + hideMigrationDialog() // no storage mounted, nothing we can do... runListViewWithoutPages() } fun onPagesChecked(quranDataStatus: QuranDataStatus) { - updateDialog?.dismiss() - updateDialog = null + hideMigrationDialog() this.quranDataStatus = quranDataStatus if (!quranDataStatus.havePages()) { From 033fca882cbd6248a24174b7398d4d22892ab2f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 19:01:13 +0000 Subject: [PATCH 077/258] Update dependency androidx.preference:preference-ktx to v1.2.1 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b3ea0460ed..a5b8455fc8 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { androidxMediaVersion = '1.6.0' androidxAnnotationVersion = '1.6.0' androidxFragmentVersion = '1.6.1' - androidxPreferencesVersion = '1.2.0' + androidxPreferencesVersion = '1.2.1' androidxAppcompatVersion = '1.6.1' androidxLocalBroadcastVersion = '1.1.0' androidxSwipeRefreshVersion = '1.1.0' From 77eb1c73823b39147e9fb635cf72b33fae850f37 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 10 Aug 2023 22:07:01 +0500 Subject: [PATCH 078/258] Allow setting a sideline preference --- app/build.gradle | 1 + .../main/java/com/quran/labs/androidquran/data/Constants.kt | 1 + .../java/com/quran/labs/androidquran/util/QuranSettings.java | 4 ++++ .../java/com/quran/labs/androidquran/util/SettingsImpl.kt | 4 ++++ app/src/main/res/values/preferences_keys.xml | 1 - common/data/src/main/java/com/quran/data/dao/Settings.kt | 1 + .../java/com/quran/data/pageinfo/common/MadaniDataSource.kt | 1 + .../src/main/java/com/quran/data/source/PageContentType.kt | 4 ++-- .../src/main/java/com/quran/data/source/QuranDataSource.kt | 1 + common/preference/build.gradle | 5 +++++ common/preference/src/main/res/values/preferences_keys.xml | 5 +++++ settings.gradle | 1 + 12 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 common/preference/build.gradle create mode 100644 common/preference/src/main/res/values/preferences_keys.xml diff --git a/app/build.gradle b/app/build.gradle index ed996514cb..a4d899b0f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -116,6 +116,7 @@ dependencies { implementation project(path: ':common:download') implementation project(path: ':common:networking') implementation project(path: ':common:pages') + implementation project(path: ':common:preference') implementation project(path: ':common:reading') implementation project(path: ':common:recitation') implementation project(path: ':common:search') diff --git a/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt b/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt index e398b58dac..bf537f7565 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt +++ b/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt @@ -98,4 +98,5 @@ object Constants { const val PREF_CHECKED_PARTIAL_IMAGES = "didCheckPartialImages" const val PREF_CURRENT_AUDIO_REVISION = "currentAudioRevision" const val PREF_SURA_TRANSLATED_NAME = "suraTranslatedName" + const val PREF_SHOW_SIDELINES = "showSidelines" } diff --git a/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java b/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java index 843681eeeb..c87cb15065 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java +++ b/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java @@ -162,6 +162,10 @@ public String getPageType() { return prefs.getString(Constants.PREF_PAGE_TYPE, null); } + public boolean isSidelines() { + return prefs.getBoolean(Constants.PREF_SHOW_SIDELINES, false); + } + public void setPageType(String pageType) { prefs.edit().putString(Constants.PREF_PAGE_TYPE, pageType).apply(); clearDefaultImagesDirectory(); diff --git a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt index 0d0a482109..b11500da72 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt @@ -63,5 +63,9 @@ class SettingsImpl @Inject constructor(private val quranSettings: QuranSettings) return quranSettings.pageType } + override suspend fun showSidelines(): Boolean { + return quranSettings.isSidelines + } + override fun preferencesFlow(): Flow = preferencesFlow } diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index fade892d30..d4f84f740e 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -36,6 +36,5 @@ exportKeyCSV sendLogsKey pageTypeKey - readingCategoryKey dualScreenKey diff --git a/common/data/src/main/java/com/quran/data/dao/Settings.kt b/common/data/src/main/java/com/quran/data/dao/Settings.kt index a65707e41e..eac44c2af5 100644 --- a/common/data/src/main/java/com/quran/data/dao/Settings.kt +++ b/common/data/src/main/java/com/quran/data/dao/Settings.kt @@ -13,6 +13,7 @@ interface Settings { suspend fun shouldShowHeaderFooter(): Boolean suspend fun shouldShowBookmarks(): Boolean suspend fun pageType(): String + suspend fun showSidelines(): Boolean fun preferencesFlow(): Flow } diff --git a/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt b/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt index 5557588ca5..0f2aaaafd0 100644 --- a/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt +++ b/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt @@ -227,4 +227,5 @@ open class MadaniDataSource : QuranDataSource { /* hizb 60 */ SuraAyah(87, 1), SuraAyah(90, 1), SuraAyah(94, 1), SuraAyah(100, 9)) override val manzilPageArray: Array = emptyArray() + override val haveSidelines: Boolean = false } diff --git a/common/data/src/main/java/com/quran/data/source/PageContentType.kt b/common/data/src/main/java/com/quran/data/source/PageContentType.kt index 9601eeaf16..2598208d6a 100644 --- a/common/data/src/main/java/com/quran/data/source/PageContentType.kt +++ b/common/data/src/main/java/com/quran/data/source/PageContentType.kt @@ -1,6 +1,6 @@ package com.quran.data.source sealed class PageContentType { - object Image : PageContentType() - data class Line(val ratio: Float, val allowOverlapOfLines: Boolean): PageContentType() + data object Image : PageContentType() + data class Line(val ratio: Float, val lineHeight: Int, val allowOverlapOfLines: Boolean): PageContentType() } diff --git a/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt b/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt index 7b9d2086be..729bf6e640 100644 --- a/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt +++ b/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt @@ -14,4 +14,5 @@ interface QuranDataSource { val quarterStartByPage: IntArray val quartersArray: Array val manzilPageArray: Array + val haveSidelines: Boolean } diff --git a/common/preference/build.gradle b/common/preference/build.gradle new file mode 100644 index 0000000000..c120c4f724 --- /dev/null +++ b/common/preference/build.gradle @@ -0,0 +1,5 @@ +plugins { + id 'quran.android.library.android' +} + +android.namespace 'com.quran.labs.androidquran.common.preference' diff --git a/common/preference/src/main/res/values/preferences_keys.xml b/common/preference/src/main/res/values/preferences_keys.xml new file mode 100644 index 0000000000..3823f53aa7 --- /dev/null +++ b/common/preference/src/main/res/values/preferences_keys.xml @@ -0,0 +1,5 @@ + + + readingCategoryKey + showSidelines + diff --git a/settings.gradle b/settings.gradle index 285c42583e..3de0281592 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,6 +11,7 @@ include ':common:networking' include ':common:pages' include ':common:reading' include ':common:recitation' +include ':common:preference' include ':common:search' include ':common:toolbar' include ':common:upgrade' From 264856f8053de5317141ba5c68189ff9d354ba8c Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 11 Aug 2023 11:46:23 +0500 Subject: [PATCH 079/258] Simplify some page calculation code --- .../androidquran/data/QuranDisplayData.kt | 2 +- .../presenter/bookmark/BookmarkPresenter.java | 6 +-- .../presenter/quran/QuranPagePresenter.kt | 2 +- .../labs/androidquran/ui/PagerActivity.java | 42 ++++++------------- .../ui/fragment/TabletFragment.java | 2 +- .../ui/helpers/QuranPageAdapter.kt | 9 ---- .../java/com/quran/data/core/QuranInfo.kt | 7 +++- 7 files changed, 25 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt b/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt index 5fcd33589b..76b9cc4f19 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt +++ b/app/src/main/java/com/quran/labs/androidquran/data/QuranDisplayData.kt @@ -157,7 +157,7 @@ class QuranDisplayData @Inject constructor(private val quranInfo: QuranInfo): Qu } fun safelyGetSuraOnPage(page: Int): Int { - return if (page < Constants.PAGES_FIRST || page > quranInfo.numberOfPages) { + return if (!quranInfo.isValidPage(page)) { Timber.e(IllegalArgumentException("safelyGetSuraOnPage with page: $page")) quranInfo.getSuraOnPage(1) } else { diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java index 31a6188f78..695f742398 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java @@ -48,6 +48,7 @@ public class BookmarkPresenter implements Presenter { private final BookmarkModel bookmarkModel; private final QuranSettings quranSettings; private final QuranRowFactory quranRowFactory; + private final QuranInfo quranInfo; private int sortOrder; private boolean groupByTags; @@ -61,7 +62,6 @@ public class BookmarkPresenter implements Presenter { private DisposableSingleObserver pendingRemoval; private List itemsToRemove; - private final int totalPages; @Inject BookmarkPresenter(Context appContext, @@ -75,12 +75,12 @@ public class BookmarkPresenter implements Presenter { this.bookmarkModel = bookmarkModel; this.arabicDatabaseUtils = arabicDatabaseUtils; this.quranRowFactory = quranRowFactory; + this.quranInfo = quranInfo; sortOrder = quranSettings.getBookmarksSortOrder(); groupByTags = quranSettings.getBookmarksGroupedByTags(); showRecents = quranSettings.getShowRecents(); showDate = quranSettings.getShowDate(); - totalPages = quranInfo.getNumberOfPages(); subscribeToChanges(); } @@ -323,7 +323,7 @@ private List getBookmarkRows(BookmarkData data, boolean groupByTags) { rows.add(0, quranRowFactory.fromRecentPageHeader(appContext, size)); for (int i = 0; i < size; i++) { int page = recentPages.get(i).getPage(); - if (page < Constants.PAGES_FIRST || page > totalPages) { + if (!quranInfo.isValidPage(page)) { page = 1; } rows.add(i + 1, quranRowFactory.fromCurrentPage(appContext, page, recentPages.get(i).getTimestamp())); diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPagePresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPagePresenter.kt index 1ef01d7531..938785b773 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPagePresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPagePresenter.kt @@ -90,7 +90,7 @@ class QuranPagePresenter @Inject constructor( // drop empty pages - this happens in Shemerly, for example, where there are an odd number of // pages. in dual page mode, we have an empty page at the end, so we don't want to try to load // the empty page. - val actualPages = pages.filter { it <= quranInfo.numberOfPages } + val actualPages = pages.filter { quranInfo.isValidPage(it) } compositeDisposable.add( quranPageLoader.loadPages(actualPages.toTypedArray()) .observeOn(AndroidSchedulers.mainThread()) 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 652d1562c6..1bb7d9e322 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 @@ -217,8 +217,6 @@ public class PagerActivity extends AppCompatActivity implements private ViewPager slidingPager; private SlidingPagerAdapter slidingPagerAdapter; - private int numberOfPages; - private int numberOfPagesDual; private int defaultNavigationBarColor; private boolean isSplitScreen = false; @@ -305,17 +303,11 @@ public void onCreate(Bundle savedInstanceState) { // that is used to generate preview windows). getWindow().setBackgroundDrawable(null); - numberOfPages = quranInfo.getNumberOfPages(); - numberOfPagesDual = quranInfo.getNumberOfPagesDual(); - int page = -1; isActionBarHidden = true; if (savedInstanceState != null) { Timber.d("non-null saved instance state!"); page = savedInstanceState.getInt(LAST_READ_PAGE, -1); - if (page != -1) { - page = numberOfPages - page; - } showingTranslation = savedInstanceState .getBoolean(LAST_READING_MODE_IS_TRANSLATION, false); if (savedInstanceState.containsKey(LAST_ACTIONBAR_STATE)) { @@ -328,7 +320,7 @@ public void onCreate(Bundle savedInstanceState) { Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { - page = numberOfPages - extras.getInt("page", Constants.PAGES_FIRST); + page = extras.getInt("page", Constants.PAGES_FIRST); showingTranslation = extras.getBoolean(EXTRA_JUMP_TO_TRANSLATION, showingTranslation); final int highlightedSura = extras.getInt(EXTRA_HIGHLIGHT_SURA, -1); final int highlightedAyah = extras.getInt(EXTRA_HIGHLIGHT_AYAH, -1); @@ -374,7 +366,7 @@ public void onCreate(Bundle savedInstanceState) { if (showingTranslation && translationNames != null) { updateActionBarSpinner(); } else { - updateActionBarTitle(numberOfPages - page); + updateActionBarTitle(page); } lastPopupTime = System.currentTimeMillis(); @@ -443,7 +435,7 @@ public void onPageSelected(int position) { // Shemerly has an odd number of pages (521), so when showing in tablet mode, // the last page is empty. default to the previous page title in those cases. final int page; - if (isDualPages && !showingTranslation && potentialPage == quranInfo.getNumberOfPages() + 1) { + if (isDualPageVisible() && potentialPage == quranInfo.getNumberOfPages() + 1) { page = quranInfo.getNumberOfPages(); } else { page = potentialPage; @@ -495,24 +487,21 @@ public void onPageSelected(int position) { if (shouldAdjustPageNumber) { // when going from two page per screen to one or vice versa, we adjust the page number, // such that the first page is always selected. - int curPage = numberOfPages - page; + int curPage = page; if (isDualPageVisible()) { if (curPage % 2 != 0) { curPage++; } - curPage = numberOfPagesDual - (curPage / 2); } else { if (curPage % 2 == 0) { curPage--; } - curPage = numberOfPages - curPage; } page = curPage; - } else if (isDualPageVisible()) { - page = page / 2; } - viewPager.setCurrentItem(page); + final int pageIndex = quranInfo.getPositionFromPage(page, isDualPageVisible()); + viewPager.setCurrentItem(pageIndex); if (page == 0) { onPageChangeListener.onPageSelected(0); } @@ -877,7 +866,7 @@ private void downloadRequiredFiles() { boolean haveDownload = false; if (!quranFileUtils.haveAyaPositionFile(this)) { String url = quranFileUtils.getAyaPositionFileUrl(); - if (QuranUtils.isDualPages(this, quranScreenInfo)) { + if (isDualPages) { url = quranFileUtils.getAyaPositionFileUrl( quranScreenInfo.getTabletWidthParam()); } @@ -930,7 +919,7 @@ public void onNewIntent(Intent intent) { recentPagePresenter.onJump(); Bundle extras = intent.getExtras(); if (extras != null) { - int page = numberOfPages - extras.getInt("page", Constants.PAGES_FIRST); + int page = extras.getInt("page", Constants.PAGES_FIRST); boolean currentValue = showingTranslation; showingTranslation = extras.getBoolean(EXTRA_JUMP_TO_TRANSLATION, showingTranslation); @@ -946,7 +935,7 @@ public void onNewIntent(Intent intent) { updateActionBarSpinner(); } else { pagerAdapter.setQuranMode(); - updateActionBarTitle(numberOfPages - page); + updateActionBarTitle(page); } supportInvalidateOptionsMenu(); @@ -956,10 +945,8 @@ public void onNewIntent(Intent intent) { // this will jump to the right page automagically ensurePage(highlightedSura, highlightedAyah); } else { - if (isDualPageVisible()) { - page = page / 2; - } - viewPager.setCurrentItem(page); + final int pagePosition = quranInfo.getPositionFromPage(page, isDualPageVisible()); + viewPager.setCurrentItem(pagePosition); } setIntent(intent); @@ -1379,7 +1366,7 @@ public void toggleActionBar() { private void ensurePage(int sura, int ayah) { int page = quranInfo.getPageFromSuraAyah(sura, ayah); - if (page >= Constants.PAGES_FIRST && page <= numberOfPages) { + if (quranInfo.isValidPage(page)) { int position = quranInfo.getPositionFromPage(page, isDualPageVisible()); if (position != viewPager.getCurrentItem()) { viewPager.setCurrentItem(position); @@ -1515,10 +1502,7 @@ public void onPlayPressed() { } int position = viewPager.getCurrentItem(); - int page = numberOfPages - position; - if (isDualPageVisible()) { - page = ((numberOfPagesDual - position) * 2) - 1; - } + int page = quranInfo.getPageFromPosition(position, isDualPageVisible()); // log the event quranEventLogger.logAudioPlayback(QuranEventLogger.AudioPlaybackSource.PAGE, diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java index bd12fc7eb2..71b75776e2 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java @@ -287,7 +287,7 @@ public AyahTrackerItem[] getAyahTrackerItems() { AyahTrackerItem right; if (mode == Mode.ARABIC) { if (leftImageView != null && rightImageView != null) { - if (quranInfo.getNumberOfPages() >= pageNumber) { + if (quranInfo.isValidPage(pageNumber)) { left = new AyahImageTrackerItem(pageNumber, quranInfo, quranDisplayData, diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt index d5112cfebb..357723471a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt @@ -119,15 +119,6 @@ class QuranPageAdapter( } } - fun getFragmentIfExistsForPage(page: Int): QuranPage? { - if (page < Constants.PAGES_FIRST || totalPages < page) { - return null - } - val position = quranInfo.getPositionFromPage(page, isDualPagesVisible) - val fragment = getFragmentIfExists(position) - return if (fragment is QuranPage && fragment.isAdded) fragment else null - } - private val isDualPagesVisible: Boolean get() = isDualPages && !(isSplitScreen && isShowingTranslation) } diff --git a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt index 2aecb96b4c..1b056d60f3 100644 --- a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt +++ b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt @@ -4,6 +4,7 @@ import com.quran.data.core.QuranConstants.LAST_SURA import com.quran.data.core.QuranConstants.MAX_AYAH import com.quran.data.core.QuranConstants.MIN_AYAH import com.quran.data.core.QuranConstants.NUMBER_OF_SURAS +import com.quran.data.core.QuranConstants.PAGES_FIRST import com.quran.data.model.SuraAyah import com.quran.data.model.VerseRange import com.quran.data.source.QuranDataSource @@ -23,7 +24,7 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { val quarters = quranDataSource.quartersArray val numberOfPages = quranDataSource.numberOfPages - val numberOfPagesDual = numberOfPages / 2 + numberOfPages % 2 + private val numberOfPagesDual = numberOfPages / 2 + numberOfPages % 2 fun getStartingPageForJuz(juz: Int): Int { return juzPageStart[juz - 1] @@ -42,6 +43,10 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { } } + fun isValidPage(page: Int): Boolean { + return page in PAGES_FIRST..numberOfPages + } + fun getSuraNumberFromPage(page: Int): Int { var sura = -1 for (i in 0 until NUMBER_OF_SURAS) { From 076cfaa8f5edd485e1458bf45ed3185d3a3e5b23 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 13 Aug 2023 13:55:34 +0500 Subject: [PATCH 080/258] Support skipping introductory pages This is mostly added to support Naskh, where the first page is page 2, but it is still on the right side. By setting skip to 1 here, we make the first page (on the right), set to page 2. --- .../labs/androidquran/ui/PagerActivity.java | 31 ++++++++---- .../ui/fragment/QuranPageFragment.java | 2 +- .../ui/fragment/TabletFragment.java | 9 ++-- .../ui/fragment/TranslationFragment.java | 2 +- .../ui/helpers/QuranPageAdapter.kt | 2 +- .../androidquran/util/QuranPageInfoImpl.kt | 4 +- .../androidquran/view/QuranImagePageLayout.kt | 4 +- .../androidquran/view/QuranPageLayout.java | 6 ++- .../view/QuranTranslationPageLayout.java | 4 +- .../labs/androidquran/view/TabletView.java | 12 ++--- .../java/com/quran/data/core/QuranInfoTest.kt | 50 +++++++++++++++++++ .../java/com/quran/data/core/QuranInfo.kt | 41 ++++++++++++--- .../java/com/quran/data/core/QuranPageInfo.kt | 1 + .../data/pageinfo/common/MadaniDataSource.kt | 1 + .../com/quran/data/source/QuranDataSource.kt | 1 + 15 files changed, 133 insertions(+), 37 deletions(-) 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 1bb7d9e322..38b703280b 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 @@ -487,15 +487,11 @@ public void onPageSelected(int position) { if (shouldAdjustPageNumber) { // when going from two page per screen to one or vice versa, we adjust the page number, // such that the first page is always selected. - int curPage = page; + final int curPage; if (isDualPageVisible()) { - if (curPage % 2 != 0) { - curPage++; - } + curPage = quranInfo.mapSinglePageToDualPage(page); } else { - if (curPage % 2 == 0) { - curPage--; - } + curPage = quranInfo.mapDualPageToSinglePage(page); } page = curPage; } @@ -572,7 +568,15 @@ public Observable getViewPagerObservable() { new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { - e.onNext(quranInfo.getPageFromPosition(position, isDualPageVisible())); + final int page = quranInfo.getPageFromPosition(position, isDualPageVisible()); + // another workaround for shemerly where there is no page 522 + final int pageToSelect; + if (isDualPageVisible() && page == quranInfo.getNumberOfPages() + 1) { + pageToSelect = page - 1; + } else { + pageToSelect = page; + } + e.onNext(pageToSelect); } }; @@ -1242,7 +1246,13 @@ private void refreshActionBarSpinner() { } private int getCurrentPage() { - return quranInfo.getPageFromPosition(viewPager.getCurrentItem(), isDualPageVisible()); + final int page = quranInfo.getPageFromPosition(viewPager.getCurrentItem(), isDualPageVisible()); + if (isDualPageVisible() && page == quranInfo.getNumberOfPages() + 1) { + // hack for shemerly, where there is no page 522. + return page - 1; + } else { + return page; + } } private void updateActionBarSpinner() { @@ -1502,7 +1512,8 @@ public void onPlayPressed() { } int position = viewPager.getCurrentItem(); - int page = quranInfo.getPageFromPosition(position, isDualPageVisible()); + final int delta = isDualPageVisible() ? 1 : 0; + int page = quranInfo.getPageFromPosition(position, isDualPageVisible()) - delta; // log the event quranEventLogger.logAudioPlayback(QuranEventLogger.AudioPlaybackSource.PAGE, diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranPageFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranPageFragment.java index 5c02a9e22d..08e73c2891 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranPageFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranPageFragment.java @@ -97,7 +97,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, Bundle savedInstanceState) { final Context context = requireContext(); quranPageLayout = new QuranImagePageLayout(context); - quranPageLayout.setPageController(this, pageNumber); + quranPageLayout.setPageController(this, pageNumber, quranInfo.getSkip()); imageView = quranPageLayout.getImageView(); return quranPageLayout; } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java index 71b75776e2..ec74806277 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java @@ -136,7 +136,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, leftImageView = ((QuranImagePageLayout) mainView.getLeftPage()).getImageView(); rightImageView = ((QuranImagePageLayout) mainView.getRightPage()).getImageView(); } - mainView.setPageController(this, pageNumber, pageNumber - 1); + mainView.setPageController(this, pageNumber, pageNumber - 1, quranInfo.getSkip()); } else if (mode == Mode.TRANSLATION) { if (!isSplitScreen) { mainView.init(TabletView.TRANSLATION_PAGE, TabletView.TRANSLATION_PAGE, pageViewFactory, pageNumber, pageNumber - 1); @@ -148,7 +148,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, PagerActivity pagerActivity = (PagerActivity) context; leftTranslation.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); rightTranslation.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); - mainView.setPageController(this, pageNumber, pageNumber - 1); + mainView.setPageController(this, pageNumber, pageNumber - 1, quranInfo.getSkip()); } else { initSplitMode(); } @@ -157,7 +157,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, } private void initSplitMode() { - isQuranOnRight = pageNumber % 2 == 1; + final int skip = quranInfo.getSkip(); + isQuranOnRight = (pageNumber + skip) % 2 == 1; final int leftPageType = isQuranOnRight ? TabletView.TRANSLATION_PAGE : TabletView.QURAN_PAGE; final int rightPageType = isQuranOnRight ? TabletView.QURAN_PAGE : TabletView.TRANSLATION_PAGE; @@ -186,7 +187,7 @@ private void initSplitMode() { PagerActivity pagerActivity = (PagerActivity) getActivity(); splitTranslationView.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); - mainView.setPageController(this, pageNumber); + mainView.setPageController(this, pageNumber, quranInfo.getSkip()); } @Override diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java index edced9eb18..60fbbc6773 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java @@ -81,7 +81,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Context context = getActivity(); mainView = new QuranTranslationPageLayout(context); - mainView.setPageController(this, pageNumber); + mainView.setPageController(this, pageNumber, quranInfo.getSkip()); translationView = mainView.getTranslationView(); translationView.setTranslationClickedListener(v -> { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt index 357723471a..ff1f74b7a8 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt @@ -21,7 +21,7 @@ class QuranPageAdapter( private val pageViewFactory: PageViewFactory? = null ) : FragmentStatePagerAdapter(fm, if (isDualPages) "dualPages" else "singlePage") { private var pageMode: PageMode = makePageMode() - private val totalPages: Int = quranInfo.numberOfPages + private val totalPages: Int = quranInfo.numberOfPagesConsideringSkipped private val totalPagesDual: Int = totalPages / 2 + (totalPages % 2) fun setTranslationMode() { diff --git a/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt index ec34a7e192..e2d8efd44a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt @@ -6,7 +6,7 @@ import com.quran.data.core.QuranPageInfo import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.ui.helpers.QuranDisplayHelper -class QuranPageInfoImpl constructor( +class QuranPageInfoImpl( private val context: Context, private val quranInfo: QuranInfo, private val quranDisplayData: QuranDisplayData @@ -35,4 +35,6 @@ class QuranPageInfoImpl constructor( override fun manzilForPage(page: Int): String { return quranDisplayData.getManzilForPage(context, page) } + + override fun skippedPagesCount(): Int = quranInfo.skip } diff --git a/app/src/main/java/com/quran/labs/androidquran/view/QuranImagePageLayout.kt b/app/src/main/java/com/quran/labs/androidquran/view/QuranImagePageLayout.kt index e3c95f217c..86dcc5b6f4 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/QuranImagePageLayout.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/QuranImagePageLayout.kt @@ -36,8 +36,8 @@ open class QuranImagePageLayout(context: Context) : QuranPageLayout(context) { imageView.setNightMode(quranSettings.isNightMode, quranSettings.nightModeTextBrightness, quranSettings.nightModeBackgroundBrightness) } - override fun setPageController(controller: PageController?, pageNumber: Int) { - super.setPageController(controller, pageNumber) + override fun setPageController(controller: PageController?, pageNumber: Int, skips: Int) { + super.setPageController(controller, pageNumber, skips) val gestureDetector = GestureDetector(context, PageGestureDetector()) val gestureListener = OnTouchListener { _, event -> gestureDetector.onTouchEvent(event) diff --git a/app/src/main/java/com/quran/labs/androidquran/view/QuranPageLayout.java b/app/src/main/java/com/quran/labs/androidquran/view/QuranPageLayout.java index fd217049e5..70d5455cd8 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/QuranPageLayout.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/QuranPageLayout.java @@ -66,6 +66,7 @@ public abstract class QuranPageLayout extends QuranPageWrapperLayout protected int pageNumber; protected boolean shouldHideLine; protected boolean isFullWidth; + private int skippedPages; private ObservableScrollView scrollView; private @BorderMode int leftBorder; @@ -205,9 +206,10 @@ private View resolveView() { return scrollView != null ? scrollView : innerView; } - public void setPageController(PageController controller, int pageNumber) { + public void setPageController(PageController controller, int pageNumber, int skippedPages) { this.pageNumber = pageNumber; this.pageController = controller; + this.skippedPages = skippedPages; } protected int getPagesVisible() { @@ -239,7 +241,7 @@ public void updateView(@NonNull QuranSettings quranSettings) { lineColor = Color.argb(nightModeTextBrightness, 255, 255, 255); } - if (pageNumber % 2 == 0) { + if ((pageNumber + skippedPages) % 2 == 0) { leftBorder = nightMode ? BorderMode.DARK : BorderMode.LIGHT; rightBorder = BorderMode.HIDDEN; } else { diff --git a/app/src/main/java/com/quran/labs/androidquran/view/QuranTranslationPageLayout.java b/app/src/main/java/com/quran/labs/androidquran/view/QuranTranslationPageLayout.java index 560ad0c53c..32876a1ff5 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/QuranTranslationPageLayout.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/QuranTranslationPageLayout.java @@ -26,8 +26,8 @@ protected View generateContentView(@NonNull Context context, boolean isLandscape } @Override - public void setPageController(PageController controller, int pageNumber) { - super.setPageController(controller, pageNumber); + public void setPageController(PageController controller, int pageNumber, int skips) { + super.setPageController(controller, pageNumber, skips); translationView.setPageController(controller); } diff --git a/app/src/main/java/com/quran/labs/androidquran/view/TabletView.java b/app/src/main/java/com/quran/labs/androidquran/view/TabletView.java index ab5e98c816..ea3a792fa9 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/TabletView.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/TabletView.java @@ -83,17 +83,17 @@ private QuranPageLayout getPageLayout( } } - public void setPageController(PageController controller, int leftPage, int rightPage) { + public void setPageController(PageController controller, int leftPage, int rightPage, int skips) { this.pageController = controller; - this.leftPage.setPageController(controller, leftPage); - this.rightPage.setPageController(controller, rightPage); + this.leftPage.setPageController(controller, leftPage, skips); + this.rightPage.setPageController(controller, rightPage, skips); } - public void setPageController(PageController controller, int pageNumber) { + public void setPageController(PageController controller, int pageNumber, int skips) { this.pageController = controller; - this.rightPage.setPageController(controller, pageNumber); - this.leftPage.setPageController(controller, pageNumber); + this.rightPage.setPageController(controller, pageNumber, skips); + this.leftPage.setPageController(controller, pageNumber, skips); } @Override diff --git a/app/src/test/java/com/quran/data/core/QuranInfoTest.kt b/app/src/test/java/com/quran/data/core/QuranInfoTest.kt index 4a7ecdb733..499d48ac10 100644 --- a/app/src/test/java/com/quran/data/core/QuranInfoTest.kt +++ b/app/src/test/java/com/quran/data/core/QuranInfoTest.kt @@ -59,4 +59,54 @@ class QuranInfoTest { assertThat(quranInfo.getJuzForDisplayFromPage(201)).isEqualTo(10) assertThat(quranInfo.getJuzFromPage(201)).isEqualTo(11) } + + @Test + fun testMapSinglePageToDualPage() { + val quranInfo = QuranInfo(MadaniDataSource()) + assertThat(quranInfo.mapSinglePageToDualPage(1)).isEqualTo(2) + assertThat(quranInfo.mapSinglePageToDualPage(2)).isEqualTo(2) + assertThat(quranInfo.mapSinglePageToDualPage(3)).isEqualTo(4) + assertThat(quranInfo.mapSinglePageToDualPage(4)).isEqualTo(4) + + // skipping a single page (ex naskh), so the first page is 2 + val quranInfoThatSkips = QuranInfo(SkippingDataSource()) + assertThat(quranInfoThatSkips.mapSinglePageToDualPage(2)).isEqualTo(3) + assertThat(quranInfoThatSkips.mapSinglePageToDualPage(3)).isEqualTo(3) + assertThat(quranInfoThatSkips.mapSinglePageToDualPage(4)).isEqualTo(5) + assertThat(quranInfoThatSkips.mapSinglePageToDualPage(5)).isEqualTo(5) + + // hypothetical example where we skip 2 pages, so the first page is 3 + val quranInfoThatSkipsExtra = QuranInfo(SkippingDataSource(2)) + assertThat(quranInfoThatSkipsExtra.mapSinglePageToDualPage(3)).isEqualTo(4) + assertThat(quranInfoThatSkipsExtra.mapSinglePageToDualPage(4)).isEqualTo(4) + assertThat(quranInfoThatSkipsExtra.mapSinglePageToDualPage(5)).isEqualTo(6) + assertThat(quranInfoThatSkipsExtra.mapSinglePageToDualPage(6)).isEqualTo(6) + } + + @Test + fun testMapDualPageToSinglePage() { + val quranInfo = QuranInfo(MadaniDataSource()) + assertThat(quranInfo.mapDualPageToSinglePage(1)).isEqualTo(1) + assertThat(quranInfo.mapDualPageToSinglePage(2)).isEqualTo(1) + assertThat(quranInfo.mapDualPageToSinglePage(3)).isEqualTo(3) + assertThat(quranInfo.mapDualPageToSinglePage(4)).isEqualTo(3) + + // skipping a single page (ex naskh), so the first page is 2 + val quranInfoThatSkips = QuranInfo(SkippingDataSource()) + assertThat(quranInfoThatSkips.mapDualPageToSinglePage(2)).isEqualTo(2) + assertThat(quranInfoThatSkips.mapDualPageToSinglePage(3)).isEqualTo(2) + assertThat(quranInfoThatSkips.mapDualPageToSinglePage(4)).isEqualTo(4) + assertThat(quranInfoThatSkips.mapDualPageToSinglePage(5)).isEqualTo(4) + + // hypothetical example where we skip 2 pages, so the first page is 3 + val quranInfoThatSkipsExtra = QuranInfo(SkippingDataSource(2)) + assertThat(quranInfoThatSkipsExtra.mapDualPageToSinglePage(3)).isEqualTo(3) + assertThat(quranInfoThatSkipsExtra.mapDualPageToSinglePage(4)).isEqualTo(3) + assertThat(quranInfoThatSkipsExtra.mapDualPageToSinglePage(5)).isEqualTo(5) + assertThat(quranInfoThatSkipsExtra.mapDualPageToSinglePage(6)).isEqualTo(5) + } + + private class SkippingDataSource(skipCount: Int = 1) : MadaniDataSource() { + override val pagesToSkip: Int = skipCount + } } diff --git a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt index 1b056d60f3..8f98072f7e 100644 --- a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt +++ b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt @@ -23,8 +23,12 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { private val manazil = quranDataSource.manzilPageArray val quarters = quranDataSource.quartersArray + val skip = quranDataSource.pagesToSkip + private val firstPage = PAGES_FIRST + skip + val numberOfPages = quranDataSource.numberOfPages - private val numberOfPagesDual = numberOfPages / 2 + numberOfPages % 2 + val numberOfPagesConsideringSkipped = numberOfPages - skip + private val numberOfPagesDual = numberOfPagesConsideringSkipped / 2 + numberOfPagesConsideringSkipped % 2 fun getStartingPageForJuz(juz: Int): Int { return juzPageStart[juz - 1] @@ -44,7 +48,7 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { } fun isValidPage(page: Int): Boolean { - return page in PAGES_FIRST..numberOfPages + return page in firstPage..numberOfPages } fun getSuraNumberFromPage(page: Int): Int { @@ -88,7 +92,7 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { val page = when { inputPage > numberOfPages -> numberOfPages - inputPage < 1 -> 1 + inputPage < firstPage -> firstPage else -> inputPage } @@ -188,9 +192,9 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { isDualPagesVisible: Boolean ): Int { return if (isDualPagesVisible) { - (numberOfPagesDual - position) * 2 + ((numberOfPagesDual - position) * 2) + skip } else { - numberOfPages - position + (numberOfPagesConsideringSkipped - position) + skip } } @@ -200,9 +204,32 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { ): Int { return if (isDualPagesVisible) { val pageToUse = if (page % 2 != 0) { page + 1 } else { page } - numberOfPagesDual - pageToUse / 2 + val delta = if (page % 2 != 0) { skip } else 0 + numberOfPagesDual - pageToUse / 2 + delta + } else { + (numberOfPagesConsideringSkipped - page) + skip + } + } + + fun mapDualPageToSinglePage(page: Int): Int { + // selects the "first" page when mapping this dual page to a single page + // i.e. maps "left | right" => "right" (i.e. to first landscape page) + val amount = skip % 2 + return if (page % 2 == amount) { + page - 1 + } else { + page + } + } + + fun mapSinglePageToDualPage(page: Int): Int { + // selects the "second" page when viewing this page by another + // i.e. "left | right" => "left" irrespective of which is chosen, left/right + val amount = skip % 2 + return if (page % 2 != amount) { + page + 1 } else { - numberOfPages - page + page } } diff --git a/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt b/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt index 97d5261a05..956494cf04 100644 --- a/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt +++ b/common/data/src/main/java/com/quran/data/core/QuranPageInfo.kt @@ -7,4 +7,5 @@ interface QuranPageInfo { fun localizedPage(page: Int): String fun pageForSuraAyah(sura: Int, ayah: Int): Int fun manzilForPage(page: Int): String + fun skippedPagesCount(): Int } diff --git a/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt b/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt index 0f2aaaafd0..823b03f508 100644 --- a/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt +++ b/common/data/src/main/java/com/quran/data/pageinfo/common/MadaniDataSource.kt @@ -228,4 +228,5 @@ open class MadaniDataSource : QuranDataSource { override val manzilPageArray: Array = emptyArray() override val haveSidelines: Boolean = false + override val pagesToSkip: Int = 0 } diff --git a/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt b/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt index 729bf6e640..aa5825d83b 100644 --- a/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt +++ b/common/data/src/main/java/com/quran/data/source/QuranDataSource.kt @@ -15,4 +15,5 @@ interface QuranDataSource { val quartersArray: Array val manzilPageArray: Array val haveSidelines: Boolean + val pagesToSkip: Int } From fbf84db4928375d332fe444606a415e4cbed8668 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 13 Aug 2023 22:06:52 +0500 Subject: [PATCH 081/258] Clean up hacks for Shemerly in tablet Before this patch, tablet mode (displaying two pages side by side in landscape) would always count the second page as the page (for all purposes - that of the title, bookmarks, recent pages, etc). This fact, coupled with the fact that Shemerly has an odd number of pages, caused some issues on the very last page of Shemerly. Hacks were added in various places to subtract one from pages to support Shemerly properly, especially with respect to the last page. This patch cleans all this up by instead updating the tablet logic to consider the first of the two pages as the primary page for all purposes (title, bookmarks, recent pages, etc). --- .../labs/androidquran/ui/PagerActivity.java | 46 ++++--------------- .../ui/fragment/TabletFragment.java | 28 +++++------ .../java/com/quran/data/core/QuranInfo.kt | 5 +- 3 files changed, 28 insertions(+), 51 deletions(-) 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 38b703280b..afb8b292e0 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 @@ -429,17 +429,7 @@ public void onPageScrolled(int position, float positionOffset, int positionOffse @Override public void onPageSelected(int position) { Timber.d("onPageSelected(): %d", position); - final int potentialPage = quranInfo.getPageFromPosition(position, isDualPageVisible()); - - // work around for empty pages at the end of the mushaf in dual screen mode - // Shemerly has an odd number of pages (521), so when showing in tablet mode, - // the last page is empty. default to the previous page title in those cases. - final int page; - if (isDualPageVisible() && potentialPage == quranInfo.getNumberOfPages() + 1) { - page = quranInfo.getNumberOfPages(); - } else { - page = potentialPage; - } + final int page = quranInfo.getPageFromPosition(position, isDualPageVisible()); if (quranSettings.shouldDisplayMarkerPopup()) { lastPopupTime = QuranDisplayHelper.displayMarkerPopup( @@ -569,14 +559,7 @@ public Observable getViewPagerObservable() { @Override public void onPageSelected(int position) { final int page = quranInfo.getPageFromPosition(position, isDualPageVisible()); - // another workaround for shemerly where there is no page 522 - final int pageToSelect; - if (isDualPageVisible() && page == quranInfo.getNumberOfPages() + 1) { - pageToSelect = page - 1; - } else { - pageToSelect = page; - } - e.onNext(pageToSelect); + e.onNext(page); } }; @@ -1169,9 +1152,6 @@ private void switchToTranslation() { pagerAdapter.setTranslationMode(); showingTranslation = true; if (shouldUpdatePageNumber()) { - if (page % 2 == 0) { - page--; - } final int position = quranInfo.getPositionFromPage(page, false); viewPager.setCurrentItem(position); } @@ -1246,13 +1226,7 @@ private void refreshActionBarSpinner() { } private int getCurrentPage() { - final int page = quranInfo.getPageFromPosition(viewPager.getCurrentItem(), isDualPageVisible()); - if (isDualPageVisible() && page == quranInfo.getNumberOfPages() + 1) { - // hack for shemerly, where there is no page 522. - return page - 1; - } else { - return page; - } + return quranInfo.getPageFromPosition(viewPager.getCurrentItem(), isDualPageVisible()); } private void updateActionBarSpinner() { @@ -1512,8 +1486,7 @@ public void onPlayPressed() { } int position = viewPager.getCurrentItem(); - final int delta = isDualPageVisible() ? 1 : 0; - int page = quranInfo.getPageFromPosition(position, isDualPageVisible()) - delta; + int page = quranInfo.getPageFromPosition(position, isDualPageVisible()); // log the event quranEventLogger.logAudioPlayback(QuranEventLogger.AudioPlaybackSource.PAGE, @@ -1524,13 +1497,14 @@ public void onPlayPressed() { List startingSuraList = quranInfo.getListOfSurahWithStartingOnPage(page); if (startingSuraList.size() == 0 || (startingSuraList.size() == 1 && startingSuraList.get(0) == startSura)) { - playFromAyah(page, startSura, startAyah); + playFromAyah(startSura, startAyah); } else { promptForMultipleChoicePlay(page, startSura, startAyah, startingSuraList); } } - private void playFromAyah(int page, int startSura, int startAyah) { + private void playFromAyah(int startSura, int startAyah) { + final int page = quranInfo.getPageFromSuraAyah(startSura, startAyah); final SuraAyah start = new SuraAyah(startSura, startAyah); final SuraAyah end = getSelectionEnd(); // handle the case of multiple ayat being selected and play them as a range if so @@ -1789,7 +1763,7 @@ public boolean onMenuItemClick(MenuItem item) { } else if (itemId == com.quran.labs.androidquran.common.toolbar.R.id.cab_play_from_here) { quranEventLogger.logAudioPlayback(QuranEventLogger.AudioPlaybackSource.AYAH, audioStatusBar.getAudioInfo(), isDualPages, showingTranslation, isSplitScreen); - playFromAyah(getCurrentPage(), startSuraAyah.sura, startSuraAyah.ayah); + playFromAyah(startSuraAyah.sura, startSuraAyah.ayah); toggleActionBarVisibility(true); } else if (itemId == com.quran.labs.androidquran.common.toolbar.R.id.cab_recite_from_here) { pagerActivityRecitationPresenter.onRecitationPressed(); @@ -1926,9 +1900,9 @@ private void promptForMultipleChoicePlay(int page, int startSura, int startAyah, .setTitle(getString(R.string.playback_prompt_title)) .setAdapter(adapter, (dialog, i) -> { if (i == 0) { - playFromAyah(page, startSura, startAyah); + playFromAyah(startSura, startAyah); } else { - playFromAyah(page, startingSuraList.get(i), 1); + playFromAyah(startingSuraList.get(i), 1); } dialog.dismiss(); promptDialog = null; diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java index ec74806277..9778107988 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java @@ -131,15 +131,15 @@ public View onCreateView(@NonNull LayoutInflater inflater, mainView = new TabletView(context); if (mode == Mode.ARABIC) { - mainView.init(TabletView.QURAN_PAGE, TabletView.QURAN_PAGE, pageViewFactory, pageNumber, pageNumber - 1); + mainView.init(TabletView.QURAN_PAGE, TabletView.QURAN_PAGE, pageViewFactory, pageNumber + 1, pageNumber); if (mainView.getLeftPage() instanceof QuranImagePageLayout) { leftImageView = ((QuranImagePageLayout) mainView.getLeftPage()).getImageView(); rightImageView = ((QuranImagePageLayout) mainView.getRightPage()).getImageView(); } - mainView.setPageController(this, pageNumber, pageNumber - 1, quranInfo.getSkip()); + mainView.setPageController(this, pageNumber + 1, pageNumber, quranInfo.getSkip()); } else if (mode == Mode.TRANSLATION) { if (!isSplitScreen) { - mainView.init(TabletView.TRANSLATION_PAGE, TabletView.TRANSLATION_PAGE, pageViewFactory, pageNumber, pageNumber - 1); + mainView.init(TabletView.TRANSLATION_PAGE, TabletView.TRANSLATION_PAGE, pageViewFactory, pageNumber + 1, pageNumber); leftTranslation = ((QuranTranslationPageLayout) mainView.getLeftPage()).getTranslationView(); rightTranslation = @@ -148,7 +148,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, PagerActivity pagerActivity = (PagerActivity) context; leftTranslation.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); rightTranslation.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); - mainView.setPageController(this, pageNumber, pageNumber - 1, quranInfo.getSkip()); + mainView.setPageController(this, pageNumber + 1, pageNumber, quranInfo.getSkip()); } else { initSplitMode(); } @@ -288,18 +288,18 @@ public AyahTrackerItem[] getAyahTrackerItems() { AyahTrackerItem right; if (mode == Mode.ARABIC) { if (leftImageView != null && rightImageView != null) { - if (quranInfo.isValidPage(pageNumber)) { - left = new AyahImageTrackerItem(pageNumber, + if (quranInfo.isValidPage(pageNumber + 1)) { + left = new AyahImageTrackerItem(pageNumber + 1, quranInfo, quranDisplayData, false, imageDrawHelpers, leftImageView); } else { - left = new NoOpImageTrackerItem(pageNumber); + left = new NoOpImageTrackerItem(pageNumber + 1); } right = new AyahImageTrackerItem( - pageNumber - 1, quranInfo, quranDisplayData, true, imageDrawHelpers, + pageNumber, quranInfo, quranDisplayData, true, imageDrawHelpers, rightImageView); } else { return new AyahTrackerItem[0]; @@ -340,8 +340,8 @@ public AyahTrackerItem[] getAyahTrackerItems() { ayahTrackerItems = new AyahTrackerItem[] { splitItem }; return ayahTrackerItems; } else { - left = new AyahTranslationTrackerItem(pageNumber, quranInfo, leftTranslation); - right = new AyahTranslationTrackerItem(pageNumber - 1, quranInfo, rightTranslation); + left = new AyahTranslationTrackerItem(pageNumber + 1, quranInfo, leftTranslation); + right = new AyahTranslationTrackerItem(pageNumber, quranInfo, rightTranslation); } } else { return new AyahTrackerItem[0]; @@ -360,7 +360,7 @@ public void onAttach(@NonNull Context context) { isSplitScreen = getArguments().getBoolean(IS_SPLIT_SCREEN, false); final int[] pages = (isSplitScreen && mode == Mode.TRANSLATION) ? - new int[]{ pageNumber } : new int[]{ pageNumber - 1, pageNumber }; + new int[]{ pageNumber } : new int[]{ pageNumber, pageNumber + 1 }; ((PagerActivity) getActivity()).getPagerActivityComponent() .quranPageComponentBuilder() @@ -387,7 +387,7 @@ public void setPageBitmap(int page, @NonNull Bitmap pageBitmap) { if (isSplitScreen && mode == Mode.TRANSLATION) { splitImageView.setImageDrawable(new BitmapDrawable(getResources(), pageBitmap)); } else { - ImageView imageView = page == pageNumber - 1 ? rightImageView : leftImageView; + ImageView imageView = page == pageNumber ? rightImageView : leftImageView; if (imageView != null) { imageView.setImageDrawable(new BitmapDrawable(getResources(), pageBitmap)); } @@ -417,9 +417,9 @@ public void setVerses(int page, splitTranslationView.setVerses(quranDisplayData, translations, verses); } else { if (page == pageNumber) { - leftTranslation.setVerses(quranDisplayData, translations, verses); - } else if (page == pageNumber - 1) { rightTranslation.setVerses(quranDisplayData, translations, verses); + } else if (page == pageNumber + 1) { + leftTranslation.setVerses(quranDisplayData, translations, verses); } } } diff --git a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt index 8f98072f7e..8e237217d8 100644 --- a/common/data/src/main/java/com/quran/data/core/QuranInfo.kt +++ b/common/data/src/main/java/com/quran/data/core/QuranInfo.kt @@ -192,7 +192,10 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { isDualPagesVisible: Boolean ): Int { return if (isDualPagesVisible) { - ((numberOfPagesDual - position) * 2) + skip + // return the "first" page in a tablet view + // i.e. for [page 2][page 1] should return [page 1]. + // similarly, for Naskh, [page 3][page 2] should return [page 2]. + return (((numberOfPagesDual - position) * 2) + skip) - 1 } else { (numberOfPagesConsideringSkipped - position) + skip } From ed77ce25ce455654b6fec3405e6fa26682fe323f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 07:49:14 +0000 Subject: [PATCH 082/258] Update dependency gradle to v8.3 --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 63375 -> 63721 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 3 ++- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index 17a8ddce2d..c30b486a89 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cdf41af1ab109bc7f253b2b887023340..7f93135c49b765f8051ef9d0a6055ff8e46073d8 100644 GIT binary patch delta 28216 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f4197d5f4..ac72c34e8a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca147..0adc8e1a53 100755 --- a/gradlew +++ b/gradlew @@ -83,7 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum From 4f0b8e59f7251891b4af11aea4fa9624c5aad49a Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 19 Aug 2023 12:38:12 +0400 Subject: [PATCH 083/258] Minor clean up to build scripts Android libraries no longer need a target sdk, since that really only makes sense on applications. --- .../main/kotlin/AndroidLibraryComposeConventionPlugin.kt | 1 - .../src/main/kotlin/AndroidLibraryConventionPlugin.kt | 1 - build.gradle | 7 ------- 3 files changed, 9 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 2e5cccd5dc..8ccb60065f 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -18,7 +18,6 @@ class AndroidLibraryComposeConventionPlugin : Plugin { extensions.configure { applyAndroidCommon(target) - defaultConfig.targetSdk = 32 buildFeatures.compose = true composeOptions.kotlinCompilerExtensionVersion = "1.5.0-dev-k1.9.0-6a60475e07f" } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 853767b1ef..6ee755e618 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -17,7 +17,6 @@ class AndroidLibraryConventionPlugin : Plugin { extensions.configure { applyAndroidCommon(target) - defaultConfig.targetSdk = 32 } applyKotlinCommon() diff --git a/build.gradle b/build.gradle index a5b8455fc8..a529baa4ef 100644 --- a/build.gradle +++ b/build.gradle @@ -33,13 +33,6 @@ buildscript { androidxJunitExtVersion = '1.1.5' deps = [ - android: [ - build: [ - minSdkVersion : 21, - targetSdkVersion : 32, - compileSdkVersion: 33 - ] - ], dagger: [ apt: "com.google.dagger:dagger-compiler:${daggerVersion}", runtime: "com.google.dagger:dagger:${daggerVersion}" From 1f517134c4aca26ccfa9d7f430d674020e554f40 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 19 Aug 2023 13:07:33 +0400 Subject: [PATCH 084/258] Update compileSdk to 34 and update Compose --- .../labs/androidquran/ui/fragment/QuranSettingsFragment.kt | 2 +- .../java/com/quran/labs/androidquran/util/SettingsImpl.kt | 6 ++++-- .../main/kotlin/AndroidLibraryComposeConventionPlugin.kt | 2 +- .../com/quran/labs/androidquran/buildutil/AndroidCommon.kt | 2 +- .../quran/labs/androidquran/buildutil/DependenciesCommon.kt | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranSettingsFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranSettingsFragment.kt index b3ee2900c6..db23c75496 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranSettingsFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranSettingsFragment.kt @@ -71,7 +71,7 @@ class QuranSettingsFragment : PreferenceFragmentCompat(), super.onPause() } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { if (key == Constants.PREF_USE_ARABIC_NAMES) { val context = activity if (context is QuranPreferenceActivity) { diff --git a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt index b11500da72..d19b8e116c 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt @@ -14,8 +14,10 @@ class SettingsImpl @Inject constructor(private val quranSettings: QuranSettings) callbackFlow { val callback = SharedPreferences.OnSharedPreferenceChangeListener { _, pref -> - trySendBlocking(pref) - .onFailure {} + if (pref != null) { + trySendBlocking(pref) + .onFailure {} + } } quranSettings.registerPreferencesListener(callback) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 8ccb60065f..4d820d2d12 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -19,7 +19,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin { extensions.configure { applyAndroidCommon(target) buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.5.0-dev-k1.9.0-6a60475e07f" + composeOptions.kotlinCompilerExtensionVersion = "1.5.0" } applyKotlinCommon() diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt index 2a166539f9..d6ffd5bbf5 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/AndroidCommon.kt @@ -7,7 +7,7 @@ import org.gradle.api.plugins.ExtensionAware import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions fun CommonExtension<*, *, *, *, *>.applyAndroidCommon(project: Project) { - compileSdk = 33 + compileSdk = 34 defaultConfig.minSdk = 21 compileOptions { diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt index 1141a18e67..9e91a4d0e6 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt @@ -6,6 +6,6 @@ import org.gradle.kotlin.dsl.dependencies fun Project.applyBoms() { dependencies { add("implementation", platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) - add("implementation", platform("androidx.compose:compose-bom:2023.06.00")) + add("implementation", platform("androidx.compose:compose-bom:2023.08.00")) } } From d47ae0f47ffeefff86db3d2be27c2c768f3dbb41 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 20 Aug 2023 22:05:37 +0400 Subject: [PATCH 085/258] Target sdk 33 --- app/src/main/AndroidManifest.xml | 9 ++- .../labs/androidquran/QuranDataActivity.kt | 44 +++++++++++- .../presenter/audio/AudioPresenter.kt | 18 ++++- .../service/util/PermissionUtil.kt | 36 ++++++++++ .../service/util/QuranDownloadNotifier.java | 3 +- .../labs/androidquran/ui/PagerActivity.java | 53 ++++++++++++--- .../androidquran/worker/AudioUpdateWorker.kt | 10 ++- app/src/main/res/values/colors.xml | 2 - app/src/main/res/values/strings.xml | 3 + .../AndroidApplicationConventionPlugin.kt | 2 +- common/toolbar/src/main/res/values/colors.xml | 2 + .../SheikhAudioDownloadsActivity.kt | 68 +++++++++++++++++-- .../sheikhdownload/SheikhDownloadDialog.kt | 5 +- .../presenter/SheikhAudioPresenter.kt | 4 ++ ...equestPostNotificationsPermissionDialog.kt | 34 ++++++++++ .../src/main/res/values/strings.xml | 2 + 16 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/RequestPostNotificationsPermissionDialog.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7de6f0d36..65acc415de 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,9 @@ + + + @@ -152,11 +155,13 @@ + android:name=".service.QuranDownloadService" + android:foregroundServiceType="dataSync"/> + android:name=".service.AudioService" + android:foregroundServiceType="mediaPlayback"> diff --git a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt index 85e83c7de2..e044b2ebb7 100644 --- a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt @@ -12,6 +12,7 @@ import android.os.Bundle import android.os.Environment import android.text.TextUtils import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback @@ -81,6 +82,8 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio private var quranDataStatus: QuranDataStatus? = null private var updateDialog: AlertDialog? = null private var disposable: Disposable? = null + private var lastForceValue: Boolean = false + private var didCheckPermissions: Boolean = false private val scope = MainScope() @@ -141,7 +144,11 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio Timber.e(ise) } } - checkPermissions() + + if (!didCheckPermissions) { + didCheckPermissions = true + checkPermissions() + } } override fun onPause() { @@ -279,6 +286,14 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio quranSettings.setSdcardPermissionsDialogPresented() } + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun requestPostNotificationPermission() { + ActivityCompat.requestPermissions( + this, arrayOf(permission.POST_NOTIFICATIONS), + REQUEST_POST_NOTIFICATION_PERMISSIONS + ) + } + override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, @@ -317,6 +332,8 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio runListViewWithoutPages() } } + } else if (requestCode == REQUEST_POST_NOTIFICATION_PERMISSIONS) { + actuallyDownloadQuranImages(lastForceValue) } } @@ -583,6 +600,30 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio * @param force whether to force the download to restart or not */ private fun downloadQuranImages(force: Boolean) { + if (PermissionUtil.havePostNotificationPermission(this)) { + actuallyDownloadQuranImages(force) + } else if (PermissionUtil.canRequestPostNotificationPermission(this)) { + val dialog = PermissionUtil.buildPostPermissionDialog( + this, + onAccept = { + lastForceValue = force + permissionsDialog = null + requestPostNotificationPermission() + }, + onDecline = { + permissionsDialog = null + actuallyDownloadQuranImages(force) + } + ) + permissionsDialog = dialog + dialog.show() + } else { + lastForceValue = force + requestPostNotificationPermission() + } + } + + private fun actuallyDownloadQuranImages(force: Boolean) { // 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 @@ -690,6 +731,7 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio companion object { const val PAGES_DOWNLOAD_KEY = "PAGES_DOWNLOAD_KEY" private const val REQUEST_WRITE_TO_SDCARD_PERMISSIONS = 1 + private const val REQUEST_POST_NOTIFICATION_PERMISSIONS = 2 private const val QURAN_DIRECTORY_MARKER_FILE = "q4a" private const val QURAN_HIDDEN_DIRECTORY_MARKER_FILE = ".q4a" } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt index 43536c5264..0be9b85176 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt @@ -67,12 +67,20 @@ constructor(private val quranDisplayData: QuranDisplayData, } } - fun play(audioRequest: AudioRequest) { + private fun play(audioRequest: AudioRequest) { lastAudioRequest = audioRequest + proceedWithAudioRequest(audioRequest) + } + + private fun proceedWithAudioRequest(audioRequest: AudioRequest, bypassChecks: Boolean = false) { pagerActivity?.let { val downloadIntent = getDownloadIntent(it, audioRequest) if (downloadIntent != null) { - it.handleRequiredDownload(downloadIntent) + if (bypassChecks) { + it.proceedWithDownload(downloadIntent) + } else { + it.handleRequiredDownload(downloadIntent) + } } else { // play the audio it.handlePlayback(audioRequest) @@ -84,6 +92,12 @@ constructor(private val quranDisplayData: QuranDisplayData, lastAudioRequest?.let { play(it) } } + fun onPostNotificationsPermissionResponse(granted: Boolean) { + lastAudioRequest?.let { audioRequest -> + proceedWithAudioRequest(audioRequest, true) + } + } + fun onDownloadSuccess() { lastAudioRequest?.let { play(it) } } diff --git a/app/src/main/java/com/quran/labs/androidquran/service/util/PermissionUtil.kt b/app/src/main/java/com/quran/labs/androidquran/service/util/PermissionUtil.kt index b68e908d9d..a973618885 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/util/PermissionUtil.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/util/PermissionUtil.kt @@ -5,10 +5,14 @@ import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.os.Build +import androidx.annotation.RequiresApi +import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import com.quran.labs.androidquran.R import com.quran.labs.androidquran.util.QuranSettings + object PermissionUtil { @JvmStatic @@ -24,4 +28,36 @@ object PermissionUtil { ActivityCompat.shouldShowRequestPermissionRationale( activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) } + + @JvmStatic + fun havePostNotificationPermission(context: Context): Boolean { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || + ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + @JvmStatic + fun canRequestPostNotificationPermission(activity: Activity): Boolean { + return ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.POST_NOTIFICATIONS) + } + + @JvmStatic + fun buildPostPermissionDialog( + context: Context, + onAccept: (() -> Unit), + onDecline: (() -> Unit) + ): AlertDialog { + val builder = AlertDialog.Builder(context) + builder.setMessage(R.string.post_notification_permission) + .setPositiveButton(R.string.downloadPrompt_ok) { dialog, _ -> + dialog.dismiss() + onAccept() + } + .setNegativeButton(R.string.downloadPrompt_no) { dialog, _ -> + dialog.dismiss() + onDecline() + } + return builder.create() + } } diff --git a/app/src/main/java/com/quran/labs/androidquran/service/util/QuranDownloadNotifier.java b/app/src/main/java/com/quran/labs/androidquran/service/util/QuranDownloadNotifier.java index 3fe16154bd..bc2f8490a4 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/util/QuranDownloadNotifier.java +++ b/app/src/main/java/com/quran/labs/androidquran/service/util/QuranDownloadNotifier.java @@ -5,6 +5,7 @@ import android.app.Service; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.os.Parcelable; import androidx.core.app.NotificationCompat; @@ -374,7 +375,7 @@ private void showNotification(String titleString, if (shouldForeground && !isForeground) { service.startForeground(notificationId, builder.build()); isForeground = true; - } else { + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || notificationManager.areNotificationsEnabled()) { notificationManager.notify(notificationId, builder.build()); } } catch (SecurityException | IllegalStateException se) { 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 afb8b292e0..13b0d3ca9c 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 @@ -4,6 +4,7 @@ import static com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter.TAG_PAGE; import static com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter.TRANSLATION_PAGE; +import android.Manifest; import android.app.ProgressDialog; import android.app.SearchManager; import android.content.BroadcastReceiver; @@ -33,6 +34,8 @@ import android.widget.FrameLayout; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -86,6 +89,7 @@ import com.quran.labs.androidquran.service.AudioService; 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.fragment.AddTagDialog; @@ -216,6 +220,7 @@ public class PagerActivity extends AppCompatActivity implements private SlidingUpPanelLayout slidingPanel; private ViewPager slidingPager; private SlidingPagerAdapter slidingPagerAdapter; + private ActivityResultLauncher requestPermissionLauncher; private int defaultNavigationBarColor; private boolean isSplitScreen = false; @@ -535,6 +540,11 @@ public void onPageSelected(int position) { ayah -> { ensurePage(ayah.sura, ayah.ayah); return null; }, sliderPage -> { showSlider(slidingPagerAdapter.getPagePosition(sliderPage)); return null; } )); + + requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + audioPresenter.onPostNotificationsPermissionResponse(isGranted); + }); } @Override @@ -963,12 +973,19 @@ public void onPause() { promptDialog.dismiss(); promptDialog = null; } - audioPresenter.unbind(this); recentPagePresenter.unbind(this); quranSettings.setWasShowingTranslation(pagerAdapter.isShowingTranslation()); super.onPause(); } + @Override + protected void onStop() { + // the activity will be paused when requesting notification + // permissions, which will otherwise break audio presenter. + audioPresenter.unbind(this); + super.onStop(); + } + @Override protected void onDestroy() { Timber.d("onDestroy()"); @@ -1544,14 +1561,34 @@ public void handleRequiredDownload(Intent downloadIntent) { if (needsPermission) { audioStatusBar.switchMode(AudioStatusBar.PROMPT_DOWNLOAD_MODE); + } else if (!PermissionUtil.havePostNotificationPermission(this)) { + if (PermissionUtil.canRequestPostNotificationPermission(this)) { + promptDialog = PermissionUtil.buildPostPermissionDialog(this, + () -> { + promptDialog = null; + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + return null; + }, () -> { + proceedWithDownload(downloadIntent); + promptDialog = null; + return null; + }); + promptDialog.show(); + } else { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } } else { - if (isActionBarHidden) { - toggleActionBar(); - } - audioStatusBar.switchMode(AudioStatusBar.DOWNLOADING_MODE); - Timber.d("starting service in handleRequiredDownload"); - startService(downloadIntent); + proceedWithDownload(downloadIntent); + } + } + + public void proceedWithDownload(Intent downloadIntent) { + if (isActionBarHidden) { + toggleActionBar(); } + audioStatusBar.switchMode(AudioStatusBar.DOWNLOADING_MODE); + Timber.d("starting service in handleRequiredDownload"); + startService(downloadIntent); } public void handlePlayback(AudioRequest request) { @@ -1743,7 +1780,7 @@ private AyahTracker resolveCurrentTracker() { private class AyahMenuItemSelectionHandler implements MenuItem.OnMenuItemClickListener { @Override - public boolean onMenuItemClick(MenuItem item) { + public boolean onMenuItemClick(@NonNull MenuItem item) { int sliderPage = -1; final AyahSelection currentSelection = readingEventPresenter.currentAyahSelection(); final SuraAyah startSuraAyah = AyahSelectionKt.startSuraAyah(currentSelection); diff --git a/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt b/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt index 2801e3d249..c778775f71 100644 --- a/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt +++ b/app/src/main/java/com/quran/labs/androidquran/worker/AudioUpdateWorker.kt @@ -2,6 +2,7 @@ package com.quran.labs.androidquran.worker import android.app.NotificationManager import android.content.Context +import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.CoroutineWorker @@ -105,11 +106,14 @@ class AudioUpdateWorker( val notificationManager = context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - NotificationChannelUtil.setupNotificationChannel( + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || notificationManager.areNotificationsEnabled()) { + NotificationChannelUtil.setupNotificationChannel( notificationManager, Constants.DOWNLOAD_CHANNEL, context.getString(R.string.notification_channel_download) - ) - notificationManager.notify(Constants.NOTIFICATION_ID_AUDIO_UPDATE, notification) + ) + notificationManager.notify(Constants.NOTIFICATION_ID_AUDIO_UPDATE, notification) + } } class Factory @Inject constructor( diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9da48dc0a3..7397e9f70b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -6,7 +6,6 @@ #89b2dfdb #FFF4CB #ff424242 - #33ffffff @color/sura_number_color #ffffffff #7fffffff @@ -33,7 +32,6 @@ #40EBEB21 #40A4A4A4 #ff33b5e5 - #552bff36 @color/accent_color @color/accent_color #ff112e44 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f6ab199b3..490dbc7fbd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -310,6 +310,9 @@ Cancel We need to download one or two small files to support sharing and translation. Download now? + + Allow Quran for Android to post notifications? This is only used to give updates on download status, or to warn when audio files are automatically updated. + Remove Translation? Are you sure you would like to remove the %1$s? diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index d93bae5ccb..0c56ae88bf 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -18,7 +18,7 @@ class AndroidApplicationConventionPlugin : Plugin { extensions.configure { applyAndroidCommon(target) - defaultConfig.targetSdk = 32 + defaultConfig.targetSdk = 33 } applyKotlinCommon() diff --git a/common/toolbar/src/main/res/values/colors.xml b/common/toolbar/src/main/res/values/colors.xml index 7d696a1371..3cb48500cd 100644 --- a/common/toolbar/src/main/res/values/colors.xml +++ b/common/toolbar/src/main/res/values/colors.xml @@ -1,4 +1,6 @@ #ed2b5836 + #33ffffff + #552bff36 diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/SheikhAudioDownloadsActivity.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/SheikhAudioDownloadsActivity.kt index 346beeb383..f6103d8998 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/SheikhAudioDownloadsActivity.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/SheikhAudioDownloadsActivity.kt @@ -1,8 +1,12 @@ package com.quran.mobile.feature.downloadmanager +import android.Manifest +import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier +import androidx.core.app.ActivityCompat import com.quran.common.search.SearchTextUtil import com.quran.labs.androidquran.common.ui.core.QuranTheme import com.quran.mobile.di.QuranApplicationComponentProvider @@ -21,6 +26,7 @@ import com.quran.mobile.feature.downloadmanager.presenter.SheikhAudioPresenter import com.quran.mobile.feature.downloadmanager.ui.sheikhdownload.DownloadErrorDialog import com.quran.mobile.feature.downloadmanager.ui.sheikhdownload.DownloadProgressDialog import com.quran.mobile.feature.downloadmanager.ui.sheikhdownload.RemoveConfirmationDialog +import com.quran.mobile.feature.downloadmanager.ui.sheikhdownload.RequestPostNotificationsPermissionDialog import com.quran.mobile.feature.downloadmanager.ui.sheikhdownload.SheikhDownloadToolbar import com.quran.mobile.feature.downloadmanager.ui.sheikhdownload.SheikhSuraInfoList import com.quran.mobile.feature.downloadmanager.ui.sheikhdownload.SuraRangeDialog @@ -42,6 +48,15 @@ class SheikhAudioDownloadsActivity : ComponentActivity() { private var qariId: Int = -1 private val scope = MainScope() + private var didRequestPostNotificationsPermission: Boolean = false + + private val requestPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + // do nothing for now + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) qariId = intent.getIntExtra(EXTRA_QARI_ID, -1) @@ -125,6 +140,8 @@ class SheikhAudioDownloadsActivity : ComponentActivity() { ) is SheikhDownloadDialog.DownloadError -> DownloadErrorDialog(dialog.errorString, sheikhAudioPresenter::onCancelDialog) + is SheikhDownloadDialog.PostNotificationsPermission -> + RequestPostNotificationsPermissionDialog(::onCanRequestPermissions, ::onDoNotRequestPermissions) else -> {} } } @@ -154,8 +171,10 @@ class SheikhAudioDownloadsActivity : ComponentActivity() { if (selectedSuras.isEmpty()) { sheikhAudioPresenter.selectSura(sura) if (!isStartSelection) { - scope.launch { - sheikhAudioPresenter.onSuraAction(qariId, sura) + processPostNotificationsPermission { + scope.launch { + sheikhAudioPresenter.onSuraAction(qariId, sura) + } } } } else { @@ -163,9 +182,39 @@ class SheikhAudioDownloadsActivity : ComponentActivity() { } } + private fun needPostNotificationsPermission(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED + } else { + false + } + } + + private fun processPostNotificationsPermission(lambda: (() -> Unit)) { + if (needPostNotificationsPermission() && + !didRequestPostNotificationsPermission && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + ) { + didRequestPostNotificationsPermission = true + if (ActivityCompat.shouldShowRequestPermissionRationale( + this, + Manifest.permission.POST_NOTIFICATIONS + ) + ) { + sheikhAudioPresenter.showPostNotificationsRationaleDialog() + } else { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } else { + lambda() + } + } + private fun onDownloadSelectionSelected() { - scope.launch { - sheikhAudioPresenter.onDownloadSelection(qariId) + processPostNotificationsPermission { + scope.launch { + sheikhAudioPresenter.onDownloadSelection(qariId) + } } } @@ -183,6 +232,17 @@ class SheikhAudioDownloadsActivity : ComponentActivity() { } } + private fun onCanRequestPermissions() { + sheikhAudioPresenter.onCancelDialog() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + } + + private fun onDoNotRequestPermissions() { + sheikhAudioPresenter.onCancelDialog() + } + companion object { const val EXTRA_QARI_ID = "SheikhAudioDownloadsActivity.extra_qari_id" } diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/model/sheikhdownload/SheikhDownloadDialog.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/model/sheikhdownload/SheikhDownloadDialog.kt index 7ebae6984b..584c535cad 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/model/sheikhdownload/SheikhDownloadDialog.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/model/sheikhdownload/SheikhDownloadDialog.kt @@ -3,9 +3,10 @@ package com.quran.mobile.feature.downloadmanager.model.sheikhdownload import kotlinx.coroutines.flow.Flow sealed class SheikhDownloadDialog { - object None : SheikhDownloadDialog() + data object None : SheikhDownloadDialog() data class RemoveConfirmation(val surasToRemove: List): SheikhDownloadDialog() - object DownloadRangeSelection: SheikhDownloadDialog() + data object DownloadRangeSelection: SheikhDownloadDialog() + data object PostNotificationsPermission : SheikhDownloadDialog() data class DownloadStatus(val statusFlow: Flow): SheikhDownloadDialog() data class DownloadError(val errorCode: Int, val errorString: String): SheikhDownloadDialog() } diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt index eda75240d3..0002b77e51 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt @@ -86,6 +86,10 @@ class SheikhAudioPresenter @Inject constructor( selectedSurasFlow.value = emptyList() } + fun showPostNotificationsRationaleDialog() { + currentDialogFlow.value = SheikhDownloadDialog.PostNotificationsPermission + } + suspend fun onSuraAction(qariId: Int, sura: SuraForQari) { if (sura.isDownloaded) { onRemoveSelection() diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/RequestPostNotificationsPermissionDialog.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/RequestPostNotificationsPermissionDialog.kt new file mode 100644 index 0000000000..a5bec201bd --- /dev/null +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/RequestPostNotificationsPermissionDialog.kt @@ -0,0 +1,34 @@ +package com.quran.mobile.feature.downloadmanager.ui.sheikhdownload + +import androidx.compose.material.AlertDialog +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.quran.mobile.feature.downloadmanager.R + +@Composable +fun RequestPostNotificationsPermissionDialog( + onConfirmation: (() -> Unit), + onDismiss: (() -> Unit), +) { + AlertDialog( + title = { + Text(text = stringResource(id = R.string.audio_manager_post_notifications_permission_title)) + }, + text = { + Text(text = stringResource(id = R.string.audio_manager_post_notifications_permission_description)) + }, + confirmButton = { + TextButton(onClick = onConfirmation) { + Text(text = stringResource(id = android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(id = com.quran.mobile.common.ui.core.R.string.cancel)) + } + }, + onDismissRequest = onDismiss + ) +} diff --git a/feature/downloadmanager/src/main/res/values/strings.xml b/feature/downloadmanager/src/main/res/values/strings.xml index cbfcfb79e0..4b1c1c08f0 100644 --- a/feature/downloadmanager/src/main/res/values/strings.xml +++ b/feature/downloadmanager/src/main/res/values/strings.xml @@ -23,6 +23,8 @@ Download all Download selection Delete selection + Allow posting notifications? + This is only used to give updates on download status, or to warn when audio files are automatically updated. Downloading… %s MB From 0dadfce5f1a5b1d3796dd8deeb3b6dd78ac7911f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 04:11:04 +0000 Subject: [PATCH 086/258] Update dependency com.android.tools.build:gradle to v8.1.1 --- build-logic/convention/build.gradle.kts | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index d6eb084bc5..4aadf151a8 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.1.0") + compileOnly("com.android.tools.build:gradle:8.1.1") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") } diff --git a/build.gradle b/build.gradle index a529baa4ef..1b07a8702d 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.0' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.8" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" From d0f440afab0f5171b1769daad7647a4cd66ad08a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 09:34:03 +0000 Subject: [PATCH 087/258] Update dependency org.mockito:mockito-core to v5.5.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a529baa4ef..3022959cdc 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ buildscript { // testing junitVersion = '4.13.2' - mockitoVersion = '5.4.0' + mockitoVersion = '5.5.0' truthVersion = '1.1.5' espressoVersion = '3.5.1' robolectricVersion = '4.10.3' From 7b67177e54881e69ce7ca5366687b5cfef6d1e63 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Aug 2023 13:56:39 +0000 Subject: [PATCH 088/258] Update dependency io.reactivex.rxjava3:rxjava to v3.1.7 --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index a4d899b0f1..50bf05282a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -146,7 +146,7 @@ dependencies { implementation "com.squareup.okio:okio:${okioVersion}" // rx - implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxjava:3.1.7' implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' // dagger From 63c41d48f8c3eae9f3b26b3abc0e1e43dcc4e9f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:16:35 +0000 Subject: [PATCH 089/258] Update daggerVersion to v2.48 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a529baa4ef..56fdd4d4f6 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { kotlinVersion = '1.9.0' coroutinesVersion = '1.7.3' - daggerVersion = '2.47' + daggerVersion = '2.48' androidxMediaVersion = '1.6.0' androidxAnnotationVersion = '1.6.0' From 1f6223d1e21f84b2191e3f93874a3549aaf3ce18 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 31 Aug 2023 20:11:25 +0000 Subject: [PATCH 090/258] Update dependency com.google.firebase:firebase-crashlytics-gradle to v2.9.9 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1b07a8702d..37f50e9987 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.1.1' - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.8" + classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" classpath "com.google.gms:google-services:4.3.15" From 6041a93863fb482d9f1e7721b31a92c6b9b072bc Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 3 Sep 2023 13:39:07 +0400 Subject: [PATCH 091/258] Update Kotlin to 1.9.10 and Compose Compiler --- .../src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt | 2 +- build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 4d820d2d12..22cf65de0d 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -19,7 +19,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin { extensions.configure { applyAndroidCommon(target) buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.5.0" + composeOptions.kotlinCompilerExtensionVersion = "1.5.3" } applyKotlinCommon() diff --git a/build.gradle b/build.gradle index 3b1475df1d..356d724aba 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - kotlinVersion = '1.9.0' + kotlinVersion = '1.9.10' coroutinesVersion = '1.7.3' daggerVersion = '2.48' From c926cd0e60494fbd81f94cefe91f9c5862be91cf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 13:43:18 +0000 Subject: [PATCH 092/258] Update actions/checkout action to v4 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/post_merge.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30deb30d84..e05666f9c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@v3 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - name: Setup JDK uses: actions/setup-java@v3 @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the code - uses: actions/checkout@v3 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - name: Setup JDK uses: actions/setup-java@v3 @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the code - uses: actions/checkout@v3 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - name: Setup JDK uses: actions/setup-java@v3 @@ -113,7 +113,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@v3 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - name: Setup JDK uses: actions/setup-java@v3 diff --git a/.github/workflows/post_merge.yml b/.github/workflows/post_merge.yml index 2fa5781982..e383bb8584 100644 --- a/.github/workflows/post_merge.yml +++ b/.github/workflows/post_merge.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@v3 + uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 - name: Setup JDK uses: actions/setup-java@v3 From 1b0a469245734145e658c217dd2796661bdf291a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 21:49:20 +0000 Subject: [PATCH 093/258] Update dependency androidx.annotation:annotation to v1.7.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 356d724aba..a63e0335ca 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { daggerVersion = '2.48' androidxMediaVersion = '1.6.0' - androidxAnnotationVersion = '1.6.0' + androidxAnnotationVersion = '1.7.0' androidxFragmentVersion = '1.6.1' androidxPreferencesVersion = '1.2.1' androidxAppcompatVersion = '1.6.1' From acf5226065d839b7eb760cd3f56e17c65d94a336 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:51:16 +0000 Subject: [PATCH 094/258] Update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v1.9.10 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 4aadf151a8..cdf93fc89b 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:8.1.1") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") } gradlePlugin { From 65a684fbe52f69cde679e2a61176034d5fac9c81 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 8 Sep 2023 23:51:24 +0400 Subject: [PATCH 095/258] Update Anvil to 2.4.8 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a63e0335ca..face931523 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { materialComponentsVersion = '1.9.0' coreKtxVersion = '1.9.0' - anvilVersion = '2.4.7' + anvilVersion = '2.4.8' moshiVersion = '1.15.0' okioVersion = '3.5.0' retrofitVersion = '2.9.0' From 3ab36606a0b7738d44558337eaa3c9153cb96adf Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 8 Sep 2023 23:56:50 +0400 Subject: [PATCH 096/258] Update compose-bom to 2023.09.00 --- .../com/quran/labs/androidquran/buildutil/DependenciesCommon.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt index 9e91a4d0e6..1071b5cb2d 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt @@ -6,6 +6,6 @@ import org.gradle.kotlin.dsl.dependencies fun Project.applyBoms() { dependencies { add("implementation", platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) - add("implementation", platform("androidx.compose:compose-bom:2023.08.00")) + add("implementation", platform("androidx.compose:compose-bom:2023.09.00")) } } From d101717eeb5e3049be865162924a3326e2e7fc7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Sep 2023 21:49:06 +0000 Subject: [PATCH 097/258] Update dependency com.google.gms:google-services to v4.4.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index face931523..6099a6246a 100644 --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ buildscript { classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" - classpath "com.google.gms:google-services:4.3.15" + classpath "com.google.gms:google-services:4.4.0" classpath "app.cash.sqldelight:gradle-plugin:$sqldelightVersion" classpath "com.squareup.anvil:gradle-plugin:$anvilVersion" } From 22c1f4573d7a5eb54cf0d6a1fe9e450e120df89c Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 17 Sep 2023 20:02:26 -0400 Subject: [PATCH 098/258] Use version catalogs --- app/build.gradle | 69 ++++++------ build.gradle | 66 ----------- build.gradle.kts | 10 ++ common/analytics/build.gradle | 6 +- common/audio/build.gradle | 14 +-- common/bookmark/build.gradle | 12 +- common/data/build.gradle | 14 +-- common/di/build.gradle | 6 +- common/download/build.gradle | 6 +- common/networking/build.gradle | 8 +- common/pages/build.gradle | 6 +- common/reading/build.gradle | 8 +- common/recitation/build.gradle | 8 +- common/toolbar/build.gradle | 8 +- common/ui/core/build.gradle | 12 +- common/upgrade/build.gradle | 6 +- feature/analytics-noop/build.gradle | 6 +- feature/audio/build.gradle | 16 +-- feature/downloadmanager/build.gradle | 24 ++-- feature/qarilist/build.gradle | 22 ++-- feature/recitation/build.gradle | 8 +- gradle/libs.versions.toml | 158 +++++++++++++++++++++++++++ pages/madani/build.gradle | 4 +- settings.gradle | 28 ----- settings.gradle.kts | 44 ++++++++ 25 files changed, 343 insertions(+), 226 deletions(-) delete mode 100644 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/app/build.gradle b/app/build.gradle index 50bf05282a..a3ff5e9e19 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -128,56 +128,55 @@ dependencies { implementation project(path: ':feature:downloadmanager') implementation project(path: ':feature:qarilist') - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android - implementation "com.squareup.retrofit2:retrofit:${retrofitVersion}" - implementation "com.squareup.retrofit2:converter-moshi:${retrofitVersion}" + implementation libs.retrofit + implementation libs.converter.moshi - implementation "androidx.appcompat:appcompat:${androidxAppcompatVersion}" - implementation "androidx.media:media:${androidxMediaVersion}" - implementation "androidx.localbroadcastmanager:localbroadcastmanager:${androidxLocalBroadcastVersion}" - implementation "androidx.preference:preference-ktx:${androidxPreferencesVersion}" - implementation "androidx.recyclerview:recyclerview:${androidxRecyclerViewVersion}" - implementation "com.google.android.material:material:${materialComponentsVersion}" - implementation "androidx.swiperefreshlayout:swiperefreshlayout:${androidxSwipeRefreshVersion}" + implementation libs.androidx.appcompat + implementation libs.androidx.media + implementation libs.androidx.localbroadcastmanager + implementation libs.androidx.preference.ktx + implementation libs.androidx.recyclerview + implementation libs.material + implementation libs.androidx.swiperefreshlayout // okio - implementation "com.squareup.okio:okio:${okioVersion}" + implementation libs.okio // rx - implementation 'io.reactivex.rxjava3:rxjava:3.1.7' - implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' + implementation libs.rxjava + implementation libs.rxandroid // dagger - kapt deps.dagger.apt - kaptTest deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + kaptTest libs.dagger.compiler + implementation libs.dagger.runtime // workmanager - implementation "androidx.work:work-runtime-ktx:${workManagerVersion}" + implementation libs.androidx.work.runtime.ktx - implementation "com.squareup.okio:okio:${okioVersion}" - implementation "com.squareup.okhttp3:okhttp" + implementation libs.okhttp - implementation "com.squareup.moshi:moshi:${moshiVersion}" - kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}") + implementation libs.moshi + kapt(libs.moshi.codegen) - implementation "dev.chrisbanes.insetter:insetter:0.6.1" - implementation 'com.jakewharton.timber:timber:5.0.1' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12' + implementation libs.insetter + implementation libs.timber + debugImplementation libs.leakcanary.android - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testImplementation "com.squareup.okhttp3:mockwebserver" - testImplementation "androidx.test.ext:junit-ktx:${androidxJunitExtVersion}" - testImplementation "org.robolectric:robolectric:${robolectricVersion}" - testImplementation "androidx.test.espresso:espresso-core:${espressoVersion}" - testImplementation "androidx.test.espresso:espresso-intents:${espressoVersion}" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.mockito.core + testImplementation libs.okhttp.mockserver + testImplementation libs.junit.ktx + testImplementation libs.robolectric + testImplementation libs.espresso.core + testImplementation libs.espresso.intents - errorprone 'com.google.errorprone:error_prone_core:2.21.1' + errorprone libs.error.prone.core // Number Picker - implementation 'io.github.ShawnLin013:number-picker:2.4.13' + implementation libs.number.picker } diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 6099a6246a..0000000000 --- a/build.gradle +++ /dev/null @@ -1,66 +0,0 @@ -buildscript { - ext { - kotlinVersion = '1.9.10' - coroutinesVersion = '1.7.3' - - daggerVersion = '2.48' - - androidxMediaVersion = '1.6.0' - androidxAnnotationVersion = '1.7.0' - androidxFragmentVersion = '1.6.1' - androidxPreferencesVersion = '1.2.1' - androidxAppcompatVersion = '1.6.1' - androidxLocalBroadcastVersion = '1.1.0' - androidxSwipeRefreshVersion = '1.1.0' - androidxRecyclerViewVersion = '1.3.1' - supportSqliteVersion = '2.1.0' - workManagerVersion = '2.8.1' - materialComponentsVersion = '1.9.0' - coreKtxVersion = '1.9.0' - - anvilVersion = '2.4.8' - moshiVersion = '1.15.0' - okioVersion = '3.5.0' - retrofitVersion = '2.9.0' - sqldelightVersion = '2.0.0' - - // testing - junitVersion = '4.13.2' - mockitoVersion = '5.5.0' - truthVersion = '1.1.5' - espressoVersion = '3.5.1' - robolectricVersion = '4.10.3' - androidxJunitExtVersion = '1.1.5' - - deps = [ - dagger: [ - apt: "com.google.dagger:dagger-compiler:${daggerVersion}", - runtime: "com.google.dagger:dagger:${daggerVersion}" - ] - ] - } - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.1.1' - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.9" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0" - classpath "com.google.gms:google-services:4.4.0" - classpath "app.cash.sqldelight:gradle-plugin:$sqldelightVersion" - classpath "com.squareup.anvil:gradle-plugin:$anvilVersion" - } -} - -allprojects { - repositories { - mavenCentral() - google() - maven { url "https://androidx.dev/storage/compose-compiler/repository/" } - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000000..04316f50c7 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.crashlytics) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.errorprone) apply false + alias(libs.plugins.google.services) apply false + alias(libs.plugins.sqldelight) apply false + alias(libs.plugins.anvil) apply false +} diff --git a/common/analytics/build.gradle b/common/analytics/build.gradle index 6a204b5f1c..7ea37876c3 100644 --- a/common/analytics/build.gradle +++ b/common/analytics/build.gradle @@ -4,9 +4,9 @@ plugins { } dependencies { - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation // dagger - kapt deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + implementation libs.dagger.runtime } diff --git a/common/audio/build.gradle b/common/audio/build.gradle index e80524aad4..5e62c6573a 100644 --- a/common/audio/build.gradle +++ b/common/audio/build.gradle @@ -12,12 +12,12 @@ dependencies { implementation project(":common:data") implementation project(":common:download") - implementation deps.dagger.runtime - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "com.squareup.okio:okio:${okioVersion}" + implementation libs.dagger.runtime + implementation libs.androidx.annotation + implementation libs.kotlinx.coroutines.core + implementation libs.okio - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" - testImplementation "com.squareup.okio:okio-fakefilesystem:${okioVersion}" + testImplementation libs.junit + testImplementation libs.truth + testImplementation libs.okio.fakefilesystem } diff --git a/common/bookmark/build.gradle b/common/bookmark/build.gradle index d13fb55f33..8ab6cbb494 100644 --- a/common/bookmark/build.gradle +++ b/common/bookmark/build.gradle @@ -22,14 +22,14 @@ dependencies { implementation project(path: ':common:data') // dagger - implementation deps.dagger.runtime + implementation libs.dagger.runtime // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android // sqldelight - implementation "app.cash.sqldelight:android-driver:$sqldelightVersion" - implementation "app.cash.sqldelight:coroutines-extensions-jvm:$sqldelightVersion" - implementation "app.cash.sqldelight:primitive-adapters:$sqldelightVersion" + implementation libs.sqldelight.android.driver + implementation libs.sqldelight.coroutines.extensions + implementation libs.sqldelight.primitive.adapters } diff --git a/common/data/build.gradle b/common/data/build.gradle index a26ef43725..4b676b6067 100644 --- a/common/data/build.gradle +++ b/common/data/build.gradle @@ -4,15 +4,15 @@ plugins { } dependencies { - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android // dagger - kapt deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + implementation libs.dagger.runtime - implementation "com.squareup.moshi:moshi:${moshiVersion}" - kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}") + implementation libs.moshi + kapt(libs.moshi.codegen) } diff --git a/common/di/build.gradle b/common/di/build.gradle index 3588c9c3a9..38eb20360b 100644 --- a/common/di/build.gradle +++ b/common/di/build.gradle @@ -8,7 +8,7 @@ android.namespace 'com.quran.mobile.di' anvil { generateDaggerFactories = true } dependencies { - implementation deps.dagger.runtime - implementation "androidx.fragment:fragment-ktx:${androidxFragmentVersion}" - implementation "androidx.preference:preference-ktx:${androidxPreferencesVersion}" + implementation libs.dagger.runtime + implementation libs.androidx.fragment.ktx + implementation libs.androidx.preference.ktx } diff --git a/common/download/build.gradle b/common/download/build.gradle index 18b30bb347..32b30ccfe9 100644 --- a/common/download/build.gradle +++ b/common/download/build.gradle @@ -11,9 +11,9 @@ dependencies { implementation project(":common:data") // dagger - implementation deps.dagger.runtime + implementation libs.dagger.runtime // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android } diff --git a/common/networking/build.gradle b/common/networking/build.gradle index d7ebf9e836..d747c86e45 100644 --- a/common/networking/build.gradle +++ b/common/networking/build.gradle @@ -4,9 +4,9 @@ plugins { } dependencies { - kapt deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + implementation libs.dagger.runtime - implementation 'dnsjava:dnsjava:2.1.9' - implementation "com.squareup.okhttp3:okhttp-dnsoverhttps" + implementation libs.dnsjava + implementation libs.okhttp.dnsoverhttps } diff --git a/common/pages/build.gradle b/common/pages/build.gradle index 8eddc1ac8d..d3aa5fe3c9 100644 --- a/common/pages/build.gradle +++ b/common/pages/build.gradle @@ -7,8 +7,8 @@ android.namespace 'com.quran.labs.androidquran.common.pages' dependencies { implementation project(path: ':common:data') - implementation "androidx.fragment:fragment-ktx:${androidxFragmentVersion}" + implementation libs.androidx.fragment.ktx - kapt deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + implementation libs.dagger.runtime } diff --git a/common/reading/build.gradle b/common/reading/build.gradle index 3430b1acdb..c2b97becac 100644 --- a/common/reading/build.gradle +++ b/common/reading/build.gradle @@ -8,11 +8,11 @@ anvil { generateDaggerFactories = true } dependencies { implementation project(":common:data") - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android // dagger - implementation deps.dagger.runtime + implementation libs.dagger.runtime } diff --git a/common/recitation/build.gradle b/common/recitation/build.gradle index 3317f86355..091a2d006e 100644 --- a/common/recitation/build.gradle +++ b/common/recitation/build.gradle @@ -10,12 +10,12 @@ anvil { generateDaggerFactories = true } dependencies { implementation project(path: ':common:data') - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation // dagger - implementation deps.dagger.runtime + implementation libs.dagger.runtime // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android } diff --git a/common/toolbar/build.gradle b/common/toolbar/build.gradle index e5419abfd4..da1ab0e1bc 100644 --- a/common/toolbar/build.gradle +++ b/common/toolbar/build.gradle @@ -12,9 +12,9 @@ dependencies { implementation project(path: ':common:reading') implementation project(path: ':common:bookmark') - implementation deps.dagger.runtime + implementation libs.dagger.runtime - implementation "androidx.appcompat:appcompat:${androidxAppcompatVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.androidx.appcompat + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android } diff --git a/common/ui/core/build.gradle b/common/ui/core/build.gradle index f881415c9c..923d4f8dbc 100644 --- a/common/ui/core/build.gradle +++ b/common/ui/core/build.gradle @@ -5,10 +5,10 @@ plugins { android.namespace 'com.quran.mobile.common.ui.core' dependencies { - implementation "androidx.compose.foundation:foundation" - implementation "androidx.compose.material:material" - implementation "androidx.compose.material3:material3" - implementation "androidx.compose.ui:ui" - implementation "androidx.compose.ui:ui-tooling-preview" - debugImplementation "androidx.compose.ui:ui-tooling" + implementation libs.compose.foundation + implementation libs.compose.material + implementation libs.compose.material3 + implementation libs.compose.ui + implementation libs.compose.ui.tooling.preview + debugImplementation libs.compose.ui.tooling } diff --git a/common/upgrade/build.gradle b/common/upgrade/build.gradle index d87bba6bd1..2455dea937 100644 --- a/common/upgrade/build.gradle +++ b/common/upgrade/build.gradle @@ -8,8 +8,8 @@ android.namespace 'com.quran.labs.androidquran.common.upgrade' dependencies { implementation project(":common:data") - kapt deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + implementation libs.dagger.runtime - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation } diff --git a/feature/analytics-noop/build.gradle b/feature/analytics-noop/build.gradle index 7abdbf3420..fdb36ba778 100644 --- a/feature/analytics-noop/build.gradle +++ b/feature/analytics-noop/build.gradle @@ -5,9 +5,9 @@ plugins { dependencies { implementation project(":common:analytics") - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation // dagger - kapt deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + implementation libs.dagger.runtime } diff --git a/feature/audio/build.gradle b/feature/audio/build.gradle index 7fff402805..0ac32c38ea 100644 --- a/feature/audio/build.gradle +++ b/feature/audio/build.gradle @@ -21,16 +21,16 @@ android { dependencies { implementation project(path: ':common:audio') - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android - implementation "com.squareup.okio:okio:${okioVersion}" + implementation libs.okio - implementation "com.squareup.moshi:moshi:${moshiVersion}" - kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}") + implementation libs.moshi + kapt(libs.moshi.codegen) - implementation "com.squareup.retrofit2:retrofit:${retrofitVersion}" + implementation libs.retrofit - testImplementation "junit:junit:${junitVersion}" - testImplementation "com.google.truth:truth:${truthVersion}" + testImplementation libs.junit + testImplementation libs.truth } diff --git a/feature/downloadmanager/build.gradle b/feature/downloadmanager/build.gradle index 07dfb7327d..407d9cf03b 100644 --- a/feature/downloadmanager/build.gradle +++ b/feature/downloadmanager/build.gradle @@ -25,22 +25,22 @@ dependencies { implementation project(path: ':common:search') implementation project(path: ':common:ui:core') - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" - implementation "androidx.activity:activity-compose:1.7.2" + implementation libs.androidx.annotation + implementation libs.androidx.activity.compose // dagger - implementation deps.dagger.runtime + implementation libs.dagger.runtime // compose - implementation "androidx.compose.animation:animation" - implementation "androidx.compose.foundation:foundation" - implementation "androidx.compose.material:material" - implementation "androidx.compose.material3:material3" - implementation "androidx.compose.ui:ui" - implementation "androidx.compose.ui:ui-tooling-preview" - debugImplementation "androidx.compose.ui:ui-tooling" + implementation libs.compose.animation + implementation libs.compose.foundation + implementation libs.compose.material + implementation libs.compose.material3 + implementation libs.compose.ui + implementation libs.compose.ui.tooling.preview + debugImplementation libs.compose.ui.tooling // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android } diff --git a/feature/qarilist/build.gradle b/feature/qarilist/build.gradle index b3b788217a..9ff392439c 100644 --- a/feature/qarilist/build.gradle +++ b/feature/qarilist/build.gradle @@ -21,21 +21,21 @@ dependencies { implementation project(path: ':common:data') implementation project(path: ':common:ui:core') - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation // dagger - implementation deps.dagger.runtime + implementation libs.dagger.runtime // compose - implementation "androidx.compose.animation:animation" - implementation "androidx.compose.foundation:foundation" - implementation "androidx.compose.material:material" - implementation "androidx.compose.material3:material3" - implementation "androidx.compose.ui:ui" - implementation "androidx.compose.ui:ui-tooling-preview" - debugImplementation "androidx.compose.ui:ui-tooling" + implementation libs.compose.animation + implementation libs.compose.foundation + implementation libs.compose.material + implementation libs.compose.material3 + implementation libs.compose.ui + implementation libs.compose.ui.tooling.preview + debugImplementation libs.compose.ui.tooling // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android } diff --git a/feature/recitation/build.gradle b/feature/recitation/build.gradle index 62289a0778..c4e1d943f0 100644 --- a/feature/recitation/build.gradle +++ b/feature/recitation/build.gradle @@ -10,12 +10,12 @@ anvil { generateDaggerFactories = true } dependencies { implementation project(path: ':common:data') implementation project(path: ':common:recitation') - implementation "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation // dagger - implementation deps.dagger.runtime + implementation libs.dagger.runtime // coroutines - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${coroutinesVersion}" + implementation libs.kotlinx.coroutines.core + implementation libs.kotlinx.coroutines.android } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000000..c71e1894e9 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,158 @@ +[versions] +agp = "8.1.1" +kotlin = "1.9.10" + +anvil = "2.4.8" +crashlytics = "2.9.9" +errorprone = "3.1.0" +sqldelight = "2.0.0" +googleServices = "4.4.0" + +androidxAppcompatVersion = "1.6.1" +androidxActivityComposeVersion = "1.7.2" +androidxAnnotationVersion = "1.7.0" +androidxCoreVersion = "1.9.0" +androidxNavigationVersion = "2.5.3" +androidxDynamicAnimationVersion = "1.0.0" +androidxFragmentVersion = "1.6.1" +androidxJunitExtVersion = "1.1.5" +androidxLocalBroadcastVersion = "1.1.0" +androidxMediaVersion = "1.6.0" +androidxPreferencesVersion = "1.2.1" +androidxRecyclerViewVersion = "1.3.1" +androidxSwipeRefreshVersion = "1.1.0" +androidxPagingVersion = "3.1.1" +androidxPagingComposeVersion = "1.0.0-alpha17" +accompanistVersion = "0.24.5-alpha" +coroutinesVersion = "1.7.3" +daggerVersion = "2.48" +balloonVersion = "1.3.9" +dnsjavaVersion = "2.1.9" +error_prone_coreVersion = "2.21.1" +firebaseAnalyticsVersion = "21.2.2" +firebaseCrashlyticsVersion = "18.3.6" +tooltipComposeVersion = "0.2.0" +espressoVersion = "3.5.1" +insetterVersion = "0.6.1" +junitVersion = "4.13.2" +leakcanaryAndroidVersion = "2.12" +materialComponentsVersion = "1.9.0" +mockitoVersion = "5.5.0" +moshiVersion = "1.15.0" +numberPickerVersion = "2.4.13" +okioVersion = "3.5.0" +retrofitVersion = "2.9.0" +robolectricVersion = "4.10.3" +rxandroidVersion = "3.0.2" +rxjavaVersion = "3.1.7" +reorderableComposeVersion = "0.7.4" +timberVersion = "5.0.1" +truthVersion = "1.1.5" +workManagerVersion = "2.8.1" +grpcOkhttpVersion = "1.41.0" +googleCloudSpeechVersion = "2.1.0" + +[libraries] +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } + +# androidx +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompatVersion" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreVersion" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityComposeVersion" } +androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidxAnnotationVersion" } +androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidxFragmentVersion" } +androidx-localbroadcastmanager = { module = "androidx.localbroadcastmanager:localbroadcastmanager", version.ref = "androidxLocalBroadcastVersion" } +androidx-dynamicanimation = { module = "androidx.dynamicanimation:dynamicanimation", version.ref = "androidxDynamicAnimationVersion" } +androidx-media = { module = "androidx.media:media", version.ref = "androidxMediaVersion" } +androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "androidxPreferencesVersion" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidxRecyclerViewVersion" } +androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidxSwipeRefreshVersion" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workManagerVersion" } +androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidxPagingVersion" } +androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidxPagingComposeVersion" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigationVersion" } + +# compose +compose-foundation = { module = "androidx.compose.foundation:foundation" } +compose-animation = { module = "androidx.compose.animation:animation" } +compose-material = { module = "androidx.compose.material:material" } +compose-material3 = { module = "androidx.compose.material3:material3" } +compose-ui = { module = "androidx.compose.ui:ui" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } + +material = { module = "com.google.android.material:material", version.ref = "materialComponentsVersion" } + +# dagger +dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "daggerVersion"} +dagger-runtime = { module = "com.google.dagger:dagger", version.ref = "daggerVersion" } + +# moshi +moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshiVersion" } +moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshiVersion" } + +# okio +okio = { module = "com.squareup.okio:okio", version.ref = "okioVersion" } +okio-fakefilesystem = { module = "com.squareup.okio:okio-fakefilesystem", version.ref = "okioVersion" } + +# okhttp +okhttp = { module = "com.squareup.okhttp3:okhttp" } +okhttp-mockserver = { module = "com.squareup.okhttp3:mockwebserver" } +okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps" } + +# rx +rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroidVersion" } +rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjavaVersion" } + +# retrofit +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofitVersion" } +converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitVersion" } + +# sqldelight +sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } +sqldelight-coroutines-extensions = { module = "app.cash.sqldelight:coroutines-extensions-jvm", version.ref = "sqldelight" } +sqldelight-paging3-extensions = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "sqldelight" } +sqldelight-primitive-adapters = { module = "app.cash.sqldelight:primitive-adapters", version.ref = "sqldelight" } + +# recitation +grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpcOkhttpVersion" } +google-cloud-speech = { module = "com.google.cloud:google-cloud-speech", version.ref = "googleCloudSpeechVersion" } + +# ui libraries +balloon = { module = "com.github.skydoves:balloon", version.ref = "balloonVersion" } +number-picker = { module = "io.github.ShawnLin013:number-picker", version.ref = "numberPickerVersion" } +insetter = { module = "dev.chrisbanes.insetter:insetter", version.ref = "insetterVersion" } +accompanist-flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanistVersion" } +tooltip-compose = { module = "com.github.skgmn:composetooltip", version.ref = "tooltipComposeVersion" } +reorderable-compose = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "reorderableComposeVersion" } + +# utils +leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroidVersion" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timberVersion" } +dnsjava = { module = "dnsjava:dnsjava", version.ref = "dnsjavaVersion" } + +# tools +error_prone_core = { module = "com.google.errorprone:error_prone_core", version.ref = "error_prone_coreVersion" } +firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebaseAnalyticsVersion" } +firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version.ref = "firebaseCrashlyticsVersion" } + +# testing +espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoVersion" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoVersion" } +junit = { module = "junit:junit", version.ref = "junitVersion" } +junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidxJunitExtVersion" } +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoVersion" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectricVersion" } +truth = { module = "com.google.truth:truth", version.ref = "truthVersion" } + + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +android-library = { id = "com.android.library", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } +anvil = { id = "com.squareup.anvil", version.ref = "anvil"} +crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "crashlytics" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone" } diff --git a/pages/madani/build.gradle b/pages/madani/build.gradle index 27a8d12843..fe4ec33409 100644 --- a/pages/madani/build.gradle +++ b/pages/madani/build.gradle @@ -11,6 +11,6 @@ dependencies { implementation project(path: ':common:audio') implementation project(path: ':common:upgrade') - kapt deps.dagger.apt - implementation deps.dagger.runtime + kapt libs.dagger.compiler + implementation libs.dagger.runtime } diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 3de0281592..0000000000 --- a/settings.gradle +++ /dev/null @@ -1,28 +0,0 @@ -includeBuild("build-logic") - -include ':app' -include ':common:analytics' -include ':common:audio' -include ':common:bookmark' -include ':common:data' -include ':common:di' -include ':common:download' -include ':common:networking' -include ':common:pages' -include ':common:reading' -include ':common:recitation' -include ':common:preference' -include ':common:search' -include ':common:toolbar' -include ':common:upgrade' -include ':common:ui:core' -include ':feature:analytics-noop' -include ':feature:audio' -include ':feature:downloadmanager' -include ':feature:qarilist' -include ':feature:recitation' -include ':pages:madani' - -if (new File(rootDir, 'extras/settings-extra.gradle').exists()) { - apply from: new File(rootDir, 'extras/settings-extra.gradle') -} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000000..6fbbf78e96 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,44 @@ +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("https://androidx.dev/storage/compose-compiler/repository/") + } +} + +includeBuild("build-logic") + +include(":app") +include(":common:analytics") +include(":common:audio") +include(":common:bookmark") +include(":common:data") +include(":common:di") +include(":common:download") +include(":common:networking") +include(":common:pages") +include(":common:reading") +include(":common:recitation") +include(":common:preference") +include(":common:search") +include(":common:toolbar") +include(":common:upgrade") +include(":common:ui:core") +include(":feature:analytics-noop") +include(":feature:audio") +include(":feature:downloadmanager") +include(":feature:qarilist") +include(":feature:recitation") +include(":pages:madani") + +if (File(rootDir, "extras/settings-extra.gradle").exists()) { + apply(File(rootDir, "extras/settings-extra.gradle")) +} From ea6b85cd5cfbb6600a9314b108b53a3bdf056a8c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 15:08:36 +0000 Subject: [PATCH 099/258] Update dependency androidx.paging:paging-compose to v1.0.0-alpha20 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c71e1894e9..5d0d7c5f2e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ androidxPreferencesVersion = "1.2.1" androidxRecyclerViewVersion = "1.3.1" androidxSwipeRefreshVersion = "1.1.0" androidxPagingVersion = "3.1.1" -androidxPagingComposeVersion = "1.0.0-alpha17" +androidxPagingComposeVersion = "1.0.0-alpha20" accompanistVersion = "0.24.5-alpha" coroutinesVersion = "1.7.3" daggerVersion = "2.48" From 4beac15d699fb0810da40c483ace97996c4ca945 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:59:13 +0000 Subject: [PATCH 100/258] Update dependency androidx.core:core-ktx to v1.12.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d0d7c5f2e..991005445f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ googleServices = "4.4.0" androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.7.2" androidxAnnotationVersion = "1.7.0" -androidxCoreVersion = "1.9.0" +androidxCoreVersion = "1.12.0" androidxNavigationVersion = "2.5.3" androidxDynamicAnimationVersion = "1.0.0" androidxFragmentVersion = "1.6.1" From 2fabd70d52b92246fa1a70f3243f959230bd8729 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:10:22 +0000 Subject: [PATCH 101/258] Update dependency androidx.navigation:navigation-compose to v2.7.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 991005445f..bed89c3fb0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.7.2" androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" -androidxNavigationVersion = "2.5.3" +androidxNavigationVersion = "2.7.2" androidxDynamicAnimationVersion = "1.0.0" androidxFragmentVersion = "1.6.1" androidxJunitExtVersion = "1.1.5" From 7dc28f6df4cdcb718e2cb49f8415c86f1164d9e3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:10:26 +0000 Subject: [PATCH 102/258] Update dependency androidx.paging:paging-runtime-ktx to v3.2.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 991005445f..19e31adb5b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ androidxMediaVersion = "1.6.0" androidxPreferencesVersion = "1.2.1" androidxRecyclerViewVersion = "1.3.1" androidxSwipeRefreshVersion = "1.1.0" -androidxPagingVersion = "3.1.1" +androidxPagingVersion = "3.2.1" androidxPagingComposeVersion = "1.0.0-alpha20" accompanistVersion = "0.24.5-alpha" coroutinesVersion = "1.7.3" From aad33900caa3f5bcee25d8cf7da5b1c748a3afe2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 18:10:30 +0000 Subject: [PATCH 103/258] Update dependency com.github.skydoves:balloon to v1.6.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 991005445f..5502b2ad70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ androidxPagingComposeVersion = "1.0.0-alpha20" accompanistVersion = "0.24.5-alpha" coroutinesVersion = "1.7.3" daggerVersion = "2.48" -balloonVersion = "1.3.9" +balloonVersion = "1.6.0" dnsjavaVersion = "2.1.9" error_prone_coreVersion = "2.21.1" firebaseAnalyticsVersion = "21.2.2" From 58b4fc1bca5fa8f1931038e03ea443e4a5930bcc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 19:54:13 +0000 Subject: [PATCH 104/258] Update dependency com.google.cloud:google-cloud-speech to v2.6.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 766ec9add1..446f7608ce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ timberVersion = "5.0.1" truthVersion = "1.1.5" workManagerVersion = "2.8.1" grpcOkhttpVersion = "1.41.0" -googleCloudSpeechVersion = "2.1.0" +googleCloudSpeechVersion = "2.6.1" [libraries] kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" } From ec995b2d1a75cc275461e9915aec80453952fc67 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 18 Sep 2023 23:53:39 +0400 Subject: [PATCH 105/258] Minor cleanup for dependency catalog --- app/build.gradle | 2 +- gradle/libs.versions.toml | 57 ++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a3ff5e9e19..7f793078b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,7 +175,7 @@ dependencies { testImplementation libs.espresso.core testImplementation libs.espresso.intents - errorprone libs.error.prone.core + errorprone libs.errorprone.core // Number Picker implementation libs.number.picker diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 766ec9add1..c80f59739b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,11 +3,23 @@ agp = "8.1.1" kotlin = "1.9.10" anvil = "2.4.8" +coroutinesVersion = "1.7.3" crashlytics = "2.9.9" +daggerVersion = "2.48" +dnsjavaVersion = "2.1.9" errorprone = "3.1.0" -sqldelight = "2.0.0" +errorproneCoreVersion = "2.21.1" googleServices = "4.4.0" +leakcanaryAndroidVersion = "2.12" +moshiVersion = "1.15.0" +okioVersion = "3.5.0" +retrofitVersion = "2.9.0" +rxandroidVersion = "3.0.2" +rxjavaVersion = "3.1.7" +sqldelight = "2.0.0" +timberVersion = "5.0.1" +# androidx library versions androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.7.2" androidxAnnotationVersion = "1.7.0" @@ -22,36 +34,33 @@ androidxPreferencesVersion = "1.2.1" androidxRecyclerViewVersion = "1.3.1" androidxSwipeRefreshVersion = "1.1.0" androidxPagingVersion = "3.2.1" -androidxPagingComposeVersion = "1.0.0-alpha20" -accompanistVersion = "0.24.5-alpha" -coroutinesVersion = "1.7.3" -daggerVersion = "2.48" +androidxPagingComposeVersion = "3.2.1" +androidxWorkManagerVersion = "2.8.1" + +# firebase +firebaseAnalyticsVersion = "21.3.0" +firebaseCrashlyticsVersion = "18.4.3" + +# ui libraries +accompanistVersion = "0.32.0" balloonVersion = "1.6.0" -dnsjavaVersion = "2.1.9" -error_prone_coreVersion = "2.21.1" -firebaseAnalyticsVersion = "21.2.2" -firebaseCrashlyticsVersion = "18.3.6" tooltipComposeVersion = "0.2.0" -espressoVersion = "3.5.1" insetterVersion = "0.6.1" -junitVersion = "4.13.2" -leakcanaryAndroidVersion = "2.12" materialComponentsVersion = "1.9.0" -mockitoVersion = "5.5.0" -moshiVersion = "1.15.0" numberPickerVersion = "2.4.13" -okioVersion = "3.5.0" -retrofitVersion = "2.9.0" -robolectricVersion = "4.10.3" -rxandroidVersion = "3.0.2" -rxjavaVersion = "3.1.7" reorderableComposeVersion = "0.7.4" -timberVersion = "5.0.1" -truthVersion = "1.1.5" -workManagerVersion = "2.8.1" + +# recitations grpcOkhttpVersion = "1.41.0" googleCloudSpeechVersion = "2.1.0" +# testing +junitVersion = "4.13.2" +espressoVersion = "3.5.1" +truthVersion = "1.1.5" +mockitoVersion = "5.5.0" +robolectricVersion = "4.10.3" + [libraries] kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } @@ -68,7 +77,7 @@ androidx-media = { module = "androidx.media:media", version.ref = "androidxMedia androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "androidxPreferencesVersion" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidxRecyclerViewVersion" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidxSwipeRefreshVersion" } -androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workManagerVersion" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidxWorkManagerVersion" } androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidxPagingVersion" } androidx-paging-compose = { module = "androidx.paging:paging-compose", version.ref = "androidxPagingComposeVersion" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigationVersion" } @@ -133,7 +142,7 @@ timber = { module = "com.jakewharton.timber:timber", version.ref = "timberVersio dnsjava = { module = "dnsjava:dnsjava", version.ref = "dnsjavaVersion" } # tools -error_prone_core = { module = "com.google.errorprone:error_prone_core", version.ref = "error_prone_coreVersion" } +errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorproneCoreVersion" } firebase-analytics-ktx = { module = "com.google.firebase:firebase-analytics-ktx", version.ref = "firebaseAnalyticsVersion" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version.ref = "firebaseCrashlyticsVersion" } From 30ca879c4f97a7829be26ed004ab26d532287935 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 20:22:12 +0000 Subject: [PATCH 106/258] Update dependency org.burnoutcrew.composereorderable:reorderable to v0.9.6 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c80f59739b..14d8353f33 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" materialComponentsVersion = "1.9.0" numberPickerVersion = "2.4.13" -reorderableComposeVersion = "0.7.4" +reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.41.0" From 368b3f812af9f61996da9459095f19b2a710e734 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Tue, 19 Sep 2023 00:42:22 +0400 Subject: [PATCH 107/258] Add Google Oauth dependency within catalog [skip ci] --- gradle/libs.versions.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 030996f0fe..efc5946a9b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,6 +52,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.41.0" +googleAuthVersion = "1.19.0" googleCloudSpeechVersion = "2.6.1" # testing @@ -127,6 +128,7 @@ sqldelight-primitive-adapters = { module = "app.cash.sqldelight:primitive-adapte # recitation grpc-okhttp = { module = "io.grpc:grpc-okhttp", version.ref = "grpcOkhttpVersion" } google-cloud-speech = { module = "com.google.cloud:google-cloud-speech", version.ref = "googleCloudSpeechVersion" } +google-auth = { module = "com.google.auth:google-auth-library-oauth2-http", version.ref = "googleAuthVersion" } # ui libraries balloon = { module = "com.github.skydoves:balloon", version.ref = "balloonVersion" } From d3540acc82769f504d26c523c366961390667594 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 20:42:52 +0000 Subject: [PATCH 108/258] Update dependency io.grpc:grpc-okhttp to v1.58.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index efc5946a9b..ffbde9ba20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -51,7 +51,7 @@ numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" # recitations -grpcOkhttpVersion = "1.41.0" +grpcOkhttpVersion = "1.58.0" googleAuthVersion = "1.19.0" googleCloudSpeechVersion = "2.6.1" From 0611c6b8fb26e0554fa471566bb7f2c4a64e018f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:02:19 +0000 Subject: [PATCH 109/258] Update dependency com.google.cloud:google-cloud-speech to v4 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ffbde9ba20..0f4f6cdf84 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.58.0" googleAuthVersion = "1.19.0" -googleCloudSpeechVersion = "2.6.1" +googleCloudSpeechVersion = "4.19.0" # testing junitVersion = "4.13.2" From 327667445df47ca23dd85f19bc56a89d43a3c867 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Tue, 19 Sep 2023 01:24:39 +0400 Subject: [PATCH 110/258] Remove lint workaround for Kotlin 1.9.10 --- gradle.properties | 3 --- 1 file changed, 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2c43635e7a..6f9754e719 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,6 +12,3 @@ android.enableResourceOptimizations=true android.uniquePackageNames=true android.enableAppCompileTimeRClass=true android.experimental.enableNewResourceShrinker=true - -# work around https://issuetracker.google.com/issues/185418482 -android.experimental.lint.version=8.2.0-alpha11 From c29625de2dca3d592fca3fa63a0822873f08a418 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:42:21 +0000 Subject: [PATCH 111/258] Update dependency androidx.navigation:navigation-compose to v2.7.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f4f6cdf84..21eb1f968f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.7.2" androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" -androidxNavigationVersion = "2.7.2" +androidxNavigationVersion = "2.7.3" androidxDynamicAnimationVersion = "1.0.0" androidxFragmentVersion = "1.6.1" androidxJunitExtVersion = "1.1.5" From e6e04424be19ef24aafb1f4346ae03c396f539ae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:20:55 +0000 Subject: [PATCH 112/258] Update dependency com.google.errorprone:error_prone_core to v2.22.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 21eb1f968f..bae832615e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ crashlytics = "2.9.9" daggerVersion = "2.48" dnsjavaVersion = "2.1.9" errorprone = "3.1.0" -errorproneCoreVersion = "2.21.1" +errorproneCoreVersion = "2.22.0" googleServices = "4.4.0" leakcanaryAndroidVersion = "2.12" moshiVersion = "1.15.0" From 9d2c2d74559ed7ca2156fdec82f5096f955904ac Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 23 Sep 2023 01:03:32 +0400 Subject: [PATCH 113/258] Add back down arrow next to Qari list Since swapping the Qari list from a spinner to a bottom sheet, people started asking how to change the Qari, not realizing the qari name is clickable. This adds back a dropdown arrow at the end of the qari name. --- .../androidquran/view/AudioStatusBar.java | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index e273b0c368..3e74737d25 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -16,10 +16,12 @@ import android.widget.ProgressBar; import android.widget.Space; import android.widget.TextView; + import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.core.view.ViewCompat; + import com.quran.data.model.audio.Qari; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.common.audio.model.QariItem; @@ -59,6 +61,7 @@ public class AudioStatusBar extends LeftToRightLinearLayout { private boolean haveCriticalError = false; private TextView qariView; + private ImageView dropdownIconView; private TextView progressText; private ProgressBar progressBar; private final RepeatButton repeatButton; @@ -254,24 +257,33 @@ private void addButton() { qariView.setBackgroundResource(itemBackground); qariView.setPadding(buttonPadding, 0, buttonPadding, 0); } - qariView.setText(currentQari.getNameResource()); - // in RTL, because this is currently an LTR LinearLayout, this shows - // the spinner and then the play button, so we can't match parent. this - // is less efficient than the LTR version. this should be fixed by making - // the parent a vanilla LinearLayout and setting the direction. - final LayoutParams params; - if (isRtl || isRecitationEnabled) { - params = new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT); - params.weight = 1; - } else { - params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + if (dropdownIconView == null) { + dropdownIconView = new ImageView(context); + dropdownIconView.setImageResource(R.drawable.ic_action_expand); + dropdownIconView.setBackgroundResource(itemBackground); + dropdownIconView.setOnClickListener(view -> audioBarListener.onShowQariList()); + dropdownIconView.setPadding(buttonPadding, 0, buttonPadding, 0); } + qariView.setText(currentQari.getNameResource()); + + final ViewGroup.LayoutParams dropdownParams = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.MATCH_PARENT + ); + + final LayoutParams params = new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT); + params.weight = 1; if (isRtl) { ViewCompat.setLayoutDirection(qariView, ViewCompat.LAYOUT_DIRECTION_RTL); + addView(dropdownIconView, dropdownParams); } addView(qariView, params); + if (!isRtl) { + addView(dropdownIconView, dropdownParams); + } } private void showPromptForDownloadMode() { From 18d376fbff56583205bb009e8cb6997dc0dfd298 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 23 Sep 2023 23:30:11 +0400 Subject: [PATCH 114/258] Set checkout action to v4 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/post_merge.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e05666f9c7..44320d8b27 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + uses: actions/checkout@v4 - name: Setup JDK uses: actions/setup-java@v3 @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + uses: actions/checkout@v4 - name: Setup JDK uses: actions/setup-java@v3 @@ -83,7 +83,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + uses: actions/checkout@v4 - name: Setup JDK uses: actions/setup-java@v3 @@ -113,7 +113,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + uses: actions/checkout@v4 - name: Setup JDK uses: actions/setup-java@v3 diff --git a/.github/workflows/post_merge.yml b/.github/workflows/post_merge.yml index e383bb8584..2141d30dc8 100644 --- a/.github/workflows/post_merge.yml +++ b/.github/workflows/post_merge.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 + uses: actions/checkout@v4 - name: Setup JDK uses: actions/setup-java@v3 From c650c55909fcd62086333f6d3adde395515fe653 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 19 Jun 2023 00:52:30 +0400 Subject: [PATCH 115/258] Add some more qaris --- common/audio/src/main/res/values-ar/readers.xml | 8 +++++++- common/audio/src/main/res/values-de/readers.xml | 8 +++++++- common/audio/src/main/res/values-es/readers.xml | 8 +++++++- common/audio/src/main/res/values-fa/readers.xml | 8 +++++++- common/audio/src/main/res/values-fr/readers.xml | 8 +++++++- common/audio/src/main/res/values-hu/readers.xml | 8 +++++++- common/audio/src/main/res/values-kk/readers.xml | 8 +++++++- common/audio/src/main/res/values-ku/readers.xml | 8 +++++++- common/audio/src/main/res/values-my/readers.xml | 8 +++++++- common/audio/src/main/res/values-nl/readers.xml | 8 +++++++- common/audio/src/main/res/values-pt/readers.xml | 8 +++++++- common/audio/src/main/res/values-ru/readers.xml | 8 +++++++- common/audio/src/main/res/values-ug/readers.xml | 8 +++++++- common/audio/src/main/res/values-uz/readers.xml | 8 +++++++- common/audio/src/main/res/values-vi/readers.xml | 8 +++++++- common/audio/src/main/res/values/readers.xml | 8 +++++++- 16 files changed, 112 insertions(+), 16 deletions(-) diff --git a/common/audio/src/main/res/values-ar/readers.xml b/common/audio/src/main/res/values-ar/readers.xml index 9fc5785ecc..c403b7c5bb 100644 --- a/common/audio/src/main/res/values-ar/readers.xml +++ b/common/audio/src/main/res/values-ar/readers.xml @@ -43,7 +43,7 @@ مصطفي إسماعيل الحصري تسجيل الإذاعة عبد العزيز بن صالح الزهراني - أيمن سويد (غير متصل) + أيمن سويد (غير متصل) مشاري بن راشد العفاسي هاني الرفاعي محمد أيوب @@ -74,4 +74,10 @@ الزين محمد أحمد محمد رشاد الشريف وديع اليمني + أيمن سويد + محمد الهادي توري + خالد الجليل + نبيل الرفاعي + نورين محمد صديق + توفيق الصايغ diff --git a/common/audio/src/main/res/values-de/readers.xml b/common/audio/src/main/res/values-de/readers.xml index 1bf39561e8..455e3528ab 100644 --- a/common/audio/src/main/res/values-de/readers.xml +++ b/common/audio/src/main/res/values-de/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (gespalten) + Ayman Suwaid (gespalten) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Muhammad Ahmad Muhammad Rashad Al-Scherif Wadee\' Al-Yamani + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-es/readers.xml b/common/audio/src/main/res/values-es/readers.xml index ec27bbe8d6..f1fba04b0b 100644 --- a/common/audio/src/main/res/values-es/readers.xml +++ b/common/audio/src/main/res/values-es/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (pausas) + Ayman Suwaid (pausas) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Muhammad Ahmad Muhammad Rashad Al-Sherif Wadee\' Al-Yamani + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-fa/readers.xml b/common/audio/src/main/res/values-fa/readers.xml index 98ac7a0219..33eab4bdec 100644 --- a/common/audio/src/main/res/values-fa/readers.xml +++ b/common/audio/src/main/res/values-fa/readers.xml @@ -43,7 +43,7 @@ مصطفي إسماعيل الحصري تسجيل الإذاعة عبد العزيز بن صالح الزهراني - أيمن سويد (شکاف زده) + أيمن سويد (شکاف زده) مشاري بن راشد العفاسي هاني الرفاعي محمد أيوب @@ -74,4 +74,10 @@ الزين محمد احمد محمد رشاد الشريف وديع اليمني + أيمن سويد + محمد الهادي توري + خالد الجليل + نبيل الرفاعي + نورين محمد صديق + توفيق الصايغ diff --git a/common/audio/src/main/res/values-fr/readers.xml b/common/audio/src/main/res/values-fr/readers.xml index 918c592af5..6de74ee887 100644 --- a/common/audio/src/main/res/values-fr/readers.xml +++ b/common/audio/src/main/res/values-fr/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (interrompu) + Ayman Suwaid (interrompu) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Mohammed Ahmad Muhammad Rashad Al-Chérif Wadee\' Al-Yamani + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-hu/readers.xml b/common/audio/src/main/res/values-hu/readers.xml index ede06afb4b..40bde1a15a 100644 --- a/common/audio/src/main/res/values-hu/readers.xml +++ b/common/audio/src/main/res/values-hu/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (hézagos) + Ayman Suwaid (hézagos) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Muhammad Ahmad Muhammad Rashad Al-Shereef Wadee\' Al-Yamani + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-kk/readers.xml b/common/audio/src/main/res/values-kk/readers.xml index baa98585a9..dfb94c149e 100644 --- a/common/audio/src/main/res/values-kk/readers.xml +++ b/common/audio/src/main/res/values-kk/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (үзілген) + Ayman Suwaid (үзілген) Мишәри Әфаси Һәни Рифаи Мұхаммед Әйюб @@ -74,4 +74,10 @@ Альзаин Мохаммед Ахмед Мухаммад Рашад Аль-Шериф Вадих әл-Ямани + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-ku/readers.xml b/common/audio/src/main/res/values-ku/readers.xml index cd4db6b40a..04afa33629 100644 --- a/common/audio/src/main/res/values-ku/readers.xml +++ b/common/audio/src/main/res/values-ku/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (بەردەوام نییە) + Ayman Suwaid (بەردەوام نییە) مەشاری ئەلعەفاسی ھانی ڕەفاعی محەمەد ئەیوب @@ -74,4 +74,10 @@ Alzain Mihemed Ehmed Mihemed Reşad Al-Şerîf Wadih El-Yamanî + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-my/readers.xml b/common/audio/src/main/res/values-my/readers.xml index 8176420fed..9c1225e304 100644 --- a/common/audio/src/main/res/values-my/readers.xml +++ b/common/audio/src/main/res/values-my/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (jurang) + Ayman Suwaid (jurang) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Mohammad Ahmad Muhammad Rashad Al-Shereef Wadee\' Al-Yamani + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-nl/readers.xml b/common/audio/src/main/res/values-nl/readers.xml index 9a2f172487..075488ec0c 100644 --- a/common/audio/src/main/res/values-nl/readers.xml +++ b/common/audio/src/main/res/values-nl/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (pauzes) + Ayman Suwaid (pauzes) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Muhammad Ahmad Muhammad Rashad Al-Scherif Wadee\' Al-Yamani + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-pt/readers.xml b/common/audio/src/main/res/values-pt/readers.xml index eeeeef1551..573480be45 100644 --- a/common/audio/src/main/res/values-pt/readers.xml +++ b/common/audio/src/main/res/values-pt/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (intervalo) + Ayman Suwaid (intervalo) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Muhammad Ahmad Muhammad Rashad Al-Shereef Wadee\' Al-Yamani + Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-ru/readers.xml b/common/audio/src/main/res/values-ru/readers.xml index e5c6b7adda..5751ad167c 100644 --- a/common/audio/src/main/res/values-ru/readers.xml +++ b/common/audio/src/main/res/values-ru/readers.xml @@ -43,7 +43,7 @@ Мустафа Исмаил Махмуд Халиль аль-Хусари (радио) Абдульазиз аз-Захрани - Айман Сувейд (интервал) + Айман Сувейд (интервал) Мишари Рашид аль-Афаси Хани ар-Рифаи Мухаммад Аюб @@ -74,4 +74,10 @@ Альзайн Мохаммед Ахмед Мухаммад Рашад Аль-Шериф Вадих Аль-Ямани + Айман Сувейд + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-ug/readers.xml b/common/audio/src/main/res/values-ug/readers.xml index 092ab82df0..bd5131a7ca 100644 --- a/common/audio/src/main/res/values-ug/readers.xml +++ b/common/audio/src/main/res/values-ug/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Ayman Suwaid (gapped) + Ayman Suwaid (gapped) مىشارى بىن راشىد ئافاسى ھانى رىفائى مۇھەممەد ئەييۇب @@ -74,4 +74,10 @@ الزين محمد أحمد محمد رشاد الشريف ۋادىھ ئەل يامانى + أيمن سويد + محمد الهادي توري + خالد الجليل + نبيل الرفاعي + نورين محمد صديق + توفيق الصايغ diff --git a/common/audio/src/main/res/values-uz/readers.xml b/common/audio/src/main/res/values-uz/readers.xml index 400d8f47a0..34b9b4aba8 100644 --- a/common/audio/src/main/res/values-uz/readers.xml +++ b/common/audio/src/main/res/values-uz/readers.xml @@ -43,7 +43,7 @@ Mustafa Ismoil Xusoriy (radio) Abdulaziz Az-Zahroniy - Ayman Suvayd (doimiy emas) + Ayman Suvayd (doimiy emas) Mishariy al-Afasiy Honiy Ar-Rifoiy Muhammad Ayyub @@ -74,4 +74,10 @@ Alzain Muhammad Ahmad Muhammad Rashad Al-Sherif Wadee\' Al-Yamani + Ayman Suvayd + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values-vi/readers.xml b/common/audio/src/main/res/values-vi/readers.xml index 6dba363b34..4a09f54444 100644 --- a/common/audio/src/main/res/values-vi/readers.xml +++ b/common/audio/src/main/res/values-vi/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Dr. Ayman Suwaid (gapped) + Dr. Ayman Suwaid (gapped) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Mohammad Ahmad Muhammad Rashad Al-Shereef Wadee\' Al-Yamani + Dr. Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh diff --git a/common/audio/src/main/res/values/readers.xml b/common/audio/src/main/res/values/readers.xml index d1bf2bb4c7..6caaad2ac6 100644 --- a/common/audio/src/main/res/values/readers.xml +++ b/common/audio/src/main/res/values/readers.xml @@ -43,7 +43,7 @@ Mostafa Ismaeel Husary Broadcast Abdulaziz Az-Zahrani - Dr. Ayman Suwaid (gapped) + Dr. Ayman Suwaid (gapped) Mishary Al-Afasy Hani Ar-Rifai Muhammad Ayyoub @@ -74,4 +74,10 @@ Alzain Mohammad Ahmad Muhammad Rashad Al-Shereef Wadee\' Al-Yamani + Dr. Ayman Suwaid + Hady Toure + Khalid Al-Jalil + Nabil Ar-Rifai + Noreen Siddiq + Tawfeeq as-Sawaigh From 5f0f99627d6a738ccbdf16f3f1de82dcb1e2ac53 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 24 Sep 2023 22:53:29 +0400 Subject: [PATCH 116/258] Bump version to 3.4.0 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7f793078b0..96ce12cbc2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { namespace 'com.quran.labs.androidquran' defaultConfig { - versionCode 3320 - versionName "3.3.2" + versionCode 3400 + versionName "3.4.0" testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" } From 1464ce4fb3d1e104e0a61fedf4a31a9c388ddcb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Sep 2023 17:10:56 +0000 Subject: [PATCH 117/258] Update dependency com.android.tools.build:gradle to v8.1.2 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index cdf93fc89b..fccdcd8c88 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.1.1") + compileOnly("com.android.tools.build:gradle:8.1.2") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") } From 10d0fb17225534743561a43d0dc70fc25212555e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 28 Sep 2023 18:27:55 +0000 Subject: [PATCH 118/258] Update agp to v8.1.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bae832615e..7eb3d4eb7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.1.1" +agp = "8.1.2" kotlin = "1.9.10" anvil = "2.4.8" From f64faa6eea8792558e4d4328676f466ede7839da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:36:05 +0000 Subject: [PATCH 119/258] Update dependency io.reactivex.rxjava3:rxjava to v3.1.8 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7eb3d4eb7a..82222b1364 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ moshiVersion = "1.15.0" okioVersion = "3.5.0" retrofitVersion = "2.9.0" rxandroidVersion = "3.0.2" -rxjavaVersion = "3.1.7" +rxjavaVersion = "3.1.8" sqldelight = "2.0.0" timberVersion = "5.0.1" From 0d0c716a75afcb79461266913e7eefdb3ae98497 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Oct 2023 03:20:35 +0000 Subject: [PATCH 120/258] Update okioVersion to v3.6.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82222b1364..077b0b92e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ errorproneCoreVersion = "2.22.0" googleServices = "4.4.0" leakcanaryAndroidVersion = "2.12" moshiVersion = "1.15.0" -okioVersion = "3.5.0" +okioVersion = "3.6.0" retrofitVersion = "2.9.0" rxandroidVersion = "3.0.2" rxjavaVersion = "3.1.8" From 3b478fe1d2d3caa2ccf7b5a5fe7e91db72c15f22 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 2 Oct 2023 21:58:54 +0400 Subject: [PATCH 121/258] Make the migration dialog not cancellable Fixes #2397 --- .../main/java/com/quran/labs/androidquran/QuranDataActivity.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt index e044b2ebb7..a0ff0b8952 100644 --- a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt @@ -248,6 +248,7 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio if (updateDialog == null) { val migrationDialog = AlertDialog.Builder(this) .setView(R.layout.migration_upgrade) + .setCancelable(false) .create() updateDialog = migrationDialog migrationDialog.show() From cd9d07d1f88d10deceeb0a24a3dee3d885e08f83 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 2 Oct 2023 22:10:50 +0400 Subject: [PATCH 122/258] Make sharing text share without footnotes Fixes #2403. --- .../com/quran/labs/androidquran/util/ShareUtil.kt | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt index 4823b1629e..592dd8326e 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt @@ -5,6 +5,8 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.text.SpannableString +import android.text.SpannableStringBuilder import android.widget.Toast import androidx.annotation.StringRes import com.quran.data.model.QuranText @@ -75,7 +77,16 @@ class ShareUtil @Inject internal constructor(private val quranDisplayData: Quran append(translationNames[i].getTranslatorName()) append(":\n") } - append(text) + + // remove footnotes for now + val spannableStringBuilder = SpannableStringBuilder(text) + translation.footnoteCognizantText( + spannableStringBuilder, + listOf(), + { _ -> SpannableString("") }, + { builder, _, _ -> builder } + ) + append(spannableStringBuilder) } } if (ayahInfo.arabicText == null) { From 993aef5fb5f1adfdd42a435006cb91614f1b037f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 04:36:57 +0000 Subject: [PATCH 123/258] Update dependency com.google.cloud:google-cloud-speech to v4.22.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 077b0b92e5..eb650d111b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.58.0" googleAuthVersion = "1.19.0" -googleCloudSpeechVersion = "4.19.0" +googleCloudSpeechVersion = "4.22.0" # testing junitVersion = "4.13.2" From 48df99e3371b3af836a91ea33aa328896053ff0e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 01:57:38 +0000 Subject: [PATCH 124/258] Update daggerVersion to v2.48.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 077b0b92e5..0ab9742ebe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ kotlin = "1.9.10" anvil = "2.4.8" coroutinesVersion = "1.7.3" crashlytics = "2.9.9" -daggerVersion = "2.48" +daggerVersion = "2.48.1" dnsjavaVersion = "2.1.9" errorprone = "3.1.0" errorproneCoreVersion = "2.22.0" From f06c29e81ac6d96bb1151c66c2abb808ab345643 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:41:22 +0000 Subject: [PATCH 125/258] Update dependency androidx.navigation:navigation-compose to v2.7.4 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 077b0b92e5..55621ad9aa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,7 +24,7 @@ androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.7.2" androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" -androidxNavigationVersion = "2.7.3" +androidxNavigationVersion = "2.7.4" androidxDynamicAnimationVersion = "1.0.0" androidxFragmentVersion = "1.6.1" androidxJunitExtVersion = "1.1.5" From e32af4f1127e742501f54d16fab4f57fe9f9b468 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:41:27 +0000 Subject: [PATCH 126/258] Update dependency androidx.activity:activity-compose to v1.8.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 077b0b92e5..9bcb049089 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ timberVersion = "5.0.1" # androidx library versions androidxAppcompatVersion = "1.6.1" -androidxActivityComposeVersion = "1.7.2" +androidxActivityComposeVersion = "1.8.0" androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" androidxNavigationVersion = "2.7.3" From 610ffded1440543ba021b95e5c8ec87ca1e46541 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 23:25:50 +0000 Subject: [PATCH 127/258] Update dependency gradle to v8.4 --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index c30b486a89..744c64d127 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac72c34e8a..3fa8f862f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 0adc8e1a53..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +202,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ From 8eaa4c10e333bd3262d018ae23afef0bb84918a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 19:08:22 +0000 Subject: [PATCH 128/258] Update dependency com.google.android.material:material to v1.10.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 077b0b92e5..5983a2413c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ accompanistVersion = "0.32.0" balloonVersion = "1.6.0" tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" -materialComponentsVersion = "1.9.0" +materialComponentsVersion = "1.10.0" numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" From c380624f27987ae562c5de028271db444ac46311 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 20:28:43 +0000 Subject: [PATCH 129/258] Update dependency com.google.auth:google-auth-library-oauth2-http to v1.20.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 28176f8ac5..25ba09e9cb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -52,7 +52,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.58.0" -googleAuthVersion = "1.19.0" +googleAuthVersion = "1.20.0" googleCloudSpeechVersion = "4.22.0" # testing From 7fd6b2899700da368224cc6b19041d63b1a0c109 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:56:41 +0000 Subject: [PATCH 130/258] Update dependency org.mockito:mockito-core to v5.6.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a4a21a7a31..c3b59cc635 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,7 +59,7 @@ googleCloudSpeechVersion = "4.22.0" junitVersion = "4.13.2" espressoVersion = "3.5.1" truthVersion = "1.1.5" -mockitoVersion = "5.5.0" +mockitoVersion = "5.6.0" robolectricVersion = "4.10.3" [libraries] From 014bbb149ea25775b86cbb03510406b535f8ddff Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 7 Oct 2023 04:14:21 +0000 Subject: [PATCH 131/258] Update dependency com.github.skydoves:balloon to v1.6.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3b59cc635..767cab5edd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -43,7 +43,7 @@ firebaseCrashlyticsVersion = "18.4.3" # ui libraries accompanistVersion = "0.32.0" -balloonVersion = "1.6.0" +balloonVersion = "1.6.1" tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" materialComponentsVersion = "1.10.0" From 6798f7007852f7f49bf2ef2120f43e9be282dceb Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 7 Oct 2023 12:17:45 +0400 Subject: [PATCH 132/258] Allow setting a default qari id for each page type Currently, all qaris default to qari id 0. This makes sense for all the Hafs masa7if, where sheikh Minshawi has qari id 0. For other qira'at, however, this might not be the best default. This allows us to set it per page type. Fixes #2414. --- .../common/audio/repository/CurrentQariManager.kt | 5 ++++- .../quran/labs/androidquran/common/audio/util/QariUtil.kt | 7 +++++++ .../src/main/java/com/quran/data/source/PageProvider.kt | 1 + .../quran/data/page/provider/madani/MadaniPageProvider.kt | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/CurrentQariManager.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/CurrentQariManager.kt index 87c3debe3b..ebb9eb1eec 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/CurrentQariManager.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/CurrentQariManager.kt @@ -13,12 +13,15 @@ import kotlinx.coroutines.flow.map @Singleton class CurrentQariManager @Inject constructor(appContext: Context, private val qariUtil: QariUtil) { private val prefs = PreferenceManager.getDefaultSharedPreferences(appContext) - private val currentQariFlow = MutableStateFlow(prefs.getInt(PREF_DEFAULT_QARI, 0)) + private val currentQariFlow = + MutableStateFlow(prefs.getInt(PREF_DEFAULT_QARI, qariUtil.getDefaultQariId())) private val qaris by lazy { qariUtil.getQariList() } fun flow(): Flow = currentQariFlow .map { qariId -> qaris.firstOrNull { it.id == qariId } ?: qaris.first() } + fun currentQariId(): Int = currentQariFlow.value + fun setCurrentQari(qariId: Int) { prefs.edit().putInt(PREF_DEFAULT_QARI, qariId).apply() currentQariFlow.value = qariId diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/QariUtil.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/QariUtil.kt index c12522188b..e800f1bc61 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/QariUtil.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/util/QariUtil.kt @@ -18,6 +18,13 @@ class QariUtil @Inject constructor(private val pageProvider: PageProvider) { return pageProvider.getQaris() } + /** + * Get the default qari id when no qari is selected. + */ + fun getDefaultQariId(): Int { + return pageProvider.getDefaultQariId() + } + /** * Get a list of all available qaris as [QariItem]s * diff --git a/common/data/src/main/java/com/quran/data/source/PageProvider.kt b/common/data/src/main/java/com/quran/data/source/PageProvider.kt index 616eb42b18..7484c76ce5 100644 --- a/common/data/src/main/java/com/quran/data/source/PageProvider.kt +++ b/common/data/src/main/java/com/quran/data/source/PageProvider.kt @@ -29,5 +29,6 @@ interface PageProvider { fun getPageContentType(): PageContentType = PageContentType.Image fun getFallbackPageType(): String? = null fun getQaris(): List + fun getDefaultQariId(): Int fun pageType(): String = "" } diff --git a/pages/madani/src/main/java/com/quran/data/page/provider/madani/MadaniPageProvider.kt b/pages/madani/src/main/java/com/quran/data/page/provider/madani/MadaniPageProvider.kt index 78829d9217..a3e538892b 100644 --- a/pages/madani/src/main/java/com/quran/data/page/provider/madani/MadaniPageProvider.kt +++ b/pages/madani/src/main/java/com/quran/data/page/provider/madani/MadaniPageProvider.kt @@ -42,6 +42,8 @@ class MadaniPageProvider : PageProvider { override fun getPreviewDescription() = R.string.madani_description + override fun getDefaultQariId(): Int = 0 + override fun getQaris(): List { return listOf( Qari( From fed1c8ef07f219ec8fb371682c31bdb91a2072d9 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 8 Oct 2023 23:51:33 +0400 Subject: [PATCH 133/258] Add a preference for drawing a pre-line divider Add a setting to allow drawing a line before each line of the Quran text. This is mostly used for Naskh. --- .../main/java/com/quran/labs/androidquran/data/Constants.kt | 1 + .../com/quran/labs/androidquran/util/QuranSettings.java | 6 ++++++ .../java/com/quran/labs/androidquran/util/SettingsImpl.kt | 4 ++++ common/data/src/main/java/com/quran/data/dao/Settings.kt | 1 + common/preference/src/main/res/values/preferences_keys.xml | 1 + 5 files changed, 13 insertions(+) diff --git a/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt b/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt index bf537f7565..2d456e5af9 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt +++ b/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt @@ -99,4 +99,5 @@ object Constants { const val PREF_CURRENT_AUDIO_REVISION = "currentAudioRevision" const val PREF_SURA_TRANSLATED_NAME = "suraTranslatedName" const val PREF_SHOW_SIDELINES = "showSidelines" + const val PREF_SHOW_LINE_DIVIDERS = "showLineDividers" } diff --git a/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java b/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java index c87cb15065..a9f74e07a9 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java +++ b/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java @@ -162,10 +162,16 @@ public String getPageType() { return prefs.getString(Constants.PREF_PAGE_TYPE, null); } + // only available for Naskh, should return false by default for non-Naskh pages public boolean isSidelines() { return prefs.getBoolean(Constants.PREF_SHOW_SIDELINES, false); } + // only available for Naskh, should return false by default for non-Naskh pages + public boolean showLineDividers() { + return prefs.getBoolean(Constants.PREF_SHOW_LINE_DIVIDERS, false); + } + public void setPageType(String pageType) { prefs.edit().putString(Constants.PREF_PAGE_TYPE, pageType).apply(); clearDefaultImagesDirectory(); diff --git a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt index d19b8e116c..0397d6d662 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt @@ -69,5 +69,9 @@ class SettingsImpl @Inject constructor(private val quranSettings: QuranSettings) return quranSettings.isSidelines } + override suspend fun showLineDividers(): Boolean { + return quranSettings.showLineDividers() + } + override fun preferencesFlow(): Flow = preferencesFlow } diff --git a/common/data/src/main/java/com/quran/data/dao/Settings.kt b/common/data/src/main/java/com/quran/data/dao/Settings.kt index eac44c2af5..3011f4dfc4 100644 --- a/common/data/src/main/java/com/quran/data/dao/Settings.kt +++ b/common/data/src/main/java/com/quran/data/dao/Settings.kt @@ -14,6 +14,7 @@ interface Settings { suspend fun shouldShowBookmarks(): Boolean suspend fun pageType(): String suspend fun showSidelines(): Boolean + suspend fun showLineDividers(): Boolean fun preferencesFlow(): Flow } diff --git a/common/preference/src/main/res/values/preferences_keys.xml b/common/preference/src/main/res/values/preferences_keys.xml index 3823f53aa7..5e6ffb689d 100644 --- a/common/preference/src/main/res/values/preferences_keys.xml +++ b/common/preference/src/main/res/values/preferences_keys.xml @@ -2,4 +2,5 @@ readingCategoryKey showSidelines + showLineDividers From 67f9edeba6b6a09146fd4671d3d14e2d7bc411ac Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 9 Oct 2023 01:02:16 +0400 Subject: [PATCH 134/258] Expose methods for settings Naskh page options --- .../quran/labs/androidquran/util/QuranSettings.java | 10 +++++++++- .../com/quran/labs/androidquran/util/SettingsImpl.kt | 10 +++++++++- .../data/src/main/java/com/quran/data/dao/Settings.kt | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java b/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java index a9f74e07a9..eb0d109671 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java +++ b/app/src/main/java/com/quran/labs/androidquran/util/QuranSettings.java @@ -167,11 +167,19 @@ public boolean isSidelines() { return prefs.getBoolean(Constants.PREF_SHOW_SIDELINES, false); } + public void setSidelines(boolean sidelines) { + prefs.edit().putBoolean(Constants.PREF_SHOW_SIDELINES, sidelines).apply(); + } + // only available for Naskh, should return false by default for non-Naskh pages - public boolean showLineDividers() { + public boolean isShowLineDividers() { return prefs.getBoolean(Constants.PREF_SHOW_LINE_DIVIDERS, false); } + public void setShowLineDividers(boolean showLineDividers) { + prefs.edit().putBoolean(Constants.PREF_SHOW_LINE_DIVIDERS, showLineDividers).apply(); + } + public void setPageType(String pageType) { prefs.edit().putString(Constants.PREF_PAGE_TYPE, pageType).apply(); clearDefaultImagesDirectory(); diff --git a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt index 0397d6d662..00802a6ee3 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt @@ -69,8 +69,16 @@ class SettingsImpl @Inject constructor(private val quranSettings: QuranSettings) return quranSettings.isSidelines } + override suspend fun setShowSidelines(show: Boolean) { + quranSettings.isSidelines = show + } + override suspend fun showLineDividers(): Boolean { - return quranSettings.showLineDividers() + return quranSettings.isShowLineDividers + } + + override suspend fun setShouldShowLineDividers(show: Boolean) { + quranSettings.isShowLineDividers = show } override fun preferencesFlow(): Flow = preferencesFlow diff --git a/common/data/src/main/java/com/quran/data/dao/Settings.kt b/common/data/src/main/java/com/quran/data/dao/Settings.kt index 3011f4dfc4..0e4ad9d85d 100644 --- a/common/data/src/main/java/com/quran/data/dao/Settings.kt +++ b/common/data/src/main/java/com/quran/data/dao/Settings.kt @@ -14,7 +14,9 @@ interface Settings { suspend fun shouldShowBookmarks(): Boolean suspend fun pageType(): String suspend fun showSidelines(): Boolean + suspend fun setShowSidelines(show: Boolean) suspend fun showLineDividers(): Boolean + suspend fun setShouldShowLineDividers(show: Boolean) fun preferencesFlow(): Flow } From 15c48cdbce48c5d8eb02deb2352f80f8483039cc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 22:24:00 +0000 Subject: [PATCH 135/258] Update dependency com.google.cloud:google-cloud-speech to v4.23.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 767cab5edd..34ab83a874 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.58.0" googleAuthVersion = "1.20.0" -googleCloudSpeechVersion = "4.22.0" +googleCloudSpeechVersion = "4.23.0" # testing junitVersion = "4.13.2" From c495aa9a2cc540175d0a9c7328ed93628c932f9e Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 13 Oct 2023 22:59:40 +0400 Subject: [PATCH 136/258] Improve the collapsed footnote number color --- .../quran/labs/androidquran/ui/translation/TranslationAdapter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt index b404b50cdd..0b2e4f0d79 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt @@ -432,6 +432,7 @@ internal class TranslationAdapter( spannable.setSpan(SuperscriptSpan(), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(RelativeSizeSpan(0.7f), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(ExpandFootnoteSpan(number, ::expandFootnote), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spannable.setSpan(ForegroundColorSpan(inlineAyahColor), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) return spannable } From b69820ece07d4c217cee791534cfc4deab7b0e53 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 15 Oct 2023 00:48:01 +0400 Subject: [PATCH 137/258] Bump version to 3.4.1 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 96ce12cbc2..48605b70ab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { namespace 'com.quran.labs.androidquran' defaultConfig { - versionCode 3400 - versionName "3.4.0" + versionCode 3410 + versionName "3.4.1" testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" } From 7dab4936d5911db80b2b00a1953e2f0d0bea306a Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 15 Oct 2023 17:30:53 +0400 Subject: [PATCH 138/258] Use ksp for Moshi and avoid kapt for Dagger We only need kapt for Dagger for the app module (and, as of today, we can't yet use ksp Dagger here due to the interop with Anvil). This at least limits kapt usage only to the app module. --- app/build.gradle | 9 +++++---- build.gradle.kts | 1 + common/analytics/build.gradle | 5 +++-- common/data/build.gradle | 8 +++++--- common/networking/build.gradle | 5 +++-- common/pages/build.gradle | 5 +++-- common/search/build.gradle | 1 - common/upgrade/build.gradle | 5 +++-- feature/analytics-noop/build.gradle | 5 +++-- feature/audio/build.gradle | 4 ++-- gradle/libs.versions.toml | 2 ++ pages/madani/build.gradle | 5 +++-- 12 files changed, 33 insertions(+), 22 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 48605b70ab..6961ea4929 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,9 +1,10 @@ plugins { - id 'quran.android.application' + id "quran.android.application" id 'org.jetbrains.kotlin.kapt' id 'org.jetbrains.kotlin.plugin.parcelize' - id 'net.ltgt.errorprone' - id 'com.squareup.anvil' + alias libs.plugins.ksp + alias libs.plugins.errorprone + alias libs.plugins.anvil } if (getGradle().getStartParameter().getTaskRequests().toString().contains("Release") && @@ -160,7 +161,7 @@ dependencies { implementation libs.okhttp implementation libs.moshi - kapt(libs.moshi.codegen) + ksp(libs.moshi.codegen) implementation libs.insetter implementation libs.timber diff --git a/build.gradle.kts b/build.gradle.kts index 04316f50c7..7f6bee1a6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.crashlytics) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.ksp) apply false alias(libs.plugins.errorprone) apply false alias(libs.plugins.google.services) apply false alias(libs.plugins.sqldelight) apply false diff --git a/common/analytics/build.gradle b/common/analytics/build.gradle index 7ea37876c3..6556044e0f 100644 --- a/common/analytics/build.gradle +++ b/common/analytics/build.gradle @@ -1,12 +1,13 @@ plugins { id 'quran.android.library' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil } +anvil { generateDaggerFactories = true } + dependencies { implementation libs.androidx.annotation // dagger - kapt libs.dagger.compiler implementation libs.dagger.runtime } diff --git a/common/data/build.gradle b/common/data/build.gradle index 4b676b6067..558cc40e43 100644 --- a/common/data/build.gradle +++ b/common/data/build.gradle @@ -1,8 +1,11 @@ plugins { id 'quran.android.library' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil + alias libs.plugins.ksp } +anvil { generateDaggerFactories = true } + dependencies { implementation libs.androidx.annotation @@ -10,9 +13,8 @@ dependencies { implementation libs.kotlinx.coroutines.android // dagger - kapt libs.dagger.compiler implementation libs.dagger.runtime implementation libs.moshi - kapt(libs.moshi.codegen) + ksp(libs.moshi.codegen) } diff --git a/common/networking/build.gradle b/common/networking/build.gradle index d747c86e45..ac636df6bf 100644 --- a/common/networking/build.gradle +++ b/common/networking/build.gradle @@ -1,10 +1,11 @@ plugins { id 'quran.android.library' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil } +anvil { generateDaggerFactories = true } + dependencies { - kapt libs.dagger.compiler implementation libs.dagger.runtime implementation libs.dnsjava diff --git a/common/pages/build.gradle b/common/pages/build.gradle index d3aa5fe3c9..b7578f4cf2 100644 --- a/common/pages/build.gradle +++ b/common/pages/build.gradle @@ -1,14 +1,15 @@ plugins { id 'quran.android.library.android' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil } +anvil { generateDaggerFactories = true } + android.namespace 'com.quran.labs.androidquran.common.pages' dependencies { implementation project(path: ':common:data') implementation libs.androidx.fragment.ktx - kapt libs.dagger.compiler implementation libs.dagger.runtime } diff --git a/common/search/build.gradle b/common/search/build.gradle index c548121845..f1552ac4e1 100644 --- a/common/search/build.gradle +++ b/common/search/build.gradle @@ -1,6 +1,5 @@ plugins { id 'quran.android.library.android' - id 'org.jetbrains.kotlin.kapt' } android.namespace 'com.quran.labs.androidquran.common.search' diff --git a/common/upgrade/build.gradle b/common/upgrade/build.gradle index 2455dea937..50a495d32b 100644 --- a/common/upgrade/build.gradle +++ b/common/upgrade/build.gradle @@ -1,14 +1,15 @@ plugins { id 'quran.android.library.android' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil } +anvil { generateDaggerFactories = true } + android.namespace 'com.quran.labs.androidquran.common.upgrade' dependencies { implementation project(":common:data") - kapt libs.dagger.compiler implementation libs.dagger.runtime implementation libs.androidx.annotation diff --git a/feature/analytics-noop/build.gradle b/feature/analytics-noop/build.gradle index fdb36ba778..50a830ef1e 100644 --- a/feature/analytics-noop/build.gradle +++ b/feature/analytics-noop/build.gradle @@ -1,13 +1,14 @@ plugins { id 'quran.android.library' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil } +anvil { generateDaggerFactories = true } + dependencies { implementation project(":common:analytics") implementation libs.androidx.annotation // dagger - kapt libs.dagger.compiler implementation libs.dagger.runtime } diff --git a/feature/audio/build.gradle b/feature/audio/build.gradle index 0ac32c38ea..383851a364 100644 --- a/feature/audio/build.gradle +++ b/feature/audio/build.gradle @@ -1,6 +1,6 @@ plugins { id 'quran.android.library.android' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.ksp } android { @@ -27,7 +27,7 @@ dependencies { implementation libs.okio implementation libs.moshi - kapt(libs.moshi.codegen) + ksp(libs.moshi.codegen) implementation libs.retrofit diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 34ab83a874..af88f111c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] agp = "8.1.2" kotlin = "1.9.10" +ksp = "1.9.10-1.0.13" anvil = "2.4.8" coroutinesVersion = "1.7.3" @@ -162,6 +163,7 @@ truth = { module = "com.google.truth:truth", version.ref = "truthVersion" } android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } anvil = { id = "com.squareup.anvil", version.ref = "anvil"} crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "crashlytics" } diff --git a/pages/madani/build.gradle b/pages/madani/build.gradle index fe4ec33409..f651cbd194 100644 --- a/pages/madani/build.gradle +++ b/pages/madani/build.gradle @@ -1,8 +1,10 @@ plugins { id 'quran.android.library.android' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil } +anvil { generateDaggerFactories = true } + android.namespace 'com.quran.labs.androidquran.pages.madani' dependencies { @@ -11,6 +13,5 @@ dependencies { implementation project(path: ':common:audio') implementation project(path: ':common:upgrade') - kapt libs.dagger.compiler implementation libs.dagger.runtime } From 72dd7ee05bc4d1b11f169b2c38fc5c1ed51d3f84 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 15 Oct 2023 17:52:50 +0400 Subject: [PATCH 139/258] Use Dagger factories instead of builders --- .../labs/androidquran/QuranApplication.kt | 6 ++---- .../data/AyahInfoDatabaseProvider.kt | 3 ++- .../database/TranslationsDBAdapter.kt | 11 ++++------ .../database/TranslationsDBHelper.kt | 3 ++- .../activity/PagerActivityComponent.kt | 20 ++++++++++++------- .../activity/QuranActivityComponent.kt | 6 +++--- .../application/ApplicationComponent.kt | 13 ++++++++++-- .../component/fragment/QuranPageComponent.kt | 11 +++++----- .../di/module/activity/PagerActivityModule.kt | 18 +++++++---------- .../module/application/ApplicationModule.kt | 19 +++++++----------- .../di/module/fragment/QuranPageModule.kt | 13 ------------ .../bookmark/BookmarkImportExportModel.java | 3 ++- .../translation/ArabicDatabaseUtils.java | 10 ++++++---- .../model/translation/TranslationModel.kt | 5 +++-- .../presenter/QuranImportPresenter.java | 8 +++++--- .../presenter/bookmark/BookmarkPresenter.java | 4 ++-- .../presenter/data/QuranDataPresenter.kt | 19 +++++++++--------- .../TranslationManagerPresenter.java | 3 ++- .../service/util/DownloadStarter.kt | 13 ++++++------ .../labs/androidquran/ui/PagerActivity.java | 12 ++++------- .../labs/androidquran/ui/QuranActivity.kt | 4 ++-- .../ui/fragment/QuranPageFragment.java | 7 +++---- .../ui/fragment/TabletFragment.java | 15 ++++++++------ .../ui/fragment/TranslationFragment.java | 7 +++---- .../ui/helpers/QuranPageLoader.kt | 5 +++-- .../labs/androidquran/util/QuranFileUtils.kt | 3 ++- .../androidquran/util/QuranScreenInfo.java | 6 ++++-- .../widget/BookmarksWidgetUpdater.kt | 11 +++++----- .../labs/androidquran/base/TestApplication.kt | 6 ++---- .../di/TestApplicationComponent.kt | 11 +++++++++- .../di/quran/TestQuranActivityComponent.kt | 6 +++--- common/audio/build.gradle | 1 + .../audio/repository/CurrentQariManager.kt | 7 ++++--- common/bookmark/build.gradle | 1 + .../mobile/bookmark/di/BookmarkDataModule.kt | 3 ++- .../mobile/di/qualifier/ActivityContext.kt | 7 +++++++ .../mobile/di/qualifier/ApplicationContext.kt | 7 +++++++ .../downloadmanager/AudioManagerActivity.kt | 2 +- .../SheikhAudioDownloadsActivity.kt | 2 +- .../di/DownloadManagerComponent.kt | 6 +++--- .../di/DownloadManagerComponentInterface.kt | 2 +- 41 files changed, 172 insertions(+), 147 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/di/module/fragment/QuranPageModule.kt create mode 100644 common/di/src/main/java/com/quran/mobile/di/qualifier/ActivityContext.kt create mode 100644 common/di/src/main/java/com/quran/mobile/di/qualifier/ApplicationContext.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/QuranApplication.kt b/app/src/main/java/com/quran/labs/androidquran/QuranApplication.kt index 4fea84f191..c3fb985be0 100644 --- a/app/src/main/java/com/quran/labs/androidquran/QuranApplication.kt +++ b/app/src/main/java/com/quran/labs/androidquran/QuranApplication.kt @@ -8,7 +8,6 @@ import androidx.work.WorkManager import com.quran.labs.androidquran.core.worker.QuranWorkerFactory import com.quran.labs.androidquran.di.component.application.ApplicationComponent import com.quran.labs.androidquran.di.component.application.DaggerApplicationComponent -import com.quran.labs.androidquran.di.module.application.ApplicationModule import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.RecordingLogTree import com.quran.labs.androidquran.widget.BookmarksWidgetSubscriber @@ -42,9 +41,8 @@ open class QuranApplication : Application(), QuranApplicationComponentProvider { } open fun initializeInjector(): ApplicationComponent { - return DaggerApplicationComponent.builder() - .applicationModule(ApplicationModule(this)) - .build() + return DaggerApplicationComponent.factory() + .generate(this) } open fun initializeWorkManager() { diff --git a/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseProvider.kt b/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseProvider.kt index 3b269de87f..30aa919a83 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseProvider.kt +++ b/app/src/main/java/com/quran/labs/androidquran/data/AyahInfoDatabaseProvider.kt @@ -3,11 +3,12 @@ package com.quran.labs.androidquran.data import android.content.Context import com.quran.data.di.ActivityScope import com.quran.labs.androidquran.util.QuranFileUtils +import com.quran.mobile.di.qualifier.ApplicationContext import javax.inject.Inject @ActivityScope class AyahInfoDatabaseProvider @Inject constructor( - private val context: Context, + @ApplicationContext private val context: Context, private val widthParameter: String, private val quranFileUtils: QuranFileUtils ) { diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt index 38924fbd5f..7f02559cda 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt @@ -5,23 +5,20 @@ import android.content.Context import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.util.SparseArray - +import androidx.annotation.WorkerThread import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.dao.translation.TranslationItem import com.quran.labs.androidquran.database.TranslationsDBHelper.TranslationsTable import com.quran.labs.androidquran.util.QuranFileUtils - -import java.util.ArrayList -import java.util.Collections +import com.quran.mobile.di.qualifier.ApplicationContext import timber.log.Timber - -import androidx.annotation.WorkerThread +import java.util.Collections import javax.inject.Inject import javax.inject.Singleton @Singleton class TranslationsDBAdapter @Inject constructor( - private val context: Context, + @ApplicationContext private val context: Context, adapter: TranslationsDBHelper, private val quranFileUtils: QuranFileUtils ) { diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt index 9f6fa816df..89dc574b4d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt @@ -3,11 +3,12 @@ package com.quran.labs.androidquran.database import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper +import com.quran.mobile.di.qualifier.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton -class TranslationsDBHelper @Inject constructor(context: Context) : +class TranslationsDBHelper @Inject constructor(@ApplicationContext context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { companion object { diff --git a/app/src/main/java/com/quran/labs/androidquran/di/component/activity/PagerActivityComponent.kt b/app/src/main/java/com/quran/labs/androidquran/di/component/activity/PagerActivityComponent.kt index 819648b638..ae983f4bab 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/component/activity/PagerActivityComponent.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/component/activity/PagerActivityComponent.kt @@ -1,24 +1,28 @@ package com.quran.labs.androidquran.di.component.activity -import com.quran.data.di.QuranReadingScope +import android.content.Context import com.quran.data.di.ActivityScope +import com.quran.data.di.QuranReadingScope import com.quran.labs.androidquran.di.component.fragment.QuranPageComponent import com.quran.labs.androidquran.di.module.activity.PagerActivityModule import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.ui.fragment.AyahPlaybackFragment import com.quran.labs.androidquran.ui.fragment.AyahTranslationFragment import com.quran.labs.androidquran.ui.fragment.TagBookmarkFragment -import com.quran.page.common.toolbar.AyahToolBar +import com.quran.labs.androidquran.ui.helpers.AyahSelectedListener import com.quran.mobile.di.QuranReadingActivityComponent +import com.quran.mobile.di.qualifier.ActivityContext import com.quran.mobile.feature.qarilist.QariListWrapper +import com.quran.page.common.toolbar.AyahToolBar import com.squareup.anvil.annotations.MergeSubcomponent +import dagger.BindsInstance import dagger.Subcomponent @ActivityScope @MergeSubcomponent(QuranReadingScope::class, modules = [PagerActivityModule::class]) interface PagerActivityComponent : QuranReadingActivityComponent { // subcomponents - fun quranPageComponentBuilder(): QuranPageComponent.Builder + fun quranPageComponentFactory(): QuranPageComponent.Factory fun inject(pagerActivity: PagerActivity) fun inject(ayahToolBar: AyahToolBar) @@ -29,9 +33,11 @@ interface PagerActivityComponent : QuranReadingActivityComponent { fun inject(qariListWrapper: QariListWrapper) - @Subcomponent.Builder - interface Builder { - fun withPagerActivityModule(pagerModule: PagerActivityModule): Builder - fun build(): PagerActivityComponent + @Subcomponent.Factory + interface Factory { + fun generate( + @BindsInstance @ActivityContext context: Context, + @BindsInstance ayahSelectedListener: AyahSelectedListener + ): PagerActivityComponent } } diff --git a/app/src/main/java/com/quran/labs/androidquran/di/component/activity/QuranActivityComponent.kt b/app/src/main/java/com/quran/labs/androidquran/di/component/activity/QuranActivityComponent.kt index f15c176504..d78ed394c7 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/component/activity/QuranActivityComponent.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/component/activity/QuranActivityComponent.kt @@ -8,8 +8,8 @@ import dagger.Subcomponent interface QuranActivityComponent { fun inject(quranActivity: QuranActivity) - @Subcomponent.Builder - interface Builder { - fun build(): QuranActivityComponent + @Subcomponent.Factory + interface Factory { + fun generate(): QuranActivityComponent } } diff --git a/app/src/main/java/com/quran/labs/androidquran/di/component/application/ApplicationComponent.kt b/app/src/main/java/com/quran/labs/androidquran/di/component/application/ApplicationComponent.kt index 24726377a5..ca4c740045 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/component/application/ApplicationComponent.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/component/application/ApplicationComponent.kt @@ -1,5 +1,6 @@ package com.quran.labs.androidquran.di.component.application +import android.content.Context import com.quran.analytics.provider.AnalyticsModule import com.quran.common.networking.NetworkModule import com.quran.data.di.AppScope @@ -33,7 +34,10 @@ import com.quran.labs.androidquran.widget.BookmarksWidget import com.quran.labs.androidquran.widget.BookmarksWidgetListProvider import com.quran.labs.androidquran.widget.ShowJumpFragmentActivity import com.quran.mobile.di.QuranApplicationComponent +import com.quran.mobile.di.qualifier.ApplicationContext import com.squareup.anvil.annotations.MergeComponent +import dagger.BindsInstance +import dagger.Component import javax.inject.Singleton @Singleton @@ -52,8 +56,8 @@ import javax.inject.Singleton ) interface ApplicationComponent: QuranApplicationComponent { // subcomponents - fun pagerActivityComponentBuilder(): PagerActivityComponent.Builder - fun quranActivityComponentBuilder(): QuranActivityComponent.Builder + fun pagerActivityComponentFactory(): PagerActivityComponent.Factory + fun quranActivityComponentFactory(): QuranActivityComponent.Factory // application fun inject(quranApplication: QuranApplication) @@ -89,4 +93,9 @@ interface ApplicationComponent: QuranApplicationComponent { // widgets fun inject(bookmarksWidgetListProvider: BookmarksWidgetListProvider) fun inject(bookmarksWidget: BookmarksWidget) + + @Component.Factory + interface Factory { + fun generate(@BindsInstance @ApplicationContext appContext: Context): ApplicationComponent + } } diff --git a/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt b/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt index 3332566703..e866e5c97a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/component/fragment/QuranPageComponent.kt @@ -2,24 +2,23 @@ package com.quran.labs.androidquran.di.component.fragment import com.quran.data.di.QuranPageScope import com.quran.data.di.QuranReadingPageScope -import com.quran.labs.androidquran.di.module.fragment.QuranPageModule import com.quran.labs.androidquran.ui.fragment.QuranPageFragment import com.quran.labs.androidquran.ui.fragment.TabletFragment import com.quran.labs.androidquran.ui.fragment.TranslationFragment import com.quran.mobile.di.QuranReadingPageComponent import com.squareup.anvil.annotations.MergeSubcomponent +import dagger.BindsInstance import dagger.Subcomponent @QuranPageScope -@MergeSubcomponent(QuranReadingPageScope::class, modules = [QuranPageModule::class]) +@MergeSubcomponent(QuranReadingPageScope::class) interface QuranPageComponent: QuranReadingPageComponent { fun inject(quranPageFragment: QuranPageFragment) fun inject(tabletFragment: TabletFragment) fun inject(translationFragment: TranslationFragment) - @Subcomponent.Builder - interface Builder { - fun withQuranPageModule(quranPageModule: QuranPageModule): Builder - fun build(): QuranPageComponent + @Subcomponent.Factory + interface Factory { + fun generate(@BindsInstance pages: IntArray): QuranPageComponent } } diff --git a/app/src/main/java/com/quran/labs/androidquran/di/module/activity/PagerActivityModule.kt b/app/src/main/java/com/quran/labs/androidquran/di/module/activity/PagerActivityModule.kt index a3bc1ec752..8f351532ca 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/module/activity/PagerActivityModule.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/module/activity/PagerActivityModule.kt @@ -1,40 +1,36 @@ package com.quran.labs.androidquran.di.module.activity +import android.content.Context import com.quran.data.core.QuranInfo import com.quran.data.core.QuranPageInfo import com.quran.data.di.ActivityScope import com.quran.labs.androidquran.data.QuranDisplayData -import com.quran.labs.androidquran.ui.PagerActivity -import com.quran.labs.androidquran.ui.helpers.AyahSelectedListener import com.quran.labs.androidquran.util.QuranPageInfoImpl import com.quran.labs.androidquran.util.QuranScreenInfo import com.quran.labs.androidquran.util.QuranUtils import com.quran.labs.androidquran.util.TranslationUtil import com.quran.mobile.di.AyahActionFragmentProvider +import com.quran.mobile.di.qualifier.ActivityContext import dagger.Module import dagger.Provides import dagger.multibindings.ElementsIntoSet @Module -class PagerActivityModule(private val pagerActivity: PagerActivity) { - - @Provides - fun provideAyahSelectedListener(): AyahSelectedListener { - return pagerActivity - } +object PagerActivityModule { @Provides fun provideQuranPageInfo( + @ActivityContext context: Context, quranInfo: QuranInfo, quranDisplayData: QuranDisplayData ): QuranPageInfo { - return QuranPageInfoImpl(pagerActivity, quranInfo, quranDisplayData) + return QuranPageInfoImpl(context, quranInfo, quranDisplayData) } @Provides @ActivityScope - fun provideImageWidth(screenInfo: QuranScreenInfo): String { - return if (QuranUtils.isDualPages(pagerActivity, screenInfo)) { + fun provideImageWidth(@ActivityContext context: Context, screenInfo: QuranScreenInfo): String { + return if (QuranUtils.isDualPages(context, screenInfo)) { screenInfo.tabletWidthParam } else { screenInfo.widthParam diff --git a/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt b/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt index 97f5c0b785..03feb4eaed 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt @@ -1,6 +1,5 @@ package com.quran.labs.androidquran.di.module.application -import android.app.Application import android.content.Context import android.graphics.Point import android.view.Display @@ -15,6 +14,7 @@ import com.quran.labs.androidquran.data.QuranFileConstants import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.SettingsImpl +import com.quran.mobile.di.qualifier.ApplicationContext import com.quran.mobile.di.ExtraPreferencesProvider import com.quran.mobile.di.ExtraScreenProvider import dagger.Module @@ -28,15 +28,10 @@ import javax.inject.Named import javax.inject.Singleton @Module -class ApplicationModule(private val application: Application) { +object ApplicationModule { @Provides - fun provideApplicationContext(): Context { - return application - } - - @Provides - fun provideDisplay(appContext: Context): Display { + fun provideDisplay(@ApplicationContext appContext: Context): Display { val w = appContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager return w.defaultDisplay } @@ -58,8 +53,8 @@ class ApplicationModule(private val application: Application) { @Provides @Singleton - fun provideQuranSettings(): QuranSettings { - return QuranSettings.getInstance(application) + fun provideQuranSettings(@ApplicationContext appContext: Context): QuranSettings { + return QuranSettings.getInstance(appContext) } @Provides @@ -94,8 +89,8 @@ class ApplicationModule(private val application: Application) { } @Provides - fun provideCacheDirectory(): File { - return application.cacheDir + fun provideCacheDirectory(@ApplicationContext appContext: Context): File { + return appContext.cacheDir } @Provides diff --git a/app/src/main/java/com/quran/labs/androidquran/di/module/fragment/QuranPageModule.kt b/app/src/main/java/com/quran/labs/androidquran/di/module/fragment/QuranPageModule.kt deleted file mode 100644 index 5a380bda66..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/di/module/fragment/QuranPageModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.quran.labs.androidquran.di.module.fragment - -import dagger.Module -import dagger.Provides - -@Module -class QuranPageModule(private vararg val pages: Int) { - - @Provides - fun providePages(): IntArray { - return pages - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/model/bookmark/BookmarkImportExportModel.java b/app/src/main/java/com/quran/labs/androidquran/model/bookmark/BookmarkImportExportModel.java index c1f491e4e7..9c898b5f2f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/model/bookmark/BookmarkImportExportModel.java +++ b/app/src/main/java/com/quran/labs/androidquran/model/bookmark/BookmarkImportExportModel.java @@ -9,6 +9,7 @@ import com.quran.data.model.bookmark.BookmarkData; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.database.BookmarksDBAdapter; +import com.quran.mobile.di.qualifier.ApplicationContext; import java.io.File; import java.io.IOException; @@ -30,7 +31,7 @@ public class BookmarkImportExportModel { private final BookmarkModel bookmarkModel; @Inject - BookmarkImportExportModel(Context appContext, + BookmarkImportExportModel(@ApplicationContext Context appContext, BookmarkJsonModel model, BookmarkModel bookmarkModel) { this.appContext = appContext; this.jsonModel = model; diff --git a/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java b/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java index 54937c461a..dd9a408f97 100644 --- a/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java +++ b/app/src/main/java/com/quran/labs/androidquran/model/translation/ArabicDatabaseUtils.java @@ -3,15 +3,19 @@ import android.content.Context; import android.database.Cursor; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + import com.quran.data.core.QuranInfo; import com.quran.data.model.QuranText; +import com.quran.data.model.SuraAyah; import com.quran.data.model.bookmark.Bookmark; import com.quran.labs.androidquran.data.QuranDataProvider; import com.quran.labs.androidquran.data.QuranFileConstants; -import com.quran.data.model.SuraAyah; import com.quran.labs.androidquran.database.DatabaseHandler; import com.quran.labs.androidquran.database.DatabaseUtils; import com.quran.labs.androidquran.util.QuranFileUtils; +import com.quran.mobile.di.qualifier.ApplicationContext; import java.util.ArrayList; import java.util.HashMap; @@ -21,8 +25,6 @@ import javax.inject.Inject; import javax.inject.Singleton; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -39,7 +41,7 @@ public class ArabicDatabaseUtils { private DatabaseHandler arabicDatabaseHandler; @Inject - ArabicDatabaseUtils(Context context, QuranInfo quranInfo, QuranFileUtils quranFileUtils) { + ArabicDatabaseUtils(@ApplicationContext Context context, QuranInfo quranInfo, QuranFileUtils quranFileUtils) { this.appContext = context; this.quranInfo = quranInfo; arabicDatabaseHandler = getArabicDatabaseHandler(); diff --git a/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt b/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt index 36f99e9757..12eb6cdca6 100644 --- a/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt +++ b/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt @@ -1,20 +1,21 @@ package com.quran.labs.androidquran.model.translation import android.content.Context +import com.quran.data.di.ActivityScope import com.quran.data.model.QuranText import com.quran.data.model.VerseRange import com.quran.data.pageinfo.mapper.AyahMapper import com.quran.labs.androidquran.data.QuranDataProvider import com.quran.labs.androidquran.database.DatabaseHandler import com.quran.labs.androidquran.database.DatabaseHandler.TextType -import com.quran.data.di.ActivityScope import com.quran.labs.androidquran.util.QuranFileUtils +import com.quran.mobile.di.qualifier.ApplicationContext import io.reactivex.rxjava3.core.Single import javax.inject.Inject @ActivityScope class TranslationModel @Inject internal constructor( - private val appContext: Context, + @ApplicationContext private val appContext: Context, private val quranFileUtils: QuranFileUtils, private val ayahMapper: AyahMapper ) { diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/QuranImportPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/QuranImportPresenter.java index 6ee2595f31..72cd13a8f9 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/QuranImportPresenter.java +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/QuranImportPresenter.java @@ -6,16 +6,18 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.ParcelFileDescriptor; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; -import com.quran.labs.androidquran.QuranImportActivity; import com.quran.data.model.bookmark.BookmarkData; +import com.quran.labs.androidquran.QuranImportActivity; import com.quran.labs.androidquran.model.bookmark.BookmarkImportExportModel; import com.quran.labs.androidquran.model.bookmark.BookmarkModel; import com.quran.labs.androidquran.service.util.PermissionUtil; import com.quran.labs.androidquran.util.QuranSettings; +import com.quran.mobile.di.qualifier.ApplicationContext; import java.io.FileDescriptor; import java.io.FileInputStream; @@ -24,9 +26,9 @@ import javax.inject.Inject; import javax.inject.Singleton; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.observers.DisposableMaybeObserver; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -48,7 +50,7 @@ public class QuranImportPresenter implements Presenter { private QuranImportActivity mCurrentActivity; @Inject - QuranImportPresenter(Context appContext, + QuranImportPresenter(@ApplicationContext Context appContext, BookmarkImportExportModel model, BookmarkModel bookmarkModel) { mAppContext = appContext; diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java index 695f742398..847623b12f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java @@ -13,7 +13,6 @@ import com.quran.data.model.bookmark.RecentPage; import com.quran.data.model.bookmark.Tag; import com.quran.labs.androidquran.dao.bookmark.BookmarkResult; -import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.model.bookmark.BookmarkModel; import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils; import com.quran.labs.androidquran.presenter.Presenter; @@ -22,6 +21,7 @@ import com.quran.labs.androidquran.ui.helpers.QuranRowFactory; import com.quran.labs.androidquran.util.QuranSettings; import com.quran.labs.androidquran.util.QuranUtils; +import com.quran.mobile.di.qualifier.ApplicationContext; import java.util.ArrayList; import java.util.HashMap; @@ -64,7 +64,7 @@ public class BookmarkPresenter implements Presenter { @Inject - BookmarkPresenter(Context appContext, + BookmarkPresenter(@ApplicationContext Context appContext, BookmarkModel bookmarkModel, QuranSettings quranSettings, ArabicDatabaseUtils arabicDatabaseUtils, diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt index d7010cba3d..3e4588807d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt @@ -11,11 +11,11 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import androidx.work.workDataOf +import com.quran.common.upgrade.LocalDataUpgrade import com.quran.data.core.QuranInfo import com.quran.data.model.QuranDataStatus -import com.quran.data.source.PageProvider -import com.quran.common.upgrade.LocalDataUpgrade import com.quran.data.source.PageContentType +import com.quran.data.source.PageProvider import com.quran.labs.androidquran.QuranDataActivity import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.presenter.Presenter @@ -26,9 +26,10 @@ import com.quran.labs.androidquran.worker.AudioUpdateWorker import com.quran.labs.androidquran.worker.MissingPageDownloadWorker import com.quran.labs.androidquran.worker.PartialPageCheckingWorker import com.quran.labs.androidquran.worker.WorkerConstants +import com.quran.mobile.di.qualifier.ApplicationContext +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.functions.Consumer import io.reactivex.rxjava3.schedulers.Schedulers @@ -38,12 +39,12 @@ import java.util.concurrent.TimeUnit.DAYS import javax.inject.Inject class QuranDataPresenter @Inject internal constructor( - val appContext: Context, - val quranInfo: QuranInfo, - val quranScreenInfo: QuranScreenInfo, - private val quranPageProvider: PageProvider, - val quranFileUtils: QuranFileUtils, - private val localDataUpgrade: LocalDataUpgrade + @ApplicationContext val appContext: Context, + val quranInfo: QuranInfo, + val quranScreenInfo: QuranScreenInfo, + private val quranPageProvider: PageProvider, + val quranFileUtils: QuranFileUtils, + private val localDataUpgrade: LocalDataUpgrade ) : Presenter { private var activity: QuranDataActivity? = null 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 b7b9778b11..74e16c69be 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 @@ -18,6 +18,7 @@ import com.quran.labs.androidquran.ui.TranslationManagerActivity; import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.labs.androidquran.util.QuranSettings; +import com.quran.mobile.di.qualifier.ApplicationContext; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; @@ -57,7 +58,7 @@ public class TranslationManagerPresenter implements Presenter Date: Sun, 15 Oct 2023 22:25:40 +0400 Subject: [PATCH 140/258] Annotate context with @ApplicationContext This was missing from the last PR, but should also be annotated. --- .../quran/labs/androidquran/database/TranslationsDaoImpl.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDaoImpl.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDaoImpl.kt index b7b694aa86..643910c2a6 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDaoImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDaoImpl.kt @@ -9,12 +9,13 @@ import com.quran.data.model.bookmark.Bookmark import com.quran.labs.androidquran.data.QuranDataProvider import com.quran.labs.androidquran.database.DatabaseHandler.TextType.Companion.TRANSLATION import com.quran.labs.androidquran.util.QuranFileUtils +import com.quran.mobile.di.qualifier.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject class TranslationsDaoImpl @Inject constructor( - private val appContext: Context, + @ApplicationContext private val appContext: Context, private val quranFileUtils: QuranFileUtils, ) : TranslationsDao { From 72896e9474c238cbd8ae0185a2d15ab0422e8136 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 16 Oct 2023 00:29:34 +0400 Subject: [PATCH 141/258] Use version catalogs within convention plugins --- build-logic/convention/build.gradle.kts | 43 ++++++++++--------- .../AndroidApplicationConventionPlugin.kt | 8 ++-- .../AndroidLibraryComposeConventionPlugin.kt | 15 +++++-- .../kotlin/AndroidLibraryConventionPlugin.kt | 7 ++- .../buildutil/DependenciesCommon.kt | 16 ++++++- build-logic/settings.gradle.kts | 14 ++++-- gradle/libs.versions.toml | 11 +++++ 7 files changed, 78 insertions(+), 36 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index fccdcd8c88..e0781c9f8c 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -1,34 +1,35 @@ plugins { - `kotlin-dsl` + `kotlin-dsl` } group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.1.2") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") + compileOnly("com.android.tools.build:gradle:8.1.2") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } gradlePlugin { - plugins { - register("androidApplication") { - id = "quran.android.application" - implementationClass = "AndroidApplicationConventionPlugin" - } + plugins { + register("androidApplication") { + id = "quran.android.application" + implementationClass = "AndroidApplicationConventionPlugin" + } - register("androidLibrary") { - id = "quran.android.library.android" - implementationClass = "AndroidLibraryConventionPlugin" - } + register("androidLibrary") { + id = "quran.android.library.android" + implementationClass = "AndroidLibraryConventionPlugin" + } - register("androidComposeLibrary") { - id = "quran.android.library.compose" - implementationClass = "AndroidLibraryComposeConventionPlugin" - } + register("androidComposeLibrary") { + id = "quran.android.library.compose" + implementationClass = "AndroidLibraryComposeConventionPlugin" + } - register("kotlinLibrary") { - id = "quran.android.library" - implementationClass = "KotlinLibraryConventionPlugin" - } - } + register("kotlinLibrary") { + id = "quran.android.library" + implementationClass = "KotlinLibraryConventionPlugin" + } + } } diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 0c56ae88bf..0fc68b79bd 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -2,18 +2,20 @@ import com.android.build.api.dsl.ApplicationExtension import com.quran.labs.androidquran.buildutil.applyAndroidCommon import com.quran.labs.androidquran.buildutil.applyBoms import com.quran.labs.androidquran.buildutil.applyKotlinCommon +import com.quran.labs.androidquran.buildutil.withLibraries import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure -import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { - apply("com.android.application") - apply("org.jetbrains.kotlin.android") + withLibraries { libs -> + apply(libs.plugins.android.application.get().pluginId) + apply(libs.plugins.kotlin.android.get().pluginId) + } } extensions.configure { diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 22cf65de0d..49a19435b3 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -2,6 +2,7 @@ import com.android.build.gradle.LibraryExtension import com.quran.labs.androidquran.buildutil.applyAndroidCommon import com.quran.labs.androidquran.buildutil.applyBoms import com.quran.labs.androidquran.buildutil.applyKotlinCommon +import com.quran.labs.androidquran.buildutil.withLibraries import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure @@ -12,14 +13,18 @@ class AndroidLibraryComposeConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { - apply("com.android.library") - apply("org.jetbrains.kotlin.android") + withLibraries { libs -> + apply(libs.plugins.android.library.get().pluginId) + apply(libs.plugins.kotlin.android.get().pluginId) + } } extensions.configure { applyAndroidCommon(target) buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.5.3" + withLibraries { libs -> + composeOptions.kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() + } } applyKotlinCommon() @@ -29,7 +34,9 @@ class AndroidLibraryComposeConventionPlugin : Plugin { // all compose projects need the runtime. // we can switch this to implementation instead of api once a fix is pushed for // https://issuetracker.google.com/issues/209688774. - add("api", "androidx.compose.runtime:runtime") + withLibraries { libs -> + add("api", libs.compose.runtime) + } } } } diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt index 6ee755e618..c2d9599e03 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt @@ -2,6 +2,7 @@ import com.android.build.gradle.LibraryExtension import com.quran.labs.androidquran.buildutil.applyAndroidCommon import com.quran.labs.androidquran.buildutil.applyBoms import com.quran.labs.androidquran.buildutil.applyKotlinCommon +import com.quran.labs.androidquran.buildutil.withLibraries import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure @@ -11,8 +12,10 @@ class AndroidLibraryConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { - apply("com.android.library") - apply("org.jetbrains.kotlin.android") + withLibraries { libs -> + apply(libs.plugins.android.library.get().pluginId) + apply(libs.plugins.kotlin.android.get().pluginId) + } } extensions.configure { diff --git a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt index 1071b5cb2d..11b5c83c66 100644 --- a/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt +++ b/build-logic/convention/src/main/kotlin/com/quran/labs/androidquran/buildutil/DependenciesCommon.kt @@ -1,11 +1,23 @@ package com.quran.labs.androidquran.buildutil +import org.gradle.accessors.dm.LibrariesForLibs import org.gradle.api.Project import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.the + +// via various comments on https://github.com/gradle/gradle/issues/15383 +fun Project.withLibraries(block: (LibrariesForLibs) -> Unit) { + if (name != "gradle-kotlin-dsl-accessors") { + val libs = the() + block.invoke(libs) + } +} fun Project.applyBoms() { dependencies { - add("implementation", platform("com.squareup.okhttp3:okhttp-bom:4.10.0")) - add("implementation", platform("androidx.compose:compose-bom:2023.09.00")) + withLibraries { libs -> + add("implementation", platform(libs.okhttp.bom)) + add("implementation", platform(libs.compose.bom)) + } } } diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index f0c8d9e099..bc446410b7 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,8 +1,14 @@ dependencyResolutionManagement { - repositories { - google() - mavenCentral() - } + repositories { + google() + mavenCentral() + } + + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } } rootProject.name = "build-logic" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af88f111c7..1b99d0da92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,12 @@ agp = "8.1.2" kotlin = "1.9.10" ksp = "1.9.10-1.0.13" +# required within the Gradle convention plugins - not unused +compose-compiler = "1.5.3" +composeBomVersion = "2023.09.00" +okhttpBomVersion = "4.10.0" + +# dependencies anvil = "2.4.8" coroutinesVersion = "1.7.3" crashlytics = "2.9.9" @@ -67,6 +73,11 @@ robolectricVersion = "4.10.3" kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } +# required within the Gradle convention plugins - not unused +okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttpBomVersion" } +compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBomVersion" } +compose-runtime = { module = "androidx.compose.runtime:runtime" } + # androidx androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompatVersion" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCoreVersion" } From d55cae228a684f4fef175cf3c47a63e25e2f6302 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Oct 2023 21:29:06 +0000 Subject: [PATCH 142/258] Update dependency androidx.compose:compose-bom to v2023.10.00 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1b99d0da92..c3983c37ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "1.9.10-1.0.13" # required within the Gradle convention plugins - not unused compose-compiler = "1.5.3" -composeBomVersion = "2023.09.00" +composeBomVersion = "2023.10.00" okhttpBomVersion = "4.10.0" # dependencies From 83eb94e662e4b25138390f6f27c7fb63fb7119b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 15 Oct 2023 22:42:58 +0000 Subject: [PATCH 143/258] Update dependency com.squareup.okhttp3:okhttp-bom to v4.11.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3983c37ab..d801749dda 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ ksp = "1.9.10-1.0.13" # required within the Gradle convention plugins - not unused compose-compiler = "1.5.3" composeBomVersion = "2023.10.00" -okhttpBomVersion = "4.10.0" +okhttpBomVersion = "4.11.0" # dependencies anvil = "2.4.8" From a8e3e52a12c8795435493da7823ca3ce26e2d035 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 03:31:35 +0000 Subject: [PATCH 144/258] Update dependency com.squareup.okhttp3:okhttp-bom to v4.12.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d801749dda..2c27b0ba50 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ ksp = "1.9.10-1.0.13" # required within the Gradle convention plugins - not unused compose-compiler = "1.5.3" composeBomVersion = "2023.10.00" -okhttpBomVersion = "4.11.0" +okhttpBomVersion = "4.12.0" # dependencies anvil = "2.4.8" From 26e14153d140ebf8e0f22302df2497cb120ec421 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:01:44 +0000 Subject: [PATCH 145/258] Update dependency com.google.firebase:firebase-crashlytics to v18.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2c27b0ba50..c4f4744f20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ androidxWorkManagerVersion = "2.8.1" # firebase firebaseAnalyticsVersion = "21.3.0" -firebaseCrashlyticsVersion = "18.4.3" +firebaseCrashlyticsVersion = "18.5.0" # ui libraries accompanistVersion = "0.32.0" From 9747c22cfce017b187e9e49b8086149dac29e7ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:40:46 +0000 Subject: [PATCH 146/258] Update dependency com.google.firebase:firebase-analytics-ktx to v21.4.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c4f4744f20..d2bb60f7c4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,7 @@ androidxPagingComposeVersion = "3.2.1" androidxWorkManagerVersion = "2.8.1" # firebase -firebaseAnalyticsVersion = "21.3.0" +firebaseAnalyticsVersion = "21.4.0" firebaseCrashlyticsVersion = "18.5.0" # ui libraries From 6913bd33c17c6bd523376e72cca6765fd514ae53 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:43:50 +0000 Subject: [PATCH 147/258] Update dependency androidx.compose:compose-bom to v2023.10.01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d2bb60f7c4..3fc917bf4c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "1.9.10-1.0.13" # required within the Gradle convention plugins - not unused compose-compiler = "1.5.3" -composeBomVersion = "2023.10.00" +composeBomVersion = "2023.10.01" okhttpBomVersion = "4.12.0" # dependencies From c396fc40224b7081002c7022384ebb04902fca05 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 18:43:55 +0000 Subject: [PATCH 148/258] Update dependency androidx.recyclerview:recyclerview to v1.3.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d2bb60f7c4..3ef6106ebe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -38,7 +38,7 @@ androidxJunitExtVersion = "1.1.5" androidxLocalBroadcastVersion = "1.1.0" androidxMediaVersion = "1.6.0" androidxPreferencesVersion = "1.2.1" -androidxRecyclerViewVersion = "1.3.1" +androidxRecyclerViewVersion = "1.3.2" androidxSwipeRefreshVersion = "1.1.0" androidxPagingVersion = "3.2.1" androidxPagingComposeVersion = "3.2.1" From 226e306d78708d44b20bb75d1b47b324ccb8c2a4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 19:25:22 +0000 Subject: [PATCH 149/258] Update dependency com.google.errorprone:error_prone_core to v2.23.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ea8abc149..b8f7aa7859 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ crashlytics = "2.9.9" daggerVersion = "2.48.1" dnsjavaVersion = "2.1.9" errorprone = "3.1.0" -errorproneCoreVersion = "2.22.0" +errorproneCoreVersion = "2.23.0" googleServices = "4.4.0" leakcanaryAndroidVersion = "2.12" moshiVersion = "1.15.0" From 28bbfb68f0593d178eb37c3301fcf67529beaa6a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 06:57:32 +0000 Subject: [PATCH 150/258] Update dependency io.grpc:grpc-okhttp to v1.59.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2ea8abc149..803fcc7142 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" # recitations -grpcOkhttpVersion = "1.58.0" +grpcOkhttpVersion = "1.59.0" googleAuthVersion = "1.20.0" googleCloudSpeechVersion = "4.23.0" From 4401de578fc05052b6d2d6ef05e4976ffea4ae86 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Tue, 24 Oct 2023 01:07:25 +0400 Subject: [PATCH 151/258] Generalize Portuguese strings and fix Malaysian Some Malaysian strings were still set as Burmese from when we were using values-my instead of values-ms. This renames them. Also, keep the Portuguese folder for all Portuguese languages instead of just for Brazil. --- .../main/res/{values-pt-rBR => values-pt}/strings.xml | 0 .../src/main/res/{values-my => values-ms}/readers.xml | 0 .../src/main/res/{values-my => values-ms}/strings.xml | 0 .../main/res/{values-pt-rBR => values-pt}/strings.xml | 0 .../main/res/{values-pt-rBR => values-pt}/strings.xml | 0 .../qarilist/src/main/res/values-pt-rBR/strings.xml | 10 ---------- 6 files changed, 10 deletions(-) rename app/src/main/res/{values-pt-rBR => values-pt}/strings.xml (100%) rename common/audio/src/main/res/{values-my => values-ms}/readers.xml (100%) rename common/toolbar/src/main/res/{values-my => values-ms}/strings.xml (100%) rename common/ui/core/src/main/res/{values-pt-rBR => values-pt}/strings.xml (100%) rename feature/downloadmanager/src/main/res/{values-pt-rBR => values-pt}/strings.xml (100%) delete mode 100644 feature/qarilist/src/main/res/values-pt-rBR/strings.xml diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt/strings.xml similarity index 100% rename from app/src/main/res/values-pt-rBR/strings.xml rename to app/src/main/res/values-pt/strings.xml diff --git a/common/audio/src/main/res/values-my/readers.xml b/common/audio/src/main/res/values-ms/readers.xml similarity index 100% rename from common/audio/src/main/res/values-my/readers.xml rename to common/audio/src/main/res/values-ms/readers.xml diff --git a/common/toolbar/src/main/res/values-my/strings.xml b/common/toolbar/src/main/res/values-ms/strings.xml similarity index 100% rename from common/toolbar/src/main/res/values-my/strings.xml rename to common/toolbar/src/main/res/values-ms/strings.xml diff --git a/common/ui/core/src/main/res/values-pt-rBR/strings.xml b/common/ui/core/src/main/res/values-pt/strings.xml similarity index 100% rename from common/ui/core/src/main/res/values-pt-rBR/strings.xml rename to common/ui/core/src/main/res/values-pt/strings.xml diff --git a/feature/downloadmanager/src/main/res/values-pt-rBR/strings.xml b/feature/downloadmanager/src/main/res/values-pt/strings.xml similarity index 100% rename from feature/downloadmanager/src/main/res/values-pt-rBR/strings.xml rename to feature/downloadmanager/src/main/res/values-pt/strings.xml diff --git a/feature/qarilist/src/main/res/values-pt-rBR/strings.xml b/feature/qarilist/src/main/res/values-pt-rBR/strings.xml deleted file mode 100644 index 81181c2d37..0000000000 --- a/feature/qarilist/src/main/res/values-pt-rBR/strings.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - Selecione um Qari - Pronto para ouvir - Recitadores com Downloads - Contínuo - Não-Continuidade - Selecionado - Despedimento - From d8607a6871685d2780493470df2449f1a7707765 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 21:31:18 +0000 Subject: [PATCH 152/258] Update dependency com.google.cloud:google-cloud-speech to v4.24.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8968b05800..44a8804599 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.59.0" googleAuthVersion = "1.20.0" -googleCloudSpeechVersion = "4.23.0" +googleCloudSpeechVersion = "4.24.0" # testing junitVersion = "4.13.2" From 996f5f2a5786a2af9deebf85ae7fc054156f04ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Oct 2023 21:47:03 +0000 Subject: [PATCH 153/258] Update dependency com.google.firebase:firebase-crashlytics to v18.5.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44a8804599..6f3cb5754f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ androidxWorkManagerVersion = "2.8.1" # firebase firebaseAnalyticsVersion = "21.4.0" -firebaseCrashlyticsVersion = "18.5.0" +firebaseCrashlyticsVersion = "18.5.1" # ui libraries accompanistVersion = "0.32.0" From edc4ce7a7cfa2baf8bd2ecae9a2f3378498c97ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:30:30 +0000 Subject: [PATCH 154/258] Update dependency org.robolectric:robolectric to v4.11 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6f3cb5754f..f1c7962d28 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ junitVersion = "4.13.2" espressoVersion = "3.5.1" truthVersion = "1.1.5" mockitoVersion = "5.6.0" -robolectricVersion = "4.10.3" +robolectricVersion = "4.11" [libraries] kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" } From 2e8bf233dd05334b3026146a88cdf7be99c7e0a0 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 20 Aug 2023 22:57:24 +0400 Subject: [PATCH 155/258] Target sdk 34 --- app/src/main/AndroidManifest.xml | 4 ++++ .../com/quran/labs/androidquran/service/AudioService.kt | 8 ++++++-- .../src/main/kotlin/AndroidApplicationConventionPlugin.kt | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65acc415de..2a87047ecc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,10 @@ + + + + diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index fa8843dc5b..df7e308080 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -263,10 +263,14 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, audioFocusHelper = AudioFocusHelper(appContext, this) broadcastManager = LocalBroadcastManager.getInstance(appContext) noisyAudioStreamReceiver = NoisyAudioStreamReceiver() - registerReceiver( + + ContextCompat.registerReceiver( + this, noisyAudioStreamReceiver, - IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) + IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY), + ContextCompat.RECEIVER_EXPORTED ) + val receiver = ComponentName(this, MediaButtonReceiver::class.java) mediaSession = MediaSessionCompat(appContext, "QuranMediaSession", receiver, null) mediaSession.setCallback(MediaSessionCallback(), serviceHandler) diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 0fc68b79bd..97f486a77c 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -20,7 +20,7 @@ class AndroidApplicationConventionPlugin : Plugin { extensions.configure { applyAndroidCommon(target) - defaultConfig.targetSdk = 33 + defaultConfig.targetSdk = 34 } applyKotlinCommon() From 038d250a16a02e8b854b33c1b6b73c71a97cdc42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:31:42 +0000 Subject: [PATCH 156/258] Update dependency com.google.firebase:firebase-analytics-ktx to v21.5.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1c7962d28..75a855e487 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,7 +45,7 @@ androidxPagingComposeVersion = "3.2.1" androidxWorkManagerVersion = "2.8.1" # firebase -firebaseAnalyticsVersion = "21.4.0" +firebaseAnalyticsVersion = "21.5.0" firebaseCrashlyticsVersion = "18.5.1" # ui libraries From 750380d6c10b27d8975991b512d8dd0a8e53774a Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Wed, 1 Nov 2023 01:16:29 +0400 Subject: [PATCH 157/258] Minor clean up of Arabic search We can use the same replacement characters for all the riwayat, since the special cased one just for Warsh is actually useful for Hafs as well. --- .../quran/labs/androidquran/database/DatabaseHandler.kt | 2 +- .../com/quran/labs/androidquran/data/QuranFileConstants.kt | 1 - .../main/java/com/quran/common/search/ArabicSearcher.kt | 7 +++---- .../quran/common/search/arabic/ArabicCharacterHelper.kt | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt b/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt index eac9ab3099..fc2602685d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/DatabaseHandler.kt @@ -84,7 +84,7 @@ class DatabaseHandler private constructor( ContextCompat.getColor(context, R.color.translation_highlight) + "\">" defaultSearcher = DefaultSearcher(matchString, MATCH_END, ELLIPSES) - arabicSearcher = ArabicSearcher(defaultSearcher, matchString, MATCH_END, QuranFileConstants.SEARCH_EXTRA_REPLACEMENTS) + arabicSearcher = ArabicSearcher(defaultSearcher, matchString, MATCH_END) // if there's no Quran base directory, there are no databases val base = quranFileUtils.getQuranDatabaseDirectory(context) diff --git a/app/src/warsh/java/com/quran/labs/androidquran/data/QuranFileConstants.kt b/app/src/warsh/java/com/quran/labs/androidquran/data/QuranFileConstants.kt index e5f301e47a..4c7c072eb2 100644 --- a/app/src/warsh/java/com/quran/labs/androidquran/data/QuranFileConstants.kt +++ b/app/src/warsh/java/com/quran/labs/androidquran/data/QuranFileConstants.kt @@ -14,7 +14,6 @@ object QuranFileConstants { const val FETCH_QUARTER_NAMES_FROM_DATABASE = true const val FALLBACK_PAGE_TYPE = "warsh" - const val SEARCH_EXTRA_REPLACEMENTS = "\u0626" var ICON_RESOURCE_ID = com.quran.labs.androidquran.pages.warsh.R.drawable.icon } diff --git a/common/search/src/main/java/com/quran/common/search/ArabicSearcher.kt b/common/search/src/main/java/com/quran/common/search/ArabicSearcher.kt index 6a183f21f6..dd6d4acfe2 100644 --- a/common/search/src/main/java/com/quran/common/search/ArabicSearcher.kt +++ b/common/search/src/main/java/com/quran/common/search/ArabicSearcher.kt @@ -8,9 +8,8 @@ import java.util.regex.Pattern class ArabicSearcher(private val defaultSearcher: Searcher, private val matchStart: String, - private val matchEnd: String, - extraReplacements: String = "") : Searcher { - private val arabicRegex = "[$arabicRegexChars$extraReplacements]".toRegex() + private val matchEnd: String) : Searcher { + private val arabicRegex = "[$arabicRegexChars]".toRegex() override fun getQuery(withSnippets: Boolean, hasFTS: Boolean, @@ -79,6 +78,6 @@ class ArabicSearcher(private val defaultSearcher: Searcher, } companion object { - private const val arabicRegexChars = "\u0627\u0623\u0621\u062a\u0629\u0647\u0648\u0649" + private const val arabicRegexChars = "\u0627\u0623\u0621\u062a\u0629\u0647\u0648\u0649\u0626" } } diff --git a/common/search/src/main/java/com/quran/common/search/arabic/ArabicCharacterHelper.kt b/common/search/src/main/java/com/quran/common/search/arabic/ArabicCharacterHelper.kt index 395e55a2ff..85325c78e0 100644 --- a/common/search/src/main/java/com/quran/common/search/arabic/ArabicCharacterHelper.kt +++ b/common/search/src/main/java/com/quran/common/search/arabic/ArabicCharacterHelper.kt @@ -37,7 +37,7 @@ object ArabicCharacterHelper { // given: ئ // match: ئﻯي - // this is helpful for rewayat Warsh, and thus only enabled on it + // this is especially helpful for rewayat Warsh "\u0626" to "\u0626\u0649\u064a", ) From 8bd2436dbc0e2cef50eda7680e5be427ac82fa33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Oct 2023 21:18:23 +0000 Subject: [PATCH 158/258] Update dependency org.robolectric:robolectric to v4.11.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75a855e487..2912762ea7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,7 +67,7 @@ junitVersion = "4.13.2" espressoVersion = "3.5.1" truthVersion = "1.1.5" mockitoVersion = "5.6.0" -robolectricVersion = "4.11" +robolectricVersion = "4.11.1" [libraries] kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" } From 455a10937f3f13f9799aecc62d33f568af496d3e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 18:55:17 +0000 Subject: [PATCH 159/258] Update dependency androidx.fragment:fragment-ktx to v1.6.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2912762ea7..9b93f8c42e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,7 +33,7 @@ androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" androidxNavigationVersion = "2.7.4" androidxDynamicAnimationVersion = "1.0.0" -androidxFragmentVersion = "1.6.1" +androidxFragmentVersion = "1.6.2" androidxJunitExtVersion = "1.1.5" androidxLocalBroadcastVersion = "1.1.0" androidxMediaVersion = "1.6.0" From 7932c394f2035582da3099c274c93feec15cc98f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Nov 2023 18:55:21 +0000 Subject: [PATCH 160/258] Update dependency androidx.navigation:navigation-compose to v2.7.5 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2912762ea7..fff370e7c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.8.0" androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" -androidxNavigationVersion = "2.7.4" +androidxNavigationVersion = "2.7.5" androidxDynamicAnimationVersion = "1.0.0" androidxFragmentVersion = "1.6.1" androidxJunitExtVersion = "1.1.5" From 6a68ec2b9d1c36bc3ac9c82a4605e13449d885b5 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 3 Nov 2023 01:10:36 +0400 Subject: [PATCH 161/258] Fix preferences listener not firing due to GC Fixes #2443. --- .../module/application/ApplicationModule.kt | 1 + .../labs/androidquran/util/SettingsImpl.kt | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt b/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt index 03feb4eaed..92a0fa55f3 100644 --- a/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt +++ b/app/src/main/java/com/quran/labs/androidquran/di/module/application/ApplicationModule.kt @@ -58,6 +58,7 @@ object ApplicationModule { } @Provides + @Singleton fun provideSettings(settingsImpl: SettingsImpl): Settings { return settingsImpl } diff --git a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt index 00802a6ee3..debdebacac 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/SettingsImpl.kt @@ -2,28 +2,43 @@ package com.quran.labs.androidquran.util import android.content.SharedPreferences import com.quran.data.dao.Settings +import kotlinx.coroutines.MainScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.onFailure import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.shareIn import javax.inject.Inject +import javax.inject.Singleton +@Singleton class SettingsImpl @Inject constructor(private val quranSettings: QuranSettings) : Settings { - private val preferencesFlow: Flow by lazy { + private val scope = MainScope() + + private val preferencesFlow = callbackFlow { - val callback = + val prefsCallback = SharedPreferences.OnSharedPreferenceChangeListener { _, pref -> if (pref != null) { trySendBlocking(pref) .onFailure {} } } - quranSettings.registerPreferencesListener(callback) - - awaitClose { quranSettings.unregisterPreferencesListener(callback) } + quranSettings.registerPreferencesListener(prefsCallback) + awaitClose { quranSettings.unregisterPreferencesListener(prefsCallback) } } - } + + // removing WhileSubscribed here breaks release versions of the app most likely + // due to being garbage collection with no strong references to the job (note + // that the callbacks are WeakReferences within SharedPreferences). see + // this issue for https://github.com/Kotlin/kotlinx.coroutines/issues/2557 + // details. While the aforementioned issue is fixed in coroutines, this issue + // still happens unless we either use WhileSubscribed as here, or we keep a + // strong reference to the SharedPreferenceChangeListener. See also this issue + // https://github.com/Kotlin/kotlinx.coroutines/issues/1061. + .shareIn(scope, SharingStarted.WhileSubscribed()) override suspend fun setVersion(version: Int) { quranSettings.version = version From d3cfe603cefe80dec18decd74a41f59f7e69ccbe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 21:21:36 +0000 Subject: [PATCH 162/258] Update dependency org.mockito:mockito-core to v5.7.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0107e87139..805ff0a40a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ googleCloudSpeechVersion = "4.24.0" junitVersion = "4.13.2" espressoVersion = "3.5.1" truthVersion = "1.1.5" -mockitoVersion = "5.6.0" +mockitoVersion = "5.7.0" robolectricVersion = "4.11.1" [libraries] From e796ce3acabe331c0f74a2b4d74cba47c6aebd8a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 12:03:54 +0000 Subject: [PATCH 163/258] Update dependency com.github.skydoves:balloon to v1.6.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 805ff0a40a..c5df3fe226 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ firebaseCrashlyticsVersion = "18.5.1" # ui libraries accompanistVersion = "0.32.0" -balloonVersion = "1.6.1" +balloonVersion = "1.6.2" tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" materialComponentsVersion = "1.10.0" From 290a1252271540462e0a703b6e3fc34090661862 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 6 Nov 2023 21:35:53 +0000 Subject: [PATCH 164/258] Update dependency com.google.cloud:google-cloud-speech to v4.25.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5df3fe226..76c66eb7e6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.59.0" googleAuthVersion = "1.20.0" -googleCloudSpeechVersion = "4.24.0" +googleCloudSpeechVersion = "4.25.0" # testing junitVersion = "4.13.2" From 47363f1288565133661a74ad58825303c94c82b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:07:38 +0000 Subject: [PATCH 165/258] Update agp to v8.1.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76c66eb7e6..fc73c1743a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.1.2" +agp = "8.1.3" kotlin = "1.9.10" ksp = "1.9.10-1.0.13" From 0fae0f28eac5a68e562457ffa9004b71f9dfce24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:07:42 +0000 Subject: [PATCH 166/258] Update dependency com.android.tools.build:gradle to v8.1.3 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index e0781c9f8c..8b37058e65 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.1.2") + compileOnly("com.android.tools.build:gradle:8.1.3") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } From 7592deba71d8d216dcf2084abccc01e2daa3d513 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 15 Nov 2023 19:18:06 +0000 Subject: [PATCH 167/258] Update dependency androidx.activity:activity-compose to v1.8.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc73c1743a..79a81e92a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ timberVersion = "5.0.1" # androidx library versions androidxAppcompatVersion = "1.6.1" -androidxActivityComposeVersion = "1.8.0" +androidxActivityComposeVersion = "1.8.1" androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" androidxNavigationVersion = "2.7.5" From 79655c4690cfba9af89d1c57942a1e00b622610a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:17:51 +0000 Subject: [PATCH 168/258] Update agp to v8.1.4 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc73c1743a..ccbbdd825f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.1.3" +agp = "8.1.4" kotlin = "1.9.10" ksp = "1.9.10-1.0.13" From cd6bfd7b57db47c98347e667cdd1111cdf7bb34c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 20:17:55 +0000 Subject: [PATCH 169/258] Update dependency com.android.tools.build:gradle to v8.1.4 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 8b37058e65..b46bedaf13 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.1.3") + compileOnly("com.android.tools.build:gradle:8.1.4") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } From 1bf8195e3905ae9c988e4d21dcc239dcb084182d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 16 Nov 2023 21:12:49 +0000 Subject: [PATCH 170/258] Update dependency com.google.firebase:firebase-crashlytics to v18.6.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ccbbdd825f..c98b40e9c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ androidxWorkManagerVersion = "2.8.1" # firebase firebaseAnalyticsVersion = "21.5.0" -firebaseCrashlyticsVersion = "18.5.1" +firebaseCrashlyticsVersion = "18.6.0" # ui libraries accompanistVersion = "0.32.0" From 797c1abd25f77477315b21f6cf5ef5d5a40ec238 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 17 Nov 2023 22:03:36 +0400 Subject: [PATCH 171/258] Update Kotlin to 1.9.20 and update Compose and KSP --- build-logic/convention/build.gradle.kts | 2 +- gradle/libs.versions.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index b46bedaf13..7e87c66314 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:8.1.4") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.10") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 88848fd397..0884f27216 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] agp = "8.1.4" -kotlin = "1.9.10" -ksp = "1.9.10-1.0.13" +kotlin = "1.9.20" +ksp = "1.9.20-1.0.14" # required within the Gradle convention plugins - not unused -compose-compiler = "1.5.3" +compose-compiler = "1.5.4" composeBomVersion = "2023.10.01" okhttpBomVersion = "4.12.0" From 79fd87aad8ae33c6bb84970ae2b7ef0aa3bd6404 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 18 Nov 2023 01:24:13 +0400 Subject: [PATCH 172/258] Use Kitab font for inline Arabic translation This uses the Kitab font for the inline ayah translation view, just like the full screen translation view does. This helps fix artifacts and strange issues on older phones with how some characters are rendered. Also made the Arabic text slightly larger in this view. Fixes #2459. --- .../ui/helpers/TypefaceWrappingSpan.kt | 16 ++++++++++++ .../view/InlineTranslationView.kt | 25 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/helpers/TypefaceWrappingSpan.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/TypefaceWrappingSpan.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/TypefaceWrappingSpan.kt new file mode 100644 index 0000000000..e14ab0501e --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/TypefaceWrappingSpan.kt @@ -0,0 +1,16 @@ +package com.quran.labs.androidquran.ui.helpers + +import android.graphics.Typeface +import android.text.TextPaint +import android.text.style.MetricAffectingSpan + +class TypefaceWrappingSpan(private val typeface: Typeface) : MetricAffectingSpan() { + + override fun updateDrawState(ds: TextPaint) { + ds.typeface = typeface + } + + override fun updateMeasureState(paint: TextPaint) { + paint.typeface = typeface + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt index ea30f212ff..2c4b225e72 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt @@ -8,7 +8,9 @@ import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextUtils import android.text.style.ForegroundColorSpan +import android.text.style.RelativeSizeSpan import android.text.style.StyleSpan +import android.text.style.TypefaceSpan import android.util.AttributeSet import android.view.View import android.widget.LinearLayout @@ -16,10 +18,14 @@ import android.widget.ScrollView import android.widget.TextView import androidx.annotation.StyleRes import androidx.core.content.ContextCompat +import com.quran.common.search.SearchTextUtil import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.common.TranslationMetadata +import com.quran.labs.androidquran.ui.helpers.TypefaceWrappingSpan +import com.quran.labs.androidquran.ui.translation.TranslationAdapter +import com.quran.labs.androidquran.ui.util.TypefaceManager import com.quran.labs.androidquran.util.QuranSettings class InlineTranslationView @JvmOverloads constructor( @@ -135,7 +141,7 @@ class InlineTranslationView @JvmOverloads constructor( } // irrespective of whether it's a link or not, show the text - builder.append(stylize(ayah.texts[i], translationText)) + builder.append(stylize(ayah.texts[i], translations[i].languageCode, translationText)) } } ayahView.append(builder) @@ -159,10 +165,27 @@ class InlineTranslationView @JvmOverloads constructor( private fun stylize( metadata: TranslationMetadata, + languageCode: String? = null, translationText: String ): CharSequence { val spannableStringBuilder = SpannableStringBuilder(translationText) + if (languageCode == "ar") { + val spans = listOf( + TypefaceWrappingSpan(TypefaceManager.getTafseerTypeface(context)), + RelativeSizeSpan(1.1f) + ) + + spans.forEach { span -> + spannableStringBuilder.setSpan( + span, + 0, + spannableStringBuilder.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + metadata.ayat.forEach { range -> val span = ForegroundColorSpan(inlineAyahColor) spannableStringBuilder.setSpan( From 9492d7d3ab0596f2d0215cdd6dac01b5d90de93d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:56:25 +0000 Subject: [PATCH 173/258] Update dependency com.github.skydoves:balloon to v1.6.3 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0884f27216..99cbc70f3e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ firebaseCrashlyticsVersion = "18.6.0" # ui libraries accompanistVersion = "0.32.0" -balloonVersion = "1.6.2" +balloonVersion = "1.6.3" tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" materialComponentsVersion = "1.10.0" From ded8a7f15d25b1045cae973c084a6c77c010fb2e Mon Sep 17 00:00:00 2001 From: Abduqadir Abliz Date: Thu, 23 Nov 2023 18:35:34 -0500 Subject: [PATCH 174/258] Uyghur language translation updated, sura name added. --- app/src/main/res/values-ug/booleans.xml | 4 + app/src/main/res/values-ug/strings.xml | 157 ++++++++++++++++++---- app/src/main/res/values-ug/sura_names.xml | 119 ++++++++++++++++ 3 files changed, 254 insertions(+), 26 deletions(-) create mode 100644 app/src/main/res/values-ug/booleans.xml create mode 100644 app/src/main/res/values-ug/sura_names.xml diff --git a/app/src/main/res/values-ug/booleans.xml b/app/src/main/res/values-ug/booleans.xml new file mode 100644 index 0000000000..b6c5d78cf3 --- /dev/null +++ b/app/src/main/res/values-ug/booleans.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index 7f6e4876d6..22f49d8444 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -2,14 +2,16 @@ قۇرئان لازىملىق ھۆججەتلەرنى چۈشۈرەمدۇ؟ - ئەڭ ياخشى ئۈنۈمگە ئېرىشىش ئۈچۈن، بەزى ھۆججەتلەرنى چۈشۈرۈش زۆرۈر. ئەگەر ھازىر بۇنداق قىلىشنى تاللىمىسىڭىز، ھەر بىر بەتنى يۈكلەشكە تېخىمۇ ئۇزۇن ۋاقىت كېتىدۇ. لازىملىق ھۆججەتلەرنى ھازىرلا چۈشۈرەمسىز؟ + قۇرئان ئاندىرويىدنىڭ نورمال ئىشلىشى ئۈچۈن بەزى ھۆججەتلەرنى چۈشۈرۈشىمىز كېرەك. ئەگەر ھازىر بۇنى قىلمىسىڭىز، بۇ دېتال ئىشەنچلىك ئىشلىمەسلىكى مۇمكىن، ئوقۇش ئۈچۈن ئىنتېرنېت ئۇلىنىشى تەلەپ قىلىنىدۇ. لازىملىق ھۆججەتلەرنى ھازىر چۈشۈرەمسىز؟ بىز يېقىندا تاختا كومپيۇتېر ئۈچۈن ياخشىلانغان سۈرەتلەرنى قوشتۇق. ئۇ سۈرەتلەرنى ھازىر چۈشۈرەمسىز؟ ئۈسكۈنىڭىز ئۈچۈن ئۆزى كىچىك ئەمما مۇھىم بولغان قۇرئان سۈرەتلىرىنىڭ يېڭىلانمىسى بار. بۇ ياماقنى ھازىرلا چۈشۈرەمسىز؟ ھەئە ياق + چۈشۈرۈۋاتىدۇ… ھۆججەتلەرنى چۈشۈرۈشنى كۈتۈڭ. (داۋاملاشتۇرۇشنى قوللايدۇ) بىر تەرەپ قىلىۋاتىدۇ… بىز ھەققىدە + باشقا ئەپلەر تەڭشەكلەر ئاخىرقى بەت خەتكۈچلەر @@ -18,6 +20,8 @@ تەرتىپلە قوشۇلغان چېسلا قۇرئاندىكى ئورنى + بەلگە بويىچە گۇرۇپپىلا + چېسلانى كۆرسەت تەرجىمە كۆرسەت قۇرئاننى كۆرسەت ياردەم @@ -25,16 +29,48 @@ تەرجىمىگە ئېرىش يۆتكەل بەتكە يۆتكەل + سەل كۈتۈڭ… + + ئاندىرويىد قۇرئان ھەقسىز قۇرئان ئەپى. نامازلىرىڭىزدا تۆھپىكارلارغا دۇئا قىلىشنى ئۇنۇتماڭ. + سانلىق مەلۇمات مەنبەسى + سۈرەتلەر + قۇرئان سۈرەتلىرى پادىشاھ فاھىد قۇرئان باسما زاۋۇتىنىڭ خەت نۇسخىسىنى ئاساس قىلغان + Naskh ئەپىنىڭ قۇرئان سۈرەتلىرى (ئىجازەت بىلەن) SHL ئۇچۇر سىستېمىسىدىن ئېلىنغان + قالۇن سۈرەتلىرى Nous Memes Editions Et Diffusion (تۇنىس) نىڭ رۇخسىتى بىلەن ئىشلىتىلدى. + يوچۇقسىز mp3 قۇرئان تىلاۋەتلىرى + ئېلېكترونلۇق موشاف قۇرۇلۇشى + پادىشاھ سەئۇد ئۇنىۋېرسىتېتى ئېلېكترونلۇق موشاف قۇرۇلۇشى كۆپلىگەن تىلدىكى ئەرەبچە تەپسىر ۋە تەرجىمىلەرنىڭ مەنبەسى. ئۇ يەنە دوكتور ئايمان سۇۋەيدنىڭ قىرائىتىنىڭمۇ مەنبەسى. + ئۇسمانى تېكىست ۋە ئەرەبچە تەپسىر + نۇرغۇن تىللارنىڭ تەرجىمىسى + ئازراق تىللارنىڭ تەرجىمىسى + نۇرېخىرا خەت نۇسخىسى ۋە تەپسىرچى تاقى تەرجىمىسى + ئوچۇق مەنبەلىك قۇرۇلۇش + باشقىلار + تۆھپىكارلار + قۇرئان ئاندىرويىدنىڭ تەرەققىياتىغا تۆھپە قوشقان كىشىلەر تىزىمى + كۆپ سورىلىدىغان سوئاللار - <b>قىرائەت ئاۋازىنى قانداق چالىدۇ؟</b> <br/> خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران ئاستىدا قىرائەت توپچىسى ۋە قىرائەت قىلغۇچىنىڭ ئىسمى كۆرۈنىدۇ. قىرائەت قىلغۇچىنىڭ ئىسمىنى چېكىپ قىرائەت قىلغۇچىلارنى تاللىغىلى بولىدۇ، چېلىش توپچىسىنى چېكىلسە نۆۋەتتىكى بەت ياكى نۆۋەتتىكى سۈرەنىڭ قىرائەت ئاۋازىنى چۈشۈرگەندىن كېيىن ئاۋازنى چېلىشنى باشلايدۇ . <br/> <br/><b>تەرجىمىنى قانداق كۆرۈمەن؟</b> <br/>خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران چوققىسىدىكى يەر شارى سىنبەلگە (ياكى ئۈچ يۇمۇلاق چېكىتتىن شەكىللەنگەن سىنبەلگە) نى چەككەندىن كېيىن تەرجىمە بېتىنى كۆرگىلى بولىدۇ. <br/> ئەگەر سىز تېخى ھېچقانداق تەرجىمە نەشرىنى چۈشۈرمىگەن بولسىڭىز، بۇ چاغدا تەرجىمە تېكىستىنى چۈشۈرۈش ۋە باشقۇرۇش بېتىگە كىرىدۇ، لازىملىق تەرجىمىنى تاللاپ چۈشۈرگەندىن كېيىن قايتىدۇ، يەر شارى سىنبەلگە قايتا چېكىلسە تەرجىمە تېكىست بېتىگە كىرىدۇ. <br/> <br/><b>بەتكە خەتكۈچنى قانداق قوشىمەن؟</b> <br/>خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران چوققىسىدا بەش يۇلتۇز سىنبەلگىسى كۆرۈنىدۇ، ئۇنى چەككەندىن كېيىن نۆۋەتتىكى بەتنى خەتكۈچكە قوشقىلى بولىدۇ، شۇنىڭ بىلەن بىللە كاۋاك بەش يۇلتۇز ئۇيۇل بەش يۇلتۇزغا ئۆزگىرىدۇ. سىنبەلگە قايتا چېكىلسە خەتكۈچ بىكار قىلىنىدۇ. <br/> <br/><b>تېكىستنى قانداق چوڭايتىمەن؟</b> <br/>ئەرەبچە ئەسلى تېكىست بېتىدە، توغرىسىغا ئېكرانغا ئالماشقاندا خەت چوڭىيىدۇ. تەرجىمە تېكىست بېتىدە تەڭشەكلەردىن خەت چوڭلۇقىنى تاللىغىلى بولىدۇ. <br/> <br/><b>بىر ئايەتنى قانداق ھەمبەھىرلەيمەن؟</b> <br/>ئەرەبچە ئەسلى تېكىست بېتىدە، مەلۇم بىر ئايەت ئۇزۇن چېكىلسە تىل مۇھىت تىزىملىكى قاڭقىپ چىقىدۇ، ئۇنىڭدا خەتكۈچ، ھەمبەھىر، تەرجىمىنى كۆرۈش، چاپلاش تاختىسىغا كۆچۈرۈش قاتارلىق ئىقتىدارلار بار. <br/> <br/><b>مالايالام/تامىل/بىنگال/ئوردۇچە خەت نۇسخىسى ئىشلىمەيدۇ!</b> <br/>ناھايىتى ئەپسۇس، ئاندىرويىد 4.0 دىن ئىلگىرىكى نەشرىلىرىدە بۇ خەت نۇسخىلىرىنى قوللىماىدۇ، ھازىرچە ياخشىراق ھەل قىلىش چارىسى يوق. + "<b>قىرائەت ئاۋازىنى قانداق قويىدۇ؟</b> <br/> خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران ئاستىدا قىرائەت توپچىسى ۋە قىرائەت قىلغۇچىنىڭ ئىسمى كۆرۈنىدۇ. قىرائەت قىلغۇچىنىڭ ئىسمىنى چېكىپ قىرائەت قىلغۇچىلارنى تاللىغىلى بولىدۇ، چېلىش توپچىسىنى چېكىلسە نۆۋەتتىكى بەت ياكى نۆۋەتتىكى سۈرەنىڭ قىرائەت ئاۋازىنى چۈشۈرگەندىن كېيىن ئاۋازنى قويۇشنى باشلايدۇ . <br/> <br/><b>تەرجىمىنى قانداق كۆرۈمەن؟</b> <br/>خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران چوققىسىدىكى يەر شارى سىنبەلگە (ياكى ئۈچ يۇمۇلاق چېكىتتىن شەكىللەنگەن سىنبەلگە) نى چەككەندىن كېيىن تەرجىمە بېتىنى كۆرگىلى بولىدۇ. <br/> ئەگەر سىز تېخى ھېچقانداق تەرجىمە نەشرىنى چۈشۈرمىگەن بولسىڭىز، بۇ چاغدا تەرجىمە تېكىستىنى چۈشۈرۈش ۋە باشقۇرۇش بېتىگە كىرىدۇ، لازىملىق تەرجىمىنى تاللاپ چۈشۈرگەندىن كېيىن قايتىدۇ، يەر شارى سىنبەلگە قايتا چېكىلسە تەرجىمە تېكىست بېتىگە كىرىدۇ. <br/> <br/><b>بەتكە خەتكۈچنى قانداق قوشىمەن؟</b> <br/>خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران چوققىسىدا بەش يۇلتۇز سىنبەلگىسى كۆرۈنىدۇ، ئۇنى چەككەندىن كېيىن نۆۋەتتىكى بەتنى خەتكۈچكە قوشقىلى بولىدۇ، شۇنىڭ بىلەن بىللە كاۋاك بەش يۇلتۇز ئۇيۇل بەش يۇلتۇزغا ئۆزگىرىدۇ. سىنبەلگە قايتا چېكىلسە خەتكۈچ بىكار قىلىنىدۇ. <br/> <br/><b>تېكىستنى قانداق چوڭايتىمەن؟</b> <br/>ئەرەبچە ئەسلى تېكىست بېتىدە، توغرىسىغا ئېكرانغا ئالماشقاندا خەت چوڭىيىدۇ. تەرجىمە تېكىست بېتىدە تەڭشەكلەردىن خەت چوڭلۇقىنى تاللىغىلى بولىدۇ. <br/> <br/><b>بىر ئايەتنى قانداق ھەمبەھىرلەيمەن؟</b> <br/>ئەرەبچە ئەسلى تېكىست بېتىدە، مەلۇم بىر ئايەت ئۇزۇن چېكىلسە تىل مۇھىت تىزىملىكى قاڭقىپ چىقىدۇ، ئۇنىڭدا خەتكۈچ، ھەمبەھىر، تەرجىمىنى كۆرۈش، چاپلاش تاختىسىغا كۆچۈرۈش قاتارلىق ئىقتىدارلار بار. <br/> <br/><b>مالايالام/تامىل/بىنگال/ئوردۇچە خەت نۇسخىسى ئىشلىمەيدۇ!</b> <br/>ناھايىتى ئەپسۇس، ئاندىرويىد 4.0 دىن ئىلگىرىكى نەشرىلىرىدە بۇ خەت نۇسخىلىرىنى قوللىماىدۇ، ھازىرچە ياخشىراق ھەل قىلىش چارىسى يوق." قۇرئاندىن ئىزدە قۇرئان سۈرەلىرى + تولۇق نەتىجە + پۈتكۈل سەھىپەدىن ئىزدە + + + + \"%s\" نىڭ بىر نەتىجىسى: + \"%1$s\" نىڭ %2$d نەتىجىسى: + + + + قۇرئان سانلىق مەلۇماتلىرىنى سىرتقى ساقلىغۇچقا ساقلاش ئۈچۈن ئىجازىتىڭىزگە موھتاج. قۇرئان بۇ ئىجازەتنى ئالماي ئىشلەيدۇ، ئەمما ئەگەر سانلىق مەلۇماتلارنى ئۆچۈرۈپ ياكى ئەپنى ئۆچۈرۈۋەتسىڭىز، چۈشۈرگەن بارلىق بەت، ئۈن ۋە سانلىق مەلۇماتلارنىڭ ھەممىسى ئۆچۈرۈلىدۇ، شۇڭا ئىجازەت بېرەمسىز؟ + كۈچكە ئىگە بولۇشى ئۈچۈن ئەپنى قايتا قوزغىتىڭ. - سۈرىدە بايقالدى %1$s, ئايەت %2$d (بەت %3$d) - "%s" نىڭ نەتىجىلىرى تېپىلمىدى + سۈرە %1$s تېپىلدى: %2$d (%3$d-بەت) + \"%s\" نىڭ نەتىجىلىرى تېپىلمىدى سىز ئەرەبچە ئىزدەش بوغچىسىنى چۈشۈرمىگەن. ئۇنى چۈشۈرۈپ ئاندىن قايتا ئىزدەڭ. ئەرەبچە ئىزدەش ساندانىغا ئېرىش @@ -45,53 +81,93 @@ تەرجىمە مايىللىقى كۆرۈنۈش تەڭشەكلىرى ئەرەبچە ھالەت (الوضع العربي) - ئەپ ئەرەبچە ئارايۈزنى ئىشلىتىدۇ + ئەپ ئارا يۈزىگە ئەرەبچە ئىشلىتىدۇ يېڭى تەگلىك قۇلۇپ ئېكران يۆنىلىشى - قۇرئان بەتلىرىنىڭ ئېكران ئايلاندۇرۇش يۆنىلىشى + مۇقىم يۆنىلىش ھالىتىنى ئىشلىتىدۇ نۆۋەتتىكى يۆنىلىش ھالىتىگە ماسلىشىدۇ توغرىسىغا يۆنىلىش - توغرىسىغا يۆنىلىش ئىشلىتىلىدۇ - بويىغا يۆنىلىش ئىشلىتىلىدۇ + ھەمىشە توغرىسىغا يۆنىلىش ھالىتىنى ئىشلىتىدۇ + ھەمىشە بويىغا يۆنىلىش ھالىتىنى ئىشلىتىدۇ كېچە ھالىتى - قارا تەگلىك يورۇق خەت نۇسخىسى ئىشلىتىلىدۇ + قارا تەگلىك يورۇق خەت نۇسخىسى ئىشلىتىدۇ تېكىست يورۇقلۇقى كېچە ھالىتى قوزغىتىلغان ۋاقىتتىكى تېكىستنىڭ يورۇقلۇقى تەگلىك يورۇقلۇقى - كەچلىك ھالەت ئاكتىپ بولغاندا بەتنىڭ يورۇقلۇقى + كېچە ھالىتى قوزغىتىلغان ۋاقىتتىكى بەتنىڭ يورۇقلۇقى بەت ئۇچۇرىنى كۆرسەت قىرائەت قىلىۋاتقاندا بەت سانى، سۈرە ئىسمى ۋە پارە سانىنى كۆرسىتىدۇ بەلگە قاڭقىش كۆزنىكىنى كۆرسەت يېتىپ كەلگەن پارە، ھىزب قاتارلىقلارنى قاڭقىش كۆزنىكىدە كۆرسىتىدۇ. + خەتكۈچلەرنى يورۇت + ئوقۇۋاتقاندا خەتكۈچلەنگەن ئايەتلەرنى يورىتىدۇ تەرجىمە تېكىست چوڭلۇقى تەرجىمىلەر تەرجىمىلەرنى چۈشۈرۈش ۋە باشقۇرۇش تەرجىمىدىن ئىلگىرى ئايەت تەرجىمىدىن ئىلگىرى ئەرەبچە ئەسلى تېكىستنى كۆرسىتىدۇ - Dyslexia friendly font - Display translations in dyslexia friendly font + Dyslexia دوستانە خەت نۇسخىسى + تەرجىمەنى دوستانە خەت نۇسخىسىدا كۆرسىتىدۇ چۈشۈرۈش تاللانمىلىرى ئېقىم - مۇمكىن بولسا ئېقىم ئاۋازىنى ئىشلىتىدۇ - چۈشۈرۈش ئۇسۇلى + چۈشۈرگەننىڭ ئورنىغا ئېقىم ئاۋازىنى ئىشلىتىدۇ + چۈشۈرۈش مىقدارى ئالدى بىلەن غەيرى يوچۇقسىز ئاۋازنى چۈشۈرىدۇ + فۇرئان ئاۋازىنى چۈشۈرىدۇ ۋە باشقۇرىدۇ چۈشۈرۈلدى چۈشۈرگىلى بولىدۇ جەزملە - ئەپ ئورنى - ھۆججەتلەرنى قايسى sd كارتىغا ساقلايدىغانلىقىڭىزنى تاللاڭ + قۇرئان سانلىق مەلۇمات مۇندەرىجىسى + قۇرئان ھۆججەتلىرىنى قەيەرگە ساقلاش تاللىنىدۇ + خاتىرە يوللا + سازلاش خاتىرىسىنى ئىجادكارغا يوللايدۇ ئىچىدىكى ساقلىغۇچ - سىرتقى SD كارتا %1$d - ئەپ چوڭلۇقى + سىرتقى ساقلىغۇچ %1$d + نۆۋەتتىكى سانلىق مەلۇمات چوڭلۇقى ئەپ چوڭلۇقىنى ھېسابلاۋاتىدۇ ئەپ ھۆججەتلىرىنى كۆچۈرۈۋاتىدۇ ئەپ ھۆججەتلىرىنى يۆتكىيەلمىدى ئەپ ھۆججەتلىرىنى يۆتكەشكە يېتەرلىك بوشلۇق يوق - تاختا كومپيۇتېر ھالىتى + %1$d MB + قوش بەت ھالىتى توغرىسىغا ھالەتتە، ئىككى بەت يانمۇيان كۆرۈنىدۇ. توغرىسىغا ھالەتتە، بىرلا بەتنى كۆرسىتىدۇ. + ئالىي تاللانما + خەتكۈچ ئەكىر/چىقار، قۇرئان سانلىق مەلۇماتلىرى مۇندەرىجىسىنى بەلگىلەش قاتارلىق. + ئەكىر + خەتكۈچ ۋە بەلگىلەرنى ئەكىرىدۇ + چىقار + خەتكۈچ ۋە بەلگىلەرنىڭ كۆپەيتىلمىسىنى چىقىرىدۇ + CSV غا چىقار + خەتكۈچ ۋە بەلگىلەرنىڭ كۆپەيتىلمىسىنى CSV غا چىقىرىدۇ + بەت تىپى (سىناق) + ئوقۇش بېتىنىڭ تىپى تاللىنىدۇ + سۈرە ئىسمىنىڭ تەرجىمىسىنى كۆرسەت + سۈرىنىڭ تەرجىمە قىلىنغان ئىسمى + ئالدىن كۆزەت + + تېخىمۇ كۆپ تەرجىمىلەر + ئىجازەت خاتالىقى سەۋەبىدىن زاپاس ھۆججەتنى ئوقۇيالمىدى. + ئىناۋەتسىز زاپاس ھۆججەت (ياكى زاپاس ھۆججەتنى ئوقۇيالمىدى). + سانلىق مەلۇمات ئەكىر + ئەگەر بۇ ھۆججەتنى ئەكىرسىڭىز، %1$d خەتكۈچ ۋە %2$d بەلگە بار ھەممە خەتكۈچلىرىڭىزنى ئالماشتۇرۇۋېتىدۇ. ئەكىرەمدۇ؟ + مۇۋەپپەقىيەتلىك ئەكىرىلدى + سانلىق مەلۇمات چىقارغاندا خاتالىق كۆرۈلدى + سانلىق مەلۇماتنى %1$s غا چىقاردى + + ئۈستىگە + ئاستىغا + تەرجىمىنى چىقىرىۋەت + + ئاگاھلاندۇرۇش + ئاندىرويىدنىڭ چەكلىمىسى تۈپەيلىدىن، ئەگەر سىز قۇرئان سانلىق مەلۇماتلىرىنى سىرتقى SD كارتىڭىزغا ساقلىماقچى بولسىڭىز، كېيىن قۇرئان ئاندىرويىدنىڭ سانلىق مەلۇماتلارنى ئۆچۈرۈۋەتسىڭىز ياكى ئۆچۈرسىڭىز، بارلىق قۇرئان ئاندىرويىد بەتلىرى ۋە ئاۋازلىرى ئۆچۈرۈلىدۇ، ئۇلارنى قايتا چۈشۈرۈشىڭىز كېرەك. سىرتقى SD كارتىنى ئىشلىتىشنى خالامسىز؟ + ئاندىرويىد ئىشلەتكۈچى شەخسىيەت ھوقۇقىنى يۇقىرىلىتىپ ئۆزگەرتكەنلىكتىن، ھۆججەتلەرنى ئەپ مۇندەرىجىسىنىڭ سىرتىغا كۆچۈرگەندە قۇرئان ئەپنىڭ سانلىق مەلۇماتنى زىيارەت قىلىشى كەلگۈسىدىكى ئاندىرويىد نەشرىدە توختىتىلىشى مۇمكىن. بۇ يولنى راستىنلا ئىشلىتەمسىز؟ + ئەپ تەڭشىكىدە ئىجازەت بېرىڭ + + بۇ ئايەتنىڭ تەپسىرى %d (چېكىلسە يايىدۇ) ئايەتنىڭ تەپسىرىسىدە بار. + يېڭىلانما بار تەرجىمە ئۈچۈن بىر يېڭىلانما بار. تەرجىمە ئېكرانىنى ھازىرلا زىيارەت قىلامسىز؟ @@ -106,20 +182,23 @@ پارە بەت سۈرە - %1$d ئايەت - %1$s - سۈرە - %1$s -سۈرە، %2$d - ئايەت - سۈرە %1$: %2$d + %1$d-ئايەت + %1$s-ئايەت %2$s، %3$s-پارە + سۈرە %1$s + سۈرە %1$s، %2$d-ئايەت + سۈرە %1$s: %2$d يۈكلەۋاتىدۇ… ۋاز كېچىۋاتىدۇ… %1$s -پارە - %2$s -پارە، %1$s -بەت + مەنزىل %1$s + %1$s-بەت، %2$s-پارە زۆرۈر ھۆججەتلەر زۆرۈر ھۆججەتلەر + SD كارتىنى تاپالمىدى. ئۇنى قىستۇرۇپ ئاندىن قايتا سىناڭ. چۈشۈرۈش مۇۋەپپەقىيەتلىك چۈشۈرۈشنى بىر تەرەپ قىلىۋاتىدۇ… چۈشۈرەلمىدى @@ -130,7 +209,7 @@ ھۆججەت بۇزۇلغان، قايتا چۈشۈرۈشنى سىناۋاتىدۇ تور خاتالىقى، داۋاملاشتۇرۇشنى سىناۋاتىدۇ… چۈشۈرۈشتىن ۋاز كەچتى - سىز wifi تورىدا ئەمەس. سانلىق مەلۇماتنى چۈشۈرىۋېرەمسىز؟ + سىز wifi تورىدا ئەمەس. سانلىق مەلۇماتنى چۈشۈرۈۋېرەمسىز؟ بىر تەرەپ قىلىۋاتقىنى %1$d / %2$d @@ -138,6 +217,9 @@ ۋاز كەچ ھەمبەھىر ۋە تەرجىمە ئىقتىدارىنى قوللاش ئۈچۈن بىر ياكى ئىككى كىچىك ھۆججەت چۈشۈرۈشىمىز كېرەك. ھازىر چۈشۈرەمدۇ؟ + + قۇرئان ئاندىرويىدنىڭ ئۇقتۇرۇش يوللىشىغا يول قويامسىز؟ بۇ پەقەت يېڭىلاشنى چۈشۈرۈش ياكى ئاۋاز ھۆججىتى ئاپتوماتىك يېڭىلانغاندا ئاگاھلاندۇرۇش ئۈچۈن ئىشلىتىلىدۇ. + تەرجىمىنى چىقىرىۋېتەمدۇ؟ سىز %1$s نى راسلا چىقىرىۋېتەمسىز؟ @@ -146,6 +228,9 @@ سىز تېخى ھېچقانداق تەرجىمە/تەپسىر چۈشۈرمىگەن. تەرجىمىگە ئېرىش + + ئەگەر سوئالىڭىزنىڭ تۆۋەندە جاۋابى بولمىسا، سىز quranandroid@gmail.com ئېلخەت ئادرېسقا قوللاش ھەققىدە خەت يازسىڭىز بولىدۇ. بىز نۇرغۇن ئېلخەت تاپشۇرۇۋالىمىز، شۇڭلاشقا ھەممە ئېلخەتكە جاۋاب قايتۇرالىشىمىز ناتايىن. + مەككە مەدىنە @@ -163,13 +248,29 @@ بەلگە تەھرىر يېڭى بەلگە بەلگە قوشۇلمىدى + + بىر تۈر ئۆچۈرۈلدى + %d تۈر ئۆچۈرۈلدى + + يېنىۋال + + يېقىنقى بەتلەر بەلگە ئاتى + بەلگە ئاتى بوش قالمايدۇ! + بەلگە ئاتى مەۋجۇت! خەتكۈچ يوق + + + قۇرئان ئاۋاز ھۆججىتى يېڭىلاش + بىر قانچە قۇرئان ئاۋاز ھۆججىتى يېڭىلاندى. قۇرئان بۇ ھۆججەتلەرنىڭ كۆپەيتىلگەن نۇسخىسىنى چىقىرىۋەتتى، بۇنداق بولغاندا ئەڭ يېڭى نەشرىنى كېلەر قېتىم قايتا قويسىڭىز چۈشۈرگىلى بولىدۇ. + + + قۇرئان كەرىم ئىنىسكىلوپىدىيىسى قويۇش ۋاقتىنچە توختاش @@ -218,6 +319,10 @@ سوئالىڭىز يۇقىرىدىكى جاۋاب بولمىسا, قوللايدىغان quranandroid@gmail.com ئېلېكترونلۇق خەت ئەۋەتسىڭىز بولىدۇ. شۇنىڭغا دىققەت قىلىڭكى, بىز نۇرغۇن ئېلېكترونلۇق خەتلەرنى قوبۇل قىلالايمىز, شۇڭا بىز ئۇلارنىڭ ھەممىسىگە جاۋاب بېرەلمەسلىكى مۇمكىن. ئاۋازلىق ھۆججەتلەر يېڭىلاندى قوش بەت مايىللىقى + قوش بەت ھالىتىدىكى قۇرئان ۋە تەرجىمە + تەرجىمە بار قوش بەت ھالىتىدە ، قۇرئان بېتى ۋە تەرجىمە كۆرۈنىدۇ + + خەتكۈچ ئىسىملىرى بوش ئەمەس! خەتكۈچ ئىسمى ئاللىبۇرۇن مەۋجۇت! بەت ئۈستى @@ -232,7 +337,7 @@ كېيىنكى ئويناشنى باشلاڭ: قۇرئان چۈشۈرۈش - قۇرئان تىلاۋەت قىلىش + قۇرئان قىرائىتى سانلىق مەلۇمات ئېكسپورتىدا خاتالىق ئەمەلدىن قالدۇرۇش بىر قانچە قۇرئان ئاۋاز ھۆججىتى يېڭىلاندى. «ئاندىرويىد ئۈچۈن قۇرئان» بۇ ھۆججەتلەرنىڭ كۆپەيتىلگەن نۇسخىسىنى ئېلىۋەتتى ، بۇنداق بولغاندا ئەڭ يېڭى نەشرىنى كېيىنكى قېتىم قايتا قويسىڭىز بولىدۇ. diff --git a/app/src/main/res/values-ug/sura_names.xml b/app/src/main/res/values-ug/sura_names.xml new file mode 100644 index 0000000000..eb239b2098 --- /dev/null +++ b/app/src/main/res/values-ug/sura_names.xml @@ -0,0 +1,119 @@ + + + + فاتىھە + بەقەرە + ئال ئىمران + نىسا + مائىدە + ئەنئام + ئەئراف + ئەنفال + تەۋبە + يۇنۇس + ھۇد + يۈسۈف + رەئىد + ئىبراھىم + ھىجر + نەھل + ئىسرا + كەھف + مەريەم + تاھا + ئەنبىيا + ھەج + مۆئمىنۇن + نۇر + فۇرقان + شۇئەرا + نەمل + قەسەس + ئەنكەبۇت + رۇم + لوقمان + سەجدە + ئەھزاب + سەبەئ + فاتىر + ياسىن + ساففات + ساد + زۇمەر + غافىر + فۇسسىلەت + شۇرا + زۇخرۇف + دۇخان + جاسىيە + ئەھقاف + مۇھەممەد + فەتىھ + ھۇجۇرات + قاف + زارىيات + تۇر + نەجم + قەمەر + رەھمان + ۋاقىئە + ھەدىد + مۇجادەلە + ھەشر + مۇمتەھىنە + سەپ + جۇمۇئە + مۇنافىقۇن + تەغابۇن + تەلاق + تەھرىم + مۇلك + قەلەم + ھاققە + مائارىج + نۇھ + جىن + مۇززەممىل + مۇددەسسىر + قىيامەت + ئىنسان + مۇرسەلات + نەبە + نازىئات + ئەبەسە + تەكۋىر + ئىنفىتار + مۇتەففىفىن + ئىنشىقاق + بۇرۇج + تارىق + ئەئلا + غاشىيە + ھىجر + بەلەد + شەمس + لەيل + زۇھا + ئىنشىراھ + تىن + ئەلەق + قەدر + بەييىنە + زەلزەلە + ئادىيات + قارىئە + تەكاسۇر + ئەسر + ھۇمەزە + فىل + قۇرەيش + مائۇن + كەۋسەر + كافىرون + نەسر + مەسەد + ئىخلاس + فەلەق + ناس + + From 01030846c04560f2b8390969e1edf29565553a34 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 04:20:53 +0000 Subject: [PATCH 175/258] Update dependency io.grpc:grpc-okhttp to v1.59.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99cbc70f3e..5c23554610 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" # recitations -grpcOkhttpVersion = "1.59.0" +grpcOkhttpVersion = "1.59.1" googleAuthVersion = "1.20.0" googleCloudSpeechVersion = "4.25.0" From b55579e62f7e8900660ef545a5d627b3a65a0d81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:59:37 +0000 Subject: [PATCH 176/258] Update dependency gradle to v8.5 --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index 744c64d127..3499ded5c1 100644 --- a/build-logic/gradle/wrapper/gradle-wrapper.properties +++ b/build-logic/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b765f8051ef9d0a6055ff8e46073d8..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3fa8f862f7..1af9e0930b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 15870f93f2a667253614ec804b00d75a89423de2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:59:41 +0000 Subject: [PATCH 177/258] Update actions/setup-java action to v4 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/post_merge.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 44320d8b27..66ce3a8e23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 17 @@ -59,7 +59,7 @@ jobs: uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 17 @@ -86,7 +86,7 @@ jobs: uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 17 @@ -116,7 +116,7 @@ jobs: uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 17 diff --git a/.github/workflows/post_merge.yml b/.github/workflows/post_merge.yml index 2141d30dc8..288e89620a 100644 --- a/.github/workflows/post_merge.yml +++ b/.github/workflows/post_merge.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu java-version: 17 From d3442ad4d935e83ffdd431c37e021d8e128a5b7f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:51:42 +0000 Subject: [PATCH 178/258] Update dependency androidx.media:media to v1.7.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c23554610..6b88b71f84 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -36,7 +36,7 @@ androidxDynamicAnimationVersion = "1.0.0" androidxFragmentVersion = "1.6.2" androidxJunitExtVersion = "1.1.5" androidxLocalBroadcastVersion = "1.1.0" -androidxMediaVersion = "1.6.0" +androidxMediaVersion = "1.7.0" androidxPreferencesVersion = "1.2.1" androidxRecyclerViewVersion = "1.3.2" androidxSwipeRefreshVersion = "1.1.0" From 73a94a2924c443a09c3e88797ef6ffd9e5d6344a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:51:46 +0000 Subject: [PATCH 179/258] Update dependency androidx.work:work-runtime-ktx to v2.9.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c23554610..2250dd8199 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ androidxRecyclerViewVersion = "1.3.2" androidxSwipeRefreshVersion = "1.1.0" androidxPagingVersion = "3.2.1" androidxPagingComposeVersion = "3.2.1" -androidxWorkManagerVersion = "2.8.1" +androidxWorkManagerVersion = "2.9.0" # firebase firebaseAnalyticsVersion = "21.5.0" From b6ff60411dd0611fcd9007090d09a182a0971f16 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:35:27 +0000 Subject: [PATCH 180/258] Update agp to v8.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67229a918c..60bccc600f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.1.4" +agp = "8.2.0" kotlin = "1.9.20" ksp = "1.9.20-1.0.14" From bfa31f23e0d26b43eff2091ebd292e5bef1f8256 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:35:31 +0000 Subject: [PATCH 181/258] Update dependency com.android.tools.build:gradle to v8.2.0 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 7e87c66314..24f562f5b6 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.1.4") + compileOnly("com.android.tools.build:gradle:8.2.0") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } From a1fb58b0c4e854b04588d4a29476e41d9f0755b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 23:56:42 +0000 Subject: [PATCH 182/258] Update daggerVersion to v2.49 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67229a918c..eac552a9fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ okhttpBomVersion = "4.12.0" anvil = "2.4.8" coroutinesVersion = "1.7.3" crashlytics = "2.9.9" -daggerVersion = "2.48.1" +daggerVersion = "2.49" dnsjavaVersion = "2.1.9" errorprone = "3.1.0" errorproneCoreVersion = "2.23.0" From 167508ffc9de5d6978576d414b819703ce285cbe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 2 Dec 2023 02:16:37 +0000 Subject: [PATCH 183/258] Update dependency org.mockito:mockito-core to v5.8.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67229a918c..c0d9514a59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ googleCloudSpeechVersion = "4.25.0" junitVersion = "4.13.2" espressoVersion = "3.5.1" truthVersion = "1.1.5" -mockitoVersion = "5.7.0" +mockitoVersion = "5.8.0" robolectricVersion = "4.11.1" [libraries] From d0f2607b47c615b0f660009feae5bb6c1626469e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:04:09 +0000 Subject: [PATCH 184/258] Update dependency com.google.cloud:google-cloud-speech to v4.26.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 21877d4ad6..09e83325fc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.59.1" googleAuthVersion = "1.20.0" -googleCloudSpeechVersion = "4.25.0" +googleCloudSpeechVersion = "4.26.0" # testing junitVersion = "4.13.2" From 7113535d5fb10ee942a023df830b42b2aba3083a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:02:11 +0000 Subject: [PATCH 185/258] Update dependency io.grpc:grpc-okhttp to v1.60.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 09e83325fc..e1eb8c26db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" # recitations -grpcOkhttpVersion = "1.59.1" +grpcOkhttpVersion = "1.60.0" googleAuthVersion = "1.20.0" googleCloudSpeechVersion = "4.26.0" From a88fe9e66154b642b41bb206c5a642dc53f51f47 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Tue, 5 Dec 2023 23:29:53 +0400 Subject: [PATCH 186/258] Update Kotlin to 1.9.21 --- build-logic/convention/build.gradle.kts | 2 +- gradle/libs.versions.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 24f562f5b6..441808f16c 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:8.2.0") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1eb8c26db..bca99ae7af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] agp = "8.2.0" -kotlin = "1.9.20" -ksp = "1.9.20-1.0.14" +kotlin = "1.9.21" +ksp = "1.9.21-1.0.15" # required within the Gradle convention plugins - not unused -compose-compiler = "1.5.4" +compose-compiler = "1.5.5-dev-k1.9.21-163bb051fe5" composeBomVersion = "2023.10.01" okhttpBomVersion = "4.12.0" From 2977e4e5e503f4237358bd05582c2f545010a6ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:18:09 +0000 Subject: [PATCH 187/258] Update sqldelight to v2.0.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bca99ae7af..f1ee60f531 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ okioVersion = "3.6.0" retrofitVersion = "2.9.0" rxandroidVersion = "3.0.2" rxjavaVersion = "3.1.8" -sqldelight = "2.0.0" +sqldelight = "2.0.1" timberVersion = "5.0.1" # androidx library versions From 473d15174da270adfed78e3b8a16d72005f3ad82 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Wed, 6 Dec 2023 22:12:56 +0400 Subject: [PATCH 188/258] Update app/src/main/res/values-ug/sura_names.xml Fix broken resource file --- app/src/main/res/values-ug/sura_names.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-ug/sura_names.xml b/app/src/main/res/values-ug/sura_names.xml index eb239b2098..73d901f9b5 100644 --- a/app/src/main/res/values-ug/sura_names.xml +++ b/app/src/main/res/values-ug/sura_names.xml @@ -116,4 +116,4 @@ فەلەق ناس - + From 3c4be62293e9b4df0cffa5539e7cb5705f43ad55 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Wed, 6 Dec 2023 22:30:36 +0400 Subject: [PATCH 189/258] Remove duplicates --- app/src/main/res/values-ug/strings.xml | 83 +++----------------------- 1 file changed, 7 insertions(+), 76 deletions(-) diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index 22f49d8444..ce38c24faf 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -11,7 +11,6 @@ ھۆججەتلەرنى چۈشۈرۈشنى كۈتۈڭ. (داۋاملاشتۇرۇشنى قوللايدۇ) بىر تەرەپ قىلىۋاتىدۇ… بىز ھەققىدە - باشقا ئەپلەر تەڭشەكلەر ئاخىرقى بەت خەتكۈچلەر @@ -20,8 +19,6 @@ تەرتىپلە قوشۇلغان چېسلا قۇرئاندىكى ئورنى - بەلگە بويىچە گۇرۇپپىلا - چېسلانى كۆرسەت تەرجىمە كۆرسەت قۇرئاننى كۆرسەت ياردەم @@ -29,7 +26,6 @@ تەرجىمىگە ئېرىش يۆتكەل بەتكە يۆتكەل - سەل كۈتۈڭ… ئاندىرويىد قۇرئان ھەقسىز قۇرئان ئەپى. نامازلىرىڭىزدا تۆھپىكارلارغا دۇئا قىلىشنى ئۇنۇتماڭ. @@ -55,8 +51,6 @@ "<b>قىرائەت ئاۋازىنى قانداق قويىدۇ؟</b> <br/> خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران ئاستىدا قىرائەت توپچىسى ۋە قىرائەت قىلغۇچىنىڭ ئىسمى كۆرۈنىدۇ. قىرائەت قىلغۇچىنىڭ ئىسمىنى چېكىپ قىرائەت قىلغۇچىلارنى تاللىغىلى بولىدۇ، چېلىش توپچىسىنى چېكىلسە نۆۋەتتىكى بەت ياكى نۆۋەتتىكى سۈرەنىڭ قىرائەت ئاۋازىنى چۈشۈرگەندىن كېيىن ئاۋازنى قويۇشنى باشلايدۇ . <br/> <br/><b>تەرجىمىنى قانداق كۆرۈمەن؟</b> <br/>خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران چوققىسىدىكى يەر شارى سىنبەلگە (ياكى ئۈچ يۇمۇلاق چېكىتتىن شەكىللەنگەن سىنبەلگە) نى چەككەندىن كېيىن تەرجىمە بېتىنى كۆرگىلى بولىدۇ. <br/> ئەگەر سىز تېخى ھېچقانداق تەرجىمە نەشرىنى چۈشۈرمىگەن بولسىڭىز، بۇ چاغدا تەرجىمە تېكىستىنى چۈشۈرۈش ۋە باشقۇرۇش بېتىگە كىرىدۇ، لازىملىق تەرجىمىنى تاللاپ چۈشۈرگەندىن كېيىن قايتىدۇ، يەر شارى سىنبەلگە قايتا چېكىلسە تەرجىمە تېكىست بېتىگە كىرىدۇ. <br/> <br/><b>بەتكە خەتكۈچنى قانداق قوشىمەن؟</b> <br/>خالىغان قۇرئان بېتىنى ئېچىپ، بارماق ئېكرانغا بوش تەگكەندە، ئېكران چوققىسىدا بەش يۇلتۇز سىنبەلگىسى كۆرۈنىدۇ، ئۇنى چەككەندىن كېيىن نۆۋەتتىكى بەتنى خەتكۈچكە قوشقىلى بولىدۇ، شۇنىڭ بىلەن بىللە كاۋاك بەش يۇلتۇز ئۇيۇل بەش يۇلتۇزغا ئۆزگىرىدۇ. سىنبەلگە قايتا چېكىلسە خەتكۈچ بىكار قىلىنىدۇ. <br/> <br/><b>تېكىستنى قانداق چوڭايتىمەن؟</b> <br/>ئەرەبچە ئەسلى تېكىست بېتىدە، توغرىسىغا ئېكرانغا ئالماشقاندا خەت چوڭىيىدۇ. تەرجىمە تېكىست بېتىدە تەڭشەكلەردىن خەت چوڭلۇقىنى تاللىغىلى بولىدۇ. <br/> <br/><b>بىر ئايەتنى قانداق ھەمبەھىرلەيمەن؟</b> <br/>ئەرەبچە ئەسلى تېكىست بېتىدە، مەلۇم بىر ئايەت ئۇزۇن چېكىلسە تىل مۇھىت تىزىملىكى قاڭقىپ چىقىدۇ، ئۇنىڭدا خەتكۈچ، ھەمبەھىر، تەرجىمىنى كۆرۈش، چاپلاش تاختىسىغا كۆچۈرۈش قاتارلىق ئىقتىدارلار بار. <br/> <br/><b>مالايالام/تامىل/بىنگال/ئوردۇچە خەت نۇسخىسى ئىشلىمەيدۇ!</b> <br/>ناھايىتى ئەپسۇس، ئاندىرويىد 4.0 دىن ئىلگىرىكى نەشرىلىرىدە بۇ خەت نۇسخىلىرىنى قوللىماىدۇ، ھازىرچە ياخشىراق ھەل قىلىش چارىسى يوق." قۇرئاندىن ئىزدە قۇرئان سۈرەلىرى - تولۇق نەتىجە - پۈتكۈل سەھىپەدىن ئىزدە @@ -99,8 +93,8 @@ قىرائەت قىلىۋاتقاندا بەت سانى، سۈرە ئىسمى ۋە پارە سانىنى كۆرسىتىدۇ بەلگە قاڭقىش كۆزنىكىنى كۆرسەت يېتىپ كەلگەن پارە، ھىزب قاتارلىقلارنى قاڭقىش كۆزنىكىدە كۆرسىتىدۇ. - خەتكۈچلەرنى يورۇت - ئوقۇۋاتقاندا خەتكۈچلەنگەن ئايەتلەرنى يورىتىدۇ + خەتكۈچنى گەۋدىلەندۈرىدۇ + ئايەتلەرنى گەۋدىلەندۈرىدۇ تەرجىمە تېكىست چوڭلۇقى تەرجىمىلەر تەرجىمىلەرنى چۈشۈرۈش ۋە باشقۇرۇش @@ -275,89 +269,26 @@ قويۇش ۋاقتىنچە توختاش باشقا ئەپلەر - گۇرۇپپا - ۋاقىتنى كۆرسىتىش - ساقلاپ تۇرۇڭ… - ئاندىرويىد ئۈچۈن قۇرئان گىرىپتار قۇرئان ئىلتىماس قىلىش. دۇئالىرىڭىزدا تۆھپىكارلىقنى ئۇنتۇپ قالماڭ. - سانلىق مەلۇمات مەنبەسى - رەسىملەر - قۇرئان رەسىملىرى پادىشاھ فەھدتىن خەت نۇسخىنى ئاساس قىلغان - SHL Info Systems دېتالى ئۈچۈن قۇرئان رەسىملىرى (ئىجازەت بىلەن) - قالون رەسىملىرى Nous Memes Editions Et Diffusion (تۇنىس) نىڭ رۇخسىتى بىلەن ئىشلىتىلىدۇ - گالستۇكى MP3 ئوقۇتقۇچىسى پائالىيەت - ئېلېكترونلۇق قۇرئاننىڭ تۈرى - عثمانى نۇسخىسى ۋە ئەرەبچە تەپسىرلەر - نۇرغۇن تىللارغا تەرجىمە - بەزى تىللارغا تەرجىمە - نورحرا خەت نۇسخىسى ۋە مفتى تقى تەرجىمىسى - ئوچۇق كود تۈرلىرى - باشقىلار - تۆھپىكار - ئاندىرويىد ئۈچۈن قۇرئاننى تەرەققىي قىلدۇرۇشقا تۆھپە قوشقانلارنىڭ تىزىملىكى - تولۇق نەتىجىسى - پۈتكۈل مصحف ئىزدەڭ - بۇنىڭ كۈچكە ئىگە بولۇشى ئۈچۈن بۇ دېتالنى قايتا قوزغىتىڭ. - خەتكۈچنى گەۋدىلەندۈرىدۇ - ئايەتلەرنى گەۋدىلەندۈرىدۇ - ئاۋاز باشقۇرۇش ۋە چۈشۈرۈش - خاتىرىسىنى ئەۋەتىش - %1$d م ب - تېخىمۇ كۆپ تەڭشەكلەر - ئىمپورت - خەتكۈچ ۋە خەتكۈچلەرنى ئىمپورت قىلىڭ - ئېكسپورت - خەتكۈچ ۋە خەتكۈچلەرنىڭ كۆپەيتىلگەن نۇسخىسىنى ئېكسپورت قىلىڭ - بەت تىپى (تەجرىبە) - ئوقۇش بېتىنىڭ تۈرىنى تاللاڭ - تەرجىمىسىنى كۆرسىتىڭ سۈرە - مەسىلەن - تېخىمۇ كۆپ تەرجىمىلەر - ئاگاھلاندۇرۇش + بەلگە بويىچە گۇرۇپپىلا + چېسلانى كۆرسەت + سەل كۈتۈڭ… + تولۇق نەتىجە + پۈتكۈل سەھىپەدىن ئىزدە ئالدىنقى - يېقىنقى بەتلەر توختا - سوئالىڭىز يۇقىرىدىكى جاۋاب بولمىسا, قوللايدىغان quranandroid@gmail.com ئېلېكترونلۇق خەت ئەۋەتسىڭىز بولىدۇ. شۇنىڭغا دىققەت قىلىڭكى, بىز نۇرغۇن ئېلېكترونلۇق خەتلەرنى قوبۇل قىلالايمىز, شۇڭا بىز ئۇلارنىڭ ھەممىسىگە جاۋاب بېرەلمەسلىكى مۇمكىن. - ئاۋازلىق ھۆججەتلەر يېڭىلاندى قوش بەت مايىللىقى قوش بەت ھالىتىدىكى قۇرئان ۋە تەرجىمە تەرجىمە بار قوش بەت ھالىتىدە ، قۇرئان بېتى ۋە تەرجىمە كۆرۈنىدۇ - خەتكۈچ ئىسىملىرى بوش ئەمەس! - خەتكۈچ ئىسمى ئاللىبۇرۇن مەۋجۇت! بەت ئۈستى - تەرجىمەنى ئۆچۈرۈڭ - يۇقىرىدا يۆتكەڭ - تۆۋەنگە يۆتكەڭ - ئىمپورت مۇۋەپپەقىيەتلىك - سانلىق مەلۇمات ئىمپورت - %1$s - آية %2$s، جزء %3$s ئىلتىماس قىلىڭ ئىلتىماس قىلىڭ ۋە قويۇش كېيىنكى ئويناشنى باشلاڭ: قۇرئان چۈشۈرۈش قۇرئان قىرائىتى - سانلىق مەلۇمات ئېكسپورتىدا خاتالىق - ئەمەلدىن قالدۇرۇش - بىر قانچە قۇرئان ئاۋاز ھۆججىتى يېڭىلاندى. «ئاندىرويىد ئۈچۈن قۇرئان» بۇ ھۆججەتلەرنىڭ كۆپەيتىلگەن نۇسخىسىنى ئېلىۋەتتى ، بۇنداق بولغاندا ئەڭ يېڭى نەشرىنى كېيىنكى قېتىم قايتا قويسىڭىز بولىدۇ. - قۇرئان ۋە تەرجىمە قوش ھالەتتە - تەرجىمە بىلەن قوش بەت شەكلىدە ، قۇرئان بېتى ۋە تەرجىمىسى كۆرسىتىلدى - ئاچقۇچىلارغا خاتالىق خاتىرىسىنى ئەۋەتىڭ - ئىجازەت خاتالىقى سەۋەبىدىن زاپاس ھۆججەتنى ئوقۇيالمىدى. - ئىناۋەتسىز زاپاس ھۆججەت (ياكى زاپاس ھۆججەتنى ئوقۇيالمايدۇ). - خەتكۈچلەرنى ئەكىرىش / چىقىرىش ، قۇرئان سانلىق مەلۇمات مۇندەرىجىسى قاتارلىقلارنى تەڭشەش. - سۈرىنىڭ ئىسمى تەرجىمە قىلىنغان - %1$s غا ئېكسپورت قىلىنغان سانلىق مەلۇمات - ئەگەر بۇ ھۆججەتنى ئىمپورت قىلسىڭىز ، ئۇ بارلىق خەتكۈچلىرىڭىزنى %1$d خەتكۈچ ۋە %2$d خەتكۈچكە ئالماشتۇرىدۇ. ئىمپورتمۇ؟ پەقەت يۇقىرىدىكى ئايەتلەرنىلا ئاڭلاڭ بىر يۈرۈش ئايەتلەرنى ئاڭلاڭ: ھەر بىر ئايەتنى ئاڭلاڭ: - SD كارتىنى تاپالمىدى. ئۇنى ئورنىتىپ قايتا سىناڭ. - ئىلتىماس تەڭشەكلىرىدە ئىجازەت بېرىڭ - بۇ ئايەتنىڭ تەفىرسى %d -ئايەتنىڭ تەفسىر بىلەن ئۆز ئىچىگە ئېلىنغان (كېڭەيتىش ئۈچۈن چېكىڭ). - ئاندىرويىد چەكلىمىسى تۈپەيلىدىن ، ئەگەر سىز قۇرئان سانلىق مەلۇماتلىرىنى سىرتقى SD كارتىڭىزغا قويماقچى بولسىڭىز ، ئاندىن قۇرئان ئاندىرويىد ئۈچۈن سانلىق مەلۇماتلارنى ئۆچۈرۈۋەتسىڭىز ياكى ئۆچۈرسىڭىز ، قۇرئاننىڭ بارلىق ئاندىرويىد بەتلىرى ۋە ئاۋازلىرى ئۆچۈرۈلىدۇ ، ئۇلارنى قايتا چۈشۈرۈشىڭىز كېرەك. سىرتقى SD كارتىنى ئىشلىتىشنى خالامسىز؟ - ئابونتلارنىڭ شەخسى مەخپىيەتلىكىنى ئاشۇرۇش ئۈچۈن ئاندىرويىد ئۆزگىرىشى سەۋەبىدىن ، ھۆججەت مۇندەرىجىسىنىڭ سىرتىدىكى ھۆججەتلەرنى كۆچۈرگەندە «ئاندىرويىد ئۈچۈن قۇرئان» نىڭ ئاندىرويىدنىڭ كەلگۈسى نەشرىدىكى سانلىق مەلۇماتلىرىنى زىيارەت قىلىشىنى توختىتىشى مۇمكىن. بۇ يولنى ئىشلىتىشنى خالامسىز؟ - «ئاندىرويىد ئۈچۈن قۇرئان» سىزنىڭ سانلىق مەلۇماتلىرىڭىزنى سىرتقى ساقلاشقا ساقلىشىڭىزغا رۇخسەت قىلىدۇ. «ئاندىرويىد ئۈچۈن قۇرئان» بۇ ئىجازەتسىز ئىشلەيدۇ ، ئەمما سانلىق مەلۇماتنى ئۆچۈرسىڭىز ياكى ئەپنى ئۆچۈرۈۋەتسىڭىز ، چۈشۈرۈلگەن بارلىق بەتلەر ، ئاۋاز ۋە سانلىق مەلۇماتلار ئۆچۈرۈلىدۇ. «ئاندىرويىد ئۈچۈن قۇرئان» ئىجازەت بېرەمسىز؟ - كىڭ سەئۇد ئۇنۋېرسىتىتى ئېلېكترونلۇق مصحف تۈرى بۇ دېتالنىڭ ئەرەبچە تەپسىرلەر نىڭ مەنبەسى ۋە ھەر خىل تىللارنىڭ تەرجىمىسى. ئۇ يەنە دوكتور ئايمان سۇۋەيدنىڭ دېكلاماتسىيە قىلىشىنىڭ مەنبەسى. From 294af52ba0eff0b0639f32d9061a169f6a364bd5 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 8 Dec 2023 22:26:19 +0400 Subject: [PATCH 190/258] Use Sqldelight for translations --- app/build.gradle | 1 + .../common/LocalTranslationDisplaySort.kt | 2 + .../dao/translation/Translation.kt | 3 +- .../dao/translation/TranslationItem.kt | 21 ++- .../androidquran/data/QuranDataProvider.java | 7 +- .../database/TranslationsDBAdapter.kt | 176 ++++++------------ .../database/TranslationsDBHelper.kt | 112 ----------- .../quran/ayahtracker/AyahTrackerItem.kt | 2 +- .../quran/ayahtracker/AyahTrackerPresenter.kt | 2 +- .../ayahtracker/AyahTranslationTrackerItem.kt | 2 +- .../translation/BaseTranslationPresenter.kt | 49 +++-- .../InlineTranslationPresenter.java | 7 +- .../TranslationManagerPresenter.java | 13 +- .../translation/TranslationPresenter.kt | 4 +- .../TranslationListCallback.kt | 10 + .../TranslationListPresenter.kt | 22 +++ .../labs/androidquran/ui/PagerActivity.java | 4 +- .../ui/TranslationManagerActivity.java | 15 +- .../ui/fragment/AyahTranslationFragment.kt | 28 +-- .../ui/fragment/TabletFragment.java | 2 +- .../ui/fragment/TranslationFragment.java | 2 +- .../androidquran/ui/helpers/AyahTracker.kt | 2 +- .../ui/translation/TranslationView.java | 5 +- .../ui/util/TranslationsSpinnerAdapter.java | 5 +- .../quran/labs/androidquran/util/ShareUtil.kt | 3 +- .../view/InlineTranslationView.kt | 5 +- .../BaseTranslationPresenterTest.kt | 4 +- common/translation/build.gradle.kts | 37 ++++ .../data/TranslationsDataSource.kt | 64 +++++++ .../translation/di/TranslationDataModule.kt | 29 +++ .../mapper/LocalTranslationMapper.kt | 33 ++++ .../translation/model}/LocalTranslation.kt | 4 +- .../quran/mobile/translation/databases/1.db | Bin 0 -> 8192 bytes .../quran/mobile/translation/databases/2.db | Bin 0 -> 8192 bytes .../quran/mobile/translation/databases/3.db | Bin 0 -> 8192 bytes .../quran/mobile/translation/databases/4.db | Bin 0 -> 8192 bytes .../quran/mobile/translation/databases/5.db | Bin 0 -> 8192 bytes .../quran/mobile/translation/migrations/1.sqm | 26 +++ .../quran/mobile/translation/migrations/2.sqm | 18 ++ .../quran/mobile/translation/migrations/3.sqm | 19 ++ .../quran/mobile/translation/migrations/4.sqm | 20 ++ .../quran/mobile/translation/translations.sq | 29 +++ settings.gradle.kts | 1 + 43 files changed, 465 insertions(+), 323 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt create mode 100644 app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListCallback.kt create mode 100644 app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt create mode 100644 common/translation/build.gradle.kts create mode 100644 common/translation/src/main/kotlin/com/quran/mobile/translation/data/TranslationsDataSource.kt create mode 100644 common/translation/src/main/kotlin/com/quran/mobile/translation/di/TranslationDataModule.kt create mode 100644 common/translation/src/main/kotlin/com/quran/mobile/translation/mapper/LocalTranslationMapper.kt rename {app/src/main/java/com/quran/labs/androidquran/common => common/translation/src/main/kotlin/com/quran/mobile/translation/model}/LocalTranslation.kt (89%) create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/1.db create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/2.db create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/3.db create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/4.db create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/5.db create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm create mode 100644 common/translation/src/main/sqldelight/com/quran/mobile/translation/translations.sq diff --git a/app/build.gradle b/app/build.gradle index 6961ea4929..4cd569585d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -122,6 +122,7 @@ dependencies { implementation project(path: ':common:recitation') implementation project(path: ':common:search') implementation project(path: ':common:toolbar') + implementation project(path: ':common:translation') implementation project(path: ':common:upgrade') implementation project(path: ':common:ui:core') 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 index 08870eefb2..65413ca1e9 100644 --- a/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslationDisplaySort.kt +++ b/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslationDisplaySort.kt @@ -1,5 +1,7 @@ package com.quran.labs.androidquran.common +import com.quran.mobile.translation.model.LocalTranslation + class LocalTranslationDisplaySort : Comparator { override fun compare(first: LocalTranslation, second: LocalTranslation): Int { return first.displayOrder.compareTo(second.displayOrder) 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 1d01051d1a..bb155f5755 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,8 +14,7 @@ data class Translation(val id: Int, val saveTo: String, val languageCode: String, val translator: String? = "", - @Json(name = "translatorForeign") val translatorNameLocalized: String? = "", - val displayOrder: Int = -1) { + @Json(name = "translatorForeign") val translatorNameLocalized: String? = "") { 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 b5ce16c63a..ecb9ef06d7 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,5 +1,7 @@ package com.quran.labs.androidquran.dao.translation +import com.quran.mobile.translation.model.LocalTranslation + data class TranslationItem @JvmOverloads constructor(val translation: Translation, val localVersion: Int = 0, val displayOrder: Int = -1) : TranslationRowData { @@ -16,7 +18,22 @@ 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) + + fun withLocalVersionAndDisplayOrder(newVersion: Int, displayOrder: Int) = this.copy(localVersion = newVersion, displayOrder = displayOrder) + + fun asLocalTranslation(): LocalTranslation { + return LocalTranslation( + id = translation.id.toLong(), + filename = translation.fileName, + name = translation.displayName, + translator = translation.translator, + translatorForeign = translation.translatorNameLocalized, + url = translation.fileUrl, + languageCode = translation.languageCode, + version = localVersion, + minimumVersion = translation.minimumVersion, + displayOrder = displayOrder + ) + } } diff --git a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java index e7da9e8bb6..bd246f8b2a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java +++ b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java @@ -11,22 +11,23 @@ import android.net.Uri; import android.provider.BaseColumns; +import androidx.annotation.NonNull; + import com.quran.data.core.QuranInfo; import com.quran.labs.androidquran.BuildConfig; import com.quran.labs.androidquran.QuranApplication; import com.quran.labs.androidquran.R; -import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.database.DatabaseHandler; import com.quran.labs.androidquran.database.DatabaseUtils; import com.quran.labs.androidquran.database.TranslationsDBAdapter; import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.labs.androidquran.util.QuranUtils; +import com.quran.mobile.translation.model.LocalTranslation; import java.util.List; import javax.inject.Inject; -import androidx.annotation.NonNull; import timber.log.Timber; public class QuranDataProvider extends ContentProvider { @@ -107,7 +108,7 @@ private Cursor search(String query) { } private List getAvailableTranslations() { - return translationsDBAdapter.getTranslations(); + return translationsDBAdapter.legacyGetTranslations(); } private Cursor getSuggestions(String query) { diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt index 7f02559cda..76634aaadd 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt @@ -1,163 +1,91 @@ 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.util.SparseArray import androidx.annotation.WorkerThread -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.dao.translation.TranslationItem -import com.quran.labs.androidquran.database.TranslationsDBHelper.TranslationsTable import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.mobile.di.qualifier.ApplicationContext -import timber.log.Timber -import java.util.Collections +import com.quran.mobile.translation.data.TranslationsDataSource +import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.runBlocking import javax.inject.Inject import javax.inject.Singleton @Singleton class TranslationsDBAdapter @Inject constructor( @ApplicationContext private val context: Context, - adapter: TranslationsDBHelper, + private val dataSource: TranslationsDataSource, private val quranFileUtils: QuranFileUtils ) { - private val db: SQLiteDatabase = adapter.writableDatabase - - @Volatile - private var cachedTranslations: List? = null + fun getTranslations(): Flow> { + return dataSource.translations() + .filterNotNull() + .map { translations -> + translations.filter { quranFileUtils.hasTranslation(context, it.filename) } + } + } - var lastWriteTime: Long = 0 - private set + @WorkerThread + fun legacyGetTranslations(): List { + return runBlocking { getTranslations().first() } + } + @WorkerThread fun getTranslationsHash(): SparseArray { val result = SparseArray() - for (item in getTranslations()) { - result.put(item.id, item) + for (item in legacyGetTranslations()) { + result.put(item.id.toInt(), item) } return result } - // intentional, since cachedTranslations can be replaced by another thread, causing the check - // to be true, but the cached object returned to be null (or to change). - @WorkerThread - fun getTranslations(): List { - // intentional, since cachedTranslations can be replaced by another thread, causing the check - // to be true, but the cached object returned to be null (or to change). - val cached = cachedTranslations - if (!cached.isNullOrEmpty()) { - return cached - } - var items: MutableList = ArrayList() - var cursor: Cursor? = null - try { - cursor = db.query(TranslationsTable.TABLE_NAME, - null, null, null, null, null, - TranslationsTable.ID + " ASC") - - while (cursor.moveToNext()) { - val id = cursor.getInt(0) - val name = cursor.getString(1) - val translator = cursor.getString(2) - val translatorForeign = cursor.getString(3) - val filename = cursor.getString(4) - val url = cursor.getString(5) - val languageCode = cursor.getString(6) - val version = cursor.getInt(7) - val minimumVersion = cursor.getInt(8) - val displayOrder = cursor.getInt(9) - - if (quranFileUtils.hasTranslation(context, filename)) { - items.add( - LocalTranslation( - id, filename, name, translator, - translatorForeign, url, languageCode, version, minimumVersion, displayOrder - ) - ) - } - } - } finally { - cursor?.close() - } + suspend fun deleteTranslationByFileName(filename: String) { + dataSource.removeTranslation(filename) + } - items = Collections.unmodifiableList(items) - if (items.size > 0) { - cachedTranslations = items + @WorkerThread + fun legacyDeleteTranslationByFileName(filename: String) { + runBlocking { + deleteTranslationByFileName(filename) } - return items } - fun deleteTranslationByFile(filename: String) { - db.execSQL("DELETE FROM " + TranslationsTable.TABLE_NAME + " WHERE " + - TranslationsTable.FILENAME + " = ?", arrayOf(filename)) + @WorkerThread + fun legacyWriteTranslationUpdates(updates: List): Boolean { + return runBlocking { writeTranslationUpdates(updates) } } - fun writeTranslationUpdates(updates: List): Boolean { - var result = true - db.beginTransaction() + suspend fun writeTranslationUpdates(updates: List): Boolean { + val (available, unavailable) = updates.partition { it.exists() } - try { - var cachedNextOrder = -1 - for (item in updates) { - if (item.exists()) { - var displayOrder = 0 - val translation = item.translation - if (item.displayOrder > -1) { - displayOrder = item.displayOrder - } else { - var cursor: Cursor? = null - if (cachedNextOrder == -1) { - try { - // get next highest display order - cursor = db.query( - TranslationsTable.TABLE_NAME, arrayOf(TranslationsTable.DISPLAY_ORDER), - null, null, null, null, - TranslationsTable.DISPLAY_ORDER + " DESC", - "1" - ) - if (cursor != null && cursor.moveToFirst()) { - cachedNextOrder = cursor.getInt(0) + 1 - displayOrder = cachedNextOrder++ - } - } finally { - cursor?.close() - } - } else { - displayOrder = cachedNextOrder++ - } - } - - val values = ContentValues() - values.put(TranslationsTable.ID, translation.id) - values.put(TranslationsTable.NAME, translation.displayName) - values.put(TranslationsTable.TRANSLATOR, translation.translator) - values.put(TranslationsTable.TRANSLATOR_FOREIGN, - translation.translatorNameLocalized) - values.put(TranslationsTable.FILENAME, translation.fileName) - values.put(TranslationsTable.URL, translation.fileUrl) - values.put(TranslationsTable.LANGUAGE_CODE, translation.languageCode) - values.put(TranslationsTable.VERSION, item.localVersion) - values.put(TranslationsTable.MINIMUM_REQUIRED_VERSION, translation.minimumVersion) - values.put(TranslationsTable.DISPLAY_ORDER, displayOrder) + val needNextOrder = available.any { it.displayOrder == -1 } + val nextOrder = if (needNextOrder) { + dataSource.maximumDisplayOrder().toInt() + 1 + } else { + (available.maxOfOrNull { it.displayOrder } ?: 0) + 1 + } - db.replace(TranslationsTable.TABLE_NAME, null, values) + val items = if (needNextOrder) { + var nextOrderNumber = nextOrder + available.map { item -> + if (item.displayOrder == -1) { + item.copy(displayOrder = nextOrderNumber++) } else { - db.delete(TranslationsTable.TABLE_NAME, - TranslationsTable.ID + " = " + item.translation.id, null) + item } } - db.setTransactionSuccessful() - - lastWriteTime = System.currentTimeMillis() - // clear the cached translations - cachedTranslations = null - } catch (e: Exception) { - result = false - Timber.d(e, "error writing translation updates") - } finally { - db.endTransaction() + } else { + available } - return result + dataSource.updateTranslations(items.map { it.asLocalTranslation() }) + dataSource.removeTranslationsById(unavailable.map { it.translation.id.toLong() }) + + return true } } diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt deleted file mode 100644 index 89dc574b4d..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.quran.labs.androidquran.database - -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import com.quran.mobile.di.qualifier.ApplicationContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class TranslationsDBHelper @Inject constructor(@ApplicationContext context: Context) : - SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { - - companion object { - private const val DB_NAME = "translations.db" - private const val DB_VERSION = 5 - private const val CREATE_TRANSLATIONS_TABLE = - "CREATE TABLE " + TranslationsTable.TABLE_NAME + "(" + - TranslationsTable.ID + " integer primary key, " + - TranslationsTable.NAME + " varchar not null, " + - TranslationsTable.TRANSLATOR + " varchar, " + - TranslationsTable.TRANSLATOR_FOREIGN + " varchar, " + - TranslationsTable.FILENAME + " varchar not null, " + - TranslationsTable.URL + " varchar, " + - TranslationsTable.LANGUAGE_CODE + " varchar, " + - TranslationsTable.VERSION + " integer not null default 0," + - TranslationsTable.MINIMUM_REQUIRED_VERSION + " integer not null default 0, " + - TranslationsTable.DISPLAY_ORDER + " integer not null default -1 " + - ");" - } - - override fun onCreate(db: SQLiteDatabase) { - db.execSQL(CREATE_TRANSLATIONS_TABLE) - } - - @Suppress("LocalVariableName") - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion < 4) { - // a new column is added and columns are re-arranged - val BACKUP_TABLE = TranslationsTable.TABLE_NAME + "_backup" - db.beginTransaction() - try { - db.execSQL("ALTER TABLE " + TranslationsTable.TABLE_NAME + " RENAME TO " + BACKUP_TABLE) - db.execSQL(CREATE_TRANSLATIONS_TABLE) - db.execSQL("INSERT INTO " + TranslationsTable.TABLE_NAME + " (" + - TranslationsTable.ID + ", " + - TranslationsTable.NAME + ", " + - TranslationsTable.TRANSLATOR + ", " + - (if (oldVersion < 2) "" else (TranslationsTable.TRANSLATOR_FOREIGN + ", ")) + - TranslationsTable.FILENAME + ", " + - TranslationsTable.URL + ", " + - (if (oldVersion < 3) "" else (TranslationsTable.LANGUAGE_CODE + ",")) + - TranslationsTable.VERSION + ", " + - TranslationsTable.DISPLAY_ORDER + ") " + - "SELECT " + TranslationsTable.ID + ", " + - TranslationsTable.NAME + ", " + - TranslationsTable.TRANSLATOR + ", " + - (if (oldVersion < 2) "" else "translator_foreign, ") + - TranslationsTable.FILENAME + ", " + - TranslationsTable.URL + ", " + - (if (oldVersion < 3) "" else (TranslationsTable.LANGUAGE_CODE + ",")) + - TranslationsTable.VERSION + ", " + - TranslationsTable.ID + - " FROM " + BACKUP_TABLE) - db.execSQL("DROP TABLE $BACKUP_TABLE") - db.execSQL("UPDATE " + TranslationsTable.TABLE_NAME + " SET " + - TranslationsTable.MINIMUM_REQUIRED_VERSION + " = 2") - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - } else if (oldVersion < 5) { - // the v3 and below update also updates to v5. - // this code is called for updating from v4. - upgradeToV5(db) - } - } - - private fun upgradeToV5(db: SQLiteDatabase) { - // 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" - ) - - // for now, set the order to be the translation id - db.execSQL("UPDATE " + TranslationsTable.TABLE_NAME + " SET " + - TranslationsTable.DISPLAY_ORDER + " = " + TranslationsTable.ID) - db.setTransactionSuccessful() - } finally { - db.endTransaction() - } - } - - internal object TranslationsTable { - const val TABLE_NAME = "translations" - const val ID = "id" - const val NAME = "name" - const val TRANSLATOR = "translator" - const val TRANSLATOR_FOREIGN = "translatorForeign" - const val FILENAME = "filename" - const val URL = "url" - const val LANGUAGE_CODE = "languageCode" - const val VERSION = "version" - const val MINIMUM_REQUIRED_VERSION = "minimumRequiredVersion" - const val DISPLAY_ORDER = "userDisplayOrder" - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerItem.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerItem.kt index 8f88d2643c..8ddaa83442 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerItem.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerItem.kt @@ -7,8 +7,8 @@ import com.quran.data.model.SuraAyah import com.quran.data.model.highlight.HighlightInfo import com.quran.data.model.highlight.HighlightType import com.quran.data.model.selection.SelectionIndicator -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.mobile.translation.model.LocalTranslation import com.quran.page.common.data.AyahCoordinates import com.quran.page.common.data.PageCoordinates diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt index 38763f56fb..eba86fea10 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt @@ -16,7 +16,6 @@ import com.quran.data.model.highlight.HighlightType import com.quran.data.model.selection.AyahSelection import com.quran.data.model.selection.SelectionIndicator import com.quran.data.model.selection.startSuraAyah -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.data.SuraAyahIterator @@ -32,6 +31,7 @@ import com.quran.labs.androidquran.ui.helpers.HighlightTypes import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.labs.androidquran.util.QuranSettings import com.quran.mobile.bookmark.model.BookmarkModel +import com.quran.mobile.translation.model.LocalTranslation import com.quran.page.common.data.AyahCoordinates import com.quran.page.common.data.PageCoordinates import com.quran.reading.common.AudioEventPresenter diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTranslationTrackerItem.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTranslationTrackerItem.kt index 170d05447d..f18e67f09b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTranslationTrackerItem.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTranslationTrackerItem.kt @@ -4,7 +4,7 @@ import com.quran.data.core.QuranInfo import com.quran.data.model.SuraAyah import com.quran.data.model.highlight.HighlightType import com.quran.data.model.selection.SelectionIndicator -import com.quran.labs.androidquran.common.LocalTranslation +import com.quran.mobile.translation.model.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.ui.translation.TranslationView 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 0fd2229669..d21637b747 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 @@ -5,7 +5,6 @@ import com.quran.data.model.QuranText import com.quran.data.model.SuraAyah import com.quran.data.model.SuraAyahIterator import com.quran.data.model.VerseRange -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.LocalTranslationDisplaySort import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.common.TranslationMetadata @@ -14,6 +13,7 @@ import com.quran.labs.androidquran.model.translation.TranslationModel import com.quran.labs.androidquran.presenter.Presenter import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.TranslationUtil +import com.quran.mobile.translation.model.LocalTranslation import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single @@ -27,7 +27,6 @@ internal open class BaseTranslationPresenter internal constructor( private val translationUtil: TranslationUtil, private val quranInfo: QuranInfo ) : Presenter { - private var lastCacheTime: Long = 0 private val translationMap: MutableMap = HashMap() var translationScreen: T? = null @@ -38,8 +37,7 @@ internal open class BaseTranslationPresenter internal constructor( verseRange: VerseRange ): Single { - val translations = translationsAdapter.getTranslations() - + val translations = translationsAdapter.legacyGetTranslations() val sortedTranslations: List = ArrayList(translations) Collections.sort(sortedTranslations, LocalTranslationDisplaySort()) @@ -51,26 +49,27 @@ internal open class BaseTranslationPresenter internal constructor( val source = Observable.fromIterable(orderedTranslationsFileNames) val translationsObservable = - source.concatMapEager { db -> - translationModel.getTranslationFromDatabase(verseRange, db) - .map { texts -> ensureProperTranslations(verseRange, texts) } - .onErrorReturnItem(ArrayList()) - .toObservable() - } - .toList() + source.concatMapEager { db -> + translationModel.getTranslationFromDatabase(verseRange, db) + .map { texts -> ensureProperTranslations(verseRange, texts) } + .onErrorReturnItem(ArrayList()) + .toObservable() + } + .toList() val arabicObservable = if (!getArabic) Single.just(ArrayList()) else translationModel.getArabicFromDatabase(verseRange).onErrorReturnItem(ArrayList()) - return Single.zip(arabicObservable, translationsObservable, getTranslationMapSingle(), - { arabic: List, - texts: List>, - map: Map -> - val translationInfos = getTranslations(orderedTranslationsFileNames, map) - val ayahInfo = combineAyahData(verseRange, arabic, texts, translationInfos) - ResultHolder(translationInfos, ayahInfo) - }) - .subscribeOn(Schedulers.io()) + return Single.zip( + arabicObservable, translationsObservable, getTranslationMapSingle() + ) { arabic: List, + texts: List>, + map: Map -> + val translationInfos = getTranslations(orderedTranslationsFileNames, map) + val ayahInfo = combineAyahData(verseRange, arabic, texts, translationInfos) + ResultHolder(translationInfos, ayahInfo) + } + .subscribeOn(Schedulers.io()) } fun getTranslations(quranSettings: QuranSettings): List { @@ -115,9 +114,9 @@ internal open class BaseTranslationPresenter internal constructor( translationMinVersion >= TranslationUtil.MINIMUM_PROCESSING_VERSION val text = quranText ?: QuranText(element.sura, element.ayah, "") if (shouldProcess) { - translationUtil.parseTranslationText(text, translationId) + translationUtil.parseTranslationText(text, translationId.toInt()) } else { - TranslationMetadata(element.sura, element.ayah, text.text, translationId) + TranslationMetadata(element.sura, element.ayah, text.text, translationId.toInt()) } } @@ -176,14 +175,12 @@ internal open class BaseTranslationPresenter internal constructor( } private fun getTranslationMapSingle(): Single> { - return if (this.translationMap.isEmpty() || - this.lastCacheTime != translationsAdapter.lastWriteTime) { - Single.fromCallable { translationsAdapter.getTranslations() } + return if (this.translationMap.isEmpty()) { + Single.fromCallable { translationsAdapter.legacyGetTranslations() } .map { translations -> translations.associateBy { it.filename } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSuccess { map -> - this.lastCacheTime = translationsAdapter.lastWriteTime this.translationMap.clear() this.translationMap.putAll(map) } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java index 07fbaea239..ed82443041 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java @@ -1,19 +1,20 @@ package com.quran.labs.androidquran.presenter.translation; +import androidx.annotation.NonNull; + import com.quran.data.core.QuranInfo; -import com.quran.labs.androidquran.common.LocalTranslation; -import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.data.model.VerseRange; +import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.database.TranslationsDBAdapter; import com.quran.labs.androidquran.model.translation.TranslationModel; import com.quran.labs.androidquran.util.QuranSettings; import com.quran.labs.androidquran.util.TranslationUtil; +import com.quran.mobile.translation.model.LocalTranslation; import java.util.List; import javax.inject.Inject; -import androidx.annotation.NonNull; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.observers.DisposableSingleObserver; 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 74e16c69be..f301894aea 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 @@ -7,7 +7,6 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -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; @@ -19,6 +18,7 @@ import com.quran.labs.androidquran.util.QuranFileUtils; import com.quran.labs.androidquran.util.QuranSettings; import com.quran.mobile.di.qualifier.ApplicationContext; +import com.quran.mobile.translation.model.LocalTranslation; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; @@ -140,16 +140,16 @@ public void updateItem(final TranslationItem item) { // old file needs to be removed from the database explicitly final Translation translation = item.getTranslation(); if (translation.getMinimumVersion() >= 5) { - translationsDBAdapter.deleteTranslationByFile(translation.getFileName()); + translationsDBAdapter.legacyDeleteTranslationByFileName(translation.getFileName()); } - return translationsDBAdapter.writeTranslationUpdates(Collections.singletonList(item)); + return translationsDBAdapter.legacyWriteTranslationUpdates(Collections.singletonList(item)); } ).subscribeOn(Schedulers.io()) .subscribe(); } public void updateItemOrdering(final List items) { - Observable.fromCallable(() -> translationsDBAdapter.writeTranslationUpdates(items)) + Observable.fromCallable(() -> translationsDBAdapter.legacyWriteTranslationUpdates(items)) .subscribeOn(Schedulers.io()) .subscribe(); } @@ -247,6 +247,7 @@ private List mergeWithServerTranslations(List serv TranslationItem override = null; if (exists) { if (local == null) { + // text version, schema version final Pair versions = getVersionFromDatabase(translation.getFileName()); item = new TranslationItem(translation, versions.first); if (versions.second != translation.getMinimumVersion()) { @@ -272,7 +273,7 @@ private List mergeWithServerTranslations(List serv // certain schema changes, especially those going to v5, keep the same filename while // changing the database entry id. this could cause duplicate entries in the database. // work around it by removing the existing entries before doing the updates. - translationsDBAdapter.deleteTranslationByFile(override.getTranslation().getFileName()); + translationsDBAdapter.legacyDeleteTranslationByFileName(override.getTranslation().getFileName()); } updates.add(override == null ? item : override); } else if (local != null && local.getLanguageCode() == null) { @@ -283,7 +284,7 @@ private List mergeWithServerTranslations(List serv } if (!updates.isEmpty()) { - translationsDBAdapter.writeTranslationUpdates(updates); + translationsDBAdapter.legacyWriteTranslationUpdates(updates); } return results; } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt index e0cd8c6747..8b7f3a9b95 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt @@ -2,14 +2,14 @@ package com.quran.labs.androidquran.presenter.translation import com.quran.data.core.QuranInfo import com.quran.data.di.QuranPageScope -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.database.TranslationsDBAdapter import com.quran.labs.androidquran.model.translation.TranslationModel import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.TranslationUtil -import io.reactivex.rxjava3.core.Observable +import com.quran.mobile.translation.model.LocalTranslation import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.observers.DisposableObserver import javax.inject.Inject diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListCallback.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListCallback.kt new file mode 100644 index 0000000000..0641ca4ffd --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListCallback.kt @@ -0,0 +1,10 @@ +package com.quran.labs.androidquran.presenter.translationlist + +import com.quran.mobile.translation.model.LocalTranslation + +interface TranslationListCallback { + fun onTranslationsUpdated( + titles: Array, + translations: List + ) +} diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt new file mode 100644 index 0000000000..d47dd4a6c3 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt @@ -0,0 +1,22 @@ +package com.quran.labs.androidquran.presenter.translationlist + +import com.quran.mobile.translation.data.TranslationsDataSource +import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.Job +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class TranslationListPresenter @Inject constructor( + private val dataSource: TranslationsDataSource +) { + fun translations(): Flow> { + return dataSource.translations() + .filterNotNull() + .map { translations -> translations.sortedBy { it.displayOrder } } + } +} 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 155a4dbb18..8de8d34960 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 @@ -68,7 +68,6 @@ import com.quran.labs.androidquran.SearchActivity; import com.quran.labs.androidquran.bridge.AudioEventPresenterBridge; import com.quran.labs.androidquran.bridge.ReadingEventPresenterBridge; -import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.common.LocalTranslationDisplaySort; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.common.audio.model.QariItem; @@ -123,6 +122,7 @@ import com.quran.mobile.di.QuranReadingPageComponentProvider; import com.quran.mobile.feature.qarilist.QariListWrapper; import com.quran.mobile.feature.qarilist.di.QariListWrapperInjector; +import com.quran.mobile.translation.model.LocalTranslation; import com.quran.page.common.factory.PageViewFactoryProvider; import com.quran.page.common.toolbar.AyahToolBar; import com.quran.page.common.toolbar.di.AyahToolBarInjector; @@ -1374,7 +1374,7 @@ private void ensurePage(int sura, int ayah) { private void requestTranslationsList() { compositeDisposable.add( Single.fromCallable(() -> - translationsDBAdapter.getTranslations()) + translationsDBAdapter.legacyGetTranslations()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(new DisposableSingleObserver>() { 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 3136b1e8bf..60f51e2648 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 @@ -162,14 +162,15 @@ public void handleDownloadSuccess() { } } - List sortedItems = sortedDownloadedItems(); - int lastDisplayOrder = sortedItems.isEmpty() ? 1 : - sortedItems.get(sortedItems.size() - 1).getDisplayOrder(); - + // TODO: we can avoid the cost of sorting once we can listen to db updates + // in which case we'd set the local version as -1 so it gets properly assigned after. + final List sortedItems = sortedDownloadedItems(); + int lastDisplayOrder = sortedItems.isEmpty() ? 0 : + sortedItems.get(sortedItems.size() - 1).getDisplayOrder(); final Translation translation = downloadingItem.getTranslation(); - TranslationItem updated = new TranslationItem(translation, - translation.getCurrentVersion(), lastDisplayOrder + 1); - updateTranslationItem(updated); + updateTranslationItem(downloadingItem.withLocalVersionAndDisplayOrder( + translation.getCurrentVersion(), lastDisplayOrder + 1) + ); // update active translations and add this item to it QuranSettings settings = QuranSettings.getInstance(this); diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt index 4f3a80f7a8..452105050e 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt @@ -1,28 +1,28 @@ package com.quran.labs.androidquran.ui.fragment -import com.quran.labs.androidquran.presenter.translation.InlineTranslationPresenter -import android.widget.ProgressBar -import com.quran.labs.androidquran.view.InlineTranslationView -import com.quran.labs.androidquran.view.QuranSpinner -import com.quran.labs.androidquran.ui.util.TranslationsSpinnerAdapter -import javax.inject.Inject -import com.quran.data.core.QuranInfo -import com.quran.labs.androidquran.util.QuranSettings -import com.quran.labs.androidquran.ui.PagerActivity -import android.view.LayoutInflater -import android.view.ViewGroup -import android.os.Bundle -import com.quran.labs.androidquran.R import android.app.Activity import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.Button -import com.quran.labs.androidquran.common.LocalTranslation +import android.widget.ProgressBar +import com.quran.data.core.QuranInfo import com.quran.data.model.VerseRange +import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.presenter.translation.InlineTranslationPresenter import com.quran.labs.androidquran.presenter.translation.InlineTranslationPresenter.TranslationScreen +import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter +import com.quran.labs.androidquran.ui.util.TranslationsSpinnerAdapter +import com.quran.labs.androidquran.util.QuranSettings +import com.quran.labs.androidquran.view.InlineTranslationView +import com.quran.labs.androidquran.view.QuranSpinner import com.quran.mobile.di.AyahActionFragmentProvider +import com.quran.mobile.translation.model.LocalTranslation +import javax.inject.Inject import kotlin.math.abs class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java index c3dd63f5c9..3a57d8347b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java @@ -18,7 +18,6 @@ import com.quran.data.core.QuranInfo; import com.quran.data.model.SuraAyah; import com.quran.data.model.selection.AyahSelection; -import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.data.QuranDisplayData; import com.quran.labs.androidquran.presenter.quran.QuranPagePresenter; @@ -42,6 +41,7 @@ import com.quran.labs.androidquran.view.QuranImagePageLayout; import com.quran.labs.androidquran.view.QuranTranslationPageLayout; import com.quran.labs.androidquran.view.TabletView; +import com.quran.mobile.translation.model.LocalTranslation; import com.quran.page.common.data.AyahCoordinates; import com.quran.page.common.data.PageCoordinates; import com.quran.page.common.draw.ImageDrawHelper; diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java index 6918e434fe..5227d9f671 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java @@ -14,7 +14,6 @@ import com.quran.data.core.QuranInfo; import com.quran.data.model.SuraAyah; import com.quran.data.model.selection.AyahSelection; -import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.data.QuranDisplayData; import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerItem; @@ -29,6 +28,7 @@ import com.quran.labs.androidquran.ui.util.PageController; import com.quran.labs.androidquran.util.QuranSettings; import com.quran.labs.androidquran.view.QuranTranslationPageLayout; +import com.quran.mobile.translation.model.LocalTranslation; import com.quran.reading.common.ReadingEventPresenter; import java.util.List; diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/AyahTracker.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/AyahTracker.kt index e22db2ede3..ef648a02f4 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/AyahTracker.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/AyahTracker.kt @@ -1,8 +1,8 @@ package com.quran.labs.androidquran.ui.helpers import com.quran.data.model.selection.SelectionIndicator -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.mobile.translation.model.LocalTranslation interface AyahTracker { fun getToolBarPosition(sura: Int, ayah: Int): SelectionIndicator 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 e7edc5ebf4..2bdf9b149d 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 @@ -18,7 +18,6 @@ import com.quran.data.model.SuraAyah; import com.quran.data.model.highlight.HighlightType; import com.quran.data.model.selection.SelectionIndicator; -import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.common.LocalTranslationDisplaySort; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.common.TranslationMetadata; @@ -27,6 +26,8 @@ import com.quran.labs.androidquran.ui.helpers.HighlightTypes; import com.quran.labs.androidquran.ui.util.PageController; import com.quran.labs.androidquran.util.QuranSettings; +import com.quran.mobile.translation.model.LocalTranslation; + import dev.chrisbanes.insetter.Insetter; import java.util.ArrayList; import java.util.Arrays; @@ -137,7 +138,7 @@ public void setVerses(@NonNull QuranDisplayData quranDisplayData, Arrays.sort(sortedTranslations, new LocalTranslationDisplaySort()); for (int j = 0; j < sortedTranslations.length; j++) { - final TranslationMetadata metadata = findText(verse.texts, sortedTranslations[j].getId()); + final TranslationMetadata metadata = findText(verse.texts, (int) sortedTranslations[j].getId()); CharSequence text = metadata != null ? metadata.getText() : ""; if (!TextUtils.isEmpty(text)) { if (wantTranslationHeaders) { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java b/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java index 41b3f89d44..bcd71fc881 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java @@ -2,7 +2,6 @@ import android.content.Context; import android.content.res.Resources; -import androidx.annotation.NonNull; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -11,9 +10,11 @@ import android.widget.CheckBox; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.quran.labs.androidquran.R; -import com.quran.labs.androidquran.common.LocalTranslation; import com.quran.labs.androidquran.ui.PagerActivity; +import com.quran.mobile.translation.model.LocalTranslation; import java.util.ArrayList; import java.util.List; diff --git a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt index 592dd8326e..811567969f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt @@ -11,12 +11,11 @@ import android.widget.Toast import androidx.annotation.StringRes import com.quran.data.model.QuranText import com.quran.labs.androidquran.R -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils import com.quran.labs.androidquran.ui.util.ToastCompat -import com.quran.labs.androidquran.ui.util.TypefaceManager +import com.quran.mobile.translation.model.LocalTranslation import dagger.Reusable import java.text.NumberFormat import java.util.Locale diff --git a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt index 2c4b225e72..6fd1e06fac 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt @@ -10,7 +10,6 @@ import android.text.TextUtils import android.text.style.ForegroundColorSpan import android.text.style.RelativeSizeSpan import android.text.style.StyleSpan -import android.text.style.TypefaceSpan import android.util.AttributeSet import android.view.View import android.widget.LinearLayout @@ -18,15 +17,13 @@ import android.widget.ScrollView import android.widget.TextView import androidx.annotation.StyleRes import androidx.core.content.ContextCompat -import com.quran.common.search.SearchTextUtil import com.quran.labs.androidquran.R -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.common.TranslationMetadata import com.quran.labs.androidquran.ui.helpers.TypefaceWrappingSpan -import com.quran.labs.androidquran.ui.translation.TranslationAdapter import com.quran.labs.androidquran.ui.util.TypefaceManager import com.quran.labs.androidquran.util.QuranSettings +import com.quran.mobile.translation.model.LocalTranslation class InlineTranslationView @JvmOverloads constructor( context: Context, 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 6033a99a08..1d2efaa0f3 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 @@ -5,12 +5,12 @@ import com.quran.data.core.QuranInfo import com.quran.data.model.QuranText import com.quran.data.model.VerseRange import com.quran.data.pageinfo.common.MadaniDataSource -import com.quran.labs.androidquran.common.LocalTranslation import com.quran.labs.androidquran.common.TranslationMetadata import com.quran.labs.androidquran.database.TranslationsDBAdapter import com.quran.labs.androidquran.model.translation.TranslationModel import com.quran.labs.androidquran.presenter.Presenter import com.quran.labs.androidquran.util.TranslationUtil +import com.quran.mobile.translation.model.LocalTranslation import org.junit.Before import org.junit.Test import org.mockito.Mockito @@ -53,7 +53,7 @@ class BaseTranslationPresenterTest { @Test fun testHashlessGetTranslationNames() { val databases = listOf("one.db", "two.db") - val map = HashMap() + val map = HashMap() val translations = presenter.getTranslations(databases, map) assertThat(translations).hasLength(2) diff --git a/common/translation/build.gradle.kts b/common/translation/build.gradle.kts new file mode 100644 index 0000000000..fcff42da2f --- /dev/null +++ b/common/translation/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("quran.android.library.android") + id("app.cash.sqldelight") + id("com.squareup.anvil") +} + +anvil { generateDaggerFactories = true } + +android.namespace = "com.quran.mobile.translation" + +sqldelight { + databases { + create("TranslationsDatabase") { + packageName.set("com.quran.mobile.translation.data") + schemaOutputDirectory.set(file("src/main/sqldelight/databases")) + verifyMigrations.set(true) + generateAsync.set(true) + } + } +} + +dependencies { + implementation(project(":common:di")) + implementation(project(":common:data")) + + // dagger + implementation(libs.dagger.runtime) + + // coroutines + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + // sqldelight + implementation(libs.sqldelight.android.driver) + implementation(libs.sqldelight.coroutines.extensions) + implementation(libs.sqldelight.primitive.adapters) +} diff --git a/common/translation/src/main/kotlin/com/quran/mobile/translation/data/TranslationsDataSource.kt b/common/translation/src/main/kotlin/com/quran/mobile/translation/data/TranslationsDataSource.kt new file mode 100644 index 0000000000..9c77d3fb6c --- /dev/null +++ b/common/translation/src/main/kotlin/com/quran/mobile/translation/data/TranslationsDataSource.kt @@ -0,0 +1,64 @@ +package com.quran.mobile.translation.data + +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.coroutines.asFlow +import app.cash.sqldelight.coroutines.mapToList +import com.quran.mobile.translation.mapper.LocalTranslationMapper +import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TranslationsDataSource @Inject constructor(translationsDatabase: TranslationsDatabase) { + private val translationsQueries = translationsDatabase.translationsQueries + + private val scope = MainScope() + private val translations by lazy { + translationsQueries.all(LocalTranslationMapper.mapper) + .asFlow() + .mapToList(Dispatchers.IO) + .stateIn(scope, SharingStarted.Lazily, null) + } + + fun translations(): StateFlow?> = translations + + suspend fun updateTranslations(items: List) { + translationsQueries.transaction { + items.forEach { + translationsQueries.update( + id = it.id, + name = it.name, + translator = it.translator, + translatorForeign = it.translatorForeign, + filename = it.filename, + url = it.url, + languageCode = it.languageCode, + version = it.version.toLong(), + minimumRequiredVersion = it.minimumVersion.toLong(), + userDisplayOrder = it.displayOrder.toLong() + ) + } + } + } + + suspend fun removeTranslation(filename: String) { + translationsQueries.deleteByFileName(filename) + } + + suspend fun removeTranslationsById(ids: List) { + translationsQueries.transaction { + ids.forEach { + translationsQueries.deleteById(it) + } + } + } + + suspend fun maximumDisplayOrder(): Long { + return translationsQueries.greatestDisplayOrder().awaitAsOne().MAX ?: 0 + } +} diff --git a/common/translation/src/main/kotlin/com/quran/mobile/translation/di/TranslationDataModule.kt b/common/translation/src/main/kotlin/com/quran/mobile/translation/di/TranslationDataModule.kt new file mode 100644 index 0000000000..a922400461 --- /dev/null +++ b/common/translation/src/main/kotlin/com/quran/mobile/translation/di/TranslationDataModule.kt @@ -0,0 +1,29 @@ +package com.quran.mobile.translation.di + +import android.content.Context +import app.cash.sqldelight.async.coroutines.synchronous +import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import com.quran.data.di.AppScope +import com.quran.mobile.di.qualifier.ApplicationContext +import com.quran.mobile.translation.data.TranslationsDatabase +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +@ContributesTo(AppScope::class) +class TranslationDataModule { + + @Singleton + @Provides + fun provideTranslationDatabase(@ApplicationContext context: Context): TranslationsDatabase { + return TranslationsDatabase( + AndroidSqliteDriver( + schema = TranslationsDatabase.Schema.synchronous(), + context = context, + name = "translations.db" + ) + ) + } +} diff --git a/common/translation/src/main/kotlin/com/quran/mobile/translation/mapper/LocalTranslationMapper.kt b/common/translation/src/main/kotlin/com/quran/mobile/translation/mapper/LocalTranslationMapper.kt new file mode 100644 index 0000000000..e75996a5ab --- /dev/null +++ b/common/translation/src/main/kotlin/com/quran/mobile/translation/mapper/LocalTranslationMapper.kt @@ -0,0 +1,33 @@ +package com.quran.mobile.translation.mapper + +import com.quran.mobile.translation.model.LocalTranslation + +object LocalTranslationMapper { + + val mapper: (( + id: Long, + name: String, + translator: String?, + translatorForeign: String?, + filename: String, + url: String, + languageCode: String?, + version: Long, + minimumRequiredVersion: Long, + userDisplayOrder: Long, + ) -> LocalTranslation) = + { id, name, translator, translatorForeign, filename, url, languageCode, version, minimumRequiredVersion, displayOrder -> + LocalTranslation( + id = id, + name = name, + translator = translator, + translatorForeign = translatorForeign, + filename = filename, + url = url, + languageCode = languageCode, + version = version.toInt(), + minimumVersion = minimumRequiredVersion.toInt(), + displayOrder = displayOrder.toInt(), + ) + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslation.kt b/common/translation/src/main/kotlin/com/quran/mobile/translation/model/LocalTranslation.kt similarity index 89% rename from app/src/main/java/com/quran/labs/androidquran/common/LocalTranslation.kt rename to common/translation/src/main/kotlin/com/quran/mobile/translation/model/LocalTranslation.kt index d541bf54f5..d567e02266 100644 --- a/app/src/main/java/com/quran/labs/androidquran/common/LocalTranslation.kt +++ b/common/translation/src/main/kotlin/com/quran/mobile/translation/model/LocalTranslation.kt @@ -1,7 +1,7 @@ -package com.quran.labs.androidquran.common +package com.quran.mobile.translation.model data class LocalTranslation( - val id: Int = -1, + val id: Long = -1, val filename: String, val name: String = "", val translator: String? = "", diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/1.db b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/1.db new file mode 100644 index 0000000000000000000000000000000000000000..38d0fcbe1b96d6609a1c88f2f228cbe47065e9df GIT binary patch literal 8192 zcmeI#QES355C`x?27(~&ZLjw*VZs=LeSuXMMV9GSBdm`ZMkv%4(zy5eP5h=_1{Glg zU&r_#J<`jCYx%up@uu_OXkGb@4fG%`g_PovNC+W0TbxbA`l2a#jrF;Z;?D28d`kAN z1wRM~KmY;|fB*y_009U<00Izzz@HWP%4GYh-upfmB-j;P?Ltd5j>3eIGs>CQV z$xtgg?a)n!C|}WZX4FKb^qNjzk~E`dmECfuv>Qi8y&Ia%IhR_;-DW`Lzk6|Oofq!+ z!&_hcobc&<%|ChkY%`sn)HqpcL-(CQyqoi-9{~XfKmY;|fB*y_009U<00Izzz&{o^ E0O(LjYXATM literal 0 HcmV?d00001 diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/2.db b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/2.db new file mode 100644 index 0000000000000000000000000000000000000000..425d1490681708f114bd890d6e46a6574d1a2dc9 GIT binary patch literal 8192 zcmeI#%}N6?5C`z22*QHs&FfqW7DR-;fU9PuG`4n|1$&H`N?<=A8@-E9;+r}vY^hZ6 zSdjm)!|r6lZ1~->TxcI1t(#EUNMms#q!d>~LI}w@;OwKT5Bh@NUHw-`aS=X`c$Yj* z1V0D}KmY;|fB*y_009U<00Izzz@7!(TX}do9LkSTv`?jr!PaeQqi^bV`+IPctJEkm z>9tn0-J!FD$UoC$X4IX^=^>xor}>JeYIV+?+E$K?dNeefa~4{6tNnl`eD{7XWleDY zrQUSbzI4Bjc@5?6>^B!$KK|xF{eS6JWoe-eT_&UMZqAo}1Oy-e0SG_<0uX=z1Rwwb L2tWV=|5)G)_#9F7 literal 0 HcmV?d00001 diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/3.db b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/3.db new file mode 100644 index 0000000000000000000000000000000000000000..76b6421a8432b80fd1ca10e82e20b12b46a9555f GIT binary patch literal 8192 zcmeI#!Ab)$5C-6+2uh*i&FfqW3L=6p;965Du2r`w*kh#W5=axsrapi^i4W=BMA=eN z=*@%t!w$QXS!T1}Et7}RL`@4HmMYRYJ7Ao%3nIoCPuWS?My_|-BHeSnEjT+3Z@u&u z-+yQ6LO=il5P$##AOHafKmY;|fB*#kPT*ya?;iI1{B;u5lhrXO*H{&ecg^N=XHbbk z3X;WTDQL4pM_nQ_r_or7Yf;gCHM%XT8QqB4N$NPY)Fj2Dr14$KX<6pgc7P9`z3)py zAGE36y0b7=|2}3F?9bU&xq78)J@E6dFX!pcIyC8@*1K#sLsw#0OiM{;-Q#>VPltX4 a1Rwwb2tWV=5P$##AOHafKmY>&Sl|PpRazqe literal 0 HcmV?d00001 diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/4.db b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/4.db new file mode 100644 index 0000000000000000000000000000000000000000..8900301f84df99443291e6d0c984a79b2027ab2e GIT binary patch literal 8192 zcmeI#K}*9h6bJBR2*N<|=Jj0$3L=8%fpumwj8khS?iguS0&R+E>jx0Oh~LDIwnXWe zC=5Kx|IyICy!18w-Ew~?ZDVLr`BFD@%JvxN?2L#p#>41^(M8O+x+L6Vz9~36_U}7k z72o|};X*(F0uX=z1Rwwb2tWV=5P$##{v_}^4d`l&FD*RC#fV=Gs$Kletd zNR%LzoJ&FL6*}k>*^DMrB`!rux9Q|MN#}GW=0~C8bZJP5M@7?{FlJK5*{(t5KWpEo z#+5fVcdN?67UuUhtuKD|F0{*AotsgWeSJB0e=)ue|Fqg=w;8$+<76fkoy1k8b+&BF r)I7J=o9toZOZ~%mHV=n>1Oy-e0SG_<0uX=z1Rwwb2tWV=|5#uN_#tY6 literal 0 HcmV?d00001 diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/5.db b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/5.db new file mode 100644 index 0000000000000000000000000000000000000000..5e91a4d4fcb3b688b4bd1f88571e913da97d8e52 GIT binary patch literal 8192 zcmeI#K}*9h6bJBR2*N<|=1F*$fr5(S7qD)b4C|&f6L*X>8-b?Pv=zJ#y!#dWrY4h) ziHe@(|7b{)m%gOGTW+pnQ)!wO&Z>$|*cRiQof0v|xNj}rnh4{yX5jlU{w+8=bZ;Ae z72kYi{y{(h0uX=z1Rwwb2tWV=5P$##76Nn2*S9+z{<2f4`%G6( zND7kCSuAKZL%VGvlhR-)#kol6Dj8fx$%HP%WZ!3UWi?50CuunHZ5+p8wkc4!Meg%d zzi`^j@?~afGW~s-+GSsBXDXl7YNoqI`muAU{-j;$|7m&3<}lO~{b(FZItr_-$&IaT zq91GHbb9-zkLpspo+%$P^*nN^c0ccNaNOPtcXNN~M?e4q5P$##AOHafKmY;|fB*y_ I@Q(%F0c*f`b^rhX literal 0 HcmV?d00001 diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm new file mode 100644 index 0000000000..29d9684960 --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm @@ -0,0 +1,26 @@ +CREATE TABLE IF NOT EXISTS translations ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + filename TEXT NOT NULL, + url TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 0 +); + +-- adds translatorForeign +CREATE TABLE translations_migration ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + translatorForeign TEXT, + filename TEXT NOT NULL, + url TEXT NOT NULL, + version INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO translations_migration +SELECT id, name, translator, "", filename, url, version +FROM translations; + +DROP TABLE translations; +ALTER TABLE translations_migration RENAME TO translations; diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm new file mode 100644 index 0000000000..69e50d0f6d --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm @@ -0,0 +1,18 @@ +-- adds languageCode +CREATE TABLE translations_migration ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + translatorForeign TEXT, + filename TEXT NOT NULL, + url TEXT NOT NULL, + languageCode TEXT, + version INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO translations_migration +SELECT id, name, translator, translatorForeign, filename, url, "", version +FROM translations; + +DROP TABLE translations; +ALTER TABLE translations_migration RENAME TO translations; diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm new file mode 100644 index 0000000000..311c357b41 --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm @@ -0,0 +1,19 @@ +-- adds minimumRequiredVersion +CREATE TABLE translations_migration ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + translatorForeign TEXT, + filename TEXT NOT NULL, + url TEXT NOT NULL, + languageCode TEXT, + version INTEGER NOT NULL DEFAULT 0, + minimumRequiredVersion INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO translations_migration +SELECT id, name, translator, translatorForeign, filename, url, languageCode, version, 2 +FROM translations; + +DROP TABLE translations; +ALTER TABLE translations_migration RENAME TO translations; diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm new file mode 100644 index 0000000000..da84f45122 --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm @@ -0,0 +1,20 @@ +-- adds userDisplayOrder +CREATE TABLE translations_migration ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + translatorForeign TEXT, + filename TEXT NOT NULL, + url TEXT NOT NULL, + languageCode TEXT, + version INTEGER NOT NULL DEFAULT 0, + minimumRequiredVersion INTEGER NOT NULL DEFAULT 0, + userDisplayOrder INTEGER NOT NULL DEFAULT -1 +); + +INSERT INTO translations_migration +SELECT id, name, translator, translatorForeign, filename, url, languageCode, version, minimumRequiredVersion, id +FROM translations; + +DROP TABLE translations; +ALTER TABLE translations_migration RENAME TO translations; diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/translations.sq b/common/translation/src/main/sqldelight/com/quran/mobile/translation/translations.sq new file mode 100644 index 0000000000..4450734d17 --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/translations.sq @@ -0,0 +1,29 @@ +CREATE TABLE translations ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + translatorForeign TEXT, + filename TEXT NOT NULL, + url TEXT NOT NULL, + languageCode TEXT, + version INTEGER NOT NULL DEFAULT 0, + minimumRequiredVersion INTEGER NOT NULL DEFAULT 0, + userDisplayOrder INTEGER NOT NULL DEFAULT -1 +); + +all: +SELECT * FROM translations ORDER BY id ASC; + +update: +REPLACE INTO translations( + id, name, translator, translatorForeign, filename, url, languageCode, version, minimumRequiredVersion, userDisplayOrder) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + +greatestDisplayOrder: +SELECT MAX(userDisplayOrder) FROM translations; + +deleteById: +DELETE FROM translations WHERE id = ?; + +deleteByFileName: +DELETE FROM translations WHERE filename = ?; diff --git a/settings.gradle.kts b/settings.gradle.kts index 6fbbf78e96..a260c5eb2a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,6 +30,7 @@ include(":common:recitation") include(":common:preference") include(":common:search") include(":common:toolbar") +include(":common:translation") include(":common:upgrade") include(":common:ui:core") include(":feature:analytics-noop") From fb0bed96ab5761caa5a92d2374c64a72b4843215 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 02:06:07 +0400 Subject: [PATCH 191/258] Update compose-compiler to 1.5.6 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f1ee60f531..264529aa2a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ kotlin = "1.9.21" ksp = "1.9.21-1.0.15" # required within the Gradle convention plugins - not unused -compose-compiler = "1.5.5-dev-k1.9.21-163bb051fe5" +compose-compiler = "1.5.6" composeBomVersion = "2023.10.01" okhttpBomVersion = "4.12.0" From ce0e497de4fe4c0a6aafd9b1efcb5a566a3f9c89 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 8 Dec 2023 22:26:19 +0400 Subject: [PATCH 192/258] Observe stream of translations in PagerActivity This patch updates PagerActivity and the InlineTranslations functionality to observe the list of translations as they are updated from the database. This solves issues like translations not showing up right when they are added without returning to the menu and coming back. Fixes #2460. --- .../database/TranslationsDBAdapter.kt | 3 + .../translation/BaseTranslationPresenter.kt | 6 +- .../InlineTranslationPresenter.java | 60 ------------ .../translation/InlineTranslationPresenter.kt | 66 +++++++++++++ .../TranslationListPresenter.kt | 13 +++ .../labs/androidquran/ui/PagerActivity.java | 94 +++++++------------ .../ui/fragment/AyahTranslationFragment.kt | 65 ++++++------- .../ui/translation/TranslationView.java | 2 +- .../quran/labs/androidquran/util/ShareUtil.kt | 2 +- .../view/InlineTranslationView.kt | 2 +- .../BaseTranslationPresenterTest.kt | 2 +- .../translation/model/LocalTranslation.kt | 2 +- 12 files changed, 157 insertions(+), 160 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt index 76634aaadd..410f265f45 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt @@ -8,6 +8,7 @@ import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.mobile.di.qualifier.ApplicationContext import com.quran.mobile.translation.data.TranslationsDataSource import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -22,6 +23,8 @@ class TranslationsDBAdapter @Inject constructor( private val dataSource: TranslationsDataSource, private val quranFileUtils: QuranFileUtils ) { + private val scope = MainScope() + fun getTranslations(): Flow> { return dataSource.translations() .filterNotNull() 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 d21637b747..7d9474fc9e 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 @@ -21,7 +21,7 @@ import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers import java.util.Collections -internal open class BaseTranslationPresenter internal constructor( +open class BaseTranslationPresenter internal constructor( private val translationModel: TranslationModel, private val translationsAdapter: TranslationsDBAdapter, private val translationUtil: TranslationUtil, @@ -189,8 +189,8 @@ internal open class BaseTranslationPresenter internal constructor( } } - internal class ResultHolder(val translations: Array, - val ayahInformation: List) + class ResultHolder(val translations: Array, + val ayahInformation: List) override fun bind(what: T) { translationScreen = what diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java deleted file mode 100644 index ed82443041..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.quran.labs.androidquran.presenter.translation; - -import androidx.annotation.NonNull; - -import com.quran.data.core.QuranInfo; -import com.quran.data.model.VerseRange; -import com.quran.labs.androidquran.common.QuranAyahInfo; -import com.quran.labs.androidquran.database.TranslationsDBAdapter; -import com.quran.labs.androidquran.model.translation.TranslationModel; -import com.quran.labs.androidquran.util.QuranSettings; -import com.quran.labs.androidquran.util.TranslationUtil; -import com.quran.mobile.translation.model.LocalTranslation; - -import java.util.List; - -import javax.inject.Inject; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.observers.DisposableSingleObserver; - -public class InlineTranslationPresenter extends - BaseTranslationPresenter { - private final QuranSettings quranSettings; - - @Inject - InlineTranslationPresenter(TranslationModel translationModel, - TranslationsDBAdapter dbAdapter, - TranslationUtil translationUtil, - QuranSettings quranSettings, - QuranInfo quranInfo) { - super(translationModel, dbAdapter, translationUtil, quranInfo); - this.quranSettings = quranSettings; - } - - public void refresh(VerseRange verseRange) { - if (getDisposable() != null) { - getDisposable().dispose(); - } - - setDisposable(getVerses(false, getTranslations(quranSettings), verseRange) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(new DisposableSingleObserver() { - @Override - public void onSuccess(@NonNull ResultHolder result) { - if (getTranslationScreen() != null) { - getTranslationScreen() - .setVerses(result.getTranslations(), result.getAyahInformation()); - } - } - - @Override - public void onError(@NonNull Throwable e) { - } - })); - } - - public interface TranslationScreen { - void setVerses(@NonNull LocalTranslation[] translations, @NonNull List verses); - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt new file mode 100644 index 0000000000..714cf0876c --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt @@ -0,0 +1,66 @@ +package com.quran.labs.androidquran.presenter.translation + +import com.quran.data.core.QuranInfo +import com.quran.data.model.VerseRange +import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.database.TranslationsDBAdapter +import com.quran.labs.androidquran.model.translation.TranslationModel +import com.quran.labs.androidquran.presenter.translationlist.TranslationListPresenter +import com.quran.labs.androidquran.util.QuranSettings +import com.quran.labs.androidquran.util.TranslationUtil +import com.quran.mobile.translation.model.LocalTranslation +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.observers.DisposableSingleObserver +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +class InlineTranslationPresenter @Inject constructor( + translationModel: TranslationModel, + dbAdapter: TranslationsDBAdapter, + translationUtil: TranslationUtil, + private val quranSettings: QuranSettings, + private val translationListPresenter: TranslationListPresenter, + quranInfo: QuranInfo +) : BaseTranslationPresenter( + translationModel, dbAdapter, translationUtil, quranInfo +) { + private val scope = MainScope() + private var cachedTranslations = emptyList() + + init { + translationListPresenter.translations() + .onEach { translations -> + cachedTranslations = translations + translationScreen?.onTranslationsUpdated(translations) + } + .launchIn(scope) + } + + fun refresh(verseRange: VerseRange) { + disposable?.dispose() + disposable = getVerses(false, getTranslations(quranSettings), verseRange) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeWith(object : DisposableSingleObserver() { + override fun onSuccess(result: ResultHolder) { + translationScreen?.setVerses(result.translations, result.ayahInformation) + } + + override fun onError(e: Throwable) {} + }) + } + + override fun bind(what: TranslationScreen) { + super.bind(what) + val translations = cachedTranslations + if (translations.isNotEmpty()) { + what.onTranslationsUpdated(translations) + } + } + + interface TranslationScreen { + fun setVerses(translations: Array, verses: List) + fun onTranslationsUpdated(translations: List) + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt index d47dd4a6c3..7b2dc1a8a5 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt @@ -14,9 +14,22 @@ import javax.inject.Inject class TranslationListPresenter @Inject constructor( private val dataSource: TranslationsDataSource ) { + private val scope = MainScope() + fun translations(): Flow> { return dataSource.translations() .filterNotNull() .map { translations -> translations.sortedBy { it.displayOrder } } } + + fun registerForTranslations(callback: TranslationListCallback): Job { + return translations() + .onEach { translations -> + callback.onTranslationsUpdated( + titles = translations.map { translation -> translation.resolveTranslatorName() }.toTypedArray(), + translations = translations + ) + } + .launchIn(scope) + } } 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 8de8d34960..5154c66d05 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 @@ -21,7 +21,6 @@ import android.os.Handler; import android.os.Message; import android.preference.PreferenceManager; -import android.text.TextUtils; import android.util.SparseBooleanArray; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -68,14 +67,12 @@ import com.quran.labs.androidquran.SearchActivity; import com.quran.labs.androidquran.bridge.AudioEventPresenterBridge; import com.quran.labs.androidquran.bridge.ReadingEventPresenterBridge; -import com.quran.labs.androidquran.common.LocalTranslationDisplaySort; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.common.audio.model.QariItem; import com.quran.labs.androidquran.dao.audio.AudioRequest; import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.data.QuranDataProvider; import com.quran.labs.androidquran.data.QuranDisplayData; -import com.quran.labs.androidquran.database.TranslationsDBAdapter; import com.quran.labs.androidquran.di.component.activity.PagerActivityComponent; import com.quran.labs.androidquran.model.bookmark.BookmarkModel; import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils; @@ -83,6 +80,7 @@ import com.quran.labs.androidquran.presenter.bookmark.RecentPagePresenter; import com.quran.labs.androidquran.presenter.data.QuranEventLogger; import com.quran.labs.androidquran.presenter.recitation.PagerActivityRecitationPresenter; +import com.quran.labs.androidquran.presenter.translationlist.TranslationListPresenter; import com.quran.labs.androidquran.service.AudioService; import com.quran.labs.androidquran.service.QuranDownloadService; import com.quran.labs.androidquran.service.util.DefaultDownloadReceiver; @@ -130,11 +128,10 @@ import com.quran.reading.common.ReadingEventPresenter; 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; +import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -142,11 +139,10 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.observers.DisposableObserver; import io.reactivex.rxjava3.observers.DisposableSingleObserver; -import io.reactivex.rxjava3.schedulers.Schedulers; +import kotlinx.coroutines.Job; import timber.log.Timber; /** @@ -233,7 +229,6 @@ public class PagerActivity extends AppCompatActivity implements @Inject QuranSettings quranSettings; @Inject QuranScreenInfo quranScreenInfo; @Inject ArabicDatabaseUtils arabicDatabaseUtils; - @Inject TranslationsDBAdapter translationsDBAdapter; @Inject QuranAppUtils quranAppUtils; @Inject ShareUtil shareUtil; @Inject AudioUtils audioUtils; @@ -248,10 +243,12 @@ public class PagerActivity extends AppCompatActivity implements @Inject PageViewFactoryProvider pageProviderFactoryProvider; @Inject Set additionalAyahPanels; @Inject PagerActivityRecitationPresenter pagerActivityRecitationPresenter; + @Inject TranslationListPresenter translationListPresenter; private AudioEventPresenterBridge audioEventPresenterBridge; private ReadingEventPresenterBridge readingEventPresenterBridge; + private Job translationJob; private CompositeDisposable compositeDisposable; private final CompositeDisposable foregroundDisposable = new CompositeDisposable(); @@ -543,6 +540,9 @@ public void onPageSelected(int position) { registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { audioPresenter.onPostNotificationsPermissionResponse(isGranted); }); + + // read the list of translations + requestTranslationsList(); } @Override @@ -761,9 +761,6 @@ public void onResume() { recentPagePresenter.bind(this); isInMultiWindowMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInMultiWindowMode(); - // read the list of translations - requestTranslationsList(); - if (shouldReconnect) { foregroundDisposable.add(Completable.timer(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) @@ -971,6 +968,7 @@ public void onPause() { } recentPagePresenter.unbind(this); quranSettings.setWasShowingTranslation(pagerAdapter.isShowingTranslation()); + super.onPause(); } @@ -996,6 +994,9 @@ protected void onDestroy() { downloadReceiver = null; } + if (translationJob != null) { + translationJob.cancel(new CancellationException()); + } currentQariBridge.unsubscribeAll(); compositeDisposable.dispose(); audioEventPresenterBridge.dispose(); @@ -1372,56 +1373,29 @@ private void ensurePage(int sura, int ayah) { } private void requestTranslationsList() { - compositeDisposable.add( - Single.fromCallable(() -> - translationsDBAdapter.legacyGetTranslations()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(new DisposableSingleObserver>() { - @Override - public void onSuccess(@NonNull List translationList) { - 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 = sortedTranslations.get(i); - if (!TextUtils.isEmpty(item.getTranslatorForeign())) { - titles[i] = item.getTranslatorForeign(); - } else if (!TextUtils.isEmpty(item.getTranslator())) { - titles[i] = item.getTranslator(); - } else { - titles[i] = item.getName(); - } - } - - Set currentActiveTranslationsFilesNames = quranSettings.getActiveTranslations(); - if (currentActiveTranslationsFilesNames.isEmpty() && items > 0) { - currentActiveTranslationsFilesNames = new HashSet<>(); - for (int i = 0; i < items; i++) { - currentActiveTranslationsFilesNames.add(sortedTranslations.get(i).getFilename()); - } - } - activeTranslationsFilesNames = currentActiveTranslationsFilesNames; - - if (translationsSpinnerAdapter != null) { - translationsSpinnerAdapter - .updateItems(titles, sortedTranslations, activeTranslationsFilesNames); - } - translationNames = titles; - translations = sortedTranslations; - - if (showingTranslation) { - // Since translation items have changed, need to - updateActionBarSpinner(); - } - } + translationJob = translationListPresenter.registerForTranslations((titles, updatedTranslations) -> { + Set currentActiveTranslationsFilesNames = quranSettings.getActiveTranslations(); + if (currentActiveTranslationsFilesNames.isEmpty() && !updatedTranslations.isEmpty()) { + currentActiveTranslationsFilesNames = new HashSet<>(); + final int items = updatedTranslations.size(); + for (int i = 0; i < items; i++) { + currentActiveTranslationsFilesNames.add(updatedTranslations.get(i).getFilename()); + } + } + activeTranslationsFilesNames = currentActiveTranslationsFilesNames; - @Override - public void onError(@NonNull Throwable e) { - } - })); + if (translationsSpinnerAdapter != null) { + translationsSpinnerAdapter + .updateItems(titles, updatedTranslations, activeTranslationsFilesNames); + } + translationNames = titles; + translations = updatedTranslations; + + if (showingTranslation) { + // Since translation items have changed, need to + updateActionBarSpinner(); + } + }); } private void toggleBookmark(final Integer sura, final Integer ayah, final int page) { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt index 452105050e..11248eeac5 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt @@ -14,6 +14,7 @@ import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.presenter.translation.InlineTranslationPresenter import com.quran.labs.androidquran.presenter.translation.InlineTranslationPresenter.TranslationScreen +import com.quran.labs.androidquran.presenter.translationlist.TranslationListPresenter import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter import com.quran.labs.androidquran.ui.util.TranslationsSpinnerAdapter @@ -22,6 +23,10 @@ import com.quran.labs.androidquran.view.InlineTranslationView import com.quran.labs.androidquran.view.QuranSpinner import com.quran.mobile.di.AyahActionFragmentProvider import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import javax.inject.Inject import kotlin.math.abs @@ -99,33 +104,20 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { } } - public override fun refreshView() { - val start = start - val end = end - if (start == null || end == null) { - return - } - val activity: Activity? = activity - if (activity is PagerActivity) { - val translations = activity.translations - if (translations == null || translations.size == 0) { - progressBar.visibility = View.GONE - emptyState.visibility = View.VISIBLE - translationControls.visibility = View.GONE - return - } - - var activeTranslationsFilesNames = activity.activeTranslationsFilesNames - if (activeTranslationsFilesNames == null) { - activeTranslationsFilesNames = quranSettings.activeTranslations - } + override fun onTranslationsUpdated(translations: List) { + if (translations.isEmpty()) { + progressBar.visibility = View.GONE + emptyState.visibility = View.VISIBLE + translationControls.visibility = View.GONE + } else { + val activeTranslationsFilesNames = quranSettings.activeTranslations val adapter = translationAdapter if (adapter == null) { translationAdapter = TranslationsSpinnerAdapter( activity, R.layout.translation_ab_spinner_item, - activity.translationNames, + translations.map { it.resolveTranslatorName() }.toTypedArray(), translations, activeTranslationsFilesNames ) { selectedItems: Set? -> @@ -135,24 +127,33 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { translator.adapter = translationAdapter } else { adapter.updateItems( - activity.translationNames, + translations.map { it.resolveTranslatorName() }.toTypedArray(), translations, activeTranslationsFilesNames ) + refreshView() } - if (start == end) { - translationControls.visibility = View.VISIBLE - } else { - translationControls.visibility = View.GONE - } - val verses = 1 + abs( - quranInfo.getAyahId(start.sura, start.ayah) - quranInfo.getAyahId(end.sura, end.ayah) - ) - val verseRange = VerseRange(start.sura, start.ayah, end.sura, end.ayah, verses) - translationPresenter.refresh(verseRange) } } + public override fun refreshView() { + val start = start + val end = end + if (start == null || end == null) { + return + } + if (start == end) { + translationControls.visibility = View.VISIBLE + } else { + translationControls.visibility = View.GONE + } + val verses = 1 + abs( + quranInfo.getAyahId(start.sura, start.ayah) - quranInfo.getAyahId(end.sura, end.ayah) + ) + val verseRange = VerseRange(start.sura, start.ayah, end.sura, end.ayah, verses) + translationPresenter.refresh(verseRange) + } + override fun setVerses(translations: Array, verses: List) { progressBar.visibility = View.GONE if (verses.isNotEmpty()) { 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 2bdf9b149d..2d9c6f5e65 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 @@ -145,7 +145,7 @@ public void setVerses(@NonNull QuranDisplayData quranDisplayData, rows.add( new TranslationViewRow(TranslationViewRow.Type.TRANSLATOR, verse, - sortedTranslations[j].getTranslatorName())); + sortedTranslations[j].resolveTranslatorName())); } rows.add(new TranslationViewRow( TranslationViewRow.Type.TRANSLATION_TEXT, verse, text, j, diff --git a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt index 811567969f..3c17285e79 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt +++ b/app/src/main/java/com/quran/labs/androidquran/util/ShareUtil.kt @@ -73,7 +73,7 @@ class ShareUtil @Inject internal constructor(private val quranDisplayData: Quran if (text.isNotEmpty()) { append("\n\n") if (i < translationNames.size) { - append(translationNames[i].getTranslatorName()) + append(translationNames[i].resolveTranslatorName()) append(":\n") } diff --git a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt index 6fd1e06fac..dd394e6436 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt @@ -129,7 +129,7 @@ class InlineTranslationView @JvmOverloads constructor( builder.append("\n\n") } val start = builder.length - builder.append(translations[i].getTranslatorName()) + builder.append(translations[i].resolveTranslatorName()) builder.setSpan( StyleSpan(Typeface.BOLD), start, builder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE 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 1d2efaa0f3..6a7fe8c3f3 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 @@ -53,7 +53,7 @@ class BaseTranslationPresenterTest { @Test fun testHashlessGetTranslationNames() { val databases = listOf("one.db", "two.db") - val map = HashMap() + val map = HashMap() val translations = presenter.getTranslations(databases, map) assertThat(translations).hasLength(2) diff --git a/common/translation/src/main/kotlin/com/quran/mobile/translation/model/LocalTranslation.kt b/common/translation/src/main/kotlin/com/quran/mobile/translation/model/LocalTranslation.kt index d567e02266..a69fd4b5ee 100644 --- a/common/translation/src/main/kotlin/com/quran/mobile/translation/model/LocalTranslation.kt +++ b/common/translation/src/main/kotlin/com/quran/mobile/translation/model/LocalTranslation.kt @@ -12,7 +12,7 @@ data class LocalTranslation( val minimumVersion: Int = 2, val displayOrder: Int = -1) { - fun getTranslatorName(): String { + fun resolveTranslatorName(): String { return when { !translatorForeign.isNullOrEmpty() -> translatorForeign !translator.isNullOrEmpty() -> translator From ad8c1a20f24b25e6c9aa793c4a862988fda6f714 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 13:25:25 +0400 Subject: [PATCH 193/258] Kotlinify TranslationManagerPresenter --- .../database/TranslationsDBAdapter.kt | 2 - .../TranslationManagerPresenter.java | 317 ------------------ .../TranslationManagerPresenter.kt | 286 ++++++++++++++++ .../TranslationManagerPresenterTest.kt | 5 +- 4 files changed, 289 insertions(+), 321 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt index 410f265f45..7217e1b458 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt @@ -8,7 +8,6 @@ import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.mobile.di.qualifier.ApplicationContext import com.quran.mobile.translation.data.TranslationsDataSource import com.quran.mobile.translation.model.LocalTranslation -import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first @@ -23,7 +22,6 @@ class TranslationsDBAdapter @Inject constructor( private val dataSource: TranslationsDataSource, private val quranFileUtils: QuranFileUtils ) { - private val scope = MainScope() fun getTranslations(): Flow> { return dataSource.translations() 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 deleted file mode 100644 index f301894aea..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java +++ /dev/null @@ -1,317 +0,0 @@ -package com.quran.labs.androidquran.presenter.translation; - -import android.content.Context; -import android.util.Pair; -import android.util.SparseArray; - -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; - -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.quran.mobile.di.qualifier.ApplicationContext; -import com.quran.mobile.translation.model.LocalTranslation; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Observable; -import io.reactivex.rxjava3.observers.DisposableObserver; -import io.reactivex.rxjava3.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 { - private static final String WEB_SERVICE_ENDPOINT = "data/translations.php?v=5"; - private static final String CACHED_RESPONSE_FILE_NAME = "translations.v5.cache"; - - private final Context appContext; - private final OkHttpClient okHttpClient; - private final QuranSettings quranSettings; - private final QuranFileUtils quranFileUtils; - private final TranslationsDBAdapter translationsDBAdapter; - - @VisibleForTesting String host; - private TranslationManagerActivity currentActivity; - - @Inject - TranslationManagerPresenter(@ApplicationContext Context appContext, - OkHttpClient okHttpClient, - QuranSettings quranSettings, - TranslationsDBAdapter dbAdapter, - QuranFileUtils quranFileUtils) { - this.host = Constants.HOST; - this.appContext = appContext; - this.okHttpClient = okHttpClient; - this.quranSettings = quranSettings; - this.quranFileUtils = quranFileUtils; - this.translationsDBAdapter = dbAdapter; - } - - public void checkForUpdates() { - getTranslationsList(true); - } - - public void getTranslationsList(boolean forceDownload) { - final boolean isCacheStale = System.currentTimeMillis() - - quranSettings.getLastUpdatedTranslationDate() > Constants.MIN_TRANSLATION_REFRESH_TIME; - final Observable source = - Observable.concat(getCachedTranslationListObservable(), getRemoteTranslationListObservable()); - final Observable observableSource; - if (forceDownload) { - // we only force if we pulled to refresh or are refreshing in the background, - // implying that we have data on the screen already (or don't need data in the - // background case), so just get remote data. - observableSource = getRemoteTranslationListObservable(); - } else if (isCacheStale) { - observableSource = source; - } else { - observableSource = source.take(1); - } - - observableSource - .filter(translationList -> !translationList.getTranslations().isEmpty()) - .map(translationList -> mergeWithServerTranslations(translationList.getTranslations())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(new DisposableObserver>() { - @Override - public void onNext(@NonNull List 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 (!(e instanceof IOException)) { - Timber.e(e, "error updating translations list"); - } - - if (currentActivity != null) { - currentActivity.onErrorDownloadTranslations(); - } - } - - @Override - public void onComplete() { - } - }); - } - - public void updateItem(final TranslationItem item) { - Observable.fromCallable(() -> { - // for upgrades, remove the old file to stop the tafseer from showing up - // twice. this happens because old and new tafaseer (ex ibn kathir) have - // different ids when they target different schema versions, and so the - // old file needs to be removed from the database explicitly - final Translation translation = item.getTranslation(); - if (translation.getMinimumVersion() >= 5) { - translationsDBAdapter.legacyDeleteTranslationByFileName(translation.getFileName()); - } - return translationsDBAdapter.legacyWriteTranslationUpdates(Collections.singletonList(item)); - } - ).subscribeOn(Schedulers.io()) - .subscribe(); - } - - public void updateItemOrdering(final List items) { - Observable.fromCallable(() -> translationsDBAdapter.legacyWriteTranslationUpdates(items)) - .subscribeOn(Schedulers.io()) - .subscribe(); - } - - Observable getCachedTranslationListObservable() { - return Observable.defer(() -> { - try { - File cachedFile = getCachedFile(); - if (cachedFile.exists()) { - Moshi moshi = new Moshi.Builder().build(); - JsonAdapter jsonAdapter = moshi.adapter(TranslationList.class); - final TranslationList list = jsonAdapter.fromJson(Okio.buffer(Okio.source(cachedFile))); - if (list != null) { - return Observable.just(list); - } - } - } catch (Exception e) { - Timber.e(e); - } - return Observable.empty(); - }); - } - - Observable getRemoteTranslationListObservable() { - final String url = host + WEB_SERVICE_ENDPOINT; - return - downloadTranslationList(url) - .onErrorResumeWith(downloadTranslationList(url)) - .doOnNext(translationList -> { - translationList.getTranslations(); - if (!translationList.getTranslations().isEmpty()) { - writeTranslationList(translationList); - } - }); - } - - private Observable downloadTranslationList(String url) { - return Observable.fromCallable(() -> { - Request request = new Request.Builder() - .url(url) - .build(); - Response response = okHttpClient.newCall(request).execute(); - - Moshi moshi = new Moshi.Builder().build(); - JsonAdapter jsonAdapter = moshi.adapter(TranslationList.class); - - ResponseBody responseBody = response.body(); - TranslationList result = jsonAdapter.fromJson(responseBody.source()); - responseBody.close(); - return result; - }); - } - - 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 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(); - Timber.e(e); - } - } - - private File getCachedFile() { - String dir = quranFileUtils.getQuranDatabaseDirectory(appContext); - return new File(dir + File.separator + CACHED_RESPONSE_FILE_NAME); - } - - private List mergeWithServerTranslations(List serverTranslations) { - List results = new ArrayList<>(serverTranslations.size()); - SparseArray localTranslations = translationsDBAdapter.getTranslationsHash(); - String databaseDir = quranFileUtils.getQuranDatabaseDirectory(appContext); - - List updates = new ArrayList<>(); - for (int i = 0, count = serverTranslations.size(); i < count; i++) { - Translation translation = serverTranslations.get(i); - LocalTranslation local = localTranslations.get(translation.getId()); - - File dbFile = new File(databaseDir, translation.getFileName()); - boolean exists = dbFile.exists(); - - TranslationItem item; - TranslationItem override = null; - if (exists) { - if (local == null) { - // text version, schema version - final Pair versions = getVersionFromDatabase(translation.getFileName()); - item = new TranslationItem(translation, versions.first); - if (versions.second != translation.getMinimumVersion()) { - // schema change, write downloaded schema version to the db and return server item - override = new TranslationItem(translation.withSchema(versions.second), versions.first); - } - } else { - item = new TranslationItem(translation, local.getVersion(), local.getDisplayOrder()); - } - } 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)) { - if (override != null && item.getTranslation().getMinimumVersion() >= 5) { - // certain schema changes, especially those going to v5, keep the same filename while - // changing the database entry id. this could cause duplicate entries in the database. - // work around it by removing the existing entries before doing the updates. - translationsDBAdapter.legacyDeleteTranslationByFileName(override.getTranslation().getFileName()); - } - updates.add(override == null ? item : override); - } else if (local != null && local.getLanguageCode() == null) { - // older items don't have a language code - updates.add(item); - } - results.add(item); - } - - if (!updates.isEmpty()) { - translationsDBAdapter.legacyWriteTranslationUpdates(updates); - } - return results; - } - - private Pair getVersionFromDatabase(String filename) { - try { - DatabaseHandler handler = - DatabaseHandler.getDatabaseHandler(appContext, filename, quranFileUtils); - if (handler.validDatabase()) { - return new Pair<>(handler.getTextVersion(), handler.getSchemaVersion()); - } - } catch (Exception e) { - Timber.d(e, "exception opening database: %s", filename); - } - return new Pair<>(0, 0); - } - - - @Override - public void bind(TranslationManagerActivity activity) { - currentActivity = activity; - } - - @Override - public void unbind(TranslationManagerActivity activity) { - if (activity == currentActivity) { - currentActivity = null; - } - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt new file mode 100644 index 0000000000..9da7506fec --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt @@ -0,0 +1,286 @@ +package com.quran.labs.androidquran.presenter.translation + +import android.content.Context +import android.util.Pair +import androidx.annotation.VisibleForTesting +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.Companion.getDatabaseHandler +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.quran.mobile.di.qualifier.ApplicationContext +import com.squareup.moshi.Moshi +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableSource +import io.reactivex.rxjava3.functions.Supplier +import io.reactivex.rxjava3.observers.DisposableObserver +import io.reactivex.rxjava3.schedulers.Schedulers +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.buffer +import okio.sink +import okio.source +import timber.log.Timber +import java.io.File +import java.io.IOException +import java.util.concurrent.Callable +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +open class TranslationManagerPresenter @Inject internal constructor( + @param:ApplicationContext private val appContext: Context, + private val okHttpClient: OkHttpClient, + private val quranSettings: QuranSettings, + private val translationsDBAdapter: TranslationsDBAdapter, + private val quranFileUtils: QuranFileUtils +) : Presenter { + @VisibleForTesting + var host: String = Constants.HOST + + private var currentActivity: TranslationManagerActivity? = null + + fun checkForUpdates() { + getTranslationsList(true) + } + + fun getTranslationsList(forceDownload: Boolean) { + val isCacheStale = System.currentTimeMillis() - + quranSettings.lastUpdatedTranslationDate > Constants.MIN_TRANSLATION_REFRESH_TIME + val source: Observable = Observable.concat( + cachedTranslationListObservable, remoteTranslationListObservable + ) + val observableSource: Observable = if (forceDownload) { + // we only force if we pulled to refresh or are refreshing in the background, + // implying that we have data on the screen already (or don't need data in the + // background case), so just get remote data. + remoteTranslationListObservable + } else if (isCacheStale) { + source + } else { + source.take(1) + } + observableSource + .filter { translationList: TranslationList -> translationList.translations.isNotEmpty() } + .map { translationList: TranslationList -> + mergeWithServerTranslations(translationList.translations) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(object : DisposableObserver>() { + override fun onNext(translationItems: List) { + currentActivity?.onTranslationsUpdated(translationItems) + + // used for marking upgrades, irrespective of whether or not there is a bound activity + var updatedTranslations = false + for (item in translationItems) { + if (item.needsUpgrade()) { + updatedTranslations = true + break + } + } + quranSettings.setHaveUpdatedTranslations(updatedTranslations) + } + + override fun onError(e: Throwable) { + if (e !is IOException) { + Timber.e(e, "error updating translations list") + } + currentActivity?.onErrorDownloadTranslations() + } + + override fun onComplete() {} + }) + } + + fun updateItem(item: TranslationItem) { + Observable.fromCallable { + + // for upgrades, remove the old file to stop the tafseer from showing up + // twice. this happens because old and new tafaseer (ex ibn kathir) have + // different ids when they target different schema versions, and so the + // old file needs to be removed from the database explicitly + val (_, minimumVersion, _, _, _, fileName) = item.translation + if (minimumVersion >= 5) { + translationsDBAdapter.legacyDeleteTranslationByFileName(fileName) + } + translationsDBAdapter.legacyWriteTranslationUpdates(listOf(item)) + }.subscribeOn(Schedulers.io()) + .subscribe() + } + + fun updateItemOrdering(items: List) { + Observable.fromCallable { translationsDBAdapter.legacyWriteTranslationUpdates(items) } + .subscribeOn(Schedulers.io()) + .subscribe() + } + + val cachedTranslationListObservable: Observable + get() = Observable.defer(Supplier> { + try { + val cachedFile = cachedFile + if (cachedFile.exists()) { + val moshi = Moshi.Builder().build() + val jsonAdapter = moshi.adapter( + TranslationList::class.java + ) + val list = jsonAdapter.fromJson(cachedFile.source().buffer()) + if (list != null) { + return@Supplier Observable.just(list) + } + } + } catch (e: Exception) { + Timber.e(e) + } + Observable.empty() + }) + val remoteTranslationListObservable: Observable + get() { + val url = host + WEB_SERVICE_ENDPOINT + return downloadTranslationList(url) + .onErrorResumeWith(downloadTranslationList(url)) + .doOnNext { translationList: TranslationList -> + translationList.translations + if (translationList.translations.isNotEmpty()) { + writeTranslationList(translationList) + } + } + } + + private fun downloadTranslationList(url: String): Observable { + return Observable.fromCallable(Callable { + val request: Request = Request.Builder() + .url(url) + .build() + val response = okHttpClient.newCall(request).execute() + val moshi = Moshi.Builder().build() + val jsonAdapter = moshi.adapter( + TranslationList::class.java + ) + val responseBody = response.body + val result = jsonAdapter.fromJson(responseBody!!.source()) + responseBody.close() + result + }) + } + + open fun writeTranslationList(list: TranslationList) { + val cacheFile = cachedFile + try { + val directory = cacheFile.getParentFile() + val directoryExists = directory.mkdirs() || directory.isDirectory() + if (directoryExists) { + if (cacheFile.exists()) { + cacheFile.delete() + } + val moshi = Moshi.Builder().build() + val jsonAdapter = moshi.adapter( + TranslationList::class.java + ) + val sink = cacheFile.sink().buffer() + jsonAdapter.toJson(sink, list) + sink.close() + quranSettings.lastUpdatedTranslationDate = System.currentTimeMillis() + } + } catch (e: Exception) { + cacheFile.delete() + Timber.e(e) + } + } + + private val cachedFile: File + get() { + val dir = quranFileUtils.getQuranDatabaseDirectory(appContext) + return File(dir + File.separator + CACHED_RESPONSE_FILE_NAME) + } + + private fun mergeWithServerTranslations(serverTranslations: List): List { + val results: MutableList = ArrayList(serverTranslations.size) + val localTranslations = translationsDBAdapter.getTranslationsHash() + val databaseDir = quranFileUtils.getQuranDatabaseDirectory(appContext) + val updates: MutableList = ArrayList() + var i = 0 + val count = serverTranslations.size + while (i < count) { + val translation = serverTranslations[i] + val local = localTranslations[translation.id] + val dbFile = File(databaseDir, translation.fileName) + var exists = dbFile.exists() + var item: TranslationItem + var override: TranslationItem? = null + if (exists) { + if (local == null) { + // text version, schema version + val versions = getVersionFromDatabase(translation.fileName) + item = TranslationItem(translation, versions.first) + if (versions.second != translation.minimumVersion) { + // schema change, write downloaded schema version to the db and return server item + override = + TranslationItem(translation.withSchema(versions.second), versions.first) + } + } else { + item = TranslationItem(translation, local.version, local.displayOrder) + } + } else { + item = 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) { + if (override != null && item.translation.minimumVersion >= 5) { + // certain schema changes, especially those going to v5, keep the same filename while + // changing the database entry id. this could cause duplicate entries in the database. + // work around it by removing the existing entries before doing the updates. + translationsDBAdapter.legacyDeleteTranslationByFileName(override.translation.fileName) + } + updates.add(override ?: item) + } else if (local != null && local.languageCode == null) { + // older items don't have a language code + updates.add(item) + } + results.add(item) + i++ + } + if (!updates.isEmpty()) { + translationsDBAdapter.legacyWriteTranslationUpdates(updates) + } + return results + } + + private fun getVersionFromDatabase(filename: String): Pair { + try { + val handler = getDatabaseHandler(appContext, filename, quranFileUtils) + if (handler.validDatabase()) { + return Pair(handler.getTextVersion(), handler.getSchemaVersion()) + } + } catch (e: Exception) { + Timber.d(e, "exception opening database: %s", filename) + } + return Pair(0, 0) + } + + override fun bind(what: TranslationManagerActivity) { + currentActivity = what + } + + override fun unbind(what: TranslationManagerActivity) { + if (what === currentActivity) { + currentActivity = null + } + } + + companion object { + private const val WEB_SERVICE_ENDPOINT = "data/translations.php?v=5" + private const val CACHED_RESPONSE_FILE_NAME = "translations.v5.cache" + } +} diff --git a/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt b/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt index ad019a9569..d7988f1e16 100644 --- a/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt +++ b/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt @@ -3,6 +3,7 @@ package com.quran.labs.androidquran.presenter.translation import android.content.Context import com.google.common.truth.Truth import com.quran.labs.androidquran.dao.translation.TranslationList +import com.quran.labs.androidquran.database.TranslationsDBAdapter import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.awaitTerminalEvent @@ -35,10 +36,10 @@ class TranslationManagerPresenterTest { val mockOkHttp = OkHttpClient.Builder().build() mockWebServer = MockWebServer() translationManager = object : TranslationManagerPresenter( - mockAppContext, mockOkHttp, mockSettings, null, + mockAppContext, mockOkHttp, mockSettings, mock(TranslationsDBAdapter::class.java), mock(QuranFileUtils::class.java) ) { - public override fun writeTranslationList(list: TranslationList) { + override fun writeTranslationList(list: TranslationList) { // no op } } From 5ec9c6275ca7fa068bbb47d0f5c17474116ffbd7 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 14:19:50 +0400 Subject: [PATCH 194/258] Kotlinify TranslationManagerActivity --- .../ui/TranslationManagerActivity.java | 545 ------------------ .../ui/TranslationManagerActivity.kt | 521 +++++++++++++++++ 2 files changed, 521 insertions(+), 545 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt 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 deleted file mode 100644 index 60f51e2648..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java +++ /dev/null @@ -1,545 +0,0 @@ -package com.quran.labs.androidquran.ui; - -import android.content.Intent; -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 androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.view.ActionMode; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.snackbar.Snackbar; -import com.quran.labs.androidquran.QuranApplication; -import com.quran.labs.androidquran.R; -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; -import com.quran.labs.androidquran.service.QuranDownloadService; -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; - -import javax.inject.Inject; - -import io.reactivex.rxjava3.disposables.Disposable; -import timber.log.Timber; - -public class TranslationManagerActivity extends AppCompatActivity - implements DefaultDownloadReceiver.SimpleDownloadListener, DownloadedMenuActionListener { - - public static final String TRANSLATION_DOWNLOAD_KEY = "TRANSLATION_DOWNLOAD_KEY"; - private static final String UPGRADING_EXTENSION = ".old"; - - private List allItems; - private List currentSortedDownloads; - private List originalSortedDownloads; - - private SparseIntArray translationPositions; - - private TranslationsAdapter adapter; - private TranslationItem downloadingItem; - private String databaseDirectory; - private QuranSettings quranSettings; - private DefaultDownloadReceiver downloadReceiver = null; - - 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; - - SwipeRefreshLayout translationSwipeRefresh; - RecyclerView translationRecycler; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ((QuranApplication) getApplication()).getApplicationComponent().inject(this); - setContentView(R.layout.translation_manager); - translationSwipeRefresh = findViewById(R.id.translation_swipe_refresh); - translationRecycler = findViewById(R.id.translation_recycler); - - RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(this); - translationRecycler.setLayoutManager(layoutManager); - - adapter = new TranslationsAdapter(this); - translationRecycler.setAdapter(adapter); - selectionListener = new TranslationSelectionListener(adapter); - - databaseDirectory = quranFileUtils.getQuranDatabaseDirectory(this); - - final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setTitle(R.string.prefs_translations); - } - - 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); - translationSwipeRefresh.setRefreshing(true); - presenter.getTranslationsList(false); - } - - @Override - public void onStop() { - if (downloadReceiver != null) { - downloadReceiver.setListener(null); - LocalBroadcastManager.getInstance(this) - .unregisterReceiver(downloadReceiver); - downloadReceiver = null; - } - super.onStop(); - } - - @Override - protected void onDestroy() { - presenter.unbind(this); - onClickDownloadDisposable.dispose(); - onClickRemoveDisposable.dispose(); - onClickRankUpDisposable.dispose(); - onClickRankDownDisposable.dispose(); - super.onDestroy(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - @Override - public void handleDownloadSuccess() { - if (downloadingItem != null) { - if (downloadingItem.exists()) { - try { - File f = new File(databaseDirectory, - downloadingItem.getTranslation().getFileName() + UPGRADING_EXTENSION); - if (f.exists()) { - f.delete(); - } - } catch (Exception e) { - Timber.d(e, "error removing old database file"); - } - } - - // TODO: we can avoid the cost of sorting once we can listen to db updates - // in which case we'd set the local version as -1 so it gets properly assigned after. - final List sortedItems = sortedDownloadedItems(); - int lastDisplayOrder = sortedItems.isEmpty() ? 0 : - sortedItems.get(sortedItems.size() - 1).getDisplayOrder(); - final Translation translation = downloadingItem.getTranslation(); - updateTranslationItem(downloadingItem.withLocalVersionAndDisplayOrder( - translation.getCurrentVersion(), lastDisplayOrder + 1) - ); - - // update active translations and add this item to it - QuranSettings settings = QuranSettings.getInstance(this); - Set activeTranslations = settings.getActiveTranslations(); - activeTranslations.add(downloadingItem.getTranslation().getFileName()); - settings.setActiveTranslations(activeTranslations); - } - downloadingItem = null; - generateListItems(); - } - - @Override - public void handleDownloadFailure(int errId) { - if (downloadingItem != null && downloadingItem.exists()) { - try { - File f = new File(databaseDirectory, - downloadingItem.getTranslation().getFileName() + UPGRADING_EXTENSION); - File destFile = new File(databaseDirectory, downloadingItem.getTranslation().getFileName()); - if (f.exists() && !destFile.exists()) { - f.renameTo(destFile); - } else { - f.delete(); - } - } catch (Exception e) { - Timber.d(e, "error restoring translation after failed download"); - } - } - downloadingItem = null; - } - - private void onRefresh() { - presenter.getTranslationsList(true); - } - - private void updateTranslationItem(TranslationItem updated) { - int id = updated.getTranslation().getId(); - int allItemsIndex = translationPositions.get(id); - if (allItems != null && allItems.size() > allItemsIndex) { - allItems.remove(allItemsIndex); - allItems.add(allItemsIndex, updated); - } - presenter.updateItem(updated); - } - - private void updateDownloadedItems() { - final List translations = adapter.getTranslations(); - final int downloadedItemCount = currentSortedDownloads.size(); - if (downloadedItemCount + 1 <= translations.size()) { - for (int i = 0; i < downloadedItemCount; i++) { - translations.remove(1); - } - - translations.addAll(1, currentSortedDownloads); - adapter.setTranslations(translations); - adapter.notifyDataSetChanged(); - } - } - - public void onErrorDownloadTranslations() { - translationSwipeRefresh.setRefreshing(false); - Snackbar - .make(translationRecycler, R.string.error_getting_translation_list, Snackbar.LENGTH_SHORT) - .show(); - } - - public void onTranslationsUpdated(List items) { - translationSwipeRefresh.setRefreshing(false); - SparseIntArray itemsSparseArray = new SparseIntArray(items.size()); - for (int i = 0, itemsSize = items.size(); i < itemsSize; i++) { - TranslationItem item = items.get(i); - itemsSparseArray.put(item.getTranslation().getId(), i); - } - allItems = items; - translationPositions = itemsSparseArray; - - generateListItems(); - } - - private void generateListItems() { - if (allItems == null) { - return; - } - - List downloaded = new ArrayList<>(); - List notDownloaded = new ArrayList<>(); - for (int i = 0, allItemsSize = allItems.size(); i < allItemsSize; i++) { - TranslationItem item = allItems.get(i); - if (item.exists()) { - downloaded.add(item); - } else { - notDownloaded.add(item); - } - } - - List result = new ArrayList<>(); - if (downloaded.size() > 0) { - TranslationHeader hdr = new TranslationHeader(getString(R.string.downloaded_translations)); - result.add(hdr); - - // sort by display order - Collections.sort(downloaded, new TranslationItemDisplaySort()); - - boolean needsUpgrade = false; - for (TranslationItem item : downloaded) { - result.add(item); - needsUpgrade = needsUpgrade || item.needsUpgrade(); - } - - if (!needsUpgrade) { - quranSettings.setHaveUpdatedTranslations(false); - } - } - originalSortedDownloads = new ArrayList<>(downloaded); - currentSortedDownloads = new ArrayList<>(downloaded); - - result.add(new TranslationHeader(getString(R.string.available_translations))); - - result.addAll(notDownloaded); - - adapter.setTranslations(result); - adapter.notifyDataSetChanged(); - } - - private void downloadItem(TranslationRowData translationRowData) { - TranslationItem selectedItem = (TranslationItem) translationRowData; - if (selectedItem.exists() && !selectedItem.needsUpgrade()) { - return; - } - - downloadingItem = selectedItem; - - final Translation translation = selectedItem.getTranslation(); - DatabaseHandler.clearDatabaseHandlerIfExists(translation.getFileName()); - if (downloadReceiver == null) { - downloadReceiver = new DefaultDownloadReceiver(this, - QuranDownloadService.DOWNLOAD_TYPE_TRANSLATION); - LocalBroadcastManager.getInstance(this).registerReceiver( - downloadReceiver, new IntentFilter( - QuranDownloadNotifier.ProgressIntent.INTENT_NAME)); - } - downloadReceiver.setListener(this); - - // actually start the download - String url = translation.getFileUrl(); - String destination = databaseDirectory; - Timber.d("downloading %s to %s", url, destination); - - if (selectedItem.exists()) { - try { - File f = new File(destination, translation.getFileName()); - if (f.exists()) { - File newPath = new File(destination, - translation.getFileName() + UPGRADING_EXTENSION); - if (newPath.exists()) { - newPath.delete(); - } - f.renameTo(newPath); - } - } catch (Exception e) { - Timber.d(e, "error backing database file up"); - } - } - - // start the download - String notificationTitle = selectedItem.name(); - Intent intent = ServiceIntentHelper.getDownloadIntent(this, url, - destination, notificationTitle, TRANSLATION_DOWNLOAD_KEY, - QuranDownloadService.DOWNLOAD_TYPE_TRANSLATION); - String filename = selectedItem.getTranslation().getFileName(); - if (url.endsWith("zip")) { - filename += ".zip"; - } - intent.putExtra(QuranDownloadService.EXTRA_OUTPUT_FILE_NAME, filename); - startService(intent); - } - - private void removeItem(final TranslationRowData translationRowData) { - if (adapter == null) { - return; - } - - final TranslationItem selectedItem = - (TranslationItem) translationRowData; - String msg = String.format(getString(R.string.remove_dlg_msg), selectedItem.name()); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.remove_dlg_title) - .setMessage(msg) - .setPositiveButton(com.quran.mobile.common.ui.core.R.string.remove_button, - (dialog, id) -> { - if (removeTranslation(selectedItem.getTranslation().getFileName())) { - TranslationItem updatedItem = selectedItem.withTranslationRemoved(); - updateTranslationItem(updatedItem); - - // remove from active translations - QuranSettings settings = QuranSettings.getInstance(this); - Set activeTranslations = settings.getActiveTranslations(); - activeTranslations.remove(selectedItem.getTranslation().getFileName()); - settings.setActiveTranslations(activeTranslations); - generateListItems(); - } - }) - .setNegativeButton(com.quran.mobile.common.ui.core.R.string.cancel, - (dialog, i) -> dialog.dismiss()); - 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) { - final TranslationItem targetItem = (TranslationItem) targetRow; - final int targetTranslationId = targetItem.getTranslation().getId(); - - int targetIndex = -1; - for (int i = 0; i < currentSortedDownloads.size(); i++) { - if (currentSortedDownloads.get(i).getTranslation().getId() == targetTranslationId) { - targetIndex = i; - break; - } - } - - if (targetIndex >= 0) { - currentSortedDownloads.remove(targetIndex); - final TranslationItem updatedItem = - targetItem.withDisplayOrder(targetItem.getDisplayOrder() + 1); - if (targetIndex + 1 < currentSortedDownloads.size()) { - currentSortedDownloads.add(targetIndex + 1, updatedItem); - } else { - currentSortedDownloads.add(updatedItem); - } - updateDownloadedItems(); - } - } - - private void rankUpItem(TranslationRowData targetRow) { - final TranslationItem targetItem = (TranslationItem) targetRow; - final int targetTranslationId = targetItem.getTranslation().getId(); - - int targetIndex = -1; - for (int i = 0; i < currentSortedDownloads.size(); i++) { - if (currentSortedDownloads.get(i).getTranslation().getId() == targetTranslationId) { - targetIndex = i; - break; - } - } - - if (targetIndex >= 0) { - currentSortedDownloads.remove(targetIndex); - final TranslationItem updatedItem = - targetItem.withDisplayOrder(targetItem.getDisplayOrder() - 1); - currentSortedDownloads.add(Math.max(targetIndex - 1, 0), updatedItem); - updateDownloadedItems(); - } - } - - private void updateTranslationOrdersIfNecessary() { - if (!originalSortedDownloads.equals(currentSortedDownloads)) { - final List normalizedSortOrders = new ArrayList<>(); - for (int i = 0; i < currentSortedDownloads.size(); i++) { - normalizedSortOrders.add(currentSortedDownloads.get(i).withDisplayOrder(i + 1)); - } - originalSortedDownloads.clear(); - originalSortedDownloads.addAll(normalizedSortOrders); - currentSortedDownloads.clear(); - currentSortedDownloads.addAll(normalizedSortOrders); - presenter.updateItemOrdering(normalizedSortOrders); - } - } - - private boolean removeTranslation(String fileName) { - String path = quranFileUtils.getQuranDatabaseDirectory(TranslationManagerActivity.this); - if (path != null) { - path += File.separator + fileName; - File f = new File(path); - return f.delete(); - } - 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; - } - - static 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) { - final int itemId = item.getItemId(); - if (itemId == R.id.dtm_delete) { - if (downloadedItemActionListener != null) { - downloadedItemActionListener.handleDeleteItemAction(); - } - endAction(); - } else if (itemId == R.id.dtm_move_up) { - if (downloadedItemActionListener != null) { - downloadedItemActionListener.handleRankUpItemAction(); - } - } else if (itemId == R.id.dtm_move_down) { - if (downloadedItemActionListener != null) { - downloadedItemActionListener.handleRankDownItemAction(); - } - } - return false; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - if (mode == actionMode) { - selectionListener.clearSelection(); - actionMode = null; - updateTranslationOrdersIfNecessary(); - } - } - - private void endAction() { - if (actionMode != null) { - selectionListener.clearSelection(); - actionMode.finish(); - } - } - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt new file mode 100644 index 0000000000..943029cdb0 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt @@ -0,0 +1,521 @@ +package com.quran.labs.androidquran.ui + +import android.content.DialogInterface +import android.content.IntentFilter +import android.os.Bundle +import android.util.SparseIntArray +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.ActionMode +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.snackbar.Snackbar +import com.quran.labs.androidquran.QuranApplication +import com.quran.labs.androidquran.R +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.Companion.clearDatabaseHandlerIfExists +import com.quran.labs.androidquran.presenter.translation.TranslationManagerPresenter +import com.quran.labs.androidquran.service.QuranDownloadService +import com.quran.labs.androidquran.service.util.DefaultDownloadReceiver +import com.quran.labs.androidquran.service.util.DefaultDownloadReceiver.SimpleDownloadListener +import com.quran.labs.androidquran.service.util.QuranDownloadNotifier +import com.quran.labs.androidquran.service.util.ServiceIntentHelper.getDownloadIntent +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 io.reactivex.rxjava3.disposables.Disposable +import timber.log.Timber +import java.io.File +import java.util.Collections +import javax.inject.Inject +import kotlin.math.max + +class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, + DownloadedMenuActionListener { + private var allItems: MutableList = mutableListOf() + private var currentSortedDownloads: MutableList = mutableListOf() + private var originalSortedDownloads: MutableList = mutableListOf() + private var translationPositions: SparseIntArray? = null + private var downloadingItem: TranslationItem? = null + private var databaseDirectory: String? = null + private var quranSettings: QuranSettings? = null + private var downloadReceiver: DefaultDownloadReceiver? = null + private var actionMode: ActionMode? = null + private var downloadedItemActionListener: DownloadedItemActionListener? = null + + @Inject + lateinit var presenter: TranslationManagerPresenter + + @Inject + lateinit var quranFileUtils: QuranFileUtils + + private lateinit var adapter: TranslationsAdapter + private lateinit var selectionListener: TranslationSelectionListener + private lateinit var onClickDownloadDisposable: Disposable + private lateinit var onClickRemoveDisposable: Disposable + private lateinit var onClickRankUpDisposable: Disposable + private lateinit var onClickRankDownDisposable: Disposable + + private lateinit var translationSwipeRefresh: SwipeRefreshLayout + private lateinit var translationRecycler: RecyclerView + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (application as QuranApplication).applicationComponent.inject(this) + setContentView(R.layout.translation_manager) + translationSwipeRefresh = findViewById(R.id.translation_swipe_refresh) + translationRecycler = findViewById(R.id.translation_recycler) + val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this) + translationRecycler.setLayoutManager(layoutManager) + adapter = TranslationsAdapter(this) + translationRecycler.setAdapter(adapter) + selectionListener = TranslationSelectionListener(adapter) + databaseDirectory = quranFileUtils.getQuranDatabaseDirectory(this) + val actionBar = supportActionBar + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true) + actionBar.setTitle(R.string.prefs_translations) + } + quranSettings = QuranSettings.getInstance(this) + onClickDownloadDisposable = adapter.getOnClickDownloadSubject() + .subscribe { translationRowData: TranslationRowData -> downloadItem(translationRowData) } + onClickRemoveDisposable = adapter.getOnClickRemoveSubject() + .subscribe { translationRowData: TranslationRowData -> removeItem(translationRowData) } + onClickRankUpDisposable = adapter.getOnClickRankUpSubject() + .subscribe { targetRow: TranslationRowData -> rankUpItem(targetRow) } + onClickRankDownDisposable = adapter.getOnClickRankDownSubject() + .subscribe { targetRow: TranslationRowData -> rankDownItem(targetRow) } + translationSwipeRefresh.setOnRefreshListener { onRefresh() } + presenter.bind(this) + translationSwipeRefresh.isRefreshing = true + presenter.getTranslationsList(false) + } + + public override fun onStop() { + val receiver = downloadReceiver + if (receiver != null) { + receiver.setListener(null) + LocalBroadcastManager.getInstance(this) + .unregisterReceiver(receiver) + downloadReceiver = null + } + super.onStop() + } + + override fun onDestroy() { + presenter.unbind(this) + onClickDownloadDisposable.dispose() + onClickRemoveDisposable.dispose() + onClickRankUpDisposable.dispose() + onClickRankDownDisposable.dispose() + super.onDestroy() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == android.R.id.home) { + finish() + true + } else { + super.onOptionsItemSelected(item) + } + } + + override fun handleDownloadSuccess() { + val downloadingItem = downloadingItem + if (downloadingItem != null) { + if (downloadingItem.exists()) { + try { + val f = File( + databaseDirectory, + downloadingItem.translation.fileName + UPGRADING_EXTENSION + ) + if (f.exists()) { + f.delete() + } + } catch (e: Exception) { + Timber.d(e, "error removing old database file") + } + } + + // TODO: we can avoid the cost of sorting once we can listen to db updates + // in which case we'd set the local version as -1 so it gets properly assigned after. + val sortedItems = sortedDownloadedItems() + val lastDisplayOrder = + if (sortedItems.isEmpty()) 0 else sortedItems[sortedItems.size - 1].displayOrder + val (_, _, currentVersion) = downloadingItem.translation + updateTranslationItem( + downloadingItem.withLocalVersionAndDisplayOrder( + currentVersion, lastDisplayOrder + 1 + ) + ) + + // update active translations and add this item to it + val settings = QuranSettings.getInstance(this) + val activeTranslations = settings.activeTranslations + activeTranslations.add(downloadingItem.translation.fileName) + settings.activeTranslations = activeTranslations + } + this.downloadingItem = null + generateListItems() + } + + override fun handleDownloadFailure(errId: Int) { + val downloadingItem = downloadingItem + if (downloadingItem != null && downloadingItem.exists()) { + try { + val f = File( + databaseDirectory, + downloadingItem.translation.fileName + UPGRADING_EXTENSION + ) + val destFile = File(databaseDirectory, downloadingItem.translation.fileName) + if (f.exists() && !destFile.exists()) { + f.renameTo(destFile) + } else { + f.delete() + } + } catch (e: Exception) { + Timber.d(e, "error restoring translation after failed download") + } + } + this.downloadingItem = null + } + + private fun onRefresh() { + presenter.getTranslationsList(true) + } + + private fun updateTranslationItem(updated: TranslationItem) { + val id = updated.translation.id + val allItemsIndex = translationPositions!![id] + if (allItems.size > allItemsIndex) { + allItems.removeAt(allItemsIndex) + allItems.add(allItemsIndex, updated) + } + presenter.updateItem(updated) + } + + private fun updateDownloadedItems() { + val translations = adapter.getTranslations().toMutableList() + val downloadedItemCount = currentSortedDownloads.size + if (downloadedItemCount + 1 <= translations.size) { + for (i in 0 until downloadedItemCount) { + translations.removeAt(1) + } + translations.addAll(1, currentSortedDownloads) + adapter.setTranslations(translations) + adapter.notifyDataSetChanged() + } + } + + fun onErrorDownloadTranslations() { + translationSwipeRefresh.isRefreshing = false + Snackbar + .make( + translationRecycler, + R.string.error_getting_translation_list, + Snackbar.LENGTH_SHORT + ) + .show() + } + + fun onTranslationsUpdated(items: List) { + translationSwipeRefresh.isRefreshing = false + val itemsSparseArray = SparseIntArray(items.size) + var i = 0 + val itemsSize = items.size + while (i < itemsSize) { + val (translation) = items[i] + itemsSparseArray.put(translation.id, i) + i++ + } + allItems = items.toMutableList() + translationPositions = itemsSparseArray + generateListItems() + } + + private fun generateListItems() { + val downloaded: MutableList = ArrayList() + val notDownloaded: MutableList = ArrayList() + var i = 0 + val allItemsSize = allItems.size + while (i < allItemsSize) { + val item = allItems[i] + if (item.exists()) { + downloaded.add(item) + } else { + notDownloaded.add(item) + } + i++ + } + val result: MutableList = ArrayList() + if (downloaded.size > 0) { + val hdr = TranslationHeader(getString(R.string.downloaded_translations)) + result.add(hdr) + + // sort by display order + Collections.sort(downloaded, TranslationItemDisplaySort()) + var needsUpgrade = false + for (item in downloaded) { + result.add(item) + needsUpgrade = needsUpgrade || item.needsUpgrade() + } + if (!needsUpgrade) { + quranSettings!!.setHaveUpdatedTranslations(false) + } + } + originalSortedDownloads = ArrayList(downloaded) + currentSortedDownloads = ArrayList(downloaded) + result.add(TranslationHeader(getString(R.string.available_translations))) + result.addAll(notDownloaded) + adapter.setTranslations(result) + adapter.notifyDataSetChanged() + } + + private fun downloadItem(translationRowData: TranslationRowData) { + val selectedItem = translationRowData as TranslationItem + if (selectedItem.exists() && !selectedItem.needsUpgrade()) { + return + } + downloadingItem = selectedItem + val (_, _, _, _, _, fileName, url) = selectedItem.translation + clearDatabaseHandlerIfExists(fileName) + if (downloadReceiver == null) { + val downloadReceiver = DefaultDownloadReceiver( + this, + QuranDownloadService.DOWNLOAD_TYPE_TRANSLATION + ) + LocalBroadcastManager.getInstance(this).registerReceiver( + downloadReceiver, IntentFilter( + QuranDownloadNotifier.ProgressIntent.INTENT_NAME + ) + ) + this.downloadReceiver = downloadReceiver + } + downloadReceiver!!.setListener(this) + + // actually start the download + val destination = databaseDirectory + Timber.d("downloading %s to %s", url, destination) + if (selectedItem.exists()) { + try { + val f = File(destination, fileName) + if (f.exists()) { + val newPath = File( + destination, + fileName + UPGRADING_EXTENSION + ) + if (newPath.exists()) { + newPath.delete() + } + f.renameTo(newPath) + } + } catch (e: Exception) { + Timber.d(e, "error backing database file up") + } + } + + // start the download + val notificationTitle = selectedItem.name() + val intent = getDownloadIntent( + this, url, + destination, notificationTitle, TRANSLATION_DOWNLOAD_KEY, + QuranDownloadService.DOWNLOAD_TYPE_TRANSLATION + ) + var filename = selectedItem.translation.fileName + if (url.endsWith("zip")) { + filename += ".zip" + } + intent.putExtra(QuranDownloadService.EXTRA_OUTPUT_FILE_NAME, filename) + startService(intent) + } + + private fun removeItem(translationRowData: TranslationRowData) { + val selectedItem = translationRowData as TranslationItem + val msg = String.format(getString(R.string.remove_dlg_msg), selectedItem.name()) + val builder = AlertDialog.Builder(this) + builder.setTitle(R.string.remove_dlg_title) + .setMessage(msg) + .setPositiveButton( + com.quran.mobile.common.ui.core.R.string.remove_button + ) { dialog: DialogInterface?, id: Int -> + if (removeTranslation(selectedItem.translation.fileName)) { + val updatedItem = selectedItem.withTranslationRemoved() + updateTranslationItem(updatedItem) + + // remove from active translations + val settings = QuranSettings.getInstance(this) + val activeTranslations = settings.activeTranslations + activeTranslations.remove(selectedItem.translation.fileName) + settings.activeTranslations = activeTranslations + generateListItems() + } + } + .setNegativeButton( + com.quran.mobile.common.ui.core.R.string.cancel + ) { dialog: DialogInterface, i: Int -> dialog.dismiss() } + builder.show() + } + + private fun sortedDownloadedItems(): List { + val result = ArrayList() + for (item in allItems) { + if (item.exists()) result.add(item) + } + Collections.sort(result, TranslationItemDisplaySort()) + return result + } + + private fun rankDownItem(targetRow: TranslationRowData) { + val targetItem = targetRow as TranslationItem + val targetTranslationId = targetItem.translation.id + var targetIndex = -1 + for (i in currentSortedDownloads.indices) { + if (currentSortedDownloads[i].translation.id == targetTranslationId) { + targetIndex = i + break + } + } + if (targetIndex >= 0) { + currentSortedDownloads.removeAt(targetIndex) + val updatedItem = targetItem.withDisplayOrder(targetItem.displayOrder + 1) + if (targetIndex + 1 < currentSortedDownloads.size) { + currentSortedDownloads.add(targetIndex + 1, updatedItem) + } else { + currentSortedDownloads.add(updatedItem) + } + updateDownloadedItems() + } + } + + private fun rankUpItem(targetRow: TranslationRowData) { + val targetItem = targetRow as TranslationItem + val targetTranslationId = targetItem.translation.id + var targetIndex = -1 + for (i in currentSortedDownloads.indices) { + if (currentSortedDownloads[i].translation.id == targetTranslationId) { + targetIndex = i + break + } + } + if (targetIndex >= 0) { + currentSortedDownloads.removeAt(targetIndex) + val updatedItem = targetItem.withDisplayOrder(targetItem.displayOrder - 1) + currentSortedDownloads.add(max(targetIndex - 1, 0), updatedItem) + updateDownloadedItems() + } + } + + private fun updateTranslationOrdersIfNecessary() { + if (originalSortedDownloads != currentSortedDownloads) { + val normalizedSortOrders: MutableList = ArrayList() + for (i in currentSortedDownloads!!.indices) { + normalizedSortOrders.add(currentSortedDownloads!![i].withDisplayOrder(i + 1)) + } + originalSortedDownloads.clear() + originalSortedDownloads.addAll(normalizedSortOrders) + currentSortedDownloads.clear() + currentSortedDownloads.addAll(normalizedSortOrders) + presenter.updateItemOrdering(normalizedSortOrders) + } + } + + private fun removeTranslation(fileName: String): Boolean { + var path = quranFileUtils.getQuranDatabaseDirectory(this@TranslationManagerActivity) + if (path != null) { + path += File.separator + fileName + val f = File(path) + return f.delete() + } + return false + } + + override fun startMenuAction( + item: TranslationItem, + downloadedItemActionListener: DownloadedItemActionListener? + ) { + this.downloadedItemActionListener = downloadedItemActionListener + if (actionMode != null) { + actionMode!!.finish() + selectionListener.clearSelection() + } else { + selectionListener.handleSelection(item) + actionMode = startSupportActionMode(ModeCallback()) + } + } + + override fun finishMenuAction() { + actionMode?.finish() + selectionListener.clearSelection() + downloadedItemActionListener = null + } + + internal class TranslationSelectionListener(private val adapter: TranslationsAdapter) { + fun handleSelection(item: TranslationItem?) { + adapter.setSelectedItem(item) + } + + fun clearSelection() { + adapter.setSelectedItem(null) + } + } + + private inner class ModeCallback : ActionMode.Callback { + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + val inflater = menuInflater + inflater.inflate(R.menu.downloaded_translation_menu, menu) + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + val itemId = item.itemId + if (itemId == R.id.dtm_delete) { + if (downloadedItemActionListener != null) { + downloadedItemActionListener!!.handleDeleteItemAction() + } + endAction() + } else if (itemId == R.id.dtm_move_up) { + if (downloadedItemActionListener != null) { + downloadedItemActionListener!!.handleRankUpItemAction() + } + } else if (itemId == R.id.dtm_move_down) { + if (downloadedItemActionListener != null) { + downloadedItemActionListener!!.handleRankDownItemAction() + } + } + return false + } + + override fun onDestroyActionMode(mode: ActionMode) { + if (mode === actionMode) { + selectionListener.clearSelection() + actionMode = null + updateTranslationOrdersIfNecessary() + } + } + + private fun endAction() { + if (actionMode != null) { + selectionListener.clearSelection() + actionMode!!.finish() + } + } + } + + companion object { + const val TRANSLATION_DOWNLOAD_KEY = "TRANSLATION_DOWNLOAD_KEY" + private const val UPGRADING_EXTENSION = ".old" + } +} From 9d3e1aa125180174ffef573ca8926dad6ecf5ad7 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 14:55:59 +0400 Subject: [PATCH 195/258] More clean up of TranslationManagerActivity --- .../ui/TranslationManagerActivity.kt | 91 ++++++++----------- 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt index 943029cdb0..b001747b66 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt @@ -41,13 +41,12 @@ import kotlin.math.max class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, DownloadedMenuActionListener { - private var allItems: MutableList = mutableListOf() - private var currentSortedDownloads: MutableList = mutableListOf() - private var originalSortedDownloads: MutableList = mutableListOf() - private var translationPositions: SparseIntArray? = null + private var allItems: List = emptyList() + private var currentSortedDownloads: List = emptyList() + private var originalSortedDownloads: List = emptyList() + private var translationPositions: SparseIntArray = SparseIntArray() private var downloadingItem: TranslationItem? = null private var databaseDirectory: String? = null - private var quranSettings: QuranSettings? = null private var downloadReceiver: DefaultDownloadReceiver? = null private var actionMode: ActionMode? = null private var downloadedItemActionListener: DownloadedItemActionListener? = null @@ -58,6 +57,9 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, @Inject lateinit var quranFileUtils: QuranFileUtils + @Inject + lateinit var quranSettings: QuranSettings + private lateinit var adapter: TranslationsAdapter private lateinit var selectionListener: TranslationSelectionListener private lateinit var onClickDownloadDisposable: Disposable @@ -85,7 +87,6 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setTitle(R.string.prefs_translations) } - quranSettings = QuranSettings.getInstance(this) onClickDownloadDisposable = adapter.getOnClickDownloadSubject() .subscribe { translationRowData: TranslationRowData -> downloadItem(translationRowData) } onClickRemoveDisposable = adapter.getOnClickRemoveSubject() @@ -195,10 +196,12 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, private fun updateTranslationItem(updated: TranslationItem) { val id = updated.translation.id - val allItemsIndex = translationPositions!![id] + val allItemsIndex = translationPositions[id] if (allItems.size > allItemsIndex) { - allItems.removeAt(allItemsIndex) - allItems.add(allItemsIndex, updated) + allItems = allItems.toMutableList().apply { + removeAt(allItemsIndex) + add(allItemsIndex, updated) + } } presenter.updateItem(updated) } @@ -237,27 +240,16 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, itemsSparseArray.put(translation.id, i) i++ } - allItems = items.toMutableList() + allItems = items translationPositions = itemsSparseArray generateListItems() } private fun generateListItems() { - val downloaded: MutableList = ArrayList() - val notDownloaded: MutableList = ArrayList() - var i = 0 - val allItemsSize = allItems.size - while (i < allItemsSize) { - val item = allItems[i] - if (item.exists()) { - downloaded.add(item) - } else { - notDownloaded.add(item) - } - i++ - } + val (downloaded, notDownloaded) = allItems.partition { it.exists() } + val result: MutableList = ArrayList() - if (downloaded.size > 0) { + if (downloaded.isNotEmpty()) { val hdr = TranslationHeader(getString(R.string.downloaded_translations)) result.add(hdr) @@ -269,7 +261,7 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, needsUpgrade = needsUpgrade || item.needsUpgrade() } if (!needsUpgrade) { - quranSettings!!.setHaveUpdatedTranslations(false) + quranSettings.setHaveUpdatedTranslations(false) } } originalSortedDownloads = ArrayList(downloaded) @@ -346,7 +338,7 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, .setMessage(msg) .setPositiveButton( com.quran.mobile.common.ui.core.R.string.remove_button - ) { dialog: DialogInterface?, id: Int -> + ) { _: DialogInterface?, _: Int -> if (removeTranslation(selectedItem.translation.fileName)) { val updatedItem = selectedItem.withTranslationRemoved() updateTranslationItem(updatedItem) @@ -377,21 +369,17 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, private fun rankDownItem(targetRow: TranslationRowData) { val targetItem = targetRow as TranslationItem val targetTranslationId = targetItem.translation.id - var targetIndex = -1 - for (i in currentSortedDownloads.indices) { - if (currentSortedDownloads[i].translation.id == targetTranslationId) { - targetIndex = i - break - } - } + val targetIndex = currentSortedDownloads.indexOfFirst { it.translation.id == targetTranslationId } if (targetIndex >= 0) { - currentSortedDownloads.removeAt(targetIndex) + val sortedDownloads = currentSortedDownloads.toMutableList() + sortedDownloads.removeAt(targetIndex) val updatedItem = targetItem.withDisplayOrder(targetItem.displayOrder + 1) - if (targetIndex + 1 < currentSortedDownloads.size) { - currentSortedDownloads.add(targetIndex + 1, updatedItem) + if (targetIndex + 1 < sortedDownloads.size) { + sortedDownloads.add(targetIndex + 1, updatedItem) } else { - currentSortedDownloads.add(updatedItem) + sortedDownloads.add(updatedItem) } + currentSortedDownloads = sortedDownloads updateDownloadedItems() } } @@ -399,31 +387,26 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, private fun rankUpItem(targetRow: TranslationRowData) { val targetItem = targetRow as TranslationItem val targetTranslationId = targetItem.translation.id - var targetIndex = -1 - for (i in currentSortedDownloads.indices) { - if (currentSortedDownloads[i].translation.id == targetTranslationId) { - targetIndex = i - break - } - } + val targetIndex = currentSortedDownloads.indexOfFirst { it.translation.id == targetTranslationId } if (targetIndex >= 0) { - currentSortedDownloads.removeAt(targetIndex) + val sortedDownloads = currentSortedDownloads.toMutableList() + sortedDownloads.removeAt(targetIndex) val updatedItem = targetItem.withDisplayOrder(targetItem.displayOrder - 1) - currentSortedDownloads.add(max(targetIndex - 1, 0), updatedItem) + sortedDownloads.add(max(targetIndex - 1, 0), updatedItem) + currentSortedDownloads = sortedDownloads updateDownloadedItems() } } private fun updateTranslationOrdersIfNecessary() { if (originalSortedDownloads != currentSortedDownloads) { - val normalizedSortOrders: MutableList = ArrayList() - for (i in currentSortedDownloads!!.indices) { - normalizedSortOrders.add(currentSortedDownloads!![i].withDisplayOrder(i + 1)) - } - originalSortedDownloads.clear() - originalSortedDownloads.addAll(normalizedSortOrders) - currentSortedDownloads.clear() - currentSortedDownloads.addAll(normalizedSortOrders) + val normalizedSortOrders: List = + currentSortedDownloads.mapIndexed { index, item -> + item.withDisplayOrder(index + 1) + } + + originalSortedDownloads = normalizedSortOrders + currentSortedDownloads = normalizedSortOrders presenter.updateItemOrdering(normalizedSortOrders) } } From 20eb3f2865d384412d2834c135cced7fcdd1db06 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 16:53:54 +0400 Subject: [PATCH 196/258] Clean up TranslationManager activity and presenter Use coroutines instead of RxJava and remove some of the legacy methods. --- app/build.gradle | 2 + .../translation/TranslationItemDisplaySort.kt | 7 - .../database/TranslationsDBAdapter.kt | 70 +++--- .../TranslationManagerPresenter.kt | 226 +++++++----------- .../labs/androidquran/ui/QuranActivity.kt | 12 +- .../ui/TranslationManagerActivity.kt | 100 ++++---- .../TranslationManagerPresenterTest.kt | 114 +++++---- gradle/libs.versions.toml | 4 +- 8 files changed, 255 insertions(+), 280 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItemDisplaySort.kt diff --git a/app/build.gradle b/app/build.gradle index 4cd569585d..5a6abe8146 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -176,6 +176,8 @@ dependencies { testImplementation libs.robolectric testImplementation libs.espresso.core testImplementation libs.espresso.intents + testImplementation libs.turbine + testImplementation libs.kotlinx.coroutines.test errorprone libs.errorprone.core 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 deleted file mode 100644 index ec390d7f70..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/dao/translation/TranslationItemDisplaySort.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.quran.labs.androidquran.dao.translation - -class TranslationItemDisplaySort : Comparator { - override fun compare(first: TranslationItem, second: TranslationItem): Int { - return first.displayOrder.compareTo(second.displayOrder) - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt index 7217e1b458..342313ea0a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt @@ -8,11 +8,13 @@ import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.mobile.di.qualifier.ApplicationContext import com.quran.mobile.translation.data.TranslationsDataSource import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -36,57 +38,49 @@ class TranslationsDBAdapter @Inject constructor( return runBlocking { getTranslations().first() } } - @WorkerThread - fun getTranslationsHash(): SparseArray { - val result = SparseArray() - for (item in legacyGetTranslations()) { - result.put(item.id.toInt(), item) + suspend fun translationsHash(): SparseArray { + return withContext(Dispatchers.IO) { + val result = SparseArray() + val translations = getTranslations().first() + for (item in translations) { + result.put(item.id.toInt(), item) + } + result } - return result } suspend fun deleteTranslationByFileName(filename: String) { dataSource.removeTranslation(filename) } - @WorkerThread - fun legacyDeleteTranslationByFileName(filename: String) { - runBlocking { - deleteTranslationByFileName(filename) - } - } - - @WorkerThread - fun legacyWriteTranslationUpdates(updates: List): Boolean { - return runBlocking { writeTranslationUpdates(updates) } - } - suspend fun writeTranslationUpdates(updates: List): Boolean { - val (available, unavailable) = updates.partition { it.exists() } + return withContext(Dispatchers.IO) { + val (available, unavailable) = updates.partition { it.exists() } - val needNextOrder = available.any { it.displayOrder == -1 } - val nextOrder = if (needNextOrder) { - dataSource.maximumDisplayOrder().toInt() + 1 - } else { - (available.maxOfOrNull { it.displayOrder } ?: 0) + 1 - } + val needNextOrder = available.any { it.displayOrder == -1 } + val nextOrder = if (needNextOrder) { + dataSource.maximumDisplayOrder().toInt() + 1 + } else { + (available.maxOfOrNull { it.displayOrder } ?: 0) + 1 + } - val items = if (needNextOrder) { - var nextOrderNumber = nextOrder - available.map { item -> - if (item.displayOrder == -1) { - item.copy(displayOrder = nextOrderNumber++) - } else { - item + val items = if (needNextOrder) { + var nextOrderNumber = nextOrder + available.map { item -> + if (item.displayOrder == -1) { + item.copy(displayOrder = nextOrderNumber++) + } else { + item + } } + } else { + available } - } else { - available - } - dataSource.updateTranslations(items.map { it.asLocalTranslation() }) - dataSource.removeTranslationsById(unavailable.map { it.translation.id.toLong() }) + dataSource.updateTranslations(items.map { it.asLocalTranslation() }) + dataSource.removeTranslationsById(unavailable.map { it.translation.id.toLong() }) - return true + true + } } } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt index 9da7506fec..3c8a696f0f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt @@ -9,18 +9,22 @@ import com.quran.labs.androidquran.dao.translation.TranslationList import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.database.DatabaseHandler.Companion.getDatabaseHandler 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.quran.mobile.di.qualifier.ApplicationContext import com.squareup.moshi.Moshi -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.ObservableSource -import io.reactivex.rxjava3.functions.Supplier -import io.reactivex.rxjava3.observers.DisposableObserver -import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import okio.buffer @@ -28,146 +32,112 @@ import okio.sink import okio.source import timber.log.Timber import java.io.File -import java.io.IOException -import java.util.concurrent.Callable import javax.inject.Inject import javax.inject.Singleton @Singleton open class TranslationManagerPresenter @Inject internal constructor( - @param:ApplicationContext private val appContext: Context, + @ApplicationContext private val appContext: Context, private val okHttpClient: OkHttpClient, private val quranSettings: QuranSettings, private val translationsDBAdapter: TranslationsDBAdapter, private val quranFileUtils: QuranFileUtils -) : Presenter { +) { @VisibleForTesting var host: String = Constants.HOST - private var currentActivity: TranslationManagerActivity? = null + private val scope = MainScope() + + private val moshiTranslationListAdapter by lazy { + val moshi = Moshi.Builder().build() + moshi.adapter(TranslationList::class.java) + } fun checkForUpdates() { - getTranslationsList(true) + Timber.d("checking whether we should update translations..") + val isCacheStale = System.currentTimeMillis() - + quranSettings.lastUpdatedTranslationDate > Constants.MIN_TRANSLATION_REFRESH_TIME + if (isCacheStale) { + Timber.d("updating translations list...") + getTranslations(true) + .catch { Timber.e(it) } + .launchIn(scope) + } } - fun getTranslationsList(forceDownload: Boolean) { + fun getTranslations(forceDownload: Boolean): Flow> { val isCacheStale = System.currentTimeMillis() - quranSettings.lastUpdatedTranslationDate > Constants.MIN_TRANSLATION_REFRESH_TIME - val source: Observable = Observable.concat( - cachedTranslationListObservable, remoteTranslationListObservable - ) - val observableSource: Observable = if (forceDownload) { - // we only force if we pulled to refresh or are refreshing in the background, - // implying that we have data on the screen already (or don't need data in the - // background case), so just get remote data. - remoteTranslationListObservable - } else if (isCacheStale) { - source + val flow = if (forceDownload) { + remoteTranslationList() } else { - source.take(1) - } - observableSource - .filter { translationList: TranslationList -> translationList.translations.isNotEmpty() } - .map { translationList: TranslationList -> - mergeWithServerTranslations(translationList.translations) + val flow = merge(cachedTranslationList(), remoteTranslationList()) + if (isCacheStale) { + flow + } else { + flow.take(1) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : DisposableObserver>() { - override fun onNext(translationItems: List) { - currentActivity?.onTranslationsUpdated(translationItems) - - // used for marking upgrades, irrespective of whether or not there is a bound activity - var updatedTranslations = false - for (item in translationItems) { - if (item.needsUpgrade()) { - updatedTranslations = true - break - } - } - quranSettings.setHaveUpdatedTranslations(updatedTranslations) - } - - override fun onError(e: Throwable) { - if (e !is IOException) { - Timber.e(e, "error updating translations list") - } - currentActivity?.onErrorDownloadTranslations() - } + } - override fun onComplete() {} - }) + return flow + .map { mergeWithServerTranslations(it.translations) } + .onEach { translations -> + val updatedTranslations = translations.any { it.needsUpgrade() } + quranSettings.setHaveUpdatedTranslations(updatedTranslations) + } } - fun updateItem(item: TranslationItem) { - Observable.fromCallable { - + suspend fun updateItem(item: TranslationItem) { + withContext(Dispatchers.IO) { // for upgrades, remove the old file to stop the tafseer from showing up // twice. this happens because old and new tafaseer (ex ibn kathir) have // different ids when they target different schema versions, and so the // old file needs to be removed from the database explicitly val (_, minimumVersion, _, _, _, fileName) = item.translation if (minimumVersion >= 5) { - translationsDBAdapter.legacyDeleteTranslationByFileName(fileName) + translationsDBAdapter.deleteTranslationByFileName(fileName) } - translationsDBAdapter.legacyWriteTranslationUpdates(listOf(item)) - }.subscribeOn(Schedulers.io()) - .subscribe() + translationsDBAdapter.writeTranslationUpdates(listOf(item)) + } } - fun updateItemOrdering(items: List) { - Observable.fromCallable { translationsDBAdapter.legacyWriteTranslationUpdates(items) } - .subscribeOn(Schedulers.io()) - .subscribe() + suspend fun updateItemOrdering(items: List) { + withContext(Dispatchers.IO) { + translationsDBAdapter.writeTranslationUpdates(items) + } } - val cachedTranslationListObservable: Observable - get() = Observable.defer(Supplier> { + internal fun cachedTranslationList(): Flow { + return flow { try { val cachedFile = cachedFile if (cachedFile.exists()) { - val moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter( - TranslationList::class.java - ) - val list = jsonAdapter.fromJson(cachedFile.source().buffer()) - if (list != null) { - return@Supplier Observable.just(list) + val list = moshiTranslationListAdapter.fromJson(cachedFile.source().buffer()) + if (list != null && list.translations.isNotEmpty()) { + emit(list) } } } catch (e: Exception) { Timber.e(e) } - Observable.empty() - }) - val remoteTranslationListObservable: Observable - get() { - val url = host + WEB_SERVICE_ENDPOINT - return downloadTranslationList(url) - .onErrorResumeWith(downloadTranslationList(url)) - .doOnNext { translationList: TranslationList -> - translationList.translations - if (translationList.translations.isNotEmpty()) { - writeTranslationList(translationList) - } - } } + .flowOn(Dispatchers.IO) + } - private fun downloadTranslationList(url: String): Observable { - return Observable.fromCallable(Callable { + internal fun remoteTranslationList(): Flow { + return flow { + val url = host + WEB_SERVICE_ENDPOINT val request: Request = Request.Builder() .url(url) .build() val response = okHttpClient.newCall(request).execute() - val moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter( - TranslationList::class.java - ) val responseBody = response.body - val result = jsonAdapter.fromJson(responseBody!!.source()) + val result = moshiTranslationListAdapter.fromJson(responseBody!!.source()) responseBody.close() - result - }) + if (result != null && result.translations.isNotEmpty()) { + emit(result) + } + }.flowOn(Dispatchers.IO) } open fun writeTranslationList(list: TranslationList) { @@ -179,12 +149,8 @@ open class TranslationManagerPresenter @Inject internal constructor( if (cacheFile.exists()) { cacheFile.delete() } - val moshi = Moshi.Builder().build() - val jsonAdapter = moshi.adapter( - TranslationList::class.java - ) val sink = cacheFile.sink().buffer() - jsonAdapter.toJson(sink, list) + moshiTranslationListAdapter.toJson(sink, list) sink.close() quranSettings.lastUpdatedTranslationDate = System.currentTimeMillis() } @@ -194,35 +160,32 @@ open class TranslationManagerPresenter @Inject internal constructor( } } - private val cachedFile: File + internal open val cachedFile: File get() { val dir = quranFileUtils.getQuranDatabaseDirectory(appContext) return File(dir + File.separator + CACHED_RESPONSE_FILE_NAME) } - private fun mergeWithServerTranslations(serverTranslations: List): List { - val results: MutableList = ArrayList(serverTranslations.size) - val localTranslations = translationsDBAdapter.getTranslationsHash() + internal open suspend fun mergeWithServerTranslations(serverTranslations: List): List { + val localTranslations = translationsDBAdapter.translationsHash() val databaseDir = quranFileUtils.getQuranDatabaseDirectory(appContext) val updates: MutableList = ArrayList() - var i = 0 - val count = serverTranslations.size - while (i < count) { - val translation = serverTranslations[i] + + val results = serverTranslations.mapIndexed { _, translation -> val local = localTranslations[translation.id] val dbFile = File(databaseDir, translation.fileName) - var exists = dbFile.exists() - var item: TranslationItem + val translationExists = dbFile.exists() var override: TranslationItem? = null - if (exists) { + + val item: TranslationItem + if (translationExists) { if (local == null) { // text version, schema version val versions = getVersionFromDatabase(translation.fileName) item = TranslationItem(translation, versions.first) if (versions.second != translation.minimumVersion) { // schema change, write downloaded schema version to the db and return server item - override = - TranslationItem(translation.withSchema(versions.second), versions.first) + override = TranslationItem(translation.withSchema(versions.second), versions.first) } } else { item = TranslationItem(translation, local.version, local.displayOrder) @@ -230,29 +193,32 @@ open class TranslationManagerPresenter @Inject internal constructor( } else { item = TranslationItem(translation) } - if (exists && !item.exists()) { + + val exists = if (translationExists && !item.exists()) { // delete the file, it has been corrupted - if (dbFile.delete()) { - exists = false - } + dbFile.delete() + false + } else { + true } + if (local == null && exists || local != null && !exists) { if (override != null && item.translation.minimumVersion >= 5) { // certain schema changes, especially those going to v5, keep the same filename while // changing the database entry id. this could cause duplicate entries in the database. // work around it by removing the existing entries before doing the updates. - translationsDBAdapter.legacyDeleteTranslationByFileName(override.translation.fileName) + translationsDBAdapter.deleteTranslationByFileName(override.translation.fileName) } updates.add(override ?: item) } else if (local != null && local.languageCode == null) { // older items don't have a language code updates.add(item) } - results.add(item) - i++ + item } - if (!updates.isEmpty()) { - translationsDBAdapter.legacyWriteTranslationUpdates(updates) + + if (updates.isNotEmpty()) { + translationsDBAdapter.writeTranslationUpdates(updates) } return results } @@ -269,16 +235,6 @@ open class TranslationManagerPresenter @Inject internal constructor( return Pair(0, 0) } - override fun bind(what: TranslationManagerActivity) { - currentActivity = what - } - - override fun unbind(what: TranslationManagerActivity) { - if (what === currentActivity) { - currentActivity = null - } - } - companion object { private const val WEB_SERVICE_ENDPOINT = "data/translations.php?v=5" private const val CACHED_RESPONSE_FILE_NAME = "translations.v5.cache" diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt b/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt index 394fd8fa2a..7714c01fbd 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt @@ -47,11 +47,10 @@ import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.QuranUtils import com.quran.labs.androidquran.view.SlidingTabLayout import com.quran.mobile.di.ExtraScreenProvider +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.CompositeDisposable -import timber.log.Timber import java.util.concurrent.TimeUnit.MILLISECONDS import javax.inject.Inject import kotlin.math.abs @@ -290,13 +289,8 @@ class QuranActivity : AppCompatActivity(), private fun updateTranslationsListAsNeeded() { if (!updatedTranslations) { - val time = settings.lastUpdatedTranslationDate - Timber.d("checking whether we should update translations..") - if (System.currentTimeMillis() - time > Constants.TRANSLATION_REFRESH_TIME) { - Timber.d("updating translations list...") - updatedTranslations = true - translationManagerPresenter.checkForUpdates() - } + translationManagerPresenter.checkForUpdates() + updatedTranslations = true } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt index b001747b66..3687eb43f0 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt @@ -18,7 +18,6 @@ import com.quran.labs.androidquran.QuranApplication import com.quran.labs.androidquran.R 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.Companion.clearDatabaseHandlerIfExists import com.quran.labs.androidquran.presenter.translation.TranslationManagerPresenter @@ -33,9 +32,14 @@ import com.quran.labs.androidquran.ui.adapter.TranslationsAdapter import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.labs.androidquran.util.QuranSettings import io.reactivex.rxjava3.disposables.Disposable +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import java.io.File -import java.util.Collections import javax.inject.Inject import kotlin.math.max @@ -70,6 +74,8 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, private lateinit var translationSwipeRefresh: SwipeRefreshLayout private lateinit var translationRecycler: RecyclerView + private val scope = MainScope() + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (application as QuranApplication).applicationComponent.inject(this) @@ -96,9 +102,8 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, onClickRankDownDisposable = adapter.getOnClickRankDownSubject() .subscribe { targetRow: TranslationRowData -> rankDownItem(targetRow) } translationSwipeRefresh.setOnRefreshListener { onRefresh() } - presenter.bind(this) translationSwipeRefresh.isRefreshing = true - presenter.getTranslationsList(false) + refreshTranslations() } public override fun onStop() { @@ -113,7 +118,7 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, } override fun onDestroy() { - presenter.unbind(this) + scope.cancel() onClickDownloadDisposable.dispose() onClickRemoveDisposable.dispose() onClickRankUpDisposable.dispose() @@ -191,7 +196,14 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, } private fun onRefresh() { - presenter.getTranslationsList(true) + refreshTranslations(true) + } + + private fun refreshTranslations(forceDownload: Boolean = false) { + presenter.getTranslations(forceDownload) + .onEach { onTranslationsUpdated(it) } + .catch { onErrorDownloadTranslations() } + .launchIn(scope) } private fun updateTranslationItem(updated: TranslationItem) { @@ -203,7 +215,9 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, add(allItemsIndex, updated) } } - presenter.updateItem(updated) + scope.launch { + presenter.updateItem(updated) + } } private fun updateDownloadedItems() { @@ -219,7 +233,7 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, } } - fun onErrorDownloadTranslations() { + private fun onErrorDownloadTranslations() { translationSwipeRefresh.isRefreshing = false Snackbar .make( @@ -230,7 +244,7 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, .show() } - fun onTranslationsUpdated(items: List) { + private fun onTranslationsUpdated(items: List) { translationSwipeRefresh.isRefreshing = false val itemsSparseArray = SparseIntArray(items.size) var i = 0 @@ -248,27 +262,26 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, private fun generateListItems() { val (downloaded, notDownloaded) = allItems.partition { it.exists() } - val result: MutableList = ArrayList() - if (downloaded.isNotEmpty()) { - val hdr = TranslationHeader(getString(R.string.downloaded_translations)) - result.add(hdr) - - // sort by display order - Collections.sort(downloaded, TranslationItemDisplaySort()) - var needsUpgrade = false - for (item in downloaded) { - result.add(item) - needsUpgrade = needsUpgrade || item.needsUpgrade() - } - if (!needsUpgrade) { - quranSettings.setHaveUpdatedTranslations(false) + // sort by display order + val sortedDownloads = downloaded.sortedBy { it.displayOrder } + + val resultList = buildList { + if (downloaded.isNotEmpty()) { + add(TranslationHeader(getString(R.string.downloaded_translations))) + addAll(sortedDownloads) } + add(TranslationHeader(getString(R.string.available_translations))) + addAll(notDownloaded) + } + + val needsUpgrade = sortedDownloads.any { it.needsUpgrade() } + if (!needsUpgrade) { + quranSettings.setHaveUpdatedTranslations(false) } + originalSortedDownloads = ArrayList(downloaded) currentSortedDownloads = ArrayList(downloaded) - result.add(TranslationHeader(getString(R.string.available_translations))) - result.addAll(notDownloaded) - adapter.setTranslations(result) + adapter.setTranslations(resultList) adapter.notifyDataSetChanged() } @@ -358,12 +371,7 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, } private fun sortedDownloadedItems(): List { - val result = ArrayList() - for (item in allItems) { - if (item.exists()) result.add(item) - } - Collections.sort(result, TranslationItemDisplaySort()) - return result + return allItems.filter { it.exists() }.sortedBy { it.displayOrder } } private fun rankDownItem(targetRow: TranslationRowData) { @@ -407,7 +415,9 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, originalSortedDownloads = normalizedSortOrders currentSortedDownloads = normalizedSortOrders - presenter.updateItemOrdering(normalizedSortOrders) + scope.launch { + presenter.updateItemOrdering(normalizedSortOrders) + } } } @@ -464,19 +474,17 @@ class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { val itemId = item.itemId - if (itemId == R.id.dtm_delete) { - if (downloadedItemActionListener != null) { - downloadedItemActionListener!!.handleDeleteItemAction() - } - endAction() - } else if (itemId == R.id.dtm_move_up) { - if (downloadedItemActionListener != null) { - downloadedItemActionListener!!.handleRankUpItemAction() - } - } else if (itemId == R.id.dtm_move_down) { - if (downloadedItemActionListener != null) { - downloadedItemActionListener!!.handleRankDownItemAction() - } + when (itemId) { + R.id.dtm_delete -> { + downloadedItemActionListener?.handleDeleteItemAction() + endAction() + } + R.id.dtm_move_up -> { + downloadedItemActionListener?.handleRankUpItemAction() + } + R.id.dtm_move_down -> { + downloadedItemActionListener?.handleRankDownItemAction() + } } return false } diff --git a/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt b/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt index d7988f1e16..212ebce3cb 100644 --- a/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt +++ b/app/src/test/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenterTest.kt @@ -1,13 +1,15 @@ package com.quran.labs.androidquran.presenter.translation import android.content.Context +import app.cash.turbine.test import com.google.common.truth.Truth +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.database.TranslationsDBAdapter import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.labs.androidquran.util.QuranSettings -import com.quran.labs.awaitTerminalEvent -import io.reactivex.rxjava3.observers.TestObserver +import kotlinx.coroutines.test.runTest import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -16,6 +18,7 @@ import okio.source import org.junit.After import org.junit.Before import org.junit.Test +import org.mockito.Mockito import org.mockito.Mockito.mock import java.io.File import java.io.IOException @@ -27,23 +30,11 @@ class TranslationManagerPresenterTest { } private lateinit var mockWebServer: MockWebServer - private lateinit var translationManager: TranslationManagerPresenter + private val mockSettings = mock(QuranSettings::class.java) @Before fun setup() { - val mockAppContext = mock(Context::class.java) - val mockSettings = mock(QuranSettings::class.java) - val mockOkHttp = OkHttpClient.Builder().build() mockWebServer = MockWebServer() - translationManager = object : TranslationManagerPresenter( - mockAppContext, mockOkHttp, mockSettings, mock(TranslationsDBAdapter::class.java), - mock(QuranFileUtils::class.java) - ) { - override fun writeTranslationList(list: TranslationList) { - // no op - } - } - translationManager.host = mockWebServer.url("").toString() } @After @@ -56,46 +47,81 @@ class TranslationManagerPresenterTest { } @Test - fun testGetCachedTranslationListObservable() { - val testObserver = TestObserver() - translationManager.cachedTranslationListObservable - .subscribe(testObserver) - testObserver.awaitTerminalEvent() - testObserver.assertNoValues() - testObserver.assertNoErrors() + fun testGetCachedTranslationList() = runTest { + translationManager().cachedTranslationList().test { + awaitComplete() + } + } + + @Test + fun testGetTranslationsWhenStaleCache() = runTest { + val translationManager = translationManager(true) + Mockito.`when`(mockSettings.lastUpdatedTranslationDate).thenReturn(0) + enqueueMockResponse() + + translationManager.getTranslations(false).test { + val firstItem = awaitItem() + val secondItem = awaitItem() + // in this test, both the cache and the network return the same result + Truth.assertThat(firstItem).isEqualTo(secondItem) + awaitComplete() + } } @Test - @Throws(Exception::class) - fun getRemoteTranslationListObservable() { + fun testGetRemoteTranslationList() = runTest { + enqueueMockResponse() + translationManager().remoteTranslationList().test { + val item = awaitItem() + Truth.assertThat(item.translations).hasSize(57) + awaitComplete() + } + } + + @Test + fun testRemoteTranslationListServerIssue() = runTest { + val mockResponse = MockResponse() + mockResponse.setResponseCode(500) + mockWebServer.enqueue(mockResponse) + + translationManager().remoteTranslationList().test { + val throwable = awaitError() + Truth.assertThat(throwable).isInstanceOf(IOException::class.java) + } + } + + private fun enqueueMockResponse() { val mockResponse = MockResponse() val file = File(CLI_ROOT_DIRECTORY, "translations.json") val buffer = Buffer() buffer.writeAll(file.source()) mockResponse.setBody(buffer) mockWebServer.enqueue(mockResponse) - - val testObserver = TestObserver() - translationManager.remoteTranslationListObservable - .subscribe(testObserver) - testObserver.awaitTerminalEvent() - testObserver.assertValueCount(1) - testObserver.assertNoErrors() - val (translations) = testObserver.values()[0] - Truth.assertThat(translations).hasSize(57) } - @Test - fun getRemoteTranslationListObservableIssue() { - val mockResponse = MockResponse() - mockResponse.setResponseCode(500) - mockWebServer.enqueue(mockResponse) + private fun translationManager(withCache: Boolean = false): TranslationManagerPresenter { + val mockAppContext = mock(Context::class.java) + val mockOkHttp = OkHttpClient.Builder().build() - val testObserver = TestObserver() - translationManager.remoteTranslationListObservable - .subscribe(testObserver) - testObserver.awaitTerminalEvent() - testObserver.assertNoValues() - testObserver.assertError(IOException::class.java) + return object : TranslationManagerPresenter( + mockAppContext, mockOkHttp, mockSettings, mock(TranslationsDBAdapter::class.java), + mock(QuranFileUtils::class.java) + ) { + override val cachedFile: File = + if (withCache) File(CLI_ROOT_DIRECTORY, "translations.json") else super.cachedFile + + // TODO: this is necessary because we don't have a way to mock the database adapter yet + override suspend fun mergeWithServerTranslations(serverTranslations: List): List { + return serverTranslations.map { + TranslationItem(it, 0) + } + } + + override fun writeTranslationList(list: TranslationList) { + // no op + } + }.apply { + host = mockWebServer.url("").toString() + } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 264529aa2a..2d99da883f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -68,6 +68,7 @@ espressoVersion = "3.5.1" truthVersion = "1.1.5" mockitoVersion = "5.8.0" robolectricVersion = "4.11.1" +turbineVersion = "1.0.0" [libraries] kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutinesVersion" } @@ -168,7 +169,8 @@ junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "androidxJun mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockitoVersion" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectricVersion" } truth = { module = "com.google.truth:truth", version.ref = "truthVersion" } - +turbine = { module = "app.cash.turbine:turbine-jvm", version.ref = "turbineVersion" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesVersion" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From c76229c07e35d70c5a1c0529068841e673874b39 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 20:36:31 +0400 Subject: [PATCH 197/258] Use coroutines instead of Rx for translations --- .../model/translation/TranslationModel.kt | 13 +- .../translation/BaseTranslationPresenter.kt | 124 ++++++++++-------- .../translation/InlineTranslationPresenter.kt | 20 +-- .../TranslationManagerPresenter.kt | 4 +- .../translation/TranslationPresenter.kt | 89 ++++++++----- .../ui/fragment/AyahTranslationFragment.kt | 12 +- .../ui/fragment/TabletFragment.java | 4 +- .../ui/fragment/TranslationFragment.java | 2 +- 8 files changed, 151 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt b/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt index 12eb6cdca6..e1f9317494 100644 --- a/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt +++ b/app/src/main/java/com/quran/labs/androidquran/model/translation/TranslationModel.kt @@ -10,7 +10,8 @@ import com.quran.labs.androidquran.database.DatabaseHandler import com.quran.labs.androidquran.database.DatabaseHandler.TextType import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.mobile.di.qualifier.ApplicationContext -import io.reactivex.rxjava3.core.Single +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import javax.inject.Inject @ActivityScope @@ -20,7 +21,7 @@ class TranslationModel @Inject internal constructor( private val ayahMapper: AyahMapper ) { - fun getArabicFromDatabase(verses: VerseRange): Single> { + suspend fun getArabicFromDatabase(verses: VerseRange): List { return getVersesFromDatabase( verses, QuranDataProvider.QURAN_ARABIC_DATABASE, @@ -29,17 +30,17 @@ class TranslationModel @Inject internal constructor( ) } - fun getTranslationFromDatabase(verses: VerseRange, db: String): Single> { + suspend fun getTranslationFromDatabase(verses: VerseRange, db: String): List { return getVersesFromDatabase(verses, db, TextType.TRANSLATION, shouldMap = true) } - private fun getVersesFromDatabase( + private suspend fun getVersesFromDatabase( verses: VerseRange, database: String, @TextType type: Int, shouldMap: Boolean = false - ): Single> { - return Single.fromCallable { + ): List { + return withContext(Dispatchers.IO) { val databaseHandler = DatabaseHandler.getDatabaseHandler(appContext, database, quranFileUtils) if (shouldMap) { 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 7d9474fc9e..12995febd4 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 @@ -5,7 +5,6 @@ import com.quran.data.model.QuranText import com.quran.data.model.SuraAyah import com.quran.data.model.SuraAyahIterator import com.quran.data.model.VerseRange -import com.quran.labs.androidquran.common.LocalTranslationDisplaySort import com.quran.labs.androidquran.common.QuranAyahInfo import com.quran.labs.androidquran.common.TranslationMetadata import com.quran.labs.androidquran.database.TranslationsDBAdapter @@ -14,12 +13,12 @@ import com.quran.labs.androidquran.presenter.Presenter import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.TranslationUtil import com.quran.mobile.translation.model.LocalTranslation -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.schedulers.Schedulers -import java.util.Collections +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext open class BaseTranslationPresenter internal constructor( private val translationModel: TranslationModel, @@ -30,46 +29,57 @@ open class BaseTranslationPresenter internal constructor( private val translationMap: MutableMap = HashMap() var translationScreen: T? = null - var disposable: Disposable? = null - - fun getVerses(getArabic: Boolean, - translationsFileNames: List, - verseRange: VerseRange - ): Single { - - val translations = translationsAdapter.legacyGetTranslations() - val sortedTranslations: List = ArrayList(translations) - Collections.sort(sortedTranslations, LocalTranslationDisplaySort()) - - val orderedTranslationsFileNames = sortedTranslations - .filter { translationsFileNames.contains(it.filename) } - .map { it.filename } - - // get all the translations for these verses, using a source of the list of ordered active translations - val source = Observable.fromIterable(orderedTranslationsFileNames) - - val translationsObservable = - source.concatMapEager { db -> - translationModel.getTranslationFromDatabase(verseRange, db) - .map { texts -> ensureProperTranslations(verseRange, texts) } - .onErrorReturnItem(ArrayList()) - .toObservable() + + suspend fun getVerses( + getArabic: Boolean, + translationsFileNames: List, + verseRange: VerseRange + ): ResultHolder { + return withContext(Dispatchers.IO) { + val translations = translationsAdapter.getTranslations().first() + val sortedTranslations: List = translations.sortedBy { it.displayOrder } + + val orderedTranslationsFileNames = sortedTranslations + .filter { translationsFileNames.contains(it.filename) } + .map { it.filename } + + val job = SupervisorJob() + // get all the translations for these verses, using a source of the list of ordered active translations + val translationData = orderedTranslationsFileNames.map { + async(job) { + val initialTexts = translationModel.getTranslationFromDatabase(verseRange, it) + ensureProperTranslations(verseRange, initialTexts) + } + } + + val arabic = async(job) { + if (getArabic) { + translationModel.getArabicFromDatabase(verseRange) + } else { + emptyList() + } + } + + val arabicText = + try { + arabic.await() + } catch (e: Exception) { + emptyList() + } + + val translationTexts = translationData.map { deferred -> + try { + deferred.await() + } catch (e: Exception) { + emptyList() + } } - .toList() - val arabicObservable = if (!getArabic) - Single.just(ArrayList()) - else - translationModel.getArabicFromDatabase(verseRange).onErrorReturnItem(ArrayList()) - return Single.zip( - arabicObservable, translationsObservable, getTranslationMapSingle() - ) { arabic: List, - texts: List>, - map: Map -> - val translationInfos = getTranslations(orderedTranslationsFileNames, map) - val ayahInfo = combineAyahData(verseRange, arabic, texts, translationInfos) + val translationMap = getTranslationMap() + + val translationInfos = getTranslations(orderedTranslationsFileNames, translationMap) + val ayahInfo = combineAyahData(verseRange, arabicText, translationTexts, translationInfos) ResultHolder(translationInfos, ayahInfo) } - .subscribeOn(Schedulers.io()) } fun getTranslations(quranSettings: QuranSettings): List { @@ -174,18 +184,19 @@ open class BaseTranslationPresenter internal constructor( return texts } - private fun getTranslationMapSingle(): Single> { - return if (this.translationMap.isEmpty()) { - Single.fromCallable { translationsAdapter.legacyGetTranslations() } - .map { translations -> translations.associateBy { it.filename } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess { map -> - this.translationMap.clear() - this.translationMap.putAll(map) - } - } else { - Single.just(this.translationMap) + private suspend fun getTranslationMap(): Map { + val currentTranslationMap = translationMap + return withContext(Dispatchers.IO) { + if (currentTranslationMap.isEmpty()) { + val updatedTranslations = translationsAdapter.getTranslations() + .map { it.associateBy { it.filename } } + .first() + translationMap.clear() + translationMap.putAll(updatedTranslations) + updatedTranslations + } else { + currentTranslationMap + } } } @@ -198,6 +209,5 @@ open class BaseTranslationPresenter internal constructor( override fun unbind(what: T) { translationScreen = null - disposable?.dispose() } } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt index 714cf0876c..678451d325 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt @@ -9,11 +9,11 @@ import com.quran.labs.androidquran.presenter.translationlist.TranslationListPres import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.TranslationUtil import com.quran.mobile.translation.model.LocalTranslation -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.observers.DisposableSingleObserver +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext import javax.inject.Inject class InlineTranslationPresenter @Inject constructor( @@ -38,17 +38,11 @@ class InlineTranslationPresenter @Inject constructor( .launchIn(scope) } - fun refresh(verseRange: VerseRange) { - disposable?.dispose() - disposable = getVerses(false, getTranslations(quranSettings), verseRange) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(object : DisposableSingleObserver() { - override fun onSuccess(result: ResultHolder) { - translationScreen?.setVerses(result.translations, result.ayahInformation) - } - - override fun onError(e: Throwable) {} - }) + suspend fun refresh(verseRange: VerseRange) { + val result = withContext(Dispatchers.IO) { + getVerses(false, getTranslations(quranSettings), verseRange) + } + translationScreen?.setVerses(result.translations, result.ayahInformation) } override fun bind(what: TranslationScreen) { diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt index 3c8a696f0f..ec7c04690a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt @@ -2,7 +2,6 @@ package com.quran.labs.androidquran.presenter.translation import android.content.Context import android.util.Pair -import androidx.annotation.VisibleForTesting import com.quran.labs.androidquran.dao.translation.Translation import com.quran.labs.androidquran.dao.translation.TranslationItem import com.quran.labs.androidquran.dao.translation.TranslationList @@ -43,8 +42,7 @@ open class TranslationManagerPresenter @Inject internal constructor( private val translationsDBAdapter: TranslationsDBAdapter, private val quranFileUtils: QuranFileUtils ) { - @VisibleForTesting - var host: String = Constants.HOST + internal var host: String = Constants.HOST private val scope = MainScope() diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt index 8b7f3a9b95..a029cc5267 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt @@ -11,43 +11,61 @@ import com.quran.mobile.translation.model.LocalTranslation import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.observers.DisposableObserver +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @QuranPageScope -internal class TranslationPresenter @Inject internal constructor(translationModel: TranslationModel, - private val quranSettings: QuranSettings, - translationsAdapter: TranslationsDBAdapter, - translationUtil: TranslationUtil, - private val quranInfo: QuranInfo, - private val pages: IntArray) : - BaseTranslationPresenter( - translationModel, translationsAdapter, translationUtil, quranInfo) { +internal class TranslationPresenter @Inject internal constructor( + translationModel: TranslationModel, + private val quranSettings: QuranSettings, + translationsAdapter: TranslationsDBAdapter, + translationUtil: TranslationUtil, + private val quranInfo: QuranInfo, + private val pages: IntArray +) : + BaseTranslationPresenter( + translationModel, translationsAdapter, translationUtil, quranInfo + ) { - fun refresh() { - disposable?.dispose() + private val scope = MainScope() - disposable = Observable.fromArray(*pages.toTypedArray()) - .flatMap { page -> - getVerses(quranSettings.wantArabicInTranslationView(), - getTranslations(quranSettings), quranInfo.getVerseRangeForPage(page)) - .toObservable() - } - .observeOn(AndroidSchedulers.mainThread()) - .subscribeWith(object : DisposableObserver() { - override fun onNext(result: ResultHolder) { - val screen = translationScreen - if (screen != null && result.ayahInformation.isNotEmpty()) { - screen.setVerses( - getPage(result.ayahInformation), result.translations, - result.ayahInformation) - screen.updateScrollPosition() - } - } - - override fun onError(e: Throwable) {} + fun legacyRefresh() { + scope.launch { + refresh() + } + } - override fun onComplete() {} - }) + suspend fun refresh() { + pages + .map { + withContext(Dispatchers.IO) { + getVerses( + quranSettings.wantArabicInTranslationView(), + getTranslations(quranSettings), quranInfo.getVerseRangeForPage(it) + ) + } + } + .onEach { result -> + val screen = translationScreen + if (screen != null && result.ayahInformation.isNotEmpty()) { + screen.setVerses( + getPage(result.ayahInformation), result.translations, + result.ayahInformation + ) + screen.updateScrollPosition() + } + } } private fun getPage(result: List): Int { @@ -60,9 +78,12 @@ internal class TranslationPresenter @Inject internal constructor(translationMode } interface TranslationScreen { - fun setVerses(page: Int, - translations: Array, - verses: List<@JvmSuppressWildcards QuranAyahInfo>) + fun setVerses( + page: Int, + translations: Array, + verses: List<@JvmSuppressWildcards QuranAyahInfo> + ) + fun updateScrollPosition() } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt index 11248eeac5..077b427b5d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.math.abs @@ -48,6 +49,8 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { @Inject lateinit var translationPresenter: InlineTranslationPresenter + private val scope = MainScope() + object Provider : AyahActionFragmentProvider { override val order = SlidingPagerAdapter.TRANSLATION_PAGE override val iconResId = com.quran.labs.androidquran.common.toolbar.R.drawable.ic_translation @@ -59,6 +62,11 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { (activity as? PagerActivity)?.pagerActivityComponent?.inject(this) } + override fun onDetach() { + scope.cancel() + super.onDetach() + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -151,7 +159,9 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { quranInfo.getAyahId(start.sura, start.ayah) - quranInfo.getAyahId(end.sura, end.ayah) ) val verseRange = VerseRange(start.sura, start.ayah, end.sura, end.ayah, verses) - translationPresenter.refresh(verseRange) + scope.launch { + translationPresenter.refresh(verseRange) + } } override fun setVerses(translations: Array, verses: List) { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java index 3a57d8347b..4e52ea55ee 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java @@ -408,7 +408,7 @@ public void hidePageDownloadError() { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); if (mode == Mode.TRANSLATION) { - translationPresenter.get().refresh(); + translationPresenter.get().legacyRefresh(); } } @@ -438,7 +438,7 @@ public void updateScrollPosition() { public void refresh() { if (mode == Mode.TRANSLATION) { - translationPresenter.get().refresh(); + translationPresenter.get().legacyRefresh(); } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java index 5227d9f671..8eb1359803 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java @@ -157,7 +157,7 @@ public void updateScrollPosition() { } public void refresh() { - presenter.refresh(); + presenter.legacyRefresh(); } @Override From c4993f0e6dcb7b812635e1ef20671db4a487dfe3 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 21:24:41 +0400 Subject: [PATCH 198/258] Avoid unnecessary reads of translations data --- .../presenter/translation/InlineTranslationPresenter.kt | 2 +- .../labs/androidquran/ui/fragment/AyahActionFragment.kt | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt index 678451d325..d084f71502 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt @@ -21,7 +21,7 @@ class InlineTranslationPresenter @Inject constructor( dbAdapter: TranslationsDBAdapter, translationUtil: TranslationUtil, private val quranSettings: QuranSettings, - private val translationListPresenter: TranslationListPresenter, + translationListPresenter: TranslationListPresenter, quranInfo: QuranInfo ) : BaseTranslationPresenter( translationModel, dbAdapter, translationUtil, quranInfo diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt index 3f0287a9de..0b7f45314f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt @@ -34,6 +34,7 @@ abstract class AyahActionFragment : Fragment() { scope = MainScope() readingEventPresenter.ayahSelectionFlow .combine(audioEventPresenter.audioPlaybackAyahFlow) { selectedAyah, playbackAyah -> + val (previousStart, previousEnd) = start to end if (selectedAyah !is AyahSelection.None) { start = selectedAyah.startSuraAyah() end = selectedAyah.endSuraAyah() @@ -41,7 +42,10 @@ abstract class AyahActionFragment : Fragment() { start = playbackAyah end = playbackAyah } - refreshView() + + if (previousStart != start || previousEnd != end) { + refreshView() + } } .launchIn(scope) From 4abd1bd18b550bec62530112dfc347aa3703d184 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 23:08:48 +0400 Subject: [PATCH 199/258] Kotlinify TabletFragment --- .../presenter/quran/QuranPageScreen.kt | 4 +- .../quran/ayahtracker/AyahTrackerPresenter.kt | 4 +- .../translation/TranslationPresenter.kt | 3 +- .../ui/fragment/TabletFragment.java | 526 ------------------ .../ui/fragment/TabletFragment.kt | 516 +++++++++++++++++ 5 files changed, 521 insertions(+), 532 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPageScreen.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPageScreen.kt index 8e5587e7e2..8743956c2f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPageScreen.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/QuranPageScreen.kt @@ -6,10 +6,10 @@ import com.quran.page.common.data.AyahCoordinates import com.quran.page.common.data.PageCoordinates interface QuranPageScreen { - fun setPageCoordinates(pageCoordinates: PageCoordinates?) + fun setPageCoordinates(pageCoordinates: PageCoordinates) fun setAyahCoordinatesError() fun setPageBitmap(page: Int, pageBitmap: Bitmap) fun hidePageDownloadError() fun setPageDownloadError(@StringRes errorMessage: Int) - fun setAyahCoordinatesData(coordinates: AyahCoordinates?) + fun setAyahCoordinatesData(coordinates: AyahCoordinates) } diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt index eba86fea10..5b19973c31 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt @@ -371,7 +371,7 @@ class AyahTrackerPresenter @Inject constructor( } override fun bind(what: AyahInteractionHandler) { - items = what.ayahTrackerItems + items = what.getAyahTrackerItems() scope = MainScope() if (isRecitationEnabled) { recitationPopupPresenter.bind(this) @@ -390,7 +390,7 @@ class AyahTrackerPresenter @Inject constructor( } interface AyahInteractionHandler { - val ayahTrackerItems: Array + fun getAyahTrackerItems(): Array } // PopupContainer <--> AyahTrackerItem adapter diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt index a029cc5267..80e2f2c115 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt @@ -25,8 +25,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject -@QuranPageScope -internal class TranslationPresenter @Inject internal constructor( +@QuranPageScope class TranslationPresenter @Inject internal constructor( translationModel: TranslationModel, private val quranSettings: QuranSettings, translationsAdapter: TranslationsDBAdapter, diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java deleted file mode 100644 index 4e52ea55ee..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ /dev/null @@ -1,526 +0,0 @@ -package com.quran.labs.androidquran.ui.fragment; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; - -import com.quran.data.core.QuranInfo; -import com.quran.data.model.SuraAyah; -import com.quran.data.model.selection.AyahSelection; -import com.quran.labs.androidquran.common.QuranAyahInfo; -import com.quran.labs.androidquran.data.QuranDisplayData; -import com.quran.labs.androidquran.presenter.quran.QuranPagePresenter; -import com.quran.labs.androidquran.presenter.quran.QuranPageScreen; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahImageTrackerItem; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahSplitConsolidationTrackerItem; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerItem; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerPresenter; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTranslationTrackerItem; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.NoOpImageTrackerItem; -import com.quran.labs.androidquran.presenter.translation.TranslationPresenter; -import com.quran.labs.androidquran.ui.PagerActivity; -import com.quran.labs.androidquran.ui.helpers.AyahSelectedListener; -import com.quran.labs.androidquran.ui.helpers.AyahTracker; -import com.quran.labs.androidquran.ui.helpers.QuranPage; -import com.quran.labs.androidquran.ui.translation.TranslationView; -import com.quran.labs.androidquran.ui.util.PageController; -import com.quran.labs.androidquran.util.QuranScreenInfo; -import com.quran.labs.androidquran.util.QuranSettings; -import com.quran.labs.androidquran.view.HighlightingImageView; -import com.quran.labs.androidquran.view.QuranImagePageLayout; -import com.quran.labs.androidquran.view.QuranTranslationPageLayout; -import com.quran.labs.androidquran.view.TabletView; -import com.quran.mobile.translation.model.LocalTranslation; -import com.quran.page.common.data.AyahCoordinates; -import com.quran.page.common.data.PageCoordinates; -import com.quran.page.common.draw.ImageDrawHelper; -import com.quran.page.common.factory.PageViewFactory; -import com.quran.page.common.factory.PageViewFactoryProvider; -import com.quran.reading.common.ReadingEventPresenter; - -import java.util.List; -import java.util.Set; - -import javax.inject.Inject; - -import dagger.Lazy; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -import timber.log.Timber; - -public class TabletFragment extends Fragment - implements PageController, TranslationPresenter.TranslationScreen, - QuranPage, QuranPageScreen, AyahTrackerPresenter.AyahInteractionHandler { - private static final String FIRST_PAGE_EXTRA = "pageNumber"; - private static final String MODE_EXTRA = "mode"; - private static final String IS_SPLIT_SCREEN = "splitScreenMode"; - private static final String SI_RIGHT_TRANSLATION_SCROLL_POSITION - = "SI_RIGHT_TRANSLATION_SCROLL_POSITION"; - - public static class Mode { - public static final int ARABIC = 1; - public static final int TRANSLATION = 2; - } - - private int mode; - private int pageNumber; - private int translationScrollPosition; - private boolean ayahCoordinatesError; - private boolean isSplitScreen = false; - private boolean isQuranOnRight = true; - - private TabletView mainView; - private TranslationView leftTranslation; - private TranslationView rightTranslation; - private HighlightingImageView leftImageView; - private HighlightingImageView rightImageView; - private final CompositeDisposable compositeDisposable = new CompositeDisposable(); - private AyahTrackerItem[] ayahTrackerItems; - - private TranslationView splitTranslationView; - private HighlightingImageView splitImageView; - private int lastLongPressPage = -1; - - @Inject QuranSettings quranSettings; - @Inject AyahTrackerPresenter ayahTrackerPresenter; - @Inject Lazy quranPagePresenter; - @Inject Lazy translationPresenter; - @Inject AyahSelectedListener ayahSelectedListener; - @Inject QuranScreenInfo quranScreenInfo; - @Inject QuranInfo quranInfo; - @Inject QuranDisplayData quranDisplayData; - @Inject Set imageDrawHelpers; - @Inject ReadingEventPresenter readingEventPresenter; - @Inject PageViewFactoryProvider pageProviderFactoryProvider; - - @Nullable PageViewFactory pageViewFactory = null; - - boolean isCustomArabicPageType = false; - - public static TabletFragment newInstance(int firstPage, int mode, boolean isSplitScreen) { - final TabletFragment f = new TabletFragment(); - final Bundle args = new Bundle(); - args.putInt(FIRST_PAGE_EXTRA, firstPage); - args.putInt(MODE_EXTRA, mode); - args.putBoolean(IS_SPLIT_SCREEN, isSplitScreen); - f.setArguments(args); - return f; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (savedInstanceState != null) { - translationScrollPosition = savedInstanceState.getInt( - SI_RIGHT_TRANSLATION_SCROLL_POSITION); - } - pageViewFactory = pageProviderFactoryProvider.providePageViewFactory(quranSettings.getPageType()); - isCustomArabicPageType = pageViewFactory != null; - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - ViewGroup container, Bundle savedInstanceState) { - final Context context = getActivity(); - mainView = new TabletView(context); - - if (mode == Mode.ARABIC) { - mainView.init(TabletView.QURAN_PAGE, TabletView.QURAN_PAGE, pageViewFactory, pageNumber + 1, pageNumber); - if (mainView.getLeftPage() instanceof QuranImagePageLayout) { - leftImageView = ((QuranImagePageLayout) mainView.getLeftPage()).getImageView(); - rightImageView = ((QuranImagePageLayout) mainView.getRightPage()).getImageView(); - } - mainView.setPageController(this, pageNumber + 1, pageNumber, quranInfo.getSkip()); - } else if (mode == Mode.TRANSLATION) { - if (!isSplitScreen) { - mainView.init(TabletView.TRANSLATION_PAGE, TabletView.TRANSLATION_PAGE, pageViewFactory, pageNumber + 1, pageNumber); - leftTranslation = - ((QuranTranslationPageLayout) mainView.getLeftPage()).getTranslationView(); - rightTranslation = - ((QuranTranslationPageLayout) mainView.getRightPage()).getTranslationView(); - - PagerActivity pagerActivity = (PagerActivity) context; - leftTranslation.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); - rightTranslation.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); - mainView.setPageController(this, pageNumber + 1, pageNumber, quranInfo.getSkip()); - } else { - initSplitMode(); - } - } - return mainView; - } - - private void initSplitMode() { - final int skip = quranInfo.getSkip(); - isQuranOnRight = (pageNumber + skip) % 2 == 1; - - final int leftPageType = isQuranOnRight ? TabletView.TRANSLATION_PAGE : TabletView.QURAN_PAGE; - final int rightPageType = isQuranOnRight ? TabletView.QURAN_PAGE : TabletView.TRANSLATION_PAGE; - - mainView.init(leftPageType, rightPageType, pageViewFactory, pageNumber, pageNumber); - - if (isQuranOnRight) { - splitTranslationView = - ((QuranTranslationPageLayout) mainView.getLeftPage()).getTranslationView(); - if (mainView.getRightPage() instanceof QuranImagePageLayout) { - splitImageView = - ((QuranImagePageLayout) mainView.getRightPage()).getImageView(); - } else { - splitImageView = null; - } - } else { - if (mainView.getLeftPage() instanceof QuranImagePageLayout) { - splitImageView = - ((QuranImagePageLayout) mainView.getLeftPage()).getImageView(); - } else { - splitImageView = null; - } - splitTranslationView = - ((QuranTranslationPageLayout) mainView.getRightPage()).getTranslationView(); - } - - PagerActivity pagerActivity = (PagerActivity) getActivity(); - splitTranslationView.setTranslationClickedListener(v -> pagerActivity.toggleActionBar()); - mainView.setPageController(this, pageNumber, quranInfo.getSkip()); - } - - @Override - public void onStart() { - super.onStart(); - ayahTrackerPresenter.bind(this); - if (mode == Mode.ARABIC) { - if (!isCustomArabicPageType) { - quranPagePresenter.get().bind(this); - } - } else { - if (isSplitScreen) { - translationPresenter.get().bind(this); - if (!isCustomArabicPageType) { - quranPagePresenter.get().bind(this); - } - } else { - translationPresenter.get().bind(this); - } - } - } - - @Override - public void onPause() { - if (mode == Mode.TRANSLATION) { - if (isSplitScreen) { - translationScrollPosition = splitTranslationView.findFirstCompletelyVisibleItemPosition(); - } else { - translationScrollPosition = rightTranslation - .findFirstCompletelyVisibleItemPosition(); - } - } - super.onPause(); - } - - @Override - public void onStop() { - ayahTrackerPresenter.unbind(this); - if (mode == Mode.ARABIC) { - quranPagePresenter.get().unbind(this); - } else { - if (isSplitScreen) { - translationPresenter.get().unbind(this); - quranPagePresenter.get().unbind(this); - } else { - translationPresenter.get().unbind(this); - } - } - super.onStop(); - } - - @Override - public void onResume() { - super.onResume(); - updateView(); - if (mode == Mode.TRANSLATION) { - if (isSplitScreen) { - splitTranslationView.refresh(quranSettings); - } else { - rightTranslation.refresh(quranSettings); - leftTranslation.refresh(quranSettings); - } - } - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - if (mode == Mode.TRANSLATION) { - if (isSplitScreen) { - outState.putInt(SI_RIGHT_TRANSLATION_SCROLL_POSITION, - splitTranslationView.findFirstCompletelyVisibleItemPosition()); - } else { - outState.putInt(SI_RIGHT_TRANSLATION_SCROLL_POSITION, - rightTranslation.findFirstCompletelyVisibleItemPosition()); - } - } - super.onSaveInstanceState(outState); - } - - @Override - public void updateView() { - if (isAdded()) { - mainView.updateView(quranSettings); - } - } - - @NonNull - @Override - public AyahTracker getAyahTracker() { - return ayahTrackerPresenter; - } - - @NonNull - @Override - public AyahTrackerItem[] getAyahTrackerItems() { - if (ayahTrackerItems == null) { - AyahTrackerItem left; - AyahTrackerItem right; - if (mode == Mode.ARABIC) { - if (leftImageView != null && rightImageView != null) { - if (quranInfo.isValidPage(pageNumber + 1)) { - left = new AyahImageTrackerItem(pageNumber + 1, - quranInfo, - quranDisplayData, - false, - imageDrawHelpers, - leftImageView); - } else { - left = new NoOpImageTrackerItem(pageNumber + 1); - } - right = new AyahImageTrackerItem( - pageNumber, quranInfo, quranDisplayData, true, imageDrawHelpers, - rightImageView); - } else { - return new AyahTrackerItem[0]; - } - } else if (mode == Mode.TRANSLATION) { - if (isSplitScreen) { - final AyahImageTrackerItem imageItem; - final AyahTranslationTrackerItem translationItem; - if (isQuranOnRight) { - translationItem = new AyahTranslationTrackerItem(pageNumber, quranInfo, splitTranslationView); - if (splitImageView != null) { - imageItem = new AyahImageTrackerItem(pageNumber, - quranInfo, - quranDisplayData, - true, - imageDrawHelpers, - splitImageView); - } else { - imageItem = null; - } - } else { - if (splitImageView != null) { - imageItem = new AyahImageTrackerItem(pageNumber, - quranInfo, - quranDisplayData, - false, - imageDrawHelpers, - splitImageView); - } else { - imageItem = null; - } - translationItem = new AyahTranslationTrackerItem(pageNumber, quranInfo, splitTranslationView); - } - final AyahTrackerItem splitItem = - imageItem == null ? - new AyahTranslationTrackerItem(pageNumber, quranInfo, splitTranslationView) : - new AyahSplitConsolidationTrackerItem(pageNumber, imageItem, translationItem); - ayahTrackerItems = new AyahTrackerItem[] { splitItem }; - return ayahTrackerItems; - } else { - left = new AyahTranslationTrackerItem(pageNumber + 1, quranInfo, leftTranslation); - right = new AyahTranslationTrackerItem(pageNumber, quranInfo, rightTranslation); - } - } else { - return new AyahTrackerItem[0]; - } - ayahTrackerItems = new AyahTrackerItem[]{ right, left }; - } - return ayahTrackerItems; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - pageNumber = getArguments().getInt(FIRST_PAGE_EXTRA); - mode = getArguments().getInt(MODE_EXTRA, Mode.ARABIC); - isSplitScreen = getArguments().getBoolean(IS_SPLIT_SCREEN, false); - - final int[] pages = (isSplitScreen && mode == Mode.TRANSLATION) ? - new int[]{ pageNumber } : new int[]{ pageNumber, pageNumber + 1 }; - - ((PagerActivity) getActivity()).getPagerActivityComponent() - .quranPageComponentFactory() - .generate(pages) - .inject(this); - } - - @Override - public void onDetach() { - super.onDetach(); - ayahSelectedListener = null; - compositeDisposable.clear(); - } - - @Override - public void setPageDownloadError(@StringRes int errorMessage) { - mainView.showError(errorMessage); - mainView.setOnClickListener(v -> ayahTrackerPresenter.onPressIgnoringSelectionState()); - } - - @Override - public void setPageBitmap(int page, @NonNull Bitmap pageBitmap) { - if (isSplitScreen && mode == Mode.TRANSLATION) { - splitImageView.setImageDrawable(new BitmapDrawable(getResources(), pageBitmap)); - } else { - ImageView imageView = page == pageNumber ? rightImageView : leftImageView; - if (imageView != null) { - imageView.setImageDrawable(new BitmapDrawable(getResources(), pageBitmap)); - } - } - } - - @Override - public void hidePageDownloadError() { - mainView.hideError(); - mainView.setOnClickListener(null); - mainView.setClickable(false); - } - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (mode == Mode.TRANSLATION) { - translationPresenter.get().legacyRefresh(); - } - } - - @Override - public void setVerses(int page, - @NonNull LocalTranslation[] translations, - @NonNull List verses) { - if (isSplitScreen) { - splitTranslationView.setVerses(quranDisplayData, translations, verses); - } else { - if (page == pageNumber) { - rightTranslation.setVerses(quranDisplayData, translations, verses); - } else if (page == pageNumber + 1) { - leftTranslation.setVerses(quranDisplayData, translations, verses); - } - } - } - - @Override - public void updateScrollPosition() { - if (isSplitScreen) { - splitTranslationView.setScrollPosition(translationScrollPosition); - } else { - rightTranslation.setScrollPosition(translationScrollPosition); - } - } - - public void refresh() { - if (mode == Mode.TRANSLATION) { - translationPresenter.get().legacyRefresh(); - } - } - - public void cleanup() { - Timber.d("cleaning up page %d", pageNumber); - if (leftImageView != null) { - leftImageView.setImageDrawable(null); - } - - if (rightImageView != null) { - rightImageView.setImageDrawable(null); - } - - if (splitImageView != null) { - splitImageView.setImageDrawable(null); - } - } - - @Override - public void setPageCoordinates(PageCoordinates pageCoordinates) { - ayahTrackerPresenter.setPageBounds(pageCoordinates); - } - - @Override - public void setAyahCoordinatesError() { - ayahCoordinatesError = true; - } - - @Override - public void setAyahCoordinatesData(AyahCoordinates ayahCoordinates) { - ayahTrackerPresenter.setAyahCoordinates(ayahCoordinates); - } - - @Override - public boolean handleTouchEvent(MotionEvent event, AyahSelectedListener.EventType eventType, int page) { - return isVisible() && ayahTrackerPresenter.handleTouchEvent(getActivity(), event, eventType, - page, ayahCoordinatesError); - } - - @Override - public void handleLongPress(SuraAyah suraAyah) { - if (isVisible()) { - final int page = quranInfo.getPageFromSuraAyah(suraAyah.sura, suraAyah.ayah); - if (page != lastLongPressPage) { - ayahTrackerPresenter.endAyahMode(); - } - lastLongPressPage = page; - ayahTrackerPresenter.onLongPress(suraAyah); - } - } - - @Override - public void handleRetryClicked() { - hidePageDownloadError(); - quranPagePresenter.get().downloadImages(); - } - - @Override - public void onScrollChanged(float y) { - if (isVisible()) { - final TranslationView[] views = new TranslationView[] { rightTranslation, leftTranslation }; - for (TranslationView view : views) { - if (view != null) { - final AyahSelection ayahSelection = readingEventPresenter.currentAyahSelection(); - if (ayahSelection instanceof AyahSelection.Ayah) { - final AyahSelection.Ayah currentAyahSelection = ((AyahSelection.Ayah) ayahSelection); - final SuraAyah suraAyah = currentAyahSelection.getSuraAyah(); - - readingEventPresenter.onAyahSelection( - new AyahSelection.Ayah(suraAyah, - view.getToolbarPosition(suraAyah.sura, suraAyah.ayah)) - ); - } - } - } - } - } - - @Override - public void endAyahMode() { - if (isVisible()) { - ayahTrackerPresenter.endAyahMode(); - } - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt new file mode 100644 index 0000000000..f9e8bc0c80 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt @@ -0,0 +1,516 @@ +package com.quran.labs.androidquran.ui.fragment + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.quran.data.core.QuranInfo +import com.quran.data.model.SuraAyah +import com.quran.data.model.selection.AyahSelection +import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.data.QuranDisplayData +import com.quran.labs.androidquran.presenter.quran.QuranPagePresenter +import com.quran.labs.androidquran.presenter.quran.QuranPageScreen +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahImageTrackerItem +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahSplitConsolidationTrackerItem +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerItem +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerPresenter +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerPresenter.AyahInteractionHandler +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTranslationTrackerItem +import com.quran.labs.androidquran.presenter.quran.ayahtracker.NoOpImageTrackerItem +import com.quran.labs.androidquran.presenter.translation.TranslationPresenter +import com.quran.labs.androidquran.ui.PagerActivity +import com.quran.labs.androidquran.ui.helpers.AyahSelectedListener +import com.quran.labs.androidquran.ui.helpers.AyahTracker +import com.quran.labs.androidquran.ui.helpers.QuranPage +import com.quran.labs.androidquran.ui.translation.TranslationView +import com.quran.labs.androidquran.ui.util.PageController +import com.quran.labs.androidquran.util.QuranScreenInfo +import com.quran.labs.androidquran.util.QuranSettings +import com.quran.labs.androidquran.view.HighlightingImageView +import com.quran.labs.androidquran.view.QuranImagePageLayout +import com.quran.labs.androidquran.view.QuranTranslationPageLayout +import com.quran.labs.androidquran.view.TabletView +import com.quran.mobile.translation.model.LocalTranslation +import com.quran.page.common.data.AyahCoordinates +import com.quran.page.common.data.PageCoordinates +import com.quran.page.common.draw.ImageDrawHelper +import com.quran.page.common.factory.PageViewFactory +import com.quran.page.common.factory.PageViewFactoryProvider +import com.quran.reading.common.ReadingEventPresenter +import dagger.Lazy +import io.reactivex.rxjava3.disposables.CompositeDisposable +import timber.log.Timber +import javax.inject.Inject + +class TabletFragment : Fragment(), PageController, TranslationPresenter.TranslationScreen, + QuranPage, QuranPageScreen, AyahInteractionHandler { + object Mode { + const val ARABIC = 1 + const val TRANSLATION = 2 + } + + private var mode = 0 + private var pageNumber = 0 + private var translationScrollPosition = 0 + private var ayahCoordinatesError = false + private var isSplitScreen = false + private var isQuranOnRight = true + private var leftTranslation: TranslationView? = null + private var rightTranslation: TranslationView? = null + private var leftImageView: HighlightingImageView? = null + private var rightImageView: HighlightingImageView? = null + private val compositeDisposable = CompositeDisposable() + private var ayahTrackerItemsStorage: Array? = null + private var splitTranslationView: TranslationView? = null + private var splitImageView: HighlightingImageView? = null + private var lastLongPressPage = -1 + + private lateinit var mainView: TabletView + + @Inject lateinit var quranSettings: QuranSettings + @Inject lateinit var ayahTrackerPresenter: AyahTrackerPresenter + @Inject lateinit var quranPagePresenter: Lazy + @Inject lateinit var translationPresenter: Lazy + @Inject lateinit var quranScreenInfo: QuranScreenInfo + @Inject lateinit var quranInfo: QuranInfo + @Inject lateinit var quranDisplayData: QuranDisplayData + @Inject lateinit var imageDrawHelpers: Set<@JvmSuppressWildcards ImageDrawHelper> + @Inject lateinit var readingEventPresenter: ReadingEventPresenter + @Inject lateinit var pageProviderFactoryProvider: PageViewFactoryProvider + + private var pageViewFactory: PageViewFactory? = null + private var isCustomArabicPageType = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState != null) { + translationScrollPosition = savedInstanceState.getInt( + SI_RIGHT_TRANSLATION_SCROLL_POSITION + ) + } + pageViewFactory = + pageProviderFactoryProvider.providePageViewFactory(quranSettings.pageType) + isCustomArabicPageType = pageViewFactory != null + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle? + ): View { + val context: Context? = activity + mainView = TabletView(context) + if (mode == Mode.ARABIC) { + mainView.init( + TabletView.QURAN_PAGE, + TabletView.QURAN_PAGE, + pageViewFactory, + pageNumber + 1, + pageNumber + ) + if (mainView.leftPage is QuranImagePageLayout) { + leftImageView = (mainView.leftPage as QuranImagePageLayout).getImageView() + rightImageView = (mainView.rightPage as QuranImagePageLayout).getImageView() + } + mainView.setPageController(this, pageNumber + 1, pageNumber, quranInfo.skip) + } else if (mode == Mode.TRANSLATION) { + if (!isSplitScreen) { + mainView.init( + TabletView.TRANSLATION_PAGE, + TabletView.TRANSLATION_PAGE, + pageViewFactory, + pageNumber + 1, + pageNumber + ) + val leftTranslation = (mainView.leftPage as QuranTranslationPageLayout).translationView + val rightTranslation = (mainView.rightPage as QuranTranslationPageLayout).translationView + val pagerActivity = context as PagerActivity + leftTranslation.setTranslationClickedListener { pagerActivity.toggleActionBar() } + rightTranslation.setTranslationClickedListener { pagerActivity.toggleActionBar() } + this.leftTranslation = leftTranslation + this.rightTranslation = rightTranslation + mainView.setPageController(this, pageNumber + 1, pageNumber, quranInfo.skip) + } else { + initSplitMode() + } + } + return mainView + } + + private fun initSplitMode() { + val skip = quranInfo.skip + isQuranOnRight = (pageNumber + skip) % 2 == 1 + val leftPageType = + if (isQuranOnRight) TabletView.TRANSLATION_PAGE else TabletView.QURAN_PAGE + val rightPageType = + if (isQuranOnRight) TabletView.QURAN_PAGE else TabletView.TRANSLATION_PAGE + mainView.init(leftPageType, rightPageType, pageViewFactory, pageNumber, pageNumber) + if (isQuranOnRight) { + splitTranslationView = + (mainView.leftPage as QuranTranslationPageLayout).translationView + splitImageView = if (mainView.rightPage is QuranImagePageLayout) { + (mainView.rightPage as QuranImagePageLayout).getImageView() + } else { + null + } + } else { + splitImageView = if (mainView.leftPage is QuranImagePageLayout) { + (mainView.leftPage as QuranImagePageLayout).getImageView() + } else { + null + } + splitTranslationView = (mainView.rightPage as QuranTranslationPageLayout).translationView + } + val pagerActivity = activity as PagerActivity + splitTranslationView?.setTranslationClickedListener { pagerActivity.toggleActionBar() } + mainView.setPageController(this, pageNumber, quranInfo.skip) + } + + override fun onStart() { + super.onStart() + ayahTrackerPresenter.bind(this) + if (mode == Mode.ARABIC) { + if (!isCustomArabicPageType) { + quranPagePresenter.get().bind(this) + } + } else { + if (isSplitScreen) { + translationPresenter.get().bind(this) + if (!isCustomArabicPageType) { + quranPagePresenter.get().bind(this) + } + } else { + translationPresenter.get().bind(this) + } + } + } + + override fun onPause() { + if (mode == Mode.TRANSLATION) { + val rightTranslation = rightTranslation + val splitTranslationView = splitTranslationView + if (isSplitScreen && splitTranslationView != null) { + translationScrollPosition = splitTranslationView.findFirstCompletelyVisibleItemPosition() + } else if (rightTranslation != null) { + translationScrollPosition = rightTranslation.findFirstCompletelyVisibleItemPosition() + } + } + super.onPause() + } + + override fun onStop() { + ayahTrackerPresenter.unbind(this) + if (mode == Mode.ARABIC) { + quranPagePresenter.get().unbind(this) + } else { + if (isSplitScreen) { + translationPresenter.get().unbind(this) + quranPagePresenter.get().unbind(this) + } else { + translationPresenter.get().unbind(this) + } + } + super.onStop() + } + + override fun onResume() { + super.onResume() + updateView() + if (mode == Mode.TRANSLATION) { + if (isSplitScreen) { + splitTranslationView?.refresh(quranSettings) + } else { + rightTranslation?.refresh(quranSettings) + leftTranslation?.refresh(quranSettings) + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + if (mode == Mode.TRANSLATION) { + if (isSplitScreen) { + outState.putInt( + SI_RIGHT_TRANSLATION_SCROLL_POSITION, + splitTranslationView!!.findFirstCompletelyVisibleItemPosition() + ) + } else { + outState.putInt( + SI_RIGHT_TRANSLATION_SCROLL_POSITION, + rightTranslation!!.findFirstCompletelyVisibleItemPosition() + ) + } + } + super.onSaveInstanceState(outState) + } + + override fun updateView() { + if (isAdded) { + mainView.updateView(quranSettings) + } + } + + override fun getAyahTracker(): AyahTracker { + return ayahTrackerPresenter + } + + override fun getAyahTrackerItems(): Array { + val cachedTrackerItems = ayahTrackerItemsStorage + return if (cachedTrackerItems == null) { + if (mode == Mode.ARABIC) { + val leftImageView = leftImageView + val rightImageView = rightImageView + if (leftImageView != null && rightImageView != null) { + val left = if (quranInfo.isValidPage(pageNumber + 1)) { + AyahImageTrackerItem( + pageNumber + 1, + quranInfo, + quranDisplayData, + false, + imageDrawHelpers, + leftImageView + ) + } else { + NoOpImageTrackerItem(pageNumber + 1) + } + + val right = AyahImageTrackerItem( + pageNumber, + quranInfo, + quranDisplayData, + true, + imageDrawHelpers, + rightImageView + ) + arrayOf(right, left) + } else { + emptyArray() + } + } else if (mode == Mode.TRANSLATION) { + if (isSplitScreen) { + val splitTranslationView = splitTranslationView!! + val (translationItem, imageItem) = if (isQuranOnRight) { + val translationItem = AyahTranslationTrackerItem(pageNumber, quranInfo, splitTranslationView) + val splitImageView = splitImageView + if (splitImageView != null) { + translationItem to AyahImageTrackerItem( + pageNumber, + quranInfo, + quranDisplayData, + true, + imageDrawHelpers, + splitImageView + ) + } else { + translationItem to null + } + } else { + val translationItem = AyahTranslationTrackerItem(pageNumber, quranInfo, splitTranslationView) + val splitImageView = splitImageView + if (splitImageView != null) { + translationItem to AyahImageTrackerItem(pageNumber, + quranInfo, + quranDisplayData, + false, + imageDrawHelpers, + splitImageView + ) + } else { + translationItem to null + } + } + + val splitItem = if (imageItem == null) { + AyahTranslationTrackerItem(pageNumber, quranInfo, splitTranslationView) + } else { + AyahSplitConsolidationTrackerItem(pageNumber, imageItem, translationItem) + } + arrayOf(splitItem) + } else { + val left = AyahTranslationTrackerItem(pageNumber + 1, quranInfo, leftTranslation!!) + val right = AyahTranslationTrackerItem(pageNumber, quranInfo, rightTranslation!!) + arrayOf(right, left) + } + } else { + emptyArray() + } + } else { + cachedTrackerItems + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + val arguments = requireArguments() + pageNumber = arguments.getInt(FIRST_PAGE_EXTRA) + mode = arguments.getInt(MODE_EXTRA, Mode.ARABIC) + isSplitScreen = arguments.getBoolean(IS_SPLIT_SCREEN, false) + val pages = + if (isSplitScreen && mode == Mode.TRANSLATION) intArrayOf(pageNumber) else intArrayOf( + pageNumber, + pageNumber + 1 + ) + (activity as PagerActivity).getPagerActivityComponent() + .quranPageComponentFactory() + .generate(pages) + .inject(this) + } + + override fun onDetach() { + super.onDetach() + compositeDisposable.clear() + } + + override fun setPageDownloadError(@StringRes errorMessage: Int) { + mainView.showError(errorMessage) + mainView.setOnClickListener { ayahTrackerPresenter.onPressIgnoringSelectionState() } + } + + override fun setPageBitmap(page: Int, pageBitmap: Bitmap) { + if (isSplitScreen && mode == Mode.TRANSLATION) { + splitImageView!!.setImageDrawable(BitmapDrawable(resources, pageBitmap)) + } else { + val imageView: ImageView? = if (page == pageNumber) rightImageView else leftImageView + imageView?.setImageDrawable(BitmapDrawable(resources, pageBitmap)) + } + } + + override fun hidePageDownloadError() { + mainView.hideError() + mainView.setOnClickListener(null) + mainView.isClickable = false + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (mode == Mode.TRANSLATION) { + translationPresenter.get().legacyRefresh() + } + } + + override fun setVerses( + page: Int, + translations: Array, + verses: List + ) { + if (isSplitScreen) { + splitTranslationView?.setVerses(quranDisplayData, translations, verses) + } else { + if (page == pageNumber) { + rightTranslation?.setVerses(quranDisplayData, translations, verses) + } else if (page == pageNumber + 1) { + leftTranslation?.setVerses(quranDisplayData, translations, verses) + } + } + } + + override fun updateScrollPosition() { + if (isSplitScreen) { + splitTranslationView?.setScrollPosition(translationScrollPosition) + } else { + rightTranslation?.setScrollPosition(translationScrollPosition) + } + } + + fun refresh() { + if (mode == Mode.TRANSLATION) { + translationPresenter.get().legacyRefresh() + } + } + + fun cleanup() { + Timber.d("cleaning up page %d", pageNumber) + leftImageView?.setImageDrawable(null) + rightImageView?.setImageDrawable(null) + splitImageView?.setImageDrawable(null) + } + + override fun setPageCoordinates(pageCoordinates: PageCoordinates) { + ayahTrackerPresenter.setPageBounds(pageCoordinates) + } + + override fun setAyahCoordinatesError() { + ayahCoordinatesError = true + } + + override fun setAyahCoordinatesData(coordinates: AyahCoordinates) { + ayahTrackerPresenter.setAyahCoordinates(coordinates) + } + + override fun handleTouchEvent( + event: MotionEvent, + eventType: AyahSelectedListener.EventType, + page: Int + ): Boolean { + return isVisible && ayahTrackerPresenter.handleTouchEvent( + requireActivity(), event, eventType, + page, ayahCoordinatesError + ) + } + + override fun handleLongPress(suraAyah: SuraAyah) { + if (isVisible) { + val page = quranInfo.getPageFromSuraAyah(suraAyah.sura, suraAyah.ayah) + if (page != lastLongPressPage) { + ayahTrackerPresenter.endAyahMode() + } + lastLongPressPage = page + ayahTrackerPresenter.onLongPress(suraAyah) + } + } + + override fun handleRetryClicked() { + hidePageDownloadError() + quranPagePresenter.get().downloadImages() + } + + override fun onScrollChanged(y: Float) { + if (isVisible) { + val views = arrayOf(rightTranslation, leftTranslation) + for (view in views) { + if (view != null) { + val ayahSelection = readingEventPresenter.currentAyahSelection() + if (ayahSelection is AyahSelection.Ayah) { + val (suraAyah) = ayahSelection + readingEventPresenter.onAyahSelection( + AyahSelection.Ayah( + suraAyah, + view.getToolbarPosition(suraAyah.sura, suraAyah.ayah) + ) + ) + } + } + } + } + } + + override fun endAyahMode() { + if (isVisible) { + ayahTrackerPresenter.endAyahMode() + } + } + + companion object { + private const val FIRST_PAGE_EXTRA = "pageNumber" + private const val MODE_EXTRA = "mode" + private const val IS_SPLIT_SCREEN = "splitScreenMode" + private const val SI_RIGHT_TRANSLATION_SCROLL_POSITION = + "SI_RIGHT_TRANSLATION_SCROLL_POSITION" + + fun newInstance(firstPage: Int, mode: Int, isSplitScreen: Boolean): TabletFragment { + val f = TabletFragment() + val args = Bundle() + args.putInt(FIRST_PAGE_EXTRA, firstPage) + args.putInt(MODE_EXTRA, mode) + args.putBoolean(IS_SPLIT_SCREEN, isSplitScreen) + f.setArguments(args) + return f + } + } +} From a44d6bb35405b8792c3820ab7e338bccda95a05a Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 23:54:39 +0400 Subject: [PATCH 200/258] Kotlinify TranslationFragment --- .../ui/fragment/TranslationFragment.java | 212 ------------------ .../ui/fragment/TranslationFragment.kt | 196 ++++++++++++++++ 2 files changed, 196 insertions(+), 212 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java deleted file mode 100644 index 8eb1359803..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java +++ /dev/null @@ -1,212 +0,0 @@ -package com.quran.labs.androidquran.ui.fragment; - -import android.app.Activity; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; - -import com.quran.data.core.QuranInfo; -import com.quran.data.model.SuraAyah; -import com.quran.data.model.selection.AyahSelection; -import com.quran.labs.androidquran.common.QuranAyahInfo; -import com.quran.labs.androidquran.data.QuranDisplayData; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerItem; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerPresenter; -import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTranslationTrackerItem; -import com.quran.labs.androidquran.presenter.translation.TranslationPresenter; -import com.quran.labs.androidquran.ui.PagerActivity; -import com.quran.labs.androidquran.ui.helpers.AyahSelectedListener; -import com.quran.labs.androidquran.ui.helpers.AyahTracker; -import com.quran.labs.androidquran.ui.helpers.QuranPage; -import com.quran.labs.androidquran.ui.translation.TranslationView; -import com.quran.labs.androidquran.ui.util.PageController; -import com.quran.labs.androidquran.util.QuranSettings; -import com.quran.labs.androidquran.view.QuranTranslationPageLayout; -import com.quran.mobile.translation.model.LocalTranslation; -import com.quran.reading.common.ReadingEventPresenter; - -import java.util.List; - -import javax.inject.Inject; - -public class TranslationFragment extends Fragment implements - AyahTrackerPresenter.AyahInteractionHandler, QuranPage, - TranslationPresenter.TranslationScreen, PageController { - private static final String PAGE_NUMBER_EXTRA = "pageNumber"; - - private static final String SI_SCROLL_POSITION = "SI_SCROLL_POSITION"; - - private int pageNumber; - private int scrollPosition; - - private TranslationView translationView; - private QuranTranslationPageLayout mainView; - private AyahTrackerItem[] ayahTrackerItems; - - @Inject QuranInfo quranInfo; - @Inject QuranDisplayData quranDisplayData; - @Inject QuranSettings quranSettings; - @Inject TranslationPresenter presenter; - @Inject AyahTrackerPresenter ayahTrackerPresenter; - @Inject AyahSelectedListener ayahSelectedListener; - @Inject ReadingEventPresenter readingEventPresenter; - - public static TranslationFragment newInstance(int page) { - final TranslationFragment f = new TranslationFragment(); - final Bundle args = new Bundle(); - args.putInt(PAGE_NUMBER_EXTRA, page); - f.setArguments(args); - return f; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - if (savedInstanceState != null) { - scrollPosition = savedInstanceState.getInt(SI_SCROLL_POSITION); - } - setHasOptionsMenu(true); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, - ViewGroup container, Bundle savedInstanceState) { - Context context = getActivity(); - mainView = new QuranTranslationPageLayout(context); - mainView.setPageController(this, pageNumber, quranInfo.getSkip()); - - translationView = mainView.getTranslationView(); - translationView.setTranslationClickedListener(v -> { - final Activity activity = getActivity(); - if (activity instanceof PagerActivity) { - ((PagerActivity) getActivity()).toggleActionBar(); - } - }); - - return mainView; - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - pageNumber = getArguments() != null ? getArguments().getInt(PAGE_NUMBER_EXTRA) : -1; - final int[] pages = { pageNumber }; - ((PagerActivity) getActivity()).getPagerActivityComponent() - .quranPageComponentFactory() - .generate(pages) - .inject(this); - } - - @Override - public void updateView() { - if (isAdded()) { - mainView.updateView(quranSettings); - refresh(); - } - } - - @NonNull - @Override - public AyahTracker getAyahTracker() { - return ayahTrackerPresenter; - } - - @NonNull - @Override - public AyahTrackerItem[] getAyahTrackerItems() { - if (ayahTrackerItems == null) { - ayahTrackerItems = new AyahTrackerItem[] { - new AyahTranslationTrackerItem(pageNumber, quranInfo, translationView) }; - } - return ayahTrackerItems; - } - - @Override - public void onResume() { - super.onResume(); - ayahTrackerPresenter.bind(this); - presenter.bind(this); - updateView(); - } - - @Override - public void onPause() { - ayahTrackerPresenter.unbind(this); - presenter.unbind(this); - super.onPause(); - } - - @Override - public void setVerses(int page, - @NonNull LocalTranslation[] translations, - @NonNull List verses) { - translationView.setVerses(quranDisplayData, translations, verses); - } - - @Override - public void updateScrollPosition() { - translationView.setScrollPosition(scrollPosition); - } - - public void refresh() { - presenter.legacyRefresh(); - } - - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - scrollPosition = translationView.findFirstCompletelyVisibleItemPosition(); - outState.putInt(SI_SCROLL_POSITION, scrollPosition); - super.onSaveInstanceState(outState); - } - - @Override - public boolean handleTouchEvent(@NonNull MotionEvent event, - @NonNull AyahSelectedListener.EventType eventType, - int page) { - return false; - } - - @Override - public void handleRetryClicked() { - } - - @Override - public void onScrollChanged(float y) { - if (isVisible()) { - final AyahSelection ayahSelection = readingEventPresenter.currentAyahSelection(); - if (ayahSelection instanceof AyahSelection.Ayah) { - final AyahSelection.Ayah currentAyahSelection = ((AyahSelection.Ayah) ayahSelection); - final SuraAyah suraAyah = currentAyahSelection.getSuraAyah(); - - readingEventPresenter.onAyahSelection( - new AyahSelection.Ayah(suraAyah, - translationView.getToolbarPosition(suraAyah.sura, suraAyah.ayah)) - ); - } - } - } - - @Override - public void handleLongPress(@NonNull SuraAyah suraAyah) { - if (isVisible()) { - readingEventPresenter.onAyahSelection( - new AyahSelection.Ayah(suraAyah, translationView.getToolbarPosition(suraAyah.sura, suraAyah.ayah)) - ); - } - } - - @Override - public void endAyahMode() { - if (isVisible()) { - ayahTrackerPresenter.endAyahMode(); - } - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt new file mode 100644 index 0000000000..98ec55f0f6 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt @@ -0,0 +1,196 @@ +package com.quran.labs.androidquran.ui.fragment + +import android.app.Activity +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.quran.data.core.QuranInfo +import com.quran.data.model.SuraAyah +import com.quran.data.model.selection.AyahSelection +import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.data.QuranDisplayData +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerItem +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerPresenter +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTrackerPresenter.AyahInteractionHandler +import com.quran.labs.androidquran.presenter.quran.ayahtracker.AyahTranslationTrackerItem +import com.quran.labs.androidquran.presenter.translation.TranslationPresenter +import com.quran.labs.androidquran.ui.PagerActivity +import com.quran.labs.androidquran.ui.helpers.AyahSelectedListener +import com.quran.labs.androidquran.ui.helpers.AyahTracker +import com.quran.labs.androidquran.ui.helpers.QuranPage +import com.quran.labs.androidquran.ui.translation.TranslationView +import com.quran.labs.androidquran.ui.util.PageController +import com.quran.labs.androidquran.util.QuranSettings +import com.quran.labs.androidquran.view.QuranTranslationPageLayout +import com.quran.mobile.translation.model.LocalTranslation +import com.quran.reading.common.ReadingEventPresenter +import javax.inject.Inject + +class TranslationFragment : Fragment(), AyahInteractionHandler, QuranPage, + TranslationPresenter.TranslationScreen, PageController { + private var pageNumber = 0 + private var scrollPosition = 0 + private var ayahTrackerItems: Array? = null + + private lateinit var mainView: QuranTranslationPageLayout + private lateinit var translationView: TranslationView + + @Inject lateinit var quranInfo: QuranInfo + @Inject lateinit var quranDisplayData: QuranDisplayData + @Inject lateinit var quranSettings: QuranSettings + @Inject lateinit var presenter: TranslationPresenter + @Inject lateinit var ayahTrackerPresenter: AyahTrackerPresenter + @Inject lateinit var ayahSelectedListener: AyahSelectedListener + @Inject lateinit var readingEventPresenter: ReadingEventPresenter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (savedInstanceState != null) { + scrollPosition = savedInstanceState.getInt(SI_SCROLL_POSITION) + } + setHasOptionsMenu(true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, savedInstanceState: Bundle? + ): View? { + val context: Context? = activity + mainView = QuranTranslationPageLayout(context) + mainView.setPageController(this, pageNumber, quranInfo.skip) + translationView = mainView.translationView + translationView.setTranslationClickedListener { + val activity: Activity? = activity + (activity as? PagerActivity?)?.toggleActionBar() + } + return mainView + } + + override fun onAttach(context: Context) { + super.onAttach(context) + + val arguments = arguments + pageNumber = arguments?.getInt(PAGE_NUMBER_EXTRA) ?: -1 + val pages = intArrayOf(pageNumber) + (activity as? PagerActivity)?.getPagerActivityComponent() + ?.quranPageComponentFactory() + ?.generate(pages) + ?.inject(this) + } + + override fun updateView() { + if (isAdded) { + mainView.updateView(quranSettings) + refresh() + } + } + + override fun getAyahTracker(): AyahTracker { + return ayahTrackerPresenter + } + + override fun getAyahTrackerItems(): Array { + val items = ayahTrackerItems + return if (items == null) { + val elements: Array = arrayOf( + AyahTranslationTrackerItem(pageNumber, quranInfo, translationView) + ) + ayahTrackerItems = elements + elements + } else { + items + } + } + + override fun onResume() { + super.onResume() + ayahTrackerPresenter.bind(this) + presenter.bind(this) + updateView() + } + + override fun onPause() { + ayahTrackerPresenter.unbind(this) + presenter.unbind(this) + super.onPause() + } + + override fun setVerses( + page: Int, + translations: Array, + verses: List + ) { + translationView.setVerses(quranDisplayData, translations, verses) + } + + override fun updateScrollPosition() { + translationView.setScrollPosition(scrollPosition) + } + + fun refresh() { + presenter.legacyRefresh() + } + + override fun onSaveInstanceState(outState: Bundle) { + scrollPosition = translationView.findFirstCompletelyVisibleItemPosition() + outState.putInt(SI_SCROLL_POSITION, scrollPosition) + super.onSaveInstanceState(outState) + } + + override fun handleTouchEvent( + event: MotionEvent, + eventType: AyahSelectedListener.EventType, + page: Int + ): Boolean { + return false + } + + override fun handleRetryClicked() {} + override fun onScrollChanged(y: Float) { + if (isVisible) { + val ayahSelection = readingEventPresenter.currentAyahSelection() + if (ayahSelection is AyahSelection.Ayah) { + val (suraAyah) = ayahSelection + readingEventPresenter.onAyahSelection( + AyahSelection.Ayah( + suraAyah, + translationView.getToolbarPosition(suraAyah.sura, suraAyah.ayah) + ) + ) + } + } + } + + override fun handleLongPress(suraAyah: SuraAyah) { + if (isVisible) { + readingEventPresenter.onAyahSelection( + AyahSelection.Ayah( + suraAyah, + translationView.getToolbarPosition(suraAyah.sura, suraAyah.ayah) + ) + ) + } + } + + override fun endAyahMode() { + if (isVisible) { + ayahTrackerPresenter.endAyahMode() + } + } + + companion object { + private const val PAGE_NUMBER_EXTRA = "pageNumber" + private const val SI_SCROLL_POSITION = "SI_SCROLL_POSITION" + fun newInstance(page: Int): TranslationFragment { + val f = TranslationFragment() + val args = Bundle() + args.putInt(PAGE_NUMBER_EXTRA, page) + f.setArguments(args) + return f + } + } +} From 8cce40a71d5fc3e4a33ed2d3d0e4b588d62a48dc Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 10 Dec 2023 23:59:33 +0400 Subject: [PATCH 201/258] Remove legacy refresh method --- .../translation/TranslationPresenter.kt | 24 ++----------------- .../ui/fragment/TabletFragment.kt | 16 ++++++++++--- .../ui/fragment/TranslationFragment.kt | 14 ++++++++++- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt index 80e2f2c115..a92d009fc6 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt @@ -8,24 +8,12 @@ import com.quran.labs.androidquran.model.translation.TranslationModel import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.util.TranslationUtil import com.quran.mobile.translation.model.LocalTranslation -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.observers.DisposableObserver import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject -@QuranPageScope class TranslationPresenter @Inject internal constructor( +@QuranPageScope +class TranslationPresenter @Inject internal constructor( translationModel: TranslationModel, private val quranSettings: QuranSettings, translationsAdapter: TranslationsDBAdapter, @@ -37,14 +25,6 @@ import javax.inject.Inject translationModel, translationsAdapter, translationUtil, quranInfo ) { - private val scope = MainScope() - - fun legacyRefresh() { - scope.launch { - refresh() - } - } - suspend fun refresh() { pages .map { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt index f9e8bc0c80..e4f4893d3e 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt @@ -47,6 +47,9 @@ import com.quran.page.common.factory.PageViewFactoryProvider import com.quran.reading.common.ReadingEventPresenter import dagger.Lazy import io.reactivex.rxjava3.disposables.CompositeDisposable +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -89,6 +92,8 @@ class TabletFragment : Fragment(), PageController, TranslationPresenter.Translat private var pageViewFactory: PageViewFactory? = null private var isCustomArabicPageType = false + private val scope = MainScope() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState != null) { @@ -363,8 +368,9 @@ class TabletFragment : Fragment(), PageController, TranslationPresenter.Translat } override fun onDetach() { - super.onDetach() compositeDisposable.clear() + scope.cancel() + super.onDetach() } override fun setPageDownloadError(@StringRes errorMessage: Int) { @@ -390,7 +396,9 @@ class TabletFragment : Fragment(), PageController, TranslationPresenter.Translat override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (mode == Mode.TRANSLATION) { - translationPresenter.get().legacyRefresh() + scope.launch { + translationPresenter.get().refresh() + } } } @@ -420,7 +428,9 @@ class TabletFragment : Fragment(), PageController, TranslationPresenter.Translat fun refresh() { if (mode == Mode.TRANSLATION) { - translationPresenter.get().legacyRefresh() + scope.launch { + translationPresenter.get().refresh() + } } } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt index 98ec55f0f6..50036d20ad 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt @@ -28,6 +28,9 @@ import com.quran.labs.androidquran.util.QuranSettings import com.quran.labs.androidquran.view.QuranTranslationPageLayout import com.quran.mobile.translation.model.LocalTranslation import com.quran.reading.common.ReadingEventPresenter +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import javax.inject.Inject class TranslationFragment : Fragment(), AyahInteractionHandler, QuranPage, @@ -47,6 +50,8 @@ class TranslationFragment : Fragment(), AyahInteractionHandler, QuranPage, @Inject lateinit var ayahSelectedListener: AyahSelectedListener @Inject lateinit var readingEventPresenter: ReadingEventPresenter + private val scope = MainScope() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState != null) { @@ -82,6 +87,11 @@ class TranslationFragment : Fragment(), AyahInteractionHandler, QuranPage, ?.inject(this) } + override fun onDetach() { + scope.cancel() + super.onDetach() + } + override fun updateView() { if (isAdded) { mainView.updateView(quranSettings) @@ -132,7 +142,9 @@ class TranslationFragment : Fragment(), AyahInteractionHandler, QuranPage, } fun refresh() { - presenter.legacyRefresh() + scope.launch { + presenter.refresh() + } } override fun onSaveInstanceState(outState: Bundle) { From 39f57f9303e5066d5736aeaf7847912686b9940a Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 11 Dec 2023 00:05:53 +0400 Subject: [PATCH 202/258] Kotlinify QuranDataProvider --- .../androidquran/data/QuranDataProvider.java | 281 ------------------ .../androidquran/data/QuranDataProvider.kt | 258 ++++++++++++++++ 2 files changed, 258 insertions(+), 281 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java create mode 100644 app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java deleted file mode 100644 index bd246f8b2a..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java +++ /dev/null @@ -1,281 +0,0 @@ -package com.quran.labs.androidquran.data; - -import android.app.SearchManager; -import android.content.ContentProvider; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.provider.BaseColumns; - -import androidx.annotation.NonNull; - -import com.quran.data.core.QuranInfo; -import com.quran.labs.androidquran.BuildConfig; -import com.quran.labs.androidquran.QuranApplication; -import com.quran.labs.androidquran.R; -import com.quran.labs.androidquran.database.DatabaseHandler; -import com.quran.labs.androidquran.database.DatabaseUtils; -import com.quran.labs.androidquran.database.TranslationsDBAdapter; -import com.quran.labs.androidquran.util.QuranFileUtils; -import com.quran.labs.androidquran.util.QuranUtils; -import com.quran.mobile.translation.model.LocalTranslation; - -import java.util.List; - -import javax.inject.Inject; - -import timber.log.Timber; - -public class QuranDataProvider extends ContentProvider { - - public static String AUTHORITY = BuildConfig.APPLICATION_ID + ".data.QuranDataProvider"; - public static final Uri SEARCH_URI = Uri.parse("content://" + AUTHORITY + "/quran/search"); - - public static final String VERSES_MIME_TYPE = - ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.com.quran.labs.androidquran"; - public static final String QURAN_ARABIC_DATABASE = QuranFileConstants.ARABIC_DATABASE; - - // UriMatcher stuff - private static final int SEARCH_VERSES = 0; - private static final int SEARCH_SUGGEST = 1; - private static final UriMatcher uriMatcher = buildUriMatcher(); - - private boolean didInject; - @Inject QuranDisplayData quranDisplayData; - @Inject TranslationsDBAdapter translationsDBAdapter; - @Inject QuranFileUtils quranFileUtils; - @Inject QuranInfo quranInfo; - - private static UriMatcher buildUriMatcher() { - UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); - matcher.addURI(AUTHORITY, "quran/search", SEARCH_VERSES); - matcher.addURI(AUTHORITY, "quran/search/*", SEARCH_VERSES); - matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST); - matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST); - return matcher; - } - - @Override - public boolean onCreate() { - return true; - } - - @Override - public Cursor query(@NonNull Uri uri, String[] projection, String selection, - String[] selectionArgs, String sortOrder) { - Context context = getContext(); - if (!didInject) { - Context appContext = context == null ? null : context.getApplicationContext(); - if (appContext instanceof QuranApplication) { - ((QuranApplication) appContext).getApplicationComponent().inject(this); - didInject = true; - } else { - Timber.e("unable to inject QuranDataProvider"); - return null; - } - } - - Timber.d("uri: %s", uri.toString()); - switch (uriMatcher.match(uri)) { - case SEARCH_SUGGEST: { - if (selectionArgs == null) { - throw new IllegalArgumentException( - "selectionArgs must be provided for the Uri: " + uri); - } - - return getSuggestions(selectionArgs[0]); - } - case SEARCH_VERSES: { - if (selectionArgs == null) { - throw new IllegalArgumentException( - "selectionArgs must be provided for the Uri: " + uri); - } - - return search(selectionArgs[0]); - } - default: { - throw new IllegalArgumentException("Unknown Uri: " + uri); - } - } - } - - private Cursor search(String query) { - return search(query, getAvailableTranslations()); - } - - private List getAvailableTranslations() { - return translationsDBAdapter.legacyGetTranslations(); - } - - private Cursor getSuggestions(String query) { - if (query.length() < 3) { - return null; - } - - final boolean queryIsArabic = QuranUtils.doesStringContainArabic(query); - final boolean haveArabic = queryIsArabic && - quranFileUtils.hasTranslation(getContext(), QURAN_ARABIC_DATABASE); - - List translations = getAvailableTranslations(); - if (translations.size() == 0 && (queryIsArabic && !haveArabic)) { - return null; - } - - int total = translations.size(); - int start = haveArabic ? -1 : 0; - - String[] cols = new String[] { BaseColumns._ID, - SearchManager.SUGGEST_COLUMN_TEXT_1, - SearchManager.SUGGEST_COLUMN_TEXT_2, - SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID }; - MatrixCursor mc = new MatrixCursor(cols); - - Context context = getContext(); - boolean gotResults = false; - boolean likelyHaveMoreResults = false; - for (int i = start; i < total; i++) { - if (gotResults) { - continue; - } - - String database; - if (i < 0) { - database = QURAN_ARABIC_DATABASE; - if (!quranFileUtils.hasArabicSearchDatabase()) { - continue; - } - } else { - LocalTranslation translation = translations.get(i); - // skip non-arabic databases if the query is in arabic - if (queryIsArabic || "ar".equals(translation.getLanguageCode())) { - // skip arabic databases even when the query is in arabic since - // searching Arabic tafaseer causes a lot of noise on the search - // results and is confusing. - continue; - } - database = translation.getFilename(); - } - - Cursor suggestions = null; - try { - suggestions = search(query, database, false); - if (context != null && suggestions != null && suggestions.moveToFirst()) { - if (suggestions.getCount() > 5) { - likelyHaveMoreResults = true; - } - - int results = 0; - do { - if (results == 5) { - break; - } - int sura = suggestions.getInt(1); - int ayah = suggestions.getInt(2); - int page = quranInfo.getPageFromSuraAyah(sura, ayah); - String text = suggestions.getString(3); - String foundText = context.getString( - R.string.found_in_sura, quranDisplayData.getSuraName(context, sura, false), ayah, page); - - gotResults = true; - MatrixCursor.RowBuilder row = mc.newRow(); - int id = suggestions.getInt(0); - - row.add(id); - row.add(text); - row.add(foundText); - row.add(id); - results++; - } while (suggestions.moveToNext()); - } - } finally { - DatabaseUtils.closeCursor(suggestions); - } - } - - if (context != null && (queryIsArabic || likelyHaveMoreResults)) { - mc.addRow(new Object[] { - -1, context.getString(R.string.search_full_results), - context.getString(R.string.search_entire_mushaf), -1 - }); - } - return mc; - } - - private Cursor search(String query, List translations) { - Timber.d("query: %s", query); - - final Context context = getContext(); - final boolean queryIsArabic = QuranUtils.doesStringContainArabic(query); - final boolean haveArabic = queryIsArabic && - quranFileUtils.hasTranslation(context, QURAN_ARABIC_DATABASE); - if (translations.size() == 0 && (queryIsArabic && !haveArabic)) { - return null; - } - - int start = haveArabic ? -1 : 0; - int total = translations.size(); - - for (int i = start; i < total; i++) { - String databaseName; - if (i < 0) { - databaseName = QURAN_ARABIC_DATABASE; - } else { - LocalTranslation translation = translations.get(i); - // skip non-arabic databases if the query is in arabic - if (queryIsArabic || "ar".equals(translation.getLanguageCode())) { - // skip arabic databases always since it's confusing to people for now. - // in the future, can think of better ways to enable tafseer search. - continue; - } - databaseName = translation.getFilename(); - } - - Cursor cursor = search(query, databaseName, true); - if (cursor != null && cursor.getCount() > 0) { - return cursor; - } - } - return null; - } - - private Cursor search(String query, String databaseName, boolean wantSnippets) { - final DatabaseHandler handler = - DatabaseHandler.getDatabaseHandler(getContext(), databaseName, quranFileUtils); - return handler.search(query, wantSnippets, QURAN_ARABIC_DATABASE.equals(databaseName)); - } - - @Override - public String getType(@NonNull Uri uri) { - switch (uriMatcher.match(uri)) { - case SEARCH_VERSES: { - return VERSES_MIME_TYPE; - } - case SEARCH_SUGGEST: { - return SearchManager.SUGGEST_MIME_TYPE; - } - default: { - throw new IllegalArgumentException("Unknown URL " + uri); - } - } - } - - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - throw new UnsupportedOperationException(); - } - - @Override - public int update(@NonNull Uri uri, ContentValues values, String selection, - String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - @Override - public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt new file mode 100644 index 0000000000..d40a2b34ef --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt @@ -0,0 +1,258 @@ +package com.quran.labs.androidquran.data + +import android.app.SearchManager +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.ContentValues +import android.content.UriMatcher +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.provider.BaseColumns +import com.quran.data.core.QuranInfo +import com.quran.labs.androidquran.BuildConfig +import com.quran.labs.androidquran.QuranApplication +import com.quran.labs.androidquran.R +import com.quran.labs.androidquran.database.DatabaseHandler.Companion.getDatabaseHandler +import com.quran.labs.androidquran.database.DatabaseUtils.closeCursor +import com.quran.labs.androidquran.database.TranslationsDBAdapter +import com.quran.labs.androidquran.util.QuranFileUtils +import com.quran.labs.androidquran.util.QuranUtils +import com.quran.mobile.translation.model.LocalTranslation +import timber.log.Timber +import javax.inject.Inject + +class QuranDataProvider : ContentProvider() { + private var didInject = false + + @Inject lateinit var quranDisplayData: QuranDisplayData + @Inject lateinit var translationsDBAdapter: TranslationsDBAdapter + @Inject lateinit var quranFileUtils: QuranFileUtils + @Inject lateinit var quranInfo: QuranInfo + + override fun onCreate(): Boolean { + return true + } + + override fun query( + uri: Uri, projection: Array?, selection: String?, + selectionArgs: Array?, sortOrder: String? + ): Cursor? { + val context = context + if (!didInject) { + val appContext = context?.applicationContext + didInject = if (appContext is QuranApplication) { + appContext.applicationComponent.inject(this) + true + } else { + Timber.e("unable to inject QuranDataProvider") + return null + } + } + Timber.d("uri: %s", uri.toString()) + return when (uriMatcher.match(uri)) { + SEARCH_SUGGEST -> { + requireNotNull(selectionArgs) { "selectionArgs must be provided for the Uri: $uri" } + getSuggestions(selectionArgs[0]) + } + + SEARCH_VERSES -> { + requireNotNull(selectionArgs) { "selectionArgs must be provided for the Uri: $uri" } + search(selectionArgs[0]) + } + + else -> { + throw IllegalArgumentException("Unknown Uri: $uri") + } + } + } + + private fun availableTranslations(): List { + return translationsDBAdapter.legacyGetTranslations() + } + + private fun getSuggestions(query: String): Cursor? { + if (query.length < 3) { + return null + } + val queryIsArabic = QuranUtils.doesStringContainArabic(query) + val haveArabic = queryIsArabic && + quranFileUtils.hasTranslation(context!!, QURAN_ARABIC_DATABASE) + val translations = availableTranslations() + if (translations.isEmpty() && queryIsArabic && !haveArabic) { + return null + } + val total = translations.size + val start = if (haveArabic) -1 else 0 + val cols = arrayOf( + BaseColumns._ID, + SearchManager.SUGGEST_COLUMN_TEXT_1, + SearchManager.SUGGEST_COLUMN_TEXT_2, + SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID + ) + val mc = MatrixCursor(cols) + val context = context + var gotResults = false + var likelyHaveMoreResults = false + for (i in start until total) { + if (gotResults) { + continue + } + var database: String + if (i < 0) { + database = QURAN_ARABIC_DATABASE + if (!quranFileUtils.hasArabicSearchDatabase()) { + continue + } + } else { + val (_, filename, _, _, _, _, languageCode) = translations[i] + // skip non-arabic databases if the query is in arabic + if (queryIsArabic || "ar" == languageCode) { + // skip arabic databases even when the query is in arabic since + // searching Arabic tafaseer causes a lot of noise on the search + // results and is confusing. + continue + } + database = filename + } + var suggestions: Cursor? = null + try { + suggestions = search(query, database, false) + if (context != null && suggestions != null && suggestions.moveToFirst()) { + if (suggestions.count > 5) { + likelyHaveMoreResults = true + } + var results = 0 + do { + if (results == 5) { + break + } + val sura = suggestions.getInt(1) + val ayah = suggestions.getInt(2) + val page = quranInfo.getPageFromSuraAyah(sura, ayah) + val text = suggestions.getString(3) + val foundText = context.getString( + R.string.found_in_sura, + quranDisplayData.getSuraName(context, sura, false), + ayah, + page + ) + gotResults = true + val row = mc.newRow() + val id = suggestions.getInt(0) + row.add(id) + row.add(text) + row.add(foundText) + row.add(id) + results++ + } while (suggestions.moveToNext()) + } + } finally { + closeCursor(suggestions) + } + } + if (context != null && (queryIsArabic || likelyHaveMoreResults)) { + mc.addRow( + arrayOf( + -1, context.getString(R.string.search_full_results), + context.getString(R.string.search_entire_mushaf), -1 + ) + ) + } + return mc + } + + private fun search( + query: String, + translations: List = availableTranslations() + ): Cursor? { + Timber.d("query: %s", query) + val context = context + val queryIsArabic = QuranUtils.doesStringContainArabic(query) + val haveArabic = queryIsArabic && + quranFileUtils.hasTranslation(context!!, QURAN_ARABIC_DATABASE) + if (translations.isEmpty() && queryIsArabic && !haveArabic) { + return null + } + val start = if (haveArabic) -1 else 0 + val total = translations.size + for (i in start until total) { + val databaseName: String = if (i < 0) { + QURAN_ARABIC_DATABASE + } else { + val (_, filename, _, _, _, _, languageCode) = translations[i] + // skip non-arabic databases if the query is in arabic + if (queryIsArabic || "ar" == languageCode) { + // skip arabic databases always since it's confusing to people for now. + // in the future, can think of better ways to enable tafseer search. + continue + } + filename + } + val cursor = search(query, databaseName, true) + if (cursor != null && cursor.count > 0) { + return cursor + } + } + return null + } + + private fun search(query: String, databaseName: String, wantSnippets: Boolean): Cursor? { + val handler = getDatabaseHandler(context!!, databaseName, quranFileUtils) + return handler.search(query, wantSnippets, QURAN_ARABIC_DATABASE == databaseName) + } + + override fun getType(uri: Uri): String? { + return when (uriMatcher.match(uri)) { + SEARCH_VERSES -> { + VERSES_MIME_TYPE + } + + SEARCH_SUGGEST -> { + SearchManager.SUGGEST_MIME_TYPE + } + + else -> { + throw IllegalArgumentException("Unknown URL $uri") + } + } + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + throw UnsupportedOperationException() + } + + override fun update( + uri: Uri, values: ContentValues?, selection: String?, + selectionArgs: Array? + ): Int { + throw UnsupportedOperationException() + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + throw UnsupportedOperationException() + } + + companion object { + private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".data.QuranDataProvider" + + @JvmField val SEARCH_URI: Uri = Uri.parse("content://$AUTHORITY/quran/search") + const val VERSES_MIME_TYPE = + ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.com.quran.labs.androidquran" + const val QURAN_ARABIC_DATABASE = QuranFileConstants.ARABIC_DATABASE + + // UriMatcher stuff + private const val SEARCH_VERSES = 0 + private const val SEARCH_SUGGEST = 1 + private val uriMatcher = buildUriMatcher() + + private fun buildUriMatcher(): UriMatcher { + val matcher = UriMatcher(UriMatcher.NO_MATCH) + matcher.addURI(AUTHORITY, "quran/search", SEARCH_VERSES) + matcher.addURI(AUTHORITY, "quran/search/*", SEARCH_VERSES) + matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, SEARCH_SUGGEST) + matcher.addURI(AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", SEARCH_SUGGEST) + return matcher + } + } +} From 121cd075b383e3ea7c6555c7524e3bcf866205fc Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Mon, 11 Dec 2023 00:14:43 +0400 Subject: [PATCH 203/258] Remove legacyGetTranslations call --- .../com/quran/labs/androidquran/data/QuranDataProvider.kt | 5 ++++- .../labs/androidquran/database/TranslationsDBAdapter.kt | 7 ------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt index d40a2b34ef..e8785b7c20 100644 --- a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt +++ b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt @@ -19,6 +19,9 @@ import com.quran.labs.androidquran.database.TranslationsDBAdapter import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.labs.androidquran.util.QuranUtils import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import timber.log.Timber import javax.inject.Inject @@ -68,7 +71,7 @@ class QuranDataProvider : ContentProvider() { } private fun availableTranslations(): List { - return translationsDBAdapter.legacyGetTranslations() + return runBlocking { translationsDBAdapter.getTranslations().first() } } private fun getSuggestions(query: String): Cursor? { diff --git a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt index 342313ea0a..b21b1ca33a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt @@ -2,7 +2,6 @@ package com.quran.labs.androidquran.database import android.content.Context import android.util.SparseArray -import androidx.annotation.WorkerThread import com.quran.labs.androidquran.dao.translation.TranslationItem import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.mobile.di.qualifier.ApplicationContext @@ -13,7 +12,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @@ -33,11 +31,6 @@ class TranslationsDBAdapter @Inject constructor( } } - @WorkerThread - fun legacyGetTranslations(): List { - return runBlocking { getTranslations().first() } - } - suspend fun translationsHash(): SparseArray { return withContext(Dispatchers.IO) { val result = SparseArray() From cbd1d24c8ddaf7ce1343055a4d6fffbee3b1dfb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 01:39:47 +0000 Subject: [PATCH 204/258] Update dependency com.google.cloud:google-cloud-speech to v4.27.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d99da883f..68174f4d13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.60.0" googleAuthVersion = "1.20.0" -googleCloudSpeechVersion = "4.26.0" +googleCloudSpeechVersion = "4.27.0" # testing junitVersion = "4.13.2" From 0e5b655603980fbbaf10209242bbf0a055272c2d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 10:42:58 +0000 Subject: [PATCH 205/258] Update dawidd6/action-download-artifact action to v3 --- .github/workflows/build.yml | 4 ++-- .github/workflows/post_build.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66ce3a8e23..c5515b3f4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: run: ./gradlew assembleMadaniDebug - name: Download Previous Debug APK - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: post_merge.yml @@ -128,7 +128,7 @@ jobs: run: ./gradlew :app:dependencies --configuration madaniReleaseRuntimeClasspath > current_dependencies.txt - name: Download Previous Dependencies List - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} workflow: post_merge.yml diff --git a/.github/workflows/post_build.yml b/.github/workflows/post_build.yml index eddbb735ea..e0a2df6c34 100644 --- a/.github/workflows/post_build.yml +++ b/.github/workflows/post_build.yml @@ -13,7 +13,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - name: Download PR Number - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} run_id: ${{ github.event.workflow_run.id }} @@ -21,7 +21,7 @@ jobs: name: pr - name: Download Apk Diff Results - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} run_id: ${{ github.event.workflow_run.id }} @@ -36,7 +36,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download Dependency Diff Results - uses: dawidd6/action-download-artifact@v2 + uses: dawidd6/action-download-artifact@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} run_id: ${{ github.event.workflow_run.id }} From bf1d7d09545680e04cf242eb1ba03535e57a427b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:10:27 +0000 Subject: [PATCH 206/258] Update dependency androidx.annotation:annotation to v1.7.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d99da883f..fdb4f3e5d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ timberVersion = "5.0.1" # androidx library versions androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.8.1" -androidxAnnotationVersion = "1.7.0" +androidxAnnotationVersion = "1.7.1" androidxCoreVersion = "1.12.0" androidxNavigationVersion = "2.7.5" androidxDynamicAnimationVersion = "1.0.0" From 97bdc8a8dcf983b1dde3d11a4183884a380e872b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 13 Dec 2023 18:10:31 +0000 Subject: [PATCH 207/258] Update dependency androidx.navigation:navigation-compose to v2.7.6 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d99da883f..03a1aa9e7a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ androidxAppcompatVersion = "1.6.1" androidxActivityComposeVersion = "1.8.1" androidxAnnotationVersion = "1.7.0" androidxCoreVersion = "1.12.0" -androidxNavigationVersion = "2.7.5" +androidxNavigationVersion = "2.7.6" androidxDynamicAnimationVersion = "1.0.0" androidxFragmentVersion = "1.6.2" androidxJunitExtVersion = "1.1.5" From 3c149265373da1b2d744bc647fe96bfb23bcacf5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 04:05:27 +0000 Subject: [PATCH 208/258] Update dependency com.google.android.material:material to v1.11.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2d99da883f..52981b0d65 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -53,7 +53,7 @@ accompanistVersion = "0.32.0" balloonVersion = "1.6.3" tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" -materialComponentsVersion = "1.10.0" +materialComponentsVersion = "1.11.0" numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" From 754b455cdbb3f9a4eee6bed5023bbcb07094a8f5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 16:17:41 +0000 Subject: [PATCH 209/258] Update dependency com.google.devtools.ksp to v1.9.21-1.0.16 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 68174f4d13..67e492dfd6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.2.0" kotlin = "1.9.21" -ksp = "1.9.21-1.0.15" +ksp = "1.9.21-1.0.16" # required within the Gradle convention plugins - not unused compose-compiler = "1.5.6" From e263ab3647cd37910c90cb01798eccf089926668 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:35:33 +0000 Subject: [PATCH 210/258] Update dependency androidx.activity:activity-compose to v1.8.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e1823f14f..dca2644cab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ timberVersion = "5.0.1" # androidx library versions androidxAppcompatVersion = "1.6.1" -androidxActivityComposeVersion = "1.8.1" +androidxActivityComposeVersion = "1.8.2" androidxAnnotationVersion = "1.7.1" androidxCoreVersion = "1.12.0" androidxNavigationVersion = "2.7.5" From 3a3248c2901ef5210fe1b32fc465302063ab29e9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:35:38 +0000 Subject: [PATCH 211/258] Update actions/upload-artifact action to v4 --- .github/workflows/build.yml | 10 +++++----- .github/workflows/post_merge.yml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c5515b3f4b..cb07bb99dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Apk Diff Results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: success() with: name: apk_differences @@ -71,7 +71,7 @@ jobs: run: ./gradlew lintMadaniDebug - name: Upload lint results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: lint_report @@ -101,7 +101,7 @@ jobs: run: ./gradlew test -PdisableCrashlytics -PdisableFirebase - name: Upload test report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: unit_test_report @@ -143,7 +143,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload Dependency Diff Results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: success() with: name: dependency_differences @@ -154,7 +154,7 @@ jobs: echo ${{ github.event.number }} > pr.txt - name: Upload PR Number - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pr path: pr.txt diff --git a/.github/workflows/post_merge.yml b/.github/workflows/post_merge.yml index 288e89620a..d446cf29f0 100644 --- a/.github/workflows/post_merge.yml +++ b/.github/workflows/post_merge.yml @@ -30,7 +30,7 @@ jobs: run: ./gradlew assembleDebug - name: Upload Debug Apk - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: success() with: name: latest-apk @@ -40,7 +40,7 @@ jobs: run: ./gradlew :app:dependencies --configuration madaniReleaseRuntimeClasspath > dependencies.txt - name: Upload dependencies list - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: success() with: name: dependencies From 8798aa42be2df2cd0a4958df9085a35696e0b3d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 17 Dec 2023 01:22:00 +0000 Subject: [PATCH 212/258] Update okioVersion to v3.7.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 74e4878023..9ce7389016 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,7 +19,7 @@ errorproneCoreVersion = "2.23.0" googleServices = "4.4.0" leakcanaryAndroidVersion = "2.12" moshiVersion = "1.15.0" -okioVersion = "3.6.0" +okioVersion = "3.7.0" retrofitVersion = "2.9.0" rxandroidVersion = "3.0.2" rxjavaVersion = "3.1.8" From 47db7d8a8743b023099de2199b23a1cfeb53f109 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:44:52 +0000 Subject: [PATCH 213/258] Update daggerVersion to v2.50 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ce7389016..9acbd47d70 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ okhttpBomVersion = "4.12.0" anvil = "2.4.8" coroutinesVersion = "1.7.3" crashlytics = "2.9.9" -daggerVersion = "2.49" +daggerVersion = "2.50" dnsjavaVersion = "2.1.9" errorprone = "3.1.0" errorproneCoreVersion = "2.23.0" From 0f6c01c5698743598f09fc71c71d0283c7c297e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Dec 2023 21:44:57 +0000 Subject: [PATCH 214/258] Update dependency com.google.truth:truth to v1.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ce7389016..b9113d2e13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,7 +65,7 @@ googleCloudSpeechVersion = "4.27.0" # testing junitVersion = "4.13.2" espressoVersion = "3.5.1" -truthVersion = "1.1.5" +truthVersion = "1.2.0" mockitoVersion = "5.8.0" robolectricVersion = "4.11.1" turbineVersion = "1.0.0" From 2da6675fe9f1c13098ef2eefa3b8131510261371 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:26:59 +0000 Subject: [PATCH 215/258] Update dependency com.google.errorprone:error_prone_core to v2.24.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b9113d2e13..31600b1fc5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ crashlytics = "2.9.9" daggerVersion = "2.49" dnsjavaVersion = "2.1.9" errorprone = "3.1.0" -errorproneCoreVersion = "2.23.0" +errorproneCoreVersion = "2.24.0" googleServices = "4.4.0" leakcanaryAndroidVersion = "2.12" moshiVersion = "1.15.0" From 911b9212b50dc194fa76bbcb264cbd7c2ca1b28c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 21:25:21 +0000 Subject: [PATCH 216/258] Update dependency io.grpc:grpc-okhttp to v1.60.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9e6d92b85..9bdbcb8034 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" # recitations -grpcOkhttpVersion = "1.60.0" +grpcOkhttpVersion = "1.60.1" googleAuthVersion = "1.20.0" googleCloudSpeechVersion = "4.27.0" From 22a270cdb5d2526c59164c81d19b787ac06d4cd8 Mon Sep 17 00:00:00 2001 From: MahmoudMabrok Date: Sun, 24 Dec 2023 14:37:11 +0200 Subject: [PATCH 217/258] feat: handle speedup and down --- .../labs/androidquran/service/AudioService.kt | 45 ++++++++++++++++++- .../labs/androidquran/ui/PagerActivity.java | 11 ++--- .../androidquran/view/AudioStatusBar.java | 8 ++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index df7e308080..1efc28da53 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -398,7 +398,12 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, processStopRequest() } else if (ACTION_REWIND == action) { processRewindRequest() - } else if (ACTION_UPDATE_REPEAT == action) { + }else if (ACTION_SPEED_DOWN == action){ + processSpeedDownPlayback() + }else if (ACTION_SPEED_UP == action){ + processSpeedUpPlayback() + } + else if (ACTION_UPDATE_REPEAT == action) { val playInfo = intent.getParcelableExtra(EXTRA_PLAY_INFO) val localAudioQueue = audioQueue if (playInfo != null && localAudioQueue != null) { @@ -677,7 +682,43 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } } } + private fun processSpeedUpPlayback() { + if (State.Playing === state) { + speedUpPlayback() + } + } + + private fun processSpeedDownPlayback() { + if (State.Playing === state) { + speedDownPlayback() + } + } + private fun speedUpPlayback() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + player?.playbackParams?.let { params -> + val newSpeed = params.speed + 0.15f + // todo should be handled based on Qari type + if (newSpeed <= 1.5){ + params.setSpeed(newSpeed) + player?.playbackParams = params + } + } + } + } + + private fun speedDownPlayback() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + player?.playbackParams?.let { params -> + val newSpeed = params.speed - 0.1f + // todo should be handled based on Qari type + if (newSpeed >= 0.5){ + params.setSpeed(newSpeed) + player?.playbackParams = params + } + } + } + } private fun processSkipRequest() { if (audioRequest == null) { return @@ -1361,6 +1402,8 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, const val ACTION_STOP = "com.quran.labs.androidquran.action.STOP" const val ACTION_SKIP = "com.quran.labs.androidquran.action.SKIP" const val ACTION_REWIND = "com.quran.labs.androidquran.action.REWIND" + const val ACTION_SPEED_UP = "com.quran.labs.androidquran.action.SPEED_UP" + const val ACTION_SPEED_DOWN = "com.quran.labs.androidquran.action.SPEED_DOWN" const val ACTION_CONNECT = "com.quran.labs.androidquran.action.CONNECT" const val ACTION_UPDATE_REPEAT = "com.quran.labs.androidquran.action.UPDATE_REPEAT" 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 6ea76c3102..977a828399 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 @@ -1585,19 +1585,16 @@ public void onPausePressed() { @Override public void onUpPressed() { - startService(audioUtils.getAudioIntent( - this, AudioService.ACTION_INCREASE_SPEAD)); - // audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE); + startService(audioUtils.getAudioIntent(this, + AudioService.ACTION_SPEED_UP)); } @Override public void onDownPressed() { - startService(audioUtils.getAudioIntent( - this, AudioService.ACTION_DECREASE_SPEAD)); - // audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE); + startService(audioUtils.getAudioIntent(this, + AudioService.ACTION_SPEED_DOWN)); } - @Override public void onNextPressed() { startService(audioUtils.getAudioIntent(this, diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index 3e74737d25..63a79dbbbe 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -74,6 +74,8 @@ public interface AudioBarListener { void onNextPressed(); void onPreviousPressed(); void onStopPressed(); + void onDownPressed(); + void onUpPressed(); void onCancelPressed(boolean stopDownload); void setRepeatCount(int repeatCount); void onAcceptPressed(); @@ -460,6 +462,8 @@ private void showPlayingMode(boolean isPaused) { addButton(R.drawable.ic_previous, withWeight); addButton(button, withWeight); addButton(R.drawable.ic_next, withWeight); + addButton(R.drawable.ic_neg_1_24, withWeight); + addButton(R.drawable.ic_plus_1_24, withWeight); addButton(repeatButton, R.drawable.ic_repeat, withWeight); updateRepeatButtonText(); @@ -571,6 +575,10 @@ public void onClick(View view) { } } else if (tag == R.drawable.ic_next) { audioBarListener.onNextPressed(); + } else if (tag == R.drawable.ic_neg_1_24) { + audioBarListener.onDownPressed(); + } else if (tag == R.drawable.ic_plus_1_24) { + audioBarListener.onUpPressed(); } else if (tag == R.drawable.ic_previous) { audioBarListener.onPreviousPressed(); } else if (tag == R.drawable.ic_repeat) { From 9b4c0b7c6f39bb82c5f8788e654b4f114ba9ca3e Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 24 Dec 2023 18:41:57 +0400 Subject: [PATCH 218/258] Improve translations view Fix the case where there are no translations, and apply minor fixes. --- .../translation/InlineTranslationPresenter.kt | 4 +--- .../ui/fragment/AyahTranslationFragment.kt | 16 +++++++++------- .../ui/util/TranslationsSpinnerAdapter.java | 18 ++++++++---------- settings.gradle.kts | 3 +-- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt index d084f71502..390b38220a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt @@ -48,9 +48,7 @@ class InlineTranslationPresenter @Inject constructor( override fun bind(what: TranslationScreen) { super.bind(what) val translations = cachedTranslations - if (translations.isNotEmpty()) { - what.onTranslationsUpdated(translations) - } + what.onTranslationsUpdated(translations) } interface TranslationScreen { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt index 077b427b5d..5e4ec0da1b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt @@ -117,6 +117,8 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { progressBar.visibility = View.GONE emptyState.visibility = View.VISIBLE translationControls.visibility = View.GONE + translator.visibility = View.GONE + translationView.visibility = View.GONE } else { val activeTranslationsFilesNames = quranSettings.activeTranslations @@ -127,9 +129,10 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { R.layout.translation_ab_spinner_item, translations.map { it.resolveTranslatorName() }.toTypedArray(), translations, - activeTranslationsFilesNames + activeTranslationsFilesNames, ) { selectedItems: Set? -> quranSettings.activeTranslations = selectedItems + // this is the refresh for when a translation is selected from the spinner refreshView() } translator.adapter = translationAdapter @@ -139,8 +142,8 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { translations, activeTranslationsFilesNames ) - refreshView() } + refreshView() } } @@ -150,11 +153,7 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { if (start == null || end == null) { return } - if (start == end) { - translationControls.visibility = View.VISIBLE - } else { - translationControls.visibility = View.GONE - } + val verses = 1 + abs( quranInfo.getAyahId(start.sura, start.ayah) - quranInfo.getAyahId(end.sura, end.ayah) ) @@ -168,6 +167,9 @@ class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { progressBar.visibility = View.GONE if (verses.isNotEmpty()) { emptyState.visibility = View.GONE + translationControls.visibility = View.VISIBLE + translator.visibility = View.VISIBLE + translationView.visibility = View.VISIBLE translationView.setAyahs(translations, verses) } else { emptyState.visibility = View.VISIBLE diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java b/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java index bcd71fc881..a0b11ee5e1 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java +++ b/app/src/main/java/com/quran/labs/androidquran/ui/util/TranslationsSpinnerAdapter.java @@ -17,6 +17,7 @@ import com.quran.mobile.translation.model.LocalTranslation; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -38,7 +39,7 @@ public TranslationsSpinnerAdapter(Context context, OnSelectionChangedListener listener) { // intentionally making a new ArrayList instead of using the constructor for String[]. // this is because clear() relies on being able to clear the List passed into the constructor, - // and the String[] constructor makes a new (immutable) List with the items of the array. + // and the String[] constructor makes a fixed size List with the items of the array. super(context, resource, new ArrayList<>()); this.context = context; this.layoutInflater = LayoutInflater.from(this.context); @@ -50,7 +51,7 @@ public TranslationsSpinnerAdapter(Context context, addAll(translationNames); } - private View.OnClickListener onCheckedChangeListener = buttonView -> { + private final View.OnClickListener onCheckedChangeListener = buttonView -> { CheckBoxHolder holder = (CheckBoxHolder) ((View) buttonView.getParent()).getTag(); LocalTranslation localTranslation = translations.get(holder.position); @@ -66,11 +67,10 @@ public TranslationsSpinnerAdapter(Context context, }; - private View.OnClickListener onTextClickedListener = textView -> { + private final View.OnClickListener onTextClickedListener = textView -> { CheckBoxHolder holder = (CheckBoxHolder) ((View) textView.getParent()).getTag(); if (holder.position == translationNames.length - 1) { - if (this.context instanceof PagerActivity) { - final PagerActivity pagerActivity = (PagerActivity) this.context; + if (this.context instanceof PagerActivity pagerActivity) { pagerActivity.startTranslationManager(); } } else { @@ -167,12 +167,10 @@ public interface OnSelectionChangedListener { } private String[] updateTranslationNames(String[] translationNames) { - List translationsList = new ArrayList<>(); - for (String translation : translationNames) { - translationsList.add(translation); - } + final List translationsList = new ArrayList<>(); + Collections.addAll(translationsList, translationNames); translationsList.add(getContext().getString(R.string.more_translations)); - translationNames = translationsList.toArray(new String[translationsList.size()]); + translationNames = translationsList.toArray(new String[0]); return translationNames; } diff --git a/settings.gradle.kts b/settings.gradle.kts index a260c5eb2a..2dfbae96b7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,5 @@ pluginManagement { + includeBuild("build-logic") repositories { google() gradlePluginPortal() @@ -14,8 +15,6 @@ dependencyResolutionManagement { } } -includeBuild("build-logic") - include(":app") include(":common:analytics") include(":common:audio") From f0f984718ad78849aa92ae116ad6f7ff7516239e Mon Sep 17 00:00:00 2001 From: MahmoudMabrok Date: Sun, 24 Dec 2023 21:58:40 +0200 Subject: [PATCH 219/258] feat: handle speedup as pre-set --- .../labs/androidquran/service/AudioService.kt | 49 ++++--------------- .../labs/androidquran/ui/PagerActivity.java | 11 ++--- .../androidquran/view/AudioStatusBar.java | 47 +++++++++++++++--- .../labs/androidquran/view/RepeatButton.kt | 4 +- app/src/main/res/drawable/ic_neg_1_24.xml | 10 ---- app/src/main/res/drawable/ic_plus_1_24.xml | 10 ---- app/src/main/res/drawable/ic_speed.xml | 5 ++ 7 files changed, 60 insertions(+), 76 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_neg_1_24.xml delete mode 100644 app/src/main/res/drawable/ic_plus_1_24.xml create mode 100644 app/src/main/res/drawable/ic_speed.xml diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index 1efc28da53..c95f41bc79 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -398,10 +398,9 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, processStopRequest() } else if (ACTION_REWIND == action) { processRewindRequest() - }else if (ACTION_SPEED_DOWN == action){ - processSpeedDownPlayback() - }else if (ACTION_SPEED_UP == action){ - processSpeedUpPlayback() + }else if (ACTION_SPEED_UPDATE == action){ + val speed = intent.getFloatExtra(EXTRA_PLAY_SPEED, 1f) + processUpdatePlaybackSpeed(speed) } else if (ACTION_UPDATE_REPEAT == action) { val playInfo = intent.getParcelableExtra(EXTRA_PLAY_INFO) @@ -682,43 +681,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } } } - private fun processSpeedUpPlayback() { - if (State.Playing === state) { - speedUpPlayback() - } - } - - private fun processSpeedDownPlayback() { - if (State.Playing === state) { - speedDownPlayback() - } - } - - private fun speedUpPlayback() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - player?.playbackParams?.let { params -> - val newSpeed = params.speed + 0.15f - // todo should be handled based on Qari type - if (newSpeed <= 1.5){ - params.setSpeed(newSpeed) - player?.playbackParams = params - } - } - } - } - - private fun speedDownPlayback() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + private fun processUpdatePlaybackSpeed(speed: Float) { + if (State.Playing === state && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { player?.playbackParams?.let { params -> - val newSpeed = params.speed - 0.1f - // todo should be handled based on Qari type - if (newSpeed >= 0.5){ - params.setSpeed(newSpeed) - player?.playbackParams = params - } + params.setSpeed(speed) + player?.playbackParams = params } } } + private fun processSkipRequest() { if (audioRequest == null) { return @@ -1402,8 +1373,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, const val ACTION_STOP = "com.quran.labs.androidquran.action.STOP" const val ACTION_SKIP = "com.quran.labs.androidquran.action.SKIP" const val ACTION_REWIND = "com.quran.labs.androidquran.action.REWIND" - const val ACTION_SPEED_UP = "com.quran.labs.androidquran.action.SPEED_UP" - const val ACTION_SPEED_DOWN = "com.quran.labs.androidquran.action.SPEED_DOWN" + const val ACTION_SPEED_UPDATE = "com.quran.labs.androidquran.action.SPEED_UPDATE" const val ACTION_CONNECT = "com.quran.labs.androidquran.action.CONNECT" const val ACTION_UPDATE_REPEAT = "com.quran.labs.androidquran.action.UPDATE_REPEAT" @@ -1421,6 +1391,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, // so user can pass in a serializable LegacyAudioRequest to the intent const val EXTRA_PLAY_INFO = "com.quran.labs.androidquran.PLAY_INFO" + const val EXTRA_PLAY_SPEED = "com.quran.labs.androidquran.PLAY_SPEED" private const val NOTIFICATION_CHANNEL_ID = Constants.AUDIO_CHANNEL private const val MSG_INCOMING = 1 private const val MSG_START_AUDIO = 2 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 977a828399..f6fcaeb622 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 @@ -1563,6 +1563,7 @@ public void proceedWithDownload(Intent downloadIntent) { public void handlePlayback(AudioRequest request) { needsPermissionToDownloadOver3g = true; + audioStatusBar.resetSpeed(); final Intent intent = new Intent(this, AudioService.class); intent.setAction(AudioService.ACTION_PLAYBACK); if (request != null) { @@ -1584,15 +1585,9 @@ public void onPausePressed() { } @Override - public void onUpPressed() { + public void setPlayBackSpeed(float speed) { startService(audioUtils.getAudioIntent(this, - AudioService.ACTION_SPEED_UP)); - } - - @Override - public void onDownPressed() { - startService(audioUtils.getAudioIntent(this, - AudioService.ACTION_SPEED_DOWN)); + AudioService.ACTION_SPEED_UPDATE).putExtra(AudioService.EXTRA_PLAY_SPEED, speed)); } @Override diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index 63a79dbbbe..41d7d9d2f0 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -5,6 +5,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; +import android.os.Build; import android.util.AttributeSet; import android.util.TypedValue; import android.view.Gravity; @@ -53,6 +54,10 @@ public class AudioStatusBar extends LeftToRightLinearLayout { private Qari currentQari; private int currentRepeat = 0; + private int defaultSpeedIndex = 2; + private int currentSpeedIndex = defaultSpeedIndex; + private final float[] speeds = { 0.5f, 0.75f, 1f, 1.25f, 1.5f}; + private float currentSpeed = speeds[currentSpeedIndex]; @DrawableRes private int itemBackground; private final boolean isRtl; private boolean isDualPageMode; @@ -65,17 +70,22 @@ public class AudioStatusBar extends LeftToRightLinearLayout { private TextView progressText; private ProgressBar progressBar; private final RepeatButton repeatButton; + private final RepeatButton speedButton; private AudioBarListener audioBarListener; private AudioBarRecitationListener audioBarRecitationListener; + public void resetSpeed() { + currentSpeedIndex = defaultSpeedIndex; + updateSpeedButtonText(); + } + public interface AudioBarListener { void onPlayPressed(); void onPausePressed(); void onNextPressed(); void onPreviousPressed(); void onStopPressed(); - void onDownPressed(); - void onUpPressed(); + void setPlayBackSpeed(float speed); void onCancelPressed(boolean stopDownload); void setRepeatCount(int repeatCount); void onAcceptPressed(); @@ -106,6 +116,7 @@ public AudioStatusBar(Context context, AttributeSet attrs, int defStyle) { this.context = context; repeatButton = new RepeatButton(context); + speedButton = new RepeatButton(context , false); Resources resources = getResources(); buttonWidth = resources.getDimensionPixelSize( R.dimen.audiobar_button_width); @@ -462,11 +473,13 @@ private void showPlayingMode(boolean isPaused) { addButton(R.drawable.ic_previous, withWeight); addButton(button, withWeight); addButton(R.drawable.ic_next, withWeight); - addButton(R.drawable.ic_neg_1_24, withWeight); - addButton(R.drawable.ic_plus_1_24, withWeight); addButton(repeatButton, R.drawable.ic_repeat, withWeight); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ + addButton(speedButton, R.drawable.ic_speed, withWeight); + } updateRepeatButtonText(); + updateSpeedButtonText(); addButton(R.drawable.ic_action_settings, withWeight); } @@ -515,6 +528,14 @@ private void incrementRepeat() { } updateRepeatButtonText(); } + private void updatePlayBackSpeed() { + currentSpeedIndex+=1; + currentSpeedIndex %= speeds.length; + currentSpeed = speeds[currentSpeedIndex]; + updateSpeedButtonText(); + } + + private void updateRepeatButtonText() { String str; @@ -528,6 +549,17 @@ private void updateRepeatButtonText() { repeatButton.setText(str); } + private void updateSpeedButtonText(){ + String str; + currentSpeed = speeds[currentSpeedIndex]; + if (currentSpeedIndex == 2) { + str = ""; + } else { + str = String.valueOf(currentSpeed); + } + speedButton.setText(str); + } + public void setRepeatCount(int repeatCount) { boolean updated = false; if (currentRepeat != repeatCount) { @@ -575,10 +607,9 @@ public void onClick(View view) { } } else if (tag == R.drawable.ic_next) { audioBarListener.onNextPressed(); - } else if (tag == R.drawable.ic_neg_1_24) { - audioBarListener.onDownPressed(); - } else if (tag == R.drawable.ic_plus_1_24) { - audioBarListener.onUpPressed(); + } else if (tag == R.drawable.ic_speed) { + updatePlayBackSpeed(); + audioBarListener.setPlayBackSpeed(currentSpeed); } else if (tag == R.drawable.ic_previous) { audioBarListener.onPreviousPressed(); } else if (tag == R.drawable.ic_repeat) { diff --git a/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt b/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt index 62371f694b..05306ba73e 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt @@ -11,6 +11,7 @@ import com.quran.labs.androidquran.R class RepeatButton @JvmOverloads constructor( context: Context, + private val shouldShiftText: Boolean = true, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatImageView(context, attrs, defStyleAttr) { @@ -46,10 +47,11 @@ class RepeatButton @JvmOverloads constructor( private fun updateCoordinates() { canDraw = false val drawable = drawable + val xDivider = if (shouldShiftText) 2 else 1 if (drawable != null) { val bounds = drawable.bounds if (bounds.width() > 0) { - textXPosition = viewWidth - (viewWidth - bounds.width()) / 2 + textXPosition = viewWidth - (viewWidth - bounds.width()) / xDivider textYPosition = textYPadding + (viewHeight - bounds.height()) / 2 canDraw = true } diff --git a/app/src/main/res/drawable/ic_neg_1_24.xml b/app/src/main/res/drawable/ic_neg_1_24.xml deleted file mode 100644 index c9830ca293..0000000000 --- a/app/src/main/res/drawable/ic_neg_1_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_plus_1_24.xml b/app/src/main/res/drawable/ic_plus_1_24.xml deleted file mode 100644 index 0e2de79573..0000000000 --- a/app/src/main/res/drawable/ic_plus_1_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_speed.xml b/app/src/main/res/drawable/ic_speed.xml new file mode 100644 index 0000000000..003852477a --- /dev/null +++ b/app/src/main/res/drawable/ic_speed.xml @@ -0,0 +1,5 @@ + + + + + From 86792a09d26cbd90082019c1a36e5fb174da4a59 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Tue, 26 Dec 2023 01:24:02 +0400 Subject: [PATCH 220/258] Minor cleanup --- .../labs/androidquran/service/AudioService.kt | 5 ++-- .../androidquran/view/AudioStatusBar.java | 24 +++++++++---------- .../labs/androidquran/view/RepeatButton.kt | 9 ++++--- app/src/main/res/drawable/ic_speed.xml | 2 -- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index c95f41bc79..d6c56dae3a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -398,11 +398,10 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, processStopRequest() } else if (ACTION_REWIND == action) { processRewindRequest() - }else if (ACTION_SPEED_UPDATE == action){ + } else if (ACTION_SPEED_UPDATE == action){ val speed = intent.getFloatExtra(EXTRA_PLAY_SPEED, 1f) processUpdatePlaybackSpeed(speed) - } - else if (ACTION_UPDATE_REPEAT == action) { + } else if (ACTION_UPDATE_REPEAT == action) { val playInfo = intent.getParcelableExtra(EXTRA_PLAY_INFO) val localAudioQueue = audioQueue if (playInfo != null && localAudioQueue != null) { diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index 41d7d9d2f0..19a17c2889 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -74,11 +74,6 @@ public class AudioStatusBar extends LeftToRightLinearLayout { private AudioBarListener audioBarListener; private AudioBarRecitationListener audioBarRecitationListener; - public void resetSpeed() { - currentSpeedIndex = defaultSpeedIndex; - updateSpeedButtonText(); - } - public interface AudioBarListener { void onPlayPressed(); void onPausePressed(); @@ -116,7 +111,7 @@ public AudioStatusBar(Context context, AttributeSet attrs, int defStyle) { this.context = context; repeatButton = new RepeatButton(context); - speedButton = new RepeatButton(context , false); + speedButton = new RepeatButton(context); Resources resources = getResources(); buttonWidth = resources.getDimensionPixelSize( R.dimen.audiobar_button_width); @@ -201,6 +196,11 @@ public QariItem getAudioInfo() { return QariItem.Companion.fromQari(context, currentQari); } + public void resetSpeed() { + currentSpeedIndex = defaultSpeedIndex; + updateSpeedButtonText(); + } + public void setProgress(int progress) { if (hasErrorText) { progressText.setText(R.string.downloading_title); @@ -528,17 +528,15 @@ private void incrementRepeat() { } updateRepeatButtonText(); } + private void updatePlayBackSpeed() { - currentSpeedIndex+=1; - currentSpeedIndex %= speeds.length; + currentSpeedIndex = (currentSpeedIndex + 1) % speeds.length; currentSpeed = speeds[currentSpeedIndex]; updateSpeedButtonText(); - } - - + } private void updateRepeatButtonText() { - String str; + final String str; if (currentRepeat == -1) { str = context.getString(R.string.infinity); } else if (currentRepeat == 0) { @@ -550,8 +548,8 @@ private void updateRepeatButtonText() { } private void updateSpeedButtonText(){ - String str; currentSpeed = speeds[currentSpeedIndex]; + final String str; if (currentSpeedIndex == 2) { str = ""; } else { diff --git a/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt b/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt index 05306ba73e..fbeaae39ea 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt @@ -11,7 +11,6 @@ import com.quran.labs.androidquran.R class RepeatButton @JvmOverloads constructor( context: Context, - private val shouldShiftText: Boolean = true, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatImageView(context, attrs, defStyleAttr) { @@ -47,11 +46,15 @@ class RepeatButton @JvmOverloads constructor( private fun updateCoordinates() { canDraw = false val drawable = drawable - val xDivider = if (shouldShiftText) 2 else 1 if (drawable != null) { val bounds = drawable.bounds if (bounds.width() > 0) { - textXPosition = viewWidth - (viewWidth - bounds.width()) / xDivider + val x = viewWidth - (viewWidth - bounds.width()) / 2 + textXPosition = if (x + bounds.width() > viewWidth) { + viewWidth - bounds.width() + } else { + x + } textYPosition = textYPadding + (viewHeight - bounds.height()) / 2 canDraw = true } diff --git a/app/src/main/res/drawable/ic_speed.xml b/app/src/main/res/drawable/ic_speed.xml index 003852477a..807b50d0ec 100644 --- a/app/src/main/res/drawable/ic_speed.xml +++ b/app/src/main/res/drawable/ic_speed.xml @@ -1,5 +1,3 @@ - - From da5945c30c35c6553d125fca65f3cf58aa5806a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:52:46 +0000 Subject: [PATCH 221/258] Update dependency com.github.skydoves:balloon to v1.6.4 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9bdbcb8034..a3a548ea13 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,7 +50,7 @@ firebaseCrashlyticsVersion = "18.6.0" # ui libraries accompanistVersion = "0.32.0" -balloonVersion = "1.6.3" +balloonVersion = "1.6.4" tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" materialComponentsVersion = "1.11.0" From 1c6f144c567c641a64f1acf8b295e138ac86711d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 03:46:48 +0000 Subject: [PATCH 222/258] Update dependency com.squareup.leakcanary:leakcanary-android to v2.13 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9bdbcb8034..2ecc8a0288 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ dnsjavaVersion = "2.1.9" errorprone = "3.1.0" errorproneCoreVersion = "2.24.0" googleServices = "4.4.0" -leakcanaryAndroidVersion = "2.12" +leakcanaryAndroidVersion = "2.13" moshiVersion = "1.15.0" okioVersion = "3.7.0" retrofitVersion = "2.9.0" From 11860dbafefbc1000345c8d820a52e6f6749d38f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 18:00:56 +0000 Subject: [PATCH 223/258] Update dependency com.google.errorprone:error_prone_core to v2.24.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9bdbcb8034..0e367ed871 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,7 @@ crashlytics = "2.9.9" daggerVersion = "2.50" dnsjavaVersion = "2.1.9" errorprone = "3.1.0" -errorproneCoreVersion = "2.24.0" +errorproneCoreVersion = "2.24.1" googleServices = "4.4.0" leakcanaryAndroidVersion = "2.12" moshiVersion = "1.15.0" From 5dc7a9187a123ebf79016acbdd5651eca9f4daad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 18:26:56 +0000 Subject: [PATCH 224/258] Update agp to v8.2.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3a548ea13..e6647c6b07 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.2.0" +agp = "8.2.1" kotlin = "1.9.21" ksp = "1.9.21-1.0.16" From d3ca12dac5a3f4e5cc1f1abf9415982b8db0a5cd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 3 Jan 2024 19:10:52 +0000 Subject: [PATCH 225/258] Update dependency com.android.tools.build:gradle to v8.2.1 --- build-logic/convention/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 441808f16c..893b6cbbde 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,7 +5,7 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.2.0") + compileOnly("com.android.tools.build:gradle:8.2.1") compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } From 6dcdf9d8571a8e2e98d6dc3ecc57e5177f6404f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 00:39:51 +0000 Subject: [PATCH 226/258] Update dependency com.google.auth:google-auth-library-oauth2-http to v1.21.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7ad990fdbb..ea156ee586 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,7 +59,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.60.1" -googleAuthVersion = "1.20.0" +googleAuthVersion = "1.21.0" googleCloudSpeechVersion = "4.27.0" # testing From 331749f463f877fa5a6d6f785dbf798debd106a7 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 6 Jan 2024 12:33:37 +0400 Subject: [PATCH 227/258] Persist audio playback speed in the audio request Also add the wiring to allow for exposing a speed setting in the audio playback panel. --- .../androidquran/dao/audio/AudioRequest.kt | 3 +- .../presenter/audio/AudioPresenter.kt | 3 +- .../labs/androidquran/service/AudioService.kt | 20 +++++------ .../labs/androidquran/ui/PagerActivity.java | 35 ++++++++++++++----- .../ui/fragment/AyahPlaybackFragment.kt | 5 +-- 5 files changed, 44 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt b/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt index d24f389fff..fc52f9c542 100644 --- a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt +++ b/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt @@ -1,8 +1,8 @@ package com.quran.labs.androidquran.dao.audio import android.os.Parcelable -import com.quran.labs.androidquran.common.audio.model.QariItem import com.quran.data.model.SuraAyah +import com.quran.labs.androidquran.common.audio.model.QariItem import kotlinx.parcelize.Parcelize @Parcelize @@ -12,6 +12,7 @@ data class AudioRequest(val start: SuraAyah, val repeatInfo: Int = 0, val rangeRepeatInfo: Int = 0, val enforceBounds: Boolean, + val playbackSpeed: Float = 1f, val shouldStream: Boolean, val audioPathInfo: AudioPathInfo) : Parcelable { fun isGapless() = qari.isGapless diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt index 0be9b85176..4eff9736af 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt @@ -32,6 +32,7 @@ constructor(private val quranDisplayData: QuranDisplayData, verseRepeat: Int, rangeRepeat: Int, enforceRange: Boolean, + playbackSpeed: Float, shouldStream: Boolean) { val audioPathInfo = getLocalAudioPathInfo(qari) if (audioPathInfo != null) { @@ -62,7 +63,7 @@ constructor(private val quranDisplayData: QuranDisplayData, } val audioRequest = AudioRequest( - actualStart, actualEnd, qari, verseRepeat, rangeRepeat, enforceRange, stream, audioPath) + actualStart, actualEnd, qari, verseRepeat, rangeRepeat, enforceRange, playbackSpeed, stream, audioPath) play(audioRequest) } } diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index d6c56dae3a..da40936e73 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -398,14 +398,14 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, processStopRequest() } else if (ACTION_REWIND == action) { processRewindRequest() - } else if (ACTION_SPEED_UPDATE == action){ - val speed = intent.getFloatExtra(EXTRA_PLAY_SPEED, 1f) - processUpdatePlaybackSpeed(speed) - } else if (ACTION_UPDATE_REPEAT == action) { + } else if (ACTION_UPDATE_SETTINGS == action) { val playInfo = intent.getParcelableExtra(EXTRA_PLAY_INFO) val localAudioQueue = audioQueue if (playInfo != null && localAudioQueue != null) { audioQueue = localAudioQueue.withUpdatedAudioRequest(playInfo) + if (playInfo.playbackSpeed != audioRequest?.playbackSpeed) { + processUpdatePlaybackSpeed(playInfo.playbackSpeed) + } audioRequest = playInfo } } else { @@ -680,6 +680,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } } } + private fun processUpdatePlaybackSpeed(speed: Float) { if (State.Playing === state && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { player?.playbackParams?.let { params -> @@ -1059,6 +1060,9 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, ) player.start() state = State.Playing + audioRequest?.playbackSpeed?.let { speed -> + processUpdatePlaybackSpeed(speed) + } serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200) } @@ -1362,19 +1366,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } companion object { - // These are the Intent actions that we are prepared to handle. Notice that - // the fact these constants exist in our class is a mere convenience: what - // really defines the actions our service can handle are the tags - // in the tag for our service in AndroidManifest.xml. + // These are the Intent actions that we are prepared to handle. const val ACTION_PLAYBACK = "com.quran.labs.androidquran.action.PLAYBACK" const val ACTION_PLAY = "com.quran.labs.androidquran.action.PLAY" const val ACTION_PAUSE = "com.quran.labs.androidquran.action.PAUSE" const val ACTION_STOP = "com.quran.labs.androidquran.action.STOP" const val ACTION_SKIP = "com.quran.labs.androidquran.action.SKIP" const val ACTION_REWIND = "com.quran.labs.androidquran.action.REWIND" - const val ACTION_SPEED_UPDATE = "com.quran.labs.androidquran.action.SPEED_UPDATE" const val ACTION_CONNECT = "com.quran.labs.androidquran.action.CONNECT" - const val ACTION_UPDATE_REPEAT = "com.quran.labs.androidquran.action.UPDATE_REPEAT" + const val ACTION_UPDATE_SETTINGS = "com.quran.labs.androidquran.action.UPDATE_SETTINGS" // pending notification request codes private const val REQUEST_CODE_MAIN = 0 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 f6fcaeb622..430da9f023 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 @@ -1496,7 +1496,7 @@ private void playFromAyah(int startSura, int startAyah) { final SuraAyah end = getSelectionEnd(); // handle the case of multiple ayat being selected and play them as a range if so final SuraAyah ending = (end == null || start.equals(end) || start.after(end))? null : end; - playFromAyah(start, ending, page, 0, 0, ending != null); + playFromAyah(start, ending, page, 0, 0, ending != null, 1.0f); } public void playFromAyah(SuraAyah start, @@ -1504,7 +1504,8 @@ public void playFromAyah(SuraAyah start, int page, int verseRepeat, int rangeRepeat, - boolean enforceRange) { + boolean enforceRange, + float playbackSpeed) { final SuraAyah ending = end != null ? end : audioUtils.getLastAyahToPlay(start, page, quranSettings.getPreferredDownloadAmount(), isDualPageVisible()); @@ -1516,7 +1517,7 @@ public void playFromAyah(SuraAyah start, final QariItem item = audioStatusBar.getAudioInfo(); final boolean shouldStream = quranSettings.shouldStream(); audioPresenter.play( - start, ending, item, verseRepeat, rangeRepeat, enforceRange, shouldStream); + start, ending, item, verseRepeat, rangeRepeat, enforceRange, playbackSpeed, shouldStream); } } @@ -1586,8 +1587,23 @@ public void onPausePressed() { @Override public void setPlayBackSpeed(float speed) { - startService(audioUtils.getAudioIntent(this, - AudioService.ACTION_SPEED_UPDATE).putExtra(AudioService.EXTRA_PLAY_SPEED, speed)); + if (lastAudioRequest != null) { + final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), + lastAudioRequest.getEnd(), + lastAudioRequest.getQari(), + lastAudioRequest.getRepeatInfo(), + lastAudioRequest.getRangeRepeatInfo(), + lastAudioRequest.getEnforceBounds(), + speed, + lastAudioRequest.getShouldStream(), + lastAudioRequest.getAudioPathInfo()); + + Intent i = new Intent(this, AudioService.class); + i.setAction(AudioService.ACTION_UPDATE_SETTINGS); + i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest); + startService(i); + lastAudioRequest = updatedAudioRequest; + } } @Override @@ -1628,7 +1644,8 @@ public void onShowQariList() { } public boolean updatePlayOptions(int rangeRepeat, - int verseRepeat, boolean enforceRange) { + int verseRepeat, + boolean enforceRange) { if (lastAudioRequest != null) { final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), lastAudioRequest.getEnd(), @@ -1636,10 +1653,11 @@ public boolean updatePlayOptions(int rangeRepeat, verseRepeat, rangeRepeat, enforceRange, + lastAudioRequest.getPlaybackSpeed(), lastAudioRequest.getShouldStream(), lastAudioRequest.getAudioPathInfo()); Intent i = new Intent(this, AudioService.class); - i.setAction(AudioService.ACTION_UPDATE_REPEAT); + i.setAction(AudioService.ACTION_UPDATE_SETTINGS); i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest); startService(i); @@ -1660,11 +1678,12 @@ public void setRepeatCount(int repeatCount) { repeatCount, lastAudioRequest.getRangeRepeatInfo(), lastAudioRequest.getEnforceBounds(), + lastAudioRequest.getPlaybackSpeed(), lastAudioRequest.getShouldStream(), lastAudioRequest.getAudioPathInfo()); Intent i = new Intent(this, AudioService.class); - i.setAction(AudioService.ACTION_UPDATE_REPEAT); + i.setAction(AudioService.ACTION_UPDATE_SETTINGS); i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest); startService(i); lastAudioRequest = updatedAudioRequest; diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt index 14f64d024f..db90ed4208 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt @@ -189,12 +189,13 @@ class AyahPlaybackFragment : AyahActionFragment() { val enforceRange = restrictToRange.isChecked var updatedRange = false + val speed = 1.0f // TODO: expose a setting within this fragment also if (currentStart != decidedStart || currentEnding != decidedEnd) { // different range or not playing, so make a new request updatedRange = true context.playFromAyah( currentStart, currentEnding, page, verseRepeat, - rangeRepeat, enforceRange + rangeRepeat, enforceRange, speed ) } else if (shouldEnforce != enforceRange || rangeRepeatCount != rangeRepeat || verseRepeatCount != verseRepeat) { // can just update repeat settings @@ -202,7 +203,7 @@ class AyahPlaybackFragment : AyahActionFragment() { ) { // audio stopped in the process, let's start it context.playFromAyah( - currentStart, currentEnding, page, verseRepeat, rangeRepeat, enforceRange + currentStart, currentEnding, page, verseRepeat, rangeRepeat, enforceRange, speed ) } } From b57631b0667f5b789f6ad95b3db128cf46b266f1 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 6 Jan 2024 14:21:08 +0400 Subject: [PATCH 228/258] Audio position update time should factor in speed The audio service continuously sends events checking the playback status every few seconds, depending on how much time is left until the end of the ayah. These update timings need to now factor in the speed, so if we are 3 seconds away from the end of the ayah and are playing at 1.5x, for example, checking after 3 seconds is too late (since we would have played some of the next ayah). --- .../labs/androidquran/service/AudioService.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index da40936e73..89b7c3fe9f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -55,6 +55,7 @@ import android.support.v4.media.session.PlaybackStateCompat import android.util.SparseIntArray import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import androidx.core.math.MathUtils.clamp import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.media.session.MediaButtonReceiver import com.quran.data.core.QuranInfo @@ -405,6 +406,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, audioQueue = localAudioQueue.withUpdatedAudioRequest(playInfo) if (playInfo.playbackSpeed != audioRequest?.playbackSpeed) { processUpdatePlaybackSpeed(playInfo.playbackSpeed) + serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200) } audioRequest = playInfo } @@ -570,14 +572,11 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } notifyAyahChanged() if (maxAyahs >= updatedAyah + 1) { - var t = gaplessSuraData[updatedAyah + 1] - localPlayer.currentPosition - Timber.d("updateAudioPlayPosition postingDelayed after: %d", t) - if (t < 100) { - t = 100 - } else if (t > 10000) { - t = 10000 - } - serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, t.toLong()) + val timeDelta = gaplessSuraData[updatedAyah + 1] - localPlayer.currentPosition + val t = clamp(timeDelta, 100, 10000) + val tAccountingForSpeed = t / (audioRequest?.playbackSpeed ?: 1f) + Timber.d("updateAudioPlayPosition after: %d, speed %f", t, tAccountingForSpeed) + serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, tAccountingForSpeed.toLong()) } else if (maxAyahs == updatedAyah) { serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 150) } From 9fefe6602e323e5936b1d5d7e025e3de63875e1a Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 6 Jan 2024 22:08:12 +0400 Subject: [PATCH 229/258] Add speed option to audio settings panel --- .../labs/androidquran/ui/PagerActivity.java | 10 +++-- .../ui/fragment/AyahPlaybackFragment.kt | 39 ++++++++++++------- .../androidquran/view/AudioStatusBar.java | 28 ++++++++----- .../res/layout-ar-land-v17/audio_panel.xml | 1 + .../main/res/layout-ar-land/audio_panel.xml | 1 + .../main/res/layout-ar-v17/audio_panel.xml | 1 + app/src/main/res/layout-ar/audio_panel.xml | 1 + app/src/main/res/layout-land/audio_panel.xml | 1 + app/src/main/res/layout/audio_panel.xml | 1 + app/src/main/res/layout/play_each_verse.xml | 4 +- .../main/res/layout/play_set_of_verses.xml | 4 +- app/src/main/res/layout/playback_speed.xml | 35 +++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 13 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 app/src/main/res/layout/playback_speed.xml 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 430da9f023..e9ffcf2cda 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 @@ -1564,13 +1564,13 @@ public void proceedWithDownload(Intent downloadIntent) { public void handlePlayback(AudioRequest request) { needsPermissionToDownloadOver3g = true; - audioStatusBar.resetSpeed(); final Intent intent = new Intent(this, AudioService.class); intent.setAction(AudioService.ACTION_PLAYBACK); if (request != null) { intent.putExtra(AudioService.EXTRA_PLAY_INFO, request); lastAudioRequest = request; audioStatusBar.setRepeatCount(request.getRepeatInfo()); + audioStatusBar.setSpeed(request.getPlaybackSpeed()); audioStatusBar.switchMode(AudioStatusBar.LOADING_MODE); } @@ -1586,7 +1586,7 @@ public void onPausePressed() { } @Override - public void setPlayBackSpeed(float speed) { + public void setPlaybackSpeed(float speed) { if (lastAudioRequest != null) { final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), lastAudioRequest.getEnd(), @@ -1645,7 +1645,8 @@ public void onShowQariList() { public boolean updatePlayOptions(int rangeRepeat, int verseRepeat, - boolean enforceRange) { + boolean enforceRange, + float playbackSpeed) { if (lastAudioRequest != null) { final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), lastAudioRequest.getEnd(), @@ -1653,7 +1654,7 @@ public boolean updatePlayOptions(int rangeRepeat, verseRepeat, rangeRepeat, enforceRange, - lastAudioRequest.getPlaybackSpeed(), + playbackSpeed, lastAudioRequest.getShouldStream(), lastAudioRequest.getAudioPathInfo()); Intent i = new Intent(this, AudioService.class); @@ -1663,6 +1664,7 @@ public boolean updatePlayOptions(int rangeRepeat, lastAudioRequest = updatedAudioRequest; audioStatusBar.setRepeatCount(verseRepeat); + audioStatusBar.setSpeed(playbackSpeed); return true; } else { return false; diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt index db90ed4208..649190224f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt @@ -35,6 +35,7 @@ class AyahPlaybackFragment : AyahActionFragment() { private var shouldEnforce = false private var rangeRepeatCount = 0 private var verseRepeatCount = 0 + private var currentSpeed = 1.0f private lateinit var applyButton: Button private lateinit var startSuraSpinner: QuranSpinner @@ -43,6 +44,7 @@ class AyahPlaybackFragment : AyahActionFragment() { private lateinit var endingAyahSpinner: QuranSpinner private lateinit var repeatVersePicker: NumberPicker private lateinit var repeatRangePicker: NumberPicker + private lateinit var playbackSpeedPicker: NumberPicker private lateinit var restrictToRange: CheckBox private lateinit var startAyahAdapter: ArrayAdapter @@ -76,6 +78,7 @@ class AyahPlaybackFragment : AyahActionFragment() { applyButton.setOnClickListener(onClickListener) repeatVersePicker = view.findViewById(R.id.repeat_verse_picker) repeatRangePicker = view.findViewById(R.id.repeat_range_picker) + playbackSpeedPicker = view.findViewById(R.id.playback_speed_picker) val context = requireContext() val isArabicNames = QuranSettings.getInstance(context).isArabicNames @@ -91,18 +94,15 @@ class AyahPlaybackFragment : AyahActionFragment() { } values[MAX_REPEATS] = getString(R.string.infinity) if (isArabicNames) { - repeatVersePicker.formatter = NumberPicker.Formatter { value: Int -> arFormat(value) } - repeatRangePicker.formatter = NumberPicker.Formatter { value: Int -> arFormat(value) } - val typeface = TypefaceManager.getHeaderFooterTypeface(context) - repeatVersePicker.typeface = typeface - repeatVersePicker.setSelectedTypeface(typeface) - repeatRangePicker.typeface = typeface - repeatRangePicker.setSelectedTypeface(typeface) - // Use larger text size since KFGQPC font is small - repeatVersePicker.setSelectedTextSize(R.dimen.arabic_number_picker_selected_text_size) - repeatRangePicker.setSelectedTextSize(R.dimen.arabic_number_picker_selected_text_size) - repeatVersePicker.setTextSize(R.dimen.arabic_number_picker_text_size) - repeatRangePicker.setTextSize(R.dimen.arabic_number_picker_text_size) + listOf(repeatVersePicker, repeatRangePicker, playbackSpeedPicker).forEach { + it.formatter = NumberPicker.Formatter { value: Int -> arFormat(value) } + val typeface = TypefaceManager.getHeaderFooterTypeface(context) + it.typeface = typeface + it.setSelectedTypeface(typeface) + // Use larger text size since KFGQPC font is small + it.setSelectedTextSize(R.dimen.arabic_number_picker_selected_text_size) + it.setTextSize(R.dimen.arabic_number_picker_text_size) + } } repeatVersePicker.minValue = 1 repeatVersePicker.maxValue = MAX_REPEATS + 1 @@ -112,6 +112,10 @@ class AyahPlaybackFragment : AyahActionFragment() { repeatRangePicker.displayedValues = values repeatRangePicker.value = defaultRangeRepeat repeatVersePicker.value = defaultVerseRepeat + playbackSpeedPicker.minValue = 1 + playbackSpeedPicker.maxValue = SPEEDS.size + playbackSpeedPicker.displayedValues = SPEEDS.map { numberFormat.format(it) }.toTypedArray() + playbackSpeedPicker.value = DEFAULT_SPEED_INDEX + 1 repeatRangePicker.setOnValueChangedListener { _: NumberPicker?, _: Int, newVal: Int -> if (newVal > 1) { // whenever we want to repeat the range, we have to enable restrictToRange @@ -189,7 +193,7 @@ class AyahPlaybackFragment : AyahActionFragment() { val enforceRange = restrictToRange.isChecked var updatedRange = false - val speed = 1.0f // TODO: expose a setting within this fragment also + val speed = SPEEDS[playbackSpeedPicker.value - 1] if (currentStart != decidedStart || currentEnding != decidedEnd) { // different range or not playing, so make a new request updatedRange = true @@ -197,9 +201,9 @@ class AyahPlaybackFragment : AyahActionFragment() { currentStart, currentEnding, page, verseRepeat, rangeRepeat, enforceRange, speed ) - } else if (shouldEnforce != enforceRange || rangeRepeatCount != rangeRepeat || verseRepeatCount != verseRepeat) { + } else if (shouldEnforce != enforceRange || rangeRepeatCount != rangeRepeat || verseRepeatCount != verseRepeat || currentSpeed != speed) { // can just update repeat settings - if (!context.updatePlayOptions(rangeRepeat, verseRepeat, enforceRange) + if (!context.updatePlayOptions(rangeRepeat, verseRepeat, enforceRange, speed) ) { // audio stopped in the process, let's start it context.playFromAyah( @@ -304,6 +308,7 @@ class AyahPlaybackFragment : AyahActionFragment() { if (lastRequest != lastSeenAudioRequest) { verseRepeatCount = lastRequest.repeatInfo rangeRepeatCount = lastRequest.rangeRepeatInfo + currentSpeed = lastRequest.playbackSpeed shouldEnforce = lastRequest.enforceBounds } else { shouldReset = false @@ -325,6 +330,7 @@ class AyahPlaybackFragment : AyahActionFragment() { } rangeRepeatCount = 0 verseRepeatCount = 0 + currentSpeed = 1.0f decidedStart = null decidedEnd = null applyButton.setText(R.string.play_apply_and_play) @@ -348,6 +354,7 @@ class AyahPlaybackFragment : AyahActionFragment() { restrictToRange.isChecked = shouldEnforce repeatRangePicker.value = rangeRepeatCount + 1 repeatVersePicker.value = verseRepeatCount + 1 + playbackSpeedPicker.value = SPEEDS.indexOf(currentSpeed) + 1 } } } @@ -356,5 +363,7 @@ class AyahPlaybackFragment : AyahActionFragment() { private val ITEM_LAYOUT = R.layout.sherlock_spinner_item private val ITEM_DROPDOWN_LAYOUT = R.layout.sherlock_spinner_dropdown_item private const val MAX_REPEATS = 25 + private val SPEEDS = listOf(0.5f, 0.75f, 1.0f, 1.25f, 1.5f) + private const val DEFAULT_SPEED_INDEX = 2 } } diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index 19a17c2889..053889af7f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -54,7 +54,7 @@ public class AudioStatusBar extends LeftToRightLinearLayout { private Qari currentQari; private int currentRepeat = 0; - private int defaultSpeedIndex = 2; + private final int defaultSpeedIndex = 2; private int currentSpeedIndex = defaultSpeedIndex; private final float[] speeds = { 0.5f, 0.75f, 1f, 1.25f, 1.5f}; private float currentSpeed = speeds[currentSpeedIndex]; @@ -80,7 +80,7 @@ public interface AudioBarListener { void onNextPressed(); void onPreviousPressed(); void onStopPressed(); - void setPlayBackSpeed(float speed); + void setPlaybackSpeed(float speed); void onCancelPressed(boolean stopDownload); void setRepeatCount(int repeatCount); void onAcceptPressed(); @@ -196,9 +196,14 @@ public QariItem getAudioInfo() { return QariItem.Companion.fromQari(context, currentQari); } - public void resetSpeed() { - currentSpeedIndex = defaultSpeedIndex; - updateSpeedButtonText(); + public void setSpeed(float speed) { + for (int i = 0; i < speeds.length; i++) { + if (speeds[i] == speed) { + currentSpeedIndex = i; + updateSpeedButtonText(); + return; + } + } } public void setProgress(int progress) { @@ -529,7 +534,7 @@ private void incrementRepeat() { updateRepeatButtonText(); } - private void updatePlayBackSpeed() { + private void updatePlaybackSpeed() { currentSpeedIndex = (currentSpeedIndex + 1) % speeds.length; currentSpeed = speeds[currentSpeedIndex]; updateSpeedButtonText(); @@ -555,7 +560,12 @@ private void updateSpeedButtonText(){ } else { str = String.valueOf(currentSpeed); } - speedButton.setText(str); + + post(() -> { + if (speedButton != null) { + speedButton.setText(str); + } + }); } public void setRepeatCount(int repeatCount) { @@ -606,8 +616,8 @@ public void onClick(View view) { } else if (tag == R.drawable.ic_next) { audioBarListener.onNextPressed(); } else if (tag == R.drawable.ic_speed) { - updatePlayBackSpeed(); - audioBarListener.setPlayBackSpeed(currentSpeed); + updatePlaybackSpeed(); + audioBarListener.setPlaybackSpeed(currentSpeed); } else if (tag == R.drawable.ic_previous) { audioBarListener.onPreviousPressed(); } else if (tag == R.drawable.ic_repeat) { diff --git a/app/src/main/res/layout-ar-land-v17/audio_panel.xml b/app/src/main/res/layout-ar-land-v17/audio_panel.xml index 311ec21ba3..d6cc338f03 100644 --- a/app/src/main/res/layout-ar-land-v17/audio_panel.xml +++ b/app/src/main/res/layout-ar-land-v17/audio_panel.xml @@ -86,6 +86,7 @@ + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 490dbc7fbd..6bc12f4030 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -346,6 +346,7 @@ Pause Stop Next + Playback Speed: 1 time From 49d805e70310064ce3f1624a2bad2ad4acba21e0 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 6 Jan 2024 23:54:17 +0400 Subject: [PATCH 230/258] Translate playback speed string --- app/src/main/res/values-ar/strings.xml | 1 + app/src/main/res/values-az/strings.xml | 1 + app/src/main/res/values-bs/strings.xml | 1 + app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values-es/strings.xml | 1 + app/src/main/res/values-fa/strings.xml | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values-hr/strings.xml | 1 + app/src/main/res/values-hu/strings.xml | 1 + app/src/main/res/values-in/strings.xml | 1 + app/src/main/res/values-it/strings.xml | 1 + app/src/main/res/values-kk/strings.xml | 1 + app/src/main/res/values-ku/strings.xml | 1 + app/src/main/res/values-ms/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 1 + app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values-pt/strings.xml | 1 + app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-sq/strings.xml | 1 + app/src/main/res/values-sr/strings.xml | 1 + app/src/main/res/values-sv/strings.xml | 1 + app/src/main/res/values-th/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-ug/strings.xml | 1 + app/src/main/res/values-uk/strings.xml | 1 + app/src/main/res/values-uz/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 1 + app/src/main/res/values-zh/strings.xml | 1 + 28 files changed, 28 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 753d62a2cd..58f3269629 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -243,6 +243,7 @@ الوضع العربي تشغيل إيقاف + سرعة التشغيل: تم تحسين الصور الخاصه بالتابلت. هل تود تحميلها الان؟ رتب حسب التصنيفات diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 30a1b6b71b..c33a686a62 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -258,6 +258,7 @@ Dinle Duraklat Durdur + Oynatma Sürati: Sonraki diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 0fa7371c9f..40747dc9a1 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -276,6 +276,7 @@ Pokreni Pauziraj Stani + Brzina reprodukcije: Sljedeći diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ffc587ed00..5ac54791a4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -288,6 +288,7 @@ Anwenden und Abspielen Abspielen Stoppen + Wiedergabegeschwindigkeit: einmal diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7858a9be06..5c93d680e3 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -161,6 +161,7 @@ Aplicar y reproducir Reproducir Detener + Velocidad de reproducción: Versículo copiado diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 8d7214050f..f3a8b87b2e 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -217,6 +217,7 @@ قبلی مکث توقف + سرعت پخش: بعدی حافظه‌ای که تمایل دارید فایل‌ها روی آن ذخیره شود را انتخاب نمایید diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 481a38b7a6..803e141bcb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -238,6 +238,7 @@ Écouter Pause Stopper + Vitesse de lecture : Suivant diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index b55e7bac1a..a8d5d02902 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -276,6 +276,7 @@ Pokreni Pauziraj Stani + Brzina reprodukcije: Sljedeći diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 35a08fe5ab..a50aa62aae 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -211,6 +211,7 @@ Alkalmaz és lejátszás Lejátszás Leállítás + Lejátszási sebesség: egyszer diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index a3a4edc90d..2729dc7202 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -263,6 +263,7 @@ Putar Jeda Stop + वापसी की गति: Selanjutnya diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7d353ec686..ef2a4af537 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -31,6 +31,7 @@ Precedente Pausa Fermare + Velocità di riproduzione: Prossimo Undo Tag diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index 24cedcb0b8..c034f19674 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -266,6 +266,7 @@ Тыңдау Кідірту Тоқтату + Жүктеу жылдамдылығы: Келесі diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 6186cd0aab..4045794495 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -272,6 +272,7 @@ گوێبگرە ڕاوەستان وەستان + سرعة التشغيل: دواتر دەست بکە بە پەخشکردنەوە لە: سەرەتای لاپەڕە diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 5d69155c37..67c5da76d5 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -234,6 +234,7 @@ Mainkan Pause Hentikan + درجة سرعة التشغيل: Seterus diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 826c26f02f..eb224010b9 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -375,6 +375,7 @@ Afspelen Pauzeren Onderbreken + Afspeelsnelheid: Volgende diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 43df4b0538..dcf385ea46 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -266,6 +266,7 @@ Odtwórz Pauza Zatrzymać + Prędkość odtwarzania: Następny diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index e283b0fab6..758f9d7146 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -283,6 +283,7 @@ escolher um leitor-qari diferente. Clique play para baixar e reproduzir a págin Ouça Pausa Parada + Velocidade de reprodução: Próximo Iniciar a reprodução a partir de: Início da página diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c91cfb7e8c..46aae4d194 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -314,6 +314,7 @@ Воспроизвести Пауза Приостановить + Скорость воспроизведения: Следующий diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index d912cba534..9bdd514e44 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -265,6 +265,7 @@ Dëgjo. Pauzë Ndalo. + Shpejtesia e Ripertrajtimit: Në vazhdim diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index e0b7005b1a..3218ca037f 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -276,6 +276,7 @@ Pokreni Pauziraj Stani + Брзина репродукције: Sledeći diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4dcbee5e99..2133a5d192 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -265,6 +265,7 @@ Spela upp Pausa Stoppa + Uppspelningshastighet: Nästa diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 41f006d4fe..8e6cf6e4e1 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -256,6 +256,7 @@ เล่น หยุด หยุด + ความเร็วในการเล่นเสียง: ต่อไป diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 30a1b6b71b..6e56bc017f 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -258,6 +258,7 @@ Dinle Duraklat Durdur + Oynatma Hızı: Sonraki diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index ce38c24faf..4efbfb686f 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -276,6 +276,7 @@ پۈتكۈل سەھىپەدىن ئىزدە ئالدىنقى توختا + Ujinu uri vifujio: قوش بەت مايىللىقى قوش بەت ھالىتىدىكى قۇرئان ۋە تەرجىمە تەرجىمە بار قوش بەت ھالىتىدە ، قۇرئان بېتى ۋە تەرجىمە كۆرۈنىدۇ diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 57147b3681..c13f1b5640 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -267,6 +267,7 @@ Слухати Пауза Зупинити + Llais yn ôl: Наступний diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index 35efd2ea7e..69295207f1 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -291,6 +291,7 @@ Chalish Pauza Toʻxtatish + Tezlikni tomosha qilish: Keyingi diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 2df98f4e9c..d5cae4af12 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -293,6 +293,7 @@ Phát Nghỉ Dừng + Tốc độ phát lại: Tiếp diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2ce0b5f1b3..283e6c34d3 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -268,6 +268,7 @@ 播放 暂停 停止 + 播放速度: 下一页 开始播放从。 页首 From e956ef24ae527abd7153a78f731f920a18e77241 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 7 Jan 2024 19:16:24 +0400 Subject: [PATCH 231/258] Bump version to 3.4.2 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 5a6abe8146..00ec5c48e5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { namespace 'com.quran.labs.androidquran' defaultConfig { - versionCode 3410 - versionName "3.4.1" + versionCode 3420 + versionName "3.4.2" testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" } From 5d2312040330fddce87c888515363116f96f6eae Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Jan 2024 04:00:04 +0000 Subject: [PATCH 232/258] Update dependency com.google.cloud:google-cloud-speech to v4.28.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea156ee586..61367b637b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.60.1" googleAuthVersion = "1.21.0" -googleCloudSpeechVersion = "4.27.0" +googleCloudSpeechVersion = "4.28.0" # testing junitVersion = "4.13.2" From fbb1c899d9c6b58bf8b7238d96a8f76d37d693a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 06:44:04 +0000 Subject: [PATCH 233/258] Update dependency io.grpc:grpc-okhttp to v1.61.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea156ee586..21739ef8e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -58,7 +58,7 @@ numberPickerVersion = "2.4.13" reorderableComposeVersion = "0.9.6" # recitations -grpcOkhttpVersion = "1.60.1" +grpcOkhttpVersion = "1.61.0" googleAuthVersion = "1.21.0" googleCloudSpeechVersion = "4.27.0" From 0737e9fed336bd2de7823c150a6dbcd7a1f57b9e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Jan 2024 19:35:28 +0000 Subject: [PATCH 234/258] Update dependency com.google.auth:google-auth-library-oauth2-http to v1.22.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 71b8b4adc6..449eff24e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -59,7 +59,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.61.0" -googleAuthVersion = "1.21.0" +googleAuthVersion = "1.22.0" googleCloudSpeechVersion = "4.28.0" # testing From 7892dc0280a1d4cd2a44b4b400f3af1798e474c4 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Fri, 12 Jan 2024 23:37:45 +0400 Subject: [PATCH 235/258] Update Kotlin to 1.9.22 --- build-logic/convention/build.gradle.kts | 2 +- gradle/libs.versions.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 893b6cbbde..1464b5c323 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -6,7 +6,7 @@ group = "com.quran.labs.androidquran.buildlogic" dependencies { compileOnly("com.android.tools.build:gradle:8.2.1") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21") + compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 71b8b4adc6..2254e2a2af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] agp = "8.2.1" -kotlin = "1.9.21" -ksp = "1.9.21-1.0.16" +kotlin = "1.9.22" +ksp = "1.9.22-1.0.16" # required within the Gradle convention plugins - not unused -compose-compiler = "1.5.6" +compose-compiler = "1.5.8" composeBomVersion = "2023.10.01" okhttpBomVersion = "4.12.0" # dependencies -anvil = "2.4.8" +anvil = "2.4.9" coroutinesVersion = "1.7.3" crashlytics = "2.9.9" daggerVersion = "2.50" From 203ca22c8e3a7331430e9d8fee3ef88055813502 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 13 Jan 2024 00:03:51 +0400 Subject: [PATCH 236/258] Fix column name issue with translation database Older versions of the translation database (versions 2 and 3) added and had a translator_foreign field. In version 4, this was renamed to translatorForeign. The code assumed it was always named as translatorForeign, which is incorrect, thus crashing for people upgrading from older versions. --- .../quran/mobile/translation/databases/2.db | Bin 8192 -> 8192 bytes .../quran/mobile/translation/databases/3.db | Bin 8192 -> 8192 bytes .../quran/mobile/translation/migrations/1.sqm | 4 ++-- .../quran/mobile/translation/migrations/2.sqm | 6 +++--- .../quran/mobile/translation/migrations/3.sqm | 5 +++-- .../quran/mobile/translation/migrations/4.sqm | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/2.db b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/2.db index 425d1490681708f114bd890d6e46a6574d1a2dc9..0b46c7c3207eea1900afbe487204f3e39c6edc4f 100644 GIT binary patch delta 71 zcmZp0XmFSyEy%*az`z8=Fu*cV$C#gmL9b{LFHne)pOb-~bF-kpY(B2W5=M4$RaM4j S?a3niolNm*o9FQ-2><|V2Mjg< delta 70 zcmZp0XmFSyEy&2gz`z8=Fu*ub$C#gyL9b{bFHne)pNoN?YqOxh96qkbVn%jxRaM4j R&B?<2os4dq=kX^A003M!3#$MC diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/3.db b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/3.db index 76b6421a8432b80fd1ca10e82e20b12b46a9555f..fedc3da40872c49df26c87690ba03979e50be507 100644 GIT binary patch delta 71 zcmZp0XmFSyEy%*az`z8=Fu*cV$C#gmL9b{LFHne)?+XLpm(79#ZG2o!oQ&+^s;Z35 Sp_Bjfbuz`LZSLeR7XSdH5DuRJ delta 70 zcmZp0XmFSyEy&2gz`z8=Fu*ub$C#gyL9b{bFHne)?<)h}*Uf?g?R;EK9E|MZs;Z35 R!IS^-buzkb?&L2Q005HL4io?Y diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm index 29d9684960..53c103dfcb 100644 --- a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/1.sqm @@ -7,12 +7,12 @@ CREATE TABLE IF NOT EXISTS translations ( version INTEGER NOT NULL DEFAULT 0 ); --- adds translatorForeign +-- adds translator_foreign, updated in v2.7.3 CREATE TABLE translations_migration ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, translator TEXT, - translatorForeign TEXT, + translator_foreign TEXT, filename TEXT NOT NULL, url TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 0 diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm index 69e50d0f6d..1a76a70d1c 100644 --- a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm @@ -1,9 +1,9 @@ --- adds languageCode +-- adds languageCode - updated in v2.7.5 CREATE TABLE translations_migration ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, translator TEXT, - translatorForeign TEXT, + translator_foreign TEXT, filename TEXT NOT NULL, url TEXT NOT NULL, languageCode TEXT, @@ -11,7 +11,7 @@ CREATE TABLE translations_migration ( ); INSERT INTO translations_migration -SELECT id, name, translator, translatorForeign, filename, url, "", version +SELECT id, name, translator, translator_foreign, filename, url, "", version FROM translations; DROP TABLE translations; diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm index 311c357b41..0efe75c829 100644 --- a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm @@ -1,4 +1,5 @@ --- adds minimumRequiredVersion +-- adds minimumRequiredVersion - updated in v2.9.2 +-- renames translator_foreign to translatorForeign also. CREATE TABLE translations_migration ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, @@ -12,7 +13,7 @@ CREATE TABLE translations_migration ( ); INSERT INTO translations_migration -SELECT id, name, translator, translatorForeign, filename, url, languageCode, version, 2 +SELECT id, name, translator, translator_foreign, filename, url, languageCode, version, 2 FROM translations; DROP TABLE translations; diff --git a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm index da84f45122..cf3386ab2c 100644 --- a/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm @@ -1,4 +1,4 @@ --- adds userDisplayOrder +-- adds userDisplayOrder, updated in v3.0.2 CREATE TABLE translations_migration ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, From 63cd8ff5b4456789e1f496dd05bb284eb4f58519 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 13 Jan 2024 00:34:52 +0400 Subject: [PATCH 237/258] Fix and catch some crashes --- .../labs/androidquran/service/AudioService.kt | 12 +++++++++--- .../androidquran/service/QuranDownloadService.java | 14 ++++++++------ .../quran/labs/androidquran/ui/QuranActivity.kt | 10 ++++++++-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index 89b7c3fe9f..24c065ff4b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -682,9 +682,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, private fun processUpdatePlaybackSpeed(speed: Float) { if (State.Playing === state && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - player?.playbackParams?.let { params -> - params.setSpeed(speed) - player?.playbackParams = params + try { + player?.playbackParams?.let { params -> + params.setSpeed(speed) + player?.playbackParams = params + } + } catch (e: Exception) { + // catch an Android 6 crash [IllegalStateException], and report the speed since some + // non-Android 6 devices also crash here, but with [IllegalArgumentException] + Timber.e(e, "Failed to set speed to $speed") } } } diff --git a/app/src/main/java/com/quran/labs/androidquran/service/QuranDownloadService.java b/app/src/main/java/com/quran/labs/androidquran/service/QuranDownloadService.java index 0922fe9381..072fa3dc5f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/QuranDownloadService.java +++ b/app/src/main/java/com/quran/labs/androidquran/service/QuranDownloadService.java @@ -239,13 +239,15 @@ private void sendNoOpMessage(int id) { @Override public int onStartCommand(Intent intent, int flags, int startId) { - // if it's a download, it wants to be a foreground service. - // quickly start as foreground before actually enqueueing the request. - if (ACTION_DOWNLOAD_URL.equals(intent.getAction())) { - notifier.notifyDownloadStarting(); - } + if (intent != null) { + // if it's a download, it wants to be a foreground service. + // quickly start as foreground before actually enqueueing the request. + if (ACTION_DOWNLOAD_URL.equals(intent.getAction())) { + notifier.notifyDownloadStarting(); + } - handleOnStartCommand(intent, startId); + handleOnStartCommand(intent, startId); + } return START_NOT_STICKY; } diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt b/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt index 7714c01fbd..875692580d 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt @@ -1,5 +1,6 @@ package com.quran.labs.androidquran.ui +import android.app.BackgroundServiceStartNotAllowedException import android.app.SearchManager import android.content.ComponentName import android.content.Context @@ -160,9 +161,14 @@ class QuranActivity : AppCompatActivity(), Completable.timer(500, MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { - startService( + try { + startService( audioUtils.getAudioIntent(this@QuranActivity, AudioService.ACTION_STOP) - ) + ) + } catch (illegalStateException: IllegalStateException) { + // do nothing, we might be in the background + // onPause should have stopped us from needing this, but it sometimes happens + } } ) } From 1038222d37281c5840697b80846ef2cab9082498 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 13 Jan 2024 01:10:43 +0400 Subject: [PATCH 238/258] Hide playback speed on unsupported versions --- .../com/quran/labs/androidquran/service/AudioService.kt | 8 ++++++-- .../labs/androidquran/ui/fragment/AyahPlaybackFragment.kt | 6 ++++++ app/src/main/res/layout/playback_speed.xml | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index 24c065ff4b..fbebe83763 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -575,7 +575,12 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, val timeDelta = gaplessSuraData[updatedAyah + 1] - localPlayer.currentPosition val t = clamp(timeDelta, 100, 10000) val tAccountingForSpeed = t / (audioRequest?.playbackSpeed ?: 1f) - Timber.d("updateAudioPlayPosition after: %d, speed %f", t, tAccountingForSpeed) + Timber.d( + "updateAudioPlayPosition before: %d, after %f, speed: %f", + t, + tAccountingForSpeed, + audioRequest?.playbackSpeed + ) serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, tAccountingForSpeed.toLong()) } else if (maxAyahs == updatedAyah) { serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 150) @@ -1395,7 +1400,6 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, // so user can pass in a serializable LegacyAudioRequest to the intent const val EXTRA_PLAY_INFO = "com.quran.labs.androidquran.PLAY_INFO" - const val EXTRA_PLAY_SPEED = "com.quran.labs.androidquran.PLAY_SPEED" private const val NOTIFICATION_CHANNEL_ID = Constants.AUDIO_CHANNEL private const val MSG_INCOMING = 1 private const val MSG_START_AUDIO = 2 diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt index 649190224f..815b2d7ccc 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt @@ -1,6 +1,7 @@ package com.quran.labs.androidquran.ui.fragment import android.content.Context +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -80,6 +81,11 @@ class AyahPlaybackFragment : AyahActionFragment() { repeatRangePicker = view.findViewById(R.id.repeat_range_picker) playbackSpeedPicker = view.findViewById(R.id.playback_speed_picker) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + val speedArea = view.findViewById(R.id.playback_speed_area) + speedArea.visibility = View.GONE + } + val context = requireContext() val isArabicNames = QuranSettings.getInstance(context).isArabicNames val locale: Locale = if (isArabicNames) { diff --git a/app/src/main/res/layout/playback_speed.xml b/app/src/main/res/layout/playback_speed.xml index 4b14f8bd90..146ea2849d 100644 --- a/app/src/main/res/layout/playback_speed.xml +++ b/app/src/main/res/layout/playback_speed.xml @@ -4,6 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" + android:id="@+id/playback_speed_area" > Date: Sat, 13 Jan 2024 01:52:11 +0400 Subject: [PATCH 239/258] Bump version to 3.4.3 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 00ec5c48e5..9509f1ce48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { namespace 'com.quran.labs.androidquran' defaultConfig { - versionCode 3420 - versionName "3.4.2" + versionCode 3430 + versionName "3.4.3" testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" } From 600b59630b289dc16cf621150d0cfc66d25abb82 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 13 Jan 2024 23:04:26 +0400 Subject: [PATCH 240/258] Move some audio models out of app and clean up --- .../androidquran/presenter/audio/AudioPresenter.kt | 6 +++--- .../presenter/audio/service/AudioQueue.kt | 2 +- .../labs/androidquran/service/AudioService.kt | 2 +- .../androidquran/service/util/DownloadStarter.kt | 2 +- .../quran/labs/androidquran/ui/PagerActivity.java | 14 +++----------- .../ui/fragment/AyahPlaybackFragment.kt | 2 +- .../extension/QariDownloadInfoExtensionTest.kt | 4 ++-- .../common/audio/cache/QariDownloadInfoManager.kt | 4 ++-- .../audio/cache/QariDownloadInfoStorageCache.kt | 2 +- .../common/audio/cache/command/AudioInfoCommand.kt | 2 +- .../audio/cache/command/GappedAudioInfoCommand.kt | 2 +- .../extension/PartiallyDownloadedSuraExtension.kt | 2 +- .../audio/extension/QariDownloadInfoExtension.kt | 2 +- .../model/{ => download}/AudioDownloadMetadata.kt | 2 +- .../{ => download}/PartiallyDownloadedSura.kt | 2 +- .../audio/model/{ => download}/QariDownloadInfo.kt | 2 +- .../common/audio/model/playback}/AudioPathInfo.kt | 2 +- .../common/audio/model/playback}/AudioRequest.kt | 5 +++-- .../cache/command/GappedAudioInfoCommandTest.kt | 2 +- .../quran/reading/common/ReadingEventPresenter.kt | 2 +- .../presenter/SheikhAudioPresenter.kt | 4 ++-- .../qarilist/presenter/QariListPresenter.kt | 2 +- 22 files changed, 31 insertions(+), 38 deletions(-) rename common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/{ => download}/AudioDownloadMetadata.kt (68%) rename common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/{ => download}/PartiallyDownloadedSura.kt (63%) rename common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/{ => download}/QariDownloadInfo.kt (89%) rename {app/src/main/java/com/quran/labs/androidquran/dao/audio => common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback}/AudioPathInfo.kt (79%) rename {app/src/main/java/com/quran/labs/androidquran/dao/audio => common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback}/AudioRequest.kt (84%) diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt index 4eff9736af..d9e391c8c8 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt @@ -5,12 +5,12 @@ import android.content.Intent import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.R import com.quran.labs.androidquran.common.audio.model.QariItem -import com.quran.labs.androidquran.dao.audio.AudioPathInfo -import com.quran.labs.androidquran.dao.audio.AudioRequest +import com.quran.labs.androidquran.common.audio.model.playback.AudioPathInfo +import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.presenter.Presenter import com.quran.labs.androidquran.service.QuranDownloadService -import com.quran.labs.androidquran.common.audio.model.AudioDownloadMetadata +import com.quran.labs.androidquran.common.audio.model.download.AudioDownloadMetadata import com.quran.labs.androidquran.service.util.ServiceIntentHelper import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.util.AudioUtils diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt index 24a252e2c0..4584911e52 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt @@ -2,7 +2,7 @@ package com.quran.labs.androidquran.presenter.audio.service import com.quran.data.core.QuranInfo import com.quran.labs.androidquran.dao.audio.AudioPlaybackInfo -import com.quran.labs.androidquran.dao.audio.AudioRequest +import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.extension.requiresBasmallah import java.util.Locale diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index fbebe83763..ef1495bc2a 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -63,7 +63,7 @@ import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.QuranApplication import com.quran.labs.androidquran.R import com.quran.labs.androidquran.dao.audio.AudioPlaybackInfo -import com.quran.labs.androidquran.dao.audio.AudioRequest +import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.data.QuranFileConstants diff --git a/app/src/main/java/com/quran/labs/androidquran/service/util/DownloadStarter.kt b/app/src/main/java/com/quran/labs/androidquran/service/util/DownloadStarter.kt index bcf8cb749f..fafd70e63f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/util/DownloadStarter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/util/DownloadStarter.kt @@ -7,7 +7,7 @@ import com.quran.data.core.QuranInfo import com.quran.data.di.AppScope import com.quran.data.model.SuraAyah import com.quran.data.model.audio.Qari -import com.quran.labs.androidquran.common.audio.model.AudioDownloadMetadata +import com.quran.labs.androidquran.common.audio.model.download.AudioDownloadMetadata import com.quran.labs.androidquran.service.QuranDownloadService import com.quran.labs.androidquran.util.AudioUtils import com.quran.mobile.common.download.Downloader 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 e9ffcf2cda..f665f8c62e 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 @@ -69,7 +69,7 @@ import com.quran.labs.androidquran.bridge.ReadingEventPresenterBridge; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.common.audio.model.QariItem; -import com.quran.labs.androidquran.dao.audio.AudioRequest; +import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest; import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.data.QuranDataProvider; import com.quran.labs.androidquran.data.QuranDisplayData; @@ -1159,7 +1159,7 @@ private void switchToTranslation() { endAyahMode(); } - if (translations.size() == 0) { + if (translations.isEmpty()) { startTranslationManager(); } else { int page = getCurrentPage(); @@ -1204,14 +1204,6 @@ public List getTranslations() { return translations; } - public String[] getTranslationNames() { - return translationNames; - } - - public Set getActiveTranslationsFilesNames() { - return activeTranslationsFilesNames; - } - @Override public void onAddTagSelected() { FragmentManager fm = getSupportFragmentManager(); @@ -1852,7 +1844,7 @@ private void shareAyah(SuraAyah start, SuraAyah end, final boolean isCopy) { compositeDisposable.add( arabicDatabaseUtils .getVerses(start, end) - .filter(quranAyahs -> quranAyahs.size() > 0) + .filter(quranAyahs -> !quranAyahs.isEmpty()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(quranAyahs -> { if (isCopy) { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt index 815b2d7ccc..994eccb637 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahPlaybackFragment.kt @@ -14,7 +14,7 @@ import android.widget.CheckBox import com.quran.data.core.QuranInfo import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.R -import com.quran.labs.androidquran.dao.audio.AudioRequest +import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter import com.quran.labs.androidquran.ui.util.TypefaceManager diff --git a/app/src/test/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtensionTest.kt b/app/src/test/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtensionTest.kt index 00d9dc05c7..1dcffb5a1e 100644 --- a/app/src/test/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtensionTest.kt +++ b/app/src/test/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtensionTest.kt @@ -2,8 +2,8 @@ package com.quran.labs.androidquran.common.audio.extension import com.quran.data.model.SuraAyah import com.quran.data.model.audio.Qari -import com.quran.labs.androidquran.common.audio.model.PartiallyDownloadedSura -import com.quran.labs.androidquran.common.audio.model.QariDownloadInfo +import com.quran.labs.androidquran.common.audio.model.download.PartiallyDownloadedSura +import com.quran.labs.androidquran.common.audio.model.download.QariDownloadInfo import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoManager.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoManager.kt index 9522169fa3..00b4a0adf6 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoManager.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoManager.kt @@ -2,8 +2,8 @@ package com.quran.labs.androidquran.common.audio.cache import com.quran.data.core.QuranFileManager import com.quran.labs.androidquran.common.audio.cache.command.AudioInfoCommand -import com.quran.labs.androidquran.common.audio.model.AudioDownloadMetadata -import com.quran.labs.androidquran.common.audio.model.QariDownloadInfo +import com.quran.labs.androidquran.common.audio.model.download.AudioDownloadMetadata +import com.quran.labs.androidquran.common.audio.model.download.QariDownloadInfo import com.quran.mobile.common.download.DownloadInfo import com.quran.mobile.common.download.DownloadInfoStreams import kotlinx.coroutines.CoroutineScope diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoStorageCache.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoStorageCache.kt index 5c9b5e6d5a..8ce0bc7e61 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoStorageCache.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/QariDownloadInfoStorageCache.kt @@ -1,6 +1,6 @@ package com.quran.labs.androidquran.common.audio.cache -import com.quran.labs.androidquran.common.audio.model.QariDownloadInfo +import com.quran.labs.androidquran.common.audio.model.download.QariDownloadInfo import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/AudioInfoCommand.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/AudioInfoCommand.kt index 517de2080a..0c25acb392 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/AudioInfoCommand.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/AudioInfoCommand.kt @@ -1,7 +1,7 @@ package com.quran.labs.androidquran.common.audio.cache.command import com.quran.data.model.audio.Qari -import com.quran.labs.androidquran.common.audio.model.QariDownloadInfo +import com.quran.labs.androidquran.common.audio.model.download.QariDownloadInfo import com.quran.labs.androidquran.common.audio.util.QariUtil import javax.inject.Inject import okio.FileSystem diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt index 1cad0ca264..831f40043f 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommand.kt @@ -1,7 +1,7 @@ package com.quran.labs.androidquran.common.audio.cache.command import com.quran.data.core.QuranInfo -import com.quran.labs.androidquran.common.audio.model.PartiallyDownloadedSura +import com.quran.labs.androidquran.common.audio.model.download.PartiallyDownloadedSura import com.quran.labs.androidquran.common.audio.util.AudioFileUtil import okio.FileSystem import okio.Path diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/PartiallyDownloadedSuraExtension.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/PartiallyDownloadedSuraExtension.kt index 1f64150521..fe2d6c3dcf 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/PartiallyDownloadedSuraExtension.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/PartiallyDownloadedSuraExtension.kt @@ -1,6 +1,6 @@ package com.quran.labs.androidquran.common.audio.extension -import com.quran.labs.androidquran.common.audio.model.PartiallyDownloadedSura +import com.quran.labs.androidquran.common.audio.model.download.PartiallyDownloadedSura fun PartiallyDownloadedSura.didDownloadAyat(currentSura: Int, start: Int, end: Int): Boolean { val ayat = IntRange(start, end) diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtension.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtension.kt index a804e31ed1..e849ba6e36 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtension.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/extension/QariDownloadInfoExtension.kt @@ -1,7 +1,7 @@ package com.quran.labs.androidquran.common.audio.extension import com.quran.data.model.SuraAyah -import com.quran.labs.androidquran.common.audio.model.QariDownloadInfo +import com.quran.labs.androidquran.common.audio.model.download.QariDownloadInfo /** * Determine whether the range of verses is downloaded diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/AudioDownloadMetadata.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/AudioDownloadMetadata.kt similarity index 68% rename from common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/AudioDownloadMetadata.kt rename to common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/AudioDownloadMetadata.kt index ce0017b99f..12beef35d1 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/AudioDownloadMetadata.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/AudioDownloadMetadata.kt @@ -1,4 +1,4 @@ -package com.quran.labs.androidquran.common.audio.model +package com.quran.labs.androidquran.common.audio.model.download import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/PartiallyDownloadedSura.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/PartiallyDownloadedSura.kt similarity index 63% rename from common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/PartiallyDownloadedSura.kt rename to common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/PartiallyDownloadedSura.kt index b71a12fa82..ba587ce0cb 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/PartiallyDownloadedSura.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/PartiallyDownloadedSura.kt @@ -1,3 +1,3 @@ -package com.quran.labs.androidquran.common.audio.model +package com.quran.labs.androidquran.common.audio.model.download data class PartiallyDownloadedSura(val sura: Int, val expectedAyahCount: Int, val downloadedAyat: List) diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/QariDownloadInfo.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/QariDownloadInfo.kt similarity index 89% rename from common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/QariDownloadInfo.kt rename to common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/QariDownloadInfo.kt index 8ed0e27acc..99a0dba8f2 100644 --- a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/QariDownloadInfo.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/download/QariDownloadInfo.kt @@ -1,4 +1,4 @@ -package com.quran.labs.androidquran.common.audio.model +package com.quran.labs.androidquran.common.audio.model.download import com.quran.data.model.audio.Qari diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioPathInfo.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioPathInfo.kt similarity index 79% rename from app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioPathInfo.kt rename to common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioPathInfo.kt index 7a1cd0e563..c36841a63f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioPathInfo.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioPathInfo.kt @@ -1,4 +1,4 @@ -package com.quran.labs.androidquran.dao.audio +package com.quran.labs.androidquran.common.audio.model.playback import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioRequest.kt similarity index 84% rename from app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt rename to common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioRequest.kt index fc52f9c542..e444a03cc6 100644 --- a/app/src/main/java/com/quran/labs/androidquran/dao/audio/AudioRequest.kt +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioRequest.kt @@ -1,4 +1,4 @@ -package com.quran.labs.androidquran.dao.audio +package com.quran.labs.androidquran.common.audio.model.playback import android.os.Parcelable import com.quran.data.model.SuraAyah @@ -14,7 +14,8 @@ data class AudioRequest(val start: SuraAyah, val enforceBounds: Boolean, val playbackSpeed: Float = 1f, val shouldStream: Boolean, - val audioPathInfo: AudioPathInfo) : Parcelable { + val audioPathInfo: AudioPathInfo +) : Parcelable { fun isGapless() = qari.isGapless fun needsIsti3athaAudio() = !isGapless() || audioPathInfo.gaplessDatabase?.contains("minshawi_murattal") ?: false diff --git a/common/audio/src/test/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommandTest.kt b/common/audio/src/test/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommandTest.kt index 7d213fee43..b8e720d1a4 100644 --- a/common/audio/src/test/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommandTest.kt +++ b/common/audio/src/test/java/com/quran/labs/androidquran/common/audio/cache/command/GappedAudioInfoCommandTest.kt @@ -3,7 +3,7 @@ package com.quran.labs.androidquran.common.audio.cache.command import com.google.common.truth.Truth import com.quran.data.core.QuranInfo import com.quran.data.pageinfo.common.MadaniDataSource -import com.quran.labs.androidquran.common.audio.model.PartiallyDownloadedSura +import com.quran.labs.androidquran.common.audio.model.download.PartiallyDownloadedSura import okio.Path.Companion.toPath import okio.fakefilesystem.FakeFileSystem import org.junit.Test diff --git a/common/reading/src/main/java/com/quran/reading/common/ReadingEventPresenter.kt b/common/reading/src/main/java/com/quran/reading/common/ReadingEventPresenter.kt index ee7f0a5cf1..017d673a05 100644 --- a/common/reading/src/main/java/com/quran/reading/common/ReadingEventPresenter.kt +++ b/common/reading/src/main/java/com/quran/reading/common/ReadingEventPresenter.kt @@ -25,7 +25,7 @@ class ReadingEventPresenter @Inject constructor(private val quranInfo: QuranInfo ) private val quranClickInternalFlow = MutableSharedFlow( replay = 0, extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST) - private val detailsPanelInternalFlow = MutableStateFlow(false) + private val detailsPanelInternalFlow = MutableStateFlow(false) private val ayahSelectionInternalFlow = MutableStateFlow(AyahSelection.None) val clicksFlow: Flow = clicksInternalFlow.asSharedFlow() diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt index 0002b77e51..b2a286a45c 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/presenter/SheikhAudioPresenter.kt @@ -4,8 +4,8 @@ import com.quran.data.core.QuranFileManager import com.quran.data.di.ActivityScope import com.quran.labs.androidquran.common.audio.cache.AudioCacheInvalidator import com.quran.labs.androidquran.common.audio.cache.QariDownloadInfoManager -import com.quran.labs.androidquran.common.audio.model.AudioDownloadMetadata -import com.quran.labs.androidquran.common.audio.model.QariDownloadInfo +import com.quran.labs.androidquran.common.audio.model.download.AudioDownloadMetadata +import com.quran.labs.androidquran.common.audio.model.download.QariDownloadInfo import com.quran.mobile.common.download.DownloadConstants import com.quran.mobile.common.download.DownloadInfo import com.quran.mobile.common.download.DownloadInfoStreams diff --git a/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/presenter/QariListPresenter.kt b/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/presenter/QariListPresenter.kt index 2357a67261..136bd71802 100644 --- a/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/presenter/QariListPresenter.kt +++ b/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/presenter/QariListPresenter.kt @@ -5,7 +5,7 @@ import com.quran.data.model.SuraAyah import com.quran.data.model.audio.Qari import com.quran.labs.androidquran.common.audio.cache.QariDownloadInfoManager import com.quran.labs.androidquran.common.audio.extension.isRangeDownloaded -import com.quran.labs.androidquran.common.audio.model.QariDownloadInfo +import com.quran.labs.androidquran.common.audio.model.download.QariDownloadInfo import com.quran.labs.androidquran.common.audio.model.QariItem import com.quran.mobile.feature.qarilist.R import com.quran.mobile.feature.qarilist.model.QariUiModel From 1289004e1deb19955103430b50b598dc0b385ff6 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 14 Jan 2024 00:20:27 +0400 Subject: [PATCH 241/258] Emit audio playback events via audio repository Previously, audio events were either broadcast via a local broadcast (now deprecated), or emitted via an AudioEventPresenter. This replaces both approaches, and rather than just emitting the current playing ayah, it emits the entire playback state instead. --- .../bridge/AudioEventPresenterBridge.kt | 27 ----- .../bridge/AudioStatusRepositoryBridge.kt | 60 +++++++++ .../presenter/audio/service/AudioQueue.kt | 1 + .../quran/ayahtracker/AyahTrackerPresenter.kt | 11 +- .../labs/androidquran/service/AudioService.kt | 114 +++++++----------- .../labs/androidquran/ui/PagerActivity.java | 69 ++--------- .../ui/fragment/AyahActionFragment.kt | 8 +- .../ui/fragment/TagBookmarkFragment.kt | 8 +- .../androidquran/view/AudioStatusBar.java | 4 +- .../audio/model/playback/AudioStatus.kt | 15 +++ .../audio/model/playback/PlaybackStatus.kt | 5 + .../audio/repository/AudioStatusRepository.kt | 19 +++ .../reading/common/AudioEventPresenter.kt | 23 ---- 13 files changed, 172 insertions(+), 192 deletions(-) delete mode 100644 app/src/main/java/com/quran/labs/androidquran/bridge/AudioEventPresenterBridge.kt create mode 100644 app/src/main/java/com/quran/labs/androidquran/bridge/AudioStatusRepositoryBridge.kt create mode 100644 common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioStatus.kt create mode 100644 common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/PlaybackStatus.kt create mode 100644 common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/AudioStatusRepository.kt delete mode 100644 common/reading/src/main/java/com/quran/reading/common/AudioEventPresenter.kt diff --git a/app/src/main/java/com/quran/labs/androidquran/bridge/AudioEventPresenterBridge.kt b/app/src/main/java/com/quran/labs/androidquran/bridge/AudioEventPresenterBridge.kt deleted file mode 100644 index ea3d929f78..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/bridge/AudioEventPresenterBridge.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.quran.labs.androidquran.bridge - -import com.quran.data.model.SuraAyah -import com.quran.reading.common.AudioEventPresenter -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - -class AudioEventPresenterBridge constructor( - audioEventPresenter: AudioEventPresenter, - onPlaybackAyahChanged: ((SuraAyah?) -> Unit) -) { - - private val scope = MainScope() - private val audioPlaybackAyahFlow = audioEventPresenter.audioPlaybackAyahFlow - - init { - audioPlaybackAyahFlow - .onEach { onPlaybackAyahChanged(it) } - .launchIn(scope) - } - - fun dispose() { - scope.cancel() - } -} diff --git a/app/src/main/java/com/quran/labs/androidquran/bridge/AudioStatusRepositoryBridge.kt b/app/src/main/java/com/quran/labs/androidquran/bridge/AudioStatusRepositoryBridge.kt new file mode 100644 index 0000000000..01e150c515 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/bridge/AudioStatusRepositoryBridge.kt @@ -0,0 +1,60 @@ +package com.quran.labs.androidquran.bridge + +import com.quran.data.model.SuraAyah +import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest +import com.quran.labs.androidquran.common.audio.model.playback.AudioStatus +import com.quran.labs.androidquran.common.audio.model.playback.PlaybackStatus +import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository +import com.quran.labs.androidquran.view.AudioStatusBar +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +class AudioStatusRepositoryBridge( + audioStatusRepository: AudioStatusRepository, + audioStatusBar: () -> AudioStatusBar, + onPlaybackAyahChanged: ((SuraAyah?) -> Unit) +) { + + private val scope = MainScope() + private val audioPlaybackAyahFlow = audioStatusRepository.audioPlaybackFlow + + init { + audioPlaybackAyahFlow + .onEach { status -> + when (status) { + is AudioStatus.Playback -> { + val statusBar = audioStatusBar() + if (status.playbackStatus == PlaybackStatus.PLAYING) { + statusBar.switchMode(AudioStatusBar.PLAYING_MODE) + if (status.audioRequest.repeatInfo >= -1) { + statusBar.setRepeatCount(status.audioRequest.repeatInfo) + statusBar.setSpeed(status.audioRequest.playbackSpeed) + } + } else if (status.playbackStatus == PlaybackStatus.PAUSED) { + statusBar.switchMode(AudioStatusBar.PAUSED_MODE) + } else if (status.playbackStatus == PlaybackStatus.PREPARING) { + statusBar.switchMode(AudioStatusBar.LOADING_MODE) + } + onPlaybackAyahChanged(status.currentAyah) + } + AudioStatus.Stopped -> { + audioStatusBar().switchMode(AudioStatusBar.STOPPED_MODE) + } + } + } + .launchIn(scope) + } + + fun audioRequest(): AudioRequest? { + return when (val status = audioPlaybackAyahFlow.value) { + is AudioStatus.Playback -> status.audioRequest + AudioStatus.Stopped -> null + } + } + + fun dispose() { + scope.cancel() + } +} diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt index 4584911e52..5feface706 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt @@ -55,6 +55,7 @@ class AudioQueue(private val quranInfo: QuranInfo, fun getCurrentSura() = playbackInfo.currentAyah.sura fun getCurrentAyah() = playbackInfo.currentAyah.ayah + fun getCurrentPlaybackAyah() = playbackInfo.currentAyah fun playNextAyah(skipAyahRepeat: Boolean = false): Boolean { if (playbackInfo.shouldPlayBasmallah) { diff --git a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt index 5b19973c31..ede00ac03f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/quran/ayahtracker/AyahTrackerPresenter.kt @@ -17,6 +17,8 @@ import com.quran.data.model.selection.AyahSelection import com.quran.data.model.selection.SelectionIndicator import com.quran.data.model.selection.startSuraAyah import com.quran.labs.androidquran.common.QuranAyahInfo +import com.quran.labs.androidquran.common.audio.model.playback.currentPlaybackAyah +import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.data.SuraAyahIterator import com.quran.labs.androidquran.presenter.Presenter @@ -34,7 +36,6 @@ import com.quran.mobile.bookmark.model.BookmarkModel import com.quran.mobile.translation.model.LocalTranslation import com.quran.page.common.data.AyahCoordinates import com.quran.page.common.data.PageCoordinates -import com.quran.reading.common.AudioEventPresenter import com.quran.reading.common.ReadingEventPresenter import com.quran.recitation.events.RecitationEventPresenter import com.quran.recitation.presenter.RecitationHighlightsPresenter @@ -58,8 +59,8 @@ class AyahTrackerPresenter @Inject constructor( private val quranSettings: QuranSettings, private val readingEventPresenter: ReadingEventPresenter, private val bookmarkModel: BookmarkModel, - private val audioEventPresenter: AudioEventPresenter, - private val recitationPresenter: RecitationPresenter, + private val audioStatusRepository: AudioStatusRepository, + recitationPresenter: RecitationPresenter, private val recitationEventPresenter: RecitationEventPresenter, private val recitationPopupPresenter: RecitationPopupPresenter, private val recitationHighlightsPresenter: RecitationHighlightsPresenter, @@ -81,8 +82,8 @@ class AyahTrackerPresenter @Inject constructor( .onEach { onAyahSelectionChanged(it) } .launchIn(scope) - audioEventPresenter.audioPlaybackAyahFlow - .onEach { onAudioSelectionChanged(it) } + audioStatusRepository.audioPlaybackFlow + .onEach { onAudioSelectionChanged(it.currentPlaybackAyah()) } .launchIn(scope) items.forEach { trackerItem -> diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index ef1495bc2a..fbf812b05b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -56,14 +56,15 @@ import android.util.SparseIntArray import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.core.math.MathUtils.clamp -import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.media.session.MediaButtonReceiver import com.quran.data.core.QuranInfo -import com.quran.data.model.SuraAyah import com.quran.labs.androidquran.QuranApplication import com.quran.labs.androidquran.R -import com.quran.labs.androidquran.dao.audio.AudioPlaybackInfo import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest +import com.quran.labs.androidquran.common.audio.model.playback.AudioStatus +import com.quran.labs.androidquran.common.audio.model.playback.PlaybackStatus +import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository +import com.quran.labs.androidquran.dao.audio.AudioPlaybackInfo import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.data.QuranFileConstants @@ -77,7 +78,6 @@ import com.quran.labs.androidquran.service.util.QuranDownloadNotifier import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.util.AudioUtils import com.quran.labs.androidquran.util.NotificationChannelUtil.setupNotificationChannel -import com.quran.reading.common.AudioEventPresenter import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Maybe import io.reactivex.rxjava3.core.Single @@ -98,17 +98,6 @@ import kotlin.math.abs */ class AudioService : Service(), OnCompletionListener, OnPreparedListener, MediaPlayer.OnErrorListener, AudioFocusable, OnSeekCompleteListener { - object AudioUpdateIntent { - const val INTENT_NAME = "com.quran.labs.androidquran.audio.AudioUpdate" - const val STATUS = "status" - const val SURA = "sura" - const val AYAH = "ayah" - const val REPEAT_COUNT = "repeat_count" - const val REQUEST = "request" - const val STOPPED = 0 - const val PLAYING = 1 - const val PAUSED = 2 - } // our media player private var player: MediaPlayer? = null @@ -162,7 +151,6 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, private lateinit var audioFocusHelper: AudioFocusHelper private lateinit var notificationManager: NotificationManager - private lateinit var broadcastManager: LocalBroadcastManager private lateinit var noisyAudioStreamReceiver: BroadcastReceiver private lateinit var mediaSession: MediaSessionCompat @@ -193,7 +181,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, lateinit var audioUtils: AudioUtils @Inject - lateinit var audioEventPresenter: AudioEventPresenter + lateinit var audioStatusRepository: AudioStatusRepository private inner class ServiceHandler(looper: Looper) : Handler(looper) { override fun handleMessage(msg: Message) { @@ -262,7 +250,6 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, // create the Audio Focus Helper audioFocusHelper = AudioFocusHelper(appContext, this) - broadcastManager = LocalBroadcastManager.getInstance(appContext) noisyAudioStreamReceiver = NoisyAudioStreamReceiver() ContextCompat.registerReceiver( @@ -347,38 +334,14 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, if (State.Stopped == state) { processStopRequest(true) } else { - var sura = -1 - var ayah = -1 - var repeatCount = -200 - var state = AudioUpdateIntent.PLAYING - if (State.Paused == this.state) { - state = AudioUpdateIntent.PAUSED - } - - val localAudioQueue = audioQueue - val localAudioRequest = audioRequest - if (localAudioQueue != null && localAudioRequest != null) { - sura = localAudioQueue.getCurrentSura() - ayah = localAudioQueue.getCurrentAyah() - repeatCount = localAudioRequest.repeatInfo - } - - val updateIntent = Intent(AudioUpdateIntent.INTENT_NAME) - updateIntent.putExtra(AudioUpdateIntent.STATUS, state) - updateIntent.putExtra(AudioUpdateIntent.SURA, sura) - updateIntent.putExtra(AudioUpdateIntent.AYAH, ayah) - updateIntent.putExtra(AudioUpdateIntent.REPEAT_COUNT, repeatCount) - updateIntent.putExtra(AudioUpdateIntent.REQUEST,localAudioRequest) - broadcastManager.sendBroadcast(updateIntent) + updateAudioPlaybackStatus() } } else if (ACTION_PLAYBACK == action) { val updatedAudioRequest = intent.getParcelableExtra(EXTRA_PLAY_INFO) if (updatedAudioRequest != null) { audioRequest = updatedAudioRequest val start = updatedAudioRequest.start - audioEventPresenter.onAyahPlayback(start) - val basmallah = !updatedAudioRequest.isGapless() && - start.requiresBasmallah() + val basmallah = !updatedAudioRequest.isGapless() && start.requiresBasmallah() audioQueue = AudioQueue( quranInfo, updatedAudioRequest, AudioPlaybackInfo(start, 1, 1, basmallah) @@ -627,7 +590,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, setUpAsForeground() } configAndStartMediaPlayer(false) - notifyAudioStatus(AudioUpdateIntent.PLAYING) + updateAudioPlaybackStatus() } } @@ -642,11 +605,12 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, // update the notification. relaxResources(releaseMediaPlayer = false, stopForeground = false) pauseNotification() - notifyAudioStatus(AudioUpdateIntent.PAUSED) + updateAudioPlaybackStatus() } else if (State.Stopped == state) { // if we get a pause while we're already stopped, it means we likely woke up because // of AudioIntentReceiver, so just stop in this case. setState(PlaybackStateCompat.STATE_STOPPED) + updateAudioPlaybackStatus() stopSelf() } } @@ -664,6 +628,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, seekTo = getSeekPosition(true) pos -= seekTo } + if (pos > 1500 && !playerOverride) { localPlayer.seekTo(seekTo) state = State.Playing // in case we were paused @@ -739,6 +704,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } if (force || State.Stopped != state) { state = State.Stopped + updateAudioPlaybackStatus() // let go of all resources... relaxResources(releaseMediaPlayer = true, stopForeground = true) @@ -750,25 +716,13 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, // stop async task if it's running timingDisposable?.dispose() - - // tell the ui we've stopped - audioEventPresenter.onAyahPlayback(null) - notifyAudioStatus(AudioUpdateIntent.STOPPED) } } private fun notifyAyahChanged() { - val localAudioQueue = audioQueue ?: return val localAudioRequest = audioRequest ?: return + updateAudioPlaybackStatus() - audioEventPresenter.onAyahPlayback( - SuraAyah(localAudioQueue.getCurrentSura(), localAudioQueue.getCurrentAyah()) - ) - val updateIntent = Intent(AudioUpdateIntent.INTENT_NAME) - updateIntent.putExtra(AudioUpdateIntent.STATUS, AudioUpdateIntent.PLAYING) - updateIntent.putExtra(AudioUpdateIntent.SURA, localAudioQueue.getCurrentSura()) - updateIntent.putExtra(AudioUpdateIntent.AYAH, localAudioQueue.getCurrentAyah()) - broadcastManager.sendBroadcast(updateIntent) val metadataBuilder = MediaMetadataCompat.Builder() .putString(MediaMetadataCompat.METADATA_KEY_TITLE, getTitle()) .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, localAudioRequest.qari.name) @@ -789,10 +743,30 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, mediaSession.setMetadata(metadataBuilder.build()) } - private fun notifyAudioStatus(status: Int) { - val updateIntent = Intent(AudioUpdateIntent.INTENT_NAME) - updateIntent.putExtra(AudioUpdateIntent.STATUS, status) - broadcastManager.sendBroadcast(updateIntent) + private fun updateAudioPlaybackStatus() { + val audioStatus = when (state) { + State.Stopped -> AudioStatus.Stopped + State.Playing, State.Preparing, State.Paused -> { + val localAudioQueue = audioQueue ?: return + val localAudioRequest = audioRequest ?: return + + AudioStatus.Playback( + localAudioQueue.getCurrentPlaybackAyah(), + localAudioRequest, + state.asPlayingPlaybackStatus() + ) + } + } + audioStatusRepository.updateAyahPlayback(audioStatus) + } + + private fun State.asPlayingPlaybackStatus(): PlaybackStatus { + return when (this) { + State.Playing -> PlaybackStatus.PLAYING + State.Preparing -> PlaybackStatus.PREPARING + State.Paused -> PlaybackStatus.PAUSED + else -> throw IllegalStateException("State $this is not a playing state") + } } /** @@ -879,6 +853,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, if (!player.isPlaying) { player.start() state = State.Playing + updateAudioPlaybackStatus() } return } @@ -902,6 +877,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } player.start() state = State.Playing + updateAudioPlaybackStatus() } } @@ -940,10 +916,6 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, val url = audioQueue?.getUrl() if (localAudioRequest == null || localAudioQueue == null || url == null) { - val updateIntent = Intent(AudioUpdateIntent.INTENT_NAME) - updateIntent.putExtra(AudioUpdateIntent.STATUS, AudioUpdateIntent.STOPPED) - audioEventPresenter.onAyahPlayback(null) - broadcastManager.sendBroadcast(updateIntent) processStopRequest(true) // stop everything! return } @@ -952,11 +924,6 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, if (!isStreaming) { val f = File(url) if (!f.exists()) { - val updateIntent = Intent(AudioUpdateIntent.INTENT_NAME) - updateIntent.putExtra(AudioUpdateIntent.STATUS, AudioUpdateIntent.STOPPED) - updateIntent.putExtra(EXTRA_PLAY_INFO, audioRequest) - audioEventPresenter.onAyahPlayback(null) - broadcastManager.sendBroadcast(updateIntent) processStopRequest(true) return } @@ -1010,6 +977,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, localPlayer.setDataSource(url) } state = State.Preparing + updateAudioPlaybackStatus() val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) @@ -1070,6 +1038,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, ) player.start() state = State.Playing + updateAudioPlaybackStatus() audioRequest?.playbackSpeed?.let { speed -> processUpdatePlaybackSpeed(speed) } @@ -1326,6 +1295,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean { Timber.e("Error: what=%s, extra=%s", what.toString(), extra.toString()) state = State.Stopped + updateAudioPlaybackStatus() relaxResources(releaseMediaPlayer = true, stopForeground = true) giveUpAudioFocus() return true // true indicates we handled the error 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 f665f8c62e..b9681bbd9a 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 @@ -7,7 +7,6 @@ import android.Manifest; import android.app.ProgressDialog; import android.app.SearchManager; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -65,11 +64,12 @@ import com.quran.labs.androidquran.QuranPreferenceActivity; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.SearchActivity; -import com.quran.labs.androidquran.bridge.AudioEventPresenterBridge; +import com.quran.labs.androidquran.bridge.AudioStatusRepositoryBridge; import com.quran.labs.androidquran.bridge.ReadingEventPresenterBridge; import com.quran.labs.androidquran.common.QuranAyahInfo; import com.quran.labs.androidquran.common.audio.model.QariItem; import com.quran.labs.androidquran.common.audio.model.playback.AudioRequest; +import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository; import com.quran.labs.androidquran.data.Constants; import com.quran.labs.androidquran.data.QuranDataProvider; import com.quran.labs.androidquran.data.QuranDisplayData; @@ -124,7 +124,6 @@ import com.quran.page.common.factory.PageViewFactoryProvider; import com.quran.page.common.toolbar.AyahToolBar; import com.quran.page.common.toolbar.di.AyahToolBarInjector; -import com.quran.reading.common.AudioEventPresenter; import com.quran.reading.common.ReadingEventPresenter; import java.lang.ref.WeakReference; @@ -189,7 +188,6 @@ public class PagerActivity extends AppCompatActivity implements private boolean needsPermissionToDownloadOver3g = true; private AlertDialog promptDialog = null; private AyahToolBar ayahToolBar; - private AudioRequest lastAudioRequest; private boolean isDualPages = false; private View toolBarArea; private FrameLayout overlay; @@ -238,14 +236,14 @@ public class PagerActivity extends AppCompatActivity implements @Inject AudioPresenter audioPresenter; @Inject CurrentQariBridge currentQariBridge; @Inject QuranEventLogger quranEventLogger; - @Inject AudioEventPresenter audioEventPresenter; + @Inject AudioStatusRepository audioStatusRepository; @Inject ReadingEventPresenter readingEventPresenter; @Inject PageViewFactoryProvider pageProviderFactoryProvider; @Inject Set additionalAyahPanels; @Inject PagerActivityRecitationPresenter pagerActivityRecitationPresenter; @Inject TranslationListPresenter translationListPresenter; - private AudioEventPresenterBridge audioEventPresenterBridge; + private AudioStatusRepositoryBridge audioStatusRepositoryBridge; private ReadingEventPresenterBridge readingEventPresenterBridge; private Job translationJob; @@ -288,8 +286,9 @@ public void onCreate(Bundle savedInstanceState) { boolean shouldAdjustPageNumber = false; isDualPages = QuranUtils.isDualPages(this, quranScreenInfo); isSplitScreen = quranSettings.isQuranSplitWithTranslation(); - audioEventPresenterBridge = new AudioEventPresenterBridge( - audioEventPresenter, + audioStatusRepositoryBridge = new AudioStatusRepositoryBridge( + audioStatusRepository, + () -> audioStatusBar, suraAyah -> { onAudioPlaybackAyahChanged(suraAyah); return null; } ); readingEventPresenterBridge = new ReadingEventPresenterBridge( @@ -315,7 +314,6 @@ public void onCreate(Bundle savedInstanceState) { } boolean lastWasDualPages = savedInstanceState.getBoolean(LAST_WAS_DUAL_PAGES, isDualPages); shouldAdjustPageNumber = (lastWasDualPages != isDualPages); - this.lastAudioRequest = savedInstanceState.getParcelable(LAST_AUDIO_REQUEST); } else { Intent intent = getIntent(); Bundle extras = intent.getExtras(); @@ -510,10 +508,6 @@ public void onPageSelected(int position) { } } - LocalBroadcastManager.getInstance(this).registerReceiver( - audioReceiver, - new IntentFilter(AudioService.AudioUpdateIntent.INTENT_NAME)); - downloadReceiver = new DefaultDownloadReceiver(this, QuranDownloadService.DOWNLOAD_TYPE_AUDIO); String action = QuranDownloadNotifier.ProgressIntent.INTENT_NAME; @@ -986,7 +980,6 @@ protected void onDestroy() { clearUiVisibilityListener(); // remove broadcast receivers - LocalBroadcastManager.getInstance(this).unregisterReceiver(audioReceiver); if (downloadReceiver != null) { downloadReceiver.setListener(null); LocalBroadcastManager.getInstance(this) @@ -999,7 +992,7 @@ protected void onDestroy() { } currentQariBridge.unsubscribeAll(); compositeDisposable.dispose(); - audioEventPresenterBridge.dispose(); + audioStatusRepositoryBridge.dispose(); readingEventPresenterBridge.dispose(); handler.removeCallbacksAndMessages(null); dismissProgressDialog(); @@ -1017,9 +1010,6 @@ public void onSaveInstanceState(Bundle state) { state.putBoolean(LAST_READING_MODE_IS_TRANSLATION, showingTranslation); state.putBoolean(LAST_ACTIONBAR_STATE, isActionBarHidden); state.putBoolean(LAST_WAS_DUAL_PAGES, isDualPages); - if (lastAudioRequest != null) { - state.putParcelable(LAST_AUDIO_REQUEST, lastAudioRequest); - } super.onSaveInstanceState(state); } @@ -1275,33 +1265,6 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { } } - private final BroadcastReceiver audioReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent != null) { - int state = intent.getIntExtra( - AudioService.AudioUpdateIntent.STATUS, -1); - int repeatCount = intent.getIntExtra( - AudioService.AudioUpdateIntent.REPEAT_COUNT, -200); - AudioRequest request = intent.getParcelableExtra(AudioService.AudioUpdateIntent.REQUEST); - if (request != null) { - lastAudioRequest = request; - } - if (state == AudioService.AudioUpdateIntent.PLAYING) { - audioStatusBar.switchMode(AudioStatusBar.PLAYING_MODE); - if (repeatCount >= -1) { - audioStatusBar.setRepeatCount(repeatCount); - } - } else if (state == AudioService.AudioUpdateIntent.PAUSED) { - audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE); - } else if (state == AudioService.AudioUpdateIntent.STOPPED) { - audioStatusBar.switchMode(AudioStatusBar.STOPPED_MODE); - lastAudioRequest = null; - } - } - } - }; - @Override public void updateDownloadProgress(int progress, long downloadedSize, long totalSize) { @@ -1560,10 +1523,6 @@ public void handlePlayback(AudioRequest request) { intent.setAction(AudioService.ACTION_PLAYBACK); if (request != null) { intent.putExtra(AudioService.EXTRA_PLAY_INFO, request); - lastAudioRequest = request; - audioStatusBar.setRepeatCount(request.getRepeatInfo()); - audioStatusBar.setSpeed(request.getPlaybackSpeed()); - audioStatusBar.switchMode(AudioStatusBar.LOADING_MODE); } Timber.d("starting service for audio playback"); @@ -1579,6 +1538,7 @@ public void onPausePressed() { @Override public void setPlaybackSpeed(float speed) { + final AudioRequest lastAudioRequest = audioStatusRepositoryBridge.audioRequest(); if (lastAudioRequest != null) { final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), lastAudioRequest.getEnd(), @@ -1594,7 +1554,6 @@ public void setPlaybackSpeed(float speed) { i.setAction(AudioService.ACTION_UPDATE_SETTINGS); i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest); startService(i); - lastAudioRequest = updatedAudioRequest; } } @@ -1639,6 +1598,7 @@ public boolean updatePlayOptions(int rangeRepeat, int verseRepeat, boolean enforceRange, float playbackSpeed) { + final AudioRequest lastAudioRequest = audioStatusRepositoryBridge.audioRequest(); if (lastAudioRequest != null) { final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), lastAudioRequest.getEnd(), @@ -1653,10 +1613,6 @@ public boolean updatePlayOptions(int rangeRepeat, i.setAction(AudioService.ACTION_UPDATE_SETTINGS); i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest); startService(i); - - lastAudioRequest = updatedAudioRequest; - audioStatusBar.setRepeatCount(verseRepeat); - audioStatusBar.setSpeed(playbackSpeed); return true; } else { return false; @@ -1665,6 +1621,7 @@ public boolean updatePlayOptions(int rangeRepeat, @Override public void setRepeatCount(int repeatCount) { + final AudioRequest lastAudioRequest = audioStatusRepositoryBridge.audioRequest(); if (lastAudioRequest != null) { final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), lastAudioRequest.getEnd(), @@ -1680,7 +1637,6 @@ public void setRepeatCount(int repeatCount) { i.setAction(AudioService.ACTION_UPDATE_SETTINGS); i.putExtra(AudioService.EXTRA_PLAY_INFO, updatedAudioRequest); startService(i); - lastAudioRequest = updatedAudioRequest; } } @@ -1688,7 +1644,6 @@ public void setRepeatCount(int repeatCount) { public void onStopPressed() { startService(audioUtils.getAudioIntent(this, AudioService.ACTION_STOP)); audioStatusBar.switchMode(AudioStatusBar.STOPPED_MODE); - lastAudioRequest = null; } @Override @@ -1740,7 +1695,7 @@ private SuraAyah getSelectionEnd() { } public AudioRequest getLastAudioRequest() { - return lastAudioRequest; + return audioStatusRepositoryBridge.audioRequest(); } public void endAyahMode() { diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt index 0b7f45314f..0bd1a4f4f1 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahActionFragment.kt @@ -6,7 +6,8 @@ import com.quran.data.model.SuraAyah import com.quran.data.model.selection.AyahSelection import com.quran.data.model.selection.endSuraAyah import com.quran.data.model.selection.startSuraAyah -import com.quran.reading.common.AudioEventPresenter +import com.quran.labs.androidquran.common.audio.model.playback.currentPlaybackAyah +import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository import com.quran.reading.common.ReadingEventPresenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope @@ -23,7 +24,7 @@ abstract class AyahActionFragment : Fragment() { lateinit var readingEventPresenter: ReadingEventPresenter @Inject - lateinit var audioEventPresenter: AudioEventPresenter + lateinit var audioStatusRepository: AudioStatusRepository protected var start: SuraAyah? = null protected var end: SuraAyah? = null @@ -33,7 +34,8 @@ abstract class AyahActionFragment : Fragment() { scope = MainScope() readingEventPresenter.ayahSelectionFlow - .combine(audioEventPresenter.audioPlaybackAyahFlow) { selectedAyah, playbackAyah -> + .combine(audioStatusRepository.audioPlaybackFlow) { selectedAyah, playbackStatus -> + val playbackAyah = playbackStatus.currentPlaybackAyah() val (previousStart, previousEnd) = start to end if (selectedAyah !is AyahSelection.None) { start = selectedAyah.startSuraAyah() diff --git a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TagBookmarkFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TagBookmarkFragment.kt index fc34229393..b680448b50 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TagBookmarkFragment.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TagBookmarkFragment.kt @@ -5,11 +5,12 @@ import android.os.Bundle import com.quran.data.core.QuranInfo import com.quran.data.model.selection.AyahSelection import com.quran.data.model.selection.startSuraAyah +import com.quran.labs.androidquran.common.audio.model.playback.currentPlaybackAyah +import com.quran.labs.androidquran.common.audio.repository.AudioStatusRepository import com.quran.labs.androidquran.common.toolbar.R import com.quran.labs.androidquran.ui.PagerActivity import com.quran.labs.androidquran.ui.helpers.SlidingPagerAdapter import com.quran.mobile.di.AyahActionFragmentProvider -import com.quran.reading.common.AudioEventPresenter import com.quran.reading.common.ReadingEventPresenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope @@ -25,7 +26,7 @@ class TagBookmarkFragment : TagBookmarkDialog() { lateinit var readingEventPresenter: ReadingEventPresenter @Inject - lateinit var audioEventPresenter: AudioEventPresenter + lateinit var audioStatusRepository: AudioStatusRepository @Inject lateinit var quranInfo: QuranInfo @@ -46,7 +47,8 @@ class TagBookmarkFragment : TagBookmarkDialog() { scope = MainScope() readingEventPresenter.ayahSelectionFlow - .combine(audioEventPresenter.audioPlaybackAyahFlow) { selectedAyah, playbackAyah -> + .combine(audioStatusRepository.audioPlaybackFlow) { selectedAyah, playbackState -> + val playbackAyah = playbackState.currentPlaybackAyah() val start = when { selectedAyah !is AyahSelection.None -> selectedAyah.startSuraAyah() playbackAyah != null -> playbackAyah diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index 053889af7f..3b939eaf02 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -261,7 +261,7 @@ private void showStoppedMode() { private void updateButton() { final TextView currentQariView = qariView; - if (currentQariView != null) { + if (currentQariView != null && currentQari != null) { currentQariView.setText(currentQari.getNameResource()); } } @@ -283,7 +283,7 @@ private void addButton() { dropdownIconView.setOnClickListener(view -> audioBarListener.onShowQariList()); dropdownIconView.setPadding(buttonPadding, 0, buttonPadding, 0); } - qariView.setText(currentQari.getNameResource()); + updateButton(); final ViewGroup.LayoutParams dropdownParams = new ViewGroup.LayoutParams( diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioStatus.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioStatus.kt new file mode 100644 index 0000000000..ba0e42b813 --- /dev/null +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/AudioStatus.kt @@ -0,0 +1,15 @@ +package com.quran.labs.androidquran.common.audio.model.playback + +import com.quran.data.model.SuraAyah + +sealed class AudioStatus { + data object Stopped : AudioStatus() + data class Playback( + val currentAyah: SuraAyah, + val audioRequest: AudioRequest, + val playbackStatus: PlaybackStatus + ) : AudioStatus() +} + +fun AudioStatus.currentPlaybackAyah() = + (this as? AudioStatus.Playback)?.currentAyah diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/PlaybackStatus.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/PlaybackStatus.kt new file mode 100644 index 0000000000..72d3bb37be --- /dev/null +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/model/playback/PlaybackStatus.kt @@ -0,0 +1,5 @@ +package com.quran.labs.androidquran.common.audio.model.playback + +enum class PlaybackStatus { + PREPARING, PLAYING, PAUSED +} diff --git a/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/AudioStatusRepository.kt b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/AudioStatusRepository.kt new file mode 100644 index 0000000000..64e8444150 --- /dev/null +++ b/common/audio/src/main/java/com/quran/labs/androidquran/common/audio/repository/AudioStatusRepository.kt @@ -0,0 +1,19 @@ +package com.quran.labs.androidquran.common.audio.repository + +import com.quran.labs.androidquran.common.audio.model.playback.AudioStatus +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AudioStatusRepository @Inject constructor() { + private val audioPlaybackInternalFlow = MutableStateFlow(AudioStatus.Stopped) + + val audioPlaybackFlow = audioPlaybackInternalFlow.asStateFlow() + + fun updateAyahPlayback(audioStatus: AudioStatus) { + audioPlaybackInternalFlow.value = audioStatus + } +} + diff --git a/common/reading/src/main/java/com/quran/reading/common/AudioEventPresenter.kt b/common/reading/src/main/java/com/quran/reading/common/AudioEventPresenter.kt deleted file mode 100644 index c6e597046e..0000000000 --- a/common/reading/src/main/java/com/quran/reading/common/AudioEventPresenter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.quran.reading.common - -import com.quran.data.model.SuraAyah -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AudioEventPresenter @Inject constructor() { - private val audioPlaybackAyahInternalFlow = MutableStateFlow(null) - - val audioPlaybackAyahFlow: StateFlow = audioPlaybackAyahInternalFlow.asStateFlow() - - fun onAyahPlayback(suraAyah: SuraAyah?) { - if (audioPlaybackAyahInternalFlow.value != suraAyah) { - audioPlaybackAyahInternalFlow.value = suraAyah - } - } - - fun currentPlaybackAyah(): SuraAyah? = audioPlaybackAyahFlow.value -} From 3b8e5841af7cf66eee45ee68e316d5b672b6f78f Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 14 Jan 2024 14:15:27 +0400 Subject: [PATCH 242/258] Replace m2 TopAppBar with m3 Also use default color tokens instead of overriding them. --- .../downloadmanager/AudioManagerActivity.kt | 2 -- .../ui/common/DownloadManagerToolbar.kt | 14 ++++---------- .../ui/sheikhdownload/SheikhDownloadToolbar.kt | 14 ++------------ feature/qarilist/build.gradle | 3 ++- .../mobile/feature/qarilist/QariListWrapper.kt | 15 ++++++--------- 5 files changed, 14 insertions(+), 34 deletions(-) diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/AudioManagerActivity.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/AudioManagerActivity.kt index da0abc9d9a..f9002524af 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/AudioManagerActivity.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/AudioManagerActivity.kt @@ -46,8 +46,6 @@ class AudioManagerActivity : ComponentActivity() { ) { DownloadManagerToolbar( title = stringResource(R.string.audio_manager), - backgroundColor = MaterialTheme.colorScheme.primary, - tintColor = MaterialTheme.colorScheme.onPrimary, onBackPressed = { finish() } ) diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/common/DownloadManagerToolbar.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/common/DownloadManagerToolbar.kt index 7c0c10d46d..2c52ba8872 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/common/DownloadManagerToolbar.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/common/DownloadManagerToolbar.kt @@ -1,23 +1,20 @@ package com.quran.mobile.feature.downloadmanager.ui.common import androidx.compose.foundation.layout.RowScope -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import com.quran.labs.androidquran.common.ui.core.modifier.autoMirror @Composable fun DownloadManagerToolbar( title: String, - backgroundColor: Color, - tintColor: Color, onBackPressed: (() -> Unit), actions: @Composable (RowScope.() -> Unit) = {} ) { @@ -25,7 +22,6 @@ fun DownloadManagerToolbar( title = { Text( text = title, - color = tintColor, maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -35,12 +31,10 @@ fun DownloadManagerToolbar( Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = "", - tint = tintColor, modifier = Modifier.autoMirror() ) } }, - backgroundColor = backgroundColor, actions = actions ) } diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/SheikhDownloadToolbar.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/SheikhDownloadToolbar.kt index 7152464321..6a31bd5037 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/SheikhDownloadToolbar.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/sheikhdownload/SheikhDownloadToolbar.kt @@ -5,7 +5,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -22,19 +21,13 @@ fun SheikhDownloadToolbar( eraseAction: (() -> Unit), onBackAction: (() -> Unit) ) { - val backgroundColor = - if (isContextual) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.primary - val tintColor = - if (isContextual) MaterialTheme.colorScheme.onSecondary else MaterialTheme.colorScheme.onPrimary - val actions: @Composable() (RowScope.() -> Unit) = { if (downloadIcon) { IconButton(onClick = downloadAction) { val contentDescription = if (isContextual) R.string.audio_manager_download_selection else R.string.audio_manager_download_all Icon( painterResource(id = R.drawable.ic_download), - contentDescription = stringResource(id = contentDescription), - tint = tintColor + contentDescription = stringResource(id = contentDescription) ) } } @@ -43,8 +36,7 @@ fun SheikhDownloadToolbar( IconButton(onClick = eraseAction) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(id = R.string.audio_manager_delete_selection), - tint = tintColor + contentDescription = stringResource(id = R.string.audio_manager_delete_selection) ) } } @@ -52,8 +44,6 @@ fun SheikhDownloadToolbar( DownloadManagerToolbar( title = if (isContextual) "" else stringResource(titleResource), - backgroundColor = backgroundColor, - tintColor = tintColor, onBackPressed = onBackAction, actions = actions ) diff --git a/feature/qarilist/build.gradle b/feature/qarilist/build.gradle index 9ff392439c..1960c35131 100644 --- a/feature/qarilist/build.gradle +++ b/feature/qarilist/build.gradle @@ -9,7 +9,8 @@ android { kotlinOptions { freeCompilerArgs += [ "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", - "-opt-in=androidx.compose.material.ExperimentalMaterialApi" + "-opt-in=androidx.compose.material.ExperimentalMaterialApi", + "-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api" ] } } diff --git a/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/QariListWrapper.kt b/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/QariListWrapper.kt index 2d7eb2c307..81dc76c956 100644 --- a/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/QariListWrapper.kt +++ b/feature/qarilist/src/main/kotlin/com/quran/mobile/feature/qarilist/QariListWrapper.kt @@ -13,17 +13,17 @@ import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.ModalBottomSheetDefaults import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetValue -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -129,20 +129,17 @@ class QariListWrapper( sheetContent = { Column { TopAppBar( - backgroundColor = MaterialTheme.colorScheme.primary, title = { Text( stringResource(R.string.qarilist_select_qari), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primaryContainer + style = MaterialTheme.typography.titleLarge ) }, navigationIcon = { IconButton(onClick = { closeDialog() }) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(R.string.qarilist_dismiss), - tint = MaterialTheme.colorScheme.primaryContainer + contentDescription = stringResource(R.string.qarilist_dismiss) ) } } From b5de994098345af947f1ebb008395f65e9984184 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 14 Jan 2024 14:44:32 +0400 Subject: [PATCH 243/258] Use compose convention plugin for freeCompilerArgs --- .../kotlin/AndroidLibraryComposeConventionPlugin.kt | 11 +++++++++++ feature/downloadmanager/build.gradle | 11 +---------- .../downloadmanager/ui/SheikhDownloadSummary.kt | 2 -- feature/qarilist/build.gradle | 12 +----------- 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 49a19435b3..cfb9e005dc 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -5,8 +5,10 @@ import com.quran.labs.androidquran.buildutil.applyKotlinCommon import com.quran.labs.androidquran.buildutil.withLibraries import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.plugins.ExtensionAware import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions class AndroidLibraryComposeConventionPlugin : Plugin { @@ -25,6 +27,15 @@ class AndroidLibraryComposeConventionPlugin : Plugin { withLibraries { libs -> composeOptions.kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } + + (this as ExtensionAware).extensions.configure("kotlinOptions") { + freeCompilerArgs += listOf( + "-opt-in=androidx.compose.ui.ExperimentalComposeUiApi", + "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", + "-opt-in=androidx.compose.material.ExperimentalMaterialApi", + "-opt-in=androidx.compose.material3.ExperimentalMaterial3Api" + ) + } } applyKotlinCommon() diff --git a/feature/downloadmanager/build.gradle b/feature/downloadmanager/build.gradle index 407d9cf03b..aa2d815065 100644 --- a/feature/downloadmanager/build.gradle +++ b/feature/downloadmanager/build.gradle @@ -3,16 +3,7 @@ plugins { id 'com.squareup.anvil' } -android { - namespace 'com.quran.mobile.feature.downloadmanager' - kotlinOptions { - freeCompilerArgs += [ - "-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi", - "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi", - "-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api" - ] - } -} +android.namespace 'com.quran.mobile.feature.downloadmanager' anvil { generateDaggerFactories = true } diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/SheikhDownloadSummary.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/SheikhDownloadSummary.kt index 5b2ffae4a3..e29c9f5b92 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/SheikhDownloadSummary.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/ui/SheikhDownloadSummary.kt @@ -9,7 +9,6 @@ import androidx.compose.material.Surface import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -26,7 +25,6 @@ import com.quran.mobile.feature.downloadmanager.R import com.quran.mobile.feature.downloadmanager.model.DownloadedSheikhUiModel import com.quran.mobile.feature.downloadmanager.ui.common.DownloadCommonRow -@OptIn(ExperimentalComposeUiApi::class) @Composable fun SheikhDownloadSummary( downloadedSheikhUiModel: DownloadedSheikhUiModel, diff --git a/feature/qarilist/build.gradle b/feature/qarilist/build.gradle index 1960c35131..f70d441e83 100644 --- a/feature/qarilist/build.gradle +++ b/feature/qarilist/build.gradle @@ -3,17 +3,7 @@ plugins { id 'com.squareup.anvil' } -android { - namespace 'com.quran.mobile.feature.qarilist' - - kotlinOptions { - freeCompilerArgs += [ - "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi", - "-opt-in=androidx.compose.material.ExperimentalMaterialApi", - "-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api" - ] - } -} +android.namespace 'com.quran.mobile.feature.qarilist' anvil { generateDaggerFactories = true } From 4284375c61bf57cd0c10fc995de664ee59f0766a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 14 Jan 2024 15:48:03 +0000 Subject: [PATCH 244/258] Update dependency org.mockito:mockito-core to v5.9.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7929e176d1..26bfc616f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ googleCloudSpeechVersion = "4.28.0" junitVersion = "4.13.2" espressoVersion = "3.5.1" truthVersion = "1.2.0" -mockitoVersion = "5.8.0" +mockitoVersion = "5.9.0" robolectricVersion = "4.11.1" turbineVersion = "1.0.0" From ee824fe232172029992f7c06e2ed643af6ad2d95 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 20:05:14 +0000 Subject: [PATCH 245/258] Update dependency com.google.firebase:firebase-crashlytics to v18.6.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26bfc616f9..3970fdb5dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ androidxWorkManagerVersion = "2.9.0" # firebase firebaseAnalyticsVersion = "21.5.0" -firebaseCrashlyticsVersion = "18.6.0" +firebaseCrashlyticsVersion = "18.6.1" # ui libraries accompanistVersion = "0.32.0" From 0b1415e65bcbabe58aa797d2b4dc628bf5371bb3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 01:32:51 +0000 Subject: [PATCH 246/258] Update dependency com.google.devtools.ksp to v1.9.22-1.0.17 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26bfc616f9..ee832b997d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.2.1" kotlin = "1.9.22" -ksp = "1.9.22-1.0.16" +ksp = "1.9.22-1.0.17" # required within the Gradle convention plugins - not unused compose-compiler = "1.5.8" From 430b6585d7f23404be171e53c07b7ba1a12599b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 23:26:17 +0000 Subject: [PATCH 247/258] Update dependency com.google.truth:truth to v1.3.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26bfc616f9..4aa486d6fd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,7 +65,7 @@ googleCloudSpeechVersion = "4.28.0" # testing junitVersion = "4.13.2" espressoVersion = "3.5.1" -truthVersion = "1.2.0" +truthVersion = "1.3.0" mockitoVersion = "5.9.0" robolectricVersion = "4.11.1" turbineVersion = "1.0.0" From ddc743cef0bde7fa7f124529e1cdccc3504cd2c2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:51:04 +0000 Subject: [PATCH 248/258] Update agp to v8.2.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26bfc616f9..baa2f42500 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.2.1" +agp = "8.2.2" kotlin = "1.9.22" ksp = "1.9.22-1.0.16" From fe6fdd01dc608004a16fd96568d94d725a4adf71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 04:02:26 +0000 Subject: [PATCH 249/258] Update dependency com.google.cloud:google-cloud-speech to v4.29.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26bfc616f9..dee4533800 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,7 @@ reorderableComposeVersion = "0.9.6" # recitations grpcOkhttpVersion = "1.61.0" googleAuthVersion = "1.22.0" -googleCloudSpeechVersion = "4.28.0" +googleCloudSpeechVersion = "4.29.0" # testing junitVersion = "4.13.2" From 62ed0742d14099daf66edc528774f78bb34cb952 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 18:57:13 +0000 Subject: [PATCH 250/258] Update dependency androidx.compose:compose-bom to v2024 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 26bfc616f9..3198cda80a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ ksp = "1.9.22-1.0.16" # required within the Gradle convention plugins - not unused compose-compiler = "1.5.8" -composeBomVersion = "2023.10.01" +composeBomVersion = "2024.01.00" okhttpBomVersion = "4.12.0" # dependencies From 9c2bf14074764e3303eb51042b38183520ab1838 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Thu, 25 Jan 2024 01:10:56 +0400 Subject: [PATCH 251/258] Share AGP and Kotlin versions with build-logic --- build-logic/convention/build.gradle.kts | 4 ++-- gradle/libs.versions.toml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 1464b5c323..191d051a97 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -5,8 +5,8 @@ plugins { group = "com.quran.labs.androidquran.buildlogic" dependencies { - compileOnly("com.android.tools.build:gradle:8.2.1") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.kotlin.gradlePlugin) implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7929e176d1..d7b11ebb32 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -172,6 +172,10 @@ truth = { module = "com.google.truth:truth", version.ref = "truthVersion" } turbine = { module = "app.cash.turbine:turbine-jvm", version.ref = "turbineVersion" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesVersion" } +# build-logic +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } From fef813899a6ee13e0d3e5db7aa5385481e980481 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jan 2024 22:33:38 +0000 Subject: [PATCH 252/258] Update dependency org.mockito:mockito-core to v5.10.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6c648e863a..009385bf47 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,7 +66,7 @@ googleCloudSpeechVersion = "4.28.0" junitVersion = "4.13.2" espressoVersion = "3.5.1" truthVersion = "1.3.0" -mockitoVersion = "5.9.0" +mockitoVersion = "5.10.0" robolectricVersion = "4.11.1" turbineVersion = "1.0.0" From 7ac5d84d81b4d0be4685422540d22ec2b7b68cc4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 02:01:43 +0000 Subject: [PATCH 253/258] Update dependency com.google.accompanist:accompanist-flowlayout to v0.34.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 009385bf47..a7236c2edb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,7 +49,7 @@ firebaseAnalyticsVersion = "21.5.0" firebaseCrashlyticsVersion = "18.6.1" # ui libraries -accompanistVersion = "0.32.0" +accompanistVersion = "0.34.0" balloonVersion = "1.6.4" tooltipComposeVersion = "0.2.0" insetterVersion = "0.6.1" From 3cfd027e639d2980a5ac562be4fd16963aa3fe82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jan 2024 23:24:45 +0000 Subject: [PATCH 254/258] Update gradle/gradle-build-action action to v3 --- .github/workflows/build.yml | 8 ++++---- .github/workflows/post_merge.yml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cb07bb99dd..92bc54bd1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 - name: Build debug APK run: ./gradlew assembleMadaniDebug @@ -65,7 +65,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 - name: Run lint run: ./gradlew lintMadaniDebug @@ -92,7 +92,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 - name: Run SqlDelight migration tests run: ./gradlew verifySqlDelightMigration @@ -122,7 +122,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 - name: Get dependencies list run: ./gradlew :app:dependencies --configuration madaniReleaseRuntimeClasspath > current_dependencies.txt diff --git a/.github/workflows/post_merge.yml b/.github/workflows/post_merge.yml index d446cf29f0..cbf51911ec 100644 --- a/.github/workflows/post_merge.yml +++ b/.github/workflows/post_merge.yml @@ -24,7 +24,7 @@ jobs: java-version: 17 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 - name: Build debug Apk run: ./gradlew assembleDebug From a4f6281b78ce819af6d5cbe8839cfd44cefb876d Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sat, 27 Jan 2024 23:57:58 +0400 Subject: [PATCH 255/258] Add padding at the bottom of the audio panel On devices using the older navigation bar, the "apply" button is often obscured in portrait mode. This fixes it by adding padding underneath it. --- app/src/main/res/layout-ar-land-v17/audio_panel.xml | 1 - app/src/main/res/layout-ar-land/audio_panel.xml | 1 - app/src/main/res/layout-ar-v17/audio_panel.xml | 6 +++++- app/src/main/res/layout-ar/audio_panel.xml | 6 +++++- app/src/main/res/layout-land/audio_panel.xml | 1 - app/src/main/res/layout/audio_panel.xml | 6 +++++- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout-ar-land-v17/audio_panel.xml b/app/src/main/res/layout-ar-land-v17/audio_panel.xml index d6cc338f03..e86febd65f 100644 --- a/app/src/main/res/layout-ar-land-v17/audio_panel.xml +++ b/app/src/main/res/layout-ar-land-v17/audio_panel.xml @@ -1,7 +1,6 @@ diff --git a/app/src/main/res/layout-ar-v17/audio_panel.xml b/app/src/main/res/layout-ar-v17/audio_panel.xml index da13bca647..54f9e6c631 100644 --- a/app/src/main/res/layout-ar-v17/audio_panel.xml +++ b/app/src/main/res/layout-ar-v17/audio_panel.xml @@ -1,7 +1,6 @@ + + diff --git a/app/src/main/res/layout-ar/audio_panel.xml b/app/src/main/res/layout-ar/audio_panel.xml index 9ca85453aa..17d5744c25 100644 --- a/app/src/main/res/layout-ar/audio_panel.xml +++ b/app/src/main/res/layout-ar/audio_panel.xml @@ -1,7 +1,6 @@ + + diff --git a/app/src/main/res/layout-land/audio_panel.xml b/app/src/main/res/layout-land/audio_panel.xml index b50ecf16c5..e27fd5c63e 100644 --- a/app/src/main/res/layout-land/audio_panel.xml +++ b/app/src/main/res/layout-land/audio_panel.xml @@ -1,7 +1,6 @@ + + From e83798a7c4ce66290a9be8dcc2f0d155dad6a59f Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 28 Jan 2024 00:45:24 +0400 Subject: [PATCH 256/258] Fixes for a plethora of crashes --- .../labs/androidquran/QuranDataActivity.kt | 9 ++- .../pageselect/PageSelectPresenter.kt | 31 +++++++-- .../labs/androidquran/service/AudioService.kt | 63 ++++++++++++++----- .../labs/androidquran/ui/PagerActivity.java | 8 ++- .../androidquran/view/AudioStatusBar.java | 5 +- .../androidquran/view/CurrentQariBridge.kt | 14 ++--- .../view/HighlightingImageView.java | 26 ++++---- .../mobile/bookmark/model/BookmarkModel.kt | 9 +-- 8 files changed, 117 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt index a0ff0b8952..cda14007d4 100644 --- a/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt +++ b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt @@ -631,9 +631,16 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio if (downloadReceiver != null && downloadReceiver!!.didReceiveBroadcast() && !force) { return } + val dataStatus = quranDataStatus + if (dataStatus == null) { + // we lost the cached data status, so just check again + checkPages() + return + } + var url: String - url = if (dataStatus!!.needPortrait() && !dataStatus.needLandscape()) { + url = if (dataStatus.needPortrait() && !dataStatus.needLandscape()) { // phone (and tablet when upgrading on some devices, ex n10) quranFileUtils.zipFileUrl } else if (dataStatus.needLandscape() && !dataStatus.needPortrait()) { diff --git a/app/src/main/java/com/quran/labs/androidquran/pageselect/PageSelectPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/pageselect/PageSelectPresenter.kt index 19e8b43e8d..f0c8c6ee3f 100644 --- a/app/src/main/java/com/quran/labs/androidquran/pageselect/PageSelectPresenter.kt +++ b/app/src/main/java/com/quran/labs/androidquran/pageselect/PageSelectPresenter.kt @@ -101,14 +101,33 @@ constructor( val updatedBookmarks = bookmarksDao.bookmarks() .map { val page = it.page - val (pageSura, pageAyah) = suraAyahFromPage(page) - val sura = it.sura ?: pageSura - val ayah = it.ayah ?: pageAyah + if (page - 1 >= sourcePageSuraStart.size) { + if (it.isPageBookmark()) { + // this bookmark is on a page that doesn't exist in the old page type + if (destination.suraForPageArray.size > page) { + // but it does exist on the new type, so it's ok, let's not re-map + it + } else { + // we can't map it, so let's just put it as the max page number to avoid bad data + it.copy(page = sourcePageAyahStart.size - 1) + } + } else { + // ayah bookmark, so let's just map it + val sura = requireNotNull(it.sura) + val ayah = requireNotNull(it.ayah) + val mappedPage = destinationQuranInfo.getPageFromSuraAyah(sura, ayah) + it.copy(page = mappedPage) + } + } else { + val (pageSura, pageAyah) = suraAyahFromPage(page) + val sura = it.sura ?: pageSura + val ayah = it.ayah ?: pageAyah - val mappedPage = destinationQuranInfo.getPageFromSuraAyah(sura, ayah) + val mappedPage = destinationQuranInfo.getPageFromSuraAyah(sura, ayah) - // we only copy the page because sura and ayah are the same. - it.copy(page = mappedPage) + // we only copy the page because sura and ayah are the same. + it.copy(page = mappedPage) + } } if (updatedBookmarks.isNotEmpty()) { diff --git a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index fbf812b05b..b7586b2d0e 100644 --- a/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt +++ b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt @@ -221,6 +221,13 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, localPlayer.setOnCompletionListener(this) localPlayer.setOnErrorListener(this) localPlayer.setOnSeekCompleteListener(this) + + val audioAttributes = AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + localPlayer.setAudioAttributes(audioAttributes) + mediaSession.isActive = true localPlayer } else { @@ -979,12 +986,6 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, state = State.Preparing updateAudioPlaybackStatus() - val audioAttributes = AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_MEDIA) - .build() - localPlayer.setAudioAttributes(audioAttributes) - // starts preparing the media player in the background. When it's // done, it will call our OnPreparedListener (that is, the // onPrepared() method on this class, since we set the listener @@ -1018,15 +1019,47 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } val builder = PlaybackStateCompat.Builder() builder.setState(state, position, 1.0f) - builder.setActions( - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_STOP or - PlaybackStateCompat.ACTION_REWIND or - PlaybackStateCompat.ACTION_FAST_FORWARD or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or - PlaybackStateCompat.ACTION_SKIP_TO_NEXT - ) + + val actions = when (state) { + PlaybackStateCompat.STATE_PLAYING -> { + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_REWIND or + PlaybackStateCompat.ACTION_FAST_FORWARD or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT + } + PlaybackStateCompat.STATE_PAUSED -> { + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_REWIND or + PlaybackStateCompat.ACTION_FAST_FORWARD or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT + } + PlaybackStateCompat.STATE_STOPPED -> { + PlaybackStateCompat.ACTION_PLAY + } + PlaybackStateCompat.STATE_CONNECTING -> { + PlaybackStateCompat.ACTION_STOP + } + PlaybackStateCompat.STATE_REWINDING -> { + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_REWIND or + PlaybackStateCompat.ACTION_FAST_FORWARD or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT + } + PlaybackStateCompat.STATE_SKIPPING_TO_NEXT -> { + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_FAST_FORWARD or + PlaybackStateCompat.ACTION_REWIND + } + else -> { PlaybackStateCompat.ACTION_STOP } + } + builder.setActions(actions) mediaSession.setPlaybackState(builder.build()) } 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 b9681bbd9a..fa9f70df5c 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 @@ -759,8 +759,12 @@ public void onResume() { foregroundDisposable.add(Completable.timer(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(() -> { - startService( - audioUtils.getAudioIntent(PagerActivity.this, AudioService.ACTION_CONNECT)); + try { + startService( + audioUtils.getAudioIntent(PagerActivity.this, AudioService.ACTION_CONNECT)); + } catch (IllegalStateException ise) { + // we're likely in the background, so ignore. + } shouldReconnect = false; })); } diff --git a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java index 3b939eaf02..f83ebd4a70 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/AudioStatusBar.java @@ -261,8 +261,9 @@ private void showStoppedMode() { private void updateButton() { final TextView currentQariView = qariView; - if (currentQariView != null && currentQari != null) { - currentQariView.setText(currentQari.getNameResource()); + final Qari qari = currentQari; + if (currentQariView != null && qari != null) { + currentQariView.setText(qari.getNameResource()); } } diff --git a/app/src/main/java/com/quran/labs/androidquran/view/CurrentQariBridge.kt b/app/src/main/java/com/quran/labs/androidquran/view/CurrentQariBridge.kt index 3d19db653d..332eab05d9 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/CurrentQariBridge.kt +++ b/app/src/main/java/com/quran/labs/androidquran/view/CurrentQariBridge.kt @@ -10,20 +10,20 @@ import kotlinx.coroutines.launch import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.observeOn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext class CurrentQariBridge @Inject constructor(private val currentQariManager: CurrentQariManager) { private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) fun listenToQaris(lambda: ((Qari) -> Unit)) { - scope.launch { - withContext(Dispatchers.Main) { - currentQariManager - .flow() - .collect { lambda(it) } - } - } + currentQariManager + .flow() + .onEach { lambda(it) } + .flowOn(Dispatchers.Main) + .launchIn(scope) } fun unsubscribeAll() { diff --git a/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java b/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java index 6f48e83e65..33ce5a2bf4 100644 --- a/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java +++ b/app/src/main/java/com/quran/labs/androidquran/view/HighlightingImageView.java @@ -1,5 +1,11 @@ package com.quran.labs.androidquran.view; +import static com.quran.data.model.highlight.HighlightType.Mode.BACKGROUND; +import static com.quran.data.model.highlight.HighlightType.Mode.COLOR; +import static com.quran.data.model.highlight.HighlightType.Mode.HIDE; +import static com.quran.data.model.highlight.HighlightType.Mode.HIGHLIGHT; +import static com.quran.data.model.highlight.HighlightType.Mode.UNDERLINE; + import android.animation.Animator; import android.animation.ValueAnimator; import android.content.Context; @@ -15,6 +21,11 @@ import android.graphics.drawable.Drawable; import android.util.AttributeSet; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.content.ContextCompat; +import androidx.core.view.DisplayCutoutCompat; + import com.quran.data.model.highlight.HighlightType; import com.quran.labs.androidquran.R; import com.quran.labs.androidquran.data.Constants; @@ -37,19 +48,8 @@ import java.util.SortedMap; import java.util.TreeMap; -import androidx.annotation.NonNull; -import androidx.appcompat.widget.AppCompatImageView; -import androidx.core.content.ContextCompat; -import androidx.core.view.DisplayCutoutCompat; import dev.chrisbanes.insetter.Insetter; -import static com.quran.data.model.highlight.HighlightType.Mode.BACKGROUND; -import static com.quran.data.model.highlight.HighlightType.Mode.COLOR; -import static com.quran.data.model.highlight.HighlightType.Mode.HIDE; -import static com.quran.data.model.highlight.HighlightType.Mode.HIGHLIGHT; -import static com.quran.data.model.highlight.HighlightType.Mode.UNDERLINE; -import java.lang.Math; - public class HighlightingImageView extends AppCompatImageView { // for debugging / visualizing glyph bounds: // when enabled, will draw bounds around each glyph to visualize the glyph bounds @@ -276,6 +276,10 @@ private void highlightFloatableAyah(Set highlights, AyahHighlight final TransitionAyahHighlight transitionHighlight = new TransitionAyahHighlight(sourceHighlight, destinationHighlight); + if (startingBounds == null) { + startingBounds = new ArrayList<>(); + } + // yes we make copies, because normalizing the bounds will change them List sourceBounds = new ArrayList<>(startingBounds); diff --git a/common/bookmark/src/main/java/com/quran/mobile/bookmark/model/BookmarkModel.kt b/common/bookmark/src/main/java/com/quran/mobile/bookmark/model/BookmarkModel.kt index 4c31a56c9a..6729cc8e37 100644 --- a/common/bookmark/src/main/java/com/quran/mobile/bookmark/model/BookmarkModel.kt +++ b/common/bookmark/src/main/java/com/quran/mobile/bookmark/model/BookmarkModel.kt @@ -2,7 +2,6 @@ package com.quran.mobile.bookmark.model import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList -import app.cash.sqldelight.coroutines.mapToOneOrNull import com.quran.data.model.SuraAyah import com.quran.data.model.bookmark.Bookmark import com.quran.labs.androidquran.BookmarksDatabase @@ -21,10 +20,12 @@ class BookmarkModel @Inject constructor(bookmarksDatabase: BookmarksDatabase) { .mapToList(Dispatchers.IO) suspend fun isSuraAyahBookmarked(suraAyah: SuraAyah): Pair { - val bookmarkId = bookmarkQueries.getBookmarkIdForSuraAyah(suraAyah.sura, suraAyah.ayah) + val bookmarkIds = bookmarkQueries.getBookmarkIdForSuraAyah(suraAyah.sura, suraAyah.ayah) .asFlow() - .mapToOneOrNull(Dispatchers.IO) + // was .mapToOneOrNull, but some people have multiple bookmarks for the same ayah + // should try to figure out why at some point or otherwise de-duplicate them + .mapToList(Dispatchers.IO) .first() - return suraAyah to (bookmarkId != null) + return suraAyah to bookmarkIds.isNotEmpty() } } From 4d2a3516064a788d4f9ac38ccc37dca82c7c1a38 Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 28 Jan 2024 17:23:13 +0400 Subject: [PATCH 257/258] Bump version to 3.4.4 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9509f1ce48..701e1c7fca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { namespace 'com.quran.labs.androidquran' defaultConfig { - versionCode 3430 - versionName "3.4.3" + versionCode 3440 + versionName "3.4.4" testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" } From 60ce69fa7176dd2095304032fa63560a9cf3379c Mon Sep 17 00:00:00 2001 From: Ahmed El-Helw Date: Sun, 28 Jan 2024 17:30:38 +0400 Subject: [PATCH 258/258] Revert "Bump version to 3.4.4" This reverts commit 4d2a3516064a788d4f9ac38ccc37dca82c7c1a38. This is due to a binary incompatible change in Compose: https://issuetracker.google.com/issues/322214617 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 701e1c7fca..9509f1ce48 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { namespace 'com.quran.labs.androidquran' defaultConfig { - versionCode 3440 - versionName "3.4.4" + versionCode 3430 + versionName "3.4.3" testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" }