diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 20108f3d1..71977a668 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -123,15 +123,38 @@ + android:excludeFromRecents="false" + android:exported="true" + android:label="@string/restore" + android:theme="@style/Theme.RetroMusic.Dialog"> + + + + + + + + + + + + + + + + @@ -273,10 +296,9 @@ + android:label="@string/app_name"> diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupFragment.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupFragment.kt index ab29cd830..170ae9387 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupFragment.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupFragment.kt @@ -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") diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupViewModel.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupViewModel.kt index 920e70c91..f482cc915 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupViewModel.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/backup/BackupViewModel.kt @@ -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) { + BackupHelper.restoreBackup(activity, inputStream, contents) withContext(Dispatchers.Main) { val intent = Intent( activity, - activity::class.java + MainActivity::class.java ) activity.startActivity(intent) exitProcess(0) diff --git a/app/src/main/java/code/name/monkey/retromusic/fragments/backup/RestoreActivity.kt b/app/src/main/java/code/name/monkey/retromusic/fragments/backup/RestoreActivity.kt index f54e697c2..feab240ea 100644 --- a/app/src/main/java/code/name/monkey/retromusic/fragments/backup/RestoreActivity.kt +++ b/app/src/main/java/code/name/monkey/retromusic/fragments/backup/RestoreActivity.kt @@ -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() + 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" } } \ No newline at end of file diff --git a/app/src/main/java/code/name/monkey/retromusic/helper/BackupHelper.kt b/app/src/main/java/code/name/monkey/retromusic/helper/BackupHelper.kt index aefe5511a..3b1e1cde2 100644 --- a/app/src/main/java/code/name/monkey/retromusic/helper/BackupHelper.kt +++ b/app/src/main/java/code/name/monkey/retromusic/helper/BackupHelper.kt @@ -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, backupFile: File) { + private suspend fun zipAll(zipItems: List, 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 { 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 { + 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 { 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 + ) { 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 } \ No newline at end of file diff --git a/app/src/main/res/drawable/rounded_drawable.xml b/app/src/main/res/drawable/rounded_drawable.xml new file mode 100644 index 000000000..6d498dbf2 --- /dev/null +++ b/app/src/main/res/drawable/rounded_drawable.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_restore.xml b/app/src/main/res/layout/activity_restore.xml index 79eda3368..fb38360b5 100644 --- a/app/src/main/res/layout/activity_restore.xml +++ b/app/src/main/res/layout/activity_restore.xml @@ -1,8 +1,83 @@ - + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ba9ea8e8..862ba952f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ Full Image Card Classic + MD3 Small Minimal Text Artist @@ -81,6 +82,7 @@ Audio focus denied. Change the sound settings and adjust the equalizer controls Auto + Backup and restore your settings, playlists @@ -113,6 +115,7 @@ Cascading Changelog Check out What\'s New + Choose what to restore Circle Circular Classic @@ -132,7 +135,9 @@ Created playlist %1$s. Members and contributors Currently listening to %1$s by %2$s. + Custom Artist Images Kinda Dark + Databases (Playlists, History, Most Played, etc.) Delete playlist %1$s?]]> @@ -156,6 +161,7 @@ Device info Allow Retro Music to modify audio settings Set ringtone + Disc Number Do you want to clear the blacklist? %1$s from the blacklist?]]> @@ -404,6 +410,7 @@ Reset Reset artist image Restore + Do you want to restore backup? Restored previous purchase. Please restart the app to make use of all features. Restored previous purchases. Restoring purchase… @@ -453,8 +460,8 @@ Sort order Ascending Album - Artist @string/album_artist + Artist Composer Date added Date modified @@ -480,6 +487,7 @@ Tiny Tiny card Title + New Backup Today Top albums Top artists @@ -495,6 +503,7 @@ Up next Update image Updating… + User Images User Name Username Version @@ -515,9 +524,4 @@ You have to select at least one category. You will be forwarded to the issue tracker website. Your account data is only used for authentication. - Do you want to restore backup? - New Backup - Backup and restore your settings, playlists - MD3 - Disc Number diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index cf607b8c6..e8e081dff 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -236,4 +236,13 @@ @color/md_deep_purple_A400 @color/md_deep_purple_500 + +