Support scoped storage
This commit is contained in:
parent
1ed67eed35
commit
9e81db8692
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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) }
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
153
app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt
Normal file
153
app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user