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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
<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
|
<application
|
||||||
android:label="Magisk Manager"
|
android:label="Magisk Manager"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.arch
|
package com.topjohnwu.magisk.arch
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.os.Build
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
@ -86,6 +87,10 @@ abstract class BaseViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun withExternalRW(callback: () -> Unit) {
|
fun withExternalRW(callback: () -> Unit) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
callback()
|
||||||
|
return
|
||||||
|
}
|
||||||
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
|
||||||
if (!it) {
|
if (!it) {
|
||||||
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
SnackbarEvent(R.string.external_rw_permission_denied).publish()
|
||||||
|
@ -2,7 +2,6 @@ package com.topjohnwu.magisk.core
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.core.content.edit
|
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.get
|
||||||
import com.topjohnwu.magisk.ktx.inject
|
import com.topjohnwu.magisk.ktx.inject
|
||||||
import com.topjohnwu.magisk.ui.theme.Theme
|
import com.topjohnwu.magisk.ui.theme.Theme
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.io.SuFile
|
import com.topjohnwu.superuser.io.SuFile
|
||||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||||
@ -111,7 +109,7 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var bootId by preference(Key.BOOT_ID, "")
|
var bootId by preference(Key.BOOT_ID, "")
|
||||||
var askedHome by preference(Key.ASKED_HOME, false)
|
var askedHome by preference(Key.ASKED_HOME, false)
|
||||||
|
|
||||||
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
var downloadPath by preference(Key.DOWNLOAD_PATH, "Magisk Manager")
|
||||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||||
|
|
||||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||||
@ -143,10 +141,6 @@ object Config : PreferenceModel, DBConfig {
|
|||||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||||
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", 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"
|
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||||
|
|
||||||
fun initialize() {
|
fun initialize() {
|
||||||
|
@ -10,8 +10,9 @@ import com.topjohnwu.magisk.core.ForegroundTracker
|
|||||||
import com.topjohnwu.magisk.core.base.BaseService
|
import com.topjohnwu.magisk.core.base.BaseService
|
||||||
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
import com.topjohnwu.magisk.core.utils.ProgressInputStream
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
import com.topjohnwu.magisk.ktx.checkSum
|
|
||||||
import com.topjohnwu.magisk.ktx.writeTo
|
import com.topjohnwu.magisk.ktx.writeTo
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.checkSum
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -68,14 +69,14 @@ abstract class BaseDownloadService : BaseService(), KoinComponent {
|
|||||||
// -- Download logic
|
// -- Download logic
|
||||||
|
|
||||||
private suspend fun Subject.startDownload() {
|
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) {
|
if (!skip) {
|
||||||
val stream = service.fetchFile(url).toProgressStream(this)
|
val stream = service.fetchFile(url).toProgressStream(this)
|
||||||
when (this) {
|
when (this) {
|
||||||
is Subject.Module -> // Download and process on-the-fly
|
is Subject.Module -> // Download and process on-the-fly
|
||||||
stream.toModule(file, service.fetchInstaller().byteStream())
|
stream.toModule(file, service.fetchInstaller().byteStream())
|
||||||
else ->
|
else ->
|
||||||
stream.writeTo(file)
|
stream.copyTo(file.outputStream())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val newId = notifyFinish(this)
|
val newId = notifyFinish(this)
|
||||||
|
@ -5,7 +5,9 @@ import android.app.Notification
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.core.download.Action.*
|
import com.topjohnwu.magisk.core.download.Action.*
|
||||||
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
|
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
|
||||||
import com.topjohnwu.magisk.core.download.Subject.*
|
import com.topjohnwu.magisk.core.download.Subject.*
|
||||||
@ -73,7 +75,7 @@ open class DownloadService : BaseDownloadService() {
|
|||||||
|
|
||||||
private fun Notification.Builder.setIntent(subject: Manager)
|
private fun Notification.Builder.setIntent(subject: Manager)
|
||||||
= when (subject.action) {
|
= when (subject.action) {
|
||||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
||||||
else -> setContentIntent(Intent())
|
else -> setContentIntent(Intent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import androidx.core.net.toFile
|
||||||
import com.topjohnwu.magisk.BuildConfig
|
import com.topjohnwu.magisk.BuildConfig
|
||||||
import com.topjohnwu.magisk.DynAPK
|
import com.topjohnwu.magisk.DynAPK
|
||||||
import com.topjohnwu.magisk.ProcessPhoenix
|
import com.topjohnwu.magisk.ProcessPhoenix
|
||||||
@ -61,13 +62,11 @@ private fun DownloadService.restore(apk: File, id: Int) {
|
|||||||
.setContentText("")
|
.setContentText("")
|
||||||
}
|
}
|
||||||
Config.export()
|
Config.export()
|
||||||
// Make it world readable
|
|
||||||
apk.setReadable(true, false)
|
|
||||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun DownloadService.handleAPK(subject: Subject.Manager) =
|
suspend fun DownloadService.handleAPK(subject: Subject.Manager) =
|
||||||
when (subject.action) {
|
when (subject.action) {
|
||||||
is Upgrade -> upgrade(subject.file, subject.notifyID())
|
is Upgrade -> upgrade(subject.file.toFile(), subject.notifyID())
|
||||||
is Restore -> restore(subject.file, subject.notifyID())
|
is Restore -> restore(subject.file.toFile(), subject.notifyID())
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
import com.topjohnwu.magisk.ktx.withStreams
|
||||||
import java.io.File
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
fun InputStream.toModule(file: File, installer: InputStream) {
|
fun InputStream.toModule(file: Uri, installer: InputStream) {
|
||||||
|
|
||||||
val input = ZipInputStream(buffered())
|
val input = ZipInputStream(buffered())
|
||||||
val output = ZipOutputStream(file.outputStream().buffered())
|
val output = ZipOutputStream(file.outputStream().buffered())
|
||||||
|
@ -1,24 +1,25 @@
|
|||||||
package com.topjohnwu.magisk.core.download
|
package com.topjohnwu.magisk.core.download
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
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.Info
|
||||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
import com.topjohnwu.magisk.core.model.ManagerJson
|
||||||
import com.topjohnwu.magisk.core.model.module.Repo
|
import com.topjohnwu.magisk.core.model.module.Repo
|
||||||
import com.topjohnwu.magisk.ktx.cachedFile
|
import com.topjohnwu.magisk.ktx.cachedFile
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils
|
||||||
import kotlinx.android.parcel.IgnoredOnParcel
|
import kotlinx.android.parcel.IgnoredOnParcel
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
sealed class Subject : Parcelable {
|
sealed class Subject : Parcelable {
|
||||||
|
|
||||||
abstract val url: String
|
abstract val url: String
|
||||||
abstract val file: File
|
abstract val file: Uri
|
||||||
abstract val action: Action
|
abstract val action: Action
|
||||||
open val title: String get() = file.name
|
abstract val title: String
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
class Module(
|
class Module(
|
||||||
@ -26,10 +27,11 @@ sealed class Subject : Parcelable {
|
|||||||
override val action: Action
|
override val action: Action
|
||||||
) : Subject() {
|
) : Subject() {
|
||||||
override val url: String get() = module.zipUrl
|
override val url: String get() = module.zipUrl
|
||||||
|
override val title: String get() = module.downloadFilename
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
File(Config.downloadDirectory, module.downloadFilename)
|
MediaStoreUtils.newFile(title).uri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ sealed class Subject : Parcelable {
|
|||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
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
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
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() {
|
private class Uninstall : Magisk() {
|
||||||
override val action get() = Action.Uninstall
|
override val action get() = Action.Uninstall
|
||||||
override val url: String get() = Info.remote.uninstaller.link
|
override val url: String get() = Info.remote.uninstaller.link
|
||||||
|
override val title: String get() = "uninstall.zip"
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
override val file by lazy {
|
||||||
get<Context>().cachedFile("uninstall.zip")
|
get<Context>().cachedFile(title).toUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
private class Download : Magisk() {
|
private class Download : Magisk() {
|
||||||
override val action get() = Action.Download
|
override val action get() = Action.Download
|
||||||
override val url: String get() = magisk.link
|
override val url: String get() = magisk.link
|
||||||
|
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
|
||||||
|
|
||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override val file by lazy {
|
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 androidx.core.os.postDelayed
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.utils.unzip
|
import com.topjohnwu.magisk.core.utils.unzip
|
||||||
import com.topjohnwu.magisk.ktx.fileName
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.getDisplayName
|
||||||
import com.topjohnwu.magisk.ktx.readUri
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -47,7 +47,7 @@ open class FlashZip(
|
|||||||
console.add("- Copying zip to temp directory")
|
console.add("- Copying zip to temp directory")
|
||||||
|
|
||||||
runCatching {
|
runCatching {
|
||||||
context.readUri(mUri).use { input ->
|
mUri.inputStream().use { input ->
|
||||||
tmpFile.outputStream().use { out -> input.copyTo(out) }
|
tmpFile.outputStream().use { out -> input.copyTo(out) }
|
||||||
}
|
}
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
@ -70,7 +70,7 @@ open class FlashZip(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
console.add("- Installing ${mUri.fileName}")
|
console.add("- Installing ${mUri.getDisplayName()}")
|
||||||
|
|
||||||
val parentFile = tmpFile.parent ?: return false
|
val parentFile = tmpFile.parent ?: return false
|
||||||
|
|
||||||
|
@ -6,18 +6,18 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.os.postDelayed
|
import androidx.core.os.postDelayed
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.Info
|
import com.topjohnwu.magisk.core.Info
|
||||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||||
import com.topjohnwu.magisk.di.Protected
|
import com.topjohnwu.magisk.di.Protected
|
||||||
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
|
||||||
import com.topjohnwu.magisk.ktx.readUri
|
|
||||||
import com.topjohnwu.magisk.ktx.reboot
|
import com.topjohnwu.magisk.ktx.reboot
|
||||||
import com.topjohnwu.magisk.ktx.withStreams
|
import com.topjohnwu.magisk.ktx.withStreams
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.signing.SignBoot
|
import com.topjohnwu.signing.SignBoot
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
@ -49,7 +49,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
|
|
||||||
protected lateinit var installDir: File
|
protected lateinit var installDir: File
|
||||||
private lateinit var srcBoot: String
|
private lateinit var srcBoot: String
|
||||||
private lateinit var destFile: File
|
private lateinit var destFile: MediaStoreUtils.MediaStoreFile
|
||||||
private lateinit var zipUri: Uri
|
private lateinit var zipUri: Uri
|
||||||
|
|
||||||
protected val console: MutableList<String>
|
protected val console: MutableList<String>
|
||||||
@ -113,7 +113,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
console.add("- Device platform: " + Build.CPU_ABI)
|
console.add("- Device platform: " + Build.CPU_ABI)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ZipInputStream(context.readUri(zipUri).buffered()).use { zi ->
|
ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
|
||||||
lateinit var ze: ZipEntry
|
lateinit var ze: ZipEntry
|
||||||
while (zi.nextEntry?.let { ze = it } != null) {
|
while (zi.nextEntry?.let { ze = it } != null) {
|
||||||
if (ze.isDirectory)
|
if (ze.isDirectory)
|
||||||
@ -166,7 +166,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
private fun handleTar(input: InputStream) {
|
private fun handleTar(input: InputStream) {
|
||||||
console.add("- Processing tar file")
|
console.add("- Processing tar file")
|
||||||
var vbmeta = false
|
var vbmeta = false
|
||||||
val tarOut = TarOutputStream(destFile)
|
val tarOut = TarOutputStream(destFile.uri.outputStream())
|
||||||
this.tarOut = tarOut
|
this.tarOut = tarOut
|
||||||
TarInputStream(input).use { tarIn ->
|
TarInputStream(input).use { tarIn ->
|
||||||
lateinit var entry: TarEntry
|
lateinit var entry: TarEntry
|
||||||
@ -224,7 +224,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
|
|
||||||
private fun handleFile(uri: Uri): Boolean {
|
private fun handleFile(uri: Uri): Boolean {
|
||||||
try {
|
try {
|
||||||
context.readUri(uri).buffered().use {
|
uri.inputStream().buffered().use {
|
||||||
it.mark(500)
|
it.mark(500)
|
||||||
val magic = ByteArray(5)
|
val magic = ByteArray(5)
|
||||||
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
|
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
|
||||||
@ -233,12 +233,12 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
}
|
}
|
||||||
it.reset()
|
it.reset()
|
||||||
if (magic.contentEquals("ustar".toByteArray())) {
|
if (magic.contentEquals("ustar".toByteArray())) {
|
||||||
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
|
destFile = MediaStoreUtils.newFile("magisk_patched.tar")
|
||||||
handleTar(it)
|
handleTar(it)
|
||||||
} else {
|
} else {
|
||||||
// Raw image
|
// Raw image
|
||||||
srcBoot = File(installDir, "boot.img").path
|
srcBoot = File(installDir, "boot.img").path
|
||||||
destFile = File(Config.downloadDirectory, "magisk_patched.img")
|
destFile = MediaStoreUtils.newFile("magisk_patched.img")
|
||||||
console.add("- Copying image to cache")
|
console.add("- Copying image to cache")
|
||||||
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
|
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
|
||||||
}
|
}
|
||||||
@ -324,7 +324,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
patched.length()))
|
patched.length()))
|
||||||
tarOut = null
|
tarOut = null
|
||||||
it
|
it
|
||||||
} ?: destFile.outputStream()
|
} ?: destFile.uri.outputStream()
|
||||||
SuFileInputStream(patched).use { it.copyTo(os); os.close() }
|
SuFileInputStream(patched).use { it.copyTo(os); os.close() }
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Failed to output to $destFile")
|
console.add("! Failed to output to $destFile")
|
||||||
@ -344,9 +344,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
private suspend fun postOTA(): Boolean {
|
private suspend fun postOTA(): Boolean {
|
||||||
val bootctl = SuFile("/data/adb/bootctl")
|
val bootctl = SuFile("/data/adb/bootctl")
|
||||||
try {
|
try {
|
||||||
withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
|
service.fetchBootctl().byteStream().copyTo(SuFileOutputStream(bootctl))
|
||||||
input, out -> input.copyTo(out)
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
console.add("! Unable to download bootctl")
|
console.add("! Unable to download bootctl")
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
@ -374,10 +372,10 @@ abstract class MagiskInstallImpl : KoinComponent {
|
|||||||
protected suspend fun secondSlot() =
|
protected suspend fun secondSlot() =
|
||||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||||
|
|
||||||
protected fun fixEnv(zip: File): Boolean {
|
protected fun fixEnv(zip: Uri): Boolean {
|
||||||
installDir = SuFile("/data/adb/magisk")
|
installDir = SuFile("/data/adb/magisk")
|
||||||
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
Shell.su("rm -rf /data/adb/magisk/*").exec()
|
||||||
zipUri = zip.toUri()
|
zipUri = zip
|
||||||
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
return extractZip() && Shell.su("fix_env").exec().isSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -432,7 +430,7 @@ sealed class MagiskInstaller(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class EnvFixTask(
|
class EnvFixTask(
|
||||||
private val zip: File
|
private val zip: Uri
|
||||||
) : MagiskInstallImpl() {
|
) : MagiskInstallImpl() {
|
||||||
override suspend fun operations() = fixEnv(zip)
|
override suspend fun operations() = fixEnv(zip)
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ import android.graphics.drawable.LayerDrawable
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.text.PrecomputedText
|
import android.text.PrecomputedText
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -33,7 +32,6 @@ import androidx.annotation.DrawableRes
|
|||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.net.toFile
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.text.PrecomputedTextCompat
|
import androidx.core.text.PrecomputedTextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
@ -43,7 +41,6 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import androidx.transition.AutoTransition
|
import androidx.transition.AutoTransition
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.topjohnwu.magisk.FileProvider
|
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.ResMgr
|
import com.topjohnwu.magisk.core.ResMgr
|
||||||
@ -56,7 +53,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.lang.reflect.Array as JArray
|
import java.lang.reflect.Array as JArray
|
||||||
|
|
||||||
val packageName: String get() = get<Context>().packageName
|
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) =
|
fun PackageManager.activities(packageName: String) =
|
||||||
getPackageInfo(packageName, GET_ACTIVITIES)
|
getPackageInfo(packageName, GET_ACTIVITIES)
|
||||||
|
|
||||||
@ -123,9 +102,6 @@ fun PackageManager.providers(packageName: String) =
|
|||||||
|
|
||||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||||
|
|
||||||
fun Context.readUri(uri: Uri) =
|
|
||||||
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
|
||||||
|
|
||||||
fun Context.getBitmap(id: Int): Bitmap {
|
fun Context.getBitmap(id: Int): Bitmap {
|
||||||
var drawable = AppCompatResources.getDrawable(this, id)!!
|
var drawable = AppCompatResources.getDrawable(this, id)!!
|
||||||
if (drawable is BitmapDrawable)
|
if (drawable is BitmapDrawable)
|
||||||
@ -249,17 +225,6 @@ fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<S
|
|||||||
return args
|
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 Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
||||||
|
|
||||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||||
@ -329,8 +294,6 @@ fun Context.unwrap(): Context {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
|
||||||
|
|
||||||
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
fun Context.hasPermissions(vararg permissions: String) = permissions.all {
|
||||||
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
ContextCompat.checkSelfPermission(this, it) == PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,10 @@ import java.io.InputStream
|
|||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import kotlin.experimental.and
|
|
||||||
|
|
||||||
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||||
var entry: ZipEntry? = nextEntry
|
var entry: ZipEntry? = nextEntry
|
||||||
@ -23,25 +21,7 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun InputStream.writeTo(file: File) =
|
fun InputStream.writeTo(file: File) = this.copyTo(file.outputStream())
|
||||||
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 }
|
|
||||||
|
|
||||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||||
inStream: In,
|
inStream: In,
|
||||||
|
@ -6,7 +6,6 @@ import android.content.pm.ActivityInfo
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.navigation.NavDeepLinkBuilder
|
import androidx.navigation.NavDeepLinkBuilder
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.arch.BaseUIActivity
|
import com.topjohnwu.magisk.arch.BaseUIActivity
|
||||||
@ -17,7 +16,6 @@ import com.topjohnwu.magisk.databinding.FragmentFlashMd2Binding
|
|||||||
import com.topjohnwu.magisk.ui.MainActivity
|
import com.topjohnwu.magisk.ui.MainActivity
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koin.core.parameter.parametersOf
|
import org.koin.core.parameter.parametersOf
|
||||||
import java.io.File
|
|
||||||
import com.topjohnwu.magisk.MainDirections.Companion.actionFlashFragment as toFlash
|
import com.topjohnwu.magisk.MainDirections.Companion.actionFlashFragment as toFlash
|
||||||
import com.topjohnwu.magisk.ui.flash.FlashFragmentArgs as args
|
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 */
|
/* Flashing is understood as installing / flashing magisk itself */
|
||||||
|
|
||||||
fun flashIntent(context: Context, file: File, isSecondSlot: Boolean, id: Int = -1) = args(
|
fun flashIntent(context: Context, file: Uri, isSecondSlot: Boolean, id: Int = -1) = args(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = flashType(isSecondSlot),
|
action = flashType(isSecondSlot),
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { createIntent(context, it) }
|
).let { createIntent(context, it) }
|
||||||
|
|
||||||
fun flash(file: File, isSecondSlot: Boolean, id: Int) = toFlash(
|
fun flash(file: Uri, isSecondSlot: Boolean, id: Int) = toFlash(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = flashType(isSecondSlot),
|
action = flashType(isSecondSlot),
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { BaseUIActivity.postDirections(it) }
|
).let { BaseUIActivity.postDirections(it) }
|
||||||
|
|
||||||
/* Patching is understood as injecting img files with magisk */
|
/* Patching is understood as injecting img files with magisk */
|
||||||
|
|
||||||
fun patchIntent(context: Context, file: File, uri: Uri, id: Int = -1) = args(
|
fun patchIntent(context: Context, file: Uri, uri: Uri, id: Int = -1) = args(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = Const.Value.PATCH_FILE,
|
action = Const.Value.PATCH_FILE,
|
||||||
additionalData = uri,
|
additionalData = uri,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { createIntent(context, it) }
|
).let { createIntent(context, it) }
|
||||||
|
|
||||||
fun patch(file: File, uri: Uri, id: Int) = toFlash(
|
fun patch(file: Uri, uri: Uri, id: Int) = toFlash(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = Const.Value.PATCH_FILE,
|
action = Const.Value.PATCH_FILE,
|
||||||
additionalData = uri,
|
additionalData = uri,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
@ -119,28 +117,28 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
|
|||||||
|
|
||||||
/* Uninstalling is understood as removing magisk entirely */
|
/* Uninstalling is understood as removing magisk entirely */
|
||||||
|
|
||||||
fun uninstallIntent(context: Context, file: File, id: Int = -1) = args(
|
fun uninstallIntent(context: Context, file: Uri, id: Int = -1) = args(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = Const.Value.UNINSTALL,
|
action = Const.Value.UNINSTALL,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { createIntent(context, it) }
|
).let { createIntent(context, it) }
|
||||||
|
|
||||||
fun uninstall(file: File, id: Int) = toFlash(
|
fun uninstall(file: Uri, id: Int) = toFlash(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = Const.Value.UNINSTALL,
|
action = Const.Value.UNINSTALL,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { BaseUIActivity.postDirections(it) }
|
).let { BaseUIActivity.postDirections(it) }
|
||||||
|
|
||||||
/* Installing is understood as flashing modules / zips */
|
/* Installing is understood as flashing modules / zips */
|
||||||
|
|
||||||
fun installIntent(context: Context, file: File, id: Int = -1) = args(
|
fun installIntent(context: Context, file: Uri, id: Int = -1) = args(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = Const.Value.FLASH_ZIP,
|
action = Const.Value.FLASH_ZIP,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { createIntent(context, it) }
|
).let { createIntent(context, it) }
|
||||||
|
|
||||||
fun install(file: File, id: Int) = toFlash(
|
fun install(file: Uri, id: Int) = toFlash(
|
||||||
installer = file.toUri(),
|
installer = file,
|
||||||
action = Const.Value.FLASH_ZIP,
|
action = Const.Value.FLASH_ZIP,
|
||||||
dismissId = id
|
dismissId = id
|
||||||
).let { BaseUIActivity.postDirections(it) }
|
).let { BaseUIActivity.postDirections(it) }
|
||||||
|
@ -10,13 +10,14 @@ import com.topjohnwu.magisk.R
|
|||||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||||
import com.topjohnwu.magisk.arch.diffListOf
|
import com.topjohnwu.magisk.arch.diffListOf
|
||||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||||
import com.topjohnwu.magisk.core.Config
|
|
||||||
import com.topjohnwu.magisk.core.Const
|
import com.topjohnwu.magisk.core.Const
|
||||||
import com.topjohnwu.magisk.core.tasks.FlashZip
|
import com.topjohnwu.magisk.core.tasks.FlashZip
|
||||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||||
import com.topjohnwu.magisk.databinding.RvBindingAdapter
|
import com.topjohnwu.magisk.databinding.RvBindingAdapter
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
import com.topjohnwu.magisk.ktx.*
|
import com.topjohnwu.magisk.ktx.*
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.magisk.view.Notifications
|
import com.topjohnwu.magisk.view.Notifications
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
@ -24,7 +25,6 @@ import com.topjohnwu.superuser.Shell
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class FlashViewModel(
|
class FlashViewModel(
|
||||||
args: FlashFragmentArgs,
|
args: FlashFragmentArgs,
|
||||||
@ -107,17 +107,17 @@ class FlashViewModel(
|
|||||||
|
|
||||||
private fun savePressed() = withExternalRW {
|
private fun savePressed() = withExternalRW {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
|
|
||||||
val file = File(Config.downloadDirectory, name)
|
|
||||||
withContext(Dispatchers.IO) {
|
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 {
|
logItems.forEach {
|
||||||
writer.write(it)
|
writer.write(it)
|
||||||
writer.newLine()
|
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.BaseViewModel
|
||||||
import com.topjohnwu.magisk.arch.diffListOf
|
import com.topjohnwu.magisk.arch.diffListOf
|
||||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
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.data.repository.LogRepository
|
||||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.magisk.view.TextItem
|
import com.topjohnwu.magisk.view.TextItem
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class LogViewModel(
|
class LogViewModel(
|
||||||
@ -40,7 +36,7 @@ class LogViewModel(
|
|||||||
|
|
||||||
// --- magisk log
|
// --- magisk log
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var consoleText= " "
|
var consoleText = " "
|
||||||
set(value) = set(value, field, { field = it }, BR.consoleText)
|
set(value) = set(value, field, { field = it }, BR.consoleText)
|
||||||
|
|
||||||
override fun refresh() = viewModelScope.launch {
|
override fun refresh() = viewModelScope.launch {
|
||||||
@ -57,23 +53,18 @@ class LogViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveMagiskLog() = withExternalRW {
|
fun saveMagiskLog() = withExternalRW {
|
||||||
val now = Calendar.getInstance()
|
viewModelScope.launch {
|
||||||
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
|
withContext(Dispatchers.IO) {
|
||||||
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
val now = Calendar.getInstance()
|
||||||
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
|
||||||
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
|
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 {
|
val logFile = MediaStoreUtils.newFile(filename)
|
||||||
logFile.createNewFile()
|
logFile.uri.outputStream().writer().use { it.write(consoleText) }
|
||||||
} catch (e: IOException) {
|
SnackbarEvent(logFile.toString()).publish()
|
||||||
Timber.e(e)
|
}
|
||||||
return@withExternalRW
|
|
||||||
}
|
|
||||||
|
|
||||||
Shell.su("cat ${Const.MAGISK_LOG} > $logFile").submit {
|
|
||||||
SnackbarEvent(logFile.path).publish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ package com.topjohnwu.magisk.ui.settings
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.databinding.Bindable
|
import androidx.databinding.Bindable
|
||||||
import com.topjohnwu.magisk.BR
|
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.DialogSettingsDownloadPathBinding
|
||||||
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
|
import com.topjohnwu.magisk.utils.MediaStoreUtils
|
||||||
import com.topjohnwu.magisk.utils.Utils
|
import com.topjohnwu.magisk.utils.Utils
|
||||||
import com.topjohnwu.magisk.utils.asTransitive
|
import com.topjohnwu.magisk.utils.asTransitive
|
||||||
import com.topjohnwu.magisk.utils.set
|
import com.topjohnwu.magisk.utils.set
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
// --- Customization
|
// --- Customization
|
||||||
|
|
||||||
@ -115,8 +114,7 @@ object DownloadPath : BaseSettingsItem.Input() {
|
|||||||
override val title = R.string.settings_download_path_title.asTransitive()
|
override val title = R.string.settings_download_path_title.asTransitive()
|
||||||
override val description get() = path.asTransitive()
|
override val description get() = path.asTransitive()
|
||||||
|
|
||||||
override val inputResult: String?
|
override val inputResult: String get() = result
|
||||||
get() = if (Utils.ensureDownloadPath(result) != null) result else null
|
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var result = value
|
var result = value
|
||||||
@ -124,7 +122,7 @@ object DownloadPath : BaseSettingsItem.Input() {
|
|||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
val path
|
val path
|
||||||
get() = File(Environment.getExternalStorageDirectory(), result).absolutePath.orEmpty()
|
get() = MediaStoreUtils.relativePath(result)
|
||||||
|
|
||||||
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
|
override fun getView(context: Context) = DialogSettingsDownloadPathBinding
|
||||||
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
|
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
|
||||||
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import com.topjohnwu.magisk.R
|
import com.topjohnwu.magisk.R
|
||||||
import com.topjohnwu.magisk.core.Config
|
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.core.Info
|
||||||
import com.topjohnwu.magisk.ktx.get
|
import com.topjohnwu.magisk.ktx.get
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object Utils {
|
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