diff --git a/app/shared/src/main/AndroidManifest.xml b/app/shared/src/main/AndroidManifest.xml
index b3e806d77..a6180f73a 100644
--- a/app/shared/src/main/AndroidManifest.xml
+++ b/app/shared/src/main/AndroidManifest.xml
@@ -4,7 +4,9 @@
-
+
Unit) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ callback()
+ return
+ }
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
if (!it) {
SnackbarEvent(R.string.external_rw_permission_denied).publish()
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/Config.kt b/app/src/main/java/com/topjohnwu/magisk/core/Config.kt
index 4d1933a47..5cbd7e85d 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/Config.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/Config.kt
@@ -2,7 +2,6 @@ package com.topjohnwu.magisk.core
import android.content.Context
import android.content.SharedPreferences
-import android.os.Environment
import android.util.Xml
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
@@ -17,7 +16,6 @@ import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.inject
import com.topjohnwu.magisk.ui.theme.Theme
-import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
@@ -111,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, Environment.DIRECTORY_DOWNLOADS)
+ var downloadPath by preference(Key.DOWNLOAD_PATH, "Magisk Manager")
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
@@ -143,10 +141,6 @@ object Config : PreferenceModel, DBConfig {
var suManager by dbStrings(Key.SU_MANAGER, "", true)
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
- // Always return a path in external storage where we can write
- val downloadDirectory get() =
- Utils.ensureDownloadPath(downloadPath) ?: get().getExternalFilesDir(null)!!
-
private const val SU_FINGERPRINT = "su_fingerprint"
fun initialize() {
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/BaseDownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/BaseDownloadService.kt
index f5d1b944f..857a52031 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/download/BaseDownloadService.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/download/BaseDownloadService.kt
@@ -10,8 +10,9 @@ import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.data.network.GithubRawServices
-import com.topjohnwu.magisk.ktx.checkSum
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
@@ -68,14 +69,14 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
// -- Download logic
private suspend fun Subject.startDownload() {
- val skip = this is Subject.Magisk && file.exists() && file.checkSum("MD5", magisk.md5)
+ val skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
if (!skip) {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
else ->
- stream.writeTo(file)
+ stream.copyTo(file.outputStream())
}
}
val newId = notifyFinish(this)
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt
index e81a144c6..23cab14a0 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/download/DownloadService.kt
@@ -5,7 +5,9 @@ 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.*
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
import com.topjohnwu.magisk.core.download.Subject.*
@@ -73,7 +75,7 @@ open class DownloadService : BaseDownloadService() {
private fun Notification.Builder.setIntent(subject: Manager)
= when (subject.action) {
- APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
+ APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
else -> setContentIntent(Intent())
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt
index e86a46142..9c2aaaadb 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/download/ManagerUpgrade.kt
@@ -1,5 +1,6 @@
package com.topjohnwu.magisk.core.download
+import androidx.core.net.toFile
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.ProcessPhoenix
@@ -61,13 +62,11 @@ private fun DownloadService.restore(apk: File, id: Int) {
.setContentText("")
}
Config.export()
- // Make it world readable
- apk.setReadable(true, false)
Shell.su("pm install $apk && pm uninstall $packageName").exec()
}
suspend fun DownloadService.handleAPK(subject: Subject.Manager) =
when (subject.action) {
- is Upgrade -> upgrade(subject.file, subject.notifyID())
- is Restore -> restore(subject.file, subject.notifyID())
+ is Upgrade -> upgrade(subject.file.toFile(), subject.notifyID())
+ is Restore -> restore(subject.file.toFile(), subject.notifyID())
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt
index 6c14d7242..b0ad3b528 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/download/ModuleProcessor.kt
@@ -1,13 +1,14 @@
package com.topjohnwu.magisk.core.download
+import android.net.Uri
import com.topjohnwu.magisk.ktx.withStreams
-import java.io.File
+import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
-fun InputStream.toModule(file: File, installer: InputStream) {
+fun InputStream.toModule(file: Uri, installer: InputStream) {
val input = ZipInputStream(buffered())
val output = ZipOutputStream(file.outputStream().buffered())
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
index 6dea6cb30..65bfbf75d 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/download/Subject.kt
@@ -1,24 +1,25 @@
package com.topjohnwu.magisk.core.download
import android.content.Context
+import android.net.Uri
import android.os.Parcelable
-import com.topjohnwu.magisk.core.Config
+import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.MagiskJson
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 kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
-import java.io.File
sealed class Subject : Parcelable {
abstract val url: String
- abstract val file: File
+ abstract val file: Uri
abstract val action: Action
- open val title: String get() = file.name
+ abstract val title: String
@Parcelize
class Module(
@@ -26,10 +27,11 @@ sealed class Subject : Parcelable {
override val action: Action
) : Subject() {
override val url: String get() = module.zipUrl
+ override val title: String get() = module.downloadFilename
@IgnoredOnParcel
override val file by lazy {
- File(Config.downloadDirectory, module.downloadFilename)
+ MediaStoreUtils.newFile(title).uri
}
}
@@ -49,7 +51,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel
override val file by lazy {
- get().cachedFile("manager.apk")
+ get().cachedFile("manager.apk").toUri()
}
}
@@ -67,7 +69,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel
override val file by lazy {
- get().cachedFile("magisk.zip")
+ get().cachedFile("magisk.zip").toUri()
}
}
@@ -75,21 +77,24 @@ sealed class Subject : Parcelable {
private class Uninstall : Magisk() {
override val action get() = Action.Uninstall
override val url: String get() = Info.remote.uninstaller.link
+ override val title: String get() = "uninstall.zip"
@IgnoredOnParcel
override val file by lazy {
- get().cachedFile("uninstall.zip")
+ get().cachedFile(title).toUri()
}
+
}
@Parcelize
private class Download : Magisk() {
override val action get() = Action.Download
override val url: String get() = magisk.link
+ override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
@IgnoredOnParcel
override val file by lazy {
- File(Config.downloadDirectory, "Magisk-${magisk.version}(${magisk.versionCode}).zip")
+ MediaStoreUtils.getFile(title).uri
}
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt
index b1a23fc73..322a6128d 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/FlashZip.kt
@@ -5,8 +5,8 @@ import android.net.Uri
import androidx.core.os.postDelayed
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.utils.unzip
-import com.topjohnwu.magisk.ktx.fileName
-import com.topjohnwu.magisk.ktx.readUri
+import com.topjohnwu.magisk.utils.MediaStoreUtils.getDisplayName
+import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.Dispatchers
@@ -47,7 +47,7 @@ open class FlashZip(
console.add("- Copying zip to temp directory")
runCatching {
- context.readUri(mUri).use { input ->
+ mUri.inputStream().use { input ->
tmpFile.outputStream().use { out -> input.copyTo(out) }
}
}.getOrElse {
@@ -70,7 +70,7 @@ open class FlashZip(
return false
}
- console.add("- Installing ${mUri.fileName}")
+ console.add("- Installing ${mUri.getDisplayName()}")
val parentFile = tmpFile.parent ?: return false
diff --git a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt
index 7eafb474d..c594fba18 100644
--- a/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/core/tasks/MagiskInstaller.kt
@@ -6,18 +6,18 @@ import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.annotation.WorkerThread
-import androidx.core.net.toUri
import androidx.core.os.postDelayed
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.topjohnwu.magisk.R
-import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
-import com.topjohnwu.magisk.ktx.readUri
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.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: File
+ private lateinit var destFile: MediaStoreUtils.MediaStoreFile
private lateinit var zipUri: Uri
protected val console: MutableList
@@ -113,7 +113,7 @@ abstract class MagiskInstallImpl : KoinComponent {
console.add("- Device platform: " + Build.CPU_ABI)
try {
- ZipInputStream(context.readUri(zipUri).buffered()).use { zi ->
+ ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
lateinit var ze: ZipEntry
while (zi.nextEntry?.let { ze = it } != null) {
if (ze.isDirectory)
@@ -166,7 +166,7 @@ abstract class MagiskInstallImpl : KoinComponent {
private fun handleTar(input: InputStream) {
console.add("- Processing tar file")
var vbmeta = false
- val tarOut = TarOutputStream(destFile)
+ val tarOut = TarOutputStream(destFile.uri.outputStream())
this.tarOut = tarOut
TarInputStream(input).use { tarIn ->
lateinit var entry: TarEntry
@@ -224,7 +224,7 @@ abstract class MagiskInstallImpl : KoinComponent {
private fun handleFile(uri: Uri): Boolean {
try {
- context.readUri(uri).buffered().use {
+ uri.inputStream().buffered().use {
it.mark(500)
val magic = ByteArray(5)
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
@@ -233,12 +233,12 @@ abstract class MagiskInstallImpl : KoinComponent {
}
it.reset()
if (magic.contentEquals("ustar".toByteArray())) {
- destFile = File(Config.downloadDirectory, "magisk_patched.tar")
+ destFile = MediaStoreUtils.newFile("magisk_patched.tar")
handleTar(it)
} else {
// Raw image
srcBoot = File(installDir, "boot.img").path
- destFile = File(Config.downloadDirectory, "magisk_patched.img")
+ destFile = MediaStoreUtils.newFile("magisk_patched.img")
console.add("- Copying image to cache")
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
}
@@ -324,7 +324,7 @@ abstract class MagiskInstallImpl : KoinComponent {
patched.length()))
tarOut = null
it
- } ?: destFile.outputStream()
+ } ?: destFile.uri.outputStream()
SuFileInputStream(patched).use { it.copyTo(os); os.close() }
} catch (e: IOException) {
console.add("! Failed to output to $destFile")
@@ -344,9 +344,7 @@ abstract class MagiskInstallImpl : KoinComponent {
private suspend fun postOTA(): Boolean {
val bootctl = SuFile("/data/adb/bootctl")
try {
- withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
- input, out -> input.copyTo(out)
- }
+ service.fetchBootctl().byteStream().copyTo(SuFileOutputStream(bootctl))
} catch (e: IOException) {
console.add("! Unable to download bootctl")
Timber.e(e)
@@ -374,10 +372,10 @@ abstract class MagiskInstallImpl : KoinComponent {
protected suspend fun secondSlot() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
- protected fun fixEnv(zip: File): Boolean {
+ protected fun fixEnv(zip: Uri): Boolean {
installDir = SuFile("/data/adb/magisk")
Shell.su("rm -rf /data/adb/magisk/*").exec()
- zipUri = zip.toUri()
+ zipUri = zip
return extractZip() && Shell.su("fix_env").exec().isSuccess
}
@@ -432,7 +430,7 @@ sealed class MagiskInstaller(
}
class EnvFixTask(
- private val zip: File
+ private val zip: Uri
) : MagiskInstallImpl() {
override suspend fun operations() = fixEnv(zip)
diff --git a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt
index 3a512e612..901820a2a 100644
--- a/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/ktx/XAndroid.kt
@@ -21,7 +21,6 @@ import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
-import android.provider.OpenableColumns
import android.text.PrecomputedText
import android.view.View
import android.view.ViewGroup
@@ -33,7 +32,6 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
-import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.text.PrecomputedTextCompat
import androidx.core.view.isGone
@@ -43,7 +41,6 @@ import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
-import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.ResMgr
@@ -56,7 +53,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
-import java.io.FileNotFoundException
import java.lang.reflect.Array as JArray
val packageName: String get() = get().packageName
@@ -92,23 +88,6 @@ val ApplicationInfo.packageInfo: PackageInfo?
}
}
-val Uri.fileName: String
- get() {
- var name: String? = null
- get().contentResolver.query(this, null, null, null, null)?.use { c ->
- val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- if (nameIndex != -1) {
- c.moveToFirst()
- name = c.getString(nameIndex)
- }
- }
- if (name == null && path != null) {
- val idx = path!!.lastIndexOf('/')
- name = path!!.substring(idx + 1)
- }
- return name.orEmpty()
- }
-
fun PackageManager.activities(packageName: String) =
getPackageInfo(packageName, GET_ACTIVITIES)
@@ -123,9 +102,6 @@ fun PackageManager.providers(packageName: String) =
fun Context.rawResource(id: Int) = resources.openRawResource(id)
-fun Context.readUri(uri: Uri) =
- contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
-
fun Context.getBitmap(id: Int): Bitmap {
var drawable = AppCompatResources.getDrawable(this, id)!!
if (drawable is BitmapDrawable)
@@ -249,17 +225,6 @@ fun Intent.toCommand(args: MutableList = mutableListOf()): MutableList Unit) {
var entry: ZipEntry? = nextEntry
@@ -23,25 +21,7 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
}
}
-fun InputStream.writeTo(file: File) =
- withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
-
-fun File.checkSum(alg: String, reference: String) = runCatching {
- inputStream().use {
- val digest = MessageDigest.getInstance(alg)
- it.copyTo(object : OutputStream() {
- override fun write(b: Int) {
- digest.update(b.toByte())
- }
- override fun write(b: ByteArray, off: Int, len: Int) {
- digest.update(b, off, len)
- }
- })
- val sb = StringBuilder()
- digest.digest().forEach { b -> sb.append("%02x".format(b and 0xff.toByte())) }
- sb.toString() == reference
- }
-}.getOrElse { false }
+fun InputStream.writeTo(file: File) = this.copyTo(file.outputStream())
inline fun withStreams(
inStream: In,
diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt
index 384539e8c..90ec3c1d6 100644
--- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashFragment.kt
@@ -6,7 +6,6 @@ import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Bundle
import android.view.*
-import androidx.core.net.toUri
import androidx.navigation.NavDeepLinkBuilder
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIActivity
@@ -17,7 +16,6 @@ import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
import com.topjohnwu.magisk.ui.MainActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
-import java.io.File
import com.topjohnwu.magisk.MainDirections.Companion.actionFlashFragment as toFlash
import com.topjohnwu.magisk.ui.flash.FlashFragmentArgs as args
@@ -89,29 +87,29 @@ class FlashFragment : BaseUIFragment()
/* Flashing is understood as installing / flashing magisk itself */
- fun flashIntent(context: Context, file: File, isSecondSlot: Boolean, id: Int = -1) = args(
- installer = file.toUri(),
+ fun flashIntent(context: Context, file: Uri, isSecondSlot: Boolean, id: Int = -1) = args(
+ installer = file,
action = flashType(isSecondSlot),
dismissId = id
).let { createIntent(context, it) }
- fun flash(file: File, isSecondSlot: Boolean, id: Int) = toFlash(
- installer = file.toUri(),
+ fun flash(file: Uri, isSecondSlot: Boolean, id: Int) = toFlash(
+ installer = file,
action = flashType(isSecondSlot),
dismissId = id
).let { BaseUIActivity.postDirections(it) }
/* Patching is understood as injecting img files with magisk */
- fun patchIntent(context: Context, file: File, uri: Uri, id: Int = -1) = args(
- installer = file.toUri(),
+ fun patchIntent(context: Context, file: Uri, uri: Uri, id: Int = -1) = args(
+ installer = file,
action = Const.Value.PATCH_FILE,
additionalData = uri,
dismissId = id
).let { createIntent(context, it) }
- fun patch(file: File, uri: Uri, id: Int) = toFlash(
- installer = file.toUri(),
+ fun patch(file: Uri, uri: Uri, id: Int) = toFlash(
+ installer = file,
action = Const.Value.PATCH_FILE,
additionalData = uri,
dismissId = id
@@ -119,28 +117,28 @@ class FlashFragment : BaseUIFragment()
/* Uninstalling is understood as removing magisk entirely */
- fun uninstallIntent(context: Context, file: File, id: Int = -1) = args(
- installer = file.toUri(),
+ fun uninstallIntent(context: Context, file: Uri, id: Int = -1) = args(
+ installer = file,
action = Const.Value.UNINSTALL,
dismissId = id
).let { createIntent(context, it) }
- fun uninstall(file: File, id: Int) = toFlash(
- installer = file.toUri(),
+ fun uninstall(file: Uri, id: Int) = toFlash(
+ installer = file,
action = Const.Value.UNINSTALL,
dismissId = id
).let { BaseUIActivity.postDirections(it) }
/* Installing is understood as flashing modules / zips */
- fun installIntent(context: Context, file: File, id: Int = -1) = args(
- installer = file.toUri(),
+ fun installIntent(context: Context, file: Uri, id: Int = -1) = args(
+ installer = file,
action = Const.Value.FLASH_ZIP,
dismissId = id
).let { createIntent(context, it) }
- fun install(file: File, id: Int) = toFlash(
- installer = file.toUri(),
+ fun install(file: Uri, id: Int) = toFlash(
+ installer = file,
action = Const.Value.FLASH_ZIP,
dismissId = id
).let { BaseUIActivity.postDirections(it) }
diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
index 1f64e6f9b..856f6b5ec 100644
--- a/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
@@ -10,13 +10,14 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.diffListOf
import com.topjohnwu.magisk.arch.itemBindingOf
-import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.tasks.FlashZip
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.utils.set
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.CallbackList
@@ -24,7 +25,6 @@ import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import java.io.File
class FlashViewModel(
args: FlashFragmentArgs,
@@ -107,17 +107,17 @@ class FlashViewModel(
private fun savePressed() = withExternalRW {
viewModelScope.launch {
- val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
- val file = File(Config.downloadDirectory, name)
withContext(Dispatchers.IO) {
- file.bufferedWriter().use { writer ->
+ val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
+ val file = MediaStoreUtils.newFile(name)
+ file.uri.outputStream().bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
}
}
+ SnackbarEvent(file.toString()).publish()
}
- SnackbarEvent(file.path).publish()
}
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt
index ed8b89f6b..b20a0a5f0 100644
--- a/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/ui/log/LogViewModel.kt
@@ -7,19 +7,15 @@ import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.diffListOf
import com.topjohnwu.magisk.arch.itemBindingOf
-import com.topjohnwu.magisk.core.Config
-import com.topjohnwu.magisk.core.Const
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.utils.set
import com.topjohnwu.magisk.view.TextItem
-import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import timber.log.Timber
-import java.io.File
-import java.io.IOException
import java.util.*
class LogViewModel(
@@ -40,7 +36,7 @@ class LogViewModel(
// --- magisk log
@get:Bindable
- var consoleText= " "
+ var consoleText = " "
set(value) = set(value, field, { field = it }, BR.consoleText)
override fun refresh() = viewModelScope.launch {
@@ -57,23 +53,18 @@ class LogViewModel(
}
fun saveMagiskLog() = withExternalRW {
- 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 = File(Config.downloadDirectory, filename)
- try {
- logFile.createNewFile()
- } catch (e: IOException) {
- Timber.e(e)
- return@withExternalRW
- }
-
- Shell.su("cat ${Const.MAGISK_LOG} > $logFile").submit {
- SnackbarEvent(logFile.path).publish()
+ 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()
+ }
}
}
diff --git a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt
index 70722047d..e93c4637c 100644
--- a/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/ui/settings/SettingsItems.kt
@@ -2,7 +2,6 @@ package com.topjohnwu.magisk.ui.settings
import android.content.Context
import android.os.Build
-import android.os.Environment
import android.view.LayoutInflater
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
@@ -19,13 +18,13 @@ 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
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
-import java.io.File
// --- Customization
@@ -115,8 +114,7 @@ object DownloadPath : BaseSettingsItem.Input() {
override val title = R.string.settings_download_path_title.asTransitive()
override val description get() = path.asTransitive()
- override val inputResult: String?
- get() = if (Utils.ensureDownloadPath(result) != null) result else null
+ override val inputResult: String get() = result
@get:Bindable
var result = value
@@ -124,7 +122,7 @@ object DownloadPath : BaseSettingsItem.Input() {
@get:Bindable
val path
- get() = File(Environment.getExternalStorageDirectory(), result).absolutePath.orEmpty()
+ get() = MediaStoreUtils.relativePath(result)
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt b/app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt
new file mode 100644
index 000000000..140b998a9
--- /dev/null
+++ b/app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt
@@ -0,0 +1,153 @@
+package com.topjohnwu.magisk.utils
+
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import android.provider.OpenableColumns
+import com.topjohnwu.magisk.core.Config
+import com.topjohnwu.magisk.ktx.get
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.io.OutputStream
+import java.security.MessageDigest
+import kotlin.experimental.and
+
+@Suppress("DEPRECATION")
+object MediaStoreUtils {
+
+ private val cr: ContentResolver by lazy { get().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)
+ }
+
+ fun relativePath(appName: String): String {
+ var path = Environment.DIRECTORY_DOWNLOADS
+ if (appName.isNotEmpty()) {
+ path += File.separator + appName
+ }
+ return path
+ }
+
+ @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.DISPLAY_NAME, displayName)
+
+ // before Android 11, MediaStore can not rename new file when file exists,
+ // insert will return null. use newFile() instead.
+ val fileUri = cr.insert(tableUri, values) ?: throw IOException("Can't insert $displayName.")
+
+ val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
+ cr.query(fileUri, projection, null, null, null)?.use { cursor ->
+ val idIndex = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
+ val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
+ if (cursor.moveToFirst()) {
+ val id = cursor.getLong(idIndex)
+ val data = cursor.getString(dataColumn)
+ return MediaStoreFile(id, data)
+ }
+ }
+
+ throw IOException("Can't insert $displayName.")
+ }
+
+ private fun queryFile(displayName: String): MediaStoreFile? {
+ 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} == ?"
+ val selectionArgs = arrayOf(displayName)
+ val sortOrder = "${MediaStore.MediaColumns.DATE_ADDED} DESC"
+ cr.query(tableUri, projection, selection, selectionArgs, sortOrder)?.use { cursor ->
+ val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID)
+ val dataColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
+ 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)
+ }
+ }
+ }
+ return null
+ }
+
+ @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 Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
+
+ fun Uri.outputStream() = cr.openOutputStream(this) ?: throw FileNotFoundException()
+
+ fun Uri.getDisplayName(): String {
+ require(scheme == "content") { "Uri lacks 'content' scheme: $this" }
+ val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
+ cr.query(this, projection, null, null, null)?.use { cursor ->
+ val displayNameColumn = cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)
+ if (cursor.moveToFirst()) {
+ return cursor.getString(displayNameColumn)
+ }
+ }
+ return this.toString()
+ }
+
+ fun Uri.checkSum(alg: String, reference: String) = runCatching {
+ this.inputStream().use {
+ val digest = MessageDigest.getInstance(alg)
+ it.copyTo(object : OutputStream() {
+ override fun write(b: Int) {
+ digest.update(b.toByte())
+ }
+
+ override fun write(b: ByteArray, off: Int, len: Int) {
+ digest.update(b, off, len)
+ }
+ })
+ val sb = StringBuilder()
+ digest.digest().forEach { b -> sb.append("%02x".format(b and 0xff.toByte())) }
+ sb.toString() == reference
+ }
+ }.getOrElse { false }
+
+ data class MediaStoreFile(val id: Long, private val data: String) {
+ val uri: Uri = ContentUris.withAppendedId(tableUri, id)
+ override fun toString() = data
+
+ fun delete(): Boolean {
+ val selection = "${MediaStore.MediaColumns._ID} == ?"
+ val selectionArgs = arrayOf(id.toString())
+ return cr.delete(uri, selection, selectionArgs) == 1
+ }
+ }
+}
diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt
index 622e14440..dad7d4c46 100644
--- a/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt
+++ b/app/src/main/java/com/topjohnwu/magisk/utils/Utils.kt
@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.utils
import android.content.Context
import android.content.Intent
import android.net.Uri
-import android.os.Environment
import android.widget.Toast
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
@@ -11,7 +10,6 @@ import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.superuser.internal.UiThreadHandler
-import java.io.File
object Utils {
@@ -40,10 +38,4 @@ object Utils {
)
}
}
-
- fun ensureDownloadPath(path: String) =
- File(Environment.getExternalStorageDirectory(), path).run {
- if ((exists() && isDirectory) || mkdirs()) this else null
- }
-
}