Refactor: Made the backup system better so users chose the location.

Signed-off-by: HeCodes2Much <wayne6324@gmail.com>
This commit is contained in:
HeCodes2Much
2024-07-13 12:32:52 +01:00
parent 43b1935831
commit 623d2ab633
9 changed files with 180 additions and 181 deletions

View File

@@ -343,151 +343,6 @@ fun Context.isWorkProfileEnabled(): Boolean {
}
}
fun Context.backupSharedPreferences(backupFileNames: Array<String>) {
// Check if external storage is writable
if (!isExternalStorageWritable()) {
showLongToast("External storage is not writable.")
return
}
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val backupDir = File(downloadDir, Constants.PACKAGE_NAME)
// Ensure the directory exists, create it if necessary
if (!backupDir.exists()) {
backupDir.mkdirs()
}
for (backupFileName in backupFileNames) {
val sharedPreferences: SharedPreferences =
this.getSharedPreferences(backupFileName, Context.MODE_PRIVATE)
val allPrefs = sharedPreferences.all
val backupFile = File(backupDir, "$backupFileName.xml")
try {
backupFile.bufferedWriter().use { writer ->
writer.write("<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n")
writer.write("<map>\n")
// Loop through all preferences
for ((key, value) in allPrefs) {
if (value != null) {
val valueString = value.toString().replace("'", "&apos;")
val line = when (value) {
is Boolean, is Int, is Float, is Long, is String -> "\t<${value::class.simpleName!!.lowercase()} name='$key' value='$valueString' />\n"
else -> null
}
line?.let {
writer.write(it)
}
}
}
writer.write("</map>")
}
showLongToast("Backup for $backupFileName completed successfully.")
} catch (e: IOException) {
e.printStackTrace()
showLongToast("Failed to backup SharedPreferences $backupFileName: ${e.message}")
}
}
}
private fun isExternalStorageWritable(): Boolean {
return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}
fun Context.restoreSharedPreferences(backupFileNames: Array<String>) {
// Check if external storage is readable
if (!isExternalStorageReadable()) {
showLongToast("External storage is not readable.")
return
}
val downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val backupDir = File(downloadDir, Constants.PACKAGE_NAME)
for (backupFileName in backupFileNames) {
val backupFile = File(backupDir, "$backupFileName.xml")
if (backupFile.exists()) {
try {
FileInputStream(backupFile).use { fileInputStream ->
val factory = XmlPullParserFactory.newInstance()
factory.isNamespaceAware = true
val xpp = factory.newPullParser()
xpp.setInput(fileInputStream, null)
var eventType = xpp.eventType
var key: String? = null
var value: Any? = null
while (eventType != XmlPullParser.END_DOCUMENT) {
when (eventType) {
XmlPullParser.START_TAG -> {
when (xpp.name) {
"boolean", "int", "float", "long", "string" -> {
key = xpp.getAttributeValue(null, "name")
val valueString = xpp.getAttributeValue(null, "value")
value = when (xpp.name) {
"boolean" -> valueString.toBoolean()
"int" -> valueString.toInt()
"float" -> valueString.toFloat()
"long" -> valueString.toLong()
else -> valueString
}
}
}
}
XmlPullParser.END_TAG -> {
when (xpp.name) {
"boolean", "int", "float", "long", "string" -> {
if (key != null && value != null) {
saveToSharedPreferences(backupFileName, key, value)
key = null
value = null
}
}
}
}
}
eventType = xpp.next()
}
}
showLongToast("Restore for $backupFileName completed successfully.")
} catch (e: Exception) {
Log.e("Exception", e.toString())
showLongToast("Failed to restore SharedPreferences $backupFileName: ${e.message}")
}
} else {
showLongToast("Backup file for $backupFileName does not exist.")
}
}
}
fun Context.saveToSharedPreferences(prefsFileName: String, key: String, value: Any) {
val sharedPreferences: SharedPreferences = this.getSharedPreferences(prefsFileName, Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
when (value) {
is Boolean -> editor.putBoolean(key, value)
is Int -> editor.putInt(key, value)
is Float -> editor.putFloat(key, value)
is Long -> editor.putLong(key, value)
is String -> editor.putString(key, value)
}
editor.apply()
}
private fun isExternalStorageReadable(): Boolean {
val state = Environment.getExternalStorageState()
return Environment.MEDIA_MOUNTED == state || Environment.MEDIA_MOUNTED_READ_ONLY == state
}
fun Context.hasInternetPermission(): Boolean {
val permission = Manifest.permission.INTERNET
val result = ContextCompat.checkSelfPermission(this, permission)

View File

@@ -1,6 +1,7 @@
package com.github.droidworksstudio.launcher.helper
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
@@ -16,8 +17,6 @@ import android.view.WindowInsets
import android.widget.TextView
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.navigation.NavOptions
import com.github.droidworksstudio.common.backupSharedPreferences
import com.github.droidworksstudio.common.restoreSharedPreferences
import com.github.droidworksstudio.common.showLongToast
import com.github.droidworksstudio.launcher.BuildConfig
import com.github.droidworksstudio.launcher.utils.Constants
@@ -29,7 +28,10 @@ import com.google.gson.Gson
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.UnknownHostException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@@ -192,14 +194,25 @@ class AppHelper @Inject constructor() {
context.startActivity(Intent.createChooser(emailIntent, "Choose Mail Application"))
}
fun backupSharedPreferences(context: Context) {
val backupFileNames = arrayOf(Constants.PREFS_FILENAME, Constants.WEATHER_PREFS)
context.backupSharedPreferences(backupFileNames)
fun storeFile(activity: Activity) {
// Generate a unique filename with a timestamp
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val fileName = "backup_$timeStamp.json"
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
putExtra(Intent.EXTRA_TITLE, fileName)
}
activity.startActivityForResult(intent, Constants.BACKUP_WRITE, null)
}
fun restoreSharedPreferences(context: Context) {
val backupFileNames = arrayOf(Constants.PREFS_FILENAME, Constants.WEATHER_PREFS)
context.restoreSharedPreferences(backupFileNames)
fun loadFile(activity: Activity) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/json"
}
activity.startActivityForResult(intent, Constants.BACKUP_READ, null)
}
fun getActionType(actionType: Constants.Swipe): NavOptions {

View File

@@ -3,14 +3,17 @@ package com.github.droidworksstudio.launcher.helper
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.util.Log
import android.view.Gravity
import com.github.droidworksstudio.launcher.utils.Constants
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class PreferenceHelper @Inject constructor(@ApplicationContext context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(Constants.PREFS_FILENAME, 0)
private val prefs: SharedPreferences = context.getSharedPreferences(Constants.PACKAGE_PREFS, 0)
private val setColor = getColor(context)
@@ -257,4 +260,45 @@ class PreferenceHelper @Inject constructor(@ApplicationContext context: Context)
}
}
}
fun saveToString(): String {
val all: HashMap<String, Any?> = HashMap(prefs.all)
return Gson().toJson(all)
}
fun loadFromString(json: String) {
val editor = prefs.edit()
val all: HashMap<String, Any?> =
Gson().fromJson(json, object : TypeToken<HashMap<String, Any?>>() {}.type)
for ((key, value) in all) {
when (value) {
is String -> editor.putString(key, value)
is Boolean -> editor.putBoolean(key, value)
is Int -> editor.putInt(key, value)
is Double -> editor.putInt(key, value.toInt()) // we store everything as int
is Float -> editor.putInt(key, value.toInt())
is MutableSet<*> -> {
val list = value.filterIsInstance<String>().toSet()
editor.putStringSet(key, list)
}
else -> {
Log.d("backup error", "$value")
}
}
}
editor.apply()
}
fun clear() {
prefs.edit().clear().apply()
}
fun clearAll(context: Context) {
val prefsLauncher: SharedPreferences = context.getSharedPreferences(Constants.PACKAGE_PREFS, 0)
val prefsWidgets: SharedPreferences = context.getSharedPreferences(Constants.WEATHER_PREFS, 0)
prefsLauncher.edit().clear().apply()
prefsWidgets.edit().clear().apply()
}
}

View File

@@ -162,15 +162,18 @@ class UpdateManagerHelper(private val fragment: Fragment) {
@Deprecated("Deprecated in Java")
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == Constants.REQUEST_INSTALL_PERMISSION) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val canInstallPackages = context.packageManager.canRequestPackageInstalls()
if (canInstallPackages) {
// Permission granted, proceed with installation
installApk()
} else {
// Permission still not granted, handle accordingly
context.applicationContext.showLongToast("Please allow install permission to install.")
when (requestCode) {
Constants.REQUEST_INSTALL_PERMISSION -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val canInstallPackages = context.packageManager.canRequestPackageInstalls()
if (canInstallPackages) {
// Permission granted, proceed with installation
installApk()
} else {
// Permission still not granted, handle accordingly
context.applicationContext.showLongToast("Please allow install permission to install.")
}
}
}
}

View File

@@ -2,7 +2,9 @@ package com.github.droidworksstudio.launcher.ui.activities
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
@@ -33,12 +35,16 @@ import com.github.droidworksstudio.common.showLongToast
import com.github.droidworksstudio.launcher.R
import com.github.droidworksstudio.launcher.databinding.ActivityMainBinding
import com.github.droidworksstudio.launcher.helper.AppHelper
import com.github.droidworksstudio.launcher.helper.AppReloader
import com.github.droidworksstudio.launcher.helper.PreferenceHelper
import com.github.droidworksstudio.launcher.utils.Constants
import com.github.droidworksstudio.launcher.viewmodel.AppViewModel
import com.github.droidworksstudio.launcher.viewmodel.PreferenceViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.FileOutputStream
import java.io.InputStreamReader
import javax.inject.Inject
@@ -274,13 +280,62 @@ class MainActivity : AppCompatActivity() {
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == Constants.REQUEST_LOCATION_PERMISSION_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, proceed with getting location
getLocation()
} else {
// Permission denied, show a message to the user
setLocationPermissionDenied(true)
when (requestCode) {
Constants.REQUEST_LOCATION_PERMISSION_CODE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, proceed with getting location
getLocation()
} else {
// Permission denied, show a message to the user
setLocationPermissionDenied(true)
}
}
}
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != Activity.RESULT_OK) {
// showToastLong(applicationContext, "Intent Error")
return
}
when (requestCode) {
Constants.BACKUP_READ -> {
data?.data?.also { uri ->
applicationContext.contentResolver.openInputStream(uri).use { inputStream ->
val stringBuilder = StringBuilder()
BufferedReader(InputStreamReader(inputStream)).use { reader ->
var line: String? = reader.readLine()
while (line != null) {
stringBuilder.append(line)
line = reader.readLine()
}
}
val string = stringBuilder.toString()
val prefs = PreferenceHelper(applicationContext)
prefs.clear()
prefs.loadFromString(string)
}
}
Handler(Looper.getMainLooper()).postDelayed({
AppReloader.restartApp(applicationContext)
}, 500)
}
Constants.BACKUP_WRITE -> {
data?.data?.also { uri ->
applicationContext.contentResolver.openFileDescriptor(uri, "w")?.use { file ->
FileOutputStream(file.fileDescriptor).use { stream ->
val text = PreferenceHelper(applicationContext).saveToString()
stream.channel.truncate(0)
stream.write(text.toByteArray())
}
}
}
}
}
}

View File

@@ -95,11 +95,21 @@ class SettingsFragment : Fragment(),
binding.miscellaneousSearchEngineControl.text = preferenceHelper.searchEngines.getString(context)
binding.miscellaneousLauncherFontsControl.text = preferenceHelper.launcherFont.getString(context)
updateGestureControlText(context, preferenceHelper.doubleTapAction, preferenceHelper.doubleTapApp, binding.gesturesDoubleTapControl)
updateGestureControlText(context, preferenceHelper.swipeUpAction, preferenceHelper.swipeUpApp, binding.gesturesSwipeUpControl)
updateGestureControlText(context, preferenceHelper.swipeDownAction, preferenceHelper.swipeDownApp, binding.gesturesSwipeDownControl)
updateGestureControlText(context, preferenceHelper.swipeLeftAction, preferenceHelper.swipeLeftApp, binding.gesturesSwipeLeftControl)
updateGestureControlText(context, preferenceHelper.swipeRightAction, preferenceHelper.swipeRightApp, binding.gesturesSwipeRightControl)
val actions = listOf(
Triple(preferenceHelper.doubleTapAction, preferenceHelper.doubleTapApp, binding.gesturesDoubleTapControl),
Triple(preferenceHelper.swipeUpAction, preferenceHelper.swipeUpApp, binding.gesturesSwipeUpControl),
Triple(preferenceHelper.swipeDownAction, preferenceHelper.swipeDownApp, binding.gesturesSwipeDownControl),
Triple(preferenceHelper.swipeLeftAction, preferenceHelper.swipeLeftApp, binding.gesturesSwipeLeftControl),
Triple(preferenceHelper.swipeRightAction, preferenceHelper.swipeRightApp, binding.gesturesSwipeRightControl)
)
actions.forEach { (action, app, control) ->
when (action) {
Constants.Action.OpenApp -> updateGestureControlText(context, action, app, control)
else -> {}
}
}
}
// Function to update UI text based on action and app name
@@ -189,12 +199,18 @@ class SettingsFragment : Fragment(),
}
binding.backupView.setOnClickListener {
appHelper.backupSharedPreferences(requireContext())
appHelper.storeFile(requireActivity())
}
binding.clearView.setOnClickListener {
preferenceHelper.clearAll(context)
Handler(Looper.getMainLooper()).postDelayed({
AppReloader.restartApp(context)
}, 500)
}
binding.restoreView.setOnClickListener {
appHelper.restoreSharedPreferences(requireContext())
AppReloader.restartApp(context)
appHelper.loadFile(requireActivity())
}
}

View File

@@ -8,18 +8,18 @@ import com.github.droidworksstudio.launcher.R
object Constants {
const val PACKAGE_NAME = "app.easy.launcher"
const val PACKAGE_NAME_DEBUG = "$PACKAGE_NAME.debug"
const val PACKAGE_PREFS = "EasyLauncher.pref"
const val WIDGETS_COUNT = 2
const val WIDGET_WEATHER = "WIDGET_WEATHER"
const val WIDGET_BATTERY = "WIDGET_BATTERY"
const val WEATHER_UNITS = "WEATHER_UNITS"
const val WEATHER_PREFS = "EasyWeather.pref"
const val WEATHER_UNITS = "WEATHER_UNITS"
const val LATITUDE = "LATITUDE"
const val LONGITUDE = "LONGITUDE"
const val PREFS_FILENAME = "EasyLauncher.pref"
const val FIRST_LAUNCH = "FIRST_LAUNCH"
const val SHOW_DATE = "SHOW_DATE"
const val SHOW_TIME = "SHOW_TIME"
@@ -80,7 +80,10 @@ object Constants {
const val LAUNCHER_FONT = "LAUNCHER_FONT"
const val REQUEST_INSTALL_PERMISSION = 123
const val REQUEST_LOCATION_PERMISSION_CODE = 246
const val REQUEST_LOCATION_PERMISSION_CODE = 234
const val BACKUP_WRITE = 987
const val BACKUP_READ = 876
const val LOCATION_DENIED = "LOCATION_DENIED"

View File

@@ -883,6 +883,15 @@
android:layout_marginVertical="10dp"
style="@style/TextDefaultStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/clear_view"
android:text="@string/settings_backups_clear"
android:textSize="@dimen/text_large"
android:layout_marginVertical="10dp"
style="@style/TextDefaultStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@@ -96,6 +96,7 @@
<string name="settings_others_github">Github</string>
<string name="settings_backups_backup">Backup Preferences</string>
<string name="settings_backups_clear">Clear Preferences</string>
<string name="settings_backups_restore">Restore Preferences</string>
<string name="settings_reload_app_backup">Preferences backed up.</string>