diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt index eceb7495e5..55990093a4 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/deeplinks/DeepLinksTest.kt @@ -105,7 +105,7 @@ class DeepLinksTest : BaseActivityTest() { private fun loadZimFileInApplicationAndReturnSchemeTypeUri(schemeType: String): Uri? { val loadFileStream = DeepLinksTest::class.java.classLoader.getResourceAsStream("testzim.zim") - val zimFile = File(sharedPreferenceUtil.prefStorage, "testzim.zim") + val zimFile = File(sharedPreferenceUtil.defaultStorage(), "testzim.zim") if (zimFile.exists()) zimFile.delete() zimFile.createNewFile() loadFileStream.use { inputStream -> diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt index 18464a0f3d..afaa28c01e 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt @@ -79,7 +79,9 @@ class KiwixSettingsFragmentTest { handleLocaleChange( it, "en", - SharedPreferenceUtil(it) + SharedPreferenceUtil(it).apply { + setIsPlayStoreBuildType(true) + } ) it.navigate(R.id.introFragment) } @@ -101,9 +103,8 @@ class KiwixSettingsFragmentTest { toggleOpenNewTabInBackground() toggleExternalLinkWarningPref() toggleWifiDownloadsOnlyPref() - clickStoragePreference() - assertStorageDialogDisplayed() - dismissDialog() + clickExternalStoragePreference() + clickInternalStoragePreference() clickClearHistoryPreference() assertHistoryDialogDisplayed() dismissDialog() diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt index 3d6ba3211c..90ca84df2c 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt @@ -28,6 +28,7 @@ import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withResourceName +import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText import applyWithViewHierarchyPrinting import org.hamcrest.Matchers @@ -36,6 +37,7 @@ import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.Findable.StringId.TextId import org.kiwix.kiwixmobile.Findable.Text import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.testutils.TestUtils.getResourceString import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView /** @@ -61,6 +63,16 @@ class SettingsRobot : BaseRobot() { ) } + private fun clickRecyclerViewItemsContainingText(@StringRes vararg stringIds: Int) { + onView( + withResourceName("recycler_view") + ).perform( + actionOnItem( + hasDescendant(Matchers.anyOf(*stringIds.subStringMatchers())), ViewActions.click() + ) + ) + } + fun toggleBackToTopPref() { clickRecyclerViewItems(R.string.pref_back_to_top) } @@ -100,12 +112,12 @@ class SettingsRobot : BaseRobot() { isVisible(TextId(R.string.pref_language_title)) } - fun clickStoragePreference() { - clickRecyclerViewItems(R.string.internal_storage, R.string.external_storage) + fun clickInternalStoragePreference() { + clickRecyclerViewItemsContainingText(R.string.internal_storage) } - fun assertStorageDialogDisplayed() { - isVisible(TextId(R.string.pref_storage)) + fun clickExternalStoragePreference() { + clickRecyclerViewItemsContainingText(R.string.external_storage) } fun clickClearHistoryPreference() { @@ -181,4 +193,7 @@ class SettingsRobot : BaseRobot() { context.resources.getStringArray(R.array.pref_dark_modes_entries) private fun IntArray.matchers() = map(::withText).toTypedArray() + private fun IntArray.subStringMatchers() = map { + withSubstring(getResourceString(it)) + }.toTypedArray() } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt index 3ef94a59aa..1e24d4093a 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/webserver/ZimHostFragmentTest.kt @@ -199,7 +199,7 @@ class ZimHostFragmentTest { private fun loadZimFileInApplication(zimFileName: String) { val loadFileStream = ZimHostFragmentTest::class.java.classLoader.getResourceAsStream(zimFileName) - val zimFile = File(sharedPreferenceUtil.prefStorage, zimFileName) + val zimFile = File(sharedPreferenceUtil.defaultStorage(), zimFileName) if (zimFile.exists()) zimFile.delete() zimFile.createNewFile() loadFileStream.use { inputStream -> diff --git a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt index 8eafc7d6cb..00f857c796 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt @@ -21,20 +21,26 @@ package org.kiwix.kiwixmobile.settings import android.os.Build import android.os.Bundle import android.os.Environment -import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import androidx.preference.Preference import androidx.preference.PreferenceCategory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.kiwix.kiwixmobile.R +import eu.mhutti1.utils.storage.StorageDevice +import eu.mhutti1.utils.storage.StorageDeviceUtils +import io.reactivex.Flowable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.navigateToSettings import org.kiwix.kiwixmobile.core.settings.CorePrefsFragment +import org.kiwix.kiwixmobile.core.settings.StorageRadioButtonPreference import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil -import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil.Companion.PREF_STORAGE +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil.Companion.PREF_EXTERNAL_STORAGE +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil.Companion.PREF_INTERNAL_STORAGE class KiwixPrefsFragment : CorePrefsFragment() { + private var storageDisposable: Disposable? = null + private var storageDeviceList: List = listOf() override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { super.onCreatePreferences(savedInstanceState, rootKey) @@ -44,20 +50,102 @@ class KiwixPrefsFragment : CorePrefsFragment() { override fun setStorage() { sharedPreferenceUtil?.let { - val internalStorage = runBlocking { internalStorage() } - findPreference(PREF_STORAGE)?.title = getString( - if (it.prefStorage == internalStorage?.let( - it::getPublicDirectoryPath + if (storageDisposable?.isDisposed == false) { + // update the storage when user switch to other storage. + setUpStoragePreference(it) + } + storageDisposable = + Flowable.fromCallable { StorageDeviceUtils.getWritableStorage(requireActivity()) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { storageList -> + storageDeviceList = storageList + showExternalPreferenceIfAvailable() + setUpStoragePreference(it) + }, + Throwable::printStackTrace ) - ) R.string.internal_storage - else R.string.external_storage - ) } - findPreference(PREF_STORAGE)?.summary = storageCalculator?.calculateAvailableSpace() } - private suspend fun internalStorage(): String? = withContext(Dispatchers.IO) { - ContextCompat.getExternalFilesDirs(requireContext(), null).firstOrNull()?.path + private fun setUpStoragePreference(sharedPreferenceUtil: SharedPreferenceUtil) { + storageDeviceList.forEachIndexed { index, storageDevice -> + val preferenceKey = if (index == 0) PREF_INTERNAL_STORAGE else PREF_EXTERNAL_STORAGE + val selectedStoragePosition = sharedPreferenceUtil.storagePosition + val isChecked = selectedStoragePosition == index + findPreference(preferenceKey)?.apply { + this.isChecked = isChecked + setOnPreferenceClickListener { + onStorageDeviceSelected(storageDevice) + true + } + setPathAndTitleForStorage( + buildStoragePathAndTitle( + storageDevice, + index, + selectedStoragePosition, + sharedPreferenceUtil + ) + ) + setFreeSpace(getFreeSpaceText(storageDevice)) + setUsedSpace(getUsedSpaceText(storageDevice)) + setProgress(calculateUsedPercentage(storageDevice)) + } + } + } + + private fun getFreeSpaceText(storageDevice: StorageDevice): String { + val freeSpace = storageCalculator?.calculateAvailableSpace(storageDevice.file) + return getString(R.string.pref_free_storage, freeSpace) + } + + private fun getUsedSpaceText(storageDevice: StorageDevice): String { + val usedSpace = storageCalculator?.calculateUsedSpace(storageDevice.file) + return getString(R.string.pref_storage_used, usedSpace) + } + + private fun buildStoragePathAndTitle( + storageDevice: StorageDevice, + index: Int, + selectedStoragePosition: Int, + sharedPreferenceUtil: SharedPreferenceUtil + ): String { + val storageName = if (storageDevice.isInternal) { + getString(R.string.internal_storage) + } else { + getString(R.string.external_storage) + } + val storagePath = if (index == selectedStoragePosition) { + sharedPreferenceUtil.prefStorage + } else { + getStoragePathForNonSelectedStorage(storageDevice, sharedPreferenceUtil) + } + val totalSpace = storageCalculator?.calculateTotalSpace(storageDevice.file) + return "$storageName - $totalSpace\n$storagePath/Kiwix" + } + + private fun getStoragePathForNonSelectedStorage( + storageDevice: StorageDevice, + sharedPreferenceUtil: SharedPreferenceUtil + ): String = + if (storageDevice.isInternal) { + sharedPreferenceUtil.getPublicDirectoryPath(storageDevice.name) + } else { + storageDevice.name + } + + @Suppress("MagicNumber") + private fun calculateUsedPercentage(storageDevice: StorageDevice): Int { + val totalSpace = storageCalculator?.totalBytes(storageDevice.file) ?: 1 + val availableSpace = storageCalculator?.availableBytes(storageDevice.file) ?: 0 + val usedSpace = totalSpace - availableSpace + return (usedSpace.toDouble() / totalSpace * 100).toInt() + } + + private fun showExternalPreferenceIfAvailable() { + findPreference(PREF_EXTERNAL_STORAGE)?.isVisible = + storageDeviceList.size > 1 } private fun setMangeExternalStoragePermission() { @@ -87,6 +175,11 @@ class KiwixPrefsFragment : CorePrefsFragment() { preferenceCategory?.isVisible = true } + override fun onDestroyView() { + storageDisposable?.dispose() + super.onDestroyView() + } + companion object { const val PREF_MANAGE_EXTERNAL_STORAGE_PERMISSION = "pref_manage_external_storage" diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt index f24edc44c4..fd7184778a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt @@ -40,7 +40,6 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.snackbar.Snackbar import eu.mhutti1.utils.storage.StorageDevice -import eu.mhutti1.utils.storage.StorageSelectDialog import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent import org.kiwix.kiwixmobile.core.CoreApp.Companion.instance import org.kiwix.kiwixmobile.core.DarkModeConfig @@ -301,9 +300,6 @@ abstract class CorePrefsFragment : if (preference.key.equals(PREF_CREDITS, ignoreCase = true)) { openCredits() } - if (preference.key.equals(SharedPreferenceUtil.PREF_STORAGE, ignoreCase = true)) { - openFolderSelect() - } if (preference.key.equals(PREF_EXPORT_BOOKMARK, ignoreCase = true) && requestExternalStorageWritePermissionForExportBookmark() ) { @@ -447,18 +443,8 @@ abstract class CorePrefsFragment : private fun isValidBookmarkFile(mimeType: String?) = mimeType == "application/xml" || mimeType == "text/xml" - private fun openFolderSelect() { - val dialogFragment = StorageSelectDialog() - dialogFragment.onSelectAction = - ::onStorageDeviceSelected - dialogFragment.show( - requireActivity().supportFragmentManager, - resources.getString(R.string.pref_storage) - ) - } - @Suppress("NestedBlockDepth") - private fun onStorageDeviceSelected(storageDevice: StorageDevice) { + fun onStorageDeviceSelected(storageDevice: StorageDevice) { sharedPreferenceUtil?.let { sharedPreferenceUtil -> findPreference(SharedPreferenceUtil.PREF_STORAGE)?.summary = storageCalculator?.calculateAvailableSpace(storageDevice.file) @@ -466,10 +452,9 @@ abstract class CorePrefsFragment : sharedPreferenceUtil.putPrefStorage( sharedPreferenceUtil.getPublicDirectoryPath(storageDevice.name) ) - findPreference(SharedPreferenceUtil.PREF_STORAGE)?.title = - getString(R.string.internal_storage) sharedPreferenceUtil.putStoragePosition(INTERNAL_SELECT_POSITION) setShowStorageOption() + setStorage() } else { if (sharedPreferenceUtil.isPlayStoreBuild) { setExternalStoragePath(storageDevice) @@ -492,10 +477,9 @@ abstract class CorePrefsFragment : private fun setExternalStoragePath(storageDevice: StorageDevice) { sharedPreferenceUtil?.putPrefStorage(storageDevice.name) - findPreference(SharedPreferenceUtil.PREF_STORAGE)?.title = - getString(R.string.external_storage) sharedPreferenceUtil?.putStoragePosition(EXTERNAL_SELECT_POSITION) setShowStorageOption() + setStorage() } private fun selectFolder() { @@ -516,10 +500,9 @@ abstract class CorePrefsFragment : result.data?.let { intent -> getPathFromUri(requireActivity(), intent)?.let { path -> sharedPreferenceUtil?.putPrefStorage(path) - findPreference(SharedPreferenceUtil.PREF_STORAGE)?.title = - getString(R.string.external_storage) sharedPreferenceUtil?.putStoragePosition(EXTERNAL_SELECT_POSITION) setShowStorageOption() + setStorage() } } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt index e796bcbefb..8fffecdbef 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt @@ -36,9 +36,12 @@ class StorageCalculator @Inject constructor( fun calculateTotalSpace(file: File = File(sharedPreferenceUtil.prefStorage)): String = Bytes(totalBytes(file)).humanReadable + fun calculateUsedSpace(file: File): String = + Bytes(totalBytes(file) - availableBytes(file)).humanReadable + fun availableBytes(file: File = File(sharedPreferenceUtil.prefStorage)) = if (file.isFileExist()) file.freeSpace() else 0L - private fun totalBytes(file: File) = if (file.isFileExist()) file.totalSpace() else 0L + fun totalBytes(file: File) = if (file.isFileExist()) file.totalSpace() else 0L } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageRadioButtonPreference.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageRadioButtonPreference.kt new file mode 100644 index 0000000000..36d4532b73 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageRadioButtonPreference.kt @@ -0,0 +1,93 @@ +/* + * Kiwix Android + * Copyright (c) 2024 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.settings + +import android.content.Context +import android.util.AttributeSet +import android.widget.ProgressBar +import android.widget.RadioButton +import android.widget.TextView +import androidx.preference.CheckBoxPreference +import androidx.preference.PreferenceViewHolder +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil.Companion.PREF_EXTERNAL_STORAGE +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil.Companion.PREF_INTERNAL_STORAGE + +class StorageRadioButtonPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : CheckBoxPreference(context, attrs, defStyleAttr) { + + init { + widgetLayoutResource = R.layout.item_storage_preference + } + + private var radioButton: RadioButton? = null + private var progressBar: ProgressBar? = null + private var usedSpaceTextView: TextView? = null + private var freeSpaceTextView: TextView? = null + private var pathAndTitleTextView: TextView? = null + private var usedSpace: String? = null + private var freeSpace: String? = null + private var pathAndTitle: String? = null + private var progress: Int = 0 + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + radioButton = holder.findViewById(R.id.radioButton) as RadioButton + progressBar = holder.findViewById(R.id.storageProgressBar) as ProgressBar + usedSpaceTextView = holder.findViewById(R.id.usedSpace) as TextView + freeSpaceTextView = holder.findViewById(R.id.freeSpace) as TextView + pathAndTitleTextView = holder.findViewById(R.id.storagePathAndTitle) as TextView + radioButton?.isChecked = isChecked + + usedSpaceTextView?.let { it.text = usedSpace } + freeSpaceTextView?.let { it.text = freeSpace } + pathAndTitleTextView?.let { it.text = pathAndTitle } + progressBar?.let { it.progress = progress } + } + + override fun onClick() { + if (isChecked) return + preferenceManager.findPreference(PREF_INTERNAL_STORAGE)?.isChecked = false + preferenceManager.findPreference(PREF_EXTERNAL_STORAGE)?.isChecked = false + super.onClick() + } + + fun setProgress(usedPercentage: Int) { + progress = usedPercentage + progressBar?.progress = usedPercentage + } + + fun setUsedSpace(usedSpace: String) { + this.usedSpace = usedSpace + usedSpaceTextView?.text = usedSpace + } + + fun setFreeSpace(freeSpace: String) { + this.freeSpace = freeSpace + freeSpaceTextView?.text = freeSpace + } + + fun setPathAndTitleForStorage(storageTitleAndPath: String) { + pathAndTitle = storageTitleAndPath + pathAndTitleTextView?.text = storageTitleAndPath + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt index 8c4754a9aa..2c7c2d9c6e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt @@ -275,6 +275,8 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) { const val PREF_LANG = "pref_language_chooser" const val PREF_DEVICE_DEFAULT_LANG = "pref_device_default_language" const val PREF_STORAGE = "pref_select_folder" + const val PREF_INTERNAL_STORAGE = "pref_internal_storage" + const val PREF_EXTERNAL_STORAGE = "pref_external_storage" const val STORAGE_POSITION = "storage_position" const val PREF_WIFI_ONLY = "pref_wifi_only" const val PREF_KIWIX_MOBILE = "kiwix-mobile" diff --git a/core/src/main/res/drawable/progress_bar_state.xml b/core/src/main/res/drawable/progress_bar_state.xml new file mode 100644 index 0000000000..46820b2162 --- /dev/null +++ b/core/src/main/res/drawable/progress_bar_state.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/main/res/layout/item_storage_preference.xml b/core/src/main/res/layout/item_storage_preference.xml new file mode 100644 index 0000000000..e45f9b2f2c --- /dev/null +++ b/core/src/main/res/layout/item_storage_preference.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 33f542be5b..f9abd9b150 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -138,6 +138,8 @@ ZIM files download in-app are located in the external storage directory in a folder entitled Kiwix. Storage Current Folder + %s used + %s free Sorry we were unable to delete some files. You should try using a file manager instead. pause resume diff --git a/core/src/main/res/xml/preferences.xml b/core/src/main/res/xml/preferences.xml index 9d6df5a209..b89015b41b 100644 --- a/core/src/main/res/xml/preferences.xml +++ b/core/src/main/res/xml/preferences.xml @@ -65,9 +65,14 @@ app:iconSpaceReserved="false" app:title="@string/pref_storage"> - + + @@ -118,8 +123,8 @@ + app:iconSpaceReserved="false" + app:isPreferenceVisible="false">