Support scoped storage

This commit is contained in:
vvb2060 2020-08-21 22:36:59 +08:00 committed by John Wu
parent 1ed67eed35
commit 9e81db8692
18 changed files with 248 additions and 166 deletions

View File

@ -4,7 +4,9 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<application
android:label="Magisk Manager"

View File

@ -1,6 +1,7 @@
package com.topjohnwu.magisk.arch
import android.Manifest
import android.os.Build
import androidx.annotation.CallSuper
import androidx.core.graphics.Insets
import androidx.databinding.Bindable
@ -86,6 +87,10 @@ abstract class BaseViewModel(
}
fun withExternalRW(callback: () -> 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()

View File

@ -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<Context>().getExternalFilesDir(null)!!
private const val SU_FINGERPRINT = "su_fingerprint"
fun initialize() {

View File

@ -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)

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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())

View File

@ -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<Context>().cachedFile("manager.apk")
get<Context>().cachedFile("manager.apk").toUri()
}
}
@ -67,7 +69,7 @@ sealed class Subject : Parcelable {
@IgnoredOnParcel
override val file by lazy {
get<Context>().cachedFile("magisk.zip")
get<Context>().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<Context>().cachedFile("uninstall.zip")
get<Context>().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
}
}

View File

@ -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

View File

@ -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<String>
@ -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)

View File

@ -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<Context>().packageName
@ -92,23 +88,6 @@ val ApplicationInfo.packageInfo: PackageInfo?
}
}
val Uri.fileName: String
get() {
var name: String? = null
get<Context>().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<String> = mutableListOf()): MutableList<S
return args
}
fun File.provide(context: Context = get()): Uri {
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
}
fun File.mv(destination: File) {
inputStream().writeTo(destination)
deleteRecursively()
}
fun String.toFile() = File(this)
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
fun Context.cachedFile(name: String) = File(cacheDir, name)
@ -329,8 +294,6 @@ fun Context.unwrap(): Context {
return context
}
fun Uri.writeTo(file: File) = toFile().copyTo(file)
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
}

View File

@ -8,12 +8,10 @@ import java.io.InputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import kotlin.experimental.and
fun ZipInputStream.forEach(callback: (ZipEntry) -> 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 <In : InputStream, Out : OutputStream> withStreams(
inStream: In,

View File

@ -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<FlashViewModel, FragmentFlashMd2Binding>()
/* 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<FlashViewModel, FragmentFlashMd2Binding>()
/* 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) }

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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<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)
}
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
}
}
}

View File

@ -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
}
}