v1.1 : fiabilisation de l'envoi et refonte de l'interface en Material 3

- Accusés d'envoi SMS réels par destinataire (PendingIntent + BroadcastReceiver) :
  le statut final distingue succès, échecs par numéro et envois non confirmés
- Repli GPS : si pas de position fraîche en 10 s, utilisation de la dernière
  position connue (mention de son âge dans le SMS)
- API SmsManager moderne (getSystemService sur Android 12+, compatible multi-SIM)
- Envois espacés de 500 ms entre destinataires (anti-filtrage opérateur)
- UI Material 3 : gros bouton d'envoi central, indicateur de progression,
  statut détaillé, carte destinataires ; paramètres avec validation des numéros
- Validation des numéros à la saisie, versionCode 2 / versionName 1.1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cosano-star 2026-07-13 14:05:04 +02:00
parent 29bbae5823
commit 3bd72ccb36
13 changed files with 544 additions and 138 deletions

View file

@ -13,8 +13,8 @@ android {
applicationId = "com.example.maposition"
minSdk = 21
targetSdk = 36
versionCode = 1
versionName = "1.0"
versionCode = 2
versionName = "1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View file

@ -7,12 +7,12 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="Ma Position"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="true"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light"
android:theme="@style/Theme.MaPosition"
>
<activity android:name=".SettingsActivity" />

View file

@ -1,111 +1,300 @@
package com.example.maposition
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.Location
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.telephony.SmsManager
import android.widget.Button
import android.widget.Toast
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.tasks.CancellationTokenSource
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.progressindicator.CircularProgressIndicator
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity() {
companion object {
private const val ACTION_SMS_SENT = "com.example.maposition.SMS_SENT"
private const val EXTRA_RECIPIENT = "recipient"
private const val REQUEST_PERMISSIONS = 1
private const val LOCATION_TIMEOUT_MS = 10_000L
private const val SEND_STAGGER_MS = 500L
private const val REPORT_TIMEOUT_MS = 20_000L
}
private val permissions = arrayOf(
Manifest.permission.SEND_SMS,
Manifest.permission.ACCESS_FINE_LOCATION
)
private val REQUEST_ALL = 1
private val handler = Handler(Looper.getMainLooper())
private lateinit var sendButton: MaterialButton
private lateinit var statusText: TextView
private lateinit var progress: CircularProgressIndicator
private lateinit var recipientsText: TextView
// État d'un envoi en cours
private var busy = false
private var expectedReports = 0
private var receivedReports = 0
private var successCount = 0
private val failedNumbers = mutableListOf<String>()
private var reportTimeout: Runnable? = null
private val smsSentReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!busy) return
val num = intent.getStringExtra(EXTRA_RECIPIENT) ?: "?"
receivedReports++
if (resultCode == Activity.RESULT_OK) successCount++ else failedNumbers.add(num)
if (receivedReports >= expectedReports) {
finishSending()
} else {
setStatus(getString(R.string.status_sending_progress, receivedReports, expectedReports))
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
title = "Paramètres ----> "
val sendButton = findViewById<Button>(R.id.sendButton)
sendButton.setOnClickListener {
if (!hasPermissions()) {
ActivityCompat.requestPermissions(this, permissions, REQUEST_ALL)
} else {
getPreciseLocationAndSend()
}
}
sendButton = findViewById(R.id.sendButton)
statusText = findViewById(R.id.statusText)
progress = findViewById(R.id.progress)
recipientsText = findViewById(R.id.recipientsText)
ContextCompat.registerReceiver(
this, smsSentReceiver, IntentFilter(ACTION_SMS_SENT),
ContextCompat.RECEIVER_NOT_EXPORTED
)
sendButton.setOnClickListener { startSendFlow() }
findViewById<ImageButton>(R.id.settingsButton).setOnClickListener { openSettings() }
findViewById<MaterialCardView>(R.id.recipientsCard).setOnClickListener { openSettings() }
}
private fun hasPermissions(): Boolean {
return permissions.all { perm ->
ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED
override fun onResume() {
super.onResume()
refreshRecipientsCard()
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(smsSentReceiver)
handler.removeCallbacksAndMessages(null)
}
private fun openSettings() {
startActivity(Intent(this, SettingsActivity::class.java))
}
private fun getSavedSettings(): Pair<String, List<String>> {
val prefs = getSharedPreferences("MaPositionPrefs", MODE_PRIVATE)
val name = prefs.getString("name", "") ?: ""
val phones = prefs.getString("phones", "") ?: ""
val phoneList = phones.split(";").map { it.trim() }.filter { it.isNotEmpty() }
return Pair(name, phoneList)
}
private fun getPreciseLocationAndSend() {
val fusedClient = LocationServices.getFusedLocationProviderClient(this)
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000)
.setMaxUpdates(1)
.build()
fusedClient.requestLocationUpdates(request, object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
val location: Location? = result.lastLocation
if (location != null) {
sendSmsWithLocation(location)
private fun refreshRecipientsCard() {
val (name, phones) = getSavedSettings()
recipientsText.text = if (phones.isEmpty()) {
getString(R.string.recipients_none)
} else {
Toast.makeText(applicationContext, "Impossible d'obtenir la localisation", Toast.LENGTH_SHORT).show()
val who = if (name.isBlank()) "" else "$name"
who + phones.joinToString(", ")
}
fusedClient.removeLocationUpdates(this)
}
}, mainLooper)
}
private fun sendSmsWithLocation(location: Location) {
val (name, phoneList) = getSavedSettings()
val lat = location.latitude
val lon = location.longitude
val link = "https://maps.google.com/?q=$lat,$lon"
val message = "Position de $name : $link"
if (phoneList.isEmpty()) {
Toast.makeText(this, "Aucun numéro enregistré", Toast.LENGTH_SHORT).show()
private fun startSendFlow() {
if (busy) return
val (_, phones) = getSavedSettings()
if (phones.isEmpty()) {
setStatus(getString(R.string.status_no_recipients))
openSettings()
return
}
val sms = SmsManager.getDefault()
phoneList.forEach { num ->
sms.sendTextMessage(num, null, message, null, null)
if (!hasPermissions()) {
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS)
} else {
locateAndSend()
}
}
Toast.makeText(this, "Localisation envoyée", Toast.LENGTH_SHORT).show()
private fun hasPermissions(): Boolean = permissions.all { perm ->
ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED
}
override fun onCreateOptionsMenu(menu: android.view.Menu?): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
return true
override fun onRequestPermissionsResult(
requestCode: Int, perms: Array<out String>, results: IntArray
) {
super.onRequestPermissionsResult(requestCode, perms, results)
if (requestCode != REQUEST_PERMISSIONS) return
if (results.isNotEmpty() && results.all { it == PackageManager.PERMISSION_GRANTED }) {
locateAndSend()
} else {
setStatus(getString(R.string.status_need_permissions))
}
}
override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean {
if (item.itemId == R.id.menu_settings) {
startActivity(android.content.Intent(this, SettingsActivity::class.java))
return true
// --- Localisation : position fraîche, sinon repli sur la dernière connue ---
@SuppressLint("MissingPermission")
private fun locateAndSend() {
busy = true
resetReportState()
sendButton.isEnabled = false
progress.visibility = View.VISIBLE
setStatus(getString(R.string.status_locating))
val fused = LocationServices.getFusedLocationProviderClient(this)
val cts = CancellationTokenSource()
var settled = false
val timeout = Runnable {
if (!settled) {
settled = true
cts.cancel()
fallbackToLastLocation(fused)
}
return super.onOptionsItemSelected(item)
}
handler.postDelayed(timeout, LOCATION_TIMEOUT_MS)
fused.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, cts.token)
.addOnSuccessListener { loc ->
if (settled) return@addOnSuccessListener
settled = true
handler.removeCallbacks(timeout)
if (loc != null) sendSmsWithLocation(loc, stale = false)
else fallbackToLastLocation(fused)
}
.addOnFailureListener {
if (settled) return@addOnFailureListener
settled = true
handler.removeCallbacks(timeout)
fallbackToLastLocation(fused)
}
}
@SuppressLint("MissingPermission")
private fun fallbackToLastLocation(fused: FusedLocationProviderClient) {
setStatus(getString(R.string.status_fallback))
fused.lastLocation
.addOnSuccessListener { loc ->
if (loc != null) sendSmsWithLocation(loc, stale = true)
else abort(getString(R.string.status_no_location))
}
.addOnFailureListener { abort(getString(R.string.status_no_location)) }
}
// --- Envoi des SMS avec accusé d'envoi et espacement ---
private fun smsManager(): SmsManager =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
getSystemService(SmsManager::class.java)
} else {
@Suppress("DEPRECATION")
SmsManager.getDefault()
}
private fun sendSmsWithLocation(location: Location, stale: Boolean) {
val (name, phones) = getSavedSettings()
val link = "https://maps.google.com/?q=${location.latitude},${location.longitude}"
val ageMin = TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - location.time)
val staleSuffix = if (stale && ageMin >= 2) getString(R.string.msg_stale_suffix, ageMin) else ""
val who = name.ifBlank { getString(R.string.msg_default_name) }.take(40)
val message = getString(R.string.msg_body, who, link) + staleSuffix
expectedReports = phones.size
setStatus(getString(R.string.status_sending_progress, 0, expectedReports))
val piFlags = PendingIntent.FLAG_ONE_SHOT or
(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
val requestBase = (SystemClock.elapsedRealtime() and 0xFFFFF).toInt()
phones.forEachIndexed { i, num ->
handler.postDelayed({
try {
val sentIntent = Intent(ACTION_SMS_SENT)
.setPackage(packageName)
.putExtra(EXTRA_RECIPIENT, num)
val pi = PendingIntent.getBroadcast(this, requestBase + i, sentIntent, piFlags)
smsManager().sendTextMessage(num, null, message, pi, null)
} catch (e: Exception) {
receivedReports++
failedNumbers.add(num)
if (receivedReports >= expectedReports) finishSending()
}
}, i * SEND_STAGGER_MS)
}
// Garde-fou : si l'opérateur ne confirme pas tous les envois, on conclut quand même
reportTimeout = Runnable { finishSending() }.also {
handler.postDelayed(it, phones.size * SEND_STAGGER_MS + REPORT_TIMEOUT_MS)
}
}
private fun finishSending() {
reportTimeout?.let { handler.removeCallbacks(it) }
reportTimeout = null
val unconfirmed = expectedReports - receivedReports
val status = when {
failedNumbers.isNotEmpty() ->
getString(
R.string.status_sent_partial,
successCount, expectedReports, failedNumbers.joinToString(", ")
)
unconfirmed > 0 ->
getString(R.string.status_sent_unconfirmed, successCount, unconfirmed)
else ->
getString(R.string.status_sent_ok, successCount)
}
resetUi()
setStatus(status)
}
private fun abort(message: String) {
resetUi()
setStatus(message)
}
private fun resetUi() {
busy = false
progress.visibility = View.GONE
sendButton.isEnabled = true
}
private fun resetReportState() {
expectedReports = 0
receivedReports = 0
successCount = 0
failedNumbers.clear()
}
private fun setStatus(text: String) {
statusText.text = text
}
}

View file

@ -0,0 +1,57 @@
package com.example.maposition
import android.content.Context
import android.os.Bundle
import android.widget.ImageButton
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
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}$""")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
val prefs = getSharedPreferences("MaPositionPrefs", Context.MODE_PRIVATE)
val nameInput = findViewById<TextInputEditText>(R.id.nameInput)
val phonesLayout = findViewById<TextInputLayout>(R.id.phonesLayout)
val phonesInput = findViewById<TextInputEditText>(R.id.phonesInput)
nameInput.setText(prefs.getString("name", ""))
phonesInput.setText(prefs.getString("phones", ""))
findViewById<ImageButton>(R.id.backButton).setOnClickListener { finish() }
findViewById<MaterialButton>(R.id.saveButton).setOnClickListener {
val rawPhones = phonesInput.text.toString()
val phones = rawPhones.split(";").map { it.trim() }.filter { it.isNotEmpty() }
when {
phones.isEmpty() -> {
phonesLayout.error = getString(R.string.error_no_phone)
return@setOnClickListener
}
phones.any { !phoneRegex.matches(it) } -> {
val bad = phones.first { !phoneRegex.matches(it) }
phonesLayout.error = getString(R.string.error_bad_phone, bad)
return@setOnClickListener
}
}
phonesLayout.error = null
prefs.edit()
.putString("name", nameInput.text.toString().trim())
.putString("phones", phones.joinToString(";"))
.apply()
Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show()
finish()
}
}
}

View file

@ -1,36 +0,0 @@
package com.example.maposition
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
class SettingsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
val prefs = getSharedPreferences("MaPositionPrefs", Context.MODE_PRIVATE)
val nameInput = findViewById<EditText>(R.id.nameInput)
val phonesInput = findViewById<EditText>(R.id.phonesInput)
val saveButton = findViewById<Button>(R.id.saveButton)
// Charger valeurs déjà enregistrées
nameInput.setText(prefs.getString("name", ""))
phonesInput.setText(prefs.getString("phones", ""))
saveButton.setOnClickListener {
prefs.edit()
.putString("name", nameInput.text.toString())
.putString("phones", phonesInput.text.toString())
.apply()
Toast.makeText(this, "Paramètres enregistrés", Toast.LENGTH_SHORT).show()
finish()
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13C19,5.13 15.87,2 12,2zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5S13.38,11.5 12,11.5z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z" />
</vector>

View file

@ -1,14 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="20dp"
android:gravity="center"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:padding="24dp">
<Button
<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/settingsButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/settingsButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/settings_title"
android:src="@drawable/ic_settings"
app:layout_constraintBottom_toBottomOf="@id/titleText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/titleText"
app:tint="?attr/colorOnSurfaceVariant" />
<com.google.android.material.button.MaterialButton
android:id="@+id/sendButton"
android:text="Envoyer ma position"
android:layout_width="240dp"
android:layout_height="240dp"
android:text="@string/send_button"
android:textColor="@color/white"
android:textSize="17sp"
android:textStyle="bold"
app:backgroundTint="@color/brand_blue"
app:cornerRadius="120dp"
app:icon="@drawable/ic_pin"
app:iconGravity="top"
app:iconSize="44dp"
app:iconTint="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.38" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="@color/brand_blue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sendButton" />
<TextView
android:id="@+id/statusText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progress" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/recipientsCard"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
app:cardCornerRadius="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/recipients_label"
android:textSize="13sp"
android:textStyle="bold"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/recipientsText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="15sp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,28 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:padding="24dp">
<EditText
android:id="@+id/nameInput"
android:hint="Votre nom"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/phonesInput"
android:hint="Numéros destinataires (ex: 0611223344;0622334455)"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"/>
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/saveButton"
android:text="Enregistrer"
android:layout_marginTop="20dp"
<ImageButton
android:id="@+id/backButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/back"
android:src="@drawable/ic_back"
app:tint="?attr/colorOnSurface" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/settings_title"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:hint="@string/name_hint"
app:helperText="@string/name_helper">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/nameInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:maxLength="40" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/phonesLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:hint="@string/phones_hint"
app:helperText="@string/phones_helper">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/phonesInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/saveButton"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="32dp"
android:text="@string/save"
android:textSize="16sp"
app:backgroundTint="@color/brand_blue"
app:cornerRadius="16dp" />
</LinearLayout>

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_settings"
android:title="Paramètres"
android:showAsAction="always" />
</menu>

View file

@ -2,4 +2,5 @@
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="brand_blue">#1A73E8</color>
</resources>

View file

@ -1,3 +1,36 @@
<resources>
<string name="app_name">MaPosition</string>
<string name="app_name">Ma Position</string>
<string name="settings_title">Paramètres</string>
<string name="back">Retour</string>
<!-- Écran principal -->
<string name="send_button">ENVOYER\nMA POSITION</string>
<string name="recipients_label">Destinataires — toucher pour modifier</string>
<string name="recipients_none">Aucun destinataire configuré.\nTouchez ici pour en ajouter.</string>
<!-- Statuts -->
<string name="status_locating">Recherche de votre position…</string>
<string name="status_fallback">GPS lent — utilisation de la dernière position connue…</string>
<string name="status_no_location">❌ Position introuvable. Vérifiez que la localisation est activée, puis réessayez.</string>
<string name="status_no_recipients">Ajoutez d\'abord un destinataire.</string>
<string name="status_need_permissions">❌ Les autorisations SMS et localisation sont nécessaires pour envoyer votre position.</string>
<string name="status_sending_progress">Envoi des SMS… (%1$d/%2$d)</string>
<string name="status_sent_ok">✅ Position envoyée à %1$d destinataire(s)</string>
<string name="status_sent_partial">⚠️ Envoyé à %1$d destinataire(s) sur %2$d.\nÉchec pour : %3$s</string>
<string name="status_sent_unconfirmed">✅ %1$d envoyé(s) — %2$d en attente de confirmation de l\'opérateur</string>
<!-- Message SMS -->
<string name="msg_body">Position de %1$s : %2$s</string>
<string name="msg_default_name">moi</string>
<string name="msg_stale_suffix">" (dernière position connue, il y a %1$d min)"</string>
<!-- Paramètres -->
<string name="name_hint">Votre nom</string>
<string name="name_helper">Apparaîtra dans le SMS envoyé</string>
<string name="phones_hint">Numéros destinataires</string>
<string name="phones_helper">Plusieurs numéros : séparez-les par un point-virgule. Ex : 0611223344;0622334455</string>
<string name="save">Enregistrer</string>
<string name="saved">Paramètres enregistrés</string>
<string name="error_no_phone">Saisissez au moins un numéro</string>
<string name="error_bad_phone">Numéro invalide : %1$s</string>
</resources>