diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca400154ac..92bc54bd1d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,22 +14,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu 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 - 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 @@ -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 @@ -56,16 +56,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 - name: Run lint run: ./gradlew lintMadaniDebug - name: Upload lint results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: lint_report @@ -77,16 +83,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu 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 @@ -95,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 @@ -107,22 +113,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu 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 - 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 @@ -137,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 @@ -148,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_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 }} diff --git a/.github/workflows/post_merge.yml b/.github/workflows/post_merge.yml index 2fa5781982..cbf51911ec 100644 --- a/.github/workflows/post_merge.yml +++ b/.github/workflows/post_merge.yml @@ -15,22 +15,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: zulu 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 - 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 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). diff --git a/app/build.gradle b/app/build.gradle index 40b3844ded..9509f1ce48 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") && @@ -16,11 +17,13 @@ android { namespace 'com.quran.labs.androidquran' defaultConfig { - versionCode 3320 - versionName "3.3.2" + versionCode 3430 + versionName "3.4.3" testInstrumentationRunner "com.quran.labs.androidquran.core.QuranTestRunner" } + buildFeatures.buildConfig = true + signingConfigs { release { storeFile file(STORE_FILE) @@ -30,7 +33,7 @@ android { } } - flavorDimensions "pageType" + flavorDimensions = ["pageType"] productFlavors { madani { applicationId "com.quran.labs.androidquran" @@ -58,7 +61,7 @@ android { } } - applicationVariants.all { variant -> + applicationVariants.configureEach { variant -> resValue "string", "authority", applicationId + '.data.QuranDataProvider' resValue "string", "file_authority", applicationId + '.fileprovider' if (applicationId.endsWith("debug")) { @@ -84,12 +87,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'] @@ -120,10 +117,12 @@ 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') implementation project(path: ':common:toolbar') + implementation project(path: ':common:translation') implementation project(path: ':common:upgrade') implementation project(path: ':common:ui:core') @@ -131,57 +130,57 @@ 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.6' - 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 + ksp(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.10' - implementation 'com.google.firebase:firebase-crashlytics:18.3.6' + 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 + testImplementation libs.turbine + testImplementation libs.kotlinx.coroutines.test - errorprone 'com.google.errorprone:error_prone_core:2.18.0' + errorprone libs.errorprone.core // Number Picker - implementation 'io.github.ShawnLin013:number-picker:2.4.13' + implementation libs.number.picker } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f7de6f0d36..2a87047ecc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,13 @@ + + + + + + + @@ -152,11 +159,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/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/QuranDataActivity.kt b/app/src/main/java/com/quran/labs/androidquran/QuranDataActivity.kt index d851ee3473..cda14007d4 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() { @@ -158,8 +165,7 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio errorDialog?.dismiss() errorDialog = null - updateDialog?.dismiss() - updateDialog = null + hideMigrationDialog() scope.cancel() super.onPause() @@ -238,12 +244,24 @@ 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) + .setCancelable(false) .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 +275,7 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio } private fun checkPages() { + showMigrationDialog() quranDataPresenter.checkPages() } @@ -268,6 +287,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, @@ -306,6 +333,8 @@ class QuranDataActivity : Activity(), SimpleDownloadListener, OnRequestPermissio runListViewWithoutPages() } } + } else if (requestCode == REQUEST_POST_NOTIFICATION_PERMISSIONS) { + actuallyDownloadQuranImages(lastForceValue) } } @@ -382,15 +411,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()) { @@ -574,15 +601,46 @@ 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 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()) { @@ -681,6 +739,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/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/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/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/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/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/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/data/Constants.kt b/app/src/main/java/com/quran/labs/androidquran/data/Constants.kt index e398b58dac..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 @@ -98,4 +98,6 @@ 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" + const val PREF_SHOW_LINE_DIVIDERS = "showLineDividers" } 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 e7da9e8bb6..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.java +++ /dev/null @@ -1,280 +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 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 java.util.List; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -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.getTranslations(); - } - - 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..e8785b7c20 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/data/QuranDataProvider.kt @@ -0,0 +1,261 @@ +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 kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +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 runBlocking { translationsDBAdapter.getTranslations().first() } + } + + 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 + } + } +} 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..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 @@ -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) } @@ -148,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/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/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..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) @@ -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/database/TranslationsDBAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBAdapter.kt index 38924fbd5f..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 @@ -1,166 +1,79 @@ 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 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 timber.log.Timber - -import androidx.annotation.WorkerThread +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.withContext import javax.inject.Inject import javax.inject.Singleton @Singleton class TranslationsDBAdapter @Inject constructor( - private val context: Context, - adapter: TranslationsDBHelper, + @ApplicationContext private val context: Context, + private val dataSource: TranslationsDataSource, private val quranFileUtils: QuranFileUtils ) { - private val db: SQLiteDatabase = adapter.writableDatabase - - @Volatile - private var cachedTranslations: List? = null - - var lastWriteTime: Long = 0 - private set - fun getTranslationsHash(): SparseArray { - val result = SparseArray() - for (item in getTranslations()) { - result.put(item.id, item) - } - return result + fun getTranslations(): Flow> { + return dataSource.translations() + .filterNotNull() + .map { translations -> + translations.filter { quranFileUtils.hasTranslation(context, it.filename) } + } } - // 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 - ) - ) - } + 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) } - } finally { - cursor?.close() - } - - items = Collections.unmodifiableList(items) - if (items.size > 0) { - cachedTranslations = items + result } - return items } - fun deleteTranslationByFile(filename: String) { - db.execSQL("DELETE FROM " + TranslationsTable.TABLE_NAME + " WHERE " + - TranslationsTable.FILENAME + " = ?", arrayOf(filename)) + suspend fun deleteTranslationByFileName(filename: String) { + dataSource.removeTranslation(filename) } - fun writeTranslationUpdates(updates: List): Boolean { - var result = true - db.beginTransaction() + suspend fun writeTranslationUpdates(updates: List): Boolean { + 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 + } - 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 + val items = if (needNextOrder) { + var nextOrderNumber = nextOrder + available.map { item -> + if (item.displayOrder == -1) { + item.copy(displayOrder = nextOrderNumber++) } 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++ - } + item } - - 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) - - db.replace(TranslationsTable.TABLE_NAME, null, values) - } else { - db.delete(TranslationsTable.TABLE_NAME, - TranslationsTable.ID + " = " + item.translation.id, null) } + } else { + available } - 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() - } + dataSource.updateTranslations(items.map { it.asLocalTranslation() }) + dataSource.removeTranslationsById(unavailable.map { it.translation.id.toLong() }) - return result + 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 9f6fa816df..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/database/TranslationsDBHelper.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.quran.labs.androidquran.database - -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class TranslationsDBHelper @Inject constructor(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/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 { 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 248dfdeed3..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.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.BindsInstance import dagger.Subcomponent @QuranPageScope -@MergeSubcomponent(QuranReadingPageScope::class, modules = [QuranPageModule::class]) -interface QuranPageComponent: QuranPageExtrasComponent { +@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..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 @@ -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,11 +53,12 @@ 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 + @Singleton fun provideSettings(settingsImpl: SettingsImpl): Settings { return settingsImpl } @@ -94,8 +90,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..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 @@ -1,25 +1,27 @@ 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 io.reactivex.rxjava3.core.Single +import com.quran.mobile.di.qualifier.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext 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 ) { - fun getArabicFromDatabase(verses: VerseRange): Single> { + suspend fun getArabicFromDatabase(verses: VerseRange): List { return getVersesFromDatabase( verses, QuranDataProvider.QURAN_ARABIC_DATABASE, @@ -28,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/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/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/audio/AudioPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/AudioPresenter.kt index 43536c5264..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 @@ -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,17 +63,25 @@ 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) } } - 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 +93,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/presenter/audio/service/AudioQueue.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/audio/service/AudioQueue.kt index 24a252e2c0..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 @@ -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 @@ -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/bookmark/BookmarkPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/bookmark/BookmarkPresenter.java index 31a6188f78..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; @@ -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,10 +62,9 @@ public class BookmarkPresenter implements Presenter { private DisposableSingleObserver pendingRemoval; private List itemsToRemove; - private final int totalPages; @Inject - BookmarkPresenter(Context appContext, + BookmarkPresenter(@ApplicationContext Context appContext, BookmarkModel bookmarkModel, QuranSettings quranSettings, ArabicDatabaseUtils arabicDatabaseUtils, @@ -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/data/QuranDataPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/data/QuranDataPresenter.kt index 3904cb0338..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 @@ -115,7 +116,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/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/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/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/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..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 @@ -16,8 +16,9 @@ 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.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 @@ -32,9 +33,9 @@ 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 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 -> @@ -371,7 +372,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 +391,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/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..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,8 +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 import com.quran.labs.androidquran.database.TranslationsDBAdapter @@ -14,63 +12,74 @@ 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 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 - -internal open class BaseTranslationPresenter internal constructor( +import com.quran.mobile.translation.model.LocalTranslation +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, private val translationsAdapter: TranslationsDBAdapter, private val translationUtil: TranslationUtil, private val quranInfo: QuranInfo ) : Presenter { - private var lastCacheTime: Long = 0 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.getTranslations() - - 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() - } - .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()) + + 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() + } + } + val translationMap = getTranslationMap() + + val translationInfos = getTranslations(orderedTranslationsFileNames, translationMap) + val ayahInfo = combineAyahData(verseRange, arabicText, translationTexts, translationInfos) + ResultHolder(translationInfos, ayahInfo) + } } fun getTranslations(quranSettings: QuranSettings): List { @@ -115,9 +124,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()) } } @@ -175,25 +184,24 @@ internal open class BaseTranslationPresenter internal constructor( return texts } - private fun getTranslationMapSingle(): Single> { - return if (this.translationMap.isEmpty() || - this.lastCacheTime != translationsAdapter.lastWriteTime) { - Single.fromCallable { translationsAdapter.getTranslations() } - .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) - } - } 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 + } } } - internal class ResultHolder(val translations: Array, - val ayahInformation: List) + class ResultHolder(val translations: Array, + val ayahInformation: List) override fun bind(what: T) { translationScreen = what @@ -201,6 +209,5 @@ internal 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.java b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java deleted file mode 100644 index 07fbaea239..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.quran.labs.androidquran.presenter.translation; - -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.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 java.util.List; - -import javax.inject.Inject; - -import androidx.annotation.NonNull; -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..390b38220a --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/InlineTranslationPresenter.kt @@ -0,0 +1,58 @@ +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 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( + translationModel: TranslationModel, + dbAdapter: TranslationsDBAdapter, + translationUtil: TranslationUtil, + private val quranSettings: QuranSettings, + 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) + } + + 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) { + super.bind(what) + val translations = cachedTranslations + 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/translation/TranslationManagerPresenter.java b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java deleted file mode 100644 index b7b9778b11..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.java +++ /dev/null @@ -1,315 +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.common.LocalTranslation; -import com.quran.labs.androidquran.dao.translation.Translation; -import com.quran.labs.androidquran.dao.translation.TranslationItem; -import com.quran.labs.androidquran.dao.translation.TranslationList; -import com.quran.labs.androidquran.data.Constants; -import com.quran.labs.androidquran.database.DatabaseHandler; -import com.quran.labs.androidquran.database.TranslationsDBAdapter; -import com.quran.labs.androidquran.presenter.Presenter; -import com.quran.labs.androidquran.ui.TranslationManagerActivity; -import com.quran.labs.androidquran.util.QuranFileUtils; -import com.quran.labs.androidquran.util.QuranSettings; -import com.squareup.moshi.JsonAdapter; -import com.squareup.moshi.Moshi; - -import java.io.File; -import java.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(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.deleteTranslationByFile(translation.getFileName()); - } - return translationsDBAdapter.writeTranslationUpdates(Collections.singletonList(item)); - } - ).subscribeOn(Schedulers.io()) - .subscribe(); - } - - public void updateItemOrdering(final List items) { - Observable.fromCallable(() -> translationsDBAdapter.writeTranslationUpdates(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) { - 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.deleteTranslationByFile(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.writeTranslationUpdates(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..ec7c04690a --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationManagerPresenter.kt @@ -0,0 +1,240 @@ +package com.quran.labs.androidquran.presenter.translation + +import android.content.Context +import android.util.Pair +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.util.QuranFileUtils +import com.quran.labs.androidquran.util.QuranSettings +import com.quran.mobile.di.qualifier.ApplicationContext +import com.squareup.moshi.Moshi +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 +import okio.sink +import okio.source +import timber.log.Timber +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +open class TranslationManagerPresenter @Inject internal constructor( + @ApplicationContext private val appContext: Context, + private val okHttpClient: OkHttpClient, + private val quranSettings: QuranSettings, + private val translationsDBAdapter: TranslationsDBAdapter, + private val quranFileUtils: QuranFileUtils +) { + internal var host: String = Constants.HOST + + private val scope = MainScope() + + private val moshiTranslationListAdapter by lazy { + val moshi = Moshi.Builder().build() + moshi.adapter(TranslationList::class.java) + } + + fun checkForUpdates() { + 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 getTranslations(forceDownload: Boolean): Flow> { + val isCacheStale = System.currentTimeMillis() - + quranSettings.lastUpdatedTranslationDate > Constants.MIN_TRANSLATION_REFRESH_TIME + val flow = if (forceDownload) { + remoteTranslationList() + } else { + val flow = merge(cachedTranslationList(), remoteTranslationList()) + if (isCacheStale) { + flow + } else { + flow.take(1) + } + } + + return flow + .map { mergeWithServerTranslations(it.translations) } + .onEach { translations -> + val updatedTranslations = translations.any { it.needsUpgrade() } + quranSettings.setHaveUpdatedTranslations(updatedTranslations) + } + } + + 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.deleteTranslationByFileName(fileName) + } + translationsDBAdapter.writeTranslationUpdates(listOf(item)) + } + } + + suspend fun updateItemOrdering(items: List) { + withContext(Dispatchers.IO) { + translationsDBAdapter.writeTranslationUpdates(items) + } + } + + internal fun cachedTranslationList(): Flow { + return flow { + try { + val cachedFile = cachedFile + if (cachedFile.exists()) { + val list = moshiTranslationListAdapter.fromJson(cachedFile.source().buffer()) + if (list != null && list.translations.isNotEmpty()) { + emit(list) + } + } + } catch (e: Exception) { + Timber.e(e) + } + } + .flowOn(Dispatchers.IO) + } + + 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 responseBody = response.body + val result = moshiTranslationListAdapter.fromJson(responseBody!!.source()) + responseBody.close() + if (result != null && result.translations.isNotEmpty()) { + emit(result) + } + }.flowOn(Dispatchers.IO) + } + + 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 sink = cacheFile.sink().buffer() + moshiTranslationListAdapter.toJson(sink, list) + sink.close() + quranSettings.lastUpdatedTranslationDate = System.currentTimeMillis() + } + } catch (e: Exception) { + cacheFile.delete() + Timber.e(e) + } + } + + internal open val cachedFile: File + get() { + val dir = quranFileUtils.getQuranDatabaseDirectory(appContext) + return File(dir + File.separator + CACHED_RESPONSE_FILE_NAME) + } + + internal open suspend fun mergeWithServerTranslations(serverTranslations: List): List { + val localTranslations = translationsDBAdapter.translationsHash() + val databaseDir = quranFileUtils.getQuranDatabaseDirectory(appContext) + val updates: MutableList = ArrayList() + + val results = serverTranslations.mapIndexed { _, translation -> + val local = localTranslations[translation.id] + val dbFile = File(databaseDir, translation.fileName) + val translationExists = dbFile.exists() + var override: TranslationItem? = null + + 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) + } + } else { + item = TranslationItem(translation, local.version, local.displayOrder) + } + } else { + item = TranslationItem(translation) + } + + val exists = if (translationExists && !item.exists()) { + // delete the file, it has been corrupted + 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.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) + } + item + } + + if (updates.isNotEmpty()) { + translationsDBAdapter.writeTranslationUpdates(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) + } + + 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/presenter/translation/TranslationPresenter.kt b/app/src/main/java/com/quran/labs/androidquran/presenter/translation/TranslationPresenter.kt index e0cd8c6747..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 @@ -2,52 +2,49 @@ 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 io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.observers.DisposableObserver +import com.quran.mobile.translation.model.LocalTranslation +import kotlinx.coroutines.Dispatchers +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) { +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() - - disposable = Observable.fromArray(*pages.toTypedArray()) - .flatMap { page -> - getVerses(quranSettings.wantArabicInTranslationView(), - getTranslations(quranSettings), quranInfo.getVerseRangeForPage(page)) - .toObservable() + suspend fun refresh() { + pages + .map { + withContext(Dispatchers.IO) { + getVerses( + quranSettings.wantArabicInTranslationView(), + getTranslations(quranSettings), quranInfo.getVerseRangeForPage(it) + ) } - .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) {} - - override fun onComplete() {} - }) + } + .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 +57,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/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..7b2dc1a8a5 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/presenter/translationlist/TranslationListPresenter.kt @@ -0,0 +1,35 @@ +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 +) { + 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/service/AudioService.kt b/app/src/main/java/com/quran/labs/androidquran/service/AudioService.kt index fa8843dc5b..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 @@ -55,14 +55,16 @@ 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.core.math.MathUtils.clamp 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.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.dao.audio.AudioRequest import com.quran.labs.androidquran.data.Constants import com.quran.labs.androidquran.data.QuranDisplayData import com.quran.labs.androidquran.data.QuranFileConstants @@ -76,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 @@ -97,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 @@ -161,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 @@ -192,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) { @@ -232,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 { @@ -261,12 +257,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, // create the Audio Focus Helper 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) @@ -342,38 +341,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) @@ -394,11 +369,15 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, processStopRequest() } else if (ACTION_REWIND == action) { processRewindRequest() - } 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) + serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200) + } audioRequest = playInfo } } else { @@ -563,14 +542,16 @@ 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 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) } @@ -616,7 +597,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, setUpAsForeground() } configAndStartMediaPlayer(false) - notifyAudioStatus(AudioUpdateIntent.PLAYING) + updateAudioPlaybackStatus() } } @@ -631,11 +612,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() } } @@ -653,6 +635,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 @@ -674,6 +657,21 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } } + private fun processUpdatePlaybackSpeed(speed: Float) { + if (State.Playing === state && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + 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") + } + } + } + private fun processSkipRequest() { if (audioRequest == null) { return @@ -713,6 +711,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) @@ -724,25 +723,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) @@ -763,10 +750,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") + } } /** @@ -853,6 +860,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, if (!player.isPlaying) { player.start() state = State.Playing + updateAudioPlaybackStatus() } return } @@ -876,6 +884,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, } player.start() state = State.Playing + updateAudioPlaybackStatus() } } @@ -914,10 +923,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 } @@ -926,11 +931,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 } @@ -984,12 +984,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, localPlayer.setDataSource(url) } state = State.Preparing - - val audioAttributes = AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_MEDIA) - .build() - localPlayer.setAudioAttributes(audioAttributes) + updateAudioPlaybackStatus() // starts preparing the media player in the background. When it's // done, it will call our OnPreparedListener (that is, the @@ -1024,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()) } @@ -1044,6 +1071,10 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, ) player.start() state = State.Playing + updateAudioPlaybackStatus() + audioRequest?.playbackSpeed?.let { speed -> + processUpdatePlaybackSpeed(speed) + } serviceHandler.sendEmptyMessageDelayed(MSG_UPDATE_AUDIO_POS, 200) } @@ -1297,6 +1328,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 @@ -1347,10 +1379,7 @@ 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" @@ -1358,7 +1387,7 @@ class AudioService : Service(), OnCompletionListener, OnPreparedListener, const val ACTION_SKIP = "com.quran.labs.androidquran.action.SKIP" const val ACTION_REWIND = "com.quran.labs.androidquran.action.REWIND" 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/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/service/util/DownloadStarter.kt b/app/src/main/java/com/quran/labs/androidquran/service/util/DownloadStarter.kt index cc44de46f4..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,16 +7,17 @@ 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 +import com.quran.mobile.di.qualifier.ApplicationContext import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @ContributesBinding(AppScope::class) class DownloadStarter @Inject constructor( - private val context: Context, + @ApplicationContext private val appContext: Context, private val quranInfo: QuranInfo, private val fileManager: QuranFileManager, private val audioUtils: AudioUtils @@ -30,10 +31,10 @@ class DownloadStarter @Inject constructor( val basePath = fileManager.audioFileDirectory() val baseUri = basePath + qari.path val isGapless = qari.isGapless - val sheikhName = context.getString(qari.nameResource) + val sheikhName = appContext.getString(qari.nameResource) val intent = ServiceIntentHelper.getDownloadIntent( - context, + appContext, audioUtils.getQariUrl(qari), baseUri, sheikhName, @@ -45,14 +46,14 @@ class DownloadStarter @Inject constructor( putExtra(QuranDownloadService.EXTRA_IS_GAPLESS, isGapless) putExtra(QuranDownloadService.EXTRA_METADATA, AudioDownloadMetadata(qari.id)) } - context.startService(intent) + appContext.startService(intent) } override fun cancelDownloads() { - val intent = Intent(context, QuranDownloadService::class.java).apply { + val intent = Intent(appContext, QuranDownloadService::class.java).apply { action = QuranDownloadService.ACTION_CANCEL_DOWNLOADS } - context.startService(intent) + appContext.startService(intent) } companion object { 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 eb4698b524..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 @@ -4,9 +4,9 @@ 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; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -20,7 +20,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; @@ -33,6 +32,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; @@ -57,37 +58,33 @@ 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; 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.LocalTranslation; -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.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; -import com.quran.labs.androidquran.database.TranslationsDBAdapter; import com.quran.labs.androidquran.di.component.activity.PagerActivityComponent; -import com.quran.labs.androidquran.di.module.activity.PagerActivityModule; -import com.quran.labs.androidquran.di.module.fragment.QuranPageModule; import com.quran.labs.androidquran.model.bookmark.BookmarkModel; import com.quran.labs.androidquran.model.translation.ArabicDatabaseUtils; import com.quran.labs.androidquran.presenter.audio.AudioPresenter; 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; +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; @@ -119,20 +116,21 @@ 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.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; -import com.quran.reading.common.AudioEventPresenter; 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; @@ -140,11 +138,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; /** @@ -161,7 +158,7 @@ public class PagerActivity extends AppCompatActivity implements AyahSelectedListener, JumpDestination, QuranReadingActivityComponentProvider, - QuranPageExtrasComponentProvider, + QuranReadingPageComponentProvider, AyahToolBarInjector, QariListWrapperInjector, ActivityCompat.OnRequestPermissionsResultCallback { @@ -191,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; @@ -216,9 +212,8 @@ public class PagerActivity extends AppCompatActivity implements private SlidingUpPanelLayout slidingPanel; private ViewPager slidingPager; private SlidingPagerAdapter slidingPagerAdapter; + private ActivityResultLauncher requestPermissionLauncher; - private int numberOfPages; - private int numberOfPagesDual; private int defaultNavigationBarColor; private boolean isSplitScreen = false; @@ -232,7 +227,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; @@ -242,15 +236,17 @@ 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; private CompositeDisposable compositeDisposable; private final CompositeDisposable foregroundDisposable = new CompositeDisposable(); @@ -290,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( @@ -305,17 +302,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)) { @@ -323,12 +314,11 @@ 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(); 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 +364,7 @@ public void onCreate(Bundle savedInstanceState) { if (showingTranslation && translationNames != null) { updateActionBarSpinner(); } else { - updateActionBarTitle(numberOfPages - page); + updateActionBarTitle(page); } lastPopupTime = System.currentTimeMillis(); @@ -437,17 +427,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 (isDualPages && !showingTranslation && potentialPage == quranInfo.getNumberOfPages() + 1) { - page = quranInfo.getNumberOfPages(); - } else { - page = potentialPage; - } + final int page = quranInfo.getPageFromPosition(position, isDualPageVisible()); if (quranSettings.shouldDisplayMarkerPopup()) { lastPopupTime = QuranDisplayHelper.displayMarkerPopup( @@ -495,24 +475,17 @@ 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; + final int curPage; if (isDualPageVisible()) { - if (curPage % 2 != 0) { - curPage++; - } - curPage = numberOfPagesDual - (curPage / 2); + curPage = quranInfo.mapSinglePageToDualPage(page); } else { - if (curPage % 2 == 0) { - curPage--; - } - curPage = numberOfPages - curPage; + curPage = quranInfo.mapDualPageToSinglePage(page); } 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); } @@ -535,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; @@ -560,6 +529,14 @@ 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); + }); + + // read the list of translations + requestTranslationsList(); } @Override @@ -583,7 +560,8 @@ 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()); + e.onNext(page); } }; @@ -777,15 +755,16 @@ 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()) .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; })); } @@ -806,9 +785,8 @@ public PagerActivityComponent getPagerActivityComponent() { if (pagerActivityComponent == null) { pagerActivityComponent = ((QuranApplication) getApplication()) .getApplicationComponent() - .pagerActivityComponentBuilder() - .withPagerActivityModule(new PagerActivityModule(this)) - .build(); + .pagerActivityComponentFactory() + .generate(this, this); } return pagerActivityComponent; } @@ -826,11 +804,10 @@ public QuranReadingActivityComponent provideQuranReadingActivityComponent() { @NonNull @Override - public QuranPageExtrasComponent provideQuranPageExtrasComponent(@NonNull int... pages) { + public QuranReadingPageComponent provideQuranReadingPageComponent(@NonNull int... pages) { return getPagerActivityComponent() - .quranPageComponentBuilder() - .withQuranPageModule(new QuranPageModule(pages)) - .build(); + .quranPageComponentFactory() + .generate(pages); } @Override @@ -877,7 +854,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 +907,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 +923,7 @@ public void onNewIntent(Intent intent) { updateActionBarSpinner(); } else { pagerAdapter.setQuranMode(); - updateActionBarTitle(numberOfPages - page); + updateActionBarTitle(page); } supportInvalidateOptionsMenu(); @@ -956,10 +933,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); @@ -989,19 +964,26 @@ 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()"); clearUiVisibilityListener(); // remove broadcast receivers - LocalBroadcastManager.getInstance(this).unregisterReceiver(audioReceiver); if (downloadReceiver != null) { downloadReceiver.setListener(null); LocalBroadcastManager.getInstance(this) @@ -1009,9 +991,12 @@ protected void onDestroy() { downloadReceiver = null; } + if (translationJob != null) { + translationJob.cancel(new CancellationException()); + } currentQariBridge.unsubscribeAll(); compositeDisposable.dispose(); - audioEventPresenterBridge.dispose(); + audioStatusRepositoryBridge.dispose(); readingEventPresenterBridge.dispose(); handler.removeCallbacksAndMessages(null); dismissProgressDialog(); @@ -1029,9 +1014,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); } @@ -1171,16 +1153,13 @@ private void switchToTranslation() { endAyahMode(); } - if (translations.size() == 0) { + if (translations.isEmpty()) { startTranslationManager(); } else { int page = getCurrentPage(); pagerAdapter.setTranslationMode(); showingTranslation = true; if (shouldUpdatePageNumber()) { - if (page % 2 == 0) { - page--; - } final int position = quranInfo.getPositionFromPage(page, false); viewPager.setCurrentItem(position); } @@ -1219,14 +1198,6 @@ public List getTranslations() { return translations; } - public String[] getTranslationNames() { - return translationNames; - } - - public Set getActiveTranslationsFilesNames() { - return activeTranslationsFilesNames; - } - @Override public void onAddTagSelected() { FragmentManager fm = getSupportFragmentManager(); @@ -1298,33 +1269,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) { @@ -1379,7 +1323,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); @@ -1388,56 +1332,29 @@ private void ensurePage(int sura, int ayah) { } private void requestTranslationsList() { - compositeDisposable.add( - Single.fromCallable(() -> - translationsDBAdapter.getTranslations()) - .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) { @@ -1515,10 +1432,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, @@ -1529,18 +1443,19 @@ 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 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, @@ -1548,7 +1463,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()); @@ -1560,7 +1476,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); } } @@ -1575,14 +1491,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) { @@ -1591,9 +1527,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.switchMode(AudioStatusBar.LOADING_MODE); } Timber.d("starting service for audio playback"); @@ -1607,6 +1540,27 @@ public void onPausePressed() { audioStatusBar.switchMode(AudioStatusBar.PAUSED_MODE); } + @Override + public void setPlaybackSpeed(float speed) { + final AudioRequest lastAudioRequest = audioStatusRepositoryBridge.audioRequest(); + 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); + } + } + @Override public void onNextPressed() { startService(audioUtils.getAudioIntent(this, @@ -1645,7 +1599,10 @@ public void onShowQariList() { } public boolean updatePlayOptions(int rangeRepeat, - int verseRepeat, boolean enforceRange) { + int verseRepeat, + boolean enforceRange, + float playbackSpeed) { + final AudioRequest lastAudioRequest = audioStatusRepositoryBridge.audioRequest(); if (lastAudioRequest != null) { final AudioRequest updatedAudioRequest = new AudioRequest(lastAudioRequest.getStart(), lastAudioRequest.getEnd(), @@ -1653,15 +1610,13 @@ public boolean updatePlayOptions(int rangeRepeat, verseRepeat, rangeRepeat, enforceRange, + playbackSpeed, 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; - audioStatusBar.setRepeatCount(verseRepeat); return true; } else { return false; @@ -1670,6 +1625,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(), @@ -1677,14 +1633,14 @@ 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; } } @@ -1692,7 +1648,6 @@ public void setRepeatCount(int repeatCount) { public void onStopPressed() { startService(audioUtils.getAudioIntent(this, AudioService.ACTION_STOP)); audioStatusBar.switchMode(AudioStatusBar.STOPPED_MODE); - lastAudioRequest = null; } @Override @@ -1744,7 +1699,7 @@ private SuraAyah getSelectionEnd() { } public AudioRequest getLastAudioRequest() { - return lastAudioRequest; + return audioStatusRepositoryBridge.audioRequest(); } public void endAyahMode() { @@ -1774,7 +1729,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); @@ -1794,7 +1749,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(); @@ -1848,7 +1803,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) { @@ -1931,9 +1886,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/QuranActivity.kt b/app/src/main/java/com/quran/labs/androidquran/ui/QuranActivity.kt index 64e85cb767..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 @@ -47,11 +48,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 @@ -101,8 +101,8 @@ class QuranActivity : AppCompatActivity(), super.onCreate(savedInstanceState) quranApp.applicationComponent - .quranActivityComponentBuilder() - .build() + .quranActivityComponentFactory() + .generate() .inject(this) setContentView(R.layout.quran_index) @@ -161,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 + } } ) } @@ -290,13 +295,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.java b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java deleted file mode 100644 index 3136b1e8bf..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.java +++ /dev/null @@ -1,544 +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"); - } - } - - List sortedItems = sortedDownloadedItems(); - int lastDisplayOrder = sortedItems.isEmpty() ? 1 : - sortedItems.get(sortedItems.size() - 1).getDisplayOrder(); - - final Translation translation = downloadingItem.getTranslation(); - TranslationItem updated = new TranslationItem(translation, - translation.getCurrentVersion(), lastDisplayOrder + 1); - updateTranslationItem(updated); - - // 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..3687eb43f0 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/TranslationManagerActivity.kt @@ -0,0 +1,512 @@ +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.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 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 javax.inject.Inject +import kotlin.math.max + +class TranslationManagerActivity : AppCompatActivity(), SimpleDownloadListener, + DownloadedMenuActionListener { + 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 downloadReceiver: DefaultDownloadReceiver? = null + private var actionMode: ActionMode? = null + private var downloadedItemActionListener: DownloadedItemActionListener? = null + + @Inject + lateinit var presenter: TranslationManagerPresenter + + @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 + 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 + + private val scope = MainScope() + + 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) + } + 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() } + translationSwipeRefresh.isRefreshing = true + refreshTranslations() + } + + 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() { + scope.cancel() + 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() { + refreshTranslations(true) + } + + private fun refreshTranslations(forceDownload: Boolean = false) { + presenter.getTranslations(forceDownload) + .onEach { onTranslationsUpdated(it) } + .catch { onErrorDownloadTranslations() } + .launchIn(scope) + } + + private fun updateTranslationItem(updated: TranslationItem) { + val id = updated.translation.id + val allItemsIndex = translationPositions[id] + if (allItems.size > allItemsIndex) { + allItems = allItems.toMutableList().apply { + removeAt(allItemsIndex) + add(allItemsIndex, updated) + } + } + scope.launch { + 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() + } + } + + private fun onErrorDownloadTranslations() { + translationSwipeRefresh.isRefreshing = false + Snackbar + .make( + translationRecycler, + R.string.error_getting_translation_list, + Snackbar.LENGTH_SHORT + ) + .show() + } + + private 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 + translationPositions = itemsSparseArray + generateListItems() + } + + private fun generateListItems() { + val (downloaded, notDownloaded) = allItems.partition { it.exists() } + + // 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) + adapter.setTranslations(resultList) + 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 + ) { _: DialogInterface?, _: 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 { + return allItems.filter { it.exists() }.sortedBy { it.displayOrder } + } + + private fun rankDownItem(targetRow: TranslationRowData) { + val targetItem = targetRow as TranslationItem + val targetTranslationId = targetItem.translation.id + val targetIndex = currentSortedDownloads.indexOfFirst { it.translation.id == targetTranslationId } + if (targetIndex >= 0) { + val sortedDownloads = currentSortedDownloads.toMutableList() + sortedDownloads.removeAt(targetIndex) + val updatedItem = targetItem.withDisplayOrder(targetItem.displayOrder + 1) + if (targetIndex + 1 < sortedDownloads.size) { + sortedDownloads.add(targetIndex + 1, updatedItem) + } else { + sortedDownloads.add(updatedItem) + } + currentSortedDownloads = sortedDownloads + updateDownloadedItems() + } + } + + private fun rankUpItem(targetRow: TranslationRowData) { + val targetItem = targetRow as TranslationItem + val targetTranslationId = targetItem.translation.id + val targetIndex = currentSortedDownloads.indexOfFirst { it.translation.id == targetTranslationId } + if (targetIndex >= 0) { + val sortedDownloads = currentSortedDownloads.toMutableList() + sortedDownloads.removeAt(targetIndex) + val updatedItem = targetItem.withDisplayOrder(targetItem.displayOrder - 1) + sortedDownloads.add(max(targetIndex - 1, 0), updatedItem) + currentSortedDownloads = sortedDownloads + updateDownloadedItems() + } + } + + private fun updateTranslationOrdersIfNecessary() { + if (originalSortedDownloads != currentSortedDownloads) { + val normalizedSortOrders: List = + currentSortedDownloads.mapIndexed { index, item -> + item.withDisplayOrder(index + 1) + } + + originalSortedDownloads = normalizedSortOrders + currentSortedDownloads = normalizedSortOrders + scope.launch { + 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 + 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 + } + + 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" + } +} 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..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,9 @@ 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() end = selectedAyah.endSuraAyah() @@ -41,7 +44,10 @@ abstract class AyahActionFragment : Fragment() { start = playbackAyah end = playbackAyah } - refreshView() + + if (previousStart != start || previousEnd != end) { + refreshView() + } } .launchIn(scope) 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..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 @@ -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 @@ -13,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 @@ -35,6 +36,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 +45,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 +79,12 @@ 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) + + 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 @@ -91,18 +100,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 +118,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,20 +199,21 @@ class AyahPlaybackFragment : AyahActionFragment() { val enforceRange = restrictToRange.isChecked var updatedRange = false + val speed = SPEEDS[playbackSpeedPicker.value - 1] 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) { + } 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( - currentStart, currentEnding, page, verseRepeat, rangeRepeat, enforceRange + currentStart, currentEnding, page, verseRepeat, rangeRepeat, enforceRange, speed ) } } @@ -303,6 +314,7 @@ class AyahPlaybackFragment : AyahActionFragment() { if (lastRequest != lastSeenAudioRequest) { verseRepeatCount = lastRequest.repeatInfo rangeRepeatCount = lastRequest.rangeRepeatInfo + currentSpeed = lastRequest.playbackSpeed shouldEnforce = lastRequest.enforceBounds } else { shouldReset = false @@ -324,6 +336,7 @@ class AyahPlaybackFragment : AyahActionFragment() { } rangeRepeatCount = 0 verseRepeatCount = 0 + currentSpeed = 1.0f decidedStart = null decidedEnd = null applyButton.setText(R.string.play_apply_and_play) @@ -347,6 +360,7 @@ class AyahPlaybackFragment : AyahActionFragment() { restrictToRange.isChecked = shouldEnforce repeatRangePicker.value = rangeRepeatCount + 1 repeatVersePicker.value = verseRepeatCount + 1 + playbackSpeedPicker.value = SPEEDS.indexOf(currentSpeed) + 1 } } } @@ -355,5 +369,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/ui/fragment/AyahTranslationFragment.kt b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/AyahTranslationFragment.kt index 4f3a80f7a8..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 @@ -1,28 +1,34 @@ 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.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 +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 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 class AyahTranslationFragment : AyahActionFragment(), TranslationScreen { @@ -43,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 @@ -54,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?, @@ -99,56 +112,53 @@ 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 + translator.visibility = View.GONE + translationView.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 + activeTranslationsFilesNames, ) { selectedItems: Set? -> quranSettings.activeTranslations = selectedItems + // this is the refresh for when a translation is selected from the spinner refreshView() } translator.adapter = translationAdapter } else { adapter.updateItems( - activity.translationNames, + translations.map { it.resolveTranslatorName() }.toTypedArray(), translations, activeTranslationsFilesNames ) } - 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) + refreshView() + } + } + + public override fun refreshView() { + val start = start + val end = end + if (start == null || end == null) { + return + } + + 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) + scope.launch { translationPresenter.refresh(verseRange) } } @@ -157,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/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/fragment/QuranPageFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/QuranPageFragment.java index 5c02a9e22d..8b08d0b16b 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 @@ -22,7 +22,6 @@ import com.quran.data.model.selection.SelectionIndicator; import com.quran.data.model.selection.SelectionIndicatorKt; import com.quran.labs.androidquran.data.QuranDisplayData; -import com.quran.labs.androidquran.di.module.fragment.QuranPageModule; 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; @@ -97,7 +96,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; } @@ -139,10 +138,10 @@ public void onAttach(@NonNull Context context) { super.onAttach(context); pageNumber = getArguments().getInt(PAGE_NUMBER_EXTRA); + final int[] pages = { pageNumber }; ((PagerActivity) getActivity()).getPagerActivityComponent() - .quranPageComponentBuilder() - .withQuranPageModule(new QuranPageModule(pageNumber)) - .build() + .quranPageComponentFactory() + .generate(pages) .inject(this); } 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/ui/fragment/TabletFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java deleted file mode 100644 index bd12fc7eb2..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.java +++ /dev/null @@ -1,522 +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.LocalTranslation; -import com.quran.labs.androidquran.common.QuranAyahInfo; -import com.quran.labs.androidquran.data.QuranDisplayData; -import com.quran.labs.androidquran.di.module.fragment.QuranPageModule; -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.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 java.util.List; -import java.util.Set; -import javax.inject.Inject; -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, pageNumber - 1); - if (mainView.getLeftPage() instanceof QuranImagePageLayout) { - leftImageView = ((QuranImagePageLayout) mainView.getLeftPage()).getImageView(); - rightImageView = ((QuranImagePageLayout) mainView.getRightPage()).getImageView(); - } - mainView.setPageController(this, pageNumber, pageNumber - 1); - } else if (mode == Mode.TRANSLATION) { - if (!isSplitScreen) { - mainView.init(TabletView.TRANSLATION_PAGE, TabletView.TRANSLATION_PAGE, pageViewFactory, pageNumber, pageNumber - 1); - 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, pageNumber - 1); - } else { - initSplitMode(); - } - } - return mainView; - } - - private void initSplitMode() { - isQuranOnRight = pageNumber % 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); - } - - @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.getNumberOfPages() >= pageNumber) { - left = new AyahImageTrackerItem(pageNumber, - quranInfo, - quranDisplayData, - false, - imageDrawHelpers, - leftImageView); - } else { - left = new NoOpImageTrackerItem(pageNumber); - } - right = new AyahImageTrackerItem( - pageNumber - 1, 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, quranInfo, leftTranslation); - right = new AyahTranslationTrackerItem(pageNumber - 1, 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 - 1, pageNumber }; - - ((PagerActivity) getActivity()).getPagerActivityComponent() - .quranPageComponentBuilder() - .withQuranPageModule(new QuranPageModule(pages)) - .build() - .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 - 1 ? 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().refresh(); - } - } - - @Override - public void setVerses(int page, - @NonNull LocalTranslation[] translations, - @NonNull List verses) { - if (isSplitScreen) { - splitTranslationView.setVerses(quranDisplayData, translations, verses); - } else { - if (page == pageNumber) { - leftTranslation.setVerses(quranDisplayData, translations, verses); - } else if (page == pageNumber - 1) { - rightTranslation.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().refresh(); - } - } - - 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..e4f4893d3e --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TabletFragment.kt @@ -0,0 +1,526 @@ +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 kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +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 + + private val scope = MainScope() + + 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() { + compositeDisposable.clear() + scope.cancel() + super.onDetach() + } + + 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) { + scope.launch { + translationPresenter.get().refresh() + } + } + } + + 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) { + scope.launch { + translationPresenter.get().refresh() + } + } + } + + 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 + } + } +} 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/ui/fragment/TranslationFragment.java b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java deleted file mode 100644 index edced9eb18..0000000000 --- a/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.java +++ /dev/null @@ -1,213 +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.LocalTranslation; -import com.quran.labs.androidquran.common.QuranAyahInfo; -import com.quran.labs.androidquran.data.QuranDisplayData; -import com.quran.labs.androidquran.di.module.fragment.QuranPageModule; -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.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); - - 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; - ((PagerActivity) getActivity()).getPagerActivityComponent() - .quranPageComponentBuilder() - .withQuranPageModule(new QuranPageModule(pageNumber)) - .build() - .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.refresh(); - } - - @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..50036d20ad --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/ui/fragment/TranslationFragment.kt @@ -0,0 +1,208 @@ +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 kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +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 + + private val scope = MainScope() + + 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 onDetach() { + scope.cancel() + super.onDetach() + } + + 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() { + scope.launch { + presenter.refresh() + } + } + + 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 + } + } +} 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/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/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/ui/helpers/QuranPageAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageAdapter.kt index d5112cfebb..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() { @@ -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/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageLoader.kt b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageLoader.kt index cf380c14b8..7bc18d240b 100644 --- a/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageLoader.kt +++ b/app/src/main/java/com/quran/labs/androidquran/ui/helpers/QuranPageLoader.kt @@ -1,10 +1,11 @@ package com.quran.labs.androidquran.ui.helpers import android.content.Context -import com.quran.labs.androidquran.common.Response import com.quran.data.di.ActivityScope +import com.quran.labs.androidquran.common.Response import com.quran.labs.androidquran.util.QuranFileUtils import com.quran.labs.androidquran.util.QuranScreenInfo +import com.quran.mobile.di.qualifier.ApplicationContext import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers import okhttp3.OkHttpClient @@ -13,7 +14,7 @@ import javax.inject.Inject @ActivityScope class QuranPageLoader @Inject internal constructor( - private val appContext: Context, + @ApplicationContext private val appContext: Context, private val okHttpClient: OkHttpClient, private val imageWidth: String, private val quranScreenInfo: QuranScreenInfo, 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); } 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/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/ui/translation/TranslationAdapter.kt b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationAdapter.kt index 9f3675090f..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 @@ -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,38 @@ 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) + spannable.setSpan(ForegroundColorSpan(inlineAyahColor), 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/TranslationView.java b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationView.java index e7edc5ebf4..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 @@ -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,14 +138,14 @@ 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) { 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/ui/translation/TranslationViewRow.kt b/app/src/main/java/com/quran/labs/androidquran/ui/translation/TranslationViewRow.kt index cce87049eb..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 @@ -1,8 +1,11 @@ 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 +import com.quran.labs.androidquran.ui.helpers.TranslationFootnoteHelper internal class TranslationViewRow @JvmOverloads constructor( @field:Type val type: Int, @@ -13,8 +16,25 @@ 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( + spannableStringBuilder: SpannableStringBuilder, + expandedFootnotes: List, + collapsedFootnoteSpannableStyler: ((Int) -> SpannableString), + expandedFootnoteSpannableStyler: ((SpannableStringBuilder, Int, Int) -> SpannableStringBuilder) + ): CharSequence { + return TranslationFootnoteHelper.footnoteCognizantText( + data, + footnotes, + spannableStringBuilder, + expandedFootnotes, + collapsedFootnoteSpannableStyler, + expandedFootnoteSpannableStyler + ) + } + @IntDef( Type.BASMALLAH, Type.SURA_HEADER, 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..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 @@ -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,11 +10,14 @@ 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.Collections; import java.util.List; import java.util.Set; @@ -37,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); @@ -49,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); @@ -65,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 { @@ -166,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/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/main/java/com/quran/labs/androidquran/util/QuranFileUtils.kt b/app/src/main/java/com/quran/labs/androidquran/util/QuranFileUtils.kt index df22224704..f78d969291 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 @@ -14,6 +14,7 @@ import com.quran.labs.androidquran.BuildConfig import com.quran.labs.androidquran.common.Response import com.quran.labs.androidquran.data.QuranDataProvider import com.quran.labs.androidquran.extension.closeQuietly +import com.quran.mobile.di.qualifier.ApplicationContext import okhttp3.OkHttpClient import okhttp3.Request.Builder import okhttp3.ResponseBody @@ -35,7 +36,7 @@ import java.util.Locale import javax.inject.Inject class QuranFileUtils @Inject constructor( - context: Context, + @ApplicationContext context: Context, pageProvider: PageProvider, private val quranScreenInfo: QuranScreenInfo ): QuranFileManager { @@ -250,6 +251,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/QuranPageInfoImpl.kt b/app/src/main/java/com/quran/labs/androidquran/util/QuranPageInfoImpl.kt index 510a5a732f..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 @@ -31,4 +31,10 @@ 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) + } + + override fun skippedPagesCount(): Int = quranInfo.skip } diff --git a/app/src/main/java/com/quran/labs/androidquran/util/QuranScreenInfo.java b/app/src/main/java/com/quran/labs/androidquran/util/QuranScreenInfo.java index de0dd95b46..215c0e69ee 100644 --- a/app/src/main/java/com/quran/labs/androidquran/util/QuranScreenInfo.java +++ b/app/src/main/java/com/quran/labs/androidquran/util/QuranScreenInfo.java @@ -4,11 +4,13 @@ import android.graphics.Point; import android.view.Display; +import androidx.annotation.NonNull; + import com.quran.data.source.PageSizeCalculator; +import com.quran.mobile.di.qualifier.ApplicationContext; import javax.inject.Inject; -import androidx.annotation.NonNull; import timber.log.Timber; public class QuranScreenInfo { @@ -20,7 +22,7 @@ public class QuranScreenInfo { private final PageSizeCalculator pageSizeCalculator; @Inject - public QuranScreenInfo(@NonNull Context appContext, + public QuranScreenInfo(@NonNull @ApplicationContext Context appContext, @NonNull Display display, @NonNull PageSizeCalculator pageSizeCalculator) { final Point point = new Point(); 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..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 @@ -162,6 +162,24 @@ 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); + } + + 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 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/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/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..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,26 +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 -> - trySendBlocking(pref) - .onFailure {} + 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 @@ -63,5 +80,21 @@ class SettingsImpl @Inject constructor(private val quranSettings: QuranSettings) return quranSettings.pageType } + override suspend fun showSidelines(): Boolean { + return quranSettings.isSidelines + } + + override suspend fun setShowSidelines(show: Boolean) { + quranSettings.isSidelines = show + } + + override suspend fun showLineDividers(): Boolean { + return quranSettings.isShowLineDividers + } + + override suspend fun setShouldShowLineDividers(show: Boolean) { + quranSettings.isShowLineDividers = show + } + override fun preferencesFlow(): Flow = preferencesFlow } 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..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 @@ -5,16 +5,17 @@ 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 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 @@ -72,10 +73,19 @@ 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") } - append(text) + + // remove footnotes for now + val spannableStringBuilder = SpannableStringBuilder(text) + translation.footnoteCognizantText( + spannableStringBuilder, + listOf(), + { _ -> SpannableString("") }, + { builder, _, _ -> builder } + ) + append(spannableStringBuilder) } } if (ayahInfo.arabicText == null) { 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/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..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 @@ -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; @@ -16,10 +17,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; @@ -51,6 +54,10 @@ public class AudioStatusBar extends LeftToRightLinearLayout { private Qari currentQari; private int currentRepeat = 0; + 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]; @DrawableRes private int itemBackground; private final boolean isRtl; private boolean isDualPageMode; @@ -59,9 +66,11 @@ 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; + private final RepeatButton speedButton; private AudioBarListener audioBarListener; private AudioBarRecitationListener audioBarRecitationListener; @@ -71,6 +80,7 @@ public interface AudioBarListener { void onNextPressed(); void onPreviousPressed(); void onStopPressed(); + void setPlaybackSpeed(float speed); void onCancelPressed(boolean stopDownload); void setRepeatCount(int repeatCount); void onAcceptPressed(); @@ -101,6 +111,7 @@ public AudioStatusBar(Context context, AttributeSet attrs, int defStyle) { this.context = context; repeatButton = new RepeatButton(context); + speedButton = new RepeatButton(context); Resources resources = getResources(); buttonWidth = resources.getDimensionPixelSize( R.dimen.audiobar_button_width); @@ -185,6 +196,16 @@ public QariItem getAudioInfo() { return QariItem.Companion.fromQari(context, currentQari); } + 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) { if (hasErrorText) { progressText.setText(R.string.downloading_title); @@ -240,8 +261,9 @@ private void showStoppedMode() { private void updateButton() { final TextView currentQariView = qariView; - if (currentQariView != null) { - currentQariView.setText(currentQari.getNameResource()); + final Qari qari = currentQari; + if (currentQariView != null && qari != null) { + currentQariView.setText(qari.getNameResource()); } } @@ -254,24 +276,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); } + updateButton(); + + 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() { @@ -450,7 +481,11 @@ private void showPlayingMode(boolean isPaused) { addButton(R.drawable.ic_next, 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); } @@ -500,8 +535,14 @@ private void incrementRepeat() { updateRepeatButtonText(); } + private void updatePlaybackSpeed() { + 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) { @@ -512,6 +553,22 @@ private void updateRepeatButtonText() { repeatButton.setText(str); } + private void updateSpeedButtonText(){ + currentSpeed = speeds[currentSpeedIndex]; + final String str; + if (currentSpeedIndex == 2) { + str = ""; + } else { + str = String.valueOf(currentSpeed); + } + + post(() -> { + if (speedButton != null) { + speedButton.setText(str); + } + }); + } + public void setRepeatCount(int repeatCount) { boolean updated = false; if (currentRepeat != repeatCount) { @@ -559,6 +616,9 @@ 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); } 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/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 c6b5834f28..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); @@ -385,9 +389,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 +403,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 +481,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/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..dd394e6436 --- /dev/null +++ b/app/src/main/java/com/quran/labs/androidquran/view/InlineTranslationView.kt @@ -0,0 +1,203 @@ +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.RelativeSizeSpan +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 androidx.core.content.ContextCompat +import com.quran.labs.androidquran.R +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.util.TypefaceManager +import com.quran.labs.androidquran.util.QuranSettings +import com.quran.mobile.translation.model.LocalTranslation + +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 var inlineAyahColor: Int = 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 + inlineAyahColor = ContextCompat.getColor(context, R.color.translation_translator_color) + } + + 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].resolveTranslatorName()) + 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(stylize(ayah.texts[i], translations[i].languageCode, 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) + } + + 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, + 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( + span, + range.first, + range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + + return metadata.footnoteCognizantText( + spannableStringBuilder, + listOf(), + ::collapsedFootnoteSpan, + ::expandedFootnote + ) + } +} 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/RepeatButton.kt b/app/src/main/java/com/quran/labs/androidquran/view/RepeatButton.kt index 62371f694b..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 @@ -49,7 +49,12 @@ class RepeatButton @JvmOverloads constructor( if (drawable != null) { val bounds = drawable.bounds if (bounds.width() > 0) { - textXPosition = viewWidth - (viewWidth - bounds.width()) / 2 + 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/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/main/java/com/quran/labs/androidquran/widget/BookmarksWidgetUpdater.kt b/app/src/main/java/com/quran/labs/androidquran/widget/BookmarksWidgetUpdater.kt index 685318b321..fd45ce37e0 100644 --- a/app/src/main/java/com/quran/labs/androidquran/widget/BookmarksWidgetUpdater.kt +++ b/app/src/main/java/com/quran/labs/androidquran/widget/BookmarksWidgetUpdater.kt @@ -4,6 +4,7 @@ import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import com.quran.labs.androidquran.R +import com.quran.mobile.di.qualifier.ApplicationContext import javax.inject.Inject /** @@ -22,19 +23,19 @@ interface BookmarksWidgetUpdater { fun updateBookmarksWidget() } -class BookmarksWidgetUpdaterImpl @Inject constructor(private val context: Context) : +class BookmarksWidgetUpdaterImpl @Inject constructor(@ApplicationContext private val appContext: Context) : BookmarksWidgetUpdater { override fun checkForAnyBookmarksWidgets() = - AppWidgetManager.getInstance(context) - ?.getAppWidgetIds(ComponentName(context, BookmarksWidget::class.java)) + AppWidgetManager.getInstance(appContext) + ?.getAppWidgetIds(ComponentName(appContext, BookmarksWidget::class.java)) ?.isNotEmpty() == true override fun updateBookmarksWidget() { - val appWidgetManager = AppWidgetManager.getInstance(context) + val appWidgetManager = AppWidgetManager.getInstance(appContext) if (appWidgetManager != null) { val appWidgetIds = - appWidgetManager.getAppWidgetIds(ComponentName(context, BookmarksWidget::class.java)) + appWidgetManager.getAppWidgetIds(ComponentName(appContext, BookmarksWidget::class.java)) appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.list_view_widget) } } 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/drawable/ic_speed.xml b/app/src/main/res/drawable/ic_speed.xml new file mode 100644 index 0000000000..807b50d0ec --- /dev/null +++ b/app/src/main/res/drawable/ic_speed.xml @@ -0,0 +1,3 @@ + + + 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..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 @@ + @@ -89,6 +88,7 @@ + + + + diff --git a/app/src/main/res/layout-ar/audio_panel.xml b/app/src/main/res/layout-ar/audio_panel.xml index 299fc676a4..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 4361bef7e1..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 @@ + + + + diff --git a/app/src/main/res/layout/play_each_verse.xml b/app/src/main/res/layout/play_each_verse.xml index ede1adb7ae..9a639d26bd 100644 --- a/app/src/main/res/layout/play_each_verse.xml +++ b/app/src/main/res/layout/play_each_verse.xml @@ -15,10 +15,10 @@ + + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index df3e71ce71..58f3269629 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 جاري تحميل الملفات المطلوبة جاري تحميل الملفات المطلوبة @@ -242,6 +243,7 @@ الوضع العربي تشغيل إيقاف + سرعة التشغيل: تم تحسين الصور الخاصه بالتابلت. هل تود تحميلها الان؟ رتب حسب التصنيفات @@ -320,6 +322,6 @@ مكان غير مجلد التطبيق الافتراضي قد يؤدي إلى عدم تمكن التطبيق من الوصول لملفاته في إصدارات أندرويد القادمة، هل ترغب في نقل البيانات على أية حال؟ - " ، " + "،" 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 4fa7dab4ec..eb224010b9 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,48 @@ 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 +364,18 @@ 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 + Afspeelsnelheid: 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 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/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-rBR/strings.xml b/app/src/main/res/values-pt/strings.xml similarity index 99% rename from app/src/main/res/values-pt-rBR/strings.xml rename to app/src/main/res/values-pt/strings.xml index e283b0fab6..758f9d7146 100644 --- a/app/src/main/res/values-pt-rBR/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/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..4efbfb686f 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -2,11 +2,12 @@ قۇرئان لازىملىق ھۆججەتلەرنى چۈشۈرەمدۇ؟ - ئەڭ ياخشى ئۈنۈمگە ئېرىشىش ئۈچۈن، بەزى ھۆججەتلەرنى چۈشۈرۈش زۆرۈر. ئەگەر ھازىر بۇنداق قىلىشنى تاللىمىسىڭىز، ھەر بىر بەتنى يۈكلەشكە تېخىمۇ ئۇزۇن ۋاقىت كېتىدۇ. لازىملىق ھۆججەتلەرنى ھازىرلا چۈشۈرەمسىز؟ + قۇرئان ئاندىرويىدنىڭ نورمال ئىشلىشى ئۈچۈن بەزى ھۆججەتلەرنى چۈشۈرۈشىمىز كېرەك. ئەگەر ھازىر بۇنى قىلمىسىڭىز، بۇ دېتال ئىشەنچلىك ئىشلىمەسلىكى مۇمكىن، ئوقۇش ئۈچۈن ئىنتېرنېت ئۇلىنىشى تەلەپ قىلىنىدۇ. لازىملىق ھۆججەتلەرنى ھازىر چۈشۈرەمسىز؟ بىز يېقىندا تاختا كومپيۇتېر ئۈچۈن ياخشىلانغان سۈرەتلەرنى قوشتۇق. ئۇ سۈرەتلەرنى ھازىر چۈشۈرەمسىز؟ ئۈسكۈنىڭىز ئۈچۈن ئۆزى كىچىك ئەمما مۇھىم بولغان قۇرئان سۈرەتلىرىنىڭ يېڭىلانمىسى بار. بۇ ياماقنى ھازىرلا چۈشۈرەمسىز؟ ھەئە ياق + چۈشۈرۈۋاتىدۇ… ھۆججەتلەرنى چۈشۈرۈشنى كۈتۈڭ. (داۋاملاشتۇرۇشنى قوللايدۇ) بىر تەرەپ قىلىۋاتىدۇ… بىز ھەققىدە @@ -26,15 +27,44 @@ يۆتكەل بەتكە يۆتكەل + + ئاندىرويىد قۇرئان ھەقسىز قۇرئان ئەپى. نامازلىرىڭىزدا تۆھپىكارلارغا دۇئا قىلىشنى ئۇنۇتماڭ. + سانلىق مەلۇمات مەنبەسى + سۈرەتلەر + قۇرئان سۈرەتلىرى پادىشاھ فاھىد قۇرئان باسما زاۋۇتىنىڭ خەت نۇسخىسىنى ئاساس قىلغان + 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 +75,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 +176,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 +203,7 @@ ھۆججەت بۇزۇلغان، قايتا چۈشۈرۈشنى سىناۋاتىدۇ تور خاتالىقى، داۋاملاشتۇرۇشنى سىناۋاتىدۇ… چۈشۈرۈشتىن ۋاز كەچتى - سىز wifi تورىدا ئەمەس. سانلىق مەلۇماتنى چۈشۈرىۋېرەمسىز؟ + سىز wifi تورىدا ئەمەس. سانلىق مەلۇماتنى چۈشۈرۈۋېرەمسىز؟ بىر تەرەپ قىلىۋاتقىنى %1$d / %2$d @@ -138,6 +211,9 @@ ۋاز كەچ ھەمبەھىر ۋە تەرجىمە ئىقتىدارىنى قوللاش ئۈچۈن بىر ياكى ئىككى كىچىك ھۆججەت چۈشۈرۈشىمىز كېرەك. ھازىر چۈشۈرەمدۇ؟ + + قۇرئان ئاندىرويىدنىڭ ئۇقتۇرۇش يوللىشىغا يول قويامسىز؟ بۇ پەقەت يېڭىلاشنى چۈشۈرۈش ياكى ئاۋاز ھۆججىتى ئاپتوماتىك يېڭىلانغاندا ئاگاھلاندۇرۇش ئۈچۈن ئىشلىتىلىدۇ. + تەرجىمىنى چىقىرىۋېتەمدۇ؟ سىز %1$s نى راسلا چىقىرىۋېتەمسىز؟ @@ -146,6 +222,9 @@ سىز تېخى ھېچقانداق تەرجىمە/تەپسىر چۈشۈرمىگەن. تەرجىمىگە ئېرىش + + ئەگەر سوئالىڭىزنىڭ تۆۋەندە جاۋابى بولمىسا، سىز quranandroid@gmail.com ئېلخەت ئادرېسقا قوللاش ھەققىدە خەت يازسىڭىز بولىدۇ. بىز نۇرغۇن ئېلخەت تاپشۇرۇۋالىمىز، شۇڭلاشقا ھەممە ئېلخەتكە جاۋاب قايتۇرالىشىمىز ناتايىن. + مەككە مەدىنە @@ -163,96 +242,54 @@ بەلگە تەھرىر يېڭى بەلگە بەلگە قوشۇلمىدى + + بىر تۈر ئۆچۈرۈلدى + %d تۈر ئۆچۈرۈلدى + + يېنىۋال + + يېقىنقى بەتلەر بەلگە ئاتى + بەلگە ئاتى بوش قالمايدۇ! + بەلگە ئاتى مەۋجۇت! خەتكۈچ يوق + + + قۇرئان ئاۋاز ھۆججىتى يېڭىلاش + بىر قانچە قۇرئان ئاۋاز ھۆججىتى يېڭىلاندى. قۇرئان بۇ ھۆججەتلەرنىڭ كۆپەيتىلگەن نۇسخىسىنى چىقىرىۋەتتى، بۇنداق بولغاندا ئەڭ يېڭى نەشرىنى كېلەر قېتىم قايتا قويسىڭىز چۈشۈرگىلى بولىدۇ. + + + قۇرئان كەرىم ئىنىسكىلوپىدىيىسى قويۇش ۋاقتىنچە توختاش باشقا ئەپلەر - گۇرۇپپا - ۋاقىتنى كۆرسىتىش - ساقلاپ تۇرۇڭ… - ئاندىرويىد ئۈچۈن قۇرئان گىرىپتار قۇرئان ئىلتىماس قىلىش. دۇئالىرىڭىزدا تۆھپىكارلىقنى ئۇنتۇپ قالماڭ. - سانلىق مەلۇمات مەنبەسى - رەسىملەر - قۇرئان رەسىملىرى پادىشاھ فەھدتىن خەت نۇسخىنى ئاساس قىلغان - SHL Info Systems دېتالى ئۈچۈن قۇرئان رەسىملىرى (ئىجازەت بىلەن) - قالون رەسىملىرى Nous Memes Editions Et Diffusion (تۇنىس) نىڭ رۇخسىتى بىلەن ئىشلىتىلىدۇ - گالستۇكى MP3 ئوقۇتقۇچىسى پائالىيەت - ئېلېكترونلۇق قۇرئاننىڭ تۈرى - عثمانى نۇسخىسى ۋە ئەرەبچە تەپسىرلەر - نۇرغۇن تىللارغا تەرجىمە - بەزى تىللارغا تەرجىمە - نورحرا خەت نۇسخىسى ۋە مفتى تقى تەرجىمىسى - ئوچۇق كود تۈرلىرى - باشقىلار - تۆھپىكار - ئاندىرويىد ئۈچۈن قۇرئاننى تەرەققىي قىلدۇرۇشقا تۆھپە قوشقانلارنىڭ تىزىملىكى - تولۇق نەتىجىسى - پۈتكۈل مصحف ئىزدەڭ - بۇنىڭ كۈچكە ئىگە بولۇشى ئۈچۈن بۇ دېتالنى قايتا قوزغىتىڭ. - خەتكۈچنى گەۋدىلەندۈرىدۇ - ئايەتلەرنى گەۋدىلەندۈرىدۇ - ئاۋاز باشقۇرۇش ۋە چۈشۈرۈش - خاتىرىسىنى ئەۋەتىش - %1$d م ب - تېخىمۇ كۆپ تەڭشەكلەر - ئىمپورت - خەتكۈچ ۋە خەتكۈچلەرنى ئىمپورت قىلىڭ - ئېكسپورت - خەتكۈچ ۋە خەتكۈچلەرنىڭ كۆپەيتىلگەن نۇسخىسىنى ئېكسپورت قىلىڭ - بەت تىپى (تەجرىبە) - ئوقۇش بېتىنىڭ تۈرىنى تاللاڭ - تەرجىمىسىنى كۆرسىتىڭ سۈرە - مەسىلەن - تېخىمۇ كۆپ تەرجىمىلەر - ئاگاھلاندۇرۇش + بەلگە بويىچە گۇرۇپپىلا + چېسلانى كۆرسەت + سەل كۈتۈڭ… + تولۇق نەتىجە + پۈتكۈل سەھىپەدىن ئىزدە ئالدىنقى - يېقىنقى بەتلەر توختا - سوئالىڭىز يۇقىرىدىكى جاۋاب بولمىسا, قوللايدىغان quranandroid@gmail.com ئېلېكترونلۇق خەت ئەۋەتسىڭىز بولىدۇ. شۇنىڭغا دىققەت قىلىڭكى, بىز نۇرغۇن ئېلېكترونلۇق خەتلەرنى قوبۇل قىلالايمىز, شۇڭا بىز ئۇلارنىڭ ھەممىسىگە جاۋاب بېرەلمەسلىكى مۇمكىن. - ئاۋازلىق ھۆججەتلەر يېڭىلاندى + Ujinu uri vifujio: قوش بەت مايىللىقى - خەتكۈچ ئىسىملىرى بوش ئەمەس! - خەتكۈچ ئىسمى ئاللىبۇرۇن مەۋجۇت! + قوش بەت ھالىتىدىكى قۇرئان ۋە تەرجىمە + تەرجىمە بار قوش بەت ھالىتىدە ، قۇرئان بېتى ۋە تەرجىمە كۆرۈنىدۇ + + بەت ئۈستى - تەرجىمەنى ئۆچۈرۈڭ - يۇقىرىدا يۆتكەڭ - تۆۋەنگە يۆتكەڭ - ئىمپورت مۇۋەپپەقىيەتلىك - سانلىق مەلۇمات ئىمپورت - %1$s - آية %2$s، جزء %3$s ئىلتىماس قىلىڭ ئىلتىماس قىلىڭ ۋە قويۇش كېيىنكى ئويناشنى باشلاڭ: قۇرئان چۈشۈرۈش - قۇرئان تىلاۋەت قىلىش - سانلىق مەلۇمات ئېكسپورتىدا خاتالىق - ئەمەلدىن قالدۇرۇش - بىر قانچە قۇرئان ئاۋاز ھۆججىتى يېڭىلاندى. «ئاندىرويىد ئۈچۈن قۇرئان» بۇ ھۆججەتلەرنىڭ كۆپەيتىلگەن نۇسخىسىنى ئېلىۋەتتى ، بۇنداق بولغاندا ئەڭ يېڭى نەشرىنى كېيىنكى قېتىم قايتا قويسىڭىز بولىدۇ. - قۇرئان ۋە تەرجىمە قوش ھالەتتە - تەرجىمە بىلەن قوش بەت شەكلىدە ، قۇرئان بېتى ۋە تەرجىمىسى كۆرسىتىلدى - ئاچقۇچىلارغا خاتالىق خاتىرىسىنى ئەۋەتىڭ - ئىجازەت خاتالىقى سەۋەبىدىن زاپاس ھۆججەتنى ئوقۇيالمىدى. - ئىناۋەتسىز زاپاس ھۆججەت (ياكى زاپاس ھۆججەتنى ئوقۇيالمايدۇ). - خەتكۈچلەرنى ئەكىرىش / چىقىرىش ، قۇرئان سانلىق مەلۇمات مۇندەرىجىسى قاتارلىقلارنى تەڭشەش. - سۈرىنىڭ ئىسمى تەرجىمە قىلىنغان - %1$s غا ئېكسپورت قىلىنغان سانلىق مەلۇمات - ئەگەر بۇ ھۆججەتنى ئىمپورت قىلسىڭىز ، ئۇ بارلىق خەتكۈچلىرىڭىزنى %1$d خەتكۈچ ۋە %2$d خەتكۈچكە ئالماشتۇرىدۇ. ئىمپورتمۇ؟ + قۇرئان قىرائىتى پەقەت يۇقىرىدىكى ئايەتلەرنىلا ئاڭلاڭ بىر يۈرۈش ئايەتلەرنى ئاڭلاڭ: ھەر بىر ئايەتنى ئاڭلاڭ: - SD كارتىنى تاپالمىدى. ئۇنى ئورنىتىپ قايتا سىناڭ. - ئىلتىماس تەڭشەكلىرىدە ئىجازەت بېرىڭ - بۇ ئايەتنىڭ تەفىرسى %d -ئايەتنىڭ تەفسىر بىلەن ئۆز ئىچىگە ئېلىنغان (كېڭەيتىش ئۈچۈن چېكىڭ). - ئاندىرويىد چەكلىمىسى تۈپەيلىدىن ، ئەگەر سىز قۇرئان سانلىق مەلۇماتلىرىنى سىرتقى SD كارتىڭىزغا قويماقچى بولسىڭىز ، ئاندىن قۇرئان ئاندىرويىد ئۈچۈن سانلىق مەلۇماتلارنى ئۆچۈرۈۋەتسىڭىز ياكى ئۆچۈرسىڭىز ، قۇرئاننىڭ بارلىق ئاندىرويىد بەتلىرى ۋە ئاۋازلىرى ئۆچۈرۈلىدۇ ، ئۇلارنى قايتا چۈشۈرۈشىڭىز كېرەك. سىرتقى SD كارتىنى ئىشلىتىشنى خالامسىز؟ - ئابونتلارنىڭ شەخسى مەخپىيەتلىكىنى ئاشۇرۇش ئۈچۈن ئاندىرويىد ئۆزگىرىشى سەۋەبىدىن ، ھۆججەت مۇندەرىجىسىنىڭ سىرتىدىكى ھۆججەتلەرنى كۆچۈرگەندە «ئاندىرويىد ئۈچۈن قۇرئان» نىڭ ئاندىرويىدنىڭ كەلگۈسى نەشرىدىكى سانلىق مەلۇماتلىرىنى زىيارەت قىلىشىنى توختىتىشى مۇمكىن. بۇ يولنى ئىشلىتىشنى خالامسىز؟ - «ئاندىرويىد ئۈچۈن قۇرئان» سىزنىڭ سانلىق مەلۇماتلىرىڭىزنى سىرتقى ساقلاشقا ساقلىشىڭىزغا رۇخسەت قىلىدۇ. «ئاندىرويىد ئۈچۈن قۇرئان» بۇ ئىجازەتسىز ئىشلەيدۇ ، ئەمما سانلىق مەلۇماتنى ئۆچۈرسىڭىز ياكى ئەپنى ئۆچۈرۈۋەتسىڭىز ، چۈشۈرۈلگەن بارلىق بەتلەر ، ئاۋاز ۋە سانلىق مەلۇماتلار ئۆچۈرۈلىدۇ. «ئاندىرويىد ئۈچۈن قۇرئان» ئىجازەت بېرەمسىز؟ - كىڭ سەئۇد ئۇنۋېرسىتىتى ئېلېكترونلۇق مصحف تۈرى بۇ دېتالنىڭ ئەرەبچە تەپسىرلەر نىڭ مەنبەسى ۋە ھەر خىل تىللارنىڭ تەرجىمىسى. ئۇ يەنە دوكتور ئايمان سۇۋەيدنىڭ دېكلاماتسىيە قىلىشىنىڭ مەنبەسى. 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..73d901f9b5 --- /dev/null +++ b/app/src/main/res/values-ug/sura_names.xml @@ -0,0 +1,119 @@ + + + + فاتىھە + بەقەرە + ئال ئىمران + نىسا + مائىدە + ئەنئام + ئەئراف + ئەنفال + تەۋبە + يۇنۇس + ھۇد + يۈسۈف + رەئىد + ئىبراھىم + ھىجر + نەھل + ئىسرا + كەھف + مەريەم + تاھا + ئەنبىيا + ھەج + مۆئمىنۇن + نۇر + فۇرقان + شۇئەرا + نەمل + قەسەس + ئەنكەبۇت + رۇم + لوقمان + سەجدە + ئەھزاب + سەبەئ + فاتىر + ياسىن + ساففات + ساد + زۇمەر + غافىر + فۇسسىلەت + شۇرا + زۇخرۇف + دۇخان + جاسىيە + ئەھقاف + مۇھەممەد + فەتىھ + ھۇجۇرات + قاف + زارىيات + تۇر + نەجم + قەمەر + رەھمان + ۋاقىئە + ھەدىد + مۇجادەلە + ھەشر + مۇمتەھىنە + سەپ + جۇمۇئە + مۇنافىقۇن + تەغابۇن + تەلاق + تەھرىم + مۇلك + قەلەم + ھاققە + مائارىج + نۇھ + جىن + مۇززەممىل + مۇددەسسىر + قىيامەت + ئىنسان + مۇرسەلات + نەبە + نازىئات + ئەبەسە + تەكۋىر + ئىنفىتار + مۇتەففىفىن + ئىنشىقاق + بۇرۇج + تارىق + ئەئلا + غاشىيە + ھىجر + بەلەد + شەمس + لەيل + زۇھا + ئىنشىراھ + تىن + ئەلەق + قەدر + بەييىنە + زەلزەلە + ئادىيات + قارىئە + تەكاسۇر + ئەسر + ھۇمەزە + فىل + قۇرەيش + مائۇن + كەۋسەر + كافىرون + نەسر + مەسەد + ئىخلاس + فەلەق + ناس + + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f051400537..c13f1b5640 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 @@ -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 new file mode 100644 index 0000000000..d5cae4af12 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,351 @@ + + + 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 + Tốc độ phát lại: + 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/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 @@ 播放 暂停 停止 + 播放速度: 下一页 开始播放从。 页首 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/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/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fca7f16f62..6bc12f4030 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 @@ -309,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? @@ -342,6 +346,7 @@ Pause Stop Next + Playback Speed: 1 time 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 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/app/src/test/java/com/quran/labs/androidquran/base/TestApplication.kt b/app/src/test/java/com/quran/labs/androidquran/base/TestApplication.kt index 0e73ca3401..f01d7d0f15 100644 --- a/app/src/test/java/com/quran/labs/androidquran/base/TestApplication.kt +++ b/app/src/test/java/com/quran/labs/androidquran/base/TestApplication.kt @@ -3,14 +3,12 @@ package com.quran.labs.androidquran.base import com.quran.labs.androidquran.QuranApplication import com.quran.labs.androidquran.di.DaggerTestApplicationComponent import com.quran.labs.androidquran.di.component.application.ApplicationComponent -import com.quran.labs.androidquran.di.module.application.ApplicationModule class TestApplication : QuranApplication() { override fun initializeInjector(): ApplicationComponent { - return DaggerTestApplicationComponent.builder() - .applicationModule(ApplicationModule(this)) - .build() + return DaggerTestApplicationComponent.factory() + .generate(this) } override fun setupTimber() { 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/app/src/test/java/com/quran/labs/androidquran/di/TestApplicationComponent.kt b/app/src/test/java/com/quran/labs/androidquran/di/TestApplicationComponent.kt index 0f6b0ffed6..e3ac1d7ae2 100644 --- a/app/src/test/java/com/quran/labs/androidquran/di/TestApplicationComponent.kt +++ b/app/src/test/java/com/quran/labs/androidquran/di/TestApplicationComponent.kt @@ -1,5 +1,6 @@ package com.quran.labs.androidquran.di +import android.content.Context import com.quran.analytics.provider.AnalyticsModule import com.quran.common.networking.NetworkModule import com.quran.data.di.AppScope @@ -11,7 +12,10 @@ import com.quran.labs.androidquran.di.module.application.DatabaseModule import com.quran.labs.androidquran.di.module.application.PageAggregationModule import com.quran.labs.androidquran.di.module.widgets.BookmarksWidgetUpdaterModule import com.quran.labs.androidquran.di.quran.TestQuranActivityComponent +import com.quran.mobile.di.qualifier.ApplicationContext import com.squareup.anvil.annotations.MergeComponent +import dagger.BindsInstance +import dagger.Component import javax.inject.Singleton @Singleton @@ -29,5 +33,10 @@ import javax.inject.Singleton ] ) interface TestApplicationComponent : ApplicationComponent { - override fun quranActivityComponentBuilder(): TestQuranActivityComponent.Builder + override fun quranActivityComponentFactory(): TestQuranActivityComponent.Factory + + @Component.Factory + interface Factory { + fun generate(@BindsInstance @ApplicationContext appContext: Context): TestApplicationComponent + } } diff --git a/app/src/test/java/com/quran/labs/androidquran/di/quran/TestQuranActivityComponent.kt b/app/src/test/java/com/quran/labs/androidquran/di/quran/TestQuranActivityComponent.kt index 9fe341ee3a..26daca8f92 100644 --- a/app/src/test/java/com/quran/labs/androidquran/di/quran/TestQuranActivityComponent.kt +++ b/app/src/test/java/com/quran/labs/androidquran/di/quran/TestQuranActivityComponent.kt @@ -8,8 +8,8 @@ import dagger.Subcomponent @Subcomponent(modules = [TestQuranActivityModule::class]) interface TestQuranActivityComponent : QuranActivityComponent { - @Subcomponent.Builder - interface Builder : QuranActivityComponent.Builder { - override fun build(): TestQuranActivityComponent + @Subcomponent.Factory + interface Factory : QuranActivityComponent.Factory { + override fun generate(): TestQuranActivityComponent } } 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..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 @@ -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 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..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,12 +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 @@ -15,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 @@ -26,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, null, - mock(QuranFileUtils::class.java) - ) { - public override fun writeTranslationList(list: TranslationList) { - // no op - } - } - translationManager.host = mockWebServer.url("").toString() } @After @@ -55,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 + fun testGetRemoteTranslationList() = runTest { + enqueueMockResponse() + translationManager().remoteTranslationList().test { + val item = awaitItem() + Truth.assertThat(item.translations).hasSize(57) + awaitComplete() + } } @Test - @Throws(Exception::class) - fun getRemoteTranslationListObservable() { + 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/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/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index 2e49747fd7..191d051a97 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:7.4.2") - compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") + compileOnly(libs.android.gradlePlugin) + compileOnly(libs.kotlin.gradlePlugin) + 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 2c842d673c..97f486a77c 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -2,23 +2,25 @@ 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 { - applyAndroidCommon() - defaultConfig.targetSdk = 32 + applyAndroidCommon(target) + defaultConfig.targetSdk = 34 } applyKotlinCommon() diff --git a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt index 26e22f4854..cfb9e005dc 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt @@ -2,23 +2,40 @@ 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.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 { override fun apply(target: Project) { with(target) { with(pluginManager) { - apply("com.android.library") + withLibraries { libs -> + apply(libs.plugins.android.library.get().pluginId) + apply(libs.plugins.kotlin.android.get().pluginId) + } } extensions.configure { - applyAndroidCommon() + applyAndroidCommon(target) buildFeatures.compose = true - composeOptions.kotlinCompilerExtensionVersion = "1.4.3" + 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() @@ -28,7 +45,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 1ba14fe76d..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,13 +12,14 @@ 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 { - applyAndroidCommon() - defaultConfig.targetSdk = 32 + applyAndroidCommon(target) } 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 9447bd32cd..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 @@ -2,19 +2,26 @@ 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() { - compileSdk = 33 +fun CommonExtension<*, *, *, *, *>.applyAndroidCommon(project: Project) { + compileSdk = 34 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() + } + + lint { + checkReleaseBuilds = true + enable.add("Interoperability") + lintConfig = project.rootProject.file("lint.xml") } } 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..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.01.00")) + withLibraries { libs -> + add("implementation", platform(libs.okhttp.bom)) + add("implementation", platform(libs.compose.bom)) + } } } 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..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(11) + 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") + } + } } } diff --git a/build-logic/gradle/wrapper/gradle-wrapper.properties b/build-logic/gradle/wrapper/gradle-wrapper.properties index bdc9a83b1e..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.0.2-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/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/build.gradle b/build.gradle deleted file mode 100644 index 01e68fed06..0000000000 --- a/build.gradle +++ /dev/null @@ -1,73 +0,0 @@ -buildscript { - ext { - kotlinVersion = '1.8.10' - coroutinesVersion = '1.6.4' - - daggerVersion = '2.45' - - androidxMediaVersion = '1.6.0' - androidxAnnotationVersion = '1.6.0' - androidxFragmentVersion = '1.5.6' - androidxPreferencesVersion = '1.2.0' - androidxAppcompatVersion = '1.6.1' - androidxLocalBroadcastVersion = '1.1.0' - androidxSwipeRefreshVersion = '1.1.0' - androidxRecyclerViewVersion = '1.3.0' - supportSqliteVersion = '2.1.0' - workManagerVersion = '2.8.1' - materialComponentsVersion = '1.8.0' - coreKtxVersion = '1.9.0' - - anvilVersion = '2.4.4' - moshiVersion = '1.14.0' - okioVersion = '3.3.0' - retrofitVersion = '2.9.0' - sqldelightVersion = '2.0.0-alpha05' - - // testing - junitVersion = '4.13.2' - mockitoVersion = '5.2.0' - truthVersion = '1.1.3' - espressoVersion = '3.5.1' - robolectricVersion = '4.9.2' - 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}" - ] - ] - } - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - dependencies { - classpath "com.android.tools.build:gradle:7.4.2" - classpath "com.google.firebase:firebase-crashlytics-gradle:2.9.4" - 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" - 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..7f6bee1a6f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,11 @@ +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.ksp) 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..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 "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation // dagger - kapt deps.dagger.apt - implementation deps.dagger.runtime + implementation libs.dagger.runtime } 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/common/audio/build.gradle b/common/audio/build.gradle index e80524aad4..543f10a990 100644 --- a/common/audio/build.gradle +++ b/common/audio/build.gradle @@ -9,15 +9,16 @@ anvil { generateDaggerFactories = true } android.namespace 'com.quran.labs.androidquran.common.audio' dependencies { + implementation project(":common:di") 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/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 77% 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 d24f389fff..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,8 +1,8 @@ -package com.quran.labs.androidquran.dao.audio +package com.quran.labs.androidquran.common.audio.model.playback 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,8 +12,10 @@ 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 { + val audioPathInfo: AudioPathInfo +) : Parcelable { fun isGapless() = qari.isGapless fun needsIsti3athaAudio() = !isGapless() || audioPathInfo.gaplessDatabase?.contains("minshawi_murattal") ?: false 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/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..09d324edec 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 @@ -4,21 +4,25 @@ import android.content.Context import android.preference.PreferenceManager import com.quran.data.model.audio.Qari import com.quran.labs.androidquran.common.audio.util.QariUtil -import javax.inject.Inject -import javax.inject.Singleton +import com.quran.mobile.di.qualifier.ApplicationContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton @Singleton -class CurrentQariManager @Inject constructor(appContext: Context, private val qariUtil: QariUtil) { +class CurrentQariManager @Inject constructor(@ApplicationContext 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/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-ms/readers.xml similarity index 91% rename from common/audio/src/main/res/values-my/readers.xml rename to common/audio/src/main/res/values-ms/readers.xml index 8176420fed..9c1225e304 100644 --- a/common/audio/src/main/res/values-my/readers.xml +++ b/common/audio/src/main/res/values-ms/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 new file mode 100644 index 0000000000..075488ec0c --- /dev/null +++ b/common/audio/src/main/res/values-nl/readers.xml @@ -0,0 +1,83 @@ + + + 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 + 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 new file mode 100644 index 0000000000..4a09f54444 --- /dev/null +++ b/common/audio/src/main/res/values-vi/readers.xml @@ -0,0 +1,83 @@ + + + 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 + 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 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/bookmark/build.gradle b/common/bookmark/build.gradle index d13fb55f33..6a0fafaccb 100644 --- a/common/bookmark/build.gradle +++ b/common/bookmark/build.gradle @@ -19,17 +19,18 @@ sqldelight { } dependencies { + implementation project(path: ':common:di') 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/bookmark/src/main/java/com/quran/mobile/bookmark/di/BookmarkDataModule.kt b/common/bookmark/src/main/java/com/quran/mobile/bookmark/di/BookmarkDataModule.kt index 39de6e2e45..d6922676ef 100644 --- a/common/bookmark/src/main/java/com/quran/mobile/bookmark/di/BookmarkDataModule.kt +++ b/common/bookmark/src/main/java/com/quran/mobile/bookmark/di/BookmarkDataModule.kt @@ -10,6 +10,7 @@ import com.quran.data.di.AppScope import com.quran.labs.androidquran.BookmarksDatabase import com.quran.mobile.bookmark.Bookmarks import com.quran.mobile.bookmark.Last_pages +import com.quran.mobile.di.qualifier.ApplicationContext import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides @@ -22,7 +23,7 @@ class BookmarkDataModule { @Singleton @Provides - fun provideBookmarksDatabase(context: Context, settings: Settings): BookmarksDatabase { + fun provideBookmarksDatabase(@ApplicationContext context: Context, settings: Settings): BookmarksDatabase { val driver: SqlDriver = AndroidSqliteDriver( schema = BookmarksDatabase.Schema, context = context, 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() } } diff --git a/common/data/build.gradle b/common/data/build.gradle index a26ef43725..558cc40e43 100644 --- a/common/data/build.gradle +++ b/common/data/build.gradle @@ -1,18 +1,20 @@ plugins { id 'quran.android.library' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil + alias libs.plugins.ksp } +anvil { generateDaggerFactories = true } + 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 + implementation libs.dagger.runtime - implementation "com.squareup.moshi:moshi:${moshiVersion}" - kapt("com.squareup.moshi:moshi-kotlin-codegen:${moshiVersion}") + implementation libs.moshi + ksp(libs.moshi.codegen) } 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 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..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 @@ -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 @@ -19,10 +20,15 @@ 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 skip = quranDataSource.pagesToSkip + private val firstPage = PAGES_FIRST + skip + val numberOfPages = quranDataSource.numberOfPages - 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] @@ -32,6 +38,19 @@ 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 isValidPage(page: Int): Boolean { + return page in firstPage..numberOfPages + } + fun getSuraNumberFromPage(page: Int): Int { var sura = -1 for (i in 0 until NUMBER_OF_SURAS) { @@ -73,7 +92,7 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { val page = when { inputPage > numberOfPages -> numberOfPages - inputPage < 1 -> 1 + inputPage < firstPage -> firstPage else -> inputPage } @@ -173,9 +192,12 @@ class QuranInfo @Inject constructor(quranDataSource: QuranDataSource) { isDualPagesVisible: Boolean ): Int { return if (isDualPagesVisible) { - (numberOfPagesDual - position) * 2 + // 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 { - numberOfPages - position + (numberOfPagesConsideringSkipped - position) + skip } } @@ -185,9 +207,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 024fd1f7ed..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 @@ -6,4 +6,6 @@ interface QuranPageInfo { fun displayRub3(page: Int): String 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/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) } 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..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 @@ -13,6 +13,10 @@ interface Settings { suspend fun shouldShowHeaderFooter(): Boolean 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 } 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..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 @@ -225,4 +225,8 @@ 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() + override val haveSidelines: Boolean = false + override val pagesToSkip: Int = 0 } 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..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,5 +1,6 @@ package com.quran.data.source -enum class PageContentType { - IMAGE, LINE +sealed class 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/PageProvider.kt b/common/data/src/main/java/com/quran/data/source/PageProvider.kt index c40ea35d8b..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 @@ -26,8 +26,9 @@ 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 getDefaultQariId(): Int fun pageType(): String = "" } 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..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 @@ -13,4 +13,7 @@ interface QuranDataSource { val isMakkiBySuraArray: BooleanArray val quarterStartByPage: IntArray val quartersArray: Array + val manzilPageArray: Array + val haveSidelines: Boolean + val pagesToSkip: Int } 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/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/common/di/src/main/java/com/quran/mobile/di/qualifier/ActivityContext.kt b/common/di/src/main/java/com/quran/mobile/di/qualifier/ActivityContext.kt new file mode 100644 index 0000000000..fd7c96d6fc --- /dev/null +++ b/common/di/src/main/java/com/quran/mobile/di/qualifier/ActivityContext.kt @@ -0,0 +1,7 @@ +package com.quran.mobile.di.qualifier + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ActivityContext diff --git a/common/di/src/main/java/com/quran/mobile/di/qualifier/ApplicationContext.kt b/common/di/src/main/java/com/quran/mobile/di/qualifier/ApplicationContext.kt new file mode 100644 index 0000000000..00f4ac23c7 --- /dev/null +++ b/common/di/src/main/java/com/quran/mobile/di/qualifier/ApplicationContext.kt @@ -0,0 +1,7 @@ +package com.quran.mobile.di.qualifier + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class ApplicationContext 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..ac636df6bf 100644 --- a/common/networking/build.gradle +++ b/common/networking/build.gradle @@ -1,12 +1,13 @@ plugins { id 'quran.android.library' - id 'org.jetbrains.kotlin.kapt' + alias libs.plugins.anvil } +anvil { generateDaggerFactories = true } + dependencies { - kapt deps.dagger.apt - implementation deps.dagger.runtime + 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..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 "androidx.fragment:fragment-ktx:${androidxFragmentVersion}" + implementation libs.androidx.fragment.ktx - kapt deps.dagger.apt - implementation deps.dagger.runtime + implementation libs.dagger.runtime } 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..5e6ffb689d --- /dev/null +++ b/common/preference/src/main/res/values/preferences_keys.xml @@ -0,0 +1,6 @@ + + + readingCategoryKey + showSidelines + showLineDividers + 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/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 -} 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/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/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/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", ) 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/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/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/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/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 82% 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..a69fd4b5ee 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? = "", @@ -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 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 0000000000..38d0fcbe1b Binary files /dev/null and b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/1.db differ 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 0000000000..0b46c7c320 Binary files /dev/null and b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/2.db differ 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 0000000000..fedc3da408 Binary files /dev/null and b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/3.db differ 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 0000000000..8900301f84 Binary files /dev/null and b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/4.db differ 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 0000000000..5e91a4d4fc Binary files /dev/null and b/common/translation/src/main/sqldelight/com/quran/mobile/translation/databases/5.db differ 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..53c103dfcb --- /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 translator_foreign, updated in v2.7.3 +CREATE TABLE translations_migration ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + translator_foreign 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..1a76a70d1c --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/2.sqm @@ -0,0 +1,18 @@ +-- adds languageCode - updated in v2.7.5 +CREATE TABLE translations_migration ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + translator TEXT, + translator_foreign 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, translator_foreign, 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..0efe75c829 --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/3.sqm @@ -0,0 +1,20 @@ +-- 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, + 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, translator_foreign, 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..cf3386ab2c --- /dev/null +++ b/common/translation/src/main/sqldelight/com/quran/mobile/translation/migrations/4.sqm @@ -0,0 +1,20 @@ +-- adds userDisplayOrder, updated in v3.0.2 +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/common/ui/core/build.gradle b/common/ui/core/build.gradle index a546adeca9..923d4f8dbc 100644 --- a/common/ui/core/build.gradle +++ b/common/ui/core/build.gradle @@ -1,15 +1,14 @@ plugins { - id 'quran.android.library.android' id 'quran.android.library.compose' } 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/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/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/common/upgrade/build.gradle b/common/upgrade/build.gradle index d87bba6bd1..50a495d32b 100644 --- a/common/upgrade/build.gradle +++ b/common/upgrade/build.gradle @@ -1,15 +1,16 @@ 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 deps.dagger.apt - implementation deps.dagger.runtime + 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..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 "androidx.annotation:annotation:${androidxAnnotationVersion}" + implementation libs.androidx.annotation // dagger - kapt deps.dagger.apt - implementation deps.dagger.runtime + implementation libs.dagger.runtime } 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 +} diff --git a/feature/audio/build.gradle b/feature/audio/build.gradle index 7fff402805..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 { @@ -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 + ksp(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 3f55642f89..aa2d815065 100644 --- a/feature/downloadmanager/build.gradle +++ b/feature/downloadmanager/build.gradle @@ -1,19 +1,9 @@ plugins { - id 'quran.android.library.android' id 'quran.android.library.compose' 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 } @@ -26,22 +16,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.0" + 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/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 b1556a96e7..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 @@ -32,7 +32,7 @@ class AudioManagerActivity : ComponentActivity() { val injector = (application as? QuranApplicationComponentProvider) ?.provideQuranApplicationComponent() as? DownloadManagerComponentInterface - injector?.downloadManagerComponentBuilder()?.build()?.inject(this) + injector?.downloadManagerComponentFactory()?.generate()?.inject(this) val downloadedShuyookhFlow = audioManagerPresenter.downloadedShuyookh { QariItem.fromQari(this, it) } @@ -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/SheikhAudioDownloadsActivity.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/SheikhAudioDownloadsActivity.kt index 346beeb383..bc74ee1552 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) @@ -52,7 +67,7 @@ class SheikhAudioDownloadsActivity : ComponentActivity() { val injector = (application as? QuranApplicationComponentProvider) ?.provideQuranApplicationComponent() as? DownloadManagerComponentInterface - injector?.downloadManagerComponentBuilder()?.build()?.inject(this) + injector?.downloadManagerComponentFactory()?.generate()?.inject(this) val isRtl = SearchTextUtil.isRtl(quranNaming.getSuraNameWithNumber(this, 1, false)) val suras = (1..114).map { @@ -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/di/DownloadManagerComponent.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/di/DownloadManagerComponent.kt index dd62a470bb..b6a8f62638 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/di/DownloadManagerComponent.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/di/DownloadManagerComponent.kt @@ -11,8 +11,8 @@ interface DownloadManagerComponent { fun inject(audioManagerActivity: AudioManagerActivity) fun inject(sheikhAudioDownloadsActivity: SheikhAudioDownloadsActivity) - @Subcomponent.Builder - interface Builder { - fun build(): DownloadManagerComponent + @Subcomponent.Factory + interface Factory { + fun generate(): DownloadManagerComponent } } diff --git a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/di/DownloadManagerComponentInterface.kt b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/di/DownloadManagerComponentInterface.kt index fd471c2013..03efbd86a0 100644 --- a/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/di/DownloadManagerComponentInterface.kt +++ b/feature/downloadmanager/src/main/kotlin/com/quran/mobile/feature/downloadmanager/di/DownloadManagerComponentInterface.kt @@ -5,5 +5,5 @@ import com.squareup.anvil.annotations.ContributesTo @ContributesTo(AppScope::class) interface DownloadManagerComponentInterface { - fun downloadManagerComponentBuilder(): DownloadManagerComponent.Builder + fun downloadManagerComponentFactory(): DownloadManagerComponent.Factory } 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..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 @@ -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/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/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/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/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/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/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/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 diff --git a/feature/qarilist/build.gradle b/feature/qarilist/build.gradle index c7140f2caa..f70d441e83 100644 --- a/feature/qarilist/build.gradle +++ b/feature/qarilist/build.gradle @@ -1,19 +1,9 @@ plugins { - id 'quran.android.library.android' id 'quran.android.library.compose' 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" - ] - } -} +android.namespace 'com.quran.mobile.feature.qarilist' anvil { generateDaggerFactories = true } @@ -22,21 +12,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/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) ) } } 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 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 - 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/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..e6f6d6e9ed --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,188 @@ +[versions] +agp = "8.2.2" +kotlin = "1.9.22" +ksp = "1.9.22-1.0.17" + +# required within the Gradle convention plugins - not unused +compose-compiler = "1.5.8" +composeBomVersion = "2024.01.00" +okhttpBomVersion = "4.12.0" + +# dependencies +anvil = "2.4.9" +coroutinesVersion = "1.7.3" +crashlytics = "2.9.9" +daggerVersion = "2.50" +dnsjavaVersion = "2.1.9" +errorprone = "3.1.0" +errorproneCoreVersion = "2.24.1" +googleServices = "4.4.0" +leakcanaryAndroidVersion = "2.13" +moshiVersion = "1.15.0" +okioVersion = "3.7.0" +retrofitVersion = "2.9.0" +rxandroidVersion = "3.0.2" +rxjavaVersion = "3.1.8" +sqldelight = "2.0.1" +timberVersion = "5.0.1" + +# androidx library versions +androidxAppcompatVersion = "1.6.1" +androidxActivityComposeVersion = "1.8.2" +androidxAnnotationVersion = "1.7.1" +androidxCoreVersion = "1.12.0" +androidxNavigationVersion = "2.7.6" +androidxDynamicAnimationVersion = "1.0.0" +androidxFragmentVersion = "1.6.2" +androidxJunitExtVersion = "1.1.5" +androidxLocalBroadcastVersion = "1.1.0" +androidxMediaVersion = "1.7.0" +androidxPreferencesVersion = "1.2.1" +androidxRecyclerViewVersion = "1.3.2" +androidxSwipeRefreshVersion = "1.1.0" +androidxPagingVersion = "3.2.1" +androidxPagingComposeVersion = "3.2.1" +androidxWorkManagerVersion = "2.9.0" + +# firebase +firebaseAnalyticsVersion = "21.5.0" +firebaseCrashlyticsVersion = "18.6.1" + +# ui libraries +accompanistVersion = "0.34.0" +balloonVersion = "1.6.4" +tooltipComposeVersion = "0.2.0" +insetterVersion = "0.6.1" +materialComponentsVersion = "1.11.0" +numberPickerVersion = "2.4.13" +reorderableComposeVersion = "0.9.6" + +# recitations +grpcOkhttpVersion = "1.61.0" +googleAuthVersion = "1.22.0" +googleCloudSpeechVersion = "4.29.0" + +# testing +junitVersion = "4.13.2" +espressoVersion = "3.5.1" +truthVersion = "1.3.0" +mockitoVersion = "5.10.0" +robolectricVersion = "4.11.1" +turbineVersion = "1.0.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" } + +# 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" } +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 = "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" } + +# 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" } +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" } +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 +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" } + +# 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" } +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" } +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" } +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } +errorprone = { id = "net.ltgt.errorprone", version.ref = "errorprone" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba7710..d64cd49177 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b1e..1af9e0930b 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.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d421c..1aa94a4269 100755 --- a/gradlew +++ b/gradlew @@ -83,10 +83,8 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -# 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"' +# 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 @@ -133,10 +131,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. @@ -144,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 @@ -152,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 @@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then done fi -# 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. + +# 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, 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" \ diff --git a/app/lint.xml b/lint.xml similarity index 100% rename from app/lint.xml rename to lint.xml diff --git a/pages/madani/build.gradle b/pages/madani/build.gradle index 27a8d12843..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 deps.dagger.apt - implementation deps.dagger.runtime + implementation libs.dagger.runtime } 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 -} 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( 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. + diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 285c42583e..0000000000 --- a/settings.gradle +++ /dev/null @@ -1,27 +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: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..2dfbae96b7 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,44 @@ +pluginManagement { + includeBuild("build-logic") + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + maven("https://androidx.dev/storage/compose-compiler/repository/") + } +} + +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:translation") +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")) +}