Added ability to restore
This commit is contained in:
parent
897b160834
commit
d703a05182
4 changed files with 120 additions and 8 deletions
|
@ -16,8 +16,9 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class BackupActivity : AbsThemeActivity() {
|
class BackupActivity : AbsThemeActivity(), BackupAdapter.BackupClickedListener {
|
||||||
|
|
||||||
private val backupViewModel by viewModels<BackupViewModel>()
|
private val backupViewModel by viewModels<BackupViewModel>()
|
||||||
private var backupAdapter: BackupAdapter? = null
|
private var backupAdapter: BackupAdapter? = null
|
||||||
|
@ -59,7 +60,7 @@ class BackupActivity : AbsThemeActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initAdapter() {
|
private fun initAdapter() {
|
||||||
backupAdapter = BackupAdapter(this@BackupActivity, ArrayList())
|
backupAdapter = BackupAdapter(this@BackupActivity, ArrayList(), this)
|
||||||
backupAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
backupAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
super.onChanged()
|
super.onChanged()
|
||||||
|
@ -82,4 +83,10 @@ class BackupActivity : AbsThemeActivity() {
|
||||||
adapter = backupAdapter
|
adapter = backupAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBackupClicked(file: File) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
backupViewModel.restoreBackup(this@BackupActivity, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,11 +1,17 @@
|
||||||
package code.name.monkey.retromusic.activities.backup
|
package code.name.monkey.retromusic.activities.backup
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import code.name.monkey.retromusic.helper.BackupHelper
|
import code.name.monkey.retromusic.helper.BackupHelper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
class BackupViewModel : ViewModel() {
|
class BackupViewModel : ViewModel() {
|
||||||
private val backupsMutableLiveData = MutableLiveData<List<File>>()
|
private val backupsMutableLiveData = MutableLiveData<List<File>>()
|
||||||
|
@ -14,10 +20,22 @@ class BackupViewModel : ViewModel() {
|
||||||
fun loadBackups() {
|
fun loadBackups() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
File(BackupHelper.backupRootPath).listFiles { _, name ->
|
File(BackupHelper.backupRootPath).listFiles { _, name ->
|
||||||
return@listFiles name.endsWith(BackupHelper.backupExt)
|
return@listFiles name.endsWith(BackupHelper.BACKUP_EXTENSION)
|
||||||
}?.toList()?.let {
|
}?.toList()?.let {
|
||||||
backupsMutableLiveData.value = it
|
backupsMutableLiveData.value = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun restoreBackup(activity: Activity, file: File) {
|
||||||
|
BackupHelper.restoreBackup(activity, file)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val intent = Intent(
|
||||||
|
activity,
|
||||||
|
activity::class.java
|
||||||
|
)
|
||||||
|
activity.startActivity(intent)
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -12,6 +12,7 @@ import java.io.File
|
||||||
class BackupAdapter(
|
class BackupAdapter(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
var dataSet: MutableList<File>,
|
var dataSet: MutableList<File>,
|
||||||
|
val backupClickedListener: BackupClickedListener
|
||||||
) : RecyclerView.Adapter<BackupAdapter.ViewHolder>() {
|
) : RecyclerView.Adapter<BackupAdapter.ViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
@ -35,6 +36,13 @@ class BackupAdapter(
|
||||||
val title: TextView = itemView.findViewById(R.id.title)
|
val title: TextView = itemView.findViewById(R.id.title)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
backupClickedListener.onBackupClicked(dataSet[bindingAdapterPosition])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BackupClickedListener {
|
||||||
|
fun onBackupClicked(file: File)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,17 +2,20 @@ package code.name.monkey.retromusic.helper
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.widget.Toast
|
||||||
import code.name.monkey.retromusic.BuildConfig
|
import code.name.monkey.retromusic.BuildConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
object BackupHelper {
|
object BackupHelper {
|
||||||
suspend fun createBackup(context: Context) {
|
suspend fun createBackup(context: Context) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val finalPath = backupRootPath + System.currentTimeMillis().toString() + backupExt
|
val finalPath =
|
||||||
|
backupRootPath + System.currentTimeMillis().toString() + APPEND_EXTENSION
|
||||||
val zipItems = mutableListOf<ZipItem>()
|
val zipItems = mutableListOf<ZipItem>()
|
||||||
zipItems.addAll(getDatabaseZipItems(context))
|
zipItems.addAll(getDatabaseZipItems(context))
|
||||||
zipItems.addAll(getSettingsZipItems(context))
|
zipItems.addAll(getSettingsZipItems(context))
|
||||||
|
@ -39,7 +42,7 @@ object BackupHelper {
|
||||||
return context.databaseList().filter {
|
return context.databaseList().filter {
|
||||||
it.endsWith(".db")
|
it.endsWith(".db")
|
||||||
}.map {
|
}.map {
|
||||||
ZipItem(context.getDatabasePath(it).absolutePath, "databases${File.separator}$it")
|
ZipItem(context.getDatabasePath(it).absolutePath, "$DATABASES_PATH${File.separator}$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ object BackupHelper {
|
||||||
"${BuildConfig.APPLICATION_ID}_preferences.xml", // App settings pref path
|
"${BuildConfig.APPLICATION_ID}_preferences.xml", // App settings pref path
|
||||||
"$THEME_PREFS_KEY_DEFAULT.xml" // appthemehelper pref path
|
"$THEME_PREFS_KEY_DEFAULT.xml" // appthemehelper pref path
|
||||||
).map {
|
).map {
|
||||||
ZipItem(sharedPrefPath + it, "preferences${File.separator}$it")
|
ZipItem(sharedPrefPath + it, "$SETTINGS_PATH${File.separator}$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,15 +60,91 @@ object BackupHelper {
|
||||||
return context.filesDir.listFiles { _, name ->
|
return context.filesDir.listFiles { _, name ->
|
||||||
name.endsWith(".jpg")
|
name.endsWith(".jpg")
|
||||||
}?.map {
|
}?.map {
|
||||||
ZipItem(it.absolutePath, "userImages${File.separator}${it.name}")
|
ZipItem(it.absolutePath, "$IMAGES_PATH${File.separator}${it.name}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun restoreBackup(context: Context, file: File) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ZipInputStream(FileInputStream(file)).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)
|
||||||
|
entry = it.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Toast.makeText(context, "Restore Completed Successfully", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreImages(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) {
|
||||||
|
val filePath =
|
||||||
|
context.filesDir.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 restorePreferences(context: Context, zipIn: ZipInputStream, zipEntry: ZipEntry) {
|
||||||
|
val file = File(
|
||||||
|
context.filesDir.parent!! + File.separator + "shared_prefs" + File.separator + zipEntry.getFileName()
|
||||||
|
)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
BufferedOutputStream(FileOutputStream(file)).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 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 ->
|
||||||
|
val bytesIn = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||||
|
var read: Int
|
||||||
|
while (zipIn.read(bytesIn).also { read = it } != -1) {
|
||||||
|
bos.write(bytesIn, 0, read)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val backupRootPath =
|
val backupRootPath =
|
||||||
Environment.getExternalStorageDirectory().toString() + "/RetroMusic/Backups/"
|
Environment.getExternalStorageDirectory().toString() + "/RetroMusic/Backups/"
|
||||||
const val backupExt = ".rmbak"
|
const val BACKUP_EXTENSION = "zip"
|
||||||
|
private const val APPEND_EXTENSION = ".$BACKUP_EXTENSION"
|
||||||
|
private const val DATABASES_PATH = "databases"
|
||||||
|
private const val SETTINGS_PATH = "prefs"
|
||||||
|
private const val IMAGES_PATH = "userImages"
|
||||||
private const val THEME_PREFS_KEY_DEFAULT = "[[kabouzeid_app-theme-helper]]"
|
private const val THEME_PREFS_KEY_DEFAULT = "[[kabouzeid_app-theme-helper]]"
|
||||||
|
|
||||||
|
private fun ZipEntry.isDatabaseEntry(): Boolean {
|
||||||
|
return name.startsWith(DATABASES_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ZipEntry.isPreferenceEntry(): Boolean {
|
||||||
|
return name.startsWith(SETTINGS_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ZipEntry.isImageEntry(): Boolean {
|
||||||
|
return name.startsWith(IMAGES_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ZipEntry.getFileName(): String {
|
||||||
|
return name.substring(name.lastIndexOf(File.separator))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ZipItem(val filePath: String, val zipPath: String)
|
data class ZipItem(val filePath: String, val zipPath: String)
|
Loading…
Reference in a new issue