diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0edd771..ba0aeb6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,8 +23,8 @@ android { applicationId = "com.example.maposition" minSdk = 21 targetSdk = 36 - versionCode = 7 - versionName = "1.6" + versionCode = 8 + versionName = "1.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 928745c..7745218 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ > + () + + private lateinit var recipientsList: LinearLayout + private lateinit var nameLayout: TextInputLayout + private lateinit var nameInput: TextInputEditText + + private val pickContact = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != RESULT_OK) return@registerForActivityResult + val uri = result.data?.data ?: return@registerForActivityResult + contentResolver.query( + uri, + arrayOf( + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Phone.NUMBER + ), + null, null, null + )?.use { c -> + if (c.moveToFirst()) { + val name = c.getString(0) ?: "" + val phone = (c.getString(1) ?: "").replace(" ", "") + if (phone.isBlank()) { + Toast.makeText(this, R.string.error_contact_no_phone, Toast.LENGTH_SHORT).show() + } else { + addRecipient(Recipient(name, phone)) + } + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_edit_group) + + recipientsList = findViewById(R.id.recipientsList) + nameLayout = findViewById(R.id.groupNameLayout) + nameInput = findViewById(R.id.groupNameInput) + + originalName = intent.getStringExtra(EXTRA_GROUP) + val editing = originalName != null + + findViewById(R.id.editTitle).setText( + if (editing) R.string.group_edit else R.string.group_new + ) + + if (editing) { + Prefs.getGroups(this).find { it.name == originalName }?.let { g -> + nameInput.setText(g.name) + recipients.addAll(g.recipients) + } + } + + findViewById(R.id.backButton).setOnClickListener { finish() } + findViewById(R.id.pickContactButton).setOnClickListener { + pickContact.launch( + Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI) + ) + } + findViewById(R.id.addManualButton).setOnClickListener { + showRecipientDialog(null) + } + findViewById(R.id.saveGroupButton).setOnClickListener { saveGroup() } + + val deleteButton = findViewById(R.id.deleteGroupButton) + if (editing) { + deleteButton.setOnClickListener { confirmDelete() } + } else { + deleteButton.visibility = android.view.View.GONE + } + + renderRecipients() + } + + // --- Liste des destinataires --- + + private fun addRecipient(r: Recipient) { + if (recipients.any { it.phone == r.phone }) { + Toast.makeText(this, R.string.error_dup_phone, Toast.LENGTH_SHORT).show() + return + } + recipients.add(r) + renderRecipients() + } + + private fun renderRecipients() { + recipientsList.removeAllViews() + if (recipients.isEmpty()) { + val empty = TextView(this).apply { + text = getString(R.string.no_recipients_yet) + setPadding(8, 16, 8, 16) + } + recipientsList.addView(empty) + return + } + recipients.forEachIndexed { index, r -> + val item = layoutInflater.inflate(R.layout.item_recipient, recipientsList, false) + item.findViewById(R.id.recipientName).text = r.name.ifBlank { getString(R.string.unnamed_recipient) } + item.findViewById(R.id.recipientPhone).text = r.phone + item.findViewById(R.id.removeButton).setOnClickListener { + recipients.removeAt(index) + renderRecipients() + } + item.setOnClickListener { showRecipientDialog(index) } + recipientsList.addView(item) + } + } + + private fun showRecipientDialog(index: Int?) { + val existing = index?.let { recipients[it] } + val view = layoutInflater.inflate(R.layout.dialog_recipient, null) + val rNameInput = view.findViewById(R.id.recipientNameInput) + val rPhoneLayout = view.findViewById(R.id.recipientPhoneLayout) + val rPhoneInput = view.findViewById(R.id.recipientPhoneInput) + + if (existing != null) { + rNameInput.setText(existing.name) + rPhoneInput.setText(existing.phone) + } + + val dialog = MaterialAlertDialogBuilder(this) + .setTitle(if (existing == null) R.string.recipient_new else R.string.recipient_edit) + .setView(view) + .setPositiveButton(R.string.save, null) + .setNegativeButton(android.R.string.cancel, null) + .create() + + dialog.setOnShowListener { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + val name = rNameInput.text.toString().trim() + val phone = rPhoneInput.text.toString().trim() + rPhoneLayout.error = null + when { + phone.isEmpty() || !phoneRegex.matches(phone) -> + rPhoneLayout.error = getString(R.string.error_bad_phone, phone) + recipients.anyIndexed(index) { it.phone == phone } -> + rPhoneLayout.error = getString(R.string.error_dup_phone) + else -> { + val r = Recipient(name, phone) + if (index != null) recipients[index] = r else recipients.add(r) + renderRecipients() + dialog.dismiss() + } + } + } + } + dialog.show() + } + + /** any() en ignorant l'élément en cours d'édition. */ + private fun List.anyIndexed(skip: Int?, pred: (Recipient) -> Boolean): Boolean = + filterIndexed { i, _ -> i != skip }.any(pred) + + // --- Enregistrement / suppression du groupe --- + + private fun saveGroup() { + val name = nameInput.text.toString().trim() + nameLayout.error = null + + val groups = Prefs.getGroups(this) + when { + name.isEmpty() -> { + nameLayout.error = getString(R.string.error_no_group_name) + return + } + groups.any { it.name == name && it.name != originalName } -> { + nameLayout.error = getString(R.string.error_dup_group) + return + } + recipients.isEmpty() -> { + Toast.makeText(this, R.string.error_no_recipients, Toast.LENGTH_SHORT).show() + return + } + } + + val idx = groups.indexOfFirst { it.name == originalName } + val group = Group(name, recipients.toList()) + if (idx >= 0) groups[idx] = group else groups.add(group) + Prefs.saveGroups(this, groups) + if (originalName != null && Prefs.getActiveGroupName(this) == originalName) { + Prefs.setActiveGroupName(this, name) + } + finish() + } + + private fun confirmDelete() { + MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.delete_group_confirm, originalName)) + .setPositiveButton(R.string.delete) { _, _ -> + val groups = Prefs.getGroups(this) + groups.removeAll { it.name == originalName } + Prefs.saveGroups(this, groups) + finish() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } +} diff --git a/app/src/main/java/com/example/maposition/MainActivity.kt b/app/src/main/java/com/example/maposition/MainActivity.kt index 75a6b14..ccb05b1 100644 --- a/app/src/main/java/com/example/maposition/MainActivity.kt +++ b/app/src/main/java/com/example/maposition/MainActivity.kt @@ -149,7 +149,7 @@ class MainActivity : AppCompatActivity() { } groupChips.addView(chip) } - recipientsText.text = active.phones.joinToString(", ") + recipientsText.text = active.displayList() } // --- Déclenchement de l'envoi --- diff --git a/app/src/main/java/com/example/maposition/Prefs.kt b/app/src/main/java/com/example/maposition/Prefs.kt index 293cd33..f25ddd6 100644 --- a/app/src/main/java/com/example/maposition/Prefs.kt +++ b/app/src/main/java/com/example/maposition/Prefs.kt @@ -4,7 +4,15 @@ import android.content.Context import org.json.JSONArray import org.json.JSONObject -data class Group(val name: String, val phones: List) +data class Recipient(val name: String, val phone: String) + +data class Group(val name: String, val recipients: List) { + val phones: List get() = recipients.map { it.phone } + + /** "Alice, Bob, 0611…" — le nom si renseigné, sinon le numéro. */ + fun displayList(): String = + recipients.joinToString(", ") { it.name.ifBlank { it.phone } } +} object Prefs { private const val FILE = "MaPositionPrefs" @@ -24,17 +32,28 @@ object Prefs { val arr = JSONArray(raw) for (i in 0 until arr.length()) { val o = arr.getJSONObject(i) - val phones = mutableListOf() - val pa = o.getJSONArray("phones") - for (j in 0 until pa.length()) phones.add(pa.getString(j)) - groups.add(Group(o.getString("name"), phones)) + val recipients = mutableListOf() + val ra = o.optJSONArray("recipients") + if (ra != null) { + for (j in 0 until ra.length()) { + val ro = ra.getJSONObject(j) + recipients.add(Recipient(ro.optString("name", ""), ro.getString("phone"))) + } + } else { + // Migration depuis le format v1.4 : numéros sans nom + val pa = o.optJSONArray("phones") + if (pa != null) { + for (j in 0 until pa.length()) recipients.add(Recipient("", pa.getString(j))) + } + } + groups.add(Group(o.getString("name"), recipients)) } } else { - // Migration depuis l'ancien format : liste unique de numéros + // Migration depuis le format v1.0-1.3 : liste unique de numéros val legacy = sp(ctx).getString("phones", "") ?: "" val phones = legacy.split(";").map { it.trim() }.filter { it.isNotEmpty() } if (phones.isNotEmpty()) { - groups.add(Group(ctx.getString(R.string.default_group_name), phones)) + groups.add(Group(ctx.getString(R.string.default_group_name), phones.map { Recipient("", it) })) saveGroups(ctx, groups) } } @@ -44,7 +63,11 @@ object Prefs { fun saveGroups(ctx: Context, groups: List) { val arr = JSONArray() groups.forEach { g -> - arr.put(JSONObject().put("name", g.name).put("phones", JSONArray(g.phones))) + val ra = JSONArray() + g.recipients.forEach { r -> + ra.put(JSONObject().put("name", r.name).put("phone", r.phone)) + } + arr.put(JSONObject().put("name", g.name).put("recipients", ra)) } sp(ctx).edit().putString("groups", arr.toString()).apply() } diff --git a/app/src/main/java/com/example/maposition/SettingsActivity.kt b/app/src/main/java/com/example/maposition/SettingsActivity.kt index 24395b7..d950da4 100644 --- a/app/src/main/java/com/example/maposition/SettingsActivity.kt +++ b/app/src/main/java/com/example/maposition/SettingsActivity.kt @@ -1,22 +1,18 @@ package com.example.maposition +import android.content.Intent import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.view.View import android.widget.ImageButton import android.widget.LinearLayout import android.widget.TextView -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.google.android.material.button.MaterialButton -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText -import com.google.android.material.textfield.TextInputLayout class SettingsActivity : AppCompatActivity() { - private val phoneRegex = Regex("""^\+?[0-9][0-9 .\-]{5,16}$""") private lateinit var groupsList: LinearLayout override fun onCreate(savedInstanceState: Bundle?) { @@ -36,8 +32,13 @@ class SettingsActivity : AppCompatActivity() { }) findViewById(R.id.backButton).setOnClickListener { finish() } - findViewById(R.id.addGroupButton).setOnClickListener { showGroupDialog(null) } + findViewById(R.id.addGroupButton).setOnClickListener { + startActivity(Intent(this, EditGroupActivity::class.java)) + } + } + override fun onResume() { + super.onResume() renderGroups() } @@ -63,76 +64,14 @@ class SettingsActivity : AppCompatActivity() { g.name } item.findViewById(R.id.groupName).text = title - item.findViewById(R.id.groupPhones).text = g.phones.joinToString(", ") - item.setOnClickListener { showGroupDialog(g) } + item.findViewById(R.id.groupPhones).text = g.displayList() + item.setOnClickListener { + startActivity( + Intent(this, EditGroupActivity::class.java) + .putExtra(EditGroupActivity.EXTRA_GROUP, g.name) + ) + } groupsList.addView(item) } } - - private fun showGroupDialog(existing: Group?) { - val view = layoutInflater.inflate(R.layout.dialog_group, null) - val nameLayout = view.findViewById(R.id.groupNameLayout) - val nameInput = view.findViewById(R.id.groupNameInput) - val phonesLayout = view.findViewById(R.id.groupPhonesLayout) - val phonesInput = view.findViewById(R.id.groupPhonesInput) - - if (existing != null) { - nameInput.setText(existing.name) - phonesInput.setText(existing.phones.joinToString(";")) - } - - val builder = MaterialAlertDialogBuilder(this) - .setTitle(if (existing == null) R.string.group_new else R.string.group_edit) - .setView(view) - .setPositiveButton(R.string.save, null) - .setNegativeButton(android.R.string.cancel, null) - if (existing != null) builder.setNeutralButton(R.string.delete, null) - - val dialog = builder.create() - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { - val name = nameInput.text.toString().trim() - val phones = phonesInput.text.toString() - .split(";").map { it.trim() }.filter { it.isNotEmpty() } - nameLayout.error = null - phonesLayout.error = null - - val groups = Prefs.getGroups(this) - when { - name.isEmpty() -> - nameLayout.error = getString(R.string.error_no_group_name) - groups.any { it.name == name && it.name != existing?.name } -> - nameLayout.error = getString(R.string.error_dup_group) - phones.isEmpty() -> - phonesLayout.error = getString(R.string.error_no_phone) - phones.any { !phoneRegex.matches(it) } -> - phonesLayout.error = getString( - R.string.error_bad_phone, - phones.first { !phoneRegex.matches(it) } - ) - else -> { - val idx = groups.indexOfFirst { it.name == existing?.name } - if (idx >= 0) groups[idx] = Group(name, phones) - else groups.add(Group(name, phones)) - Prefs.saveGroups(this, groups) - if (existing != null && Prefs.getActiveGroupName(this) == existing.name) { - Prefs.setActiveGroupName(this, name) - } - renderGroups() - dialog.dismiss() - } - } - } - if (existing != null) { - dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { - val groups = Prefs.getGroups(this) - groups.removeAll { it.name == existing.name } - Prefs.saveGroups(this, groups) - renderGroups() - dialog.dismiss() - } - } - } - dialog.show() - } } diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000..b018863 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_edit_group.xml b/app/src/main/res/layout/activity_edit_group.xml new file mode 100644 index 0000000..4fc2d32 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_group.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_group.xml b/app/src/main/res/layout/dialog_recipient.xml similarity index 72% rename from app/src/main/res/layout/dialog_group.xml rename to app/src/main/res/layout/dialog_recipient.xml index ba885af..54cc784 100644 --- a/app/src/main/res/layout/dialog_group.xml +++ b/app/src/main/res/layout/dialog_recipient.xml @@ -1,6 +1,5 @@ + android:hint="@string/recipient_name_hint"> + android:inputType="textPersonName" + android:maxLength="40" /> + android:hint="@string/recipient_phone_hint"> + android:inputType="phone" /> diff --git a/app/src/main/res/layout/item_recipient.xml b/app/src/main/res/layout/item_recipient.xml new file mode 100644 index 0000000..d370541 --- /dev/null +++ b/app/src/main/res/layout/item_recipient.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 66c77ee..e83bdf1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,6 +19,20 @@ + Ajouter un groupe Aucun groupe pour l\'instant. Supprimer + Supprimer ce groupe + Supprimer le groupe « %1$s » ? + Destinataires du groupe + Aucun destinataire pour l\'instant. + 📇 Depuis le répertoire + ✏️ Saisie manuelle + Ajouter un destinataire + Modifier le destinataire + Nom (ex : Maman) + Numéro de téléphone + Sans nom + Ce numéro est déjà dans le groupe + Ajoutez au moins un destinataire + Ce contact n\'a pas de numéro Donnez un nom au groupe Un groupe porte déjà ce nom @@ -41,10 +55,6 @@ Votre nom Apparaîtra dans le SMS envoyé - Numéros destinataires - Plusieurs numéros : séparez-les par un point-virgule. Ex : 0611223344;0622334455 Enregistrer - Paramètres enregistrés - Saisissez au moins un numéro Numéro invalide : %1$s