Skip to content

Commit

Permalink
GrindrPlus: Add backup/restore for database
Browse files Browse the repository at this point in the history
  • Loading branch information
R0rt1z2 committed Sep 15, 2024
1 parent 37339b2 commit 2666b50
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 28 deletions.
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand Down
14 changes: 9 additions & 5 deletions app/src/main/java/com/grindrplus/GrindrPlus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ object GrindrPlus {
hookManager.init()
}

fun runOnMainThread(block: Runnable) {
Handler(context.mainLooper).post(block)
fun runOnMainThread(appContext: Context? = null, block: (Context) -> Unit) {
val useContext = appContext ?: context
Handler(useContext.mainLooper).post {
block(useContext)
}
}

fun runOnMainThreadWithCurrentActivity(block: (Activity) -> Unit) {
Expand All @@ -116,9 +119,10 @@ object GrindrPlus {
}
}

fun showToast(duration: Int, message: String) {
runOnMainThread {
Toast.makeText(context, message, duration).show()
fun showToast(duration: Int, message: String, appContext: Context? = null) {
val useContext = appContext ?: context
runOnMainThread(useContext) {
Toast.makeText(useContext, message, duration).show()
}
}

Expand Down
27 changes: 27 additions & 0 deletions app/src/main/java/com/grindrplus/core/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,33 @@ class Database(context: Context, databasePath: String?) : SQLiteOpenHelper(
}
}

fun restoreDatabase(backupPath: String): Boolean {
synchronized(lock) {
val dbFile = File(writableDatabase.path)
val backupFile = File(backupPath)

if (!backupFile.exists()) {
return false
}

writableDatabase.close()

return try {
backupFile.inputStream().use { input ->
dbFile.outputStream().use { output ->
input.copyTo(output)
}
}
true
} catch (e: Exception) {
e.printStackTrace()
false
} finally {
writableDatabase
}
}
}

private val tables = listOf(
Table(
name = "ExpiringPhotos",
Expand Down
1 change: 0 additions & 1 deletion app/src/main/java/com/grindrplus/hooks/ModSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.grindrplus.GrindrPlus
import com.grindrplus.utils.Hook
import com.grindrplus.utils.HookStage
import com.grindrplus.utils.hook
import com.grindrplus.utils.hookConstructor
import de.robv.android.xposed.XposedHelpers.getObjectField

class ModSettings : Hook(
Expand Down
172 changes: 150 additions & 22 deletions app/src/main/java/com/grindrplus/ui/fragments/SettingsFragment.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.grindrplus.ui.fragments

import Database
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
Expand All @@ -10,6 +11,7 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.util.TypedValue
import android.view.*
import android.view.inputmethod.EditorInfo
Expand All @@ -18,35 +20,56 @@ import android.widget.EditText
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.SwitchCompat
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import com.google.android.material.appbar.AppBarLayout
import com.grindrplus.GrindrPlus
import com.grindrplus.core.Config
import com.grindrplus.ui.Utils
import com.grindrplus.ui.colors.Colors
import java.io.File
import kotlin.system.exitProcess

class SettingsFragment : Fragment() {

private lateinit var importConfigLauncher: ActivityResultLauncher<Intent>
private var isDatabaseOperation: Boolean = false
private lateinit var importLauncher: ActivityResultLauncher<Intent>
private lateinit var exportLauncher: ActivityResultLauncher<Intent>
private lateinit var subLinearLayout: LinearLayout

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

importConfigLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
exportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri ->
importConfigFromUri(uri)
if (isDatabaseOperation) {
exportDatabaseToUri(uri)
} else {
exportConfigToUri(uri)
}
}
}
}

importLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.also { uri ->
if (isDatabaseOperation) {
importDatabaseFromUri(uri)
} else {
importConfigFromUri(uri)
}
}
}
}

}

override fun onCreateView(
Expand Down Expand Up @@ -132,27 +155,41 @@ class SettingsFragment : Fragment() {

toolbar.addView(toolbarTitle)

toolbar.menu.add(Menu.NONE, 1, Menu.NONE, "Export current config").apply {
toolbar.menu.add(Menu.NONE, 1, Menu.NONE, "Export config").apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
toolbar.menu.add(Menu.NONE, 2, Menu.NONE, "Import config").apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
toolbar.menu.add(Menu.NONE, 3, Menu.NONE, "Export database").apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
toolbar.menu.add(Menu.NONE, 2, Menu.NONE, "Import config from file").apply {
toolbar.menu.add(Menu.NONE, 4, Menu.NONE, "Import database").apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
toolbar.menu.add(Menu.NONE, 3, Menu.NONE, "Reset GrindrPlus").apply {
toolbar.menu.add(Menu.NONE, 5, Menu.NONE, "Reset GrindrPlus").apply {
setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}

toolbar.setOnMenuItemClickListener { item ->
when (item.itemId) {
1 -> {
exportConfig()
promptFolderSelection(false)
true
}
2 -> {
promptFileSelection()
promptImportSelection(false)
true
}
3 -> {
promptFolderSelection(true)
true
}
4 -> {
promptImportSelection(true)
true
}
5 -> {
showResetConfirmationDialog()
true
}
Expand All @@ -173,12 +210,30 @@ class SettingsFragment : Fragment() {
toolbar.overflowIcon = overflowIcon
}

private fun promptFileSelection() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
private fun importDatabaseFromUri(uri: Uri) {
val context = requireContext()
val backupPath = File(context.cacheDir, "grindrplus_backup.db").absolutePath
val databasePath = context.filesDir.absolutePath + "/grindrplus.db"

try {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val backupFile = File(backupPath)
inputStream.copyTo(backupFile.outputStream())

val database = Database(context, databasePath)
val restored = database.restoreDatabase(backupPath)

if (restored) {
GrindrPlus.showToast(Toast.LENGTH_LONG, "Database imported successfully!", context)
showImportSuccessDialog()
} else {
GrindrPlus.showToast(Toast.LENGTH_LONG, "Failed to restore database!", context)
}
}
} catch (e: Exception) {
e.printStackTrace()
GrindrPlus.showToast(Toast.LENGTH_LONG, "Failed to import database!", context)
}
importConfigLauncher.launch(intent)
}

private fun importConfigFromUri(uri: Uri) {
Expand All @@ -188,22 +243,91 @@ class SettingsFragment : Fragment() {
val configJson = inputStream.bufferedReader().use { it.readText() }
Config.importFromJson(configJson)
updateUIFromConfig()
GrindrPlus.showToast(Toast.LENGTH_LONG, "Config imported successfully!", context)
}
} catch (e: Exception) {
e.printStackTrace()
GrindrPlus.showToast(Toast.LENGTH_LONG, "Failed to import config!", context)
}
}

private fun exportConfig() {
val configJson = Config.getConfigJson()
private fun showImportSuccessDialog() {
val context = requireContext()
AlertDialog.Builder(context)
.setTitle("Database Import")
.setMessage("The database has been successfully imported. The app will now close to apply the changes.")
.setPositiveButton("OK") { _, _ ->
closeApp()
}
.setCancelable(false)
.show()
}

val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, configJson)
private fun promptImportSelection(isDatabase: Boolean) {
isDatabaseOperation = isDatabase
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
if (isDatabase) {
type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/x-sqlite3", "application/vnd.sqlite3", "application/db", "*/*"))
} else {
type = "application/json"
}
}
importLauncher.launch(intent)
}

startActivity(Intent.createChooser(shareIntent, "Share config"))
private fun promptFolderSelection(isDatabase: Boolean) {
isDatabaseOperation = isDatabase
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
exportLauncher.launch(intent)
}

private fun exportDatabaseToUri(uri: Uri) {
val context = requireContext()
val databasePath = context.filesDir.absolutePath + "/grindrplus.db"

val databaseFile = File(databasePath)
val contentResolver = context.contentResolver

try {
val pickedDir = DocumentFile.fromTreeUri(context, uri)

val newFile = pickedDir?.createFile("application/x-sqlite3", "grindrplus_backup.db")
newFile?.uri?.let { exportUri ->
contentResolver.openOutputStream(exportUri)?.use { outputStream ->
databaseFile.inputStream().use { inputStream ->
inputStream.copyTo(outputStream)
}
GrindrPlus.showToast(Toast.LENGTH_LONG, "Database exported successfully!", requireContext())
}
}
} catch (e: Exception) {
e.printStackTrace()
GrindrPlus.showToast(Toast.LENGTH_LONG, "Failed to export database!", requireContext())
}
}

private fun exportConfigToUri(uri: Uri) {
val context = requireContext()
val configJson = Config.getConfigJson()

val contentResolver = context.contentResolver

try {
val pickedDir = DocumentFile.fromTreeUri(context, uri)

val configFile = pickedDir?.createFile("application/json", "grindrplus_config.json")
configFile?.uri?.let { exportUri ->
contentResolver.openOutputStream(exportUri)?.use { outputStream ->
outputStream.write(configJson.toByteArray())
GrindrPlus.showToast(Toast.LENGTH_LONG, "Config exported successfully!", context)
}
}
} catch (e: Exception) {
e.printStackTrace()
GrindrPlus.showToast(Toast.LENGTH_LONG, "Failed to export config!", context)
}
}

private fun updateUIFromConfig() {
Expand Down Expand Up @@ -275,6 +399,10 @@ class SettingsFragment : Fragment() {

private fun resetConfigAndCloseApp() {
Config.resetConfig(true)
closeApp()
}

fun closeApp() {
val activity = requireActivity()
activity.finishAffinity()
exitProcess(0)
Expand Down

0 comments on commit 2666b50

Please sign in to comment.