Several changes for using MediaStore

- Change config key name so default downloads to folder 'Download'
- Always use getFile as we do not need existing file deleted
- Fallback to use File based I/O pre API 29 as officially MediaStore
  APIs do not support general purpose usage. And also, it was working
  fine on all devices before. If it ain't broke, don't fix it
- Show full download path in settings to make it more clear to the user
- Close streams after using them
This commit is contained in:
topjohnwu 2020-08-22 04:20:18 -07:00 committed by John Wu
parent 9e81db8692
commit 14a2f63b8b
12 changed files with 90 additions and 84 deletions

View File

@ -49,7 +49,7 @@ object Config : PreferenceModel, DBConfig {
const val DARK_THEME = "dark_theme_extended" const val DARK_THEME = "dark_theme_extended"
const val REPO_ORDER = "repo_order" const val REPO_ORDER = "repo_order"
const val SHOW_SYSTEM_APP = "show_system" const val SHOW_SYSTEM_APP = "show_system"
const val DOWNLOAD_PATH = "download_path" const val DOWNLOAD_DIR = "download_dir"
const val SAFETY = "safety_notice" const val SAFETY = "safety_notice"
const val THEME_ORDINAL = "theme_ordinal" const val THEME_ORDINAL = "theme_ordinal"
const val BOOT_ID = "boot_id" const val BOOT_ID = "boot_id"
@ -109,7 +109,7 @@ object Config : PreferenceModel, DBConfig {
var bootId by preference(Key.BOOT_ID, "") var bootId by preference(Key.BOOT_ID, "")
var askedHome by preference(Key.ASKED_HOME, false) var askedHome by preference(Key.ASKED_HOME, false)
var downloadPath by preference(Key.DOWNLOAD_PATH, "Magisk Manager") var downloadDir by preference(Key.DOWNLOAD_DIR, "")
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE) var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10) var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)

View File

@ -8,11 +8,12 @@ import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ForegroundTracker import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.base.BaseService import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.checkSum
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.MediaStoreUtils.checkSum
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -76,7 +77,7 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
is Subject.Module -> // Download and process on-the-fly is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream()) stream.toModule(file, service.fetchInstaller().byteStream())
else -> else ->
stream.copyTo(file.outputStream()) withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
} }
} }
val newId = notifyFinish(this) val newId = notifyFinish(this)

View File

@ -5,7 +5,6 @@ import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.net.toFile import androidx.core.net.toFile
import com.topjohnwu.magisk.core.download.Action.* import com.topjohnwu.magisk.core.download.Action.*

View File

@ -2,7 +2,7 @@ package com.topjohnwu.magisk.core.download
import android.net.Uri import android.net.Uri
import com.topjohnwu.magisk.ktx.withStreams import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream

View File

@ -10,7 +10,7 @@ import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.ktx.cachedFile import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ktx.get import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
@ -31,7 +31,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel @IgnoredOnParcel
override val file by lazy { override val file by lazy {
MediaStoreUtils.newFile(title).uri MediaStoreUtils.getFile(title).uri
} }
} }

View File

@ -4,9 +4,10 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.core.utils.unzip import com.topjohnwu.magisk.core.utils.unzip
import com.topjohnwu.magisk.utils.MediaStoreUtils.getDisplayName import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -47,9 +48,7 @@ open class FlashZip(
console.add("- Copying zip to temp directory") console.add("- Copying zip to temp directory")
runCatching { runCatching {
mUri.inputStream().use { input -> mUri.inputStream().writeTo(tmpFile)
tmpFile.outputStream().use { out -> input.copyTo(out) }
}
}.getOrElse { }.getOrElse {
when (it) { when (it) {
is FileNotFoundException -> console.add("! Invalid Uri") is FileNotFoundException -> console.add("! Invalid Uri")
@ -70,7 +69,7 @@ open class FlashZip(
return false return false
} }
console.add("- Installing ${mUri.getDisplayName()}") console.add("- Installing ${mUri.displayName}")
val parentFile = tmpFile.parent ?: return false val parentFile = tmpFile.parent ?: return false

View File

@ -15,9 +15,9 @@ import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.events.dialog.EnvFixDialog import com.topjohnwu.magisk.events.dialog.EnvFixDialog
import com.topjohnwu.magisk.ktx.reboot import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.ktx.withStreams import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.SignBoot import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
@ -49,7 +49,7 @@ abstract class MagiskInstallImpl : KoinComponent {
protected lateinit var installDir: File protected lateinit var installDir: File
private lateinit var srcBoot: String private lateinit var srcBoot: String
private lateinit var destFile: MediaStoreUtils.MediaStoreFile private lateinit var destFile: MediaStoreUtils.UriFile
private lateinit var zipUri: Uri private lateinit var zipUri: Uri
protected val console: MutableList<String> protected val console: MutableList<String>
@ -233,12 +233,12 @@ abstract class MagiskInstallImpl : KoinComponent {
} }
it.reset() it.reset()
if (magic.contentEquals("ustar".toByteArray())) { if (magic.contentEquals("ustar".toByteArray())) {
destFile = MediaStoreUtils.newFile("magisk_patched.tar") destFile = MediaStoreUtils.getFile("magisk_patched.tar")
handleTar(it) handleTar(it)
} else { } else {
// Raw image // Raw image
srcBoot = File(installDir, "boot.img").path srcBoot = File(installDir, "boot.img").path
destFile = MediaStoreUtils.newFile("magisk_patched.img") destFile = MediaStoreUtils.getFile("magisk_patched.img")
console.add("- Copying image to cache") console.add("- Copying image to cache")
FileOutputStream(srcBoot).use { out -> it.copyTo(out) } FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
} }

View File

@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ContentResolver import android.content.ContentResolver
@ -10,6 +10,9 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.annotation.RequiresApi
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.ktx.get import com.topjohnwu.magisk.ktx.get
import java.io.File import java.io.File
@ -24,32 +27,24 @@ object MediaStoreUtils {
private val cr: ContentResolver by lazy { get<Context>().contentResolver } private val cr: ContentResolver by lazy { get<Context>().contentResolver }
@SuppressLint("InlinedApi") @get:RequiresApi(api = 29)
private val tableUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { private val tableUri
MediaStore.Downloads.EXTERNAL_CONTENT_URI get() = MediaStore.Downloads.EXTERNAL_CONTENT_URI
} else {
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
}
fun relativePath(appName: String): String { private fun relativePath(name: String) =
var path = Environment.DIRECTORY_DOWNLOADS if (name.isEmpty()) Environment.DIRECTORY_DOWNLOADS
if (appName.isNotEmpty()) { else Environment.DIRECTORY_DOWNLOADS + File.separator + name
path += File.separator + appName
}
return path
}
fun fullPath(name: String): String =
File(Environment.getExternalStorageDirectory(), relativePath(name)).canonicalPath
private val relativePath get() = relativePath(Config.downloadDir)
@RequiresApi(api = 29)
@Throws(IOException::class) @Throws(IOException::class)
private fun insertFile(displayName: String): MediaStoreFile { private fun insertFile(displayName: String): MediaStoreFile {
val values = ContentValues() val values = ContentValues()
val relativePath = relativePath(Config.downloadPath) values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
} else {
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
values.put(MediaStore.MediaColumns.DATA, File(parent, displayName).path)
parent.mkdirs()
}
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
// before Android 11, MediaStore can not rename new file when file exists, // before Android 11, MediaStore can not rename new file when file exists,
@ -70,7 +65,14 @@ object MediaStoreUtils {
throw IOException("Can't insert $displayName.") throw IOException("Can't insert $displayName.")
} }
private fun queryFile(displayName: String): MediaStoreFile? { private fun queryFile(displayName: String): UriFile? {
if (Build.VERSION.SDK_INT < 29) {
// Before official general purpose MediaStore API exists, fallback to file based I/O
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
parent.mkdirs()
return LegacyUriFile(File(parent, displayName))
}
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA) val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used. // Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?" val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
@ -82,7 +84,6 @@ object MediaStoreUtils {
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn) val id = cursor.getLong(idColumn)
val data = cursor.getString(dataColumn) val data = cursor.getString(dataColumn)
val relativePath = relativePath(Config.downloadPath)
if (data.endsWith(relativePath + File.separator + displayName)) { if (data.endsWith(relativePath + File.separator + displayName)) {
return MediaStoreFile(id, data) return MediaStoreFile(id, data)
} }
@ -91,26 +92,22 @@ object MediaStoreUtils {
return null return null
} }
@SuppressLint("NewApi")
@Throws(IOException::class) @Throws(IOException::class)
fun newFile(displayName: String): MediaStoreFile { fun getFile(displayName: String): UriFile {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return queryFile(displayName) ?:
insertFile(displayName) /* this code path will never happen pre 29 */ insertFile(displayName)
} else {
queryFile(displayName)?.delete()
insertFile(displayName)
}
}
@Throws(IOException::class)
fun getFile(displayName: String): MediaStoreFile {
return queryFile(displayName) ?: insertFile(displayName)
} }
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException() fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
fun Uri.outputStream() = cr.openOutputStream(this) ?: throw FileNotFoundException() fun Uri.outputStream() = cr.openOutputStream(this) ?: throw FileNotFoundException()
fun Uri.getDisplayName(): String { val Uri.displayName: String get() {
if (scheme == "file") {
// Simple uri wrapper over file, directly get file name
return toFile().name
}
require(scheme == "content") { "Uri lacks 'content' scheme: $this" } require(scheme == "content") { "Uri lacks 'content' scheme: $this" }
val projection = arrayOf(OpenableColumns.DISPLAY_NAME) val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
cr.query(this, projection, null, null, null)?.use { cursor -> cr.query(this, projection, null, null, null)?.use { cursor ->
@ -140,11 +137,22 @@ object MediaStoreUtils {
} }
}.getOrElse { false } }.getOrElse { false }
data class MediaStoreFile(val id: Long, private val data: String) { interface UriFile {
val uri: Uri = ContentUris.withAppendedId(tableUri, id) val uri: Uri
override fun toString() = data fun delete(): Boolean
}
fun delete(): Boolean { private class LegacyUriFile(private val file: File) : UriFile {
override val uri = file.toUri()
override fun delete() = file.delete()
override fun toString() = file.toString()
}
@RequiresApi(api = 29)
private class MediaStoreFile(private val id: Long, private val data: String) : UriFile {
override val uri = ContentUris.withAppendedId(tableUri, id)
override fun toString() = data
override fun delete(): Boolean {
val selection = "${MediaStore.MediaColumns._ID} == ?" val selection = "${MediaStore.MediaColumns._ID} == ?"
val selectionArgs = arrayOf(id.toString()) val selectionArgs = arrayOf(id.toString())
return cr.delete(uri, selection, selectionArgs) == 1 return cr.delete(uri, selection, selectionArgs) == 1

View File

@ -21,7 +21,8 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
} }
} }
fun InputStream.writeTo(file: File) = this.copyTo(file.outputStream()) fun InputStream.writeTo(file: File) =
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
inline fun <In : InputStream, Out : OutputStream> withStreams( inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In, inStream: In,

View File

@ -16,8 +16,8 @@ import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.databinding.RvBindingAdapter import com.topjohnwu.magisk.databinding.RvBindingAdapter
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ktx.* import com.topjohnwu.magisk.ktx.*
import com.topjohnwu.magisk.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
@ -109,7 +109,7 @@ class FlashViewModel(
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard)) val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
val file = MediaStoreUtils.newFile(name) val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer -> file.uri.outputStream().bufferedWriter().use { writer ->
logItems.forEach { logItems.forEach {
writer.write(it) writer.write(it)

View File

@ -9,8 +9,8 @@ import com.topjohnwu.magisk.arch.diffListOf
import com.topjohnwu.magisk.arch.itemBindingOf import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.data.repository.LogRepository import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.view.TextItem import com.topjohnwu.magisk.view.TextItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -53,18 +53,16 @@ class LogViewModel(
} }
fun saveMagiskLog() = withExternalRW { fun saveMagiskLog() = withExternalRW {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) { val now = Calendar.getInstance()
val now = Calendar.getInstance() val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format( now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
now.get(Calendar.MINUTE), now.get(Calendar.SECOND) )
) val logFile = MediaStoreUtils.getFile(filename)
val logFile = MediaStoreUtils.newFile(filename) logFile.uri.outputStream().writer().use { it.write(consoleText) }
logFile.uri.outputStream().writer().use { it.write(consoleText) } SnackbarEvent(logFile.toString()).publish()
SnackbarEvent(logFile.toString()).publish()
}
} }
} }

View File

@ -12,13 +12,13 @@ import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.UpdateCheckService import com.topjohnwu.magisk.core.UpdateCheckService
import com.topjohnwu.magisk.core.utils.BiometricHelper import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.availableLocales import com.topjohnwu.magisk.core.utils.availableLocales
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.ktx.get import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asTransitive import com.topjohnwu.magisk.utils.asTransitive
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
@ -108,8 +108,8 @@ object AddShortcut : BaseSettingsItem.Blank() {
} }
object DownloadPath : BaseSettingsItem.Input() { object DownloadPath : BaseSettingsItem.Input() {
override var value = Config.downloadPath override var value = Config.downloadDir
set(value) = setV(value, field, { field = it }) { Config.downloadPath = it } set(value) = setV(value, field, { field = it }) { Config.downloadDir = it }
override val title = R.string.settings_download_path_title.asTransitive() override val title = R.string.settings_download_path_title.asTransitive()
override val description get() = path.asTransitive() override val description get() = path.asTransitive()
@ -122,7 +122,7 @@ object DownloadPath : BaseSettingsItem.Input() {
@get:Bindable @get:Bindable
val path val path
get() = MediaStoreUtils.relativePath(result) get() = MediaStoreUtils.fullPath(result)
override fun getView(context: Context) = DialogSettingsDownloadPathBinding override fun getView(context: Context) = DialogSettingsDownloadPathBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root .inflate(LayoutInflater.from(context)).also { it.data = this }.root