mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-16 15:02:48 +00:00
501 lines
25 KiB
Kotlin
501 lines
25 KiB
Kotlin
/*
|
|
* Copyright (C) 2014 The Android Open Source Project
|
|
* modified
|
|
* SPDX-License-Identifier: Apache-2.0 AND GPL-3.0-only
|
|
*/
|
|
package helium314.keyboard.latin.settings
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.app.Activity
|
|
import android.content.Intent
|
|
import android.content.SharedPreferences
|
|
import android.graphics.BitmapFactory
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Bundle
|
|
import helium314.keyboard.latin.utils.Log
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.appcompat.app.AlertDialog
|
|
import androidx.preference.Preference
|
|
import androidx.preference.PreferenceManager
|
|
import kotlinx.serialization.encodeToString
|
|
import kotlinx.serialization.json.Json
|
|
import helium314.keyboard.compat.locale
|
|
import helium314.keyboard.dictionarypack.DictionaryPackConstants
|
|
import helium314.keyboard.latin.utils.ChecksumCalculator
|
|
import helium314.keyboard.keyboard.KeyboardLayoutSet
|
|
import helium314.keyboard.keyboard.KeyboardSwitcher
|
|
import helium314.keyboard.latin.AudioAndHapticFeedbackManager
|
|
import helium314.keyboard.latin.BuildConfig
|
|
import helium314.keyboard.latin.R
|
|
import helium314.keyboard.latin.SystemBroadcastReceiver
|
|
import helium314.keyboard.latin.checkVersionUpgrade
|
|
import helium314.keyboard.latin.common.FileUtils
|
|
import helium314.keyboard.latin.common.LocaleUtils.constructLocale
|
|
import helium314.keyboard.latin.settings.SeekBarDialogPreference.ValueProxy
|
|
import helium314.keyboard.latin.utils.CUSTOM_LAYOUT_PREFIX
|
|
import helium314.keyboard.latin.utils.DeviceProtectedUtils
|
|
import helium314.keyboard.latin.utils.JniUtils
|
|
import helium314.keyboard.latin.utils.editCustomLayout
|
|
import helium314.keyboard.latin.utils.infoDialog
|
|
import helium314.keyboard.latin.utils.reloadEnabledSubtypes
|
|
import java.io.File
|
|
import java.io.FileInputStream
|
|
import java.io.FileOutputStream
|
|
import java.io.IOException
|
|
import java.io.OutputStream
|
|
import java.util.zip.ZipEntry
|
|
import java.util.zip.ZipInputStream
|
|
import java.util.zip.ZipOutputStream
|
|
|
|
/**
|
|
* "Advanced" settings sub screen.
|
|
*
|
|
* This settings sub screen handles the following advanced preferences.
|
|
* - Key popup dismiss delay
|
|
* - Keypress vibration duration
|
|
* - Keypress sound volume
|
|
* - Show app icon
|
|
* - Improve keyboard
|
|
* - Debug settings
|
|
*/
|
|
class AdvancedSettingsFragment : SubScreenFragment() {
|
|
private val libfile by lazy { File(requireContext().filesDir.absolutePath + File.separator + JniUtils.JNI_LIB_IMPORT_FILE_NAME) }
|
|
private val backupFilePatterns by lazy { listOf(
|
|
"blacklists/.*\\.txt".toRegex(),
|
|
"layouts/.*.(txt|json)".toRegex(),
|
|
"dicts/.*/.*user\\.dict".toRegex(),
|
|
"UserHistoryDictionary.*/UserHistoryDictionary.*\\.(body|header)".toRegex(),
|
|
"custom_background_image.*".toRegex(),
|
|
) }
|
|
|
|
// is there any way to get additional information into the ActivityResult? would remove the need for 5 times the (almost) same code
|
|
private val libraryFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
|
val uri = it.data?.data ?: return@registerForActivityResult
|
|
copyLibrary(uri)
|
|
}
|
|
|
|
private val backupFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
|
val uri = it.data?.data ?: return@registerForActivityResult
|
|
backup(uri)
|
|
}
|
|
|
|
private val restoreFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
|
val uri = it.data?.data ?: return@registerForActivityResult
|
|
restore(uri)
|
|
}
|
|
|
|
private val dayImageFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
|
val uri = it.data?.data ?: return@registerForActivityResult
|
|
loadImage(uri, false)
|
|
}
|
|
|
|
private val nightImageFilePicker = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
if (it.resultCode != Activity.RESULT_OK) return@registerForActivityResult
|
|
val uri = it.data?.data ?: return@registerForActivityResult
|
|
loadImage(uri, true)
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
setupPreferences()
|
|
}
|
|
|
|
private fun setupPreferences() {
|
|
addPreferencesFromResource(R.xml.prefs_screen_advanced)
|
|
setDebugPrefVisibility()
|
|
val context = requireContext()
|
|
|
|
// When we are called from the Settings application but we are not already running, some
|
|
// singleton and utility classes may not have been initialized. We have to call
|
|
// initialization method of these classes here. See {@link LatinIME#onCreate()}.
|
|
AudioAndHapticFeedbackManager.init(context)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON)
|
|
}
|
|
if (BuildConfig.BUILD_TYPE == "nouserlib") {
|
|
removePreference("load_gesture_library")
|
|
}
|
|
setupKeyLongpressTimeoutSettings()
|
|
findPreference<Preference>("load_gesture_library")?.setOnPreferenceClickListener { onClickLoadLibrary() }
|
|
findPreference<Preference>("backup_restore")?.setOnPreferenceClickListener { showBackupRestoreDialog() }
|
|
findPreference<Preference>("custom_background_image")?.setOnPreferenceClickListener { onClickLoadImage() }
|
|
|
|
findPreference<Preference>("custom_symbols_layout")?.setOnPreferenceClickListener {
|
|
val layoutName = Settings.readSymbolsLayoutName(context, context.resources.configuration.locale()).takeIf { it.startsWith(CUSTOM_LAYOUT_PREFIX) }
|
|
val oldLayout = if (layoutName != null) null
|
|
else context.assets.open("layouts${File.separator}symbols.txt").reader().readText()
|
|
editCustomLayout(layoutName ?: "${CUSTOM_LAYOUT_PREFIX}symbols.txt", context, oldLayout, true)
|
|
true
|
|
}
|
|
findPreference<Preference>("custom_shift_symbols_layout")?.setOnPreferenceClickListener {
|
|
val layoutName = Settings.readShiftedSymbolsLayoutName(context).takeIf { it.startsWith(CUSTOM_LAYOUT_PREFIX) }
|
|
val oldLayout = if (layoutName != null) null
|
|
else context.assets.open("layouts${File.separator}symbols_shifted.txt").reader().readText()
|
|
editCustomLayout(layoutName ?: "${CUSTOM_LAYOUT_PREFIX}shift_symbols.txt", context, oldLayout, true)
|
|
true
|
|
}
|
|
}
|
|
|
|
override fun onStart() {
|
|
super.onStart()
|
|
// Remove debug preference. This is already done in onCreate, but if we come back from
|
|
// debug prefs and have just disabled debug settings, they should disappear.
|
|
setDebugPrefVisibility()
|
|
}
|
|
|
|
private fun setDebugPrefVisibility() {
|
|
if (!BuildConfig.DEBUG && !sharedPreferences.getBoolean(DebugSettings.PREF_SHOW_DEBUG_SETTINGS, false)) {
|
|
removePreference(Settings.SCREEN_DEBUG)
|
|
}
|
|
}
|
|
|
|
@SuppressLint("ApplySharedPref")
|
|
private fun onClickLoadLibrary(): Boolean {
|
|
// get architecture for telling user which file to use
|
|
val abi = Build.SUPPORTED_ABIS[0]
|
|
// show delete / add dialog
|
|
val builder = AlertDialog.Builder(requireContext())
|
|
.setTitle(R.string.load_gesture_library)
|
|
.setMessage(requireContext().getString(R.string.load_gesture_library_message, abi))
|
|
.setPositiveButton(R.string.load_gesture_library_button_load) { _, _ ->
|
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
|
.setType("application/octet-stream")
|
|
libraryFilePicker.launch(intent)
|
|
}
|
|
.setNegativeButton(android.R.string.cancel, null)
|
|
if (libfile.exists()) {
|
|
builder.setNeutralButton(R.string.load_gesture_library_button_delete) { _, _ ->
|
|
libfile.delete()
|
|
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit().remove(Settings.PREF_LIBRARY_CHECKSUM).commit()
|
|
Runtime.getRuntime().exit(0)
|
|
}
|
|
}
|
|
builder.show()
|
|
return true
|
|
}
|
|
|
|
private fun copyLibrary(uri: Uri) {
|
|
val tmpfile = File(requireContext().filesDir.absolutePath + File.separator + "tmplib")
|
|
try {
|
|
val inputStream = requireContext().contentResolver.openInputStream(uri)
|
|
val outputStream = FileOutputStream(tmpfile)
|
|
outputStream.use {
|
|
tmpfile.setReadOnly() // as per recommendations in https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading
|
|
FileUtils.copyStreamToOtherStream(inputStream, it)
|
|
}
|
|
|
|
val checksum = ChecksumCalculator.checksum(tmpfile.inputStream()) ?: ""
|
|
if (checksum == JniUtils.expectedDefaultChecksum()) {
|
|
renameToLibfileAndRestart(tmpfile, checksum)
|
|
} else {
|
|
val abi = Build.SUPPORTED_ABIS[0]
|
|
AlertDialog.Builder(requireContext())
|
|
.setMessage(getString(R.string.checksum_mismatch_message, abi))
|
|
.setPositiveButton(android.R.string.ok) { _, _ -> renameToLibfileAndRestart(tmpfile, checksum) }
|
|
.setNegativeButton(android.R.string.cancel) { _, _ -> tmpfile.delete() }
|
|
.show()
|
|
}
|
|
} catch (e: IOException) {
|
|
tmpfile.delete()
|
|
// should inform user, but probably the issues will only come when reading the library
|
|
}
|
|
}
|
|
|
|
private fun onClickLoadImage(): Boolean {
|
|
if (Settings.readDayNightPref(sharedPreferences, resources)) {
|
|
AlertDialog.Builder(requireContext())
|
|
.setMessage(R.string.day_or_night_image)
|
|
.setPositiveButton(R.string.day_or_night_day) { _, _ -> customImageDialog(false) }
|
|
.setNegativeButton(R.string.day_or_night_night) { _, _ -> customImageDialog(true) }
|
|
.setNeutralButton(android.R.string.cancel, null)
|
|
.show()
|
|
} else {
|
|
customImageDialog(false)
|
|
}
|
|
return true
|
|
}
|
|
|
|
private fun customImageDialog(night: Boolean) {
|
|
val imageFile = Settings.getCustomBackgroundFile(requireContext(), night)
|
|
val builder = AlertDialog.Builder(requireContext())
|
|
.setMessage(R.string.customize_background_image)
|
|
.setPositiveButton(R.string.button_load_custom) { _, _ ->
|
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
|
.setType("image/*")
|
|
if (night) nightImageFilePicker.launch(intent)
|
|
else dayImageFilePicker.launch(intent)
|
|
}
|
|
.setNegativeButton(android.R.string.cancel, null)
|
|
if (imageFile.exists()) {
|
|
builder.setNeutralButton(R.string.delete) { _, _ ->
|
|
imageFile.delete()
|
|
Settings.clearCachedBackgroundImages()
|
|
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
|
|
}
|
|
}
|
|
builder.show()
|
|
}
|
|
|
|
private fun loadImage(uri: Uri, night: Boolean) {
|
|
val imageFile = Settings.getCustomBackgroundFile(requireContext(), night)
|
|
FileUtils.copyStreamToNewFile(requireContext().contentResolver.openInputStream(uri), imageFile)
|
|
try {
|
|
BitmapFactory.decodeFile(imageFile.absolutePath)
|
|
} catch (_: Exception) {
|
|
infoDialog(requireContext(), R.string.file_read_error)
|
|
imageFile.delete()
|
|
}
|
|
Settings.clearCachedBackgroundImages()
|
|
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
|
|
}
|
|
|
|
@SuppressLint("ApplySharedPref")
|
|
private fun renameToLibfileAndRestart(file: File, checksum: String) {
|
|
libfile.delete()
|
|
// store checksum in default preferences (soo JniUtils)
|
|
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit().putString(Settings.PREF_LIBRARY_CHECKSUM, checksum).commit()
|
|
file.renameTo(libfile)
|
|
Runtime.getRuntime().exit(0) // exit will restart the app, so library will be loaded
|
|
}
|
|
|
|
private fun showBackupRestoreDialog(): Boolean {
|
|
AlertDialog.Builder(requireContext())
|
|
.setTitle(R.string.backup_restore_title)
|
|
.setMessage(R.string.backup_restore_message)
|
|
.setNegativeButton(R.string.button_backup) { _, _ ->
|
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
|
.putExtra(
|
|
Intent.EXTRA_TITLE,
|
|
requireContext().getString(R.string.english_ime_name)
|
|
.replace(" ", "_") + "_backup.zip"
|
|
)
|
|
.setType("application/zip")
|
|
backupFilePicker.launch(intent)
|
|
}
|
|
.setPositiveButton(android.R.string.cancel, null)
|
|
.setNeutralButton(R.string.button_restore) { _, _ ->
|
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
|
.addCategory(Intent.CATEGORY_OPENABLE)
|
|
.setType("application/zip")
|
|
restoreFilePicker.launch(intent)
|
|
}
|
|
.show()
|
|
return true
|
|
}
|
|
|
|
private fun backup(uri: Uri) {
|
|
// zip all files matching the backup patterns
|
|
// essentially this is the typed words information, and user-added dictionaries
|
|
val filesDir = requireContext().filesDir ?: return
|
|
val filesPath = filesDir.path + File.separator
|
|
val files = mutableListOf<File>()
|
|
filesDir.walk().forEach { file ->
|
|
val path = file.path.replace(filesPath, "")
|
|
if (backupFilePatterns.any { path.matches(it) })
|
|
files.add(file)
|
|
}
|
|
val protectedFilesDir = DeviceProtectedUtils.getDeviceProtectedContext(requireContext()).filesDir
|
|
val protectedFilesPath = protectedFilesDir.path + File.separator
|
|
val protectedFiles = mutableListOf<File>()
|
|
protectedFilesDir.walk().forEach { file ->
|
|
val path = file.path.replace(protectedFilesPath, "")
|
|
if (backupFilePatterns.any { path.matches(it) })
|
|
protectedFiles.add(file)
|
|
}
|
|
try {
|
|
activity?.contentResolver?.openOutputStream(uri)?.use { os ->
|
|
// write files to zip
|
|
val zipStream = ZipOutputStream(os)
|
|
files.forEach {
|
|
val fileStream = FileInputStream(it).buffered()
|
|
zipStream.putNextEntry(ZipEntry(it.path.replace(filesPath, "")))
|
|
fileStream.copyTo(zipStream, 1024)
|
|
fileStream.close()
|
|
zipStream.closeEntry()
|
|
}
|
|
protectedFiles.forEach {
|
|
val fileStream = FileInputStream(it).buffered()
|
|
zipStream.putNextEntry(ZipEntry(it.path.replace(protectedFilesDir.path, "unprotected")))
|
|
fileStream.copyTo(zipStream, 1024)
|
|
fileStream.close()
|
|
zipStream.closeEntry()
|
|
}
|
|
zipStream.putNextEntry(ZipEntry(PREFS_FILE_NAME))
|
|
settingsToJsonStream(sharedPreferences.all, zipStream)
|
|
zipStream.closeEntry()
|
|
zipStream.putNextEntry(ZipEntry(PROTECTED_PREFS_FILE_NAME))
|
|
settingsToJsonStream(PreferenceManager.getDefaultSharedPreferences(requireContext()).all, zipStream)
|
|
zipStream.closeEntry()
|
|
zipStream.close()
|
|
}
|
|
} catch (t: Throwable) {
|
|
// inform about every error
|
|
Log.w(TAG, "error during backup", t)
|
|
infoDialog(requireContext(), requireContext().getString(R.string.backup_error, t.message))
|
|
}
|
|
}
|
|
|
|
private fun restore(uri: Uri) {
|
|
try {
|
|
activity?.contentResolver?.openInputStream(uri)?.use { inputStream ->
|
|
ZipInputStream(inputStream).use { zip ->
|
|
var entry: ZipEntry? = zip.nextEntry
|
|
val filesDir = requireContext().filesDir?.path ?: return
|
|
val deviceProtectedFilesDir = DeviceProtectedUtils.getDeviceProtectedContext(requireContext()).filesDir?.path ?: return
|
|
while (entry != null) {
|
|
if (entry.name.startsWith("unprotected${File.separator}")) {
|
|
val adjustedName = entry.name.substringAfter("unprotected${File.separator}")
|
|
if (backupFilePatterns.any { adjustedName.matches(it) }) {
|
|
val targetFileName = upgradeFileNames(adjustedName)
|
|
val file = File(deviceProtectedFilesDir, targetFileName)
|
|
FileUtils.copyStreamToNewFile(zip, file)
|
|
}
|
|
} else if (backupFilePatterns.any { entry!!.name.matches(it) }) {
|
|
val targetFileName = upgradeFileNames(entry.name)
|
|
val file = File(filesDir, targetFileName)
|
|
FileUtils.copyStreamToNewFile(zip, file)
|
|
} else if (entry.name == PREFS_FILE_NAME) {
|
|
val prefLines = String(zip.readBytes()).split("\n")
|
|
sharedPreferences.edit().clear().apply()
|
|
readJsonLinesToSettings(prefLines, sharedPreferences)
|
|
} else if (entry.name == PROTECTED_PREFS_FILE_NAME) {
|
|
val prefLines = String(zip.readBytes()).split("\n")
|
|
val protectedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
protectedPrefs.edit().clear().apply()
|
|
readJsonLinesToSettings(prefLines, protectedPrefs)
|
|
}
|
|
zip.closeEntry()
|
|
entry = zip.nextEntry
|
|
}
|
|
}
|
|
}
|
|
checkVersionUpgrade(requireContext())
|
|
reloadEnabledSubtypes(requireContext())
|
|
val newDictBroadcast = Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)
|
|
activity?.sendBroadcast(newDictBroadcast)
|
|
// reload current prefs screen
|
|
preferenceScreen.removeAll()
|
|
setupPreferences()
|
|
KeyboardSwitcher.getInstance().forceUpdateKeyboardTheme(requireContext())
|
|
} catch (t: Throwable) {
|
|
// inform about every error
|
|
Log.w(TAG, "error during restore", t)
|
|
infoDialog(requireContext(), requireContext().getString(R.string.restore_error, t.message))
|
|
}
|
|
}
|
|
|
|
// todo (later): remove this when new package name has been in use for long enough, this is only for migrating from old openboard name
|
|
private fun upgradeFileNames(originalName: String): String {
|
|
return when {
|
|
originalName.endsWith(USER_DICTIONARY_SUFFIX) -> {
|
|
// replace directory after switch to language tag
|
|
val dirName = originalName.substringAfter(File.separator).substringBefore(File.separator)
|
|
originalName.replace(dirName, dirName.constructLocale().toLanguageTag())
|
|
}
|
|
originalName.startsWith("blacklists") -> {
|
|
// replace file name after switch to language tag
|
|
val fileName = originalName.substringAfter("blacklists${File.separator}").substringBefore(".txt")
|
|
originalName.replace(fileName, fileName.constructLocale().toLanguageTag())
|
|
}
|
|
originalName.startsWith("layouts") -> {
|
|
// replace file name after switch to language tag
|
|
// but only if it's not a symbols layout
|
|
val localeString = originalName.substringAfter(".").substringBefore(".")
|
|
val locale = localeString.constructLocale()
|
|
if (locale.toLanguageTag() != "und")
|
|
originalName.replace(localeString, locale.toLanguageTag())
|
|
else
|
|
originalName // no valid locale -> must be symbols layout, don't change
|
|
}
|
|
originalName.startsWith("UserHistoryDictionary") -> {
|
|
val localeString = originalName.substringAfter(".").substringBefore(".")
|
|
val locale = localeString.constructLocale()
|
|
originalName.replace(localeString, locale.toLanguageTag())
|
|
}
|
|
else -> originalName
|
|
}
|
|
}
|
|
|
|
private fun setupKeyLongpressTimeoutSettings() {
|
|
val prefs = sharedPreferences
|
|
findPreference<SeekBarDialogPreference>(Settings.PREF_KEY_LONGPRESS_TIMEOUT)?.setInterface(object : ValueProxy {
|
|
override fun writeValue(value: Int, key: String) = prefs.edit().putInt(key, value).apply()
|
|
|
|
override fun writeDefaultValue(key: String) = prefs.edit().remove(key).apply()
|
|
|
|
override fun readValue(key: String) = Settings.readKeyLongpressTimeout(prefs, resources)
|
|
|
|
override fun readDefaultValue(key: String) = Settings.readDefaultKeyLongpressTimeout(resources)
|
|
|
|
override fun getValueText(value: Int) =
|
|
resources.getString(R.string.abbreviation_unit_milliseconds, value.toString())
|
|
|
|
override fun feedbackValue(value: Int) {}
|
|
})
|
|
}
|
|
|
|
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String?) {
|
|
when (key) {
|
|
Settings.PREF_SHOW_SETUP_WIZARD_ICON -> SystemBroadcastReceiver.toggleAppIcon(requireContext())
|
|
Settings.PREF_MORE_POPUP_KEYS -> KeyboardLayoutSet.onSystemLocaleChanged()
|
|
}
|
|
}
|
|
|
|
companion object {
|
|
@Suppress("UNCHECKED_CAST") // it is checked... but whatever (except string set, because can't check for that))
|
|
private fun settingsToJsonStream(settings: Map<String, Any?>, out: OutputStream) {
|
|
val booleans = settings.filterValues { it is Boolean } as Map<String, Boolean>
|
|
val ints = settings.filterValues { it is Int } as Map<String, Int>
|
|
val longs = settings.filterValues { it is Long } as Map<String, Long>
|
|
val floats = settings.filterValues { it is Float } as Map<String, Float>
|
|
val strings = settings.filterValues { it is String } as Map<String, String>
|
|
val stringSets = settings.filterValues { it is Set<*> } as Map<String, Set<String>>
|
|
// now write
|
|
out.write("boolean settings\n".toByteArray())
|
|
out.write(Json.encodeToString(booleans).toByteArray())
|
|
out.write("\nint settings\n".toByteArray())
|
|
out.write(Json.encodeToString(ints).toByteArray())
|
|
out.write("\nlong settings\n".toByteArray())
|
|
out.write(Json.encodeToString(longs).toByteArray())
|
|
out.write("\nfloat settings\n".toByteArray())
|
|
out.write(Json.encodeToString(floats).toByteArray())
|
|
out.write("\nstring settings\n".toByteArray())
|
|
out.write(Json.encodeToString(strings).toByteArray())
|
|
out.write("\nstring set settings\n".toByteArray())
|
|
out.write(Json.encodeToString(stringSets).toByteArray())
|
|
}
|
|
|
|
private fun readJsonLinesToSettings(list: List<String>, prefs: SharedPreferences): Boolean {
|
|
val i = list.iterator()
|
|
val e = prefs.edit()
|
|
try {
|
|
while (i.hasNext()) {
|
|
when (i.next()) {
|
|
"boolean settings" -> Json.decodeFromString<Map<String, Boolean>>(i.next()).forEach { e.putBoolean(it.key, it.value) }
|
|
"int settings" -> Json.decodeFromString<Map<String, Int>>(i.next()).forEach { e.putInt(it.key, it.value) }
|
|
"long settings" -> Json.decodeFromString<Map<String, Long>>(i.next()).forEach { e.putLong(it.key, it.value) }
|
|
"float settings" -> Json.decodeFromString<Map<String, Float>>(i.next()).forEach { e.putFloat(it.key, it.value) }
|
|
"string settings" -> Json.decodeFromString<Map<String, String>>(i.next()).forEach { e.putString(it.key, it.value) }
|
|
"string set settings" -> Json.decodeFromString<Map<String, Set<String>>>(i.next()).forEach { e.putStringSet(it.key, it.value) }
|
|
}
|
|
}
|
|
e.apply()
|
|
return true
|
|
} catch (e: Exception) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private const val PREFS_FILE_NAME = "preferences.json"
|
|
private const val PROTECTED_PREFS_FILE_NAME = "protected_preferences.json"
|
|
private const val TAG = "AdvancedSettingsFragment"
|