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 REPO_ORDER = "repo_order"
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 THEME_ORDINAL = "theme_ordinal"
const val BOOT_ID = "boot_id"
@ -109,7 +109,7 @@ object Config : PreferenceModel, DBConfig {
var bootId by preference(Key.BOOT_ID, "")
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 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.core.ForegroundTracker
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.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.withStreams
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -76,7 +77,7 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
else ->
stream.copyTo(file.outputStream())
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
}
}
val newId = notifyFinish(this)

View File

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

View File

@ -2,7 +2,7 @@ package com.topjohnwu.magisk.core.download
import android.net.Uri
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.util.zip.ZipEntry
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.ktx.cachedFile
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.Parcelize
@ -31,7 +31,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel
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 androidx.core.os.postDelayed
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.utils.MediaStoreUtils.getDisplayName
import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.Dispatchers
@ -47,9 +48,7 @@ open class FlashZip(
console.add("- Copying zip to temp directory")
runCatching {
mUri.inputStream().use { input ->
tmpFile.outputStream().use { out -> input.copyTo(out) }
}
mUri.inputStream().writeTo(tmpFile)
}.getOrElse {
when (it) {
is FileNotFoundException -> console.add("! Invalid Uri")
@ -70,7 +69,7 @@ open class FlashZip(
return false
}
console.add("- Installing ${mUri.getDisplayName()}")
console.add("- Installing ${mUri.displayName}")
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.ktx.reboot
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell
@ -49,7 +49,7 @@ abstract class MagiskInstallImpl : KoinComponent {
protected lateinit var installDir: File
private lateinit var srcBoot: String
private lateinit var destFile: MediaStoreUtils.MediaStoreFile
private lateinit var destFile: MediaStoreUtils.UriFile
private lateinit var zipUri: Uri
protected val console: MutableList<String>
@ -233,12 +233,12 @@ abstract class MagiskInstallImpl : KoinComponent {
}
it.reset()
if (magic.contentEquals("ustar".toByteArray())) {
destFile = MediaStoreUtils.newFile("magisk_patched.tar")
destFile = MediaStoreUtils.getFile("magisk_patched.tar")
handleTar(it)
} else {
// Raw image
srcBoot = File(installDir, "boot.img").path
destFile = MediaStoreUtils.newFile("magisk_patched.img")
destFile = MediaStoreUtils.getFile("magisk_patched.img")
console.add("- Copying image to cache")
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.content.ContentResolver
@ -10,6 +10,9 @@ import android.os.Build
import android.os.Environment
import android.provider.MediaStore
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.ktx.get
import java.io.File
@ -24,32 +27,24 @@ object MediaStoreUtils {
private val cr: ContentResolver by lazy { get<Context>().contentResolver }
@SuppressLint("InlinedApi")
private val tableUri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Downloads.EXTERNAL_CONTENT_URI
} else {
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
}
@get:RequiresApi(api = 29)
private val tableUri
get() = MediaStore.Downloads.EXTERNAL_CONTENT_URI
fun relativePath(appName: String): String {
var path = Environment.DIRECTORY_DOWNLOADS
if (appName.isNotEmpty()) {
path += File.separator + appName
}
return path
}
private fun relativePath(name: String) =
if (name.isEmpty()) Environment.DIRECTORY_DOWNLOADS
else Environment.DIRECTORY_DOWNLOADS + File.separator + name
fun fullPath(name: String): String =
File(Environment.getExternalStorageDirectory(), relativePath(name)).canonicalPath
private val relativePath get() = relativePath(Config.downloadDir)
@RequiresApi(api = 29)
@Throws(IOException::class)
private fun insertFile(displayName: String): MediaStoreFile {
val values = ContentValues()
val relativePath = relativePath(Config.downloadPath)
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.RELATIVE_PATH, relativePath)
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
// before Android 11, MediaStore can not rename new file when file exists,
@ -70,7 +65,14 @@ object MediaStoreUtils {
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)
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
@ -82,7 +84,6 @@ object MediaStoreUtils {
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
val data = cursor.getString(dataColumn)
val relativePath = relativePath(Config.downloadPath)
if (data.endsWith(relativePath + File.separator + displayName)) {
return MediaStoreFile(id, data)
}
@ -91,26 +92,22 @@ object MediaStoreUtils {
return null
}
@SuppressLint("NewApi")
@Throws(IOException::class)
fun newFile(displayName: String): MediaStoreFile {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
insertFile(displayName)
} else {
queryFile(displayName)?.delete()
insertFile(displayName)
}
}
@Throws(IOException::class)
fun getFile(displayName: String): MediaStoreFile {
return queryFile(displayName) ?: insertFile(displayName)
fun getFile(displayName: String): UriFile {
return queryFile(displayName) ?:
/* this code path will never happen pre 29 */ insertFile(displayName)
}
fun Uri.inputStream() = cr.openInputStream(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" }
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
cr.query(this, projection, null, null, null)?.use { cursor ->
@ -140,11 +137,22 @@ object MediaStoreUtils {
}
}.getOrElse { false }
data class MediaStoreFile(val id: Long, private val data: String) {
val uri: Uri = ContentUris.withAppendedId(tableUri, id)
override fun toString() = data
interface UriFile {
val uri: Uri
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 selectionArgs = arrayOf(id.toString())
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(
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.events.SnackbarEvent
import com.topjohnwu.magisk.ktx.*
import com.topjohnwu.magisk.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.CallbackList
@ -109,7 +109,7 @@ class FlashViewModel(
viewModelScope.launch {
withContext(Dispatchers.IO) {
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 ->
logItems.forEach {
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.data.repository.LogRepository
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.view.TextItem
import kotlinx.coroutines.Dispatchers
@ -53,18 +53,16 @@ class LogViewModel(
}
fun saveMagiskLog() = withExternalRW {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val now = Calendar.getInstance()
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
)
val logFile = MediaStoreUtils.newFile(filename)
logFile.uri.outputStream().writer().use { it.write(consoleText) }
SnackbarEvent(logFile.toString()).publish()
}
viewModelScope.launch(Dispatchers.IO) {
val now = Calendar.getInstance()
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
)
val logFile = MediaStoreUtils.getFile(filename)
logFile.uri.outputStream().writer().use { it.write(consoleText) }
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.UpdateCheckService
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.currentLocale
import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.utils.MediaStoreUtils
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asTransitive
import com.topjohnwu.magisk.utils.set
@ -108,8 +108,8 @@ object AddShortcut : BaseSettingsItem.Blank() {
}
object DownloadPath : BaseSettingsItem.Input() {
override var value = Config.downloadPath
set(value) = setV(value, field, { field = it }) { Config.downloadPath = it }
override var value = Config.downloadDir
set(value) = setV(value, field, { field = it }) { Config.downloadDir = it }
override val title = R.string.settings_download_path_title.asTransitive()
override val description get() = path.asTransitive()
@ -122,7 +122,7 @@ object DownloadPath : BaseSettingsItem.Input() {
@get:Bindable
val path
get() = MediaStoreUtils.relativePath(result)
get() = MediaStoreUtils.fullPath(result)
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root