Temporary workaround for auto backup restore on >Android11

This commit is contained in:
Kelvin 2023-10-09 17:33:29 +02:00
parent ebcb894011
commit 1768d73c01
5 changed files with 151 additions and 6 deletions

View file

@ -0,0 +1,9 @@
package com.futo.platformplayer.activities
import android.content.Intent
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
interface IWithResultLauncher {
fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit);
}

View file

@ -10,6 +10,9 @@ import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.widget.FrameLayout
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.WindowCompat
@ -24,6 +27,7 @@ import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event3
import com.futo.platformplayer.fragment.mainactivity.bottombar.MenuBottomBarFragment
import com.futo.platformplayer.fragment.mainactivity.main.*
import com.futo.platformplayer.fragment.mainactivity.topbar.AddTopBarFragment
@ -48,7 +52,7 @@ import java.io.StringWriter
import java.lang.reflect.InvocationTargetException
import java.util.*
class MainActivity : AppCompatActivity {
class MainActivity : AppCompatActivity, IWithResultLauncher {
//TODO: Move to dimensions
private val HEIGHT_MENU_DP = 48f;
@ -364,6 +368,7 @@ class MainActivity : AppCompatActivity {
//startActivity(Intent(this, TestActivity::class.java));
}
/*
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
@ -892,6 +897,28 @@ class MainActivity : AppCompatActivity {
_fragContainerMain.setPadding(0,0,0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingBottom, resources.displayMetrics).toInt());
}
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
private var requestCode: Int? = -1;
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult ->
val handler = synchronized(resultLauncherMap) {
resultLauncherMap.remove(requestCode);
}
if(handler != null)
handler(result);
};
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
synchronized(resultLauncherMap) {
resultLauncherMap[code] = handler;
}
requestCode = code;
resultLauncher.launch(intent);
}
companion object {
private val TAG = "MainActivity"

View file

@ -6,13 +6,16 @@ import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.futo.platformplayer.*
import com.futo.platformplayer.views.fields.FieldForm
import com.futo.platformplayer.views.fields.ReadOnlyTextField
import com.google.android.material.button.MaterialButton
class SettingsActivity : AppCompatActivity() {
class SettingsActivity : AppCompatActivity(), IWithResultLauncher {
private lateinit var _form: FieldForm;
private lateinit var _buttonBack: ImageButton;
@ -78,6 +81,28 @@ class SettingsActivity : AppCompatActivity() {
overridePendingTransition(R.anim.slide_lighten, R.anim.slide_out_up)
}
private var resultLauncherMap = mutableMapOf<Int, (ActivityResult)->Unit>();
private var requestCode: Int? = -1;
private val resultLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
result: ActivityResult ->
val handler = synchronized(resultLauncherMap) {
resultLauncherMap.remove(requestCode);
}
if(handler != null)
handler(result);
};
override fun launchForResult(intent: Intent, code: Int, handler: (ActivityResult)->Unit) {
synchronized(resultLauncherMap) {
resultLauncherMap[code] = handler;
}
requestCode = code;
resultLauncher.launch(intent);
}
companion object {
//TODO: Temporary for solving Settings issues
@SuppressLint("StaticFieldLeak")

View file

@ -2,7 +2,9 @@ package com.futo.platformplayer.states
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.media.AudioManager
@ -10,12 +12,20 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import android.util.DisplayMetrics
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.work.*
import com.futo.platformplayer.*
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.background.BackgroundWorker
import com.futo.platformplayer.casting.StateCasting
@ -43,6 +53,9 @@ class StateApp {
val isMainActive: Boolean get() = contextOrNull != null && contextOrNull is MainActivity; //if context is MainActivity, it means its active
private val externalRootDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Grayjay");
fun getExternalRootDirectory(): File? {
if(!externalRootDirectory.exists()) {
val result = externalRootDirectory.mkdirs();
@ -158,6 +171,32 @@ class StateApp {
return state;
}
fun requestDirectoryAccess(activity: IWithResultLauncher, name: String, path: Uri?, handle: (Uri?)->Unit)
{
if(activity is Context)
{
UIDialogs.showDialog(activity, R.drawable.ic_security, "Missing Access", "Please grant access to ${name}", null, 0,
UIDialogs.Action("Cancel", {}),
UIDialogs.Action("Ok", {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if(path != null)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, path);
intent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
.and(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.and(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
.and(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
activity.launchForResult(intent, 99) {
if(it.resultCode == Activity.RESULT_OK) {
handle(it.data?.data);
}
else
UIDialogs.showDialogOk(context, R.drawable.ic_security_pred, "No access granted");
};
}, UIDialogs.ActionStyle.PRIMARY));
}
}
//Lifecycle
fun setGlobalContext(context: Context, coroutineScope: CoroutineScope? = null) {
_context = context;

View file

@ -1,11 +1,20 @@
package com.futo.platformplayer.states
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract.EXTRA_INITIAL_URI
import androidx.activity.ComponentActivity
import androidx.core.app.ShareCompat
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import com.futo.platformplayer.R
import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.activities.IWithResultLauncher
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.activities.SettingsActivity
import com.futo.platformplayer.encryption.EncryptionProvider
import com.futo.platformplayer.getNowDiffHours
@ -22,6 +31,9 @@ import kotlinx.serialization.json.Json
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.InputStream
import java.time.OffsetDateTime
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@ -34,6 +46,14 @@ class StateBackup {
private val _autoBackupLock = Object();
private fun getAutomaticBackupDocumentFiles(context: Context, root: Uri, create: Boolean = false): Pair<DocumentFile?, DocumentFile?> {
val dir = DocumentFile.fromTreeUri(context, root);
if(dir == null)
throw IllegalStateException("Can't access external document files");
val mainBackupFile = dir.findFile("GrayjayBackup.ezip") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip") else null;
val secondaryBackupFile = dir.findFile("GrayjayBackup.ezip.old") ?: if(create) dir.createFile("grayjay/ezip", "GrayjayBackup.ezip.old") else null;
return Pair(mainBackupFile, secondaryBackupFile);
}
private fun getAutomaticBackupFiles(): Pair<File, File> {
val dir = StateApp.instance.getExternalRootDirectory();
if(dir == null)
@ -97,7 +117,13 @@ class StateBackup {
}
}
}
fun restoreAutomaticBackup(context: Context, scope: CoroutineScope, password: String, ifExists: Boolean = false) {
//TODO: This contains a temporary workaround to make it semi-compatible with > Android 11. By mixing "File" and "DocumentFile" usage.
//TODO: For now this is used to at least recover and gain temporary access to docs after losing access (due to permission lost after reinstall)
//TODO: Should be replaced with a more re-usable system that leverages OPEN_DOCUMENT_TREE once, and somehow persist this content after uninstall
//TODO: DocumentFiles are not compatible with normal files and require its own system.
//TODO: Investigate persistence of DOCUMENT_TREE files after uninstall...
fun restoreAutomaticBackup(context: Context, scope: CoroutineScope, password: String, ifExists: Boolean = false, withStream: InputStream? = null) {
if(ifExists && !hasAutomaticBackup()) {
Logger.i(TAG, "No AutoBackup exists, not restoring");
return;
@ -110,14 +136,33 @@ class StateBackup {
val backupFiles = getAutomaticBackupFiles();
try {
if (!backupFiles.first.exists())
if (!backupFiles.first.exists() && withStream == null)
throw IllegalStateException("Backup file does not exist");
val backupBytesEncrypted = backupFiles.first.readBytes();
val backupBytesEncrypted = if(withStream != null) withStream.readBytes() else backupFiles.first.readBytes();
val backupBytes = EncryptionProvider.instance.decrypt(backupBytesEncrypted, getAutomaticBackupPassword(password));
importZipBytes(context, scope, backupBytes);
Logger.i(TAG, "Finished AutoBackup restore");
} catch (ex: Throwable) {
}
catch (exSec: FileNotFoundException) {
Logger.e(TAG, "Failed to access backup file", exSec);
val activity = if(SettingsActivity.getActivity() != null)
SettingsActivity.getActivity();
else if(StateApp.instance.isMainActive)
StateApp.instance.contextOrNull;
else null;
if(activity != null) {
if(activity is IWithResultLauncher)
StateApp.instance.requestDirectoryAccess(activity, "Grayjay Backup Directory", backupFiles.first.parent?.toUri()) {
if(it != null) {
val customFiles = StateBackup.getAutomaticBackupDocumentFiles(activity, it);
if(customFiles.first != null && customFiles.first!!.isFile && customFiles.first!!.exists() && customFiles.first!!.canRead())
restoreAutomaticBackup(context, scope, password, ifExists, activity.contentResolver.openInputStream(customFiles.first!!.uri));
}
};
}
}
catch (ex: Throwable) {
Logger.e(TAG, "Failed main AutoBackup restore", ex)
if (!backupFiles.second.exists())
throw ex;