Fixed crash and added more options to backup

This commit is contained in:
Prathamesh More 2021-10-23 21:31:33 +05:30
parent 75410bf77b
commit db3a7d4097
7 changed files with 171 additions and 26 deletions

View file

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

View file

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

View file

@ -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 {
MaterialDialog(requireContext()).show {
title(res = R.string.action_rename)
input(prefill = System.currentTimeMillis().toString()) { _, text ->
// Text submitted with the action button
lifecycleScope.launch { lifecycleScope.launch {
BackupHelper.createBackup(requireContext()) BackupHelper.createBackup(requireContext(), text.toString())
backupViewModel.loadBackups() 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) {
AlertDialog.Builder(requireContext())
.setTitle(R.string.restore)
.setMessage(R.string.restore_message)
.setPositiveButton(R.string.restore) { _, _ ->
lifecycleScope.launch { lifecycleScope.launch {
backupViewModel.restoreBackup(requireActivity(), file) 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
}
} }

View file

@ -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,20 +13,24 @@ 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 {
ZipOutputStream(BufferedOutputStream(FileOutputStream(backupFile))).use { out ->
for (zipItem in zipItems) { for (zipItem in zipItems) {
FileInputStream(zipItem.filePath).use { fi -> FileInputStream(zipItem.filePath).use { fi ->
BufferedInputStream(fi).use { origin -> BufferedInputStream(fi).use { origin ->
@ -36,6 +41,9 @@ object BackupHelper {
} }
} }
} }
} catch (exception: FileNotFoundException) {
Toast.makeText(App.getContext(), "Couldn't create backup", Toast.LENGTH_SHORT).show()
}
} }
private fun getDatabaseZipItems(context: Context): List<ZipItem> { private fun getDatabaseZipItems(context: Context): List<ZipItem> {
@ -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"

View file

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

View 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>

View file

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