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 5cbd7e85d..8d0e0be61 100644 --- a/app/src/main/java/com/topjohnwu/magisk/core/Config.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/Config.kt @@ -49,7 +49,7 @@ object Config : PreferenceModel, DBConfig { const val DARK_THEME = "dark_theme_extended" const val REPO_ORDER = "repo_order" const val SHOW_SYSTEM_APP = "show_system" - const val DOWNLOAD_PATH = "download_path" + const val DOWNLOAD_DIR = "download_dir" const val SAFETY = "safety_notice" const val THEME_ORDINAL = "theme_ordinal" const val BOOT_ID = "boot_id" @@ -109,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, "Magisk Manager") + var downloadDir by preference(Key.DOWNLOAD_DIR, "") var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE) var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10) 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 857a52031..b01a28c8a 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 @@ -8,11 +8,12 @@ import androidx.lifecycle.MutableLiveData import com.topjohnwu.magisk.R import com.topjohnwu.magisk.core.ForegroundTracker import com.topjohnwu.magisk.core.base.BaseService +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.checkSum +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.ProgressInputStream import com.topjohnwu.magisk.data.network.GithubRawServices +import com.topjohnwu.magisk.ktx.withStreams 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 @@ -76,7 +77,7 @@ abstract class BaseDownloadService : BaseService(), KoinComponent { is Subject.Module -> // Download and process on-the-fly stream.toModule(file, service.fetchInstaller().byteStream()) else -> - stream.copyTo(file.outputStream()) + withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) } } } 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 23cab14a0..f60b39193 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,6 @@ 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.* 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 b0ad3b528..8aebc7f81 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 @@ -2,7 +2,7 @@ package com.topjohnwu.magisk.core.download import android.net.Uri import com.topjohnwu.magisk.ktx.withStreams -import com.topjohnwu.magisk.utils.MediaStoreUtils.outputStream +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import java.io.InputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream 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 65bfbf75d..635d7248c 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 @@ -10,7 +10,7 @@ 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 com.topjohnwu.magisk.core.utils.MediaStoreUtils import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize @@ -31,7 +31,7 @@ sealed class Subject : Parcelable { @IgnoredOnParcel override val file by lazy { - MediaStoreUtils.newFile(title).uri + 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 322a6128d..e054f7c1b 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 @@ -4,9 +4,10 @@ import android.content.Context import android.net.Uri import androidx.core.os.postDelayed import com.topjohnwu.magisk.core.Const +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName import com.topjohnwu.magisk.core.utils.unzip -import com.topjohnwu.magisk.utils.MediaStoreUtils.getDisplayName -import com.topjohnwu.magisk.utils.MediaStoreUtils.inputStream +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream +import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.internal.UiThreadHandler import kotlinx.coroutines.Dispatchers @@ -47,9 +48,7 @@ open class FlashZip( console.add("- Copying zip to temp directory") runCatching { - mUri.inputStream().use { input -> - tmpFile.outputStream().use { out -> input.copyTo(out) } - } + mUri.inputStream().writeTo(tmpFile) }.getOrElse { when (it) { is FileNotFoundException -> console.add("! Invalid Uri") @@ -70,7 +69,7 @@ open class FlashZip( return false } - console.add("- Installing ${mUri.getDisplayName()}") + console.add("- Installing ${mUri.displayName}") 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 c594fba18..934d1451b 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 @@ -15,9 +15,9 @@ import com.topjohnwu.magisk.di.Protected import com.topjohnwu.magisk.events.dialog.EnvFixDialog 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.core.utils.MediaStoreUtils +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream +import com.topjohnwu.magisk.core.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: MediaStoreUtils.MediaStoreFile + private lateinit var destFile: MediaStoreUtils.UriFile private lateinit var zipUri: Uri protected val console: MutableList @@ -233,12 +233,12 @@ abstract class MagiskInstallImpl : KoinComponent { } it.reset() if (magic.contentEquals("ustar".toByteArray())) { - destFile = MediaStoreUtils.newFile("magisk_patched.tar") + destFile = MediaStoreUtils.getFile("magisk_patched.tar") handleTar(it) } else { // Raw image srcBoot = File(installDir, "boot.img").path - destFile = MediaStoreUtils.newFile("magisk_patched.img") + destFile = MediaStoreUtils.getFile("magisk_patched.img") console.add("- Copying image to cache") FileOutputStream(srcBoot).use { out -> it.copyTo(out) } } diff --git a/app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt b/app/src/main/java/com/topjohnwu/magisk/core/utils/MediaStoreUtils.kt similarity index 70% rename from app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt rename to app/src/main/java/com/topjohnwu/magisk/core/utils/MediaStoreUtils.kt index 140b998a9..d564b7a09 100644 --- a/app/src/main/java/com/topjohnwu/magisk/utils/MediaStoreUtils.kt +++ b/app/src/main/java/com/topjohnwu/magisk/core/utils/MediaStoreUtils.kt @@ -1,4 +1,4 @@ -package com.topjohnwu.magisk.utils +package com.topjohnwu.magisk.core.utils import android.annotation.SuppressLint import android.content.ContentResolver @@ -10,6 +10,9 @@ import android.os.Build import android.os.Environment import android.provider.MediaStore import android.provider.OpenableColumns +import androidx.annotation.RequiresApi +import androidx.core.net.toFile +import androidx.core.net.toUri import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.ktx.get import java.io.File @@ -24,32 +27,24 @@ 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) - } + @get:RequiresApi(api = 29) + private val tableUri + get() = MediaStore.Downloads.EXTERNAL_CONTENT_URI - fun relativePath(appName: String): String { - var path = Environment.DIRECTORY_DOWNLOADS - if (appName.isNotEmpty()) { - path += File.separator + appName - } - return path - } + private fun relativePath(name: String) = + if (name.isEmpty()) Environment.DIRECTORY_DOWNLOADS + else Environment.DIRECTORY_DOWNLOADS + File.separator + name + fun fullPath(name: String): String = + File(Environment.getExternalStorageDirectory(), relativePath(name)).canonicalPath + + private val relativePath get() = relativePath(Config.downloadDir) + + @RequiresApi(api = 29) @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.RELATIVE_PATH, relativePath) values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName) // before Android 11, MediaStore can not rename new file when file exists, @@ -70,7 +65,14 @@ object MediaStoreUtils { throw IOException("Can't insert $displayName.") } - private fun queryFile(displayName: String): MediaStoreFile? { + private fun queryFile(displayName: String): UriFile? { + if (Build.VERSION.SDK_INT < 29) { + // Before official general purpose MediaStore API exists, fallback to file based I/O + val parent = File(Environment.getExternalStorageDirectory(), relativePath) + parent.mkdirs() + return LegacyUriFile(File(parent, displayName)) + } + 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} == ?" @@ -82,7 +84,6 @@ object MediaStoreUtils { 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) } @@ -91,26 +92,22 @@ object MediaStoreUtils { return null } + @SuppressLint("NewApi") @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 getFile(displayName: String): UriFile { + return queryFile(displayName) ?: + /* this code path will never happen pre 29 */ insertFile(displayName) } fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException() fun Uri.outputStream() = cr.openOutputStream(this) ?: throw FileNotFoundException() - fun Uri.getDisplayName(): String { + val Uri.displayName: String get() { + if (scheme == "file") { + // Simple uri wrapper over file, directly get file name + return toFile().name + } require(scheme == "content") { "Uri lacks 'content' scheme: $this" } val projection = arrayOf(OpenableColumns.DISPLAY_NAME) cr.query(this, projection, null, null, null)?.use { cursor -> @@ -140,11 +137,22 @@ object MediaStoreUtils { } }.getOrElse { false } - data class MediaStoreFile(val id: Long, private val data: String) { - val uri: Uri = ContentUris.withAppendedId(tableUri, id) - override fun toString() = data + interface UriFile { + val uri: Uri + fun delete(): Boolean + } - fun delete(): Boolean { + private class LegacyUriFile(private val file: File) : UriFile { + override val uri = file.toUri() + override fun delete() = file.delete() + override fun toString() = file.toString() + } + + @RequiresApi(api = 29) + private class MediaStoreFile(private val id: Long, private val data: String) : UriFile { + override val uri = ContentUris.withAppendedId(tableUri, id) + override fun toString() = data + override 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/ktx/XJava.kt b/app/src/main/java/com/topjohnwu/magisk/ktx/XJava.kt index 5b5826260..3b086e71d 100644 --- a/app/src/main/java/com/topjohnwu/magisk/ktx/XJava.kt +++ b/app/src/main/java/com/topjohnwu/magisk/ktx/XJava.kt @@ -21,7 +21,8 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) { } } -fun InputStream.writeTo(file: File) = this.copyTo(file.outputStream()) +fun InputStream.writeTo(file: File) = + withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) } inline fun withStreams( inStream: In, 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 856f6b5ec..e6f92c607 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 @@ -16,8 +16,8 @@ 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.core.utils.MediaStoreUtils +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.superuser.CallbackList @@ -109,7 +109,7 @@ class FlashViewModel( viewModelScope.launch { withContext(Dispatchers.IO) { val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard)) - val file = MediaStoreUtils.newFile(name) + val file = MediaStoreUtils.getFile(name) file.uri.outputStream().bufferedWriter().use { writer -> logItems.forEach { writer.write(it) 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 b20a0a5f0..a81d7a839 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 @@ -9,8 +9,8 @@ import com.topjohnwu.magisk.arch.diffListOf import com.topjohnwu.magisk.arch.itemBindingOf 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.core.utils.MediaStoreUtils +import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.view.TextItem import kotlinx.coroutines.Dispatchers @@ -53,18 +53,16 @@ class LogViewModel( } fun saveMagiskLog() = withExternalRW { - 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() - } + viewModelScope.launch(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.getFile(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 e93c4637c..3e37058fe 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 @@ -12,13 +12,13 @@ import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.UpdateCheckService import com.topjohnwu.magisk.core.utils.BiometricHelper +import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.availableLocales import com.topjohnwu.magisk.core.utils.currentLocale 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 @@ -108,8 +108,8 @@ object AddShortcut : BaseSettingsItem.Blank() { } object DownloadPath : BaseSettingsItem.Input() { - override var value = Config.downloadPath - set(value) = setV(value, field, { field = it }) { Config.downloadPath = it } + override var value = Config.downloadDir + set(value) = setV(value, field, { field = it }) { Config.downloadDir = it } override val title = R.string.settings_download_path_title.asTransitive() override val description get() = path.asTransitive() @@ -122,7 +122,7 @@ object DownloadPath : BaseSettingsItem.Input() { @get:Bindable val path - get() = MediaStoreUtils.relativePath(result) + get() = MediaStoreUtils.fullPath(result) override fun getView(context: Context) = DialogSettingsDownloadPathBinding .inflate(LayoutInflater.from(context)).also { it.data = this }.root