@@ -7,6 +7,7 @@ import android.widget.Button
77import android.widget.Toast
88import androidx.activity.result.contract.ActivityResultContracts
99import androidx.appcompat.app.AppCompatActivity
10+ import androidx.core.content.edit
1011import androidx.preference.PreferenceFragmentCompat
1112import org.json.JSONArray
1213import org.json.JSONException
@@ -24,55 +25,119 @@ class SettingsActivity : AppCompatActivity() {
2425 val importButton: Button = findViewById(R .id.btn_import)
2526
2627 exportButton.setOnClickListener {
27- exportFileLauncher.launch(" spam_call_blocker_prefs .json" )
28+ exportFileLauncher.launch(" spam_call_blocker_export .json" )
2829 }
2930
3031 importButton.setOnClickListener {
3132 importFileLauncher.launch(arrayOf(" application/json" ))
3233 }
3334 }
3435
36+ /* *
37+ * Updates the settings container by replacing its content with a new instance of SettingsFragment.
38+ * This function is typically called when the activity is created or when settings need to be refreshed,
39+ * for example, after importing new settings.
40+ */
3541 private fun updateSettingsContainer () {
3642 supportFragmentManager
3743 .beginTransaction()
3844 .replace(R .id.settings_container, SettingsFragment ())
3945 .commit()
4046 }
4147
42- // Fragmento para cargar las preferencias
48+ /* *
49+ * A [PreferenceFragmentCompat] that displays the application's settings.
50+ * It loads preferences from an XML resource file.
51+ */
4352 class SettingsFragment : PreferenceFragmentCompat () {
4453 override fun onCreatePreferences (savedInstanceState : Bundle ? , rootKey : String? ) {
4554 setPreferencesFromResource(R .xml.preferences, rootKey)
4655 }
4756 }
4857
58+ /* *
59+ * ActivityResultLauncher for creating a document to export SharedPreferences.
60+ *
61+ * This launcher is used to initiate the system's file picker to select a location
62+ * and name for the JSON file where the SharedPreferences data will be exported.
63+ *
64+ * Upon successful selection of a URI by the user, the [exportAllSharedPreferences]
65+ * method is called with the chosen URI. A Toast message is displayed to indicate
66+ * the success or failure of the export operation.
67+ */
4968 private val exportFileLauncher =
5069 registerForActivityResult(ActivityResultContracts .CreateDocument (" application/json" )) { uri: Uri ? ->
5170 uri?.let {
5271 if (exportAllSharedPreferences(it)) {
53- Toast .makeText(this , " Preferencias exportadas con éxito" , Toast .LENGTH_SHORT )
72+ Toast .makeText(
73+ this ,
74+ this .getString(R .string.preferences_export_success),
75+ Toast .LENGTH_SHORT
76+ )
5477 .show()
5578 } else {
56- Toast .makeText(this , " Error al exportar preferencias" , Toast .LENGTH_SHORT )
79+ Toast .makeText(
80+ this ,
81+ this .getString(R .string.preferences_export_error),
82+ Toast .LENGTH_SHORT
83+ )
5784 .show()
5885 }
5986 }
6087 }
6188
89+ /* *
90+ * ActivityResultLauncher for handling the result of opening a document to import SharedPreferences.
91+ * It expects a URI as a result. If a URI is received, it attempts to import
92+ * SharedPreferences from the selected file.
93+ * Displays a success or error Toast message based on the outcome of the import operation.
94+ * After a successful import, it updates the settings container.
95+ */
6296 private val importFileLauncher =
6397 registerForActivityResult(ActivityResultContracts .OpenDocument ()) { uri: Uri ? ->
6498 uri?.let {
6599 if (importAllSharedPreferences(it)) {
66100 updateSettingsContainer()
67- Toast .makeText(this , " Preferencias importadas con éxito" , Toast .LENGTH_SHORT )
101+ Toast .makeText(
102+ this ,
103+ this .getString(R .string.preferences_import_success),
104+ Toast .LENGTH_SHORT
105+ )
68106 .show()
69107 } else {
70- Toast .makeText(this , " Error al importar preferencias" , Toast .LENGTH_SHORT )
108+ Toast .makeText(
109+ this ,
110+ this .getString(R .string.preferences_import_error),
111+ Toast .LENGTH_SHORT
112+ )
71113 .show()
72114 }
73115 }
74116 }
75117
118+ /* *
119+ * Exports all SharedPreferences to a JSON file.
120+ *
121+ * This function iterates through all SharedPreferences files in the application's
122+ * `shared_prefs` directory. For each file, it reads all key-value pairs
123+ * and constructs a JSON object representing the preferences. These individual
124+ * JSON objects are then combined into a single parent JSON object, where each
125+ * key is the name of the SharedPreferences file (without the .xml extension)
126+ * and the value is the corresponding preferences JSON object.
127+ *
128+ * The resulting parent JSON object is then written to the `OutputStream`
129+ * associated with the provided `Uri`.
130+ *
131+ * Special handling is implemented for `Set<String>` values, which are
132+ * converted to JSONArrays. Other primitive types (Boolean, Int, Long, Float, String)
133+ * are directly put into the JSON object.
134+ *
135+ * @param uri The Uri of the file where the SharedPreferences data will be exported.
136+ * This Uri should be obtained from a file picker (e.g., using
137+ * `ActivityResultContracts.CreateDocument`).
138+ * @return `true` if the export was successful, `false` otherwise (e.g., if an
139+ * IOException or JSONException occurs).
140+ */
76141 private fun exportAllSharedPreferences (uri : Uri ): Boolean {
77142 return try {
78143 val prefsDir = File (applicationInfo.dataDir, " shared_prefs" )
@@ -109,6 +174,21 @@ class SettingsActivity : AppCompatActivity() {
109174 }
110175 }
111176
177+ /* *
178+ * Imports all SharedPreferences from a JSON file specified by the given URI.
179+ *
180+ * This function reads a JSON file, validates its structure, and then iterates through
181+ * each SharedPreferences file represented in the JSON. For each preference file,
182+ * it reads the key-value pairs and writes them to the corresponding SharedPreferences
183+ * on the device.
184+ *
185+ * Supported data types for import are: `String`, `Int`, `Long`, `Float`, `Boolean`,
186+ * and `Set<String>` (represented as a JSONArray of strings in the JSON).
187+ *
188+ * @param uri The URI of the JSON file to import SharedPreferences from.
189+ * @return `true` if the import was successful, `false` otherwise (e.g., if the file is invalid,
190+ * an I/O error occurs, or a JSON parsing error occurs).
191+ */
112192 private fun importAllSharedPreferences (uri : Uri ): Boolean {
113193 return try {
114194 contentResolver.openInputStream(uri)?.use { inputStream ->
@@ -117,35 +197,37 @@ class SettingsActivity : AppCompatActivity() {
117197
118198 // Validar el archivo JSON antes de importar
119199 if (! isValidSharedPreferencesJson(jsonObject)) {
120- Toast .makeText(this , " El archivo JSON no es válido" , Toast .LENGTH_SHORT ).show()
200+ Toast .makeText(
201+ this ,
202+ this .getString(R .string.invalid_json_file),
203+ Toast .LENGTH_SHORT
204+ ).show()
121205 return false
122206 }
123207
124208 for (prefName in jsonObject.keys()) {
125209 val sharedPreferences = getSharedPreferences(prefName, Context .MODE_PRIVATE )
126- val editor = sharedPreferences.edit()
127-
128- val prefJsonObject = jsonObject.getJSONObject(prefName)
129- for (key in prefJsonObject.keys()) {
130- val value = prefJsonObject.get(key)
131- when (value) {
132- is JSONArray -> {
133- val set = mutableSetOf<String >()
134- for (i in 0 until value.length()) {
135- set.add(value.getString(i))
210+ sharedPreferences.edit {
211+
212+ val prefJsonObject = jsonObject.getJSONObject(prefName)
213+ for (key in prefJsonObject.keys()) {
214+ when (val value = prefJsonObject.get(key)) {
215+ is JSONArray -> {
216+ val set = mutableSetOf<String >()
217+ for (i in 0 until value.length()) {
218+ set.add(value.getString(i))
219+ }
220+ putStringSet(key, set)
136221 }
137- editor.putStringSet(key, set)
138- }
139222
140- is Int -> editor. putInt(key, value)
141- is Long -> editor. putLong(key, value)
142- is Float -> editor. putFloat(key, value)
143- is Boolean -> editor. putBoolean(key, value)
144- is String -> editor. putString(key, value)
145- else -> throw IllegalArgumentException ( " Tipo no soportado " )
223+ is Int -> putInt(key, value)
224+ is Long -> putLong(key, value)
225+ is Float -> putFloat(key, value)
226+ is Boolean -> putBoolean(key, value)
227+ is String -> putString(key, value)
228+ }
146229 }
147230 }
148- editor.apply ()
149231 }
150232 }
151233 true
@@ -155,6 +237,30 @@ class SettingsActivity : AppCompatActivity() {
155237 }
156238 }
157239
240+ /* *
241+ * Validates if the given JSONObject represents a valid SharedPreferences structure.
242+ *
243+ * This function checks if the JSON object conforms to the expected format of
244+ * SharedPreferences data. It iterates through each preference file (represented by
245+ * a key in the root JSONObject) and then iterates through each key-value pair
246+ * within that preference file.
247+ *
248+ * The validation ensures:
249+ * - Each preference file is a JSONObject.
250+ * - Values within each preference file are of supported types:
251+ * - Int
252+ * - Long
253+ * - Float
254+ * - Boolean
255+ * - String
256+ * - JSONArray (where all elements are Strings, representing a Set<String>)
257+ *
258+ * If any part of the JSON structure does not conform to these rules, or if a
259+ * JSONException occurs during parsing, the function returns false.
260+ *
261+ * @param jsonObject The JSONObject to validate.
262+ * @return True if the JSONObject is a valid representation of SharedPreferences, false otherwise.
263+ */
158264 private fun isValidSharedPreferencesJson (jsonObject : JSONObject ): Boolean {
159265 return try {
160266 for (prefName in jsonObject.keys()) {
0 commit comments