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