Fixed crash and added more options to backup
This commit is contained in:
parent
75410bf77b
commit
db3a7d4097
7 changed files with 171 additions and 26 deletions
|
@ -132,9 +132,6 @@
|
||||||
<data android:pathPattern=".*\\.rmbak" />
|
<data android:pathPattern=".*\\.rmbak" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
|
||||||
android:name="code.name.monkey.retromusic.fragments.backup.BackupFragment"
|
|
||||||
android:exported="true" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".appshortcuts.AppShortcutLauncherActivity"
|
android:name=".appshortcuts.AppShortcutLauncherActivity"
|
||||||
|
@ -169,7 +166,6 @@
|
||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}"
|
android:authorities="${applicationId}"
|
||||||
|
|
|
@ -1,23 +1,27 @@
|
||||||
package code.name.monkey.retromusic.adapter.backup
|
package code.name.monkey.retromusic.adapter.backup
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import code.name.monkey.retromusic.R
|
import code.name.monkey.retromusic.R
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
class BackupAdapter(
|
class BackupAdapter(
|
||||||
val context: Context,
|
val activity: FragmentActivity,
|
||||||
var dataSet: MutableList<File>,
|
var dataSet: MutableList<File>,
|
||||||
val backupClickedListener: BackupClickedListener
|
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 {
|
||||||
return ViewHolder(
|
return ViewHolder(
|
||||||
LayoutInflater.from(context).inflate(R.layout.item_list_card, parent, false)
|
LayoutInflater.from(activity).inflate(R.layout.item_list_card, parent, false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,8 +38,20 @@ class BackupAdapter(
|
||||||
|
|
||||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
val title: TextView = itemView.findViewById(R.id.title)
|
val title: TextView = itemView.findViewById(R.id.title)
|
||||||
|
val menu: AppCompatImageView = itemView.findViewById(R.id.menu)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
menu.setOnClickListener { view ->
|
||||||
|
val popupMenu = PopupMenu(activity, view)
|
||||||
|
popupMenu.inflate(R.menu.menu_backup)
|
||||||
|
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||||
|
return@setOnMenuItemClickListener backupClickedListener.onBackupMenuClicked(
|
||||||
|
dataSet[bindingAdapterPosition],
|
||||||
|
menuItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
popupMenu.show()
|
||||||
|
}
|
||||||
itemView.setOnClickListener {
|
itemView.setOnClickListener {
|
||||||
backupClickedListener.onBackupClicked(dataSet[bindingAdapterPosition])
|
backupClickedListener.onBackupClicked(dataSet[bindingAdapterPosition])
|
||||||
}
|
}
|
||||||
|
@ -44,5 +60,7 @@ class BackupAdapter(
|
||||||
|
|
||||||
interface BackupClickedListener {
|
interface BackupClickedListener {
|
||||||
fun onBackupClicked(file: File)
|
fun onBackupClicked(file: File)
|
||||||
|
|
||||||
|
fun onBackupMenuClicked(file: File, menuItem: MenuItem): Boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,11 @@
|
||||||
package code.name.monkey.retromusic.fragments.backup
|
package code.name.monkey.retromusic.fragments.backup
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
@ -12,6 +16,9 @@ import code.name.monkey.retromusic.R
|
||||||
import code.name.monkey.retromusic.adapter.backup.BackupAdapter
|
import code.name.monkey.retromusic.adapter.backup.BackupAdapter
|
||||||
import code.name.monkey.retromusic.databinding.FragmentBackupBinding
|
import code.name.monkey.retromusic.databinding.FragmentBackupBinding
|
||||||
import code.name.monkey.retromusic.helper.BackupHelper
|
import code.name.monkey.retromusic.helper.BackupHelper
|
||||||
|
import code.name.monkey.retromusic.util.BackupUtil
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.afollestad.materialdialogs.input.input
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -40,15 +47,25 @@ class BackupFragment : Fragment(R.layout.fragment_backup), BackupAdapter.BackupC
|
||||||
|
|
||||||
private fun setupButtons() {
|
private fun setupButtons() {
|
||||||
binding.createBackup.setOnClickListener {
|
binding.createBackup.setOnClickListener {
|
||||||
lifecycleScope.launch {
|
MaterialDialog(requireContext()).show {
|
||||||
BackupHelper.createBackup(requireContext())
|
title(res = R.string.action_rename)
|
||||||
backupViewModel.loadBackups()
|
input(prefill = System.currentTimeMillis().toString()) { _, text ->
|
||||||
|
// Text submitted with the action button
|
||||||
|
lifecycleScope.launch {
|
||||||
|
BackupHelper.createBackup(requireContext(), text.toString())
|
||||||
|
backupViewModel.loadBackups()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positiveButton(R.string.action_rename)
|
||||||
|
negativeButton(R.string.action_cancel)
|
||||||
|
setTitle(R.string.action_rename)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initAdapter() {
|
private fun initAdapter() {
|
||||||
backupAdapter = BackupAdapter(requireContext(), ArrayList(), this)
|
backupAdapter = BackupAdapter(requireActivity(), ArrayList(), this)
|
||||||
backupAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
backupAdapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||||
override fun onChanged() {
|
override fun onChanged() {
|
||||||
super.onChanged()
|
super.onChanged()
|
||||||
|
@ -72,8 +89,64 @@ class BackupFragment : Fragment(R.layout.fragment_backup), BackupAdapter.BackupC
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackupClicked(file: File) {
|
override fun onBackupClicked(file: File) {
|
||||||
lifecycleScope.launch {
|
AlertDialog.Builder(requireContext())
|
||||||
backupViewModel.restoreBackup(requireActivity(), file)
|
.setTitle(R.string.restore)
|
||||||
|
.setMessage(R.string.restore_message)
|
||||||
|
.setPositiveButton(R.string.restore) { _, _ ->
|
||||||
|
lifecycleScope.launch {
|
||||||
|
backupViewModel.restoreBackup(requireActivity(), file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackupMenuClicked(file: File, menuItem: MenuItem): Boolean {
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
R.id.action_delete -> {
|
||||||
|
try {
|
||||||
|
file.delete()
|
||||||
|
} catch (exception: SecurityException) {
|
||||||
|
Toast.makeText(
|
||||||
|
activity,
|
||||||
|
"Could not delete backup",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
backupViewModel.loadBackups()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_share -> {
|
||||||
|
activity?.startActivity(
|
||||||
|
Intent.createChooser(BackupUtil.createShareFileIntent(file, requireContext()), null))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_rename -> {
|
||||||
|
MaterialDialog(requireContext()).show {
|
||||||
|
title(res = R.string.action_rename)
|
||||||
|
input(prefill = file.nameWithoutExtension) { _, text ->
|
||||||
|
// Text submitted with the action button
|
||||||
|
val renamedFile =
|
||||||
|
File(file.parent + File.separator + text + BackupHelper.APPEND_EXTENSION)
|
||||||
|
if (!renamedFile.exists()) {
|
||||||
|
file.renameTo(renamedFile)
|
||||||
|
backupViewModel.loadBackups()
|
||||||
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
"File already exists",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positiveButton(R.string.action_rename)
|
||||||
|
negativeButton(R.string.action_cancel)
|
||||||
|
setTitle(R.string.action_rename)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ 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 android.widget.Toast
|
||||||
|
import code.name.monkey.retromusic.App
|
||||||
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
|
||||||
|
@ -12,29 +13,36 @@ 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, name: String) {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val finalPath =
|
val backupFile =
|
||||||
backupRootPath + System.currentTimeMillis().toString() + APPEND_EXTENSION
|
File(backupRootPath + name + APPEND_EXTENSION)
|
||||||
|
if (backupFile.parentFile?.exists() != true) {
|
||||||
|
backupFile.parentFile?.mkdirs()
|
||||||
|
}
|
||||||
val zipItems = mutableListOf<ZipItem>()
|
val zipItems = mutableListOf<ZipItem>()
|
||||||
zipItems.addAll(getDatabaseZipItems(context))
|
zipItems.addAll(getDatabaseZipItems(context))
|
||||||
zipItems.addAll(getSettingsZipItems(context))
|
zipItems.addAll(getSettingsZipItems(context))
|
||||||
getUserImageZipItems(context)?.let { zipItems.addAll(it) }
|
getUserImageZipItems(context)?.let { zipItems.addAll(it) }
|
||||||
zipAll(zipItems, finalPath)
|
zipAll(zipItems, backupFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun zipAll(zipItems: List<ZipItem>, finalPath: String) {
|
private fun zipAll(zipItems: List<ZipItem>, backupFile: File) {
|
||||||
ZipOutputStream(BufferedOutputStream(FileOutputStream(finalPath))).use { out ->
|
try {
|
||||||
for (zipItem in zipItems) {
|
ZipOutputStream(BufferedOutputStream(FileOutputStream(backupFile))).use { out ->
|
||||||
FileInputStream(zipItem.filePath).use { fi ->
|
for (zipItem in zipItems) {
|
||||||
BufferedInputStream(fi).use { origin ->
|
FileInputStream(zipItem.filePath).use { fi ->
|
||||||
val entry = ZipEntry(zipItem.zipPath)
|
BufferedInputStream(fi).use { origin ->
|
||||||
out.putNextEntry(entry)
|
val entry = ZipEntry(zipItem.zipPath)
|
||||||
origin.copyTo(out, 1024)
|
out.putNextEntry(entry)
|
||||||
|
origin.copyTo(out, 1024)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (exception: FileNotFoundException) {
|
||||||
|
Toast.makeText(App.getContext(), "Couldn't create backup", Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +132,7 @@ object BackupHelper {
|
||||||
val backupRootPath =
|
val backupRootPath =
|
||||||
Environment.getExternalStorageDirectory().toString() + "/RetroMusic/Backups/"
|
Environment.getExternalStorageDirectory().toString() + "/RetroMusic/Backups/"
|
||||||
const val BACKUP_EXTENSION = "rmbak"
|
const val BACKUP_EXTENSION = "rmbak"
|
||||||
private const val APPEND_EXTENSION = ".$BACKUP_EXTENSION"
|
const val APPEND_EXTENSION = ".$BACKUP_EXTENSION"
|
||||||
private const val DATABASES_PATH = "databases"
|
private const val DATABASES_PATH = "databases"
|
||||||
private const val SETTINGS_PATH = "prefs"
|
private const val SETTINGS_PATH = "prefs"
|
||||||
private const val IMAGES_PATH = "userImages"
|
private const val IMAGES_PATH = "userImages"
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package code.name.monkey.retromusic.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object BackupUtil {
|
||||||
|
fun createShareFileIntent(file: File, context: Context): Intent? {
|
||||||
|
return try {
|
||||||
|
Intent().setAction(Intent.ACTION_SEND).putExtra(
|
||||||
|
Intent.EXTRA_STREAM,
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
context.applicationContext.packageName,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION).setType("*/*")
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
"Could not share this file.",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
Intent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
app/src/main/res/menu/menu_backup.xml
Normal file
19
app/src/main/res/menu/menu_backup.xml
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_delete"
|
||||||
|
android:icon="@drawable/ic_delete"
|
||||||
|
android:title="@string/action_delete"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_share"
|
||||||
|
android:icon="@drawable/ic_share"
|
||||||
|
android:title="@string/action_share"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_rename"
|
||||||
|
android:icon="@drawable/ic_edit"
|
||||||
|
android:title="@string/action_rename"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
|
@ -514,4 +514,5 @@
|
||||||
<string name="you_have_to_select_at_least_one_category">You have to select at least one category.</string>
|
<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="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="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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue