diff --git a/app/src/main/java/com/topjohnwu/magisk/Config.kt b/app/src/main/java/com/topjohnwu/magisk/Config.kt index 31f6c0666..e1f4f891d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/Config.kt +++ b/app/src/main/java/com/topjohnwu/magisk/Config.kt @@ -43,6 +43,7 @@ object Config : PreferenceModel, DBConfig { const val REPO_ORDER = "repo_order" const val SHOW_SYSTEM_APP = "show_system" const val DOWNLOAD_CACHE = "download_cache" + const val DOWNLOAD_PATH = "download_path" // system state const val MAGISKHIDE = "magiskhide" @@ -92,10 +93,13 @@ object Config : PreferenceModel, DBConfig { } private val defaultChannel = - if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL - else Value.DEFAULT_CHANNEL + if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL + else Value.DEFAULT_CHANNEL + + private val defaultDownloadPath get() = Const.EXTERNAL_PATH.toRelativeString(Const.EXTERNAL_PATH.parentFile) var isDownloadCacheEnabled by preference(Key.DOWNLOAD_CACHE, true) + var downloadPath by preference(Key.DOWNLOAD_PATH, defaultDownloadPath) var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE) var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10) @@ -123,6 +127,15 @@ object Config : PreferenceModel, DBConfig { @JvmStatic var suManager by dbStrings(Key.SU_MANAGER, "") + fun downloadsFile(path: String = downloadPath) = + File(Const.EXTERNAL_PATH.parentFile, path).run { + if (exists()) { + if (isDirectory) this else null + } else { + if (mkdirs()) this else null + } + } + fun initialize() = prefs.edit { val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS) if (config.exists()) runCatching { @@ -190,8 +203,10 @@ object Config : PreferenceModel, DBConfig { fun export() { // Flush prefs to disk prefs.edit().apply() - val xml = File("${get(Protected).filesDir.parent}/shared_prefs", - "${packageName}_preferences.xml") + val xml = File( + "${get(Protected).filesDir.parent}/shared_prefs", + "${packageName}_preferences.xml" + ) Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec() } diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt index a31e9cc2e..82262c1e8 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/DownloadService.kt @@ -11,6 +11,7 @@ import android.widget.Toast import androidx.annotation.RequiresPermission import androidx.core.app.NotificationCompat import com.topjohnwu.magisk.ClassMap +import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.R import com.topjohnwu.magisk.model.entity.internal.Configuration.* @@ -29,7 +30,7 @@ import kotlin.random.Random.Default.nextInt open class DownloadService : RemoteFileService() { private val context get() = this - private val String.downloadsFile get() = File(Const.EXTERNAL_PATH, this) + private val String.downloadsFile get() = Config.downloadsFile()?.let { File(it, this) } private val File.type get() = MimeTypeMap.getSingleton() .getMimeTypeFromExtension(extension) @@ -100,19 +101,36 @@ open class DownloadService : RemoteFileService() { // --- private fun moveToDownloads(file: File) { - val destination = file.name.downloadsFile + val destination = file.name.downloadsFile ?: let { + Utils.toast( + getString(R.string.download_file_folder_error), + Toast.LENGTH_LONG + ) + return + } + if (file != destination) { destination.deleteRecursively() file.copyTo(destination) } + Utils.toast( - getString(R.string.internal_storage, "/Download/${file.name}"), + getString( + R.string.internal_storage, + "/" + destination.toRelativeString(Const.EXTERNAL_PATH.parentFile) + ), Toast.LENGTH_LONG ) } private fun fileIntent(fileName: String): Intent { - val file = fileName.downloadsFile + val file = fileName.downloadsFile ?: let { + Utils.toast( + getString(R.string.download_file_folder_error), + Toast.LENGTH_LONG + ) + return Intent() + } return Intent(Intent.ACTION_VIEW) .setDataAndType(file.provide(this), file.type) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt b/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt index fd73019ba..ac77544eb 100644 --- a/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt +++ b/app/src/main/java/com/topjohnwu/magisk/model/download/RemoteFileService.kt @@ -9,6 +9,7 @@ import com.topjohnwu.magisk.R import com.topjohnwu.magisk.data.repository.FileRepository import com.topjohnwu.magisk.model.entity.internal.DownloadSubject import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.* +import com.topjohnwu.magisk.utils.firstMap import com.topjohnwu.magisk.utils.writeToCachedFile import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.superuser.ShellUtils @@ -25,6 +26,13 @@ abstract class RemoteFileService : NotificationService() { private val repo by inject() + private val supportedFolders + get() = listOfNotNull( + cacheDir, + Config.downloadsFile(), + Const.EXTERNAL_PATH + ) + override val defaultNotification: NotificationCompat.Builder get() = Notifications .progress(this, "") @@ -53,15 +61,7 @@ abstract class RemoteFileService : NotificationService() { throw IllegalStateException("The download cache is disabled") } - val file = runCatching { - cacheDir.list().orEmpty() - .first { it == subject.fileName } // this throws an exception if not found - .let { File(cacheDir, it) } - }.getOrElse { - Const.EXTERNAL_PATH.list().orEmpty() - .first { it == subject.fileName } // this throws an exception if not found - .let { File(Const.EXTERNAL_PATH, it) } - } + val file = supportedFolders.firstMap { it.find(subject.fileName) } if (subject is Magisk) { if (!ShellUtils.checkSum("MD5", file, subject.magisk.hash)) { @@ -91,6 +91,10 @@ abstract class RemoteFileService : NotificationService() { // --- + private fun File.find(name: String) = list().orEmpty() + .firstOrNull { it == name } + ?.let { File(this, it) } + private fun ResponseBody.toFile(id: Int, name: String): File { val maxRaw = contentLength() val max = maxRaw / 1_000_000f diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.kt index 5095ed8f8..b41bc6c91 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/base/BasePreferenceFragment.kt @@ -20,6 +20,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat(), protected val prefs: SharedPreferences by inject() protected val app: App by inject() + protected val activity get() = requireActivity() as MagiskActivity<*, *> override fun onCreateView( inflater: LayoutInflater, diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt index f97164104..5a12d209d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsFragment.kt @@ -83,24 +83,34 @@ class SettingsFragment : BasePreferenceFragment() { true } + findPreference(Config.Key.DOWNLOAD_PATH).apply { + summary = Config.downloadPath + }.setOnPreferenceClickListener { preference -> + activity.withExternalRW { + onSuccess { + showUrlDialog(Config.downloadPath) { + Config.downloadsFile(it)?.let { _ -> + Config.downloadPath = it + preference.summary = it + } ?: let { + Utils.toast(R.string.settings_download_path_error, Toast.LENGTH_SHORT) + } + } + } + } + true + } + updateChannel.setOnPreferenceChangeListener { _, value -> val channel = Integer.parseInt(value as String) val previous = Config.updateChannel if (channel == Config.Value.CUSTOM_CHANNEL) { - val v = LayoutInflater.from(requireActivity()) - .inflate(R.layout.custom_channel_dialog, null) - val url = v.findViewById(R.id.custom_url) - url.setText(Config.customChannelUrl) - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.settings_update_custom) - .setView(v) - .setPositiveButton(R.string.ok) { _, _ -> - Config.customChannelUrl = url.text.toString() } - .setNegativeButton(R.string.close) { _, _ -> - Config.updateChannel = previous } - .setOnCancelListener { Config.updateChannel = previous } - .show() + showUrlDialog(Config.customChannelUrl, { + Config.updateChannel = previous + }, { + Config.customChannelUrl = it + }) } true } @@ -208,48 +218,51 @@ class SettingsFragment : BasePreferenceFragment() { private fun setLocalePreference(lp: ListPreference) { lp.isEnabled = false LocaleManager.availableLocales - .map { - val names = mutableListOf() - val values = mutableListOf() + .map { + val names = mutableListOf() + val values = mutableListOf() - names.add(LocaleManager.getString( - LocaleManager.defaultLocale, R.string.system_default)) - values.add("") + names.add( + LocaleManager.getString( + LocaleManager.defaultLocale, R.string.system_default + ) + ) + values.add("") - it.forEach { locale -> - names.add(locale.getDisplayName(locale)) - values.add(LocaleManager.toLanguageTag(locale)) - } - - Pair(names.toTypedArray(), values.toTypedArray()) - }.subscribeK { (names, values) -> - lp.isEnabled = true - lp.entries = names - lp.entryValues = values - lp.summary = LocaleManager.locale.getDisplayName(LocaleManager.locale) + it.forEach { locale -> + names.add(locale.getDisplayName(locale)) + values.add(LocaleManager.toLanguageTag(locale)) } + + Pair(names.toTypedArray(), values.toTypedArray()) + }.subscribeK { (names, values) -> + lp.isEnabled = true + lp.entries = names + lp.entryValues = values + lp.summary = LocaleManager.locale.getDisplayName(LocaleManager.locale) + } } private fun setSummary(key: String) { when (key) { Config.Key.ROOT_ACCESS -> rootConfig.summary = resources - .getStringArray(R.array.su_access)[Config.rootMode] + .getStringArray(R.array.su_access)[Config.rootMode] Config.Key.SU_MULTIUSER_MODE -> multiuserConfig.summary = resources - .getStringArray(R.array.multiuser_summary)[Config.suMultiuserMode] + .getStringArray(R.array.multiuser_summary)[Config.suMultiuserMode] Config.Key.SU_MNT_NS -> nsConfig.summary = resources - .getStringArray(R.array.namespace_summary)[Config.suMntNamespaceMode] + .getStringArray(R.array.namespace_summary)[Config.suMntNamespaceMode] Config.Key.UPDATE_CHANNEL -> { var ch = Config.updateChannel ch = if (ch < 0) Config.Value.STABLE_CHANNEL else ch updateChannel.summary = resources - .getStringArray(R.array.update_channel)[ch] + .getStringArray(R.array.update_channel)[ch] } Config.Key.SU_AUTO_RESPONSE -> autoRes.summary = resources - .getStringArray(R.array.auto_response)[Config.suAutoReponse] + .getStringArray(R.array.auto_response)[Config.suAutoReponse] Config.Key.SU_NOTIFICATION -> suNotification.summary = resources - .getStringArray(R.array.su_notification)[Config.suNotification] + .getStringArray(R.array.su_notification)[Config.suNotification] Config.Key.SU_REQUEST_TIMEOUT -> requestTimeout.summary = - getString(R.string.request_timeout_summary, Config.suDefaultTimeout) + getString(R.string.request_timeout_summary, Config.suDefaultTimeout) } } @@ -262,4 +275,26 @@ class SettingsFragment : BasePreferenceFragment() { setSummary(Config.Key.SU_NOTIFICATION) setSummary(Config.Key.SU_REQUEST_TIMEOUT) } + + private inline fun showUrlDialog( + initialValue: String, + crossinline onCancel: () -> Unit = {}, + crossinline onSuccess: (String) -> Unit + ) { + val v = LayoutInflater + .from(requireActivity()) + .inflate(R.layout.custom_channel_dialog, null) + + val url = v.findViewById(R.id.custom_url).apply { + setText(initialValue) + } + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.settings_update_custom) + .setView(v) + .setPositiveButton(R.string.ok) { _, _ -> onSuccess(url.text.toString()) } + .setNegativeButton(R.string.close) { _, _ -> onCancel() } + .setOnCancelListener { onCancel() } + .show() + } } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/XJava.kt b/app/src/main/java/com/topjohnwu/magisk/utils/XJava.kt index 689471a6f..cbcab88bc 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/XJava.kt +++ b/app/src/main/java/com/topjohnwu/magisk/utils/XJava.kt @@ -35,4 +35,11 @@ inline fun withStreams( withBoth(reader, writer) } } +} + +inline fun List.firstMap(mapper: (T) -> R?): R { + for (item: T in this) { + return mapper(item) ?: continue + } + throw NoSuchElementException("Collection contains no element matching the predicate.") } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index baebfea75..9842ad7ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,6 +75,7 @@ %1$.2f / %2$.2f MB Injecting installer… Error downloading file + Unable to fetch parent folder in order to save the downloaded file, check permissions. Magisk Update Available! Magisk Manager Update Available! @@ -126,11 +127,13 @@ Download one module at a time. + Downloads General Dark Theme Enable dark theme. Download Cache Enables download cache for Magisk and Module zip files. + Download path Clear Repo Cache Clear the cached information for online repos. This forces the app to refresh online. Hide Magisk Manager @@ -192,6 +195,7 @@ Each root session will have its own isolated namespace. Does not support Android 8.0+. No fingerprints were set or no device support. + Error creating folder. It must be accessible from storage root directory and must not be a file. Superuser Request diff --git a/app/src/main/res/xml/app_settings.xml b/app/src/main/res/xml/app_settings.xml index 91e1347ba..ad8004b03 100644 --- a/app/src/main/res/xml/app_settings.xml +++ b/app/src/main/res/xml/app_settings.xml @@ -1,35 +1,28 @@ - + + android:key="dark_theme" + android:summary="@string/settings_dark_theme_summary" + android:title="@string/settings_dark_theme_title" /> - - + android:key="locale" + android:title="@string/language" /> + android:summary="@string/settings_clear_cache_summary" + android:title="@string/settings_clear_cache_title" /> + android:summary="@string/settings_hide_manager_summary" + android:title="@string/settings_hide_manager_title" /> + + + + + + + + + android:key="check_update" + android:summary="@string/settings_check_update_summary" + android:title="@string/settings_check_update_title" /> + android:entryValues="@array/value_array" + android:key="update_channel" + android:title="@string/settings_update_channel_title" /> @@ -62,18 +71,18 @@ + android:summary="@string/settings_core_only_summary" + android:title="@string/settings_core_only_title" /> + android:summary="@string/settings_magiskhide_summary" + android:title="@string/magiskhide" /> + android:summary="@string/settings_hosts_summary" + android:title="@string/settings_hosts_title" /> @@ -82,55 +91,55 @@ android:title="@string/superuser"> + android:entryValues="@array/value_array" + android:key="root_access" + android:title="@string/superuser_access" /> + android:entryValues="@array/value_array" + android:key="multiuser_mode" + android:title="@string/multiuser_mode" /> + android:entryValues="@array/value_array" + android:key="mnt_ns" + android:title="@string/mount_namespace_mode" /> + android:entryValues="@array/value_array" + android:key="su_auto_response" + android:title="@string/auto_response" /> + android:entryValues="@array/request_timeout_value" + android:key="su_request_timeout" + android:title="@string/request_timeout" /> + android:entryValues="@array/value_array" + android:key="su_notification" + android:title="@string/superuser_notification" /> + android:summary="@string/settings_su_fingerprint_summary" + android:title="@string/settings_su_fingerprint_title" /> + android:key="su_reauth" + android:summary="@string/settings_su_reauth_summary" + android:title="@string/settings_su_reauth_title" />