diff --git a/src/SmsSyncAndroid/.gitignore b/src/SmsSyncAndroid/.gitignore deleted file mode 100644 index aa724b7..0000000 --- a/src/SmsSyncAndroid/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/src/SmsSyncAndroid/.idea/.gitignore b/src/SmsSyncAndroid/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/src/SmsSyncAndroid/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/src/SmsSyncAndroid/.idea/AndroidProjectSystem.xml b/src/SmsSyncAndroid/.idea/AndroidProjectSystem.xml deleted file mode 100644 index 4a53bee..0000000 --- a/src/SmsSyncAndroid/.idea/AndroidProjectSystem.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/codeStyles/Project.xml b/src/SmsSyncAndroid/.idea/codeStyles/Project.xml deleted file mode 100644 index 7643783..0000000 --- a/src/SmsSyncAndroid/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/codeStyles/codeStyleConfig.xml b/src/SmsSyncAndroid/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/src/SmsSyncAndroid/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/compiler.xml b/src/SmsSyncAndroid/.idea/compiler.xml deleted file mode 100644 index b589d56..0000000 --- a/src/SmsSyncAndroid/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/deploymentTargetSelector.xml b/src/SmsSyncAndroid/.idea/deploymentTargetSelector.xml deleted file mode 100644 index ca16a99..0000000 --- a/src/SmsSyncAndroid/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/gradle.xml b/src/SmsSyncAndroid/.idea/gradle.xml deleted file mode 100644 index 97f0a8e..0000000 --- a/src/SmsSyncAndroid/.idea/gradle.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/migrations.xml b/src/SmsSyncAndroid/.idea/migrations.xml deleted file mode 100644 index f8051a6..0000000 --- a/src/SmsSyncAndroid/.idea/migrations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/misc.xml b/src/SmsSyncAndroid/.idea/misc.xml deleted file mode 100644 index 3b0be22..0000000 --- a/src/SmsSyncAndroid/.idea/misc.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/.idea/runConfigurations.xml b/src/SmsSyncAndroid/.idea/runConfigurations.xml deleted file mode 100644 index 16660f1..0000000 --- a/src/SmsSyncAndroid/.idea/runConfigurations.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/SmsSyncAndroid/app/.gitignore b/src/SmsSyncAndroid/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/src/SmsSyncAndroid/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/src/SmsSyncAndroid/app/build.gradle.kts b/src/SmsSyncAndroid/app/build.gradle.kts deleted file mode 100644 index 272314d..0000000 --- a/src/SmsSyncAndroid/app/build.gradle.kts +++ /dev/null @@ -1,91 +0,0 @@ -plugins { - alias(libs.plugins.android.application) - alias(libs.plugins.compose.compiler) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "com.example.smssyncandroid" - compileSdk = 36 - defaultConfig { - applicationId = "com.example.smssyncandroid" - minSdk = 24 - targetSdk = 36 - versionCode = 1 - versionName = "1.0" - } - - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - buildFeatures { - compose = true - aidl = false - buildConfig = false - shaders = false - } - - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - } -} - -kotlin { - jvmToolchain(17) -} - -dependencies { - val composeBom = platform(libs.androidx.compose.bom) - implementation(composeBom) - androidTestImplementation(composeBom) - - // Core Android dependencies - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.activity.compose) - - // Arch Components - implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.androidx.lifecycle.viewmodel.compose) - - // Compose - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.compose.material3) - // Tooling - debugImplementation(libs.androidx.compose.ui.tooling) - // Instrumented tests - androidTestImplementation(libs.androidx.compose.ui.test.junit4) - debugImplementation(libs.androidx.compose.ui.test.manifest) - - // Local tests: jUnit, coroutines, Android runner - testImplementation(libs.junit) - testImplementation(libs.kotlinx.coroutines.test) - - // Instrumented tests: jUnit rules and runners - androidTestImplementation(libs.androidx.test.core) - androidTestImplementation(libs.androidx.test.ext.junit) - androidTestImplementation(libs.androidx.test.runner) - androidTestImplementation(libs.androidx.test.espresso.core) - - // Navigation - implementation(libs.androidx.navigation3.ui) - implementation(libs.androidx.navigation3.runtime) - implementation(libs.androidx.lifecycle.viewmodel.navigation3) - - // OkHttp pour les WebSockets - implementation("com.squareup.okhttp3:okhttp:4.12.0") - - // Serialization & Compose Icons - implementation(libs.kotlinx.serialization.json) - implementation("androidx.compose.material:material-icons-core") -} diff --git a/src/SmsSyncAndroid/app/src/androidTest/java/com/example/smssyncandroid/ui/main/MainScreenTest.kt b/src/SmsSyncAndroid/app/src/androidTest/java/com/example/smssyncandroid/ui/main/MainScreenTest.kt deleted file mode 100644 index ebd9ccc..0000000 --- a/src/SmsSyncAndroid/app/src/androidTest/java/com/example/smssyncandroid/ui/main/MainScreenTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.smssyncandroid.ui.main - -import androidx.activity.ComponentActivity -import androidx.compose.ui.test.junit4.createAndroidComposeRule -import androidx.compose.ui.test.onNodeWithText -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -/** UI tests for [com.example.smssyncandroid.ui.main.MainScreen]. */ -class MainScreenTest { - - @get:Rule val composeTestRule = createAndroidComposeRule() - - @Before - fun setup() { - composeTestRule.setContent { MainScreen(FAKE_DATA) } - } - - @Test - fun firstItem_exists() { - FAKE_DATA.forEach { composeTestRule.onNodeWithText("Hello $it!").assertExists() } - } -} - -private val FAKE_DATA = listOf("Sample1", "Sample2", "Sample3") diff --git a/src/SmsSyncAndroid/app/src/main/AndroidManifest.xml b/src/SmsSyncAndroid/app/src/main/AndroidManifest.xml deleted file mode 100644 index cab3897..0000000 --- a/src/SmsSyncAndroid/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/MainActivity.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/MainActivity.kt deleted file mode 100644 index b32e419..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/MainActivity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.smssyncandroid - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.ui.Modifier -import com.example.smssyncandroid.theme.SmsSyncAndroidTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - enableEdgeToEdge() - setContent { - SmsSyncAndroidTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { MainNavigation() } } - } - } -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/Navigation.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/Navigation.kt deleted file mode 100644 index 1819437..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/Navigation.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.smssyncandroid - -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack -import androidx.navigation3.ui.NavDisplay -import com.example.smssyncandroid.ui.main.MainScreen - -@Composable -fun MainNavigation() { - val backStack = rememberNavBackStack(Main) - - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - entryProvider = - entryProvider { - entry
{ - MainScreen(onItemClick = { navKey -> backStack.add(navKey) }, modifier = Modifier.safeDrawingPadding().padding(16.dp)) - } - }, - ) -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/NavigationKeys.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/NavigationKeys.kt deleted file mode 100644 index da8de8f..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/NavigationKeys.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.smssyncandroid - -import androidx.navigation3.runtime.NavKey -import kotlinx.serialization.Serializable - -@Serializable data object Main : NavKey diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/data/DataRepository.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/data/DataRepository.kt deleted file mode 100644 index 1f4ea52..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/data/DataRepository.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.smssyncandroid.data - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -interface DataRepository { - val data: Flow> -} - -class DefaultDataRepository : DataRepository { - override val data: Flow> = flow { emit(listOf("Android")) } -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/models/Models.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/models/Models.kt deleted file mode 100644 index 21f48ca..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/models/Models.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.smssyncandroid.models - -import kotlinx.serialization.Serializable - -@Serializable -data class WebSocketMessage( - val Type: String, - val Payload: String -) - -@Serializable -data class SmsMessage( - val Id: String, - val Address: String, - val ContactName: String?, - val Body: String, - val DateTimestamp: Long, - val Type: Int, // 1 = INBOX, 2 = SENT - val IsSynced: Boolean, - val AttachmentBase64: String? = null, - val AttachmentMimeType: String? = null -) - -@Serializable -data class SendSmsPayload( - val Address: String, - val Body: String, - val RequestId: String -) - -@Serializable -data class SendSmsStatusPayload( - val RequestId: String, - val Success: Boolean, - val ErrorMessage: String? -) - -@Serializable -data class FavoriteContact( - val Name: String, - val Number: String -) diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/receivers/SmsReceiver.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/receivers/SmsReceiver.kt deleted file mode 100644 index 414e16e..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/receivers/SmsReceiver.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.smssyncandroid.receivers - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.provider.Telephony -import android.util.Log -import com.example.smssyncandroid.services.SmsSyncService - -class SmsReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) { - val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent) - for (sms in messages) { - val address = sms.displayOriginatingAddress ?: continue - val body = sms.displayMessageBody ?: "" - val date = sms.timestampMillis - - Log.d("SmsReceiver", "SMS reçu de $address : $body") - - // Transmettre le SMS au service de synchronisation - val serviceIntent = Intent(context, SmsSyncService::class.java).apply { - action = SmsSyncService.ACTION_NEW_SMS - putExtra(SmsSyncService.EXTRA_SMS_ADDRESS, address) - putExtra(SmsSyncService.EXTRA_SMS_BODY, body) - putExtra(SmsSyncService.EXTRA_SMS_DATE, date) - } - - try { - context.startForegroundService(serviceIntent) - } catch (e: Exception) { - Log.e("SmsReceiver", "Impossible de démarrer le service en premier plan: ${e.message}") - try { - context.startService(serviceIntent) - } catch (ex: Exception) { - Log.e("SmsReceiver", "Échec total du démarrage du service: ${ex.message}") - } - } - } - } - } -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/services/SmsNotificationListener.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/services/SmsNotificationListener.kt deleted file mode 100644 index b16299c..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/services/SmsNotificationListener.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.smssyncandroid.services - -import android.app.Notification -import android.content.Context -import android.content.Intent -import android.database.Cursor -import android.os.Bundle -import android.os.Parcelable -import android.provider.ContactsContract -import android.service.notification.NotificationListenerService -import android.service.notification.StatusBarNotification -import android.util.Log - -class SmsNotificationListener : NotificationListenerService() { - - override fun onNotificationPosted(sbn: StatusBarNotification) { - val packageName = sbn.packageName - if (packageName != "com.google.android.apps.messaging") return - - val extras = sbn.notification.extras ?: return - val title = extras.getString(Notification.EXTRA_TITLE) ?: "" - val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: "" - - if (title.isEmpty() || text.isEmpty()) return - - Log.d("SmsNotificationListener", "Notification reçue de Google Messages. Expéditeur: $title, Message: $text") - - // Tenter d'extraire le numéro de téléphone depuis la notification (EXTRA_MESSAGING_PERSON) - var address: String? = null - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) { - val person = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON) - person?.uri?.let { uriStr -> - if (uriStr.startsWith("tel:")) { - address = uriStr.removePrefix("tel:") - } - } - } - - // Si non trouvé, chercher le numéro dans les contacts à partir du nom d'affichage - if (address.isNullOrEmpty()) { - address = getPhoneNumberFromName(this, title) - } - - // Si toujours non trouvé, utiliser le titre directement (qui peut être un numéro brut) - if (address.isNullOrEmpty()) { - address = title - } - - // Nettoyer l'adresse (enlever les espaces inutiles) - val finalAddress = address!!.trim().replace(" ", "") - - if (finalAddress.isNotEmpty()) { - Log.d("SmsNotificationListener", "Redirection du message RCS de $finalAddress vers SmsSyncService") - val intent = Intent(this, SmsSyncService::class.java).apply { - action = SmsSyncService.ACTION_NEW_SMS - putExtra(SmsSyncService.EXTRA_SMS_ADDRESS, finalAddress) - putExtra(SmsSyncService.EXTRA_SMS_BODY, text) - putExtra(SmsSyncService.EXTRA_SMS_DATE, sbn.postTime) - } - startService(intent) - } - } - - private fun getPhoneNumberFromName(context: Context, contactName: String): String? { - var number: String? = null - val uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI - val projection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER) - val selection = "${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} = ?" - val selectionArgs = arrayOf(contactName) - - var cursor: Cursor? = null - try { - cursor = context.contentResolver.query(uri, projection, selection, selectionArgs, null) - if (cursor != null && cursor.moveToFirst()) { - number = cursor.getString(0) - } - } catch (e: Exception) { - Log.e("SmsNotificationListener", "Erreur lors de la recherche du contact par nom: ${e.message}") - } finally { - cursor?.close() - } - return number - } -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/services/SmsSyncService.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/services/SmsSyncService.kt deleted file mode 100644 index 01c3bf0..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/services/SmsSyncService.kt +++ /dev/null @@ -1,654 +0,0 @@ -package com.example.smssyncandroid.services - -import android.app.* -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.pm.ServiceInfo -import android.database.Cursor -import android.net.Uri -import android.os.Build -import android.os.IBinder -import android.provider.ContactsContract -import android.provider.Telephony -import android.telephony.SmsManager -import android.util.Log -import androidx.core.app.NotificationCompat -import com.example.smssyncandroid.MainActivity -import com.example.smssyncandroid.models.* -import kotlinx.coroutines.* -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import okhttp3.* -import java.math.BigInteger -import java.security.MessageDigest -import java.util.* -import java.util.concurrent.TimeUnit - -class SmsSyncService : Service() { - - private val serviceScope = CoroutineScope(Dispatchers.Default + Job()) - private var webSocket: WebSocket? = null - private val client = OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .build() - - private var serverUrl: String = "" - private var isServiceRunning = false - private var isConnected = false - - companion object { - const val CHANNEL_ID = "SmsSyncChannel" - const val NOTIFICATION_ID = 101 - - const val ACTION_START = "com.example.smssyncandroid.ACTION_START" - const val ACTION_STOP = "com.example.smssyncandroid.ACTION_STOP" - const val ACTION_NEW_SMS = "com.example.smssyncandroid.ACTION_NEW_SMS" - - const val EXTRA_SERVER_IP = "com.example.smssyncandroid.EXTRA_SERVER_IP" - const val EXTRA_SMS_ADDRESS = "com.example.smssyncandroid.EXTRA_SMS_ADDRESS" - const val EXTRA_SMS_BODY = "com.example.smssyncandroid.EXTRA_SMS_BODY" - const val EXTRA_SMS_DATE = "com.example.smssyncandroid.EXTRA_SMS_DATE" - } - - override fun onCreate() { - super.onCreate() - createNotificationChannel() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - if (intent == null) return START_STICKY - - when (intent.action) { - ACTION_START -> { - val ip = intent.getStringExtra(EXTRA_SERVER_IP) ?: "" - if (ip.isNotEmpty()) { - val cleanedIp = ip.removePrefix("ws://").removePrefix("wss://") - val formattedIp = if (!cleanedIp.contains(":")) { - "${ip.trimEnd('/')}:8888" - } else { - ip - } - serverUrl = if (formattedIp.startsWith("ws://")) formattedIp else "ws://$formattedIp" - startForegroundServiceCompat() - connectWebSocket() - } - } - ACTION_STOP -> { - stopWebSocket() - stopSelf() - } - ACTION_NEW_SMS -> { - val address = intent.getStringExtra(EXTRA_SMS_ADDRESS) ?: "" - val body = intent.getStringExtra(EXTRA_SMS_BODY) ?: "" - val date = intent.getLongExtra(EXTRA_SMS_DATE, System.currentTimeMillis()) - - if (address.isNotEmpty() && body.isNotEmpty()) { - sendNewSmsToWebsocket(address, body, date) - } - } - } - - return START_STICKY - } - - private fun startForegroundServiceCompat() { - val notification = createNotification("Connexion au PC en cours...") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground( - NOTIFICATION_ID, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - ) - } else { - startForeground(NOTIFICATION_ID, notification) - } - isServiceRunning = true - } - - private fun createNotification(contentText: String): Notification { - val notificationIntent = Intent(this, MainActivity::class.java) - val pendingIntent = PendingIntent.getActivity( - this, 0, notificationIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0 - ) - - return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("SMS Sync") - .setContentText(contentText) - .setSmallIcon(android.R.drawable.stat_notify_sync) - .setContentIntent(pendingIntent) - .setOngoing(true) - .build() - } - - private fun updateNotification(contentText: String) { - val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.notify(NOTIFICATION_ID, createNotification(contentText)) - } - - private fun connectWebSocket() { - if (serverUrl.isEmpty()) return - - Log.d("SmsSyncService", "Connexion WebSocket vers $serverUrl") - updateNotification("Recherche du PC à l'adresse $serverUrl...") - broadcastStatus(false, "Connexion en cours...") - - val request = Request.Builder().url(serverUrl).build() - webSocket = client.newWebSocket(request, object : WebSocketListener() { - override fun onOpen(webSocket: WebSocket, response: Response) { - isConnected = true - Log.d("SmsSyncService", "WebSocket Connecté !") - updateNotification("Connecté au PC. Synchronisation active.") - broadcastStatus(true, "Connecté au PC") - - // Envoyer un message de bienvenue - sendWsMessage("connect", "Android connecté") - } - - override fun onMessage(webSocket: WebSocket, text: String) { - handleIncomingMessage(text) - } - - override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { - isConnected = false - Log.d("SmsSyncService", "WebSocket fermeture en cours...") - } - - override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { - isConnected = false - Log.e("SmsSyncService", "Échec WebSocket: ${t.message}") - updateNotification("Déconnecté. Tentative de reconnexion...") - broadcastStatus(false, "Déconnecté. Connexion échouée : ${t.message}") - - // Reconnexion automatique après 5 secondes - serviceScope.launch { - delay(5000) - if (isServiceRunning && !isConnected) { - connectWebSocket() - } - } - } - - override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { - isConnected = false - Log.d("SmsSyncService", "WebSocket fermé. Tentative de reconnexion...") - broadcastStatus(false, "Déconnecté. Connexion fermée par l'hôte.") - - // Reconnexion automatique après 5 secondes - serviceScope.launch { - delay(5000) - if (isServiceRunning && !isConnected) { - connectWebSocket() - } - } - } - }) - } - - private fun stopWebSocket() { - webSocket?.close(1000, "Service arrêté") - webSocket = null - isConnected = false - isServiceRunning = false - broadcastStatus(false, "Service arrêté") - } - - private fun handleIncomingMessage(text: String) { - try { - val message = Json.decodeFromString(text) - when (message.Type) { - "request_sync" -> { - serviceScope.launch { - syncSmsHistory() - syncFavoriteContacts() - } - } - "send_sms" -> { - val payload = Json.decodeFromString(message.Payload) - sendSms(payload.Address, payload.Body, payload.RequestId) - } - } - } catch (e: Exception) { - Log.e("SmsSyncService", "Erreur parsing websocket msg: ${e.message}") - } - } - private suspend fun syncSmsHistory() { - if (!isConnected) return - Log.d("SmsSyncService", "Début de la synchronisation de l'historique SMS/MMS") - - try { - val smsList = getSmsHistory() - val mmsList = getMmsHistory() - val allMessages = (smsList + mmsList).sortedByDescending { it.DateTimestamp } - - val totalCount = allMessages.size - Log.d("SmsSyncService", "Total messages trouvés (SMS + MMS) : $totalCount") - - sendWsMessage("sync_start", """{"Total":$totalCount}""") - - var processed = 0 - var currentChunk = mutableListOf() - - for (msg in allMessages) { - // If the message has an image attachment, flush current text chunk first, - // then load the image bytes and send the message individually. - if (msg.AttachmentMimeType != null) { - if (currentChunk.isNotEmpty()) { - sendWsMessage("sms_history", Json.encodeToString(currentChunk)) - currentChunk = mutableListOf() - delay(50) - } - - val mmsId = msg.Id.removePrefix("mms_") - val bytes = getMmsImageBytes(this, mmsId) - val base64 = bytes?.let { - android.util.Base64.encodeToString(it, android.util.Base64.NO_WRAP) - } - - val msgWithImage = msg.copy(AttachmentBase64 = base64) - processed++ - - sendWsMessage("sms_history", Json.encodeToString(listOf(msgWithImage))) - sendWsMessage("sync_progress", """{"Current":$processed,"Total":$totalCount}""") - delay(50) - } else { - currentChunk.add(msg) - processed++ - - if (currentChunk.size >= 200) { - sendWsMessage("sms_history", Json.encodeToString(currentChunk)) - sendWsMessage("sync_progress", """{"Current":$processed,"Total":$totalCount}""") - currentChunk = mutableListOf() - delay(50) - } - } - } - - if (currentChunk.isNotEmpty()) { - sendWsMessage("sms_history", Json.encodeToString(currentChunk)) - } - sendWsMessage("sync_progress", """{"Current":$totalCount,"Total":$totalCount}""") - sendWsMessage("sync_end", "") - - Log.d("SmsSyncService", "Synchronisation de l'historique terminée.") - } catch (e: Exception) { - Log.e("SmsSyncService", "Erreur lors de la synchronisation: ${e.message}") - } - } - - private fun getSmsHistory(): List { - val list = mutableListOf() - val uri = Uri.parse("content://sms/") - val projection = arrayOf("_id", "address", "body", "date", "type") - val contactCache = mutableMapOf() - var cursor: Cursor? = null - try { - cursor = contentResolver.query( - uri, projection, null, null, "date DESC LIMIT 40000" - ) - cursor?.let { - val idIndex = it.getColumnIndexOrThrow("_id") - val addressIndex = it.getColumnIndexOrThrow("address") - val bodyIndex = it.getColumnIndexOrThrow("body") - val dateIndex = it.getColumnIndexOrThrow("date") - val typeIndex = it.getColumnIndexOrThrow("type") - - while (it.moveToNext()) { - val id = it.getString(idIndex) ?: UUID.randomUUID().toString() - val address = it.getString(addressIndex) ?: "" - val body = it.getString(bodyIndex) ?: "" - val date = it.getLong(dateIndex) - val type = it.getInt(typeIndex) - - if (address.isNotEmpty()) { - val contactName = contactCache.getOrPut(address) { - getContactName(this@SmsSyncService, address) - } - list.add( - SmsMessage( - Id = id, - Address = address, - ContactName = contactName, - Body = body, - DateTimestamp = date, - Type = type, - IsSynced = true - ) - ) - } - } - } - } catch (e: Exception) { - Log.e("SmsSyncService", "Erreur lecture SMS: ${e.message}") - } finally { - cursor?.close() - } - return list - } - - private fun getMmsHistory(): List { - val list = mutableListOf() - val uri = Uri.parse("content://mms/") - val projection = arrayOf("_id", "date", "msg_box") - val contactCache = mutableMapOf() - var cursor: Cursor? = null - try { - cursor = contentResolver.query( - uri, projection, null, null, "date DESC LIMIT 10000" - ) - cursor?.let { - val idIndex = it.getColumnIndexOrThrow("_id") - val dateIndex = it.getColumnIndexOrThrow("date") - val msgBoxIndex = it.getColumnIndexOrThrow("msg_box") - - while (it.moveToNext()) { - val mmsId = it.getString(idIndex) ?: continue - val dateSec = it.getLong(dateIndex) - val dateMs = dateSec * 1000 - val msgBox = it.getInt(msgBoxIndex) - - val address = getMmsAddress(this, mmsId, msgBox) - if (address.isEmpty()) continue - - val body = getMmsText(this, mmsId) - val mimeType = getMmsImageMimeType(this, mmsId) - - val contactName = contactCache.getOrPut(address) { - getContactName(this, address) - } - - list.add( - SmsMessage( - Id = "mms_$mmsId", - Address = address, - ContactName = contactName, - Body = body.ifEmpty { "Photo" }, - DateTimestamp = dateMs, - Type = msgBox, - IsSynced = true, - AttachmentBase64 = null, - AttachmentMimeType = mimeType - ) - ) - } - } - } catch (e: Exception) { - Log.e("SmsSyncService", "Erreur lecture MMS: ${e.message}") - } finally { - cursor?.close() - } - return list - } - - private fun getMmsAddress(context: Context, mmsId: String, msgBox: Int): String { - val targetType = if (msgBox == 1) "137" else "151" - val uri = Uri.parse("content://mms/$mmsId/addr") - val cursor = context.contentResolver.query( - uri, arrayOf("address"), "type = ?", arrayOf(targetType), null - ) - cursor?.use { - if (it.moveToFirst()) { - val addr = it.getString(0) - if (addr != null && addr != "insert-address-token") { - return addr - } - } - } - - // Fallback to the other type if the preferred is not found - val fallbackType = if (msgBox == 1) "151" else "137" - val cursorFallback = context.contentResolver.query( - uri, arrayOf("address"), "type = ?", arrayOf(fallbackType), null - ) - cursorFallback?.use { - if (it.moveToFirst()) { - val addr = it.getString(0) - if (addr != null && addr != "insert-address-token") { - return addr - } - } - } - return "" - } - - private fun getMmsText(context: Context, mmsId: String): String { - val uri = Uri.parse("content://mms/part") - val cursor = context.contentResolver.query( - uri, arrayOf("text"), "mid = ? AND ct = 'text/plain'", arrayOf(mmsId), null - ) - cursor?.use { - if (it.moveToFirst()) { - return it.getString(0) ?: "" - } - } - return "" - } - - private fun getMmsImageMimeType(context: Context, mmsId: String): String? { - val uri = Uri.parse("content://mms/part") - val cursor = context.contentResolver.query( - uri, arrayOf("ct"), "mid = ? AND ct LIKE 'image/%'", arrayOf(mmsId), null - ) - cursor?.use { - if (it.moveToFirst()) { - return it.getString(0) - } - } - return null - } - - private fun getMmsImageBytes(context: Context, mmsId: String): ByteArray? { - val uri = Uri.parse("content://mms/part") - val cursor = context.contentResolver.query( - uri, arrayOf("_id"), "mid = ? AND ct LIKE 'image/%'", arrayOf(mmsId), null - ) - cursor?.use { - if (it.moveToFirst()) { - val partId = it.getString(0) - val partUri = Uri.parse("content://mms/part/$partId") - try { - context.contentResolver.openInputStream(partUri)?.use { inputStream -> - return inputStream.readBytes() - } - } catch (e: Exception) { - Log.e("SmsSyncService", "Erreur lecture image MMS part $partId: ${e.message}") - } - } - } - return null - } - - private fun syncFavoriteContacts() { - if (!isConnected) return - Log.d("SmsSyncService", "Début de la synchronisation des contacts favoris") - val favorites = getFavoriteContacts() - if (favorites.isNotEmpty()) { - val jsonPayload = Json.encodeToString(favorites) - sendWsMessage("favorite_contacts", jsonPayload) - Log.d("SmsSyncService", "Liste de ${favorites.size} contacts favoris envoyée.") - } - } - - private fun getFavoriteContacts(): List { - val list = mutableListOf() - val uri = ContactsContract.Contacts.CONTENT_URI - val projection = arrayOf( - ContactsContract.Contacts._ID, - ContactsContract.Contacts.DISPLAY_NAME - ) - val selection = "${ContactsContract.Contacts.STARRED} = 1" - var cursor: Cursor? = null - try { - cursor = contentResolver.query(uri, projection, selection, null, null) - cursor?.let { - val idIndex = it.getColumnIndexOrThrow(ContactsContract.Contacts._ID) - val nameIndex = it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME) - - while (it.moveToNext()) { - val id = it.getString(idIndex) - val name = it.getString(nameIndex) ?: "" - - if (id != null && name.isNotEmpty()) { - // Récupérer le(s) numéro(s) de téléphone - val phoneCursor = contentResolver.query( - ContactsContract.CommonDataKinds.Phone.CONTENT_URI, - arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER), - "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?", - arrayOf(id), - null - ) - phoneCursor?.use { pc -> - val numberIndex = pc.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER) - while (pc.moveToNext()) { - val number = pc.getString(numberIndex) ?: "" - if (number.isNotEmpty()) { - list.add(FavoriteContact(name, number)) - } - } - } - } - } - } - } catch (e: Exception) { - Log.e("SmsSyncService", "Erreur lecture favoris: ${e.message}") - } finally { - cursor?.close() - } - return list - } - - private fun sendNewSmsToWebsocket(address: String, body: String, date: Long) { - val id = generateMd5("$address:$date:$body") - val contactName = getContactName(this, address) - val smsMessage = SmsMessage( - Id = id, - Address = address, - ContactName = contactName, - Body = body, - DateTimestamp = date, - Type = 1, // Reçu - IsSynced = true - ) - val jsonPayload = Json.encodeToString(smsMessage) - sendWsMessage("new_sms", jsonPayload) - } - - private fun sendSms(address: String, body: String, requestId: String) { - try { - val smsManager = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - getSystemService(SmsManager::class.java) - } else { - @Suppress("DEPRECATION") - SmsManager.getDefault() - } - - val sentAction = "com.example.smssyncandroid.SMS_SENT_$requestId" - val sentIntent = PendingIntent.getBroadcast( - this, 0, Intent(sentAction), - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT else PendingIntent.FLAG_ONE_SHOT - ) - - // Enregistrer temporairement un récepteur pour vérifier si le message a été envoyé - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val success = resultCode == Activity.RESULT_OK - val errorMsg = if (success) null else "Code erreur SMS: $resultCode" - - val statusPayload = SendSmsStatusPayload( - RequestId = requestId, - Success = success, - ErrorMessage = errorMsg - ) - - sendWsMessage("send_sms_status", Json.encodeToString(statusPayload)) - - // Désenregistrer le récepteur après réception du statut - unregisterReceiver(this) - } - } - - // Enregistrer le récepteur avec l'action unique - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - registerReceiver(receiver, IntentFilter(sentAction), RECEIVER_EXPORTED) - } else { - @Suppress("UnspecifiedRegisterReceiverFlag") - registerReceiver(receiver, IntentFilter(sentAction)) - } - - smsManager.sendTextMessage(address, null, body, sentIntent, null) - Log.d("SmsSyncService", "Envoi SMS en cours vers $address...") - } catch (e: Exception) { - Log.e("SmsSyncService", "Erreur envoi SMS: ${e.message}") - val statusPayload = SendSmsStatusPayload( - RequestId = requestId, - Success = false, - ErrorMessage = e.message ?: "Erreur inconnue" - ) - sendWsMessage("send_sms_status", Json.encodeToString(statusPayload)) - } - } - - private fun sendWsMessage(type: String, payload: String) { - if (!isConnected || webSocket == null) return - val message = WebSocketMessage(Type = type, Payload = payload) - val json = Json.encodeToString(message) - webSocket?.send(json) - } - - private fun getContactName(context: Context, phoneNumber: String): String? { - val uri = Uri.withAppendedPath( - ContactsContract.PhoneLookup.CONTENT_FILTER_URI, - Uri.encode(phoneNumber) - ) - val projection = arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME) - var cursor: Cursor? = null - try { - cursor = context.contentResolver.query(uri, projection, null, null, null) - if (cursor != null && cursor.moveToFirst()) { - return cursor.getString(0) - } - } catch (e: Exception) { - // Pas de permission ou erreur - } finally { - cursor?.close() - } - return null - } - - private fun generateMd5(input: String): String { - val md = MessageDigest.getInstance("MD5") - return BigInteger(1, md.digest(input.toByteArray())).toString(16).padStart(32, '0') - } - - private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val serviceChannel = NotificationChannel( - CHANNEL_ID, - "Canal SMS Sync Service", - NotificationManager.IMPORTANCE_LOW - ) - val manager = getSystemService(NotificationManager::class.java) - manager.createNotificationChannel(serviceChannel) - } - } - - private fun broadcastStatus(connected: Boolean, message: String) { - val intent = Intent("com.example.smssyncandroid.CONNECTION_STATUS").apply { - putExtra("extra_connected", connected) - putExtra("extra_status", message) - } - sendBroadcast(intent) - } - - override fun onBind(intent: Intent?): IBinder? { - return null - } - - override fun onDestroy() { - super.onDestroy() - stopWebSocket() - serviceScope.cancel() - } -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Color.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Color.kt deleted file mode 100644 index 1d7da84..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.smssyncandroid.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Theme.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Theme.kt deleted file mode 100644 index 143adb1..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Theme.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.smssyncandroid.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -private val DarkColorScheme = darkColorScheme(primary = Purple80, secondary = PurpleGrey80, tertiary = Pink80) - -private val LightColorScheme = - lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40, - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ - ) - -@Composable -fun SmsSyncAndroidTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit, -) { - val colorScheme = - when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - - MaterialTheme(colorScheme = colorScheme, typography = Typography, content = content) -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Type.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Type.kt deleted file mode 100644 index d5cea34..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/theme/Type.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.smssyncandroid.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -// Set of Material typography styles to start with -val Typography = - Typography( - bodyLarge = - TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ - ) diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/ui/main/MainScreen.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/ui/main/MainScreen.kt deleted file mode 100644 index 2057114..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/ui/main/MainScreen.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.example.smssyncandroid.ui.main - -import android.Manifest -import android.content.Intent -import android.provider.Settings -import android.content.pm.PackageManager -import android.os.Build -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation3.runtime.NavKey - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MainScreen( - onItemClick: (NavKey) -> Unit, - modifier: Modifier = Modifier, - viewModel: MainScreenViewModel = viewModel() -) { - val context = LocalContext.current - - val lifecycleOwner = androidx.compose.ui.platform.LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val observer = androidx.lifecycle.LifecycleEventObserver { _, event -> - if (event == androidx.lifecycle.Lifecycle.Event.ON_RESUME) { - viewModel.checkNotificationAccess() - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } - - // Déterminer les permissions requises - val requiredPermissions = remember { - val list = mutableListOf( - Manifest.permission.RECEIVE_SMS, - Manifest.permission.READ_SMS, - Manifest.permission.SEND_SMS, - Manifest.permission.READ_CONTACTS - ) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - list.add(Manifest.permission.POST_NOTIFICATIONS) - } - list.toTypedArray() - } - - var permissionsGranted by remember { - mutableStateOf(requiredPermissions.all { - ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED - }) - } - - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestMultiplePermissions() - ) { result -> - permissionsGranted = result.values.all { it } - } - - Scaffold( - topBar = { - TopAppBar( - title = { Text("SMS Sync Android", fontWeight = FontWeight.Bold) }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) - ) - } - ) { paddingValues -> - Box( - modifier = modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - if (!permissionsGranted) { - // Écran de demande de permissions - PermissionRequestScreen( - onRequestPermissions = { - launcher.launch(requiredPermissions) - } - ) - } else { - // Écran de configuration de la connexion - ConnectionConfigScreen( - viewModel = viewModel - ) - } - } - } -} - -@Composable -fun PermissionRequestScreen(onRequestPermissions: () -> Unit) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) - ) { - Column( - modifier = Modifier.padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(64.dp) - ) - - Text( - text = "Permissions Requises", - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = "Cette application a besoin des permissions SMS (Lecture, Envoi, Réception) et Contacts pour pouvoir synchroniser et envoyer vos messages depuis Windows.", - textAlign = TextAlign.Center, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Button( - onClick = onRequestPermissions, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) { - Text("Accorder les Permissions", fontWeight = FontWeight.Bold) - } - } - } -} - -@Composable -fun ConnectionConfigScreen(viewModel: MainScreenViewModel) { - val statusColor by animateColorAsState( - targetValue = when { - viewModel.isConnected -> Color(0xFF4CAF50) // Vert connecté - viewModel.statusMessage.contains("Erreur") -> Color(0xFFF44336) // Rouge - viewModel.statusMessage.contains("cours") -> Color(0xFFFF9800) // Orange - else -> Color(0xFF9E9E9E) // Gris - }, - label = "statusColor" - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Card de statut - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) - ) { - Row( - modifier = Modifier - .padding(20.dp) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - Box( - modifier = Modifier - .size(16.dp) - .clip(CircleShape) - .background(statusColor) - ) - - Column { - Text( - text = if (viewModel.isConnected) "Connecté au PC" else "Déconnecté", - fontWeight = FontWeight.Bold, - fontSize = 16.sp - ) - Text( - text = viewModel.statusMessage, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f) - ) - } - } - } - - // Alerte Accès aux notifications pour les messages RCS - if (!viewModel.isNotificationAccessGranted) { - val context = LocalContext.current - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer) - ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.onErrorContainer - ) - Text( - text = "Accès aux notifications requis", - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onErrorContainer, - fontSize = 14.sp - ) - } - Text( - text = "Pour synchroniser les messages RCS (Google Messages) en temps réel, l'application doit pouvoir lire ses notifications.", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f) - ) - Button( - onClick = { - try { - val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS) - context.startActivity(intent) - } catch (e: Exception) { - // Fallback - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = Color.White - ), - shape = RoundedCornerShape(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - Text("Accorder l'accès", fontWeight = FontWeight.Bold) - } - } - } - } - - // Card de configuration IP - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp) - ) { - Column( - modifier = Modifier.padding(20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - Text( - text = "Adresse IP du PC", - fontWeight = FontWeight.Bold, - fontSize = 16.sp - ) - - OutlinedTextField( - value = viewModel.ipAddress, - onValueChange = { viewModel.updateIpAddress(it) }, - placeholder = { Text("ex: 192.168.1.50:8888") }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - singleLine = true, - enabled = !viewModel.isConnected - ) - - Text( - text = "Indiquez l'IP et le port affichés sur l'application de bureau Windows pour démarrer la synchronisation locale.", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - } - } - - // Boutons de commande - if (!viewModel.isConnected && !viewModel.statusMessage.contains("cours")) { - Button( - onClick = { viewModel.startSyncService() }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - shape = RoundedCornerShape(12.dp) - ) { - Icon(Icons.Default.CheckCircle, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text("Démarrer la Synchronisation", fontWeight = FontWeight.Bold, fontSize = 16.sp) - } - } else { - Button( - onClick = { viewModel.stopSyncService() }, - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) - ) { - Icon(Icons.Default.Refresh, contentDescription = null) - Spacer(modifier = Modifier.width(8.dp)) - Text("Arrêter la Synchronisation", fontWeight = FontWeight.Bold, fontSize = 16.sp) - } - } - - Row( - modifier = Modifier.padding(top = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(16.dp) - ) - Text( - text = "Les deux appareils doivent être connectés au même Wi-Fi.", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) - ) - } - } -} diff --git a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/ui/main/MainScreenViewModel.kt b/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/ui/main/MainScreenViewModel.kt deleted file mode 100644 index ba0ec1f..0000000 --- a/src/SmsSyncAndroid/app/src/main/java/com/example/smssyncandroid/ui/main/MainScreenViewModel.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.example.smssyncandroid.ui.main - -import android.app.Application -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.Build -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.AndroidViewModel -import com.example.smssyncandroid.services.SmsSyncService - -class MainScreenViewModel(application: Application) : AndroidViewModel(application) { - - var ipAddress by mutableStateOf("") - private set - - var isConnected by mutableStateOf(false) - private set - - var statusMessage by mutableStateOf("Service arrêté. Entrez l'IP du PC.") - private set - - var isNotificationAccessGranted by mutableStateOf(false) - private set - - private val sharedPrefs = application.getSharedPreferences("SmsSyncPrefs", Context.MODE_PRIVATE) - - private val connectionStatusReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - intent?.let { - isConnected = it.getBooleanExtra("extra_connected", false) - statusMessage = it.getStringExtra("extra_status") ?: "" - } - } - } - - init { - ipAddress = sharedPrefs.getString("server_ip", "") ?: "" - checkNotificationAccess() - - val filter = IntentFilter("com.example.smssyncandroid.CONNECTION_STATUS") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - application.registerReceiver(connectionStatusReceiver, filter, Context.RECEIVER_EXPORTED) - } else { - @Suppress("UnspecifiedRegisterReceiverFlag") - application.registerReceiver(connectionStatusReceiver, filter) - } - } - - fun checkNotificationAccess() { - val context = getApplication() - val pkgName = context.packageName - val flat = android.provider.Settings.Secure.getString(context.contentResolver, "enabled_notification_listeners") - isNotificationAccessGranted = if (!flat.isNullOrEmpty()) { - flat.split(":").any { - val cn = android.content.ComponentName.unflattenFromString(it) - cn != null && cn.packageName == pkgName - } - } else { - false - } - } - - fun updateIpAddress(newIp: String) { - ipAddress = newIp - sharedPrefs.edit().putString("server_ip", newIp).apply() - } - - fun startSyncService() { - if (ipAddress.isBlank()) { - statusMessage = "Erreur: L'adresse IP ne peut pas être vide." - return - } - - val intent = Intent(getApplication(), SmsSyncService::class.java).apply { - action = SmsSyncService.ACTION_START - putExtra(SmsSyncService.EXTRA_SERVER_IP, ipAddress) - } - - try { - getApplication().startForegroundService(intent) - statusMessage = "Démarrage du service..." - } catch (e: Exception) { - statusMessage = "Erreur de démarrage : ${e.message}" - } - } - - fun stopSyncService() { - val intent = Intent(getApplication(), SmsSyncService::class.java).apply { - action = SmsSyncService.ACTION_STOP - } - getApplication().startService(intent) - isConnected = false - statusMessage = "Service arrêté." - } - - override fun onCleared() { - super.onCleared() - try { - getApplication().unregisterReceiver(connectionStatusReceiver) - } catch (e: Exception) { - // Déjà désenregistré - } - } -} diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index d4a927c..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index e6db4ed..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 650bf12..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index c784665..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 04687e9..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 828c066..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index de5493d..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index c3581ff..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 35569fc..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/src/SmsSyncAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 5ecc30b..0000000 Binary files a/src/SmsSyncAndroid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/src/SmsSyncAndroid/app/src/main/res/values/strings.xml b/src/SmsSyncAndroid/app/src/main/res/values/strings.xml deleted file mode 100644 index 72b8a70..0000000 --- a/src/SmsSyncAndroid/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - SmsSyncAndroid - \ No newline at end of file diff --git a/src/SmsSyncAndroid/app/src/main/res/values/themes.xml b/src/SmsSyncAndroid/app/src/main/res/values/themes.xml deleted file mode 100644 index 6ac5df3..0000000 --- a/src/SmsSyncAndroid/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - -