Added Chooser to choose what to restore

This commit is contained in:
Prathamesh More 2021-12-19 15:52:54 +05:30
parent 2e16994276
commit 5a41a07b76
9 changed files with 315 additions and 61 deletions

View file

@ -123,15 +123,38 @@
<activity android:name=".activities.LockScreenActivity" />
<activity
android:name=".fragments.backup.RestoreActivity"
android:exported="true">
android:excludeFromRecents="false"
android:exported="true"
android:label="@string/restore"
android:theme="@style/Theme.RetroMusic.Dialog">
<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:mimeType="application/octet-stream" />
<data android:mimeType="application/x-zip-compressed" />
<data android:mimeType="application/zip" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<!--
Work around Android's ugly primitive PatternMatcher
implementation that can't cope with finding a . early in
the path unless it's explicitly matched.
-->
<data android:host="*" />
<data android:pathPattern=".*\\.rmbak" />
<data android:pathPattern=".*\\..*\\.rmbak" />
<data android:pathPattern=".*\\..*\\..*\\.rmbak" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.rmbak" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.rmbak" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\.rmbak" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\.rmbak" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.rmbak" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\..*\\.rmbak" />
</intent-filter>
</activity>
@ -273,10 +296,9 @@
<service
android:name=".service.MusicService"
android:enabled="true"
android:exported="true"
android:exported="false"
android:foregroundServiceType="mediaPlayback"
android:label="@string/app_name"
tools:ignore="ExportedService">
android:label="@string/app_name">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>

View file

@ -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")

View file

@ -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)

View file

@ -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"
}
}

View file

@ -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))
}
@ -262,3 +317,11 @@ fun CharSequence.sanitize(): String {
.replace("\\", "_")
.replace("&", "_")
}
enum class BackupContent {
SETTINGS,
USER_IMAGES,
CUSTOM_ARTIST_IMAGES,
PLAYLISTS,
QUEUE
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?colorSurface"/>
<corners android:radius="28dp" />
</shape>

View file

@ -1,8 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.backup.RestoreActivity">
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/backupNameContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
android:hint="@string/label_file_name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/backupName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/materialTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/choose_restore_title" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/check_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:minHeight="48dp"
android:text="@string/action_settings" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/check_databases"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="@string/databases_description" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/check_queue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="@string/now_playing_queue" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/check_user_images"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="@string/user_images_description" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/check_artist_images"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:text="@string/custom_artist_images" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel_button"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/action_cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/restore_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/restore" />
</LinearLayout>
</LinearLayout>

View file

@ -73,6 +73,7 @@
<string name="app_widget_big_name">Full Image</string>
<string name="app_widget_card_name">Card</string>
<string name="app_widget_classic_name">Classic</string>
<string name="app_widget_md3_name">MD3</string>
<string name="app_widget_small_name">Small</string>
<string name="app_widget_text_name">Minimal Text</string>
<string name="artist">Artist</string>
@ -81,6 +82,7 @@
<string name="audio_focus_denied">Audio focus denied.</string>
<string name="audio_settings_summary">Change the sound settings and adjust the equalizer controls</string>
<string name="auto">Auto</string>
<string name="backup_restore_settings_summary">Backup and restore your settings, playlists</string>
<string name="backup_restore_title">
<![CDATA[Backup & Restore]]>
</string>
@ -113,6 +115,7 @@
<string name="cascading">Cascading</string>
<string name="changelog">Changelog</string>
<string name="changelog_summary">Check out What\'s New</string>
<string name="choose_restore_title">Choose what to restore</string>
<string name="circle">Circle</string>
<string name="circular">Circular</string>
<string name="classic">Classic</string>
@ -132,7 +135,9 @@
<string name="created_playlist_x">Created playlist %1$s.</string>
<string name="credit_title">Members and contributors </string>
<string name="currently_listening_to_x_by_x">Currently listening to %1$s by %2$s.</string>
<string name="custom_artist_images">Custom Artist Images</string>
<string name="dark_theme_name">Kinda Dark</string>
<string name="databases_description">Databases (Playlists, History, Most Played, etc.)</string>
<string name="delete_playlist_title">Delete playlist</string>
<string name="delete_playlist_x">
<![CDATA[Delete the playlist <b>%1$s</b>?]]>
@ -156,6 +161,7 @@
<string name="device_info">Device info</string>
<string name="dialog_message_set_ringtone">Allow Retro Music to modify audio settings</string>
<string name="dialog_title_set_ringtone">Set ringtone</string>
<string name="disc_hint">Disc Number</string>
<string name="do_you_want_to_clear_the_blacklist">Do you want to clear the blacklist?</string>
<string name="do_you_want_to_remove_from_the_blacklist">
<![CDATA[Do you want to remove <b>%1$s</b> from the blacklist?]]>
@ -404,6 +410,7 @@
<string name="reset_action">Reset</string>
<string name="reset_artist_image">Reset artist image</string>
<string name="restore">Restore</string>
<string name="restore_message">Do you want to restore backup?</string>
<string name="restored_previous_purchase_please_restart">Restored previous purchase. Please restart the app to make use of all features.</string>
<string name="restored_previous_purchases">Restored previous purchases.</string>
<string name="restoring_purchase">Restoring purchase…</string>
@ -453,8 +460,8 @@
<string name="sort_order">Sort order</string>
<string name="sort_order_a_z">Ascending</string>
<string name="sort_order_album">Album</string>
<string name="sort_order_artist">Artist</string>
<string name="sort_order_album_artist">@string/album_artist</string>
<string name="sort_order_artist">Artist</string>
<string name="sort_order_composer">Composer</string>
<string name="sort_order_date">Date added</string>
<string name="sort_order_date_modified">Date modified</string>
@ -480,6 +487,7 @@
<string name="tiny">Tiny</string>
<string name="tiny_card_style">Tiny card</string>
<string name="title">Title</string>
<string name="title_new_backup">New Backup</string>
<string name="today">Today</string>
<string name="top_albums">Top albums</string>
<string name="top_artists">Top artists</string>
@ -495,6 +503,7 @@
<string name="up_next">Up next</string>
<string name="update_image">Update image</string>
<string name="updating">Updating…</string>
<string name="user_images_description">User Images</string>
<string name="user_name">User Name</string>
<string name="username">Username</string>
<string name="version">Version</string>
@ -515,9 +524,4 @@
<string name="you_have_to_select_at_least_one_category">You have to select at least one category.</string>
<string name="you_will_be_forwarded_to_the_issue_tracker_website">You will be forwarded to the issue tracker website.</string>
<string name="your_account_data_is_only_used_for_authentication">Your account data is only used for authentication.</string>
<string name="restore_message">Do you want to restore backup?</string>
<string name="title_new_backup">New Backup</string>
<string name="backup_restore_settings_summary">Backup and restore your settings, playlists</string>
<string name="app_widget_md3_name">MD3</string>
<string name="disc_hint">Disc Number</string>
</resources>

View file

@ -236,4 +236,13 @@
<item name="colorAccent">@color/md_deep_purple_A400</item>
<item name="colorPrimary">@color/md_deep_purple_500</item>
</style>
<style name="Theme.RetroMusic.Dialog" parent="Theme.Material3.DayNight.Dialog">
<item name="windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@drawable/rounded_drawable</item>
<item name="android:windowFrame">@null</item>
<item name="background">@color/transparent</item>
</style>
</resources>