From e73b56ed4df2fe31f183871ad2ea12b46335f0ed Mon Sep 17 00:00:00 2001 From: X1nto Date: Tue, 13 Apr 2021 14:18:53 +0400 Subject: [PATCH] require external storage --- app/src/main/AndroidManifest.xml | 20 +++- .../core/downloader/MicrogDownloader.kt | 2 +- .../core/downloader/MusicDownloader.kt | 2 +- .../com/vanced/manager/ui/MainActivity.kt | 7 ++ .../manager/ui/dialogs/DialogContainer.kt | 20 ++++ .../manager/ui/fragments/LogFragment.kt | 34 ++++--- .../manager/ui/viewmodels/HomeViewModel.kt | 8 +- .../vanced/manager/utils/DownloadHelper.kt | 98 ++++++++++--------- .../com/vanced/manager/utils/Extensions.kt | 2 + .../com/vanced/manager/utils/PackageHelper.kt | 29 +++--- .../com/vanced/manager/utils/StorageUtils.kt | 53 ++++++++++ app/src/main/res/values/strings.xml | 2 + 12 files changed, 188 insertions(+), 89 deletions(-) create mode 100644 app/src/main/java/com/vanced/manager/utils/StorageUtils.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 786de94c..cc6ff1f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,12 +8,18 @@ + + + - + + @@ -32,7 +38,9 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - tools:ignore="UnusedAttribute"> + tools:ignore="UnusedAttribute" + android:requestLegacyExternalStorage="true"> + + android:theme="@style/DarkTheme" /> + + + android:pathPrefix="/downloads" + android:scheme="https" /> diff --git a/app/src/main/java/com/vanced/manager/core/downloader/MicrogDownloader.kt b/app/src/main/java/com/vanced/manager/core/downloader/MicrogDownloader.kt index 1095bfae..f3880c5b 100644 --- a/app/src/main/java/com/vanced/manager/core/downloader/MicrogDownloader.kt +++ b/app/src/main/java/com/vanced/manager/core/downloader/MicrogDownloader.kt @@ -24,6 +24,6 @@ object MicrogDownloader { fun startMicrogInstall(context: Context) { installing.postValue(true) postReset() - install("${context.getExternalFilesDir(folderName)}/$fileName", context) + install("$folderName/$fileName".managerFilepath, context) } } diff --git a/app/src/main/java/com/vanced/manager/core/downloader/MusicDownloader.kt b/app/src/main/java/com/vanced/manager/core/downloader/MusicDownloader.kt index 0a6e663e..8fc95532 100644 --- a/app/src/main/java/com/vanced/manager/core/downloader/MusicDownloader.kt +++ b/app/src/main/java/com/vanced/manager/core/downloader/MusicDownloader.kt @@ -68,6 +68,6 @@ object MusicDownloader { if (variant == "root") installMusicRoot(context) else - install("${context.getExternalFilesDir("music/nonroot")}/nonroot.apk", context) + install("music/nonroot/nonroot.apk".managerFilepath, context) } } diff --git a/app/src/main/java/com/vanced/manager/ui/MainActivity.kt b/app/src/main/java/com/vanced/manager/ui/MainActivity.kt index 9d7c6ab3..cedf844d 100644 --- a/app/src/main/java/com/vanced/manager/ui/MainActivity.kt +++ b/app/src/main/java/com/vanced/manager/ui/MainActivity.kt @@ -40,6 +40,10 @@ class MainActivity : AppCompatActivity() { lateinit var binding: ActivityMainBinding private val navHost by lazy { findNavController(R.id.nav_host) } + companion object { + const val REQUEST_CODE = 69 + } + private val loadingObserver = object : LoadingStateListener { val tag = "VMLocalisation" override fun onDataChanged() { @@ -98,6 +102,9 @@ class MainActivity : AppCompatActivity() { setFinalTheme() super.onResume() Crowdin.registerDataLoadingObserver(loadingObserver) + if (!canAccessStorage(this)) { + DialogContainer.storageDialog(this) + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/java/com/vanced/manager/ui/dialogs/DialogContainer.kt b/app/src/main/java/com/vanced/manager/ui/dialogs/DialogContainer.kt index e9d2490b..15756977 100644 --- a/app/src/main/java/com/vanced/manager/ui/dialogs/DialogContainer.kt +++ b/app/src/main/java/com/vanced/manager/ui/dialogs/DialogContainer.kt @@ -1,13 +1,16 @@ package com.vanced.manager.ui.dialogs import android.content.Context +import android.os.Build import androidx.core.content.edit +import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vanced.manager.R import com.vanced.manager.utils.showWithAccent import com.vanced.manager.utils.isMiuiOptimizationsEnabled import com.vanced.manager.utils.openUrl +import com.vanced.manager.utils.requestStoragePerms object DialogContainer { @@ -30,6 +33,23 @@ object DialogContainer { prefs.edit { putBoolean("firstLaunch", false) } } + fun storageDialog(activity: FragmentActivity) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return + + MaterialAlertDialogBuilder(activity).apply { + setTitle(R.string.storage_access_required) + setMessage(R.string.storage_access_required_summary) + setPositiveButton(R.string.auth_dialog_ok) { dialog, _ -> + dialog.cancel() + requestStoragePerms(activity) + } + setCancelable(false) + create() + showWithAccent() + } + } + fun miuiDialog(context: Context) { MaterialAlertDialogBuilder(context).apply { setTitle(context.getString(R.string.miui_one_title)) diff --git a/app/src/main/java/com/vanced/manager/ui/fragments/LogFragment.kt b/app/src/main/java/com/vanced/manager/ui/fragments/LogFragment.kt index c0346c54..078f67d6 100644 --- a/app/src/main/java/com/vanced/manager/ui/fragments/LogFragment.kt +++ b/app/src/main/java/com/vanced/manager/ui/fragments/LogFragment.kt @@ -8,7 +8,10 @@ import android.widget.Toast import com.vanced.manager.R import com.vanced.manager.core.ui.base.BindingFragment import com.vanced.manager.databinding.FragmentLogBinding +import com.vanced.manager.utils.AppUtils import com.vanced.manager.utils.AppUtils.logs +import com.vanced.manager.utils.managerFilepath +import com.vanced.manager.utils.performStorageAction import java.io.File import java.io.FileWriter import java.io.IOException @@ -30,23 +33,30 @@ class LogFragment : BindingFragment() { val logs = TextUtils.concat(*logs.toTypedArray()) logText.text = logs logSave.setOnClickListener { + val calendar = Calendar.getInstance() + val year = calendar.get(Calendar.YEAR) + val month = calendar.get(Calendar.MONTH) + val day = calendar.get(Calendar.DAY_OF_MONTH) + val hour = calendar.get(Calendar.HOUR_OF_DAY) + val minute = calendar.get(Calendar.MINUTE) + val second = calendar.get(Calendar.SECOND) try { - val calendar = Calendar.getInstance() - val year = calendar.get(Calendar.YEAR) - val month = calendar.get(Calendar.MONTH) - val day = calendar.get(Calendar.DAY_OF_MONTH) - val hour = calendar.get(Calendar.HOUR_OF_DAY) - val minute = calendar.get(Calendar.MINUTE) - val second = calendar.get(Calendar.SECOND) - val log = File(requireActivity().getExternalFilesDir("logs")?.path + "/$year$month${day}_$hour$minute$second.log") - FileWriter(log).apply { - append(logs) - flush() - close() + performStorageAction(requireActivity()) { + val logPath = File("logs".managerFilepath).apply { + if (!exists()) { + mkdirs() + } + }.path + FileWriter(File(logPath, "$year$month${day}_$hour$minute$second.log")).apply { + append(logs) + flush() + close() + } } Toast.makeText(requireActivity(), R.string.logs_saved, Toast.LENGTH_SHORT).show() } catch (e: IOException) { Toast.makeText(requireActivity(), R.string.logs_not_saved, Toast.LENGTH_SHORT).show() + AppUtils.log("VMIO", "Could not save logs: $e") } } } diff --git a/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt b/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt index 29ce8de0..64f587a1 100644 --- a/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/vanced/manager/ui/viewmodels/HomeViewModel.kt @@ -51,7 +51,7 @@ class HomeViewModel(private val activity: FragmentActivity): ViewModel() { val microgModel = MutableLiveData() val musicModel = MutableLiveData() val musicRootModel = MutableLiveData() - val managerModel = MutableLiveData() + private val managerModel = MutableLiveData() fun fetchData() { viewModelScope.launch { @@ -110,7 +110,7 @@ class HomeViewModel(private val activity: FragmentActivity): ViewModel() { activity.getString(R.string.vanced) -> { when (variant) { "nonroot" -> { - if (vancedInstallFilesExist(activity)) { + if (vancedInstallFilesExist()) { InstallationFilesDetectedDialog.newInstance(app).show(activity) } else { VancedPreferencesDialog().show(activity) @@ -124,7 +124,7 @@ class HomeViewModel(private val activity: FragmentActivity): ViewModel() { activity.getString(R.string.music) -> { when (variant) { "nonroot" -> { - if (musicApkExists(activity)) { + if (musicApkExists()) { InstallationFilesDetectedDialog.newInstance(app).show(activity) } else { MusicPreferencesDialog().show(activity) @@ -136,7 +136,7 @@ class HomeViewModel(private val activity: FragmentActivity): ViewModel() { } } activity.getString(R.string.microg) -> { - if (apkExist(activity, "microg.apk")) { + if (apkExist("microg.apk")) { InstallationFilesDetectedDialog.newInstance(app).show(activity) } else { AppDownloadDialog.newInstance(app).show(activity) diff --git a/app/src/main/java/com/vanced/manager/utils/DownloadHelper.kt b/app/src/main/java/com/vanced/manager/utils/DownloadHelper.kt index 2232cfda..453dcaeb 100644 --- a/app/src/main/java/com/vanced/manager/utils/DownloadHelper.kt +++ b/app/src/main/java/com/vanced/manager/utils/DownloadHelper.kt @@ -49,13 +49,13 @@ object DownloadHelper : CoroutineScope by CoroutineScope(Dispatchers.IO) { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { launch { - if (response.body()?.let { writeFile(it, context.getExternalFilesDir(fileFolder)?.path + "/" + fileName) } == true) { + if (response.body()?.let { writeFile(it, fileFolder, fileName) } == true) { onDownloadComplete() } else { - onError("Could not save file") - downloadProgress.postValue(0) + onError(response.errorBody().toString()) log("VMDownloader", "Failed to save file: $url\n${response.errorBody()}") } + downloadProgress.postValue(0) } } else { val errorBody = response.errorBody().toString() @@ -68,76 +68,78 @@ object DownloadHelper : CoroutineScope by CoroutineScope(Dispatchers.IO) { override fun onFailure(call: Call, t: Throwable) { if (call.isCanceled) { log("VMDownloader", "Download canceled") - downloadProgress.postValue(0) } else { onError(t.stackTraceToString()) - downloadProgress.postValue(0) log("VMDownloader", "Failed to download file: $url") } + downloadProgress.postValue(0) } }) } - fun writeFile(body: ResponseBody, filePath: String): Boolean { - return try { - val file = File(filePath) + fun writeFile(body: ResponseBody, folderName: String, fileName: String): Boolean = + try { val totalBytes = body.contentLength() - var inputStream: InputStream? = null - var outputStream: OutputStream? = null - try { - val fileReader = ByteArray(4096) - var downloadedBytes: Long = 0 - inputStream = body.byteStream() - outputStream = FileOutputStream(file) - var read: Int - while (inputStream.read(fileReader).also { read = it } != -1) { - outputStream.write(fileReader, 0, read) - downloadedBytes += read.toLong() - downloadProgress.postValue((downloadedBytes * 100 / totalBytes).toInt()) + val fileReader = ByteArray(8096) + var downloadedBytes: Long = 0 + val dir = File(managerPath, folderName).apply { + if (!exists()) { + mkdirs() } - outputStream.flush() - true - } catch (e: IOException) { - false - } finally { - inputStream?.close() - outputStream?.close() } + val inputStream: InputStream = body.byteStream() + val outputStream: OutputStream = FileOutputStream("${dir.path}/$fileName") + var read: Int + while (inputStream.read(fileReader).also { read = it } != -1) { + outputStream.write(fileReader, 0, read) + downloadedBytes += read.toLong() + downloadProgress.postValue((downloadedBytes * 100 / totalBytes).toInt()) + } + outputStream.flush() + inputStream.close() + outputStream.close() + true } catch (e: IOException) { + log("VMIO", "Failed to save file: $e") false } - } fun downloadManager(context: Context) { val url = "https://github.com/YTVanced/VancedManager/releases/latest/download/manager.apk" - download(url,"https://github.com/YTVanced/VancedManager/", "manager", "manager.apk", context, onDownloadComplete = { - val apk = File("${context.getExternalFilesDir("manager")?.path}/manager.apk") - val uri = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - FileProvider.getUriForFile(context, "${context.packageName}.provider", apk) - else - Uri.fromFile(apk) + download( + url, + "https://github.com/YTVanced/VancedManager/", + "manager", + "manager.apk", + context, + onDownloadComplete = { + val apk = File("manager/manager.apk".managerFilepath) + val uri = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + FileProvider.getUriForFile(context, "${context.packageName}.provider", apk) + else + Uri.fromFile(apk) - val intent = Intent(Intent.ACTION_VIEW) - intent.setDataAndType(uri, "application/vnd.android.package-archive") - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - try { - context.startActivity(intent) - } catch (e: ActivityNotFoundException) { - log("VMDownloader", e.stackTraceToString()) - } finally { - sendCloseDialog(context) - } - }, onError = { + val intent = Intent(Intent.ACTION_VIEW) + intent.setDataAndType(uri, "application/vnd.android.package-archive") + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + log("VMDownloader", e.stackTraceToString()) + } finally { + sendCloseDialog(context) + } + }) { downloadingFile.postValue( context.getString( R.string.error_downloading, "manager.apk" ) ) - }) + } } } diff --git a/app/src/main/java/com/vanced/manager/utils/Extensions.kt b/app/src/main/java/com/vanced/manager/utils/Extensions.kt index 71c418c0..3ec6cab6 100644 --- a/app/src/main/java/com/vanced/manager/utils/Extensions.kt +++ b/app/src/main/java/com/vanced/manager/utils/Extensions.kt @@ -19,6 +19,8 @@ import java.util.* val RadioGroup.checkedButtonTag: String? get() = findViewById(checkedRadioButtonId)?.tag?.toString() +val String.managerFilepath get() = "$managerPath/$this" + fun DialogFragment.show(activity: FragmentActivity) { try { show(activity.supportFragmentManager, "") diff --git a/app/src/main/java/com/vanced/manager/utils/PackageHelper.kt b/app/src/main/java/com/vanced/manager/utils/PackageHelper.kt index 00a68490..6d50f376 100644 --- a/app/src/main/java/com/vanced/manager/utils/PackageHelper.kt +++ b/app/src/main/java/com/vanced/manager/utils/PackageHelper.kt @@ -100,16 +100,16 @@ object PackageHelper { } } - fun apkExist(context: Context, apk: String): Boolean { - val apkPath = File(context.getExternalFilesDir(apk.substring(0, apk.length - 4))?.path, apk) + fun apkExist(apk: String): Boolean { + val apkPath = File("${apk.removeSuffix(".apk")}/$apk".managerFilepath) if (apkPath.exists()) return true return false } - fun musicApkExists(context: Context): Boolean { - val apkPath = File(context.getExternalFilesDir("music/nonroot")?.path, "nonroot.apk") + fun musicApkExists(): Boolean { + val apkPath = File("music/nonroot/nonroot.apk".managerFilepath) if (apkPath.exists()) { return true } @@ -117,8 +117,8 @@ object PackageHelper { return false } - fun vancedInstallFilesExist(context: Context): Boolean { - val apksPath = File(context.getExternalFilesDir("vanced/nonroot")?.path.toString()) + fun vancedInstallFilesExist(): Boolean { + val apksPath = File("vanced/nonroot".managerFilepath) val splitFiles = mutableListOf() if (apksPath.exists()) { val files = apksPath.listFiles() @@ -212,8 +212,8 @@ object PackageHelper { private fun installRootApp(context: Context, app: String, appVerCode: Int?, pkg: String, modApkBool: (fileName: String) -> Boolean) = CoroutineScope(Dispatchers.IO).launch { Shell.getShell { - val apkFilesPath = context.getExternalFilesDir("$app/root")?.path - val files = File(apkFilesPath.toString()).listFiles()?.toList() + val apkFilesPath = "$app/root".managerFilepath + val files = File(apkFilesPath).listFiles()?.toList() if (files != null) { val modApk: File? = files.lastOrNull { modApkBool(it.name) } if (modApk != null) { @@ -268,7 +268,7 @@ object PackageHelper { appName: String ) { val packageInstaller = context.packageManager.packageInstaller - val folder = File(context.getExternalFilesDir("$appName/nonroot")?.path.toString()) + val folder = File("$appName/nonroot".managerFilepath) var session: PackageInstaller.Session? = null val sessionId: Int val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) @@ -299,17 +299,10 @@ object PackageHelper { } } - private fun installSplitApkFilesRoot(apkFiles: List?, context: Context): Boolean { + private fun installSplitApkFilesRoot(apkFiles: List?, context: Context) : Boolean { val filenames = arrayOf("black.apk", "dark.apk", "blue.apk", "pink.apk", "hash.json") log(INSTALLER_TAG, "installing split apk files: ${apkFiles?.map { it.name }}") - val sessionId = Shell.su("pm install-create -r").exec().out.joinToString(" ").filter { it.isDigit() }.toIntOrNull() - - if (sessionId == null) { - sendFailure("Session ID is null", context) - sendCloseDialog(context) - return false - } - + val sessionId = Shell.su("pm install-create").exec().out.joinToString(" ").filter { it.isDigit() }.toInt() apkFiles?.filter { !filenames.contains(it.name) }?.forEach { apkFile -> val apkName = apkFile.name log(INSTALLER_TAG, "installing APK: $apkName") diff --git a/app/src/main/java/com/vanced/manager/utils/StorageUtils.kt b/app/src/main/java/com/vanced/manager/utils/StorageUtils.kt new file mode 100644 index 00000000..8cd87c29 --- /dev/null +++ b/app/src/main/java/com/vanced/manager/utils/StorageUtils.kt @@ -0,0 +1,53 @@ +package com.vanced.manager.utils + +import android.Manifest +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Environment +import android.provider.Settings +import androidx.fragment.app.FragmentActivity +import com.vanced.manager.ui.MainActivity +import com.vanced.manager.ui.dialogs.DialogContainer.storageDialog + +@Suppress("DEPRECATION") +val managerPath get() = "${Environment.getExternalStorageDirectory().path}/Vanced Manager" + +inline fun performStorageAction(activity: FragmentActivity, action: () -> Unit) { + if (canAccessStorage(activity)) { + action() + } else { + storageDialog(activity) + } +} + +fun canAccessStorage(activity: FragmentActivity): Boolean = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Environment.isExternalStorageManager() + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> { + listOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ).all { + activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED + } + } + else -> true +} + +fun requestStoragePerms(activity: FragmentActivity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + activity.startActivity( + Intent( + Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION + ) + ) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + activity.requestPermissions( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ), + MainActivity.REQUEST_CODE + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c0c0c65..53355b90 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,6 +94,8 @@ Due to a bug in the original microG, installing Music v4.11+ first requires you to install v4.07.51, open it, then login and only then can you install v4.11 and higher. Do you want to proceed with the installation of v4.07.51? Please do NOT exit the app during this process! Welcome + Storage access required + In order for Vanced Manager to work, you must grant the storage permission Choose your preferred language(s) for Vanced