Merge pull request #4147 from kiwix/Fixes#3990

Added support for Android 15.
This commit is contained in:
Kelson 2025-01-03 17:07:24 +01:00 committed by GitHub
commit 359a1fc4bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
53 changed files with 329 additions and 171 deletions

View File

@ -12,7 +12,7 @@ jobs:
name: Automated tests name: Automated tests
strategy: strategy:
matrix: matrix:
api-level: [ 25, 30, 33, 34 ] api-level: [ 25, 30, 33, 34, 35 ]
fail-fast: true fail-fast: true
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@ -57,7 +57,7 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_2 profile: pixel_2
ram-size: 3072M 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" 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: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_2 profile: pixel_2
ram-size: 3072M ram-size: 3072M
@ -118,7 +118,7 @@ jobs:
name: Automated tests for PlayStore variant name: Automated tests for PlayStore variant
strategy: strategy:
matrix: matrix:
api-level: [ 25, 30, 33, 34 ] api-level: [ 25, 30, 33, 34, 35 ]
fail-fast: true fail-fast: true
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@ -163,7 +163,7 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_2 profile: pixel_2
ram-size: 2048M 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" 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: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_2 profile: pixel_2
ram-size: 2048M ram-size: 2048M
@ -199,7 +199,7 @@ jobs:
name: Automated tests for Custom app name: Automated tests for Custom app
strategy: strategy:
matrix: matrix:
api-level: [ 25, 30, 33, 34 ] api-level: [ 25, 30, 33, 34, 35 ]
fail-fast: true fail-fast: true
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@ -244,7 +244,7 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_2 profile: pixel_2
ram-size: 2048M 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" 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: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_2 profile: pixel_2
ram-size: 2048M ram-size: 2048M
@ -280,7 +280,7 @@ jobs:
name: Automated tests on Tablet name: Automated tests on Tablet
strategy: strategy:
matrix: matrix:
api-level: [ 30, 33, 34 ] api-level: [ 25, 30, 33, 34, 35 ]
fail-fast: true fail-fast: true
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@ -325,7 +325,7 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2 uses: reactivecircus/android-emulator-runner@v2
with: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_2 profile: pixel_2
ram-size: 2048M 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" 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: with:
api-level: ${{ matrix.api-level }} api-level: ${{ matrix.api-level }}
target: default target: ${{ (matrix.api-level == 35) && 'google_apis' || 'default' }}
arch: x86_64 arch: x86_64
profile: pixel_c profile: pixel_c
ram-size: 2048M ram-size: 2048M

View File

@ -133,7 +133,9 @@ class OpeningFilesFromStorageTest : BaseActivityTest() {
@Test @Test
fun testOpeningFileFromFileManager() { 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 { activityScenario.onActivity {
kiwixMainActivity = it kiwixMainActivity = it
it.navigate(R.id.libraryFragment) it.navigate(R.id.libraryFragment)

View File

@ -25,7 +25,6 @@ import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.test.core.app.canTakeScreenshot import androidx.test.core.app.canTakeScreenshot
import androidx.test.core.app.takeScreenshot import androidx.test.core.app.takeScreenshot
@ -91,10 +90,6 @@ object TestUtils {
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED ) == 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 && @JvmStatic fun hasStoragePermission() = Build.VERSION.SDK_INT > Build.VERSION_CODES.M &&
hasReadExternalStoragePermission() && hasWriteExternalStoragePermission() hasReadExternalStoragePermission() && hasWriteExternalStoragePermission()

View File

@ -51,6 +51,7 @@ import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DOWNLOAD_NOTIFICATION_TITLE 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.extensions.toast
import org.kiwix.kiwixmobile.core.main.ACTION_NEW_TAB import org.kiwix.kiwixmobile.core.main.ACTION_NEW_TAB
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
@ -122,11 +123,13 @@ class KiwixMainActivity : CoreMainActivity() {
setContentView(activityKiwixMainBinding.root) setContentView(activityKiwixMainBinding.root)
navController.addOnDestinationChangedListener(finishActionModeOnDestinationChange) navController.addOnDestinationChangedListener(finishActionModeOnDestinationChange)
activityKiwixMainBinding.drawerNavView.setupWithNavController(navController) activityKiwixMainBinding.drawerNavView.apply {
activityKiwixMainBinding.drawerNavView.setNavigationItemSelectedListener { item -> setupWithNavController(navController)
setNavigationItemSelectedListener { item ->
closeNavigationDrawer() closeNavigationDrawer()
onNavigationItemSelected(item) onNavigationItemSelected(item)
} }
}
activityKiwixMainBinding.bottomNavView.setupWithNavController(navController) activityKiwixMainBinding.bottomNavView.setupWithNavController(navController)
lifecycleScope.launch { lifecycleScope.launch {
migrateInternalToPublicAppDirectory() migrateInternalToPublicAppDirectory()
@ -134,6 +137,7 @@ class KiwixMainActivity : CoreMainActivity() {
handleZimFileIntent(intent) handleZimFileIntent(intent)
handleNotificationIntent(intent) handleNotificationIntent(intent)
handleGetContentIntent(intent) handleGetContentIntent(intent)
activityKiwixMainBinding.root.applyEdgeToEdgeInsets()
} }
private suspend fun migrateInternalToPublicAppDirectory() { private suspend fun migrateInternalToPublicAppDirectory() {

View File

@ -21,8 +21,7 @@
android:id="@+id/navigation_container" android:id="@+id/navigation_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:contentDescription="@string/open_drawer" android:contentDescription="@string/open_drawer">
android:fitsSystemWindows="true">
<org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout <org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -53,7 +52,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_main" app:headerLayout="@layout/nav_main"
app:menu="@menu/menu_drawer_main" /> app:menu="@menu/menu_drawer_main" />
@ -62,7 +60,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end" android:layout_gravity="end"
android:fitsSystemWindows="true"
app:headerLayout="@layout/drawer_right" /> app:headerLayout="@layout/drawer_right" />

View File

@ -1,24 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"> android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/layout_standard_app_bar" /> <include layout="@layout/layout_standard_app_bar" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
tools:listitem="@layout/item_language"
android:id="@+id/language_recycler_view" android:id="@+id/language_recycler_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:contentDescription="@string/pref_language_title" android:contentDescription="@string/pref_language_title"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/app_bar" /> app:layout_constraintTop_toBottomOf="@id/app_bar"
android:clipToPadding="false"
tools:listitem="@layout/item_language" />
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/language_progressbar" android:id="@+id/language_progressbar"

View File

@ -22,7 +22,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="org.kiwix.kiwixmobile.webserver.ZimHostFragment"> tools:context="org.kiwix.kiwixmobile.webserver.ZimHostFragment">
<include layout="@layout/layout_toolbar" /> <include layout="@layout/layout_toolbar" />

View File

@ -49,6 +49,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:contentDescription="@string/library" android:contentDescription="@string/library"
android:scrollbars="vertical" android:scrollbars="vertical"
android:clipToPadding="false"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_download" /> tools:listitem="@layout/item_download" />

View File

@ -21,8 +21,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fitsSystemWindows="true">
<org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout <org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -55,6 +54,7 @@
android:id="@+id/zimfilelist" android:id="@+id/zimfilelist"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
android:contentDescription="@string/crash_checkbox_zimfiles" android:contentDescription="@string/crash_checkbox_zimfiles"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -5,7 +5,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:fitsSystemWindows="true"
tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferFragment"> tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferFragment">
@ -80,6 +79,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:clipToPadding="false"
android:visibility="invisible" android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -161,6 +161,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:clipToPadding="false"
android:contentDescription="@string/files_for_transfer" android:contentDescription="@string/files_for_transfer"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -22,6 +22,7 @@ import android.app.Application
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.TRANSPORT_WIFI import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.os.Build
import com.jraska.livedata.test import com.jraska.livedata.test
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every 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.ConnectivityBroadcastReceiver
import org.kiwix.kiwixmobile.core.zim_manager.Language import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.NetworkState 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.MULTI
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.NORMAL import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.NORMAL
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem 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
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile 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.MultiModeFinished
import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions.RequestDeleteMultiSelection import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions.RequestDeleteMultiSelection
import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions.RequestMultiSelection import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions.RequestMultiSelection
@ -139,7 +140,12 @@ class ZimManageViewModelTest {
every { newLanguagesDao.languages() } returns languages every { newLanguagesDao.languages() } returns languages
every { fat32Checker.fileSystemStates } returns fileSystemStates every { fat32Checker.fileSystemStates } returns fileSystemStates
every { connectivityBroadcastReceiver.networkStates } returns networkStates every { connectivityBroadcastReceiver.networkStates } returns networkStates
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 { application.registerReceiver(any(), any()) } returns mockk()
}
every { dataSource.booksOnDiskAsListItems() } returns booksOnDiskListItems every { dataSource.booksOnDiskAsListItems() } returns booksOnDiskListItems
every { every {
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
@ -167,10 +173,17 @@ class ZimManageViewModelTest {
inner class Context { inner class Context {
@Test @Test
fun `registers broadcastReceiver in init`() { fun `registers broadcastReceiver in init`() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
verify {
application.registerReceiver(connectivityBroadcastReceiver, any(), any())
}
} else {
@Suppress("UnspecifiedRegisterReceiverFlag")
verify { verify {
application.registerReceiver(connectivityBroadcastReceiver, any()) application.registerReceiver(connectivityBroadcastReceiver, any())
} }
} }
}
@Test @Test
fun `unregisters broadcastReceiver in onCleared`() { fun `unregisters broadcastReceiver in onCleared`() {

View File

@ -7,6 +7,7 @@ buildscript {
dependencies { dependencies {
classpath(Libs.com_android_tools_build_gradle) classpath(Libs.com_android_tools_build_gradle)
classpath(Libs.kotlin_gradle_plugin) classpath(Libs.kotlin_gradle_plugin)
classpath(Libs.kotlin_ksp)
classpath(Libs.navigation_safe_args_gradle_plugin) classpath(Libs.navigation_safe_args_gradle_plugin)
classpath(Libs.keeper) classpath(Libs.keeper)

View File

@ -11,8 +11,9 @@ repositories {
} }
dependencies { 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("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.jacoco:org.jacoco.core:0.8.12")
implementation("org.jlleitschuh.gradle:ktlint-gradle:10.3.0") implementation("org.jlleitschuh.gradle:ktlint-gradle:10.3.0")
implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20230406-2.0.0") { implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20230406-2.0.0") {

View File

@ -22,11 +22,11 @@ object Config {
// Here is a list of all Android versions with their corresponding API // Here is a list of all Android versions with their corresponding API
// levels: https://apilevels.com/ // 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 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 // Version Information
const val versionMajor = 3 // Major version component of the app's version name and version code. const val versionMajor = 3 // Major version component of the app's version name and version code.

View File

@ -1,5 +1,3 @@
import io.opencensus.trace.Tracing
/** /**
* Generated by https://github.com/jmfayard/buildSrcVersions * 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:" + const val kotlin_stdlib_jdk8: String = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:" +
Versions.org_jetbrains_kotlin 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 * 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 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 * https://github.com/kiwix/java-libkiwix
*/ */

View File

@ -14,13 +14,13 @@ object Versions {
const val document_file_version: String = "1.0.1" 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 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" 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 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 androidx_navigation: String = "2.5.3"
const val navigation_ui_ktx: String = "2.4.1" 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 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" 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 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" const val junit_jupiter: String = "5.11.0"
@ -70,7 +72,7 @@ object Versions {
const val core_testing: String = "2.2.0" 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" const val testing_ktx: String = "1.3.0"
@ -86,11 +88,13 @@ object Versions {
const val rxandroid: String = "2.1.1" 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 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" const val multidex: String = "2.0.1"
@ -98,13 +102,13 @@ object Versions {
const val rxjava: String = "2.2.21" 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 junit: String = "1.1.5"
const val material_show_case_view: String = "1.3.7" 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" const val zxing = "3.5.3"

View File

@ -37,6 +37,7 @@ class AllProjectConfigurer {
fun applyPlugins(target: Project) { fun applyPlugins(target: Project) {
target.plugins.apply("kotlin-android") target.plugins.apply("kotlin-android")
target.plugins.apply("kotlin-kapt") target.plugins.apply("kotlin-kapt")
target.plugins.apply("com.google.devtools.ksp")
target.plugins.apply("kotlin-parcelize") target.plugins.apply("kotlin-parcelize")
target.plugins.apply("jacoco") target.plugins.apply("jacoco")
target.plugins.apply("org.jlleitschuh.gradle.ktlint") target.plugins.apply("org.jlleitschuh.gradle.ktlint")
@ -77,7 +78,7 @@ class AllProjectConfigurer {
} }
target.tasks.withType(KotlinCompile::class.java) { target.tasks.withType(KotlinCompile::class.java) {
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8) jvmTarget.set(JvmTarget.JVM_17)
freeCompilerArgs.add("-Xjvm-default=all-compatibility") freeCompilerArgs.add("-Xjvm-default=all-compatibility")
} }
} }
@ -137,7 +138,7 @@ class AllProjectConfigurer {
} }
fun configureCommonExtension(target: Project) { fun configureCommonExtension(target: Project) {
target.configureExtension<CommonExtension<*, *, *, *, *>> { target.configureExtension<CommonExtension<*, *, *, *, *, *>> {
lint { lint {
abortOnError = true abortOnError = true
checkAllWarnings = true checkAllWarnings = true
@ -231,7 +232,9 @@ class AllProjectConfigurer {
implementation(Libs.roomRxjava2) implementation(Libs.roomRxjava2)
kapt(Libs.roomCompiler) kapt(Libs.roomCompiler)
implementation(Libs.tracing) implementation(Libs.tracing)
implementation(Libs.fetch)
implementation(Libs.fetchOkhttp) implementation(Libs.fetchOkhttp)
implementation(Libs.androidx_activity)
} }
} }
} }

View File

@ -48,6 +48,9 @@ internal fun DependencyHandlerScope.compileOnly(dependency: String) =
internal fun DependencyHandlerScope.kapt(dependency: String) = internal fun DependencyHandlerScope.kapt(dependency: String) =
addDependency("kapt", dependency) addDependency("kapt", dependency)
internal fun DependencyHandlerScope.ksp(dependency: String) =
addDependency("ksp", dependency)
internal fun DependencyHandlerScope.testImplementation(dependency: String) = internal fun DependencyHandlerScope.testImplementation(dependency: String) =
addDependency("testImplementation", dependency) addDependency("testImplementation", dependency)

View File

@ -63,8 +63,4 @@ dependencies {
implementation(Libs.kotlinx_coroutines_android) implementation(Libs.kotlinx_coroutines_android)
implementation(Libs.kotlinx_coroutines_rx3) implementation(Libs.kotlinx_coroutines_rx3)
implementation(Libs.zxing) implementation(Libs.zxing)
api(Libs.fetch) {
// Todo: Will remove this when we add support for Android 15
exclude("androidx.core", "core-ktx")
}
} }

View File

@ -21,12 +21,10 @@ package eu.mhutti1.utils.storage
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.os.Environment import android.os.Environment
import androidx.core.content.ContextCompat
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import java.io.File import java.io.File
import java.io.FileFilter import java.io.FileFilter
import java.io.RandomAccessFile import java.io.RandomAccessFile
import java.util.ArrayList
object StorageDeviceUtils { object StorageDeviceUtils {
@JvmStatic @JvmStatic
@ -63,7 +61,7 @@ object StorageDeviceUtils {
private fun externalFilesDirsDevices( private fun externalFilesDirsDevices(
context: Context, context: Context,
writable: Boolean writable: Boolean
) = ContextCompat.getExternalFilesDirs(context, "") ) = context.getExternalFilesDirs("")
.filterNotNull() .filterNotNull()
.mapIndexed { index, dir -> StorageDevice(generalisePath(dir.path, writable), index == 0) } .mapIndexed { index, dir -> StorageDevice(generalisePath(dir.path, writable), index == 0) }

View File

@ -116,6 +116,9 @@ abstract class CoreApp : Application() {
detectLeakedSqlLiteObjects() detectLeakedSqlLiteObjects()
penaltyLog() penaltyLog()
detectLeakedRegistrationObjects() detectLeakedRegistrationObjects()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
detectUnsafeIntentLaunch()
}
}.build() }.build()
) )
} }

View File

@ -17,8 +17,13 @@
*/ */
package org.kiwix.kiwixmobile.core.base package org.kiwix.kiwixmobile.core.base
import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity 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.LanguageUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import javax.inject.Inject import javax.inject.Inject
@ -29,7 +34,14 @@ open class BaseActivity : AppCompatActivity() {
lateinit var sharedPreferenceUtil: SharedPreferenceUtil lateinit var sharedPreferenceUtil: SharedPreferenceUtil
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.dark(Color.BLACK),
navigationBarStyle = SystemBarStyle.dark(Color.BLACK)
)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
setWindowBackgroundColorForAndroid15AndAbove()
}
LanguageUtils.handleLocaleChange(this, sharedPreferenceUtil) LanguageUtils.handleLocaleChange(this, sharedPreferenceUtil)
} }
} }

View File

@ -19,13 +19,16 @@
package org.kiwix.kiwixmobile.core.base package org.kiwix.kiwixmobile.core.base
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import org.kiwix.kiwixmobile.core.R 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.getToolbarNavigationIcon
import org.kiwix.kiwixmobile.core.extensions.setFragmentBackgroundColorForAndroid15AndAbove
import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription
/** /**
@ -46,7 +49,11 @@ abstract class BaseFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
enableEdgeToEdgeMode()
setupToolbar() setupToolbar()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
setFragmentBackgroundColorForAndroid15AndAbove()
}
} }
// Setup toolbar to handle common back pressed event // Setup toolbar to handle common back pressed event

View File

@ -45,7 +45,7 @@ class AdapterDelegateManager<T> {
private fun getDelegateIndexFor(item: T): Int { private fun getDelegateIndexFor(item: T): Int {
for (index in 0..delegates.size()) { for (index in 0..delegates.size()) {
val valueAt = delegates.valueAt(index) val valueAt = delegates.valueAt(index)
if (valueAt?.isFor(item) == true) { if (valueAt.isFor(item) == true) {
return index return index
} }
} }

View File

@ -24,7 +24,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch 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.compat.ResolveInfoFlagsCompat
import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.databinding.ActivityKiwixErrorBinding 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.extensions.toast
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.utils.CRASH_AND_FEEDBACK_EMAIL_ADDRESS import org.kiwix.kiwixmobile.core.utils.CRASH_AND_FEEDBACK_EMAIL_ADDRESS
@ -86,6 +86,7 @@ open class ErrorActivity : BaseActivity() {
} }
setupReportButton() setupReportButton()
activityKiwixErrorBinding?.restartButton?.setOnClickListener { restartApp() } activityKiwixErrorBinding?.restartButton?.setOnClickListener { restartApp() }
activityKiwixErrorBinding?.root.applyEdgeToEdgeInsets()
} }
override fun onDestroy() { override fun onDestroy() {
@ -101,7 +102,7 @@ open class ErrorActivity : BaseActivity() {
val targetedIntents = createEmailIntents(emailIntent, activities) val targetedIntents = createEmailIntents(emailIntent, activities)
if (activities.isNotEmpty() && targetedIntents.isNotEmpty()) { if (activities.isNotEmpty() && targetedIntents.isNotEmpty()) {
val chooserIntent = val chooserIntent =
Intent.createChooser(targetedIntents.removeFirst(), "Send email...") Intent.createChooser(targetedIntents.removeAt(0), "Send email...")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray()) chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
sendEmailLauncher.launch(chooserIntent) sendEmailLauncher.launch(chooserIntent)
} else { } else {
@ -245,7 +246,7 @@ open class ErrorActivity : BaseActivity() {
""".trimIndent() """.trimIndent()
private fun externalFileDetails(): String = 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 { private fun safeContains(extras: Bundle): Boolean {
return try { return try {
@ -272,7 +273,7 @@ open class ErrorActivity : BaseActivity() {
private val versionName: String private val versionName: String
@SuppressLint("WrongConstant") @SuppressLint("WrongConstant")
get() = packageManager get() = packageManager
.getPackageInformation(packageName, ZERO).versionName .getPackageInformation(packageName, ZERO).versionName.toString()
private fun toStackTraceString(exception: Throwable): String = private fun toStackTraceString(exception: Throwable): String =
try { try {

View File

@ -24,11 +24,13 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
@ -199,4 +201,18 @@ object ActivityExtensions {
val isWideEnough = configuration.smallestScreenWidthDp >= 600 val isWideEnough = configuration.smallestScreenWidthDp >= 600
return isLargeOrXLarge && isWideEnough 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)
}
} }

View File

@ -19,15 +19,22 @@
package org.kiwix.kiwixmobile.core.extensions package org.kiwix.kiwixmobile.core.extensions
import android.content.Context import android.content.Context
import android.graphics.Color
import android.os.Build
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders 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 import org.kiwix.kiwixmobile.core.main.CoreMainActivity
inline fun <reified T : ViewModel> Fragment.viewModel( inline fun <reified T : ViewModel> Fragment.viewModel(
@ -55,3 +62,29 @@ fun View.closeKeyboard() {
} }
val Fragment.coreMainActivity get() = activity as CoreMainActivity 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)
}
}

View File

@ -21,13 +21,16 @@ package org.kiwix.kiwixmobile.core.extensions
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -92,7 +95,7 @@ fun View.showFullScreenMode(window: Window) {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsetsControllerCompat(window, window.decorView).apply { WindowInsetsControllerCompat(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.statusBars()) hide(WindowInsetsCompat.Type.systemBars())
hide(WindowInsetsCompat.Type.displayCutout()) hide(WindowInsetsCompat.Type.displayCutout())
systemBarsBehavior = systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
@ -106,10 +109,10 @@ fun View.showFullScreenMode(window: Window) {
} }
fun View.closeFullScreenMode(window: Window) { fun View.closeFullScreenMode(window: Window) {
WindowCompat.setDecorFitsSystemWindows(window, true) WindowCompat.setDecorFitsSystemWindows(window, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsetsControllerCompat(window, window.decorView).apply { WindowInsetsControllerCompat(window, window.decorView).apply {
show(WindowInsetsCompat.Type.statusBars()) show(WindowInsetsCompat.Type.systemBars())
show(WindowInsetsCompat.Type.displayCutout()) show(WindowInsetsCompat.Type.displayCutout())
} }
} }
@ -119,3 +122,31 @@ fun View.closeFullScreenMode(window: Window) {
clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) 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<ViewGroup.MarginLayoutParams> {
topMargin = systemBarsInsets.top
leftMargin = systemBarsInsets.left
bottomMargin = systemBarsInsets.bottom
rightMargin = systemBarsInsets.right
}
WindowInsetsCompat.CONSUMED
}
}
}

View File

@ -1869,9 +1869,9 @@ abstract class CoreReaderFragment :
} }
reopenBook() reopenBook()
showTabSwitcher() showTabSwitcher()
setUpWithTextToSpeech(tempWebViewListForUndo.last()) setUpWithTextToSpeech(tempWebViewListForUndo[tempWebViewListForUndo.lastIndex])
updateBottomToolbarVisibility() updateBottomToolbarVisibility()
safelyAddWebView(tempWebViewListForUndo.last()) safelyAddWebView(tempWebViewListForUndo[tempWebViewListForUndo.lastIndex])
} }
} }

View File

@ -229,7 +229,7 @@ abstract class CorePrefsFragment :
@Suppress("TooGenericExceptionThrown") @Suppress("TooGenericExceptionThrown")
get() = try { get() = try {
requireActivity().packageManager requireActivity().packageManager
.getPackageInformation(requireActivity().packageName, 0).versionName .getPackageInformation(requireActivity().packageName, 0).versionName.toString()
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
throw RuntimeException(e) throw RuntimeException(e)
} }

View File

@ -129,7 +129,7 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
get() = sharedPreferences.getInt(STORAGE_POSITION, 0) get() = sharedPreferences.getInt(STORAGE_POSITION, 0)
fun defaultStorage(): String = fun defaultStorage(): String =
getExternalFilesDirs(context, null)[0]?.path context.getExternalFilesDirs(null)[0]?.path
?: context.filesDir.path // a workaround for emulators ?: context.filesDir.path // a workaround for emulators
fun defaultPublicStorage(): String = fun defaultPublicStorage(): String =

View File

@ -30,7 +30,6 @@ import android.os.storage.StorageManager
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -583,7 +582,7 @@ object FileUtils {
@JvmStatic @JvmStatic
fun getDemoFilePathForCustomApp(context: Context) = fun getDemoFilePathForCustomApp(context: Context) =
"${ContextCompat.getExternalFilesDirs(context, null)[0]}/demo.zim" "${context.getExternalFilesDirs(null)[0]}/demo.zim"
@SuppressLint("Recycle") @SuppressLint("Recycle")
@JvmStatic @JvmStatic

View File

@ -18,10 +18,10 @@
--> -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"> android:background="@android:color/transparent">
<include layout="@layout/layout_standard_app_bar" /> <include layout="@layout/layout_standard_app_bar" />
@ -30,19 +30,20 @@
android:id="@+id/navigationHistoryRecyclerView" android:id="@+id/navigationHistoryRecyclerView"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/item_bookmark_history" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/app_bar" /> app:layout_constraintTop_toBottomOf="@+id/app_bar"
tools:listitem="@layout/item_bookmark_history" />
<TextView <TextView
android:id="@+id/searchNoResults" android:id="@+id/searchNoResults"
style="@style/no_content" style="@style/no_content"
android:text="@string/no_history" android:text="@string/no_history"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,5 +4,5 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clickable="true" android:clickable="true"
android:fitsSystemWindows="true" android:clipToPadding="false"
android:focusable="true" /> android:focusable="true" />

View File

@ -4,7 +4,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".help.HelpFragment"> tools:context=".help.HelpFragment">
<include layout="@layout/layout_standard_app_bar" /> <include layout="@layout/layout_standard_app_bar" />
@ -45,6 +44,7 @@
android:id="@+id/activity_help_recycler_view" android:id="@+id/activity_help_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -3,8 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar" android:id="@+id/app_bar"
@ -17,8 +16,8 @@
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:popupTheme="@style/KiwixTheme"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
app:popupTheme="@style/KiwixTheme"
tools:showIn="@layout/fragment_search" /> tools:showIn="@layout/fragment_search" />
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
@ -42,6 +41,7 @@
android:id="@+id/recycler_view" android:id="@+id/recycler_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View File

@ -20,8 +20,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/navigation_fragment_main_drawer_layout" android:id="@+id/navigation_fragment_main_drawer_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -4,7 +4,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:fitsSystemWindows="true"
android:orientation="vertical"> android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@ -18,12 +17,13 @@
android:id="@+id/search_list" android:id="@+id/search_list"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginTop="?actionBarSize"
android:clipToPadding="false"
android:contentDescription="@string/searched_list"
app:layout_constraintBottom_toTopOf="@+id/loadingMoreDataIndicator" app:layout_constraintBottom_toTopOf="@+id/loadingMoreDataIndicator"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
android:contentDescription="@string/searched_list"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
android:layout_marginTop="?actionBarSize"
tools:listitem="@layout/list_item_search" /> tools:listitem="@layout/list_item_search" />
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
@ -32,10 +32,10 @@
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="40dp" android:layout_height="40dp"
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent" />
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -24,7 +24,6 @@
<ImageView <ImageView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
android:src="@drawable/ic_home_kiwix_banner" /> android:src="@drawable/ic_home_kiwix_banner" />
</LinearLayout> </LinearLayout>

View File

@ -26,8 +26,7 @@
<org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout <org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fitsSystemWindows="true">
<FrameLayout <FrameLayout
android:id="@+id/activity_main_content_frame" android:id="@+id/activity_main_content_frame"

View File

@ -1,8 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fitsSystemWindows="true">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -16,5 +16,6 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/device_list" android:id="@+id/device_list"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
android:clipToPadding="false" />
</LinearLayout> </LinearLayout>

View File

@ -2,13 +2,13 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:fitsSystemWindows="true">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/tab_switcher_recycler_view" android:id="@+id/tab_switcher_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="horizontal" android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />

View File

@ -143,7 +143,7 @@
<string name="help_11">This is a descriptive message explaining where the Zim files are located after downloading. It is showing on the help screen.</string> <string name="help_11">This is a descriptive message explaining where the Zim files are located after downloading. It is showing on the help screen.</string>
<string name="pref_storage">{{Identical|Storage}}</string> <string name="pref_storage">{{Identical|Storage}}</string>
<string name="pref_current_folder">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.</string> <string name="pref_current_folder">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.</string>
<string name="pref_free_storage">This refers to free (unused) storage space, not to free as in free of charge.</string> <string name="pref_free_storage">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.</string>
<string name="delete_zim_failed">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.</string> <string name="delete_zim_failed">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.</string>
<string name="tts_pause">{{identical|pause}}</string> <string name="tts_pause">{{identical|pause}}</string>
<string name="tts_resume">{{identical|resume}}</string> <string name="tts_resume">{{identical|resume}}</string>

View File

@ -82,7 +82,6 @@
</style> </style>
<style name="Base.MaterialThemeBuilder" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="Base.MaterialThemeBuilder" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:statusBarColor" tools:ignore="NewApi">@color/black</item> <item name="android:statusBarColor" tools:ignore="NewApi">@color/black</item>
<item name="actionModeBackground">@color/cornflower_blue</item> <item name="actionModeBackground">@color/cornflower_blue</item>
<item name="windowActionModeOverlay">true</item> <item name="windowActionModeOverlay">true</item>
@ -92,6 +91,8 @@
<item name="colorAccent">?colorSecondary</item> <item name="colorAccent">?colorSecondary</item>
<item name="android:navigationBarColor">@color/black</item> <item name="android:navigationBarColor">@color/black</item>
<!-- To prevent showing content behind the cutouts -->
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">never</item>
</style> </style>
</resources> </resources>

View File

@ -25,7 +25,7 @@ import io.objectbox.Box
import io.objectbox.query.Query import io.objectbox.query.Query
import io.objectbox.query.QueryBuilder import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.flow.flowOf 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.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
@ -43,7 +43,7 @@ internal class NewRecentSearchDaoTest {
@Nested @Nested
inner class RecentSearchTests { inner class RecentSearchTests {
@Test @Test
fun `recentSearches searches by Id passed`() = runBlockingTest { fun `recentSearches searches by Id passed`() = runTest {
val zimId = "id" val zimId = "id"
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity()) val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
expectFromRecentSearches(queryResult, zimId) expectFromRecentSearches(queryResult, zimId)
@ -56,7 +56,7 @@ internal class NewRecentSearchDaoTest {
} }
@Test @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>(recentSearchEntity()) val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
expectFromRecentSearches(queryResult, "") expectFromRecentSearches(queryResult, "")
newRecentSearchDao.recentSearches(null) newRecentSearchDao.recentSearches(null)
@ -68,7 +68,7 @@ internal class NewRecentSearchDaoTest {
} }
@Test @Test
fun `recentSearches searches returns distinct entities by searchTerm`() = runBlockingTest { fun `recentSearches searches returns distinct entities by searchTerm`() = runTest {
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity(), recentSearchEntity()) val queryResult = listOf<RecentSearchEntity>(recentSearchEntity(), recentSearchEntity())
expectFromRecentSearches(queryResult, "") expectFromRecentSearches(queryResult, "")
newRecentSearchDao.recentSearches("") newRecentSearchDao.recentSearches("")
@ -80,7 +80,7 @@ internal class NewRecentSearchDaoTest {
} }
@Test @Test
fun `recentSearches searches returns a limitedNumber of entities`() = runBlockingTest { fun `recentSearches searches returns a limitedNumber of entities`() = runTest {
val searchResults: List<RecentSearchEntity> = val searchResults: List<RecentSearchEntity> =
(0..200).map { recentSearchEntity(searchTerm = "$it") } (0..200).map { recentSearchEntity(searchTerm = "$it") }
expectFromRecentSearches(searchResults, "") expectFromRecentSearches(searchResults, "")

View File

@ -26,7 +26,13 @@ import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.processors.PublishProcessor import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import io.reactivex.schedulers.TestScheduler 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.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith 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.InstantExecutorExtension
import org.kiwix.sharedFunctions.setScheduler import org.kiwix.sharedFunctions.setScheduler
@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(InstantExecutorExtension::class) @ExtendWith(InstantExecutorExtension::class)
internal class PageViewModelTest { internal class PageViewModelTest {
private val pageDao: PageDao = mockk() private val pageDao: PageDao = mockk()
@ -69,6 +76,7 @@ internal class PageViewModelTest {
@BeforeEach @BeforeEach
fun init() { fun init() {
Dispatchers.setMain(UnconfinedTestDispatcher())
clearAllMocks() clearAllMocks()
every { zimReaderContainer.id } returns "id" every { zimReaderContainer.id } returns "id"
every { zimReaderContainer.name } returns "zimName" every { zimReaderContainer.name } returns "zimName"
@ -77,6 +85,11 @@ internal class PageViewModelTest {
viewModel = TestablePageViewModel(zimReaderContainer, sharedPreferenceUtil, pageDao) viewModel = TestablePageViewModel(zimReaderContainer, sharedPreferenceUtil, pageDao)
} }
@AfterEach
fun tearDown() {
Dispatchers.resetMain()
}
@Test @Test
fun `initial state is Initialising`() { fun `initial state is Initialising`() {
viewModel.state.test().assertValue(pageState()) viewModel.state.test().assertValue(pageState())

View File

@ -24,7 +24,6 @@ import io.mockk.clearAllMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -33,11 +32,10 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@ -84,7 +82,7 @@ internal class SearchViewModelTest {
private val zimReaderContainer: ZimReaderContainer = mockk() private val zimReaderContainer: ZimReaderContainer = mockk()
private val searchResultGenerator: SearchResultGenerator = mockk() private val searchResultGenerator: SearchResultGenerator = mockk()
private val zimFileReader: ZimFileReader = mockk() private val zimFileReader: ZimFileReader = mockk()
private val testDispatcher = TestCoroutineDispatcher() private val testDispatcher = StandardTestDispatcher()
private val searchMutex: Mutex = mockk() private val searchMutex: Mutex = mockk()
lateinit var viewModel: SearchViewModel lateinit var viewModel: SearchViewModel
@ -101,7 +99,7 @@ internal class SearchViewModelTest {
Dispatchers.resetMain() Dispatchers.resetMain()
Dispatchers.setMain(testDispatcher) Dispatchers.setMain(testDispatcher)
clearAllMocks() clearAllMocks()
recentsFromDb = Channel(kotlinx.coroutines.channels.Channel.UNLIMITED) recentsFromDb = Channel(Channel.UNLIMITED)
every { zimReaderContainer.zimFileReader } returns zimFileReader every { zimReaderContainer.zimFileReader } returns zimFileReader
coEvery { coEvery {
searchResultGenerator.generateSearchResults("", zimFileReader) searchResultGenerator.generateSearchResults("", zimFileReader)
@ -115,7 +113,7 @@ internal class SearchViewModelTest {
@Nested @Nested
inner class StateTests { inner class StateTests {
@Test @Test
fun `initial state is Initialising`() = runBlockingTest { fun `initial state is Initialising`() = runTest {
viewModel.state.test(this).assertValue( viewModel.state.test(this).assertValue(
SearchState("", SearchResultsWithTerm("", null, searchMutex), emptyList(), FromWebView) SearchState("", SearchResultsWithTerm("", null, searchMutex), emptyList(), FromWebView)
).finish() ).finish()
@ -152,12 +150,12 @@ internal class SearchViewModelTest {
inner class ActionMapping { inner class ActionMapping {
@Test @Test
fun `ExitedSearch offers PopFragmentBackstack`() = runBlockingTest { fun `ExitedSearch offers PopFragmentBackstack`() = runTest {
actionResultsInEffects(ExitedSearch, PopFragmentBackstack) actionResultsInEffects(ExitedSearch, PopFragmentBackstack)
} }
@Test @Test
fun `OnItemClick offers Saves and Opens`() = runBlockingTest { fun `OnItemClick offers Saves and Opens`() = runTest {
val searchListItem = RecentSearchListItem("", "") val searchListItem = RecentSearchListItem("", "")
actionResultsInEffects( actionResultsInEffects(
OnItemClick(searchListItem), OnItemClick(searchListItem),
@ -170,7 +168,7 @@ internal class SearchViewModelTest {
} }
@Test @Test
fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() = runBlockingTest { fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() = runTest {
val searchListItem = RecentSearchListItem("", "") val searchListItem = RecentSearchListItem("", "")
actionResultsInEffects( actionResultsInEffects(
OnOpenInNewTabClick(searchListItem), OnOpenInNewTabClick(searchListItem),
@ -183,7 +181,7 @@ internal class SearchViewModelTest {
} }
@Test @Test
fun `OnItemLongClick offers Saves and Opens`() = runBlockingTest { fun `OnItemLongClick offers Saves and Opens`() = runTest {
val searchListItem = RecentSearchListItem("", "") val searchListItem = RecentSearchListItem("", "")
actionResultsInEffects( actionResultsInEffects(
OnItemLongClick(searchListItem), OnItemLongClick(searchListItem),
@ -192,12 +190,12 @@ internal class SearchViewModelTest {
} }
@Test @Test
fun `ClickedSearchInText offers SearchInPreviousScreen`() = runBlockingTest { fun `ClickedSearchInText offers SearchInPreviousScreen`() = runTest {
actionResultsInEffects(ClickedSearchInText, SearchInPreviousScreen("")) actionResultsInEffects(ClickedSearchInText, SearchInPreviousScreen(""))
} }
@Test @Test
fun `ConfirmedDelete offers Delete and Toast`() = runBlockingTest { fun `ConfirmedDelete offers Delete and Toast`() = runTest {
val searchListItem = RecentSearchListItem("", "") val searchListItem = RecentSearchListItem("", "")
actionResultsInEffects( actionResultsInEffects(
ConfirmedDelete(searchListItem), ConfirmedDelete(searchListItem),
@ -207,7 +205,7 @@ internal class SearchViewModelTest {
} }
@Test @Test
fun `CreatedWithArguments offers SearchArgumentProcessing`() = runBlockingTest { fun `CreatedWithArguments offers SearchArgumentProcessing`() = runTest {
val bundle = mockk<Bundle>() val bundle = mockk<Bundle>()
actionResultsInEffects( actionResultsInEffects(
CreatedWithArguments(bundle), CreatedWithArguments(bundle),
@ -216,7 +214,7 @@ internal class SearchViewModelTest {
} }
@Test @Test
fun `ReceivedPromptForSpeechInput offers StartSpeechInput`() = runBlockingTest { fun `ReceivedPromptForSpeechInput offers StartSpeechInput`() = runTest {
actionResultsInEffects( actionResultsInEffects(
ReceivedPromptForSpeechInput, ReceivedPromptForSpeechInput,
StartSpeechInput(viewModel.actions) StartSpeechInput(viewModel.actions)
@ -224,7 +222,7 @@ internal class SearchViewModelTest {
} }
@Test @Test
fun `StartSpeechInputFailed offers ShowToast`() = runBlockingTest { fun `StartSpeechInputFailed offers ShowToast`() = runTest {
actionResultsInEffects( actionResultsInEffects(
StartSpeechInputFailed, StartSpeechInputFailed,
ShowToast(string.speech_not_supported) ShowToast(string.speech_not_supported)
@ -232,22 +230,29 @@ internal class SearchViewModelTest {
} }
@Test @Test
fun `ActivityResultReceived offers ProcessActivityResult`() = runBlockingTest { fun `ActivityResultReceived offers ProcessActivityResult`() = runTest {
actionResultsInEffects( actionResultsInEffects(
ActivityResultReceived(0, 1, null), ActivityResultReceived(0, 1, null),
ProcessActivityResult(0, 1, null, viewModel.actions) ProcessActivityResult(0, 1, null, viewModel.actions)
) )
} }
private fun TestCoroutineScope.actionResultsInEffects( private fun TestScope.actionResultsInEffects(
action: Action, action: Action,
vararg effects: SideEffect<*> vararg effects: SideEffect<*>
) { ) {
viewModel.effects if (effects.size > 1) return
.test(this) val collectedEffects = mutableListOf<SideEffect<*>>()
.also { viewModel.actions.trySend(action).isSuccess } val job = launch {
.assertValues(*effects) viewModel.effects.collect {
.finish() collectedEffects.add(it)
}
}
viewModel.actions.trySend(action).isSuccess
advanceUntilIdle()
assertThat(collectedEffects).containsExactlyElementsOf(effects.toList())
job.cancel()
} }
} }
@ -271,34 +276,51 @@ internal class SearchViewModelTest {
} }
} }
fun <T> Flow<T>.test(scope: CoroutineScope) = TestObserver(scope, this) fun <T> Flow<T>.test(scope: TestScope): TestObserver<T> {
val observer = TestObserver(scope, this)
scope.launch { observer.startCollecting() }
return observer
}
class TestObserver<T>( class TestObserver<T>(
scope: CoroutineScope, private val scope: TestScope,
flow: Flow<T> private val flow: Flow<T>
) { ) {
private val values = mutableListOf<T>() private val values = mutableListOf<T>()
private val job: Job = scope.launch { private val completionChannel = Channel<Unit>()
private var job: Job? = null
suspend fun startCollecting() {
job = scope.launch {
flow.collect { flow.collect {
values.add(it) values.add(it)
} }
} }
completionChannel.send(Unit)
}
fun assertValues(vararg values: T): TestObserver<T> { private suspend fun awaitCompletion() {
completionChannel.receive()
}
suspend fun assertValues(vararg values: T): TestObserver<T> {
awaitCompletion()
assertThat(values.toList()).containsExactlyElementsOf(this.values) assertThat(values.toList()).containsExactlyElementsOf(this.values)
return this return this
} }
fun assertValue(value: T): TestObserver<T> { suspend fun assertValue(value: T): TestObserver<T> {
awaitCompletion()
assertThat(values.last()).isEqualTo(value) assertThat(values.last()).isEqualTo(value)
return this return this
} }
fun finish() { fun finish() {
job.cancel() job?.cancel()
} }
fun assertValue(value: (T) -> Boolean): TestObserver<T> { suspend fun assertValue(value: (T) -> Boolean): TestObserver<T> {
awaitCompletion()
assertThat(values.last()).satisfies({ value(it) }) assertThat(values.last()).satisfies({ value(it) })
return this return this
} }

View File

@ -22,7 +22,6 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.AssetFileDescriptor import android.content.res.AssetFileDescriptor
import android.content.res.AssetManager import android.content.res.AssetManager
import androidx.core.content.ContextCompat
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile
@ -96,7 +95,7 @@ class CustomFileValidator @Inject constructor(private val context: Context) {
private fun obbFiles() = private fun obbFiles() =
scanDirs( scanDirs(
ContextCompat.getObbDirs(context).filterNotNull().filter(File::exists).toTypedArray(), context.obbDirs.filterNotNull().filter(File::exists).toTypedArray(),
"obb" "obb"
) )
@ -105,7 +104,7 @@ class CustomFileValidator @Inject constructor(private val context: Context) {
val directoryList = mutableListOf<File>() val directoryList = mutableListOf<File>()
// Get the external files directories for the app // Get the external files directories for the app
ContextCompat.getExternalFilesDirs(context, null).filterNotNull() context.getExternalFilesDirs(null).filterNotNull()
.filter(File::exists) .filter(File::exists)
.forEach { dir -> .forEach { dir ->
// Check if the directory's parent is not null // Check if the directory's parent is not null

View File

@ -38,6 +38,7 @@ import org.kiwix.kiwixmobile.custom.BuildConfig
import org.kiwix.kiwixmobile.custom.R import org.kiwix.kiwixmobile.custom.R
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.R.drawable 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.customActivityComponent
import org.kiwix.kiwixmobile.custom.databinding.ActivityCustomMainBinding import org.kiwix.kiwixmobile.custom.databinding.ActivityCustomMainBinding
@ -84,6 +85,7 @@ class CustomMainActivity : CoreMainActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
activityCustomMainBinding = ActivityCustomMainBinding.inflate(layoutInflater) activityCustomMainBinding = ActivityCustomMainBinding.inflate(layoutInflater)
setContentView(activityCustomMainBinding.root) setContentView(activityCustomMainBinding.root)
activityCustomMainBinding.root.applyEdgeToEdgeInsets()
if (savedInstanceState != null) { if (savedInstanceState != null) {
return return
} }

View File

@ -22,7 +22,6 @@
android:id="@+id/custom_drawer_container" android:id="@+id/custom_drawer_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:ignore="UnusedIds"> tools:ignore="UnusedIds">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
@ -38,7 +37,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_main" app:headerLayout="@layout/nav_main"
app:menu="@menu/menu_drawer_main" /> app:menu="@menu/menu_drawer_main" />
@ -48,6 +46,5 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginTop="24dp" android:layout_marginTop="24dp"
android:fitsSystemWindows="true"
app:headerLayout="@layout/drawer_right" /> app:headerLayout="@layout/drawer_right" />
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>

View File

@ -1,6 +1,6 @@
#Mon Dec 19 16:13:45 IST 2022 #Mon Dec 19 16:13:45 IST 2022
distributionBase=GRADLE_USER_HOME 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 distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<lint> <lint>
<issue id="TypographyQuotes"> <issue id="TypographyQuotes" severity="warning">
<ignore path="**-qq/**.xml" /> <ignore path="**-qq/**.xml" />
<ignore path="**-iw/**.xml" /> <ignore path="**-iw/**.xml" />
</issue> </issue>
@ -48,4 +48,7 @@
<issue id="DataExtractionRules" severity="warning" /> <issue id="DataExtractionRules" severity="warning" />
<issue id="ObsoleteSdkInt" severity="warning" /> <issue id="ObsoleteSdkInt" severity="warning" />
<issue id="AppLinksAutoVerify" severity="warning" /> <issue id="AppLinksAutoVerify" severity="warning" />
<issue id="CheckResult">
<ignore path="**/androidTest/**.kt" />
</issue>
</lint> </lint>