diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96e4cc848..ae0dbaa7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: name: Automated tests strategy: matrix: - api-level: [ 25, 30, 33, 34 ] + api-level: [ 25, 30, 33, 34, 35 ] fail-fast: true runs-on: ubuntu-22.04 steps: @@ -57,7 +57,7 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_2 ram-size: 3072M @@ -76,7 +76,7 @@ jobs: GRADLE_OPTS: "-Dorg.gradle.internal.http.connectionTimeout=60000 -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.network.retry.max.attempts=6 -Dorg.gradle.internal.network.retry.initial.backOff=2000" with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_2 ram-size: 3072M @@ -118,7 +118,7 @@ jobs: name: Automated tests for PlayStore variant strategy: matrix: - api-level: [ 25, 30, 33, 34 ] + api-level: [ 25, 30, 33, 34, 35 ] fail-fast: true runs-on: ubuntu-22.04 steps: @@ -163,7 +163,7 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_2 ram-size: 2048M @@ -182,7 +182,7 @@ jobs: GRADLE_OPTS: "-Dorg.gradle.internal.http.connectionTimeout=60000 -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.network.retry.max.attempts=6 -Dorg.gradle.internal.network.retry.initial.backOff=2000" with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_2 ram-size: 2048M @@ -199,7 +199,7 @@ jobs: name: Automated tests for Custom app strategy: matrix: - api-level: [ 25, 30, 33, 34 ] + api-level: [ 25, 30, 33, 34, 35 ] fail-fast: true runs-on: ubuntu-22.04 steps: @@ -244,7 +244,7 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_2 ram-size: 2048M @@ -263,7 +263,7 @@ jobs: GRADLE_OPTS: "-Dorg.gradle.internal.http.connectionTimeout=60000 -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.network.retry.max.attempts=6 -Dorg.gradle.internal.network.retry.initial.backOff=2000" with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_2 ram-size: 2048M @@ -280,7 +280,7 @@ jobs: name: Automated tests on Tablet strategy: matrix: - api-level: [ 30, 33, 34 ] + api-level: [ 25, 30, 33, 34, 35 ] fail-fast: true runs-on: ubuntu-22.04 steps: @@ -325,7 +325,7 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_2 ram-size: 2048M @@ -344,7 +344,7 @@ jobs: GRADLE_OPTS: "-Dorg.gradle.internal.http.connectionTimeout=60000 -Dorg.gradle.internal.http.socketTimeout=60000 -Dorg.gradle.internal.network.retry.max.attempts=6 -Dorg.gradle.internal.network.retry.initial.backOff=2000" with: api-level: ${{ matrix.api-level }} - target: default + target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }} arch: x86_64 profile: pixel_c ram-size: 2048M diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/OpeningFilesFromStorageTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/OpeningFilesFromStorageTest.kt index d39ade22e..cc0979e1a 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/OpeningFilesFromStorageTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/OpeningFilesFromStorageTest.kt @@ -133,7 +133,9 @@ class OpeningFilesFromStorageTest : BaseActivityTest() { @Test fun testOpeningFileFromFileManager() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM + ) { activityScenario.onActivity { kiwixMainActivity = it it.navigate(R.id.libraryFragment) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/testutils/TestUtils.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/testutils/TestUtils.kt index 3b7127692..c6c55613c 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/testutils/TestUtils.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/testutils/TestUtils.kt @@ -25,7 +25,6 @@ import android.content.pm.PackageManager import android.graphics.Bitmap import android.os.Build import android.os.Environment -import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.test.core.app.canTakeScreenshot import androidx.test.core.app.takeScreenshot @@ -91,10 +90,6 @@ object TestUtils { Manifest.permission.WRITE_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_GRANTED - @RequiresApi(Build.VERSION_CODES.R) - private fun hasManageExternalStoragePermission(): Boolean = - Environment.isExternalStorageManager() - @JvmStatic fun hasStoragePermission() = Build.VERSION.SDK_INT > Build.VERSION_CODES.M && hasReadExternalStoragePermission() && hasWriteExternalStoragePermission() diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/KiwixMainActivity.kt b/app/src/main/java/org/kiwix/kiwixmobile/main/KiwixMainActivity.kt index 41bc521db..5a3d644a1 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/main/KiwixMainActivity.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/main/KiwixMainActivity.kt @@ -51,6 +51,7 @@ import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.downloader.downloadManager.DOWNLOAD_NOTIFICATION_TITLE +import org.kiwix.kiwixmobile.core.extensions.applyEdgeToEdgeInsets import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.main.ACTION_NEW_TAB import org.kiwix.kiwixmobile.core.main.CoreMainActivity @@ -122,10 +123,12 @@ class KiwixMainActivity : CoreMainActivity() { setContentView(activityKiwixMainBinding.root) navController.addOnDestinationChangedListener(finishActionModeOnDestinationChange) - activityKiwixMainBinding.drawerNavView.setupWithNavController(navController) - activityKiwixMainBinding.drawerNavView.setNavigationItemSelectedListener { item -> - closeNavigationDrawer() - onNavigationItemSelected(item) + activityKiwixMainBinding.drawerNavView.apply { + setupWithNavController(navController) + setNavigationItemSelectedListener { item -> + closeNavigationDrawer() + onNavigationItemSelected(item) + } } activityKiwixMainBinding.bottomNavView.setupWithNavController(navController) lifecycleScope.launch { @@ -134,6 +137,7 @@ class KiwixMainActivity : CoreMainActivity() { handleZimFileIntent(intent) handleNotificationIntent(intent) handleGetContentIntent(intent) + activityKiwixMainBinding.root.applyEdgeToEdgeInsets() } private suspend fun migrateInternalToPublicAppDirectory() { diff --git a/app/src/main/res/layout/activity_kiwix_main.xml b/app/src/main/res/layout/activity_kiwix_main.xml index 9679f3981..111f83368 100644 --- a/app/src/main/res/layout/activity_kiwix_main.xml +++ b/app/src/main/res/layout/activity_kiwix_main.xml @@ -21,8 +21,7 @@ android:id="@+id/navigation_container" android:layout_width="match_parent" android:layout_height="match_parent" - android:contentDescription="@string/open_drawer" - android:fitsSystemWindows="true"> + android:contentDescription="@string/open_drawer"> @@ -62,7 +60,6 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="end" - android:fitsSystemWindows="true" app:headerLayout="@layout/drawer_right" /> diff --git a/app/src/main/res/layout/activity_language.xml b/app/src/main/res/layout/activity_language.xml index 00aed9c19..d1bc2a370 100644 --- a/app/src/main/res/layout/activity_language.xml +++ b/app/src/main/res/layout/activity_language.xml @@ -1,24 +1,24 @@ + android:layout_width="match_parent" + android:layout_height="match_parent"> + app:layout_constraintTop_toBottomOf="@id/app_bar" + android:clipToPadding="false" + tools:listitem="@layout/item_language" /> diff --git a/app/src/main/res/layout/fragment_destination_download.xml b/app/src/main/res/layout/fragment_destination_download.xml index f07c90f28..2714feb64 100644 --- a/app/src/main/res/layout/fragment_destination_download.xml +++ b/app/src/main/res/layout/fragment_destination_download.xml @@ -49,6 +49,7 @@ android:layout_height="match_parent" android:contentDescription="@string/library" android:scrollbars="vertical" + android:clipToPadding="false" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/item_download" /> diff --git a/app/src/main/res/layout/fragment_destination_library.xml b/app/src/main/res/layout/fragment_destination_library.xml index 31086b8dd..462076d01 100644 --- a/app/src/main/res/layout/fragment_destination_library.xml +++ b/app/src/main/res/layout/fragment_destination_library.xml @@ -21,8 +21,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true"> + android:layout_height="match_parent"> @@ -80,6 +79,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@android:color/transparent" + android:clipToPadding="false" android:visibility="invisible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -161,6 +161,7 @@ android:layout_width="match_parent" android:layout_height="0dp" android:background="@android:color/transparent" + android:clipToPadding="false" android:contentDescription="@string/files_for_transfer" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt index c4d3c09db..333c6b315 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt @@ -22,6 +22,7 @@ import android.app.Application import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.os.Build import com.jraska.livedata.test import io.mockk.clearAllMocks import io.mockk.every @@ -51,6 +52,8 @@ import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener import org.kiwix.kiwixmobile.core.zim_manager.ConnectivityBroadcastReceiver import org.kiwix.kiwixmobile.core.zim_manager.Language import org.kiwix.kiwixmobile.core.zim_manager.NetworkState +import org.kiwix.kiwixmobile.core.zim_manager.NetworkState.CONNECTED +import org.kiwix.kiwixmobile.core.zim_manager.NetworkState.NOT_CONNECTED import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.NORMAL import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem @@ -58,8 +61,6 @@ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDis import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile -import org.kiwix.kiwixmobile.core.zim_manager.NetworkState.CONNECTED -import org.kiwix.kiwixmobile.core.zim_manager.NetworkState.NOT_CONNECTED import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions.MultiModeFinished import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions.RequestDeleteMultiSelection import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions.RequestMultiSelection @@ -139,7 +140,12 @@ class ZimManageViewModelTest { every { newLanguagesDao.languages() } returns languages every { fat32Checker.fileSystemStates } returns fileSystemStates every { connectivityBroadcastReceiver.networkStates } returns networkStates - every { application.registerReceiver(any(), any()) } returns mockk() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + every { application.registerReceiver(any(), any(), any()) } returns mockk() + } else { + @Suppress("UnspecifiedRegisterReceiverFlag") + every { application.registerReceiver(any(), any()) } returns mockk() + } every { dataSource.booksOnDiskAsListItems() } returns booksOnDiskListItems every { connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) @@ -167,8 +173,15 @@ class ZimManageViewModelTest { inner class Context { @Test fun `registers broadcastReceiver in init`() { - verify { - application.registerReceiver(connectivityBroadcastReceiver, any()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + verify { + application.registerReceiver(connectivityBroadcastReceiver, any(), any()) + } + } else { + @Suppress("UnspecifiedRegisterReceiverFlag") + verify { + application.registerReceiver(connectivityBroadcastReceiver, any()) + } } } diff --git a/build.gradle.kts b/build.gradle.kts index 792b02bbe..e5c5dbcd0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ buildscript { dependencies { classpath(Libs.com_android_tools_build_gradle) classpath(Libs.kotlin_gradle_plugin) + classpath(Libs.kotlin_ksp) classpath(Libs.navigation_safe_args_gradle_plugin) classpath(Libs.keeper) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index b174f55a7..bb0074ed9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -11,8 +11,9 @@ repositories { } dependencies { - implementation("com.android.tools.build:gradle:8.1.3") + implementation("com.android.tools.build:gradle:8.7.2") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0") + implementation("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:2.0.0-1.0.24") implementation("org.jacoco:org.jacoco.core:0.8.12") implementation("org.jlleitschuh.gradle:ktlint-gradle:10.3.0") implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20230406-2.0.0") { diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index e6f6facfe..75eb961cf 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -22,11 +22,11 @@ object Config { // Here is a list of all Android versions with their corresponding API // levels: https://apilevels.com/ - const val compileSdk = 34 // SDK version used by Gradle to compile our app. + const val compileSdk = 35 // SDK version used by Gradle to compile our app. const val minSdk = 25 // Minimum SDK (Minimum Support Device) is 25 (Android 7.1 Nougat). - const val targetSdk = 34 // Target SDK (Maximum Support Device) is 34 (Android 14). + const val targetSdk = 35 // Target SDK (Maximum Support Device) is 34 (Android 14). - val javaVersion = JavaVersion.VERSION_1_8 + val javaVersion = JavaVersion.VERSION_17 // Version Information const val versionMajor = 3 // Major version component of the app's version name and version code. diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 232bb99d5..8de3a54d4 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -1,5 +1,3 @@ -import io.opencensus.trace.Tracing - /** * Generated by https://github.com/jmfayard/buildSrcVersions * @@ -104,6 +102,9 @@ object Libs { const val kotlin_stdlib_jdk8: String = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:" + Versions.org_jetbrains_kotlin + const val kotlin_ksp: String = + "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:" + Versions.kotlin_ksp + /** * https://developer.android.com/topic/libraries/architecture/index.html */ @@ -305,6 +306,8 @@ object Libs { */ const val core_ktx: String = "androidx.core:core-ktx:" + Versions.core_ktx + const val androidx_activity: String = "androidx.activity:activity:" + Versions.androidx_activity + /** * https://github.com/kiwix/java-libkiwix */ diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index c23818e29..fa2651dbc 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -14,13 +14,13 @@ object Versions { const val document_file_version: String = "1.0.1" - const val org_jetbrains_kotlinx_kotlinx_coroutines: String = "1.8.1" + const val org_jetbrains_kotlinx_kotlinx_coroutines: String = "1.10.1" const val kotlinx_coroutines_rx3: String = "1.3.9" - const val androidx_test_espresso: String = "3.5.1" + const val androidx_test_espresso: String = "3.6.1" - const val tracing: String = "1.1.0" + const val tracing: String = "1.2.0" const val com_squareup_retrofit2: String = "2.11.0" @@ -28,6 +28,8 @@ object Versions { const val org_jetbrains_kotlin: String = "2.0.0" + const val kotlin_ksp: String = "2.0.0-1.0.24" + const val androidx_navigation: String = "2.5.3" const val navigation_ui_ktx: String = "2.4.1" @@ -46,7 +48,7 @@ object Versions { const val android_arch_lifecycle_extensions: String = "1.1.1" - const val com_android_tools_build_gradle: String = "8.1.3" + const val com_android_tools_build_gradle: String = "8.7.2" const val de_fayard_buildsrcversions_gradle_plugin: String = "0.7.0" @@ -60,9 +62,9 @@ object Versions { const val swipe_refresh_layout: String = "1.1.0" - const val collection_ktx: String = "1.1.0" + const val collection_ktx: String = "1.4.5" - const val preference_ktx: String = "1.2.0" + const val preference_ktx: String = "1.2.1" const val junit_jupiter: String = "5.11.0" @@ -70,7 +72,7 @@ object Versions { const val core_testing: String = "2.2.0" - const val fragment_ktx: String = "1.2.5" + const val fragment_ktx: String = "1.8.5" const val testing_ktx: String = "1.3.0" @@ -86,11 +88,13 @@ object Versions { const val rxandroid: String = "2.1.1" - const val core_ktx: String = "1.9.0" + const val core_ktx: String = "1.15.0" + + const val androidx_activity: String = "1.9.3" const val libkiwix: String = "2.2.3" - const val material: String = "1.8.0" + const val material: String = "1.12.0" const val multidex: String = "2.0.1" @@ -98,13 +102,13 @@ object Versions { const val rxjava: String = "2.2.21" - const val webkit: String = "1.11.0" + const val webkit: String = "1.12.1" const val junit: String = "1.1.5" const val material_show_case_view: String = "1.3.7" - const val roomVersion = "2.5.0" + const val roomVersion = "2.5.2" const val zxing = "3.5.3" diff --git a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt index a87f9f4d3..ed110b738 100644 --- a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt +++ b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt @@ -37,6 +37,7 @@ class AllProjectConfigurer { fun applyPlugins(target: Project) { target.plugins.apply("kotlin-android") target.plugins.apply("kotlin-kapt") + target.plugins.apply("com.google.devtools.ksp") target.plugins.apply("kotlin-parcelize") target.plugins.apply("jacoco") target.plugins.apply("org.jlleitschuh.gradle.ktlint") @@ -77,7 +78,7 @@ class AllProjectConfigurer { } target.tasks.withType(KotlinCompile::class.java) { compilerOptions { - jvmTarget.set(JvmTarget.JVM_1_8) + jvmTarget.set(JvmTarget.JVM_17) freeCompilerArgs.add("-Xjvm-default=all-compatibility") } } @@ -137,7 +138,7 @@ class AllProjectConfigurer { } fun configureCommonExtension(target: Project) { - target.configureExtension> { + target.configureExtension> { lint { abortOnError = true checkAllWarnings = true @@ -231,7 +232,9 @@ class AllProjectConfigurer { implementation(Libs.roomRxjava2) kapt(Libs.roomCompiler) implementation(Libs.tracing) + implementation(Libs.fetch) implementation(Libs.fetchOkhttp) + implementation(Libs.androidx_activity) } } } diff --git a/buildSrc/src/main/kotlin/plugin/ConvenienceExtensions.kt b/buildSrc/src/main/kotlin/plugin/ConvenienceExtensions.kt index 0a397888d..fb6f64721 100644 --- a/buildSrc/src/main/kotlin/plugin/ConvenienceExtensions.kt +++ b/buildSrc/src/main/kotlin/plugin/ConvenienceExtensions.kt @@ -48,6 +48,9 @@ internal fun DependencyHandlerScope.compileOnly(dependency: String) = internal fun DependencyHandlerScope.kapt(dependency: String) = addDependency("kapt", dependency) +internal fun DependencyHandlerScope.ksp(dependency: String) = + addDependency("ksp", dependency) + internal fun DependencyHandlerScope.testImplementation(dependency: String) = addDependency("testImplementation", dependency) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 1d61d4aee..20216805f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -63,8 +63,4 @@ dependencies { implementation(Libs.kotlinx_coroutines_android) implementation(Libs.kotlinx_coroutines_rx3) implementation(Libs.zxing) - api(Libs.fetch) { - // Todo: Will remove this when we add support for Android 15 - exclude("androidx.core", "core-ktx") - } } diff --git a/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt b/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt index e2b56964d..62ccb01af 100644 --- a/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt +++ b/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt @@ -21,12 +21,10 @@ package eu.mhutti1.utils.storage import android.content.Context import android.content.ContextWrapper import android.os.Environment -import androidx.core.content.ContextCompat import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import java.io.File import java.io.FileFilter import java.io.RandomAccessFile -import java.util.ArrayList object StorageDeviceUtils { @JvmStatic @@ -63,7 +61,7 @@ object StorageDeviceUtils { private fun externalFilesDirsDevices( context: Context, writable: Boolean - ) = ContextCompat.getExternalFilesDirs(context, "") + ) = context.getExternalFilesDirs("") .filterNotNull() .mapIndexed { index, dir -> StorageDevice(generalisePath(dir.path, writable), index == 0) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt index 3fc9a0330..73c208b35 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt @@ -116,6 +116,9 @@ abstract class CoreApp : Application() { detectLeakedSqlLiteObjects() penaltyLog() detectLeakedRegistrationObjects() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + detectUnsafeIntentLaunch() + } }.build() ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseActivity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseActivity.kt index 718f6476c..a6ffee420 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseActivity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseActivity.kt @@ -17,8 +17,13 @@ */ package org.kiwix.kiwixmobile.core.base +import android.graphics.Color +import android.os.Build import android.os.Bundle +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity +import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setWindowBackgroundColorForAndroid15AndAbove import org.kiwix.kiwixmobile.core.utils.LanguageUtils import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import javax.inject.Inject @@ -29,7 +34,14 @@ open class BaseActivity : AppCompatActivity() { lateinit var sharedPreferenceUtil: SharedPreferenceUtil override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.dark(Color.BLACK), + navigationBarStyle = SystemBarStyle.dark(Color.BLACK) + ) super.onCreate(savedInstanceState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + setWindowBackgroundColorForAndroid15AndAbove() + } LanguageUtils.handleLocaleChange(this, sharedPreferenceUtil) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseFragment.kt index 77e18b557..c6d4e7e2d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/base/BaseFragment.kt @@ -19,13 +19,16 @@ package org.kiwix.kiwixmobile.core.base import android.content.Context +import android.os.Build import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.extensions.enableEdgeToEdgeMode import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon +import org.kiwix.kiwixmobile.core.extensions.setFragmentBackgroundColorForAndroid15AndAbove import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription /** @@ -46,7 +49,11 @@ abstract class BaseFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + enableEdgeToEdgeMode() setupToolbar() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + setFragmentBackgroundColorForAndroid15AndAbove() + } } // Setup toolbar to handle common back pressed event diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/AdapterDelegateManager.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/AdapterDelegateManager.kt index 82a657fa4..4b6d30687 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/AdapterDelegateManager.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/base/adapter/AdapterDelegateManager.kt @@ -45,7 +45,7 @@ class AdapterDelegateManager { private fun getDelegateIndexFor(item: T): Int { for (index in 0..delegates.size()) { val valueAt = delegates.valueAt(index) - if (valueAt?.isFor(item) == true) { + if (valueAt.isFor(item) == true) { return index } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.kt index 2cd0d1f1f..bf504906f 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.kt @@ -24,7 +24,6 @@ import android.os.Build import android.os.Bundle import android.os.Process import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.launch @@ -37,6 +36,7 @@ import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.queryIntentActiv import org.kiwix.kiwixmobile.core.compat.ResolveInfoFlagsCompat import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.databinding.ActivityKiwixErrorBinding +import org.kiwix.kiwixmobile.core.extensions.applyEdgeToEdgeInsets import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.utils.CRASH_AND_FEEDBACK_EMAIL_ADDRESS @@ -86,6 +86,7 @@ open class ErrorActivity : BaseActivity() { } setupReportButton() activityKiwixErrorBinding?.restartButton?.setOnClickListener { restartApp() } + activityKiwixErrorBinding?.root.applyEdgeToEdgeInsets() } override fun onDestroy() { @@ -101,7 +102,7 @@ open class ErrorActivity : BaseActivity() { val targetedIntents = createEmailIntents(emailIntent, activities) if (activities.isNotEmpty() && targetedIntents.isNotEmpty()) { val chooserIntent = - Intent.createChooser(targetedIntents.removeFirst(), "Send email...") + Intent.createChooser(targetedIntents.removeAt(0), "Send email...") chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray()) sendEmailLauncher.launch(chooserIntent) } else { @@ -245,7 +246,7 @@ open class ErrorActivity : BaseActivity() { """.trimIndent() private fun externalFileDetails(): String = - ContextCompat.getExternalFilesDirs(this, null).joinToString("\n") { it?.path ?: "null" } + getExternalFilesDirs(null).joinToString("\n") { it?.path ?: "null" } private fun safeContains(extras: Bundle): Boolean { return try { @@ -272,7 +273,7 @@ open class ErrorActivity : BaseActivity() { private val versionName: String @SuppressLint("WrongConstant") get() = packageManager - .getPackageInformation(packageName, ZERO).versionName + .getPackageInformation(packageName, ZERO).versionName.toString() private fun toStackTraceString(exception: Throwable): String = try { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ActivityExtensions.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ActivityExtensions.kt index 7d4f2515c..bc8753e0a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ActivityExtensions.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ActivityExtensions.kt @@ -24,11 +24,13 @@ import android.app.Activity import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration +import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.Environment import android.view.Menu import android.view.MenuItem +import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.Toolbar @@ -199,4 +201,18 @@ object ActivityExtensions { val isWideEnough = configuration.smallestScreenWidthDp >= 600 return isLargeOrXLarge && isWideEnough } + + /** + * Sets the window background color to black for Android 15 and above. + * + * In Android 15, the `setStatusBarColor` method is deprecated and no longer functional. + * As a workaround, this method sets the window background color to black because the + * status bar and navigation bar now inherit the background color of the window. + * + * @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + */ + @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + fun Activity.setWindowBackgroundColorForAndroid15AndAbove() { + window.decorView.setBackgroundColor(Color.BLACK) + } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FragmentExtensions.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FragmentExtensions.kt index 2f7dd640c..0fe2afb55 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FragmentExtensions.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FragmentExtensions.kt @@ -19,15 +19,22 @@ package org.kiwix.kiwixmobile.core.extensions import android.content.Context +import android.graphics.Color +import android.os.Build import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProviders +import com.google.android.material.color.MaterialColors +import org.kiwix.kiwixmobile.core.CoreApp +import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setWindowBackgroundColorForAndroid15AndAbove import org.kiwix.kiwixmobile.core.main.CoreMainActivity inline fun Fragment.viewModel( @@ -55,3 +62,29 @@ fun View.closeKeyboard() { } val Fragment.coreMainActivity get() = activity as CoreMainActivity + +/** + * It enables the edge to edge mode for fragments. + */ +fun Fragment.enableEdgeToEdgeMode() { + activity?.window?.let { + WindowCompat.setDecorFitsSystemWindows(it, false) + } +} + +/** + * We are changing the fragment's background color for android 15 and above. + * @see setWindowBackgroundColorForAndroid15AndAbove for more details. + */ +@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) +fun Fragment.setFragmentBackgroundColorForAndroid15AndAbove() { + this.view?.let { + val darkModeActivity = CoreApp.instance.darkModeConfig.isDarkModeActive() + val windowBackGroundColor = if (darkModeActivity) { + MaterialColors.getColor(it.context, android.R.attr.windowBackground, Color.BLACK) + } else { + MaterialColors.getColor(it.context, android.R.attr.windowBackground, Color.WHITE) + } + it.setBackgroundColor(windowBackGroundColor) + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ViewExtensions.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ViewExtensions.kt index 9584c3228..5b5e3255c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ViewExtensions.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/ViewExtensions.kt @@ -21,13 +21,16 @@ package org.kiwix.kiwixmobile.core.extensions import android.annotation.SuppressLint import android.os.Build import android.view.View +import android.view.ViewGroup import android.view.Window import android.view.WindowManager import androidx.annotation.ColorInt import androidx.appcompat.widget.TooltipCompat +import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat +import androidx.core.view.updateLayoutParams import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar @@ -92,7 +95,7 @@ fun View.showFullScreenMode(window: Window) { WindowCompat.setDecorFitsSystemWindows(window, false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { WindowInsetsControllerCompat(window, window.decorView).apply { - hide(WindowInsetsCompat.Type.statusBars()) + hide(WindowInsetsCompat.Type.systemBars()) hide(WindowInsetsCompat.Type.displayCutout()) systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE @@ -106,10 +109,10 @@ fun View.showFullScreenMode(window: Window) { } fun View.closeFullScreenMode(window: Window) { - WindowCompat.setDecorFitsSystemWindows(window, true) + WindowCompat.setDecorFitsSystemWindows(window, false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { WindowInsetsControllerCompat(window, window.decorView).apply { - show(WindowInsetsCompat.Type.statusBars()) + show(WindowInsetsCompat.Type.systemBars()) show(WindowInsetsCompat.Type.displayCutout()) } } @@ -119,3 +122,31 @@ fun View.closeFullScreenMode(window: Window) { clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) } } + +/** + * Applies edge-to-edge insets to the current view by adjusting its margins + * to account for system bars and display cutouts (e.g., status bar, navigation bar, and notches). + * + * This method ensures that the view avoids overlapping with system UI components by dynamically + * setting margins based on the insets provided by the system. + * + * Usage: Call this method on any view to apply edge-to-edge handling. + */ +fun View?.applyEdgeToEdgeInsets() { + this?.let { + ViewCompat.setOnApplyWindowInsetsListener(it) { view, windowInsets -> + val systemBarsInsets = + windowInsets.getInsets( + WindowInsetsCompat.Type.displayCutout() or + WindowInsetsCompat.Type.systemBars() + ) + view.updateLayoutParams { + topMargin = systemBarsInsets.top + leftMargin = systemBarsInsets.left + bottomMargin = systemBarsInsets.bottom + rightMargin = systemBarsInsets.right + } + WindowInsetsCompat.CONSUMED + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt index 055f1d909..5a353ce2f 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt @@ -1869,9 +1869,9 @@ abstract class CoreReaderFragment : } reopenBook() showTabSwitcher() - setUpWithTextToSpeech(tempWebViewListForUndo.last()) + setUpWithTextToSpeech(tempWebViewListForUndo[tempWebViewListForUndo.lastIndex]) updateBottomToolbarVisibility() - safelyAddWebView(tempWebViewListForUndo.last()) + safelyAddWebView(tempWebViewListForUndo[tempWebViewListForUndo.lastIndex]) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt index ac8638fd1..8278b8128 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt @@ -229,7 +229,7 @@ abstract class CorePrefsFragment : @Suppress("TooGenericExceptionThrown") get() = try { requireActivity().packageManager - .getPackageInformation(requireActivity().packageName, 0).versionName + .getPackageInformation(requireActivity().packageName, 0).versionName.toString() } catch (e: PackageManager.NameNotFoundException) { throw RuntimeException(e) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt index 1e261a2fa..5556425b9 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt @@ -129,7 +129,7 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) { get() = sharedPreferences.getInt(STORAGE_POSITION, 0) fun defaultStorage(): String = - getExternalFilesDirs(context, null)[0]?.path + context.getExternalFilesDirs(null)[0]?.path ?: context.filesDir.path // a workaround for emulators fun defaultPublicStorage(): String = diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt index db3621797..b4d482946 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt @@ -30,7 +30,6 @@ import android.os.storage.StorageManager import android.provider.DocumentsContract import android.provider.MediaStore import android.webkit.URLUtil -import androidx.core.content.ContextCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -583,7 +582,7 @@ object FileUtils { @JvmStatic fun getDemoFilePathForCustomApp(context: Context) = - "${ContextCompat.getExternalFilesDirs(context, null)[0]}/demo.zim" + "${context.getExternalFilesDirs(null)[0]}/demo.zim" @SuppressLint("Recycle") @JvmStatic diff --git a/core/src/main/res/layout/dialog_navigation_history.xml b/core/src/main/res/layout/dialog_navigation_history.xml index 715a74e0d..153001b26 100644 --- a/core/src/main/res/layout/dialog_navigation_history.xml +++ b/core/src/main/res/layout/dialog_navigation_history.xml @@ -18,10 +18,10 @@ --> @@ -30,19 +30,20 @@ android:id="@+id/navigationHistoryRecyclerView" android:layout_width="0dp" android:layout_height="0dp" + android:clipToPadding="false" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - tools:listitem="@layout/item_bookmark_history" - app:layout_constraintTop_toBottomOf="@+id/app_bar" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/app_bar" + tools:listitem="@layout/item_bookmark_history" /> + app:layout_constraintTop_toTopOf="parent" /> diff --git a/core/src/main/res/layout/drawer_right.xml b/core/src/main/res/layout/drawer_right.xml index e1170eccb..fc5003d0c 100644 --- a/core/src/main/res/layout/drawer_right.xml +++ b/core/src/main/res/layout/drawer_right.xml @@ -4,5 +4,5 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="true" - android:fitsSystemWindows="true" + android:clipToPadding="false" android:focusable="true" /> diff --git a/core/src/main/res/layout/fragment_help.xml b/core/src/main/res/layout/fragment_help.xml index 158477da0..e25e528db 100644 --- a/core/src/main/res/layout/fragment_help.xml +++ b/core/src/main/res/layout/fragment_help.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:fitsSystemWindows="true" tools:context=".help.HelpFragment"> @@ -45,6 +44,7 @@ android:id="@+id/activity_help_recycler_view" android:layout_width="match_parent" android:layout_height="0dp" + android:clipToPadding="false" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/core/src/main/res/layout/fragment_page.xml b/core/src/main/res/layout/fragment_page.xml index 9a47dc529..d4e449bd2 100644 --- a/core/src/main/res/layout/fragment_page.xml +++ b/core/src/main/res/layout/fragment_page.xml @@ -3,8 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent" - android:fitsSystemWindows="true"> + android:layout_height="match_parent"> + android:layout_height="match_parent"> + app:layout_constraintStart_toStartOf="parent" /> diff --git a/core/src/main/res/layout/nav_main.xml b/core/src/main/res/layout/nav_main.xml index a22f55299..0f3fd2a6b 100644 --- a/core/src/main/res/layout/nav_main.xml +++ b/core/src/main/res/layout/nav_main.xml @@ -24,7 +24,6 @@ diff --git a/core/src/main/res/layout/reader_fragment_content.xml b/core/src/main/res/layout/reader_fragment_content.xml index b6a0ed7bc..93053de0b 100644 --- a/core/src/main/res/layout/reader_fragment_content.xml +++ b/core/src/main/res/layout/reader_fragment_content.xml @@ -26,8 +26,7 @@ + android:layout_height="match_parent"> + android:layout_height="match_parent"> + android:layout_height="wrap_content" + android:clipToPadding="false" /> diff --git a/core/src/main/res/layout/tab_switcher.xml b/core/src/main/res/layout/tab_switcher.xml index 4ae09a4e5..d242d5acf 100644 --- a/core/src/main/res/layout/tab_switcher.xml +++ b/core/src/main/res/layout/tab_switcher.xml @@ -2,13 +2,13 @@ + android:layout_height="match_parent"> diff --git a/core/src/main/res/values-qq/strings.xml b/core/src/main/res/values-qq/strings.xml index 81084bde6..d75566233 100644 --- a/core/src/main/res/values-qq/strings.xml +++ b/core/src/main/res/values-qq/strings.xml @@ -143,7 +143,7 @@ This is a descriptive message explaining where the Zim files are located after downloading. It is showing on the help screen. {{Identical|Storage}} This is showing on the preference settings screen, it shows the currently selected storage in which we are downloading the zim files, whether it is internal or external. - This refers to free (unused) storage space, ’’not’’ to free as in free of charge. + This refers to free (unused) storage space, ’’not’’ to free as in free of charge. Here %s will be replaced by the free space e.g. 20GB. This message appears in the “Android Toast” as an error message. When there are some files that could not be deleted, due to some reason. {{identical|pause}} {{identical|resume}} diff --git a/core/src/main/res/values/themes.xml b/core/src/main/res/values/themes.xml index 182d7d8f6..c5869f938 100644 --- a/core/src/main/res/values/themes.xml +++ b/core/src/main/res/values/themes.xml @@ -82,7 +82,6 @@ diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt index 3463ba837..44ddfc7a2 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt @@ -25,7 +25,7 @@ import io.objectbox.Box import io.objectbox.query.Query import io.objectbox.query.QueryBuilder import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity @@ -43,7 +43,7 @@ internal class NewRecentSearchDaoTest { @Nested inner class RecentSearchTests { @Test - fun `recentSearches searches by Id passed`() = runBlockingTest { + fun `recentSearches searches by Id passed`() = runTest { val zimId = "id" val queryResult = listOf(recentSearchEntity()) expectFromRecentSearches(queryResult, zimId) @@ -56,7 +56,7 @@ internal class NewRecentSearchDaoTest { } @Test - fun `recentSearches searches with blank Id if null passed`() = runBlockingTest { + fun `recentSearches searches with blank Id if null passed`() = runTest { val queryResult = listOf(recentSearchEntity()) expectFromRecentSearches(queryResult, "") newRecentSearchDao.recentSearches(null) @@ -68,7 +68,7 @@ internal class NewRecentSearchDaoTest { } @Test - fun `recentSearches searches returns distinct entities by searchTerm`() = runBlockingTest { + fun `recentSearches searches returns distinct entities by searchTerm`() = runTest { val queryResult = listOf(recentSearchEntity(), recentSearchEntity()) expectFromRecentSearches(queryResult, "") newRecentSearchDao.recentSearches("") @@ -80,7 +80,7 @@ internal class NewRecentSearchDaoTest { } @Test - fun `recentSearches searches returns a limitedNumber of entities`() = runBlockingTest { + fun `recentSearches searches returns a limitedNumber of entities`() = runTest { val searchResults: List = (0..200).map { recentSearchEntity(searchTerm = "$it") } expectFromRecentSearches(searchResults, "") diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt index 4421df77b..44a849e18 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt @@ -26,7 +26,13 @@ import io.reactivex.plugins.RxJavaPlugins import io.reactivex.processors.PublishProcessor import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.TestScheduler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -51,6 +57,7 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.sharedFunctions.InstantExecutorExtension import org.kiwix.sharedFunctions.setScheduler +@OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(InstantExecutorExtension::class) internal class PageViewModelTest { private val pageDao: PageDao = mockk() @@ -69,6 +76,7 @@ internal class PageViewModelTest { @BeforeEach fun init() { + Dispatchers.setMain(UnconfinedTestDispatcher()) clearAllMocks() every { zimReaderContainer.id } returns "id" every { zimReaderContainer.name } returns "zimName" @@ -77,6 +85,11 @@ internal class PageViewModelTest { viewModel = TestablePageViewModel(zimReaderContainer, sharedPreferenceUtil, pageDao) } + @AfterEach + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun `initial state is Initialising`() { viewModel.state.test().assertValue(pageState()) diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt index b214e7190..79bdb6ae0 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt @@ -24,7 +24,6 @@ import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job @@ -33,11 +32,10 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.assertj.core.api.Assertions.assertThat @@ -84,7 +82,7 @@ internal class SearchViewModelTest { private val zimReaderContainer: ZimReaderContainer = mockk() private val searchResultGenerator: SearchResultGenerator = mockk() private val zimFileReader: ZimFileReader = mockk() - private val testDispatcher = TestCoroutineDispatcher() + private val testDispatcher = StandardTestDispatcher() private val searchMutex: Mutex = mockk() lateinit var viewModel: SearchViewModel @@ -101,7 +99,7 @@ internal class SearchViewModelTest { Dispatchers.resetMain() Dispatchers.setMain(testDispatcher) clearAllMocks() - recentsFromDb = Channel(kotlinx.coroutines.channels.Channel.UNLIMITED) + recentsFromDb = Channel(Channel.UNLIMITED) every { zimReaderContainer.zimFileReader } returns zimFileReader coEvery { searchResultGenerator.generateSearchResults("", zimFileReader) @@ -115,7 +113,7 @@ internal class SearchViewModelTest { @Nested inner class StateTests { @Test - fun `initial state is Initialising`() = runBlockingTest { + fun `initial state is Initialising`() = runTest { viewModel.state.test(this).assertValue( SearchState("", SearchResultsWithTerm("", null, searchMutex), emptyList(), FromWebView) ).finish() @@ -152,12 +150,12 @@ internal class SearchViewModelTest { inner class ActionMapping { @Test - fun `ExitedSearch offers PopFragmentBackstack`() = runBlockingTest { + fun `ExitedSearch offers PopFragmentBackstack`() = runTest { actionResultsInEffects(ExitedSearch, PopFragmentBackstack) } @Test - fun `OnItemClick offers Saves and Opens`() = runBlockingTest { + fun `OnItemClick offers Saves and Opens`() = runTest { val searchListItem = RecentSearchListItem("", "") actionResultsInEffects( OnItemClick(searchListItem), @@ -170,7 +168,7 @@ internal class SearchViewModelTest { } @Test - fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() = runBlockingTest { + fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() = runTest { val searchListItem = RecentSearchListItem("", "") actionResultsInEffects( OnOpenInNewTabClick(searchListItem), @@ -183,7 +181,7 @@ internal class SearchViewModelTest { } @Test - fun `OnItemLongClick offers Saves and Opens`() = runBlockingTest { + fun `OnItemLongClick offers Saves and Opens`() = runTest { val searchListItem = RecentSearchListItem("", "") actionResultsInEffects( OnItemLongClick(searchListItem), @@ -192,12 +190,12 @@ internal class SearchViewModelTest { } @Test - fun `ClickedSearchInText offers SearchInPreviousScreen`() = runBlockingTest { + fun `ClickedSearchInText offers SearchInPreviousScreen`() = runTest { actionResultsInEffects(ClickedSearchInText, SearchInPreviousScreen("")) } @Test - fun `ConfirmedDelete offers Delete and Toast`() = runBlockingTest { + fun `ConfirmedDelete offers Delete and Toast`() = runTest { val searchListItem = RecentSearchListItem("", "") actionResultsInEffects( ConfirmedDelete(searchListItem), @@ -207,7 +205,7 @@ internal class SearchViewModelTest { } @Test - fun `CreatedWithArguments offers SearchArgumentProcessing`() = runBlockingTest { + fun `CreatedWithArguments offers SearchArgumentProcessing`() = runTest { val bundle = mockk() actionResultsInEffects( CreatedWithArguments(bundle), @@ -216,7 +214,7 @@ internal class SearchViewModelTest { } @Test - fun `ReceivedPromptForSpeechInput offers StartSpeechInput`() = runBlockingTest { + fun `ReceivedPromptForSpeechInput offers StartSpeechInput`() = runTest { actionResultsInEffects( ReceivedPromptForSpeechInput, StartSpeechInput(viewModel.actions) @@ -224,7 +222,7 @@ internal class SearchViewModelTest { } @Test - fun `StartSpeechInputFailed offers ShowToast`() = runBlockingTest { + fun `StartSpeechInputFailed offers ShowToast`() = runTest { actionResultsInEffects( StartSpeechInputFailed, ShowToast(string.speech_not_supported) @@ -232,22 +230,29 @@ internal class SearchViewModelTest { } @Test - fun `ActivityResultReceived offers ProcessActivityResult`() = runBlockingTest { + fun `ActivityResultReceived offers ProcessActivityResult`() = runTest { actionResultsInEffects( ActivityResultReceived(0, 1, null), ProcessActivityResult(0, 1, null, viewModel.actions) ) } - private fun TestCoroutineScope.actionResultsInEffects( + private fun TestScope.actionResultsInEffects( action: Action, vararg effects: SideEffect<*> ) { - viewModel.effects - .test(this) - .also { viewModel.actions.trySend(action).isSuccess } - .assertValues(*effects) - .finish() + if (effects.size > 1) return + val collectedEffects = mutableListOf>() + val job = launch { + viewModel.effects.collect { + collectedEffects.add(it) + } + } + + viewModel.actions.trySend(action).isSuccess + advanceUntilIdle() + assertThat(collectedEffects).containsExactlyElementsOf(effects.toList()) + job.cancel() } } @@ -271,34 +276,51 @@ internal class SearchViewModelTest { } } -fun Flow.test(scope: CoroutineScope) = TestObserver(scope, this) +fun Flow.test(scope: TestScope): TestObserver { + val observer = TestObserver(scope, this) + scope.launch { observer.startCollecting() } + return observer +} class TestObserver( - scope: CoroutineScope, - flow: Flow + private val scope: TestScope, + private val flow: Flow ) { private val values = mutableListOf() - private val job: Job = scope.launch { - flow.collect { - values.add(it) + private val completionChannel = Channel() + private var job: Job? = null + + suspend fun startCollecting() { + job = scope.launch { + flow.collect { + values.add(it) + } } + completionChannel.send(Unit) } - fun assertValues(vararg values: T): TestObserver { + private suspend fun awaitCompletion() { + completionChannel.receive() + } + + suspend fun assertValues(vararg values: T): TestObserver { + awaitCompletion() assertThat(values.toList()).containsExactlyElementsOf(this.values) return this } - fun assertValue(value: T): TestObserver { + suspend fun assertValue(value: T): TestObserver { + awaitCompletion() assertThat(values.last()).isEqualTo(value) return this } fun finish() { - job.cancel() + job?.cancel() } - fun assertValue(value: (T) -> Boolean): TestObserver { + suspend fun assertValue(value: (T) -> Boolean): TestObserver { + awaitCompletion() assertThat(values.last()).satisfies({ value(it) }) return this } diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt index 4fb5b1de8..7a4022594 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt @@ -22,7 +22,6 @@ import android.content.Context import android.content.pm.PackageManager import android.content.res.AssetFileDescriptor import android.content.res.AssetManager -import androidx.core.content.ContextCompat import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile @@ -96,7 +95,7 @@ class CustomFileValidator @Inject constructor(private val context: Context) { private fun obbFiles() = scanDirs( - ContextCompat.getObbDirs(context).filterNotNull().filter(File::exists).toTypedArray(), + context.obbDirs.filterNotNull().filter(File::exists).toTypedArray(), "obb" ) @@ -105,7 +104,7 @@ class CustomFileValidator @Inject constructor(private val context: Context) { val directoryList = mutableListOf() // Get the external files directories for the app - ContextCompat.getExternalFilesDirs(context, null).filterNotNull() + context.getExternalFilesDirs(null).filterNotNull() .filter(File::exists) .forEach { dir -> // Check if the directory's parent is not null diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt index 5721c21e6..f9a41b7aa 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomMainActivity.kt @@ -38,6 +38,7 @@ import org.kiwix.kiwixmobile.custom.BuildConfig import org.kiwix.kiwixmobile.custom.R import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.drawable +import org.kiwix.kiwixmobile.core.extensions.applyEdgeToEdgeInsets import org.kiwix.kiwixmobile.custom.customActivityComponent import org.kiwix.kiwixmobile.custom.databinding.ActivityCustomMainBinding @@ -84,6 +85,7 @@ class CustomMainActivity : CoreMainActivity() { super.onCreate(savedInstanceState) activityCustomMainBinding = ActivityCustomMainBinding.inflate(layoutInflater) setContentView(activityCustomMainBinding.root) + activityCustomMainBinding.root.applyEdgeToEdgeInsets() if (savedInstanceState != null) { return } diff --git a/custom/src/main/res/layout/activity_custom_main.xml b/custom/src/main/res/layout/activity_custom_main.xml index 91a95292f..86faa6017 100644 --- a/custom/src/main/res/layout/activity_custom_main.xml +++ b/custom/src/main/res/layout/activity_custom_main.xml @@ -22,7 +22,6 @@ android:id="@+id/custom_drawer_container" android:layout_width="match_parent" android:layout_height="match_parent" - android:fitsSystemWindows="true" tools:ignore="UnusedIds"> @@ -48,6 +46,5 @@ android:layout_height="match_parent" android:layout_gravity="end" android:layout_marginTop="24dp" - android:fitsSystemWindows="true" app:headerLayout="@layout/drawer_right" /> diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f1aa9a835..d190455a7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Dec 19 16:13:45 IST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/lintConfig.xml b/lintConfig.xml index 48d9225f6..e043d2bdc 100644 --- a/lintConfig.xml +++ b/lintConfig.xml @@ -1,25 +1,25 @@ - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -48,4 +48,7 @@ + + +