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:
parent
29bbae5823
commit
3bd72ccb36
13 changed files with 544 additions and 138 deletions
|
|
@ -13,8 +13,8 @@ android {
|
||||||
applicationId = "com.example.maposition"
|
applicationId = "com.example.maposition"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 1
|
versionCode = 2
|
||||||
versionName = "1.0"
|
versionName = "1.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Ma Position"
|
android:label="@string/app_name"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.AppCompat.Light"
|
android:theme="@style/Theme.MaPosition"
|
||||||
>
|
>
|
||||||
|
|
||||||
<activity android:name=".SettingsActivity" />
|
<activity android:name=".SettingsActivity" />
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,300 @@
|
||||||
package com.example.maposition
|
package com.example.maposition
|
||||||
|
|
||||||
import android.Manifest
|
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.content.pm.PackageManager
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.os.SystemClock
|
||||||
import android.telephony.SmsManager
|
import android.telephony.SmsManager
|
||||||
import android.widget.Button
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.ImageButton
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.google.android.gms.location.LocationCallback
|
import com.google.android.gms.location.FusedLocationProviderClient
|
||||||
import com.google.android.gms.location.LocationRequest
|
|
||||||
import com.google.android.gms.location.LocationResult
|
|
||||||
import com.google.android.gms.location.LocationServices
|
import com.google.android.gms.location.LocationServices
|
||||||
import com.google.android.gms.location.Priority
|
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() {
|
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(
|
private val permissions = arrayOf(
|
||||||
Manifest.permission.SEND_SMS,
|
Manifest.permission.SEND_SMS,
|
||||||
Manifest.permission.ACCESS_FINE_LOCATION
|
Manifest.permission.ACCESS_FINE_LOCATION
|
||||||
)
|
)
|
||||||
|
|
||||||
private val REQUEST_ALL = 1
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
private lateinit var sendButton: MaterialButton
|
||||||
super.onCreate(savedInstanceState)
|
private lateinit var statusText: TextView
|
||||||
setContentView(R.layout.activity_main)
|
private lateinit var progress: CircularProgressIndicator
|
||||||
title = "Paramètres ----> "
|
private lateinit var recipientsText: TextView
|
||||||
val sendButton = findViewById<Button>(R.id.sendButton)
|
|
||||||
|
|
||||||
sendButton.setOnClickListener {
|
// État d'un envoi en cours
|
||||||
if (!hasPermissions()) {
|
private var busy = false
|
||||||
ActivityCompat.requestPermissions(this, permissions, REQUEST_ALL)
|
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 {
|
} else {
|
||||||
getPreciseLocationAndSend()
|
setStatus(getString(R.string.status_sending_progress, receivedReports, expectedReports))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hasPermissions(): Boolean {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
return permissions.all { perm ->
|
super.onCreate(savedInstanceState)
|
||||||
ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED
|
setContentView(R.layout.activity_main)
|
||||||
}
|
|
||||||
|
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() }
|
||||||
|
}
|
||||||
|
|
||||||
|
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>> {
|
private fun getSavedSettings(): Pair<String, List<String>> {
|
||||||
val prefs = getSharedPreferences("MaPositionPrefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("MaPositionPrefs", MODE_PRIVATE)
|
||||||
val name = prefs.getString("name", "") ?: ""
|
val name = prefs.getString("name", "") ?: ""
|
||||||
val phones = prefs.getString("phones", "") ?: ""
|
val phones = prefs.getString("phones", "") ?: ""
|
||||||
|
|
||||||
val phoneList = phones.split(";").map { it.trim() }.filter { it.isNotEmpty() }
|
val phoneList = phones.split(";").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
return Pair(name, phoneList)
|
return Pair(name, phoneList)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPreciseLocationAndSend() {
|
private fun refreshRecipientsCard() {
|
||||||
val fusedClient = LocationServices.getFusedLocationProviderClient(this)
|
val (name, phones) = getSavedSettings()
|
||||||
|
recipientsText.text = if (phones.isEmpty()) {
|
||||||
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 2000)
|
getString(R.string.recipients_none)
|
||||||
.setMaxUpdates(1)
|
} else {
|
||||||
.build()
|
val who = if (name.isBlank()) "" else "$name → "
|
||||||
|
who + phones.joinToString(", ")
|
||||||
fusedClient.requestLocationUpdates(request, object : LocationCallback() {
|
}
|
||||||
override fun onLocationResult(result: LocationResult) {
|
|
||||||
val location: Location? = result.lastLocation
|
|
||||||
if (location != null) {
|
|
||||||
sendSmsWithLocation(location)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(applicationContext, "Impossible d'obtenir la localisation", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
fusedClient.removeLocationUpdates(this)
|
|
||||||
}
|
|
||||||
}, mainLooper)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendSmsWithLocation(location: Location) {
|
private fun startSendFlow() {
|
||||||
val (name, phoneList) = getSavedSettings()
|
if (busy) return
|
||||||
|
val (_, phones) = getSavedSettings()
|
||||||
val lat = location.latitude
|
if (phones.isEmpty()) {
|
||||||
val lon = location.longitude
|
setStatus(getString(R.string.status_no_recipients))
|
||||||
val link = "https://maps.google.com/?q=$lat,$lon"
|
openSettings()
|
||||||
val message = "Position de $name : $link"
|
|
||||||
|
|
||||||
if (phoneList.isEmpty()) {
|
|
||||||
Toast.makeText(this, "Aucun numéro enregistré", Toast.LENGTH_SHORT).show()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!hasPermissions()) {
|
||||||
val sms = SmsManager.getDefault()
|
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS)
|
||||||
phoneList.forEach { num ->
|
} else {
|
||||||
sms.sendTextMessage(num, null, message, null, null)
|
locateAndSend()
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.makeText(this, "Localisation envoyée", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: android.view.Menu?): Boolean {
|
private fun hasPermissions(): Boolean = permissions.all { perm ->
|
||||||
menuInflater.inflate(R.menu.main_menu, menu)
|
ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: android.view.MenuItem): Boolean {
|
override fun onRequestPermissionsResult(
|
||||||
if (item.itemId == R.id.menu_settings) {
|
requestCode: Int, perms: Array<out String>, results: IntArray
|
||||||
startActivity(android.content.Intent(this, SettingsActivity::class.java))
|
) {
|
||||||
return true
|
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))
|
||||||
}
|
}
|
||||||
return super.onOptionsItemSelected(item)
|
}
|
||||||
|
|
||||||
|
// --- 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
app/src/main/java/com/example/maposition/SettingsActivity.kt
Normal file
57
app/src/main/java/com/example/maposition/SettingsActivity.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
9
app/src/main/res/drawable/ic_back.xml
Normal file
9
app/src/main/res/drawable/ic_back.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/ic_pin.xml
Normal file
9
app/src/main/res/drawable/ic_pin.xml
Normal 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>
|
||||||
9
app/src/main/res/drawable/ic_settings.xml
Normal file
9
app/src/main/res/drawable/ic_settings.xml
Normal 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>
|
||||||
|
|
@ -1,14 +1,108 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:orientation="vertical"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:padding="20dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:layout_width="match_parent"
|
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: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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"
|
||||||
</LinearLayout>
|
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>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,76 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:orientation="vertical"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:padding="20dp"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
<EditText
|
<LinearLayout
|
||||||
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)"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="10dp"/>
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<Button
|
<ImageButton
|
||||||
android:id="@+id/saveButton"
|
android:id="@+id/backButton"
|
||||||
android:text="Enregistrer"
|
android:layout_width="48dp"
|
||||||
android:layout_marginTop="20dp"
|
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_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>
|
</LinearLayout>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -2,4 +2,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
<color name="brand_blue">#1A73E8</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,3 +1,36 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">MaPosition</string>
|
<string name="app_name">Ma Position</string>
|
||||||
</resources>
|
<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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue