diff --git a/.gitignore b/.gitignore index ca775f0..aa852fd 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ app.*.map.json /android/app/profile /android/app/release android/.gradle/ +/.gradle/ android/local.properties android/**/gradle-wrapper.jar android/key.properties diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 2d74980..f2a405c 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -28,7 +28,7 @@ check(sharedSigningStoreFile.isFile) { } android { - namespace = "com.gametime.app" + namespace = "com.idea.gametime" compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion @@ -38,8 +38,8 @@ android { } defaultConfig { - applicationId = "com.gametime.app" - minSdk = flutter.minSdkVersion + applicationId = "com.idea.gametime" + minSdk = 26 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName @@ -72,5 +72,7 @@ flutter { } dependencies { + implementation("androidx.health.connect:connect-client:1.1.0") implementation("com.google.android.gms:play-services-wearable:19.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2") } diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 399f698..b04c115 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,6 @@ to allow setting breakpoints, to provide hot reload, etc. --> + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c2344a4..3e1adb5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,12 +6,15 @@ + + + + + android:icon="@mipmap/ic_launcher"> @@ -37,6 +40,23 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/gametime/app/MainActivity.kt b/android/app/src/main/kotlin/com/gametime/app/MainActivity.kt deleted file mode 100644 index 89dbc3a..0000000 --- a/android/app/src/main/kotlin/com/gametime/app/MainActivity.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.gametime.app - -import com.gametime.app.session.SessionNotificationPlugin -import com.gametime.app.watch.WatchBridgePlugin -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine - -class MainActivity : FlutterActivity() { - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - WatchBridgePlugin.register(flutterEngine, applicationContext) - SessionNotificationPlugin.register(flutterEngine, this) - } -} diff --git a/android/app/src/main/kotlin/com/idea/gametime/MainActivity.kt b/android/app/src/main/kotlin/com/idea/gametime/MainActivity.kt new file mode 100644 index 0000000..0c1d5ca --- /dev/null +++ b/android/app/src/main/kotlin/com/idea/gametime/MainActivity.kt @@ -0,0 +1,22 @@ +package com.idea.gametime + +import android.os.Bundle +import com.idea.gametime.health.HealthConnectPlugin +import com.idea.gametime.session.SessionNotificationPlugin +import com.idea.gametime.watch.WatchBridgePlugin +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine + +class MainActivity : FlutterFragmentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + HealthConnectPlugin.prepare(this) + super.onCreate(savedInstanceState) + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + WatchBridgePlugin.register(flutterEngine, applicationContext) + SessionNotificationPlugin.register(flutterEngine, this) + HealthConnectPlugin.register(flutterEngine, this) + } +} diff --git a/android/app/src/main/kotlin/com/idea/gametime/health/HealthConnectPlugin.kt b/android/app/src/main/kotlin/com/idea/gametime/health/HealthConnectPlugin.kt new file mode 100644 index 0000000..085d86f --- /dev/null +++ b/android/app/src/main/kotlin/com/idea/gametime/health/HealthConnectPlugin.kt @@ -0,0 +1,470 @@ +package com.idea.gametime.health + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.result.ActivityResultLauncher +import androidx.health.connect.client.HealthConnectClient +import androidx.health.connect.client.PermissionController +import androidx.lifecycle.Lifecycle +import androidx.health.connect.client.permission.HealthPermission +import androidx.health.connect.client.records.DistanceRecord +import androidx.health.connect.client.records.ExerciseSessionRecord +import androidx.health.connect.client.records.HeartRateRecord +import androidx.health.connect.client.records.Record +import androidx.health.connect.client.records.TotalCaloriesBurnedRecord +import androidx.health.connect.client.records.metadata.Metadata +import androidx.health.connect.client.units.Energy +import androidx.health.connect.client.units.Length +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.lang.ref.WeakReference +import java.time.Instant +import java.time.ZoneOffset +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object HealthConnectPlugin { + private const val METHOD_CHANNEL = "gametime.health_connect/methods" + private const val LOG_TAG = "GTHealthConnect" + private const val HEALTH_CONNECT_PROVIDER_PACKAGE = "com.google.android.apps.healthdata" + private const val PLAY_STORE_PACKAGE = "com.android.vending" + private const val FALLBACK_OPEN_DELAY_MS = 1200L + + private val requiredPermissions = + setOf( + HealthPermission.getWritePermission(ExerciseSessionRecord::class), + HealthPermission.getWritePermission(DistanceRecord::class), + HealthPermission.getWritePermission(TotalCaloriesBurnedRecord::class), + HealthPermission.getWritePermission(HeartRateRecord::class), + ) + + private var appContext: Context? = null + private var activityRef: WeakReference? = null + private var permissionRequestLauncher: ActivityResultLauncher>? = null + private var pendingPermissionResult: MethodChannel.Result? = null + private var fallbackOpenRunnable: Runnable? = null + private val mainHandler = Handler(Looper.getMainLooper()) + private val mainScope = CoroutineScope(Dispatchers.Main) + + fun prepare(activity: Activity) { + appContext = activity.applicationContext + activityRef = WeakReference(activity) + registerPermissionRequestLauncher(activity) + } + + fun register(flutterEngine: FlutterEngine, activity: Activity) { + appContext = activity.applicationContext + activityRef = WeakReference(activity) + if (permissionRequestLauncher == null) { + registerPermissionRequestLauncher(activity) + } + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL) + .setMethodCallHandler(::handleMethodCall) + } + + private fun registerPermissionRequestLauncher(activity: Activity) { + Log.d(LOG_TAG, "registerPermissionRequestLauncher(activity=${activity::class.java.name})") + val componentActivity = activity as? ComponentActivity + if (componentActivity == null) { + Log.e(LOG_TAG, "Activity is not a ComponentActivity; permission launcher unavailable.") + return + } + try { + val context = activity.applicationContext + permissionRequestLauncher?.unregister() + permissionRequestLauncher = + componentActivity.activityResultRegistry.register( + "gametime_health_connect_permissions", + PermissionController.createRequestPermissionResultContract(), + ) { grantedPermissions -> + Log.d( + LOG_TAG, + "permission request callback(granted=${grantedPermissions.size})", + ) + cancelFallbackOpen() + val result = pendingPermissionResult ?: return@register + pendingPermissionResult = null + mainScope.launch { + val state = try { + connectionStateMap( + context, + grantedPermissions, + permissionRequestOutcome = + if (grantedPermissions.containsAll(requiredPermissions)) { + "granted" + } else { + "denied" + }, + ) + } catch (error: Throwable) { + Log.e(LOG_TAG, "connection state failed after callback.", error) + result.error("health_connect_state_failed", error.message, null) + return@launch + } + result.success(state) + } + } + Log.d(LOG_TAG, "permission launcher registered.") + } catch (error: Throwable) { + Log.e(LOG_TAG, "permission launcher registration failed.", error) + permissionRequestLauncher = null + } + } + + private fun handleMethodCall(call: MethodCall, result: MethodChannel.Result) { + val context = appContext + if (context == null) { + result.error("health_connect_unavailable", "Application context unavailable.", null) + return + } + when (call.method) { + "connectionState" -> connectionState(context, result) + "requestPermissions" -> requestPermissions(context, result) + "openSettings" -> openSettings(context, result) + "exportWorkout" -> exportWorkout(context, call.arguments, result) + else -> result.notImplemented() + } + } + + private fun connectionState(context: Context, result: MethodChannel.Result) { + mainScope.launch { + try { + result.success(connectionStateMap(context)) + } catch (error: Throwable) { + result.error("health_connect_state_failed", error.message, null) + } + } + } + + private suspend fun connectionStateMap( + context: Context, + grantedOverride: Set? = null, + permissionRequestOutcome: String? = null, + ): Map { + var availability = availability(context) + val grantedPermissions = if (availability == "available") { + try { + grantedOverride ?: HealthConnectClient.getOrCreate(context) + .permissionController + .getGrantedPermissions() + } catch (_: SecurityException) { + availability = "accessBlocked" + emptySet() + } catch (_: IllegalStateException) { + availability = "accessBlocked" + emptySet() + } + } else { + emptySet() + } + val state = mutableMapOf( + "availability" to availability, + "requiredPermissions" to requiredPermissions.toList(), + "grantedPermissions" to grantedPermissions.toList(), + ) + if (permissionRequestOutcome != null) { + state["permissionRequestOutcome"] = permissionRequestOutcome + } + return state + } + + private fun availability(context: Context): String { + return when (HealthConnectClient.getSdkStatus(context)) { + HealthConnectClient.SDK_AVAILABLE -> "available" + HealthConnectClient.SDK_UNAVAILABLE_PROVIDER_UPDATE_REQUIRED -> + "providerUpdateRequired" + else -> "unavailable" + } + } + + private fun requestPermissions(context: Context, result: MethodChannel.Result) { + val availability = availability(context) + Log.d(LOG_TAG, "requestPermissions availability=$availability") + if (availability != "available") { + mainScope.launch { + result.success(connectionStateMap(context)) + } + return + } + val activity = activityRef?.get() + if (activity == null) { + Log.e(LOG_TAG, "requestPermissions failed: activity unavailable.") + result.error("health_connect_unavailable", "Activity unavailable.", null) + return + } + if (pendingPermissionResult != null) { + Log.e(LOG_TAG, "requestPermissions failed: request already active.") + result.error( + "health_connect_request_in_progress", + "Permission request already active.", + null, + ) + return + } + pendingPermissionResult = result + val launcher = permissionRequestLauncher + if (launcher == null) { + Log.e(LOG_TAG, "requestPermissions fallback: launcher unavailable.") + openSettingsFallback(context, activity, "settingsFallback") + return + } + try { + Log.d(LOG_TAG, "launching Health Connect permission contract.") + launcher.launch(requiredPermissions) + Log.d(LOG_TAG, "Health Connect permission contract launch returned.") + scheduleFallbackOpen(context, activity) + } catch (error: Throwable) { + Log.e(LOG_TAG, "Health Connect permission contract launch failed.", error) + openSettingsFallback(context, activity, "launchFailed") + } + } + + private fun scheduleFallbackOpen(context: Context, activity: Activity) { + cancelFallbackOpen() + val runnable = Runnable { + if (pendingPermissionResult == null) { + return@Runnable + } + val componentActivity = activity as? ComponentActivity + if (componentActivity != null && + !componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) + ) { + Log.d(LOG_TAG, "permission UI appears to have focus; fallback not opened.") + return@Runnable + } + Log.d(LOG_TAG, "permission UI did not take focus; opening Health Connect settings fallback.") + openSettingsFallback(context, activity, "settingsFallback") + } + fallbackOpenRunnable = runnable + mainHandler.postDelayed(runnable, FALLBACK_OPEN_DELAY_MS) + } + + private fun cancelFallbackOpen() { + fallbackOpenRunnable?.let(mainHandler::removeCallbacks) + fallbackOpenRunnable = null + } + + private fun openSettingsFallback(context: Context, activity: Activity, outcome: String) { + cancelFallbackOpen() + val pendingResult = pendingPermissionResult + pendingPermissionResult = null + try { + activity.startActivity(settingsIntent()) + Log.d(LOG_TAG, "Health Connect settings fallback opened(outcome=$outcome).") + mainScope.launch { + try { + pendingResult?.success( + connectionStateMap( + context, + permissionRequestOutcome = outcome, + ), + ) + } catch (stateError: Throwable) { + Log.e(LOG_TAG, "connection state failed after settings fallback.", stateError) + pendingResult?.error("health_connect_state_failed", stateError.message, null) + } + } + } catch (error: Throwable) { + Log.e(LOG_TAG, "Health Connect settings fallback failed.", error) + pendingResult?.error("health_connect_permission_request_failed", error.message, null) + } + } + + private fun openSettings(context: Context, result: MethodChannel.Result) { + val activity = activityRef?.get() + if (activity == null) { + result.success(false) + return + } + val intents = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + listOf(settingsIntent()) + } else if (availability(context) == "providerUpdateRequired") { + listOf(providerUpdateIntent(context), settingsIntent()) + } else { + listOf(settingsIntent()) + } + val opened = intents.any { intent -> + try { + Log.d(LOG_TAG, "openSettings launching ${intent.action}.") + activity.startActivity(intent) + Log.d(LOG_TAG, "openSettings launch ok.") + true + } catch (error: ActivityNotFoundException) { + Log.e(LOG_TAG, "openSettings activity not found.", error) + false + } catch (error: SecurityException) { + Log.e(LOG_TAG, "openSettings security failure.", error) + false + } + } + result.success(opened) + } + + private fun providerUpdateIntent(context: Context): Intent { + val uri = + "market://details?id=$HEALTH_CONNECT_PROVIDER_PACKAGE&url=healthconnect%3A%2F%2Fonboarding" + return Intent(Intent.ACTION_VIEW).apply { + setPackage(PLAY_STORE_PACKAGE) + data = Uri.parse(uri) + putExtra("overlay", true) + putExtra("callerId", context.packageName) + } + } + + private fun settingsIntent(): Intent = + Intent(HealthConnectClient.ACTION_HEALTH_CONNECT_SETTINGS) + + private fun exportWorkout( + context: Context, + arguments: Any?, + result: MethodChannel.Result, + ) { + val map = arguments as? Map<*, *> + if (map == null) { + result.error("invalid_health_connect_workout", "Workout payload must be a map.", null) + return + } + mainScope.launch { + try { + if (availability(context) != "available") { + result.error("health_connect_unavailable", "Health Connect is unavailable.", null) + return@launch + } + val grantedPermissions = + HealthConnectClient.getOrCreate(context) + .permissionController + .getGrantedPermissions() + if (!grantedPermissions.containsAll(requiredPermissions)) { + result.error( + "health_connect_permissions_required", + "Health Connect write permissions are not granted.", + null, + ) + return@launch + } + val records = recordsFromWorkoutPayload(map) + HealthConnectClient.getOrCreate(context).insertRecords(records) + result.success(null) + } catch (error: Throwable) { + result.error("health_connect_export_failed", error.message, null) + } + } + } + + private fun recordsFromWorkoutPayload(map: Map<*, *>): List { + val historyId = nonBlankString(map["historyId"], "historyId") + val title = map["title"] as? String ?: "GameTime Basketball" + val start = epochMs(map["startedAtEpochMs"], "startedAtEpochMs") + val end = epochMs(map["endedAtEpochMs"], "endedAtEpochMs") + if (!end.isAfter(start)) { + throw IllegalArgumentException("Workout end time must be after start time.") + } + val records = mutableListOf( + ExerciseSessionRecord( + startTime = start, + startZoneOffset = ZoneOffset.UTC, + endTime = end, + endZoneOffset = ZoneOffset.UTC, + exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_BASKETBALL, + title = title, + notes = "Exported from GameTime.", + metadata = Metadata.manualEntry( + clientRecordId = clientRecordId(historyId, "exercise-session"), + ), + ), + ) + val distanceMeters = positiveDouble(map["totalDistanceMeters"]) + if (distanceMeters != null) { + records += DistanceRecord( + startTime = start, + startZoneOffset = ZoneOffset.UTC, + endTime = end, + endZoneOffset = ZoneOffset.UTC, + distance = Length.meters(distanceMeters), + metadata = Metadata.manualEntry( + clientRecordId = clientRecordId(historyId, "distance"), + ), + ) + } + val caloriesKcal = positiveDouble(map["totalCaloriesKcal"]) + if (caloriesKcal != null) { + records += TotalCaloriesBurnedRecord( + startTime = start, + startZoneOffset = ZoneOffset.UTC, + endTime = end, + endZoneOffset = ZoneOffset.UTC, + energy = Energy.kilocalories(caloriesKcal), + metadata = Metadata.manualEntry( + clientRecordId = clientRecordId(historyId, "calories"), + ), + ) + } + val heartRateSamples = heartRateSamples(map["heartRateSamples"], start, end) + if (heartRateSamples.isNotEmpty()) { + records += HeartRateRecord( + startTime = start, + startZoneOffset = ZoneOffset.UTC, + endTime = end, + endZoneOffset = ZoneOffset.UTC, + samples = heartRateSamples, + metadata = Metadata.manualEntry( + clientRecordId = clientRecordId(historyId, "heart-rate"), + ), + ) + } + return records + } + + private fun nonBlankString(value: Any?, fieldName: String): String { + val text = (value as? String)?.trim() + if (text.isNullOrEmpty()) { + throw IllegalArgumentException("$fieldName must be a non-blank string.") + } + return text + } + + private fun clientRecordId(historyId: String, kind: String): String = + "gametime:$historyId:$kind" + + private fun heartRateSamples( + payload: Any?, + start: Instant, + end: Instant, + ): List { + val items = payload as? List<*> ?: return emptyList() + return items.mapNotNull { item -> + val map = item as? Map<*, *> ?: return@mapNotNull null + val time = + (map["timeEpochMs"] as? Number)?.toLong()?.let(Instant::ofEpochMilli) + ?: return@mapNotNull null + val beatsPerMinute = (map["beatsPerMinute"] as? Number)?.toLong() + ?: return@mapNotNull null + if (beatsPerMinute <= 0 || time.isBefore(start) || time.isAfter(end)) { + return@mapNotNull null + } + HeartRateRecord.Sample(time = time, beatsPerMinute = beatsPerMinute) + } + } + + private fun epochMs(value: Any?, fieldName: String): Instant { + val epochMs = (value as? Number)?.toLong() + ?: throw IllegalArgumentException("$fieldName must be an epoch millisecond number.") + return Instant.ofEpochMilli(epochMs) + } + + private fun positiveDouble(value: Any?): Double? { + val number = (value as? Number)?.toDouble() ?: return null + return if (number > 0.0) number else null + } +} diff --git a/android/app/src/main/kotlin/com/idea/gametime/health/HealthConnectRationaleActivity.kt b/android/app/src/main/kotlin/com/idea/gametime/health/HealthConnectRationaleActivity.kt new file mode 100644 index 0000000..f960aba --- /dev/null +++ b/android/app/src/main/kotlin/com/idea/gametime/health/HealthConnectRationaleActivity.kt @@ -0,0 +1,40 @@ +package com.idea.gametime.health + +import android.app.Activity +import android.os.Bundle +import android.view.ViewGroup +import android.widget.ScrollView +import android.widget.TextView + +class HealthConnectRationaleActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + title = "Health Connect" + + val textView = + TextView(this).apply { + text = + """ + GameTime utilise Health Connect pour enregistrer les donnees compatibles de vos seances de basketball. + + Les donnees envoyees peuvent inclure la seance d'exercice, la distance, les calories et les mesures de frequence cardiaque disponibles. + + GameTime n'utilise ces donnees que pour le suivi sportif affiche dans l'application et dans Health Connect. Vous pouvez modifier ou retirer ces permissions a tout moment depuis Health Connect. + """.trimIndent() + textSize = 16f + setPadding(48, 48, 48, 48) + } + + val scrollView = + ScrollView(this).apply { + addView( + textView, + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ), + ) + } + setContentView(scrollView) + } +} diff --git a/android/app/src/main/kotlin/com/gametime/app/session/SessionNotificationPlugin.kt b/android/app/src/main/kotlin/com/idea/gametime/session/SessionNotificationPlugin.kt similarity index 99% rename from android/app/src/main/kotlin/com/gametime/app/session/SessionNotificationPlugin.kt rename to android/app/src/main/kotlin/com/idea/gametime/session/SessionNotificationPlugin.kt index d6bc950..9aef577 100644 --- a/android/app/src/main/kotlin/com/gametime/app/session/SessionNotificationPlugin.kt +++ b/android/app/src/main/kotlin/com/idea/gametime/session/SessionNotificationPlugin.kt @@ -1,4 +1,4 @@ -package com.gametime.app.session +package com.idea.gametime.session import android.Manifest import android.app.Activity diff --git a/android/app/src/main/kotlin/com/gametime/app/session/SessionStatusForegroundService.kt b/android/app/src/main/kotlin/com/idea/gametime/session/SessionStatusForegroundService.kt similarity index 93% rename from android/app/src/main/kotlin/com/gametime/app/session/SessionStatusForegroundService.kt rename to android/app/src/main/kotlin/com/idea/gametime/session/SessionStatusForegroundService.kt index f399f6b..ab87789 100644 --- a/android/app/src/main/kotlin/com/gametime/app/session/SessionStatusForegroundService.kt +++ b/android/app/src/main/kotlin/com/idea/gametime/session/SessionStatusForegroundService.kt @@ -1,4 +1,4 @@ -package com.gametime.app.session +package com.idea.gametime.session import android.app.Notification import android.app.NotificationChannel @@ -8,8 +8,8 @@ import android.app.Service import android.content.Intent import android.os.Build import android.os.IBinder -import com.gametime.app.MainActivity -import com.gametime.app.R +import com.idea.gametime.MainActivity +import com.idea.gametime.R class SessionStatusForegroundService : Service() { override fun onCreate() { @@ -62,7 +62,7 @@ class SessionStatusForegroundService : Service() { Notification.BigTextStyle().bigText("$primaryLine\n$it") } return builder - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.drawable.ic_ongoing_gt) .setContentTitle(title) .setContentText(primaryLine) .setSubText(secondaryLine) @@ -89,7 +89,7 @@ class SessionStatusForegroundService : Service() { } companion object { - const val ACTION_SHOW = "com.gametime.app.session.SHOW" + const val ACTION_SHOW = "com.idea.gametime.session.SHOW" const val EXTRA_TITLE = "title" const val EXTRA_PRIMARY_LINE = "primaryLine" const val EXTRA_SECONDARY_LINE = "secondaryLine" diff --git a/android/app/src/main/kotlin/com/gametime/app/watch/PhoneWatchBridgeListenerService.kt b/android/app/src/main/kotlin/com/idea/gametime/watch/PhoneWatchBridgeListenerService.kt similarity index 98% rename from android/app/src/main/kotlin/com/gametime/app/watch/PhoneWatchBridgeListenerService.kt rename to android/app/src/main/kotlin/com/idea/gametime/watch/PhoneWatchBridgeListenerService.kt index 6dceee7..a2925f6 100644 --- a/android/app/src/main/kotlin/com/gametime/app/watch/PhoneWatchBridgeListenerService.kt +++ b/android/app/src/main/kotlin/com/idea/gametime/watch/PhoneWatchBridgeListenerService.kt @@ -1,4 +1,4 @@ -package com.gametime.app.watch +package com.idea.gametime.watch import com.google.android.gms.wearable.CapabilityInfo import com.google.android.gms.wearable.MessageEvent diff --git a/android/app/src/main/kotlin/com/gametime/app/watch/WatchBridgePlugin.kt b/android/app/src/main/kotlin/com/idea/gametime/watch/WatchBridgePlugin.kt similarity index 90% rename from android/app/src/main/kotlin/com/gametime/app/watch/WatchBridgePlugin.kt rename to android/app/src/main/kotlin/com/idea/gametime/watch/WatchBridgePlugin.kt index c5d7c7d..1c93c6b 100644 --- a/android/app/src/main/kotlin/com/gametime/app/watch/WatchBridgePlugin.kt +++ b/android/app/src/main/kotlin/com/idea/gametime/watch/WatchBridgePlugin.kt @@ -1,4 +1,4 @@ -package com.gametime.app.watch +package com.idea.gametime.watch import android.content.Context import android.content.Intent @@ -214,32 +214,7 @@ object WatchBridgePlugin { sendMessage(context, targetNode, ACK_PATH, payload, result) return } - Wearable.getCapabilityClient(context) - .getCapability(WATCH_CAPABILITY, CapabilityClient.FILTER_REACHABLE) - .addOnSuccessListener { capability -> - val nodes = capability.nodes.toList() - if (nodes.isEmpty()) { - result.success(null) - return@addOnSuccessListener - } - var remaining = nodes.size - var failed = false - for (node in nodes) { - Wearable.getMessageClient(context) - .sendMessage(node.id, ACK_PATH, payload) - .addOnSuccessListener { - remaining -= 1 - if (remaining == 0 && !failed) result.success(null) - } - .addOnFailureListener { error -> - failed = true - result.error("send_ack_failed", error.message, null) - } - } - } - .addOnFailureListener { error -> - result.error("capability_lookup_failed", error.message, null) - } + result.success(null) } private fun sendMessage( diff --git a/android/app/src/main/kotlin/com/gametime/app/watch/WatchCompanionForegroundService.kt b/android/app/src/main/kotlin/com/idea/gametime/watch/WatchCompanionForegroundService.kt similarity index 93% rename from android/app/src/main/kotlin/com/gametime/app/watch/WatchCompanionForegroundService.kt rename to android/app/src/main/kotlin/com/idea/gametime/watch/WatchCompanionForegroundService.kt index ef5db54..772f963 100644 --- a/android/app/src/main/kotlin/com/gametime/app/watch/WatchCompanionForegroundService.kt +++ b/android/app/src/main/kotlin/com/idea/gametime/watch/WatchCompanionForegroundService.kt @@ -1,4 +1,4 @@ -package com.gametime.app.watch +package com.idea.gametime.watch import android.app.Notification import android.app.NotificationChannel @@ -7,7 +7,7 @@ import android.app.Service import android.content.Intent import android.os.Build import android.os.IBinder -import com.gametime.app.R +import com.idea.gametime.R class WatchCompanionForegroundService : Service() { override fun onCreate() { @@ -30,7 +30,7 @@ class WatchCompanionForegroundService : Service() { Notification.Builder(this) } return builder - .setSmallIcon(R.mipmap.ic_launcher) + .setSmallIcon(R.drawable.ic_ongoing_gt) .setContentTitle("GameTime") .setContentText("Séance en cours") .setOngoing(true) diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml index 399f698..b04c115 100644 --- a/android/app/src/profile/AndroidManifest.xml +++ b/android/app/src/profile/AndroidManifest.xml @@ -4,4 +4,6 @@ to allow setting breakpoints, to provide hot reload, etc. --> + +