mirror of
https://github.com/YTVanced/VancedManager
synced 2025-01-03 14:10:59 +00:00
require external storage
This commit is contained in:
parent
1c3ab835a6
commit
e73b56ed4d
12 changed files with 188 additions and 89 deletions
|
@ -8,12 +8,18 @@
|
|||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<!-- is required for some Android 5.x devices -->
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage"/>
|
||||
|
||||
<queries>
|
||||
<package android:name="com.vanced.android.youtube" />
|
||||
<package android:name="com.google.android.youtube" />
|
||||
|
@ -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">
|
||||
<!-- Only because MANAGE_EXTERNAL_STORAGE is not available in android Q -->
|
||||
|
||||
<activity
|
||||
android:name=".ui.core.SplashScreenActivity"
|
||||
|
@ -51,7 +59,7 @@
|
|||
|
||||
<activity
|
||||
android:name=".ui.WelcomeActivity"
|
||||
android:theme="@style/DarkTheme"/>
|
||||
android:theme="@style/DarkTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.MainActivity"
|
||||
|
@ -62,12 +70,14 @@
|
|||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="vancedapp.com"
|
||||
android:pathPrefix="/downloads"/>
|
||||
android:pathPrefix="/downloads"
|
||||
android:scheme="https" />
|
||||
|
||||
</intent-filter>
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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<FragmentLogBinding>() {
|
|||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ class HomeViewModel(private val activity: FragmentActivity): ViewModel() {
|
|||
val microgModel = MutableLiveData<DataModel>()
|
||||
val musicModel = MutableLiveData<DataModel>()
|
||||
val musicRootModel = MutableLiveData<RootDataModel>()
|
||||
val managerModel = MutableLiveData<DataModel>()
|
||||
private val managerModel = MutableLiveData<DataModel>()
|
||||
|
||||
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)
|
||||
|
|
|
@ -49,13 +49,13 @@ object DownloadHelper : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
|
||||
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<ResponseBody>, 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"
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import java.util.*
|
|||
|
||||
val RadioGroup.checkedButtonTag: String? get() = findViewById<MaterialRadioButton>(checkedRadioButtonId)?.tag?.toString()
|
||||
|
||||
val String.managerFilepath get() = "$managerPath/$this"
|
||||
|
||||
fun DialogFragment.show(activity: FragmentActivity) {
|
||||
try {
|
||||
show(activity.supportFragmentManager, "")
|
||||
|
|
|
@ -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<String>()
|
||||
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<File>?, context: Context): Boolean {
|
||||
private fun installSplitApkFilesRoot(apkFiles: List<File>?, 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")
|
||||
|
|
53
app/src/main/java/com/vanced/manager/utils/StorageUtils.kt
Normal file
53
app/src/main/java/com/vanced/manager/utils/StorageUtils.kt
Normal file
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -94,6 +94,8 @@
|
|||
<string name="microg_bug_summary_music">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?</string>
|
||||
<string name="please_be_patient">Please do NOT exit the app during this process!</string>
|
||||
<string name="welcome">Welcome</string>
|
||||
<string name="storage_access_required">Storage access required</string>
|
||||
<string name="storage_access_required_summary">In order for Vanced Manager to work, you must grant the storage permission</string>
|
||||
|
||||
<!-- Install Page -->
|
||||
<string name="choose_preferred_language">Choose your preferred language(s) for Vanced</string>
|
||||
|
|
Loading…
Reference in a new issue