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
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

View File

@ -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)

View File

@ -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()

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.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() {

View File

@ -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">
<org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout
android:layout_width="match_parent"
@ -53,7 +52,6 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_main"
app:menu="@menu/menu_drawer_main" />
@ -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" />

View File

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

View File

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

View File

@ -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" />

View File

@ -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">
<org.kiwix.kiwixmobile.core.utils.NestedCoordinatorLayout
android:layout_width="match_parent"
@ -55,6 +54,7 @@
android:id="@+id/zimfilelist"
android:layout_width="0dp"
android:layout_height="match_parent"
android:clipToPadding="false"
android:contentDescription="@string/crash_checkbox_zimfiles"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -5,7 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:fitsSystemWindows="true"
tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferFragment">
@ -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"

View File

@ -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())
}
}
}

View File

@ -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)

View File

@ -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") {

View File

@ -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.

View File

@ -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
*/

View File

@ -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"

View File

@ -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<CommonExtension<*, *, *, *, *>> {
target.configureExtension<CommonExtension<*, *, *, *, *, *>> {
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)
}
}
}

View File

@ -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)

View File

@ -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")
}
}

View File

@ -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) }

View File

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

View File

@ -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)
}
}

View File

@ -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

View File

@ -45,7 +45,7 @@ class AdapterDelegateManager<T> {
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
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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 <reified T : ViewModel> 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)
}
}

View File

@ -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<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()
showTabSwitcher()
setUpWithTextToSpeech(tempWebViewListForUndo.last())
setUpWithTextToSpeech(tempWebViewListForUndo[tempWebViewListForUndo.lastIndex])
updateBottomToolbarVisibility()
safelyAddWebView(tempWebViewListForUndo.last())
safelyAddWebView(tempWebViewListForUndo[tempWebViewListForUndo.lastIndex])
}
}

View File

@ -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)
}

View File

@ -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 =

View File

@ -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

View File

@ -18,10 +18,10 @@
-->
<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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
<include layout="@layout/layout_standard_app_bar" />
@ -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" />
<TextView
android:id="@+id/searchNoResults"
style="@style/no_content"
android:text="@string/no_history"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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" />

View File

@ -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">
<include layout="@layout/layout_standard_app_bar" />
@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,13 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/tab_switcher_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="horizontal"
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="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_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="tts_pause">{{identical|pause}}</string>
<string name="tts_resume">{{identical|resume}}</string>

View File

@ -82,7 +82,6 @@
</style>
<style name="Base.MaterialThemeBuilder" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:statusBarColor" tools:ignore="NewApi">@color/black</item>
<item name="actionModeBackground">@color/cornflower_blue</item>
<item name="windowActionModeOverlay">true</item>
@ -92,6 +91,8 @@
<item name="colorAccent">?colorSecondary</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>
</resources>

View File

@ -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>(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>(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(), 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<RecentSearchEntity> =
(0..200).map { recentSearchEntity(searchTerm = "$it") }
expectFromRecentSearches(searchResults, "")

View File

@ -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())

View File

@ -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<Bundle>()
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<SideEffect<*>>()
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 <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>(
scope: CoroutineScope,
flow: Flow<T>
private val scope: TestScope,
private val flow: Flow<T>
) {
private val values = mutableListOf<T>()
private val job: Job = scope.launch {
flow.collect {
values.add(it)
private val completionChannel = Channel<Unit>()
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<T> {
private suspend fun awaitCompletion() {
completionChannel.receive()
}
suspend fun assertValues(vararg values: T): TestObserver<T> {
awaitCompletion()
assertThat(values.toList()).containsExactlyElementsOf(this.values)
return this
}
fun assertValue(value: T): TestObserver<T> {
suspend fun assertValue(value: T): TestObserver<T> {
awaitCompletion()
assertThat(values.last()).isEqualTo(value)
return this
}
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) })
return this
}

View File

@ -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<File>()
// 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

View File

@ -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
}

View File

@ -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">
<androidx.fragment.app.FragmentContainerView
@ -38,7 +37,6 @@
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_main"
app:menu="@menu/menu_drawer_main" />
@ -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" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -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

View File

@ -1,25 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="TypographyQuotes">
<issue id="TypographyQuotes" severity="warning">
<ignore path="**-qq/**.xml" />
<ignore path="**-iw/**.xml" />
</issue>
<issue id="LintError">
<ignore regexp=".*BookmarksRobot.kt.*"/>
<ignore regexp=".*DebugFunctions.kt.*"/>
<ignore regexp=".*HistoryRobot.kt.*"/>
<ignore regexp=".*IntroRobot.kt.*"/>
<ignore regexp=".*LanguageRobot.kt.*"/>
<ignore regexp=".*LibraryRobot.kt.*"/>
<ignore regexp=".*LocalFileTransferRobot.kt.*"/>
<ignore regexp=".*OnlineLibraryRobot.kt.*"/>
<ignore regexp=".*ReaderRobot.kt.*"/>
<ignore regexp=".*SettingsRobot.kt.*"/>
<ignore regexp=".*TopLevelDestinationRobot.kt.*"/>
<ignore regexp=".*ZimHostRobot.kt.*"/>
<ignore regexp=".*SearchRobot.kt.*"/>
<ignore regexp=".*InitialDownloadRobot.kt.*"/>
<ignore regexp=".*DownloadRobot.kt.*"/>
<ignore regexp=".*BookmarksRobot.kt.*" />
<ignore regexp=".*DebugFunctions.kt.*" />
<ignore regexp=".*HistoryRobot.kt.*" />
<ignore regexp=".*IntroRobot.kt.*" />
<ignore regexp=".*LanguageRobot.kt.*" />
<ignore regexp=".*LibraryRobot.kt.*" />
<ignore regexp=".*LocalFileTransferRobot.kt.*" />
<ignore regexp=".*OnlineLibraryRobot.kt.*" />
<ignore regexp=".*ReaderRobot.kt.*" />
<ignore regexp=".*SettingsRobot.kt.*" />
<ignore regexp=".*TopLevelDestinationRobot.kt.*" />
<ignore regexp=".*ZimHostRobot.kt.*" />
<ignore regexp=".*SearchRobot.kt.*" />
<ignore regexp=".*InitialDownloadRobot.kt.*" />
<ignore regexp=".*DownloadRobot.kt.*" />
</issue>
<issue id="TypographyEllipsis">
<ignore path="**-iw/**.xml" />
@ -48,4 +48,7 @@
<issue id="DataExtractionRules" severity="warning" />
<issue id="ObsoleteSdkInt" severity="warning" />
<issue id="AppLinksAutoVerify" severity="warning" />
<issue id="CheckResult">
<ignore path="**/androidTest/**.kt" />
</issue>
</lint>