Added Chooser to choose what to restore
This commit is contained in:
parent
2e16994276
commit
5a41a07b76
9 changed files with 315 additions and 61 deletions
|
@ -7,7 +7,7 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
|
@ -49,7 +49,9 @@ class BackupFragment : Fragment(R.layout.fragment_backup), BackupAdapter.BackupC
|
|||
val openFilePicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
it?.let {
|
||||
backupViewModel.restoreBackup(requireActivity(), requireContext().contentResolver.openInputStream(it))
|
||||
startActivity(Intent(context, RestoreActivity::class.java).apply {
|
||||
data = it
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,17 +105,11 @@ class BackupFragment : Fragment(R.layout.fragment_backup), BackupAdapter.BackupC
|
|||
}
|
||||
|
||||
override fun onBackupClicked(file: File) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.restore)
|
||||
.setMessage(R.string.restore_message)
|
||||
.setPositiveButton(R.string.restore) { _, _ ->
|
||||
lifecycleScope.launch {
|
||||
backupViewModel.restoreBackup(requireActivity(), file.inputStream())
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
.show()
|
||||
lifecycleScope.launch {
|
||||
startActivity(Intent(context, RestoreActivity::class.java).apply {
|
||||
data = file.toUri()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.content.Intent
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import code.name.monkey.retromusic.activities.MainActivity
|
||||
import code.name.monkey.retromusic.helper.BackupContent
|
||||
import code.name.monkey.retromusic.helper.BackupHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -25,12 +27,12 @@ class BackupViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun restoreBackup(activity: Activity, inputStream: InputStream?) {
|
||||
BackupHelper.restoreBackup(activity, inputStream)
|
||||
suspend fun restoreBackup(activity: Activity, inputStream: InputStream?, contents: List<BackupContent>) {
|
||||
BackupHelper.restoreBackup(activity, inputStream, contents)
|
||||
withContext(Dispatchers.Main) {
|
||||
val intent = Intent(
|
||||
activity,
|
||||
activity::class.java
|
||||
MainActivity::class.java
|
||||
)
|
||||
activity.startActivity(intent)
|
||||
exitProcess(0)
|
||||
|
|
|
@ -1,12 +1,89 @@
|
|||
package code.name.monkey.retromusic.fragments.backup
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import code.name.monkey.retromusic.R
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import code.name.monkey.retromusic.databinding.ActivityRestoreBinding
|
||||
import code.name.monkey.retromusic.helper.BackupContent
|
||||
import code.name.monkey.retromusic.helper.BackupContent.*
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil
|
||||
import code.name.monkey.retromusic.util.theme.ThemeManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class RestoreActivity : AppCompatActivity() {
|
||||
|
||||
lateinit var binding: ActivityRestoreBinding
|
||||
private val backupViewModel: BackupViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
updateTheme()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_restore)
|
||||
binding = ActivityRestoreBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
val backupUri = intent?.data
|
||||
binding.backupName.setText(getFileName(backupUri))
|
||||
binding.cancelButton.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
binding.restoreButton.setOnClickListener {
|
||||
val backupContents = mutableListOf<BackupContent>()
|
||||
if (binding.checkSettings.isChecked) backupContents.add(SETTINGS)
|
||||
if (binding.checkQueue.isChecked) backupContents.add(QUEUE)
|
||||
if (binding.checkDatabases.isChecked) backupContents.add(PLAYLISTS)
|
||||
if (binding.checkArtistImages.isChecked) backupContents.add(CUSTOM_ARTIST_IMAGES)
|
||||
if (binding.checkUserImages.isChecked) backupContents.add(USER_IMAGES)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
if (backupUri != null) {
|
||||
contentResolver.openInputStream(backupUri)?.use {
|
||||
backupViewModel.restoreBackup(this@RestoreActivity, it, backupContents)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTheme() {
|
||||
AppCompatDelegate.setDefaultNightMode(ThemeManager.getNightMode(this))
|
||||
|
||||
// Apply dynamic colors to activity if enabled
|
||||
if (PreferenceUtil.materialYou) {
|
||||
DynamicColors.applyIfAvailable(
|
||||
this,
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_DynamicColors_DayNight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFileName(uri: Uri?): String? {
|
||||
when (uri?.scheme) {
|
||||
"file" -> {
|
||||
return uri.lastPathSegment
|
||||
}
|
||||
"content" -> {
|
||||
val proj = arrayOf(MediaStore.Images.Media.TITLE)
|
||||
contentResolver.query(
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
|
||||
MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||
} else {
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
||||
}, proj, null, null, null
|
||||
)?.use { cursor ->
|
||||
if (cursor.count != 0) {
|
||||
val columnIndex: Int =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.TITLE)
|
||||
cursor.moveToFirst()
|
||||
return cursor.getString(columnIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "Backup"
|
||||
}
|
||||
}
|
|
@ -2,9 +2,13 @@ package code.name.monkey.retromusic.helper
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
import code.name.monkey.retromusic.App
|
||||
import code.name.monkey.retromusic.BuildConfig
|
||||
import code.name.monkey.retromusic.helper.BackupContent.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.*
|
||||
|
@ -24,10 +28,11 @@ object BackupHelper {
|
|||
zipItems.addAll(getSettingsZipItems(context))
|
||||
getUserImageZipItems(context)?.let { zipItems.addAll(it) }
|
||||
zipItems.addAll(getCustomArtistZipItems(context))
|
||||
zipItems.addAll(getQueueZipItems(context))
|
||||
zipAll(zipItems, backupFile)
|
||||
}
|
||||
|
||||
private suspend fun zipAll(zipItems: List<ZipItem>, backupFile: File) {
|
||||
private suspend fun zipAll(zipItems: List<ZipItem>, backupFile: File) =
|
||||
withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(backupFile))).use { out ->
|
||||
|
@ -42,27 +47,42 @@ object BackupHelper {
|
|||
}
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(App.getContext(), "Couldn't create backup", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
throw Exception(it)
|
||||
}.onSuccess {
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(
|
||||
App.getContext(),
|
||||
"Backup created successfully",
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(App.getContext(), "Backup created successfully", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDatabaseZipItems(context: Context): List<ZipItem> {
|
||||
return context.databaseList().filter {
|
||||
it.endsWith(".db")
|
||||
it.endsWith(".db") && it != queueDatabase
|
||||
}.map {
|
||||
ZipItem(context.getDatabasePath(it).absolutePath, "$DATABASES_PATH${File.separator}$it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getQueueZipItems(context: Context): List<ZipItem> {
|
||||
Log.d("RetroMusic", context.getDatabasePath(queueDatabase).absolutePath)
|
||||
return listOf(
|
||||
ZipItem(
|
||||
context.getDatabasePath(queueDatabase).absolutePath,
|
||||
"$QUEUE_PATH${File.separator}$queueDatabase"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSettingsZipItems(context: Context): List<ZipItem> {
|
||||
val sharedPrefPath = context.filesDir.parentFile?.absolutePath + "/shared_prefs/"
|
||||
return listOf(
|
||||
|
@ -94,33 +114,47 @@ object BackupHelper {
|
|||
)
|
||||
}?.toList() ?: listOf()
|
||||
)
|
||||
zipItemList.add(
|
||||
ZipItem(
|
||||
sharedPrefPath + File.separator + "custom_artist_image.xml",
|
||||
"$CUSTOM_ARTISTS_PATH${File.separator}prefs${File.separator}custom_artist_image.xml"
|
||||
)
|
||||
)
|
||||
File(sharedPrefPath + File.separator + "custom_artist_image.xml").let {
|
||||
if (it.exists()) {
|
||||
zipItemList.add(
|
||||
ZipItem(
|
||||
it.absolutePath,
|
||||
"$CUSTOM_ARTISTS_PATH${File.separator}prefs${File.separator}custom_artist_image.xml"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return zipItemList
|
||||
}
|
||||
|
||||
suspend fun restoreBackup(context: Context, inputStream: InputStream?) {
|
||||
suspend fun restoreBackup(
|
||||
context: Context,
|
||||
inputStream: InputStream?,
|
||||
contents: List<BackupContent>
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
ZipInputStream(inputStream).use {
|
||||
var entry = it.nextEntry
|
||||
while (entry != null) {
|
||||
if (entry.isDatabaseEntry()) restoreDatabase(context, it, entry)
|
||||
if (entry.isPreferenceEntry()) restorePreferences(context, it, entry)
|
||||
if (entry.isImageEntry()) restoreImages(context, it, entry)
|
||||
if (entry.isCustomArtistImageEntry()) restoreCustomArtistImages(
|
||||
context,
|
||||
it,
|
||||
entry
|
||||
)
|
||||
if (entry.isCustomArtistPrefEntry()) restoreCustomArtistPrefs(
|
||||
context,
|
||||
it,
|
||||
entry
|
||||
)
|
||||
if (entry.isDatabaseEntry() && contents.contains(PLAYLISTS)) {
|
||||
restoreDatabase(context, it, entry)
|
||||
} else if (entry.isPreferenceEntry() && contents.contains(SETTINGS)) {
|
||||
restorePreferences(context, it, entry)
|
||||
} else if (entry.isImageEntry() && contents.contains(USER_IMAGES)) {
|
||||
restoreImages(context, it, entry)
|
||||
|
||||
} else if (entry.isCustomArtistImageEntry() && contents.contains(
|
||||
CUSTOM_ARTIST_IMAGES
|
||||
)
|
||||
) {
|
||||
restoreCustomArtistImages(context, it, entry)
|
||||
restoreCustomArtistPrefs(context, it, entry)
|
||||
} else if (entry.isQueueEntry() && contents.contains(QUEUE)) {
|
||||
restoreQueueDatabase(context, it, entry)
|
||||
}
|
||||
|
||||
entry = it.nextEntry
|
||||
}
|
||||
}
|
||||
|
@ -170,6 +204,21 @@ object BackupHelper {
|
|||
}
|
||||
}
|
||||
|
||||
private fun restoreQueueDatabase(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) {
|
||||
putInt("POSITION", 0)
|
||||
}
|
||||
val filePath =
|
||||
context.filesDir.parent!! + File.separator + DATABASES_PATH + File.separator + zipEntry.getFileName()
|
||||
BufferedOutputStream(FileOutputStream(filePath)).use { bos ->
|
||||
val bytesIn = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var read: Int
|
||||
while (zipIn.read(bytesIn).also { read = it } != -1) {
|
||||
bos.write(bytesIn, 0, read)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreCustomArtistImages(
|
||||
context: Context,
|
||||
zipIn: ZipInputStream,
|
||||
|
@ -218,10 +267,12 @@ object BackupHelper {
|
|||
const val BACKUP_EXTENSION = "rmbak"
|
||||
const val APPEND_EXTENSION = ".$BACKUP_EXTENSION"
|
||||
private const val DATABASES_PATH = "databases"
|
||||
private const val QUEUE_PATH = "queue"
|
||||
private const val SETTINGS_PATH = "prefs"
|
||||
private const val IMAGES_PATH = "userImages"
|
||||
private const val CUSTOM_ARTISTS_PATH = "artistImages"
|
||||
private const val THEME_PREFS_KEY_DEFAULT = "[[kabouzeid_app-theme-helper]]"
|
||||
private const val queueDatabase = "music_playback_state.db"
|
||||
|
||||
private fun ZipEntry.isDatabaseEntry(): Boolean {
|
||||
return name.startsWith(DATABASES_PATH)
|
||||
|
@ -243,6 +294,10 @@ object BackupHelper {
|
|||
return name.startsWith(CUSTOM_ARTISTS_PATH) && name.contains("prefs")
|
||||
}
|
||||
|
||||
private fun ZipEntry.isQueueEntry(): Boolean {
|
||||
return name.startsWith(QUEUE_PATH)
|
||||
}
|
||||
|
||||
private fun ZipEntry.getFileName(): String {
|
||||
return name.substring(name.lastIndexOf(File.separator))
|
||||
}
|
||||
|
@ -261,4 +316,12 @@ fun CharSequence.sanitize(): String {
|
|||
.replace("|", "_")
|
||||
.replace("\\", "_")
|
||||
.replace("&", "_")
|
||||
}
|
||||
|
||||
enum class BackupContent {
|
||||
SETTINGS,
|
||||
USER_IMAGES,
|
||||
CUSTOM_ARTIST_IMAGES,
|
||||
PLAYLISTS,
|
||||
QUEUE
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue