[Backup & Restore] Better Backup & Restore

Removed Playing Queue, Most Played, History, etc. from Backup. Better Backup & Restore for Playlists & Custom Artist Images.
This commit is contained in:
Prathamesh More 2021-12-31 02:25:41 +05:30
parent 076fc1d9c5
commit 1d77b3155a
40 changed files with 96 additions and 114 deletions

View file

@ -62,7 +62,7 @@ class MainActivity : AbsCastActivity(), OnSharedPreferenceChangeListener {
if (!hasPermissions()) {
findNavController(R.id.fragment_container).navigate(R.id.permissionFragment)
}
if (BuildConfig.VERSION_CODE > PreferenceUtil.lastVersion){
if (BuildConfig.VERSION_CODE > PreferenceUtil.lastVersion && !BuildConfig.DEBUG){
NavigationUtil.gotoWhatNews(this)
}
}

View file

@ -29,7 +29,8 @@ class BackupViewModel : ViewModel() {
suspend fun restoreBackup(activity: Activity, inputStream: InputStream?, contents: List<BackupContent>) {
BackupHelper.restoreBackup(activity, inputStream, contents)
if (contents.contains(BackupContent.SETTINGS)) {
if (contents.contains(BackupContent.SETTINGS) or contents.contains(BackupContent.CUSTOM_ARTIST_IMAGES)) {
// We have to restart App when Preferences i.e. Settings or Artist Images are to be restored
withContext(Dispatchers.Main) {
val intent = Intent(
activity,

View file

@ -3,9 +3,11 @@ package code.name.monkey.retromusic.fragments.backup
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import code.name.monkey.retromusic.databinding.ActivityRestoreBinding
import code.name.monkey.retromusic.helper.BackupContent
@ -27,6 +29,7 @@ class RestoreActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityRestoreBinding.inflate(layoutInflater)
setContentView(binding.root)
setWidth()
val backupUri = intent?.data
binding.backupName.setText(getFileName(backupUri))
binding.cancelButton.setOnClickListener {
@ -35,7 +38,6 @@ class RestoreActivity : AppCompatActivity() {
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)
@ -82,4 +84,9 @@ class RestoreActivity : AppCompatActivity() {
}
return "Backup"
}
private fun setWidth() {
val width = resources.displayMetrics.widthPixels * 0.8
binding.root.updateLayoutParams<ViewGroup.LayoutParams> { this.width = width.toInt() }
}
}

View file

@ -29,6 +29,7 @@ import android.widget.Toast
import androidx.core.view.doOnPreDraw
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import code.name.monkey.retromusic.Constants.USER_BANNER
import code.name.monkey.retromusic.Constants.USER_PROFILE
@ -50,7 +51,6 @@ import com.bumptech.glide.request.target.Target
import com.github.dhaval2404.imagepicker.ImagePicker
import com.github.dhaval2404.imagepicker.constant.ImageProvider
import com.google.android.material.transition.MaterialContainerTransform
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -202,15 +202,15 @@ class UserInfoFragment : Fragment() {
}
private fun saveImage(bitmap: Bitmap, fileName: String) {
CoroutineScope(Dispatchers.IO).launch {
lifecycleScope.launch(Dispatchers.IO) {
val appDir = requireContext().filesDir
val file = File(appDir, fileName)
var successful = false
runCatching {
val os = BufferedOutputStream(FileOutputStream(file))
successful = ImageUtil.resizeBitmap(bitmap, 2048)
.compress(Bitmap.CompressFormat.WEBP, 100, os)
withContext(Dispatchers.IO) { os.close() }
BufferedOutputStream(FileOutputStream(file)).use {
successful = ImageUtil.resizeBitmap(bitmap, 2048)
.compress(Bitmap.CompressFormat.WEBP, 100, it)
}
}.onFailure {
it.printStackTrace()
}

View file

@ -2,21 +2,28 @@ 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.db.PlaylistEntity
import code.name.monkey.retromusic.db.toSongEntity
import code.name.monkey.retromusic.helper.BackupContent.*
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.repository.Repository
import code.name.monkey.retromusic.repository.SongRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
object BackupHelper {
object BackupHelper : KoinComponent {
private val repository by inject<Repository>()
private val songRepository by inject<SongRepository>()
suspend fun createBackup(context: Context, name: String) {
val backupFile =
File(backupRootPath + File.separator + name + APPEND_EXTENSION)
@ -24,17 +31,18 @@ object BackupHelper {
backupFile.parentFile?.mkdirs()
}
val zipItems = mutableListOf<ZipItem>()
zipItems.addAll(getDatabaseZipItems(context))
zipItems.addAll(getPlaylistZipItems(context))
zipItems.addAll(getSettingsZipItems(context))
getUserImageZipItems(context)?.let { zipItems.addAll(it) }
zipItems.addAll(getCustomArtistZipItems(context))
zipItems.addAll(getQueueZipItems(context))
zipAll(zipItems, backupFile)
// Clean Cache Playlist Directory
File(context.filesDir, PLAYLISTS_PATH).deleteRecursively()
}
private suspend fun zipAll(zipItems: List<ZipItem>, backupFile: File) =
withContext(Dispatchers.IO) {
kotlin.runCatching {
runCatching {
ZipOutputStream(BufferedOutputStream(FileOutputStream(backupFile))).use { out ->
for (zipItem in zipItems) {
FileInputStream(zipItem.filePath).use { fi ->
@ -51,7 +59,6 @@ object BackupHelper {
Toast.makeText(App.getContext(), "Couldn't create backup", Toast.LENGTH_SHORT)
.show()
}
throw Exception(it)
}.onSuccess {
withContext(Dispatchers.Main) {
Toast.makeText(
@ -62,25 +69,30 @@ object BackupHelper {
.show()
}
}
}
private fun getDatabaseZipItems(context: Context): List<ZipItem> {
return context.databaseList().filter {
it.endsWith(".db") && it != queueDatabase
}.map {
ZipItem(context.getDatabasePath(it).absolutePath, "$DATABASES_PATH${File.separator}$it")
private suspend fun getPlaylistZipItems(context: Context): List<ZipItem> {
val playlistZipItems = mutableListOf<ZipItem>()
// Cache Playlist files in App storage
val playlistFolder = File(context.filesDir, PLAYLISTS_PATH)
if (!playlistFolder.exists()) {
playlistFolder.mkdirs()
}
}
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"
)
)
for (playlist in repository.fetchPlaylistWithSongs()) {
runCatching {
M3UWriter.writeIO(playlistFolder, playlist)
}.onSuccess { playlistFile ->
if (playlistFile.exists()) {
playlistZipItems.add(
ZipItem(
playlistFile.absolutePath,
PLAYLISTS_PATH + File.separator + playlistFile.name
)
)
}
}
}
return playlistZipItems
}
private fun getSettingsZipItems(context: Context): List<ZipItem> {
@ -124,8 +136,6 @@ object BackupHelper {
)
}
}
return zipItemList
}
@ -138,22 +148,19 @@ object BackupHelper {
ZipInputStream(inputStream).use {
var entry = it.nextEntry
while (entry != null) {
if (entry.isDatabaseEntry() && contents.contains(PLAYLISTS)) {
restoreDatabase(context, it, entry)
if (entry.isPlaylistEntry() && contents.contains(PLAYLISTS)) {
restorePlaylists(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)
} else if (entry.isCustomArtistEntry() && contents.contains(CUSTOM_ARTIST_IMAGES)) {
if (entry.isCustomArtistPrefEntry()) {
restoreCustomArtistPrefs(context, it, entry)
} else if (entry.isCustomArtistImageEntry()) {
restoreCustomArtistImages(context, it, entry)
}
}
entry = it.nextEntry
}
}
@ -183,22 +190,33 @@ object BackupHelper {
}
}
private fun restoreDatabase(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) {
val filePath =
context.filesDir.parent!! + File.separator + DATABASES_PATH + File.separator + zipEntry.getFileName()
BufferedOutputStream(FileOutputStream(filePath)).use { bos ->
zipIn.copyTo(bos)
}
}
private suspend fun restorePlaylists(
zipIn: ZipInputStream,
zipEntry: ZipEntry
) {
val playlistName = zipEntry.getFileName().substringBeforeLast(".")
val songs = mutableListOf<Song>()
private fun restoreQueueDatabase(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) {
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true) {
putInt("POSITION", 0)
// Get songs from m3u playlist files
zipIn.bufferedReader().lineSequence().forEach { line ->
if (line.startsWith(File.separator)) {
if (File(line).exists()) {
songs.addAll(songRepository.songsByFilePath(line))
}
}
}
val filePath =
context.filesDir.parent!! + File.separator + DATABASES_PATH + File.separator + zipEntry.getFileName()
BufferedOutputStream(FileOutputStream(filePath)).use { bos ->
zipIn.copyTo(bos)
val playlistEntity = repository.checkPlaylistExists(playlistName).firstOrNull()
if (playlistEntity != null) {
val songEntities = songs.map {
it.toSongEntity(playlistEntity.playListId)
}
repository.insertSongs(songEntities)
} else {
val playListId = repository.createPlaylist(PlaylistEntity(playlistName = playlistName))
val songEntities = songs.map {
it.toSongEntity(playListId)
}
repository.insertSongs(songEntities)
}
}
@ -241,16 +259,14 @@ object BackupHelper {
.toString() + "/RetroMusic/Backups/"
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 PLAYLISTS_PATH = "Playlists"
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)
private fun ZipEntry.isPlaylistEntry(): Boolean {
return name.startsWith(PLAYLISTS_PATH)
}
private fun ZipEntry.isPreferenceEntry(): Boolean {
@ -261,6 +277,10 @@ object BackupHelper {
return name.startsWith(IMAGES_PATH)
}
private fun ZipEntry.isCustomArtistEntry(): Boolean {
return name.startsWith(CUSTOM_ARTISTS_PATH)
}
private fun ZipEntry.isCustomArtistImageEntry(): Boolean {
return name.startsWith(CUSTOM_ARTISTS_PATH) && name.contains("custom_artist_images")
}
@ -269,12 +289,8 @@ 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))
return name.substring(name.lastIndexOf(File.separator) + 1)
}
}
@ -297,6 +313,5 @@ enum class BackupContent {
SETTINGS,
USER_IMAGES,
CUSTOM_ARTIST_IMAGES,
PLAYLISTS,
QUEUE
PLAYLISTS
}