mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Temporary workaround for auto backup restore on >Android11
This commit is contained in:
parent
ebcb894011
commit
1768d73c01
5 changed files with 151 additions and 6 deletions
|
@ -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);
|
||||
}
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue