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