Skip to content

Commit

Permalink
[Beta]Backup & restore accents
Browse files Browse the repository at this point in the history
Backup is stored in the internal storage of the device.
TODO: Backup app db, support Oreo
  • Loading branch information
Akilesh-T committed Jan 28, 2020
1 parent 9d8ecc7 commit c1849e9
Show file tree
Hide file tree
Showing 19 changed files with 508 additions and 48 deletions.
56 changes: 29 additions & 27 deletions app/src/main/java/app/akilesh/qacc/Const.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,43 @@ import android.content.Context
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.Q
import android.os.Environment.DIRECTORY_DOCUMENTS
import app.akilesh.qacc.model.Colour
import com.topjohnwu.superuser.Shell

object Const {

private lateinit var context : Context
lateinit var contextConst : Context
fun setContext(appContext: Context) {
context = appContext
contextConst = appContext
}
//Credits to AEX
object Colors {

val presets = listOf(
Colour("#FFC107", context.getString(R.string.amber)),
Colour("#448AFF", context.getString(R.string.blue)),
Colour("#607D8B", context.getString(R.string.blue_grey)),
Colour("#795548", context.getString(R.string.brown)),
Colour("#FF1744", context.getString(R.string.candy_red)),
Colour("#00BCD4", context.getString(R.string.cyan)),
Colour("#FF5722", context.getString(R.string.deep_orange)),
Colour("#7C4DFF", context.getString(R.string.deep_purple)),
Colour("#47AE84", context.getString(R.string.elegant_green)),
Colour("#21EF8B", context.getString(R.string.extended_green)),
Colour("#9E9E9E", context.getString(R.string.grey)),
Colour("#536DFE", context.getString(R.string.indigo)),
Colour("#9ABC98", context.getString(R.string.jade_green)),
Colour("#03A9F4", context.getString(R.string.light_blue)),
Colour("#8BC34A", context.getString(R.string.light_green)),
Colour("#CDDC39", context.getString(R.string.lime)),
Colour("#FF9800", context.getString(R.string.orange)),
Colour("#A1B6ED", context.getString(R.string.pale_blue)),
Colour("#F05361", context.getString(R.string.pale_red)),
Colour("#FF4081", context.getString(R.string.pink)),
Colour("#FF5252", context.getString(R.string.red)),
Colour("#009688", context.getString(R.string.teal)),
Colour("#FFEB3B", context.getString(R.string.yellove))
Colour("#FFC107", contextConst.getString(R.string.amber)),
Colour("#448AFF", contextConst.getString(R.string.blue)),
Colour("#607D8B", contextConst.getString(R.string.blue_grey)),
Colour("#795548", contextConst.getString(R.string.brown)),
Colour("#FF1744", contextConst.getString(R.string.candy_red)),
Colour("#00BCD4", contextConst.getString(R.string.cyan)),
Colour("#FF5722", contextConst.getString(R.string.deep_orange)),
Colour("#7C4DFF", contextConst.getString(R.string.deep_purple)),
Colour("#47AE84", contextConst.getString(R.string.elegant_green)),
Colour("#21EF8B", contextConst.getString(R.string.extended_green)),
Colour("#9E9E9E", contextConst.getString(R.string.grey)),
Colour("#536DFE", contextConst.getString(R.string.indigo)),
Colour("#9ABC98", contextConst.getString(R.string.jade_green)),
Colour("#03A9F4", contextConst.getString(R.string.light_blue)),
Colour("#8BC34A", contextConst.getString(R.string.light_green)),
Colour("#CDDC39", contextConst.getString(R.string.lime)),
Colour("#FF9800", contextConst.getString(R.string.orange)),
Colour("#A1B6ED", contextConst.getString(R.string.pale_blue)),
Colour("#F05361", contextConst.getString(R.string.pale_red)),
Colour("#FF4081", contextConst.getString(R.string.pink)),
Colour("#FF5252", contextConst.getString(R.string.red)),
Colour("#009688", contextConst.getString(R.string.teal)),
Colour("#FFEB3B", contextConst.getString(R.string.yellove))
)

}
Expand All @@ -53,10 +54,11 @@ object Const {
const val githubReleases = "$githubRepo/releases/latest"
}

object Module {
private const val modPath = "/data/adb/modules/qacc-mobile"
object Paths {
const val modPath = "/data/adb/modules/qacc-mobile"
val overlayPath = if (SDK_INT == Q) "$modPath/system/product/overlay"
else "$modPath/system/vendor/overlay"
val backupFolder = "/sdcard/${DIRECTORY_DOCUMENTS}/${contextConst.getString(R.string.app_name_short)}/backups"
}

const val prefix = "com.android.theme.color.custom."
Expand Down
20 changes: 5 additions & 15 deletions app/src/main/java/app/akilesh/qacc/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navOptions
import app.akilesh.qacc.Const.getAssetFiles
import app.akilesh.qacc.R
import app.akilesh.qacc.databinding.ActivityMainBinding
import app.akilesh.qacc.utils.AppUtils.navAnim
import app.akilesh.qacc.utils.DownloadUtils.download
import app.akilesh.qacc.viewmodel.InstallApkViewModel
import com.github.javiersantos.appupdater.AppUpdaterUtils
Expand Down Expand Up @@ -60,24 +60,14 @@ class MainActivity: AppCompatActivity() {
}
}

val navOptions = navOptions {
anim {
// Animations from Android 10
enter = R.anim.fragment_enter
exit = R.anim.fragment_exit
popEnter = R.anim.fragment_enter_pop
popExit = R.anim.fragment_exit_pop
}
}

binding.xFab.setOnClickListener {
navController.navigate(R.id.color_picker, null, navOptions)
navController.navigate(R.id.color_picker, null, navAnim)
}

binding.bottomAppBar.setOnMenuItemClickListener {
when(it.itemId) {
R.id.settings -> navController.navigate(R.id.settings, null, navOptions)
R.id.info -> navController.navigate(R.id.info, null, navOptions)
R.id.settings -> navController.navigate(R.id.settings, null, navAnim)
R.id.info -> navController.navigate(R.id.info, null, navAnim)
}
true
}
Expand All @@ -87,7 +77,7 @@ class MainActivity: AppCompatActivity() {
* May not be the correct way, but convenient.
*/
binding.bottomAppBar.setNavigationOnClickListener {
navController.navigate(R.id.home, null, navOptions)
navController.navigate(R.id.home, null, navAnim)
}

copyAssets()
Expand Down
49 changes: 49 additions & 0 deletions app/src/main/java/app/akilesh/qacc/ui/adapter/BackupListAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package app.akilesh.qacc.ui.adapter

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.AppCompatImageView
import androidx.recyclerview.widget.RecyclerView
import app.akilesh.qacc.R
import com.google.android.material.textview.MaterialTextView

class BackupListAdapter internal constructor(
context: Context,
private var filesList: MutableList<String>,
val onClick : (String) -> Unit
) : RecyclerView.Adapter<BackupListAdapter.BackupsViewHolder>() {

private val inflater: LayoutInflater = LayoutInflater.from(context)

inner class BackupsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val file: MaterialTextView = itemView.findViewById(R.id.backup_file)
val viewContents: AppCompatImageView = itemView.findViewById(R.id.view_content)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupsViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item_backups, parent, false)
return BackupsViewHolder(itemView)
}

override fun onBindViewHolder(holder: BackupsViewHolder, position: Int) {
val file = filesList[position]
holder.file.text = file.removeSuffix(".tar.gz").replace('-', ' ')
holder.viewContents.setOnClickListener { onClick(file) }
}

internal fun setFiles(files: MutableList<String>) {
this.filesList = files
notifyDataSetChanged()
}

internal fun getFileAndRemoveAt(position: Int): String {
val current = filesList[position]
filesList.removeAt(position)
notifyItemRemoved(position)
return current
}

override fun getItemCount() = filesList.size
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package app.akilesh.qacc.ui.fragments

import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import app.akilesh.qacc.Const.Paths.backupFolder
import app.akilesh.qacc.Const.Paths.modPath
import app.akilesh.qacc.Const.Paths.overlayPath
import app.akilesh.qacc.R
import app.akilesh.qacc.databinding.BackupRestoreFragmentBinding
import app.akilesh.qacc.databinding.ColorPreviewBinding
import app.akilesh.qacc.databinding.DialogTitleBinding
import app.akilesh.qacc.model.Colour
import app.akilesh.qacc.ui.adapter.BackupListAdapter
import app.akilesh.qacc.ui.adapter.ColorListAdapter
import app.akilesh.qacc.utils.AppUtils.showSnackbar
import app.akilesh.qacc.utils.SwipeToDelete
import app.akilesh.qacc.viewmodel.BackupFileViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.topjohnwu.superuser.Shell
import java.io.File
import java.io.FileInputStream
import java.util.*

class BackupRestoreFragment: Fragment() {

private lateinit var binding: BackupRestoreFragmentBinding
private lateinit var model: BackupFileViewModel
private val busyBox = "/data/adb/magisk/busybox"

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = BackupRestoreFragmentBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding.newBackup.setOnClickListener { createBackup() }
binding.restore.setOnClickListener { restore() }

val adapter = BackupListAdapter(context!!, getBackupFiles()) { file ->
val colorPreviewBinding = ColorPreviewBinding.inflate(layoutInflater)
val dialogTitleBinding = DialogTitleBinding.inflate(layoutInflater)
dialogTitleBinding.titleText.text = String.format(resources.getString(R.string.backup_contents))
dialogTitleBinding.titleIcon.setImageResource(R.drawable.ic_palette_24dp)

val contents = getBackupContents(file)
contents.removeIf { it == "./" }
contents.replaceAll { s -> s.removePrefix("./hex").removePrefix("_").removeSuffix(".apk").substringBefore('_') }
Log.d("contents", contents.toString())

val accents: List<Colour> = contents.map { Colour("#$it", getString(R.string.hex_code)) }
val adapter = ColorListAdapter(context!!, accents) {}

colorPreviewBinding.recyclerViewColor.adapter = adapter
colorPreviewBinding.recyclerViewColor.layoutManager = LinearLayoutManager(context)

MaterialAlertDialogBuilder(context)
.setCustomTitle(dialogTitleBinding.root)
.setView(colorPreviewBinding.root)
.create()
.show()
}

binding.recyclerViewBackupFiles.adapter = adapter
binding.recyclerViewBackupFiles.layoutManager = LinearLayoutManager(context)

model = ViewModelProvider(this).get(BackupFileViewModel::class.java)
model.backupFiles.observe(viewLifecycleOwner, Observer { files ->
files.let { adapter.setFiles(it) }
})

val swipeToDelete = object : SwipeToDelete(context!!) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val file = adapter.getFileAndRemoveAt(viewHolder.adapterPosition)
val result = Shell.su(
"rm -f $backupFolder/$file"
).exec()
if (result.isSuccess) Toast.makeText(context, getString(R.string.backup_deleted), Toast.LENGTH_SHORT).show()
}
}
val itemTouchHelper = ItemTouchHelper(swipeToDelete)
itemTouchHelper.attachToRecyclerView(binding.recyclerViewBackupFiles)
}

private fun getBackupContents(file: String): MutableList<String> {
return Shell.su(
".$busyBox tar t -f $backupFolder/$file"
).exec().out
}

private fun getBackupFiles(): MutableList<String> {
return Shell.su(
"ls $backupFolder"
).exec().out
}

@SuppressLint("SdCardPath")
private fun createBackup() {
var date = Calendar.getInstance().time.toString()
date = date.replace("\\s".toRegex(), "-")
val result = Shell.su(
"mkdir -p $backupFolder",
".$busyBox tar c -zv -f $backupFolder/$date.tar.gz -C $overlayPath ."
).exec()
Log.d("compress", result.out.toString())
if (result.isSuccess) {
Toast.makeText(context, getString(R.string.backup_created), Toast.LENGTH_SHORT).show()
model.backupFiles.value = getBackupFiles()
}
}

private fun restore() {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/gzip"
startActivityForResult(intent, 3)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == 3 && resultCode == RESULT_OK && data != null) {
val selectedUri = data.data
val parcelFileDescriptor = selectedUri?.let { context!!.contentResolver.openFileDescriptor(it, "r", null) }
val inputStream = FileInputStream(parcelFileDescriptor?.fileDescriptor)
val backupFile = File(context!!.cacheDir, "acc.tar.gz")
inputStream.use { stream ->
backupFile.outputStream().use {
stream.copyTo(it)
}
}

val result = Shell.su("[ -d $modPath ]").exec()
if (!result.isSuccess) {
Shell.su("mkdir -p $overlayPath").exec()
Shell.su(context!!.resources.openRawResource(R.raw.create_module)).exec()
}

val restoreResult = Shell.su(
".$busyBox tar x -zv -f $backupFile -C $overlayPath"
).exec()
Log.d("restore", restoreResult.out.toString())
if (restoreResult.isSuccess) {
context!!.cacheDir.delete()
showSnackbar(this.view!!, getString(R.string.accents_restored))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import app.akilesh.qacc.Const.Module.overlayPath
import app.akilesh.qacc.Const.Paths.overlayPath
import app.akilesh.qacc.Const.prefix
import app.akilesh.qacc.R
import app.akilesh.qacc.ui.adapter.AccentListAdapter
import app.akilesh.qacc.databinding.HomeFragmentBinding
import app.akilesh.qacc.utils.AppUtils.showSnackbar
import app.akilesh.qacc.utils.SwipeToDeleteCallback
import app.akilesh.qacc.utils.SwipeToDelete
import app.akilesh.qacc.viewmodel.AccentViewModel
import com.topjohnwu.superuser.Shell

Expand Down Expand Up @@ -53,7 +53,7 @@ class HomeFragment: Fragment() {
accents?.let { adapter.setAccents(it) }
})

val swipeHandler = object : SwipeToDeleteCallback(context!!) {
val swipeHandler = object : SwipeToDelete(context!!) {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val accent = adapter.getAccentAndRemoveAt(viewHolder.adapterPosition)
accentViewModel.delete(accent)
Expand Down
Loading

0 comments on commit c1849e9

Please sign in to comment.