From 4f417d28eeca12c2a929d93db6e2c2f265265f01 Mon Sep 17 00:00:00 2001 From: Abhishek Gupta Date: Thu, 4 Sep 2025 16:32:46 +0530 Subject: [PATCH] added companion code... --- android/app/build.gradle | 3 + android/app/src/main/AndroidManifest.xml | 9 +- .../ultimate_alarm_clock/AlarmReceiver.kt | 32 +++- .../ultimate_alarm_clock/AlarmUtils.kt | 85 ++++++++++ .../ultimate_alarm_clock/GetLatestAlarm.kt | 21 ++- .../ultimate_alarm_clock/MainActivity.kt | 40 +++++ .../Utilities/LocationCheckWorker.kt | 155 ++++++++++++++++++ .../Utilities/LocationFetcherService.kt | 3 +- .../Utilities/WeatherCheckWorker.kt | 104 ++++++++++++ .../Utilities/WeatherHelper.kt | 39 +++++ .../communicatoin/AlarmMapper.kt | 40 +++++ .../communicatoin/AlarmToDb.kt | 113 +++++++++++++ .../communicatoin/FullAlarmDTO.kt | 24 +++ .../communicatoin/PhoneSender.kt | 95 +++++++++++ .../UACDataLayerListenerService.kt | 77 +++++++++ lib/app/communication/communication.dart | 33 ++++ .../communication/native_action_handler.dart | 51 ++++++ lib/app/data/providers/isar_provider.dart | 29 ++++ .../add_or_update_alarm_controller.dart | 85 +++++++++- .../controllers/alarm_ring_controller.dart | 57 +++++++ .../alarmRing/views/alarm_ring_view.dart | 51 +----- .../modules/home/bindings/home_binding.dart | 2 + .../home/controllers/home_controller.dart | 15 ++ lib/app/modules/home/views/home_view.dart | 2 +- .../controllers/splash_screen_controller.dart | 14 ++ lib/main.dart | 6 + 26 files changed, 1126 insertions(+), 59 deletions(-) create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationCheckWorker.kt create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherCheckWorker.kt create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherHelper.kt create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmMapper.kt create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmToDb.kt create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/FullAlarmDTO.kt create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/PhoneSender.kt create mode 100644 android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/UACDataLayerListenerService.kt create mode 100644 lib/app/communication/communication.dart create mode 100644 lib/app/communication/native_action_handler.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 10aeeb95c..52568176f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -99,6 +99,9 @@ flutter { } dependencies { + implementation("androidx.work:work-runtime-ktx:2.9.0") + implementation 'com.google.android.gms:play-services-wearable:18.1.0' + implementation 'com.google.code.gson:gson:2.10.1' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.10" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 655a81847..76d4c9a29 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -53,7 +53,14 @@ android:enabled="true" android:foregroundServiceType="systemExempted" android:exported="true"> - + + + + + + diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmReceiver.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmReceiver.kt index 72d97f95d..fd98aa8e8 100644 --- a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmReceiver.kt +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmReceiver.kt @@ -8,6 +8,8 @@ import android.util.Log import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import com.google.android.gms.wearable.PutDataMapRequest +import com.google.android.gms.wearable.Wearable class AlarmReceiver : BroadcastReceiver() { companion object { @@ -15,6 +17,31 @@ class AlarmReceiver : BroadcastReceiver() { private var lastTriggeredType = "" private const val DUPLICATE_PREVENTION_WINDOW = 10000 // 10 seconds } + + private fun sendVerdictToWatch(context: Context, alarmId: String, willRing: Boolean, reason: String) { + Log.d("ActivityCheck", "Attempting to send verdict to watch for alarm: $alarmId") + val path = "/uac/pre_check_verdict" + + val putDataMapRequest = PutDataMapRequest.create(path) + putDataMapRequest.dataMap.apply { + putString("alarmID", alarmId) + putBoolean("willRing", willRing) + putString("reason", reason) + putLong("timestamp", System.currentTimeMillis()) + } + + val putDataRequest = putDataMapRequest.asPutDataRequest().setUrgent() + + val dataClient = Wearable.getDataClient(context) + dataClient.putDataItem(putDataRequest).apply { + addOnSuccessListener { + Log.d("ActivityCheck", "Successfully sent verdict to watch: ${it.uri}") + } + addOnFailureListener { + Log.e("ActivityCheck", "Failed to send verdict to watch", it) + } + } + } override fun onReceive(context: Context?, intent: Intent?) { if (context == null || intent == null) { @@ -197,7 +224,10 @@ class AlarmReceiver : BroadcastReceiver() { } Log.d("AlarmReceiver", "Decision: shouldRing = $shouldRing") - + val alarmId = intent.getStringExtra("alarmID") + if (alarmId != null) { + sendVerdictToWatch(context, alarmId, shouldRing, logMessage) + } if (shouldRing) { println("ANDROID STARTING APP") context.startActivity(flutterIntent) diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmUtils.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmUtils.kt index 67a11fa55..83b2802c7 100644 --- a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmUtils.kt +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/AlarmUtils.kt @@ -14,6 +14,12 @@ import com.ccextractor.ultimate_alarm_clock.LogDatabaseHelper import java.text.SimpleDateFormat import java.util.* import java.util.Locale +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import com.ccextractor.ultimate_alarm_clock.LocationCheckWorker +import com.ccextractor.ultimate_alarm_clock.WeatherCheckWorker +import java.util.concurrent.TimeUnit object AlarmUtils { @SuppressLint("ScheduleExactAlarm") @@ -136,6 +142,84 @@ object AlarmUtils { alarmID = alarmID ) } + + //* This checks the code for the smart-control features to send beforehand to watch + //! locaiton checker + if (isLocation == 1) { + val workManager = WorkManager.getInstance(context) + + val precheckMinutes = 1L + val triggerAtMillis = System.currentTimeMillis() + intervalToAlarm + val precheckMillis = TimeUnit.MINUTES.toMillis(precheckMinutes) + + val delay = triggerAtMillis - System.currentTimeMillis() - precheckMillis + + var actualDelay = 0L + if (delay > 0) { + actualDelay = delay + Log.d("AlarmUtils", "Scheduling pre-check with a delay of ${actualDelay}ms.") + } else { + actualDelay = 0L + Log.d("AlarmUtils", "Pre-check window has passed. Scheduling worker to run immediately.") + } + + if (intervalToAlarm > 0) { + val requestCode = if (isShared) MainActivity.REQUEST_CODE_SHARED_ALARM else MainActivity.REQUEST_CODE_LOCAL_ALARM + + val data = Data.Builder() + .putString("ALARM_ID", alarmID) + .putString("LOCATION", location) + .putInt("LOCATION_CONDITION_TYPE", locationConditionType) + .putBoolean("IS_SHARED_ALARM", isShared) + .putInt("ALARM_REQUEST_CODE", requestCode) + .build() + + val locationCheckRequest = OneTimeWorkRequest.Builder(LocationCheckWorker::class.java) + .setInitialDelay(actualDelay, TimeUnit.MILLISECONDS) + .setInputData(data) + .addTag(alarmID) + .build() + + workManager.enqueue(locationCheckRequest) + Log.d("AlarmUtils", "✅ WORKER ENQUEUED for alarm ID: $alarmID") + } + } + //! weather checker + if (isWeather == 1) { + val workManager = WorkManager.getInstance(context) + + val precheckMinutes = 1L + val triggerAtMillis = System.currentTimeMillis() + intervalToAlarm + val precheckMillis = TimeUnit.MINUTES.toMillis(precheckMinutes) + val delay = triggerAtMillis - System.currentTimeMillis() - precheckMillis + + var actualDelay = 0L + if (delay > 0) { + actualDelay = delay + Log.d("AlarmUtils", "Scheduling pre-check with a delay of ${actualDelay}ms.") + } else { + actualDelay = 0L + Log.d("AlarmUtils", "Pre-check window has passed. Scheduling worker to run immediately.") + } + + if (intervalToAlarm > 0) { + val requestCode = if (isShared) MainActivity.REQUEST_CODE_SHARED_ALARM else MainActivity.REQUEST_CODE_LOCAL_ALARM + + val data = Data.Builder() + .putString("ALARM_ID", alarmID) + .putInt("ALARM_REQUEST_CODE", requestCode) + .putString("WEATHER_TYPES", weatherTypes) + .putInt("WEATHER_CONDITION_TYPE", weatherConditionType) + .build() + val weatherCheckRequest = OneTimeWorkRequest.Builder(WeatherCheckWorker::class.java) + .setInitialDelay(actualDelay, TimeUnit.MILLISECONDS) + .setInputData(data) + .addTag(alarmID) + .build() + workManager.enqueue(weatherCheckRequest) + Log.d("AlarmUtils", "✅ WeatherCheckWorker ENQUEUED for alarm ID: $alarmID") + } + } } fun cancelAlarmById(context: Context, alarmID: String, isShared: Boolean) { @@ -229,6 +313,7 @@ object AlarmUtils { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ) + // val activityCheckTime = 1000.toLong() val activityCheckTime = triggerAtMillis - (15 * 60 * 1000) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/GetLatestAlarm.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/GetLatestAlarm.kt index 5e2f4b858..58aa69f0e 100644 --- a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/GetLatestAlarm.kt +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/GetLatestAlarm.kt @@ -438,7 +438,13 @@ data class AlarmModel( val location: String, val alarmDate: String, val alarmId: String, - val ringOn: Int + val ringOn: Int, + val isEnabled: Int, + val isGuardian: Int, + val guardian: Int, + val guardianTimer: Int, + val isCall: Int, + ) { companion object { @SuppressLint("Range") @@ -451,6 +457,12 @@ data class AlarmModel( val activityMonitor = cursor.getInt(cursor.getColumnIndex("activityMonitor")) val isWeatherEnabled = cursor.getInt(cursor.getColumnIndex("isWeatherEnabled")) val weatherTypes = cursor.getString(cursor.getColumnIndex("weatherTypes")) + val isEnabled = cursor.getInt(cursor.getColumnIndex("isEnabled")) + val isGuardian = cursor.getInt(cursor.getColumnIndex("isGuardian")) + val guardian = cursor.getInt(cursor.getColumnIndex("guardian")) + val guardianTimer = cursor.getInt(cursor.getColumnIndex("guardianTimer")) + val isCall = cursor.getInt(cursor.getColumnIndex("isCall")) + val weatherConditionTypeIndex = cursor.getColumnIndex("weatherConditionType") @@ -504,7 +516,12 @@ data class AlarmModel( location, alarmDate, alarmId, - ringOn + ringOn, + isEnabled, + isGuardian, + guardian, + guardianTimer, + isCall, ) } } diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt index 3d322e0ee..7a876ff5d 100644 --- a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt @@ -37,6 +37,13 @@ import java.util.Locale import android.app.NotificationChannel import android.graphics.Color import androidx.core.app.NotificationCompat +import com.google.gson.Gson +import com.ccextractor.ultimate_alarm_clock.communication.UACDataLayerListenerService +import com.google.android.gms.wearable.Wearable +import com.google.android.gms.wearable.DataClient +import com.google.android.gms.wearable.DataEventBuffer +import com.google.android.gms.wearable.DataEvent +import com.ccextractor.ultimate_alarm_clock.communication.PhoneSender class MainActivity : FlutterActivity() { @@ -44,6 +51,7 @@ class MainActivity : FlutterActivity() { const val CHANNEL1 = "ulticlock" const val CHANNEL2 = "timer" const val CHANNEL3 = "system_ringtones" + const val CHANNEL4 = "watch_action_channel" const val ACTION_START_FLUTTER_APP = "com.ccextractor.ultimate_alarm_clock" const val EXTRA_KEY = "alarmRing" const val ALARM_TYPE = "isAlarm" @@ -94,10 +102,14 @@ class MainActivity : FlutterActivity() { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) + + UACDataLayerListenerService.flutterEngine = flutterEngine + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) var methodChannel1 = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL1) var methodChannel2 = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL2) var methodChannel3 = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL3) + val methodChannel4 = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL4) val intent = intent @@ -141,6 +153,34 @@ class MainActivity : FlutterActivity() { alarmConfig["isSharedAlarm"] = false } + methodChannel4.setMethodCallHandler { call, result -> + when (call.method) { + "sendActionToWatch" -> { + val action = call.argument("action") + val id = call.argument("id") ?: "" + if (action != null) { + PhoneSender.sendActionToWatch(this, action, id) + result.success("Action '$action' sent to watch.") + } else { + result.error("INVALID_ARGUMENTS", "Missing 'action' or 'id'", null) + } + } + "sendAlarmToWatch" -> { + val alarmMap = call.arguments as? Map + val isarId = call.argument("isarId") + if (alarmMap != null && isarId != null) { + val alarmMapMutable = alarmMap.toMutableMap() + alarmMapMutable["isarid"] = isarId + PhoneSender.sendAlarmToWatch(context, alarmMapMutable) + result.success("Alarm sent to watch.") + } else { + result.error("INVALID_ARGUMENT", "Alarm data or alarmId missing.", null) + } + } + else -> result.notImplemented() + } + } + methodChannel3.setMethodCallHandler { call, result -> when (call.method) { "getSystemRingtones" -> { diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationCheckWorker.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationCheckWorker.kt new file mode 100644 index 000000000..19344d414 --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationCheckWorker.kt @@ -0,0 +1,155 @@ +// checks the location condition 1 min before ringing the alarm +package com.ccextractor.ultimate_alarm_clock + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt +import com.google.android.gms.wearable.PutDataMapRequest +import com.google.android.gms.wearable.Wearable + +class LocationCheckWorker(appContext: Context, workerParams: WorkerParameters) : + CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + Log.d("LocationCheckWorker", "Worker started.") + + val alarmID = inputData.getString("ALARM_ID") ?: return Result.failure() + val targetLocation = inputData.getString("LOCATION") ?: return Result.failure() + val locationConditionType = inputData.getInt("LOCATION_CONDITION_TYPE", 2) + val isSharedAlarm = inputData.getBoolean("IS_SHARED_ALARM", false) + val alarmRequestCode = inputData.getInt("ALARM_REQUEST_CODE", -1) + + if (alarmRequestCode == -1) { + Log.e("LocationCheckWorker", "Invalid Request Code received.") + return Result.failure() + } + + Log.d("LocationCheckWorker", "Processing alarm ID: $alarmID, Condition: $locationConditionType") + + try { + val locationHelper = LocationHelper(applicationContext) + val currentLocationString = locationHelper.getCurrentLocation() + if (currentLocationString.isEmpty() || currentLocationString == "error" || currentLocationString == "timeout") { + Log.e("LocationCheckWorker", "Failed to get current location: $currentLocationString") + return Result.success() + } + + val currentCoords = currentLocationString.split(",").map { it.toDouble() } + val targetCoords = targetLocation.split(",").map { it.toDouble() } + val currentLocation = Location(currentCoords[0], currentCoords[1]) + val destinationLocation = Location(targetCoords[0], targetCoords[1]) + + val distance = calculateDistance(currentLocation, destinationLocation) + val isWithin500m = distance < 500.0 + + var shouldRing: Boolean? = null + var logMessage = "" + + when (locationConditionType) { + 1 -> { // Ring when AT + shouldRing = isWithin500m + logMessage = if (shouldRing) "Pre-check: User is AT. Alarm WILL ring." else "Pre-check: User is NOT AT. Alarm will be SKIPPED." + } + 2 -> { // Cancel when AT + shouldRing = !isWithin500m + logMessage = if (!shouldRing) "Pre-check: User is AT. Alarm will be SKIPPED." else "Pre-check: User is NOT AT. Alarm WILL ring." + } + 3 -> { // Ring when AWAY + shouldRing = !isWithin500m + logMessage = if (shouldRing) "Pre-check: User is AWAY. Alarm WILL ring." else "Pre-check: User is NOT AWAY. Alarm will be SKIPPED." + } + 4 -> { // Cancel when AWAY + shouldRing = isWithin500m + logMessage = if (!shouldRing) "Pre-check: User is AWAY. Alarm will be SKIPPED." else "Pre-check: User is NOT AWAY. Alarm WILL ring." + } + } + + Log.d("LocationCheckWorker", logMessage) + + // Send the CORRECT verdict to the watch + if (shouldRing != null) { + sendVerdictToWatch(alarmID, shouldRing, logMessage) + val prefs = applicationContext.getSharedPreferences("AlarmVerdict", Context.MODE_PRIVATE) + with(prefs.edit()) { + putBoolean(alarmID, shouldRing) + apply() + } + } + return Result.success() + } catch (e: Exception) { + Log.e("LocationCheckWorker", "Error in LocationCheckWorker: ${e.message}", e) + return Result.failure() + } + } + + private fun cancelAlarm(context: Context, requestCode: Int, isSharedAlarm: Boolean) { + val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + val intent = Intent(context, AlarmReceiver::class.java).apply { + putExtra("isSharedAlarm", isSharedAlarm) + } + + val pendingIntent = PendingIntent.getBroadcast( + context, + requestCode, + intent, + PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE + ) + + if (pendingIntent != null) { + alarmManager.cancel(pendingIntent) + pendingIntent.cancel() + Log.i("LocationCheckWorker", "Successfully cancelled PendingIntent with request code $requestCode") + } else { + Log.w("LocationCheckWorker", "Could not find PendingIntent with request code $requestCode to cancel.") + } + } + + private fun sendVerdictToWatch(alarmId: String, willRing: Boolean, reason: String) { + Log.d("LocationCheckWorker", "Attempting to send verdict to watch for alarm: $alarmId") + val path = "/uac/pre_check_verdict" + + val putDataMapRequest = PutDataMapRequest.create(path) + putDataMapRequest.dataMap.apply { + putString("alarmID", alarmId) + putBoolean("willRing", willRing) + putString("reason", reason) + putLong("timestamp", System.currentTimeMillis()) + } + + val putDataRequest = putDataMapRequest.asPutDataRequest().setUrgent() + + val dataClient = Wearable.getDataClient(applicationContext) + dataClient.putDataItem(putDataRequest).apply { + addOnSuccessListener { + Log.d("LocationCheckWorker", "✅ Successfully sent verdict to watch: ${it.uri}") + } + addOnFailureListener { + Log.e("LocationCheckWorker", "❌ Failed to send verdict to watch", it) + } + } + } + + data class Location(val latitude: Double, val longitude: Double) + + private fun calculateDistance(current: Location, destination: Location): Double { + val earthRadius = 6371.0 + val lat1 = Math.toRadians(current.latitude) + val lon1 = Math.toRadians(current.longitude) + val lat2 = Math.toRadians(destination.latitude) + val lon2 = Math.toRadians(destination.longitude) + val dlon = lon2 - lon1 + val dlat = lat2 - lat1 + val a = sin(dlat / 2).pow(2) + cos(lat1) * cos(lat2) * sin(dlon / 2).pow(2) + val c = 2 * atan2(sqrt(a), sqrt(1 - a)) + return earthRadius * c * 1000 + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationFetcherService.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationFetcherService.kt index 5dbed0226..8c80f6b1e 100644 --- a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationFetcherService.kt +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/LocationFetcherService.kt @@ -57,6 +57,7 @@ class LocationFetcherService : Service() { Log.d("LocationFetcherService", "Processing alarm - ID: $alarmID, isShared: $isSharedAlarm") Log.d("LocationFetcherService", "LocationConditionType from intent: $locationConditionType") Log.d("LocationFetcherService", "Target location: $targetLocation") + startForeground(notificationId, getNotification()) // Validate location data if (targetLocation.isEmpty() || targetLocation == "0.0,0.0") { @@ -66,8 +67,6 @@ class LocationFetcherService : Service() { return START_NOT_STICKY } - startForeground(notificationId, getNotification()) - try { processLocationAlarm() } catch (e: Exception) { diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherCheckWorker.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherCheckWorker.kt new file mode 100644 index 000000000..c22500f1f --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherCheckWorker.kt @@ -0,0 +1,104 @@ +// is called 1 min before the alarm to check if the weather condition is met using the WeatherHelper.kt + +package com.ccextractor.ultimate_alarm_clock + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.google.android.gms.wearable.PutDataMapRequest +import com.google.android.gms.wearable.Wearable + +class WeatherCheckWorker(private val appContext: Context, workerParams: WorkerParameters) : + CoroutineWorker(appContext, workerParams) { + + override suspend fun doWork(): Result { + Log.d("WeatherCheckWorker", "Worker Started.") + + val alarmID = inputData.getString("ALARM_ID") ?: return Result.failure() + val alarmRequestCode = inputData.getInt("ALARM_REQUEST_CODE", -1) + val weatherTypesJSON = inputData.getString("WEATHER_TYPES") ?: "[]" + val weatherConditionType = inputData.getInt("WEATHER_CONDITION_TYPE", 0) + + Log.d("WeatherCheckWorker", "Processing alarm ID: $alarmID, Condition: $weatherConditionType") + + try { + val targetWeatherTypes = getWeatherConditionsFromString(weatherTypesJSON) + val (shouldRing, reason) = checkWeatherCondition(targetWeatherTypes, weatherConditionType) + + Log.d("WeatherCheckWorker", "Verdict for $alarmID: Should Ring = $shouldRing, Reason: $reason") + + if (shouldRing == false) { + // Save verdict for the phone's AlarmReceiver + val prefs = appContext.getSharedPreferences("AlarmVerdict", Context.MODE_PRIVATE) + with(prefs.edit()) { + putBoolean(alarmID, false).apply() + } + + sendVerdictToWatch(alarmID, false, reason) + + val logdbHelper = LogDatabaseHelper(appContext) + logdbHelper.insertLog(reason, status = LogDatabaseHelper.Status.WARNING, type = LogDatabaseHelper.LogType.NORMAL, hasRung = 0) + } + + return Result.success() + } catch (e: Exception) { + Log.e("WeatherCheckWorker", "Error: ${e.message}", e) + return Result.failure() + } + } + + private suspend fun checkWeatherCondition(targetWeatherTypes: String, conditionType: Int): Pair { + val locationHelper = LocationHelper(appContext) + val currentLocationString = locationHelper.getCurrentLocation() + if (currentLocationString.isEmpty() || currentLocationString.contains("error")) { + return Pair(true, "Pre-check (Weather): Could not get location, ringing as safeguard.") + } + + return try { + val coords = currentLocationString.split(",").map { it.toDouble() } + val latitude = coords[0] + val longitude = coords[1] + val weatherHelper = WeatherHelper(appContext) + val currentWeather = weatherHelper.fetchCurrentWeather(latitude, longitude) + + if (currentWeather == null) { + return Pair(true, "Pre-check (Weather): Could not fetch weather, ringing as safeguard.") + } + val weatherMatches = targetWeatherTypes.contains(currentWeather) + + when (conditionType) { + 1 -> Pair(weatherMatches, if (weatherMatches) "Weather matches." else "Weather doesn't match, skipping.") + 2 -> Pair(!weatherMatches, if (!weatherMatches) "Weather does not match." else "Weather matches, skipping.") + 3 -> Pair(!weatherMatches, if (!weatherMatches) "Weather is different." else "Weather is not different, skipping.") + 4 -> Pair(weatherMatches, if (weatherMatches) "Weather is not different." else "Weather is different, skipping.") + else -> Pair(true, "Unknown weather condition.") + } + } catch (e: Exception) { + Pair(true, "Error during weather check, ringing as safeguard.") + } + } + + private fun getWeatherConditionsFromString(jsonString: String): String { + val weatherMap = mapOf(0 to "sunny", 1 to "cloudy", 2 to "rainy", 3 to "windy", 4 to "stormy") + return try { + if (jsonString.isEmpty() || jsonString == "[]") return "" + val indices = jsonString.removeSurrounding("[", "]").split(",").filter { it.trim().isNotEmpty() }.map { it.trim().toInt() } + indices.mapNotNull { weatherMap[it] }.joinToString(",") + } catch (e: Exception) { "" } + } + +private fun sendVerdictToWatch(alarmId: String, willRing: Boolean, reason: String) { + val path = "/uac/pre_check_verdict" + val putDataMapRequest = PutDataMapRequest.create(path) + putDataMapRequest.dataMap.apply { + putString("alarmID", alarmId) + putBoolean("willRing", willRing) + putString("reason", reason) + } + val putDataRequest = putDataMapRequest.asPutDataRequest().setUrgent() + Wearable.getDataClient(appContext).putDataItem(putDataRequest) + .addOnSuccessListener { Log.d("WeatherCheckWorker", "✅ Verdict sent to watch.") } + .addOnFailureListener { Log.e("WeatherCheckWorker", "❌ Failed to send verdict.", it) } +} +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherHelper.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherHelper.kt new file mode 100644 index 000000000..c28a63f36 --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/Utilities/WeatherHelper.kt @@ -0,0 +1,39 @@ +// actually fetches the weather data from the API for the companion app +package com.ccextractor.ultimate_alarm_clock + +import android.content.Context +import android.util.Log +import com.android.volley.toolbox.Volley +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +class WeatherHelper(private val context: Context) { + + suspend fun fetchCurrentWeather(latitude: Double, longitude: Double): String? { + val url = "https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t=rain,snowfall,cloud_cover,wind_speed_10m" + Log.d("WeatherHelper", "Fetching weather from: $url") + + return suspendCancellableCoroutine { continuation -> + val request = GsonRequest(url, WeatherModel::class.java, + { response -> + val currentWeather = when { + (response.current?.rain ?: 0.0) > 0.1 && (response.current?.windSpeed10m ?: 0.0) > 40.0 -> "stormy" + (response.current?.rain ?: 0.0) > 0.1 -> "rainy" + (response.current?.cloudCover ?: 0) > 60 -> "cloudy" + (response.current?.windSpeed10m ?: 0.0) > 20.0 -> "windy" + else -> "sunny" + } + Log.d("WeatherHelper", "API Success. Current weather is: $currentWeather") + if (continuation.isActive) continuation.resume(currentWeather) + }, + { error -> + Log.e("WeatherHelper", "API Error: ${error.message}") + if (continuation.isActive) continuation.resume(null) + }) + + continuation.invokeOnCancellation { request.cancel() } + + Volley.newRequestQueue(context).add(request) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmMapper.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmMapper.kt new file mode 100644 index 000000000..3c1095f08 --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmMapper.kt @@ -0,0 +1,40 @@ +package com.ccextractor.ultimate_alarm_clock.communication + +import com.ccextractor.ultimate_alarm_clock.AlarmModel +import java.text.SimpleDateFormat +import java.util.* + +fun FullAlarmDTO.toAlarmModel(): AlarmModel { + val formatter = SimpleDateFormat("HH:mm", Locale.getDefault()) + val date = formatter.parse(time) ?: Date(0) + val calendar = Calendar.getInstance().apply { time = date } + + val minutesSinceMidnight = + calendar.get(Calendar.HOUR_OF_DAY) * 60 + calendar.get(Calendar.MINUTE) + + return AlarmModel( + id = id, + minutesSinceMidnight = minutesSinceMidnight, + alarmTime = time, + days = days.joinToString(","), + isOneTime = isOneTime, + isEnabled = isEnabled, + ringOn = isEnabled, + alarmId = uniqueSyncId, + isLocationEnabled = if (isLocationEnabled) 1 else 0, + locationConditionType = locationConditionType, + location = location, + isWeatherEnabled = if (isWeatherEnabled) 1 else 0, + weatherConditionType = weatherConditionType, + weatherTypes = weatherTypes.joinToString(","), + activityMonitor = if (isActivityEnabled) 1 else 0, + activityInterval = activityInterval, + activityConditionType = activityConditionType, + isGuardian = if (isGuardian) 1 else 0, + guardian = guardian.toIntOrNull() ?: 0, + guardianTimer = guardianTimer, + isCall = if (isCall) 1 else 0, + + alarmDate = "" + ) +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmToDb.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmToDb.kt new file mode 100644 index 000000000..9ac3c8b9e --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/AlarmToDb.kt @@ -0,0 +1,113 @@ +package com.ccextractor.ultimate_alarm_clock.communication + +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.ccextractor.ultimate_alarm_clock.AlarmModel +import com.google.gson.Gson +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +object ReceivedAlarmModelToDb { + + private const val CHANNEL = "com.ccextractor.alarm_channel" + private const val TAG = "UAC_ReceivedAlarmModelToDb" + + fun sendToFlutter(flutterEngine: FlutterEngine, alarm: AlarmModel, isNewAlarm: Boolean) { + val methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) + val currentDate = LocalDate.now() + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val formattedDate = currentDate.format(formatter) + val parsedDays = + (alarm.days).split(",").mapNotNull { + it.trim().toIntOrNull() + } + + val daysBooleanList = + List(7) { dayIndex -> + parsedDays.contains(dayIndex + 1) + } + + val weatherTypesIntList = alarm.weatherTypes.split(",") + .mapNotNull { it.trim().toIntOrNull() } + ?: emptyList() + + val alarmMap = + mapOf( + "isarId" to alarm.id, + "alarmTime" to alarm.alarmTime, + "alarmID" to alarm.alarmId, + "ownerId" to "watch", + "ownerName" to "Watch", + "lastEditedUserId" to "watch", + "mutexLock" to false, + "days" to daysBooleanList, + "intervalToAlarm" to 0, + "minutesSinceMidnight" to alarm.minutesSinceMidnight, + "isSharedAlarmEnabled" to false, + + "isActivityEnabled" to (alarm.activityMonitor == 1), + "activityInterval" to alarm.activityInterval, + "activityConditionType" to alarm.activityConditionType, + + "isLocationEnabled" to (alarm.isLocationEnabled == 1), + "locationConditionType" to alarm.locationConditionType, + "location" to if (alarm.location.isEmpty()) "0.0,0.0" else alarm.location, + + "isWeatherEnabled" to (alarm.isWeatherEnabled == 1), + "weatherConditionType" to alarm.weatherConditionType, + "weatherTypes" to weatherTypesIntList, + + "isGuardian" to (alarm.isGuardian == 1), + "guardianTimer" to alarm.guardianTimer, + "guardian" to alarm.guardian.toString(), + "isCall" to (alarm.isCall == 1), + + "isMathsEnabled" to false, + "mathsDifficulty" to 0, + "numMathsQuestions" to 0, + "isShakeEnabled" to false, + "shakeTimes" to 0, + "isQrEnabled" to false, + "qrValue" to "", + "isPedometerEnabled" to false, + "numberOfSteps" to 0, + "mainAlarmTime" to alarm.alarmTime, + "label" to "Synced from Watch", + "isOneTime" to (alarm.isOneTime == 1), + "snoozeDuration" to 5, + "maxSnoozeCount" to 3, + "gradient" to 0, + "ringtoneName" to "Default", + "note" to "", + "deleteAfterGoesOff" to false, + "showMotivationalQuote" to false, + "volMax" to 1.0, + "volMin" to 0.5, + "activityMonitor" to alarm.activityMonitor, + "alarmDate" to formattedDate, + "profile" to "Default", + "isSunriseEnabled" to false, + "sunriseDuration" to 0, + "sunriseIntensity" to 0.5, + "sunriseColorScheme" to 0, + "sharedUserIds" to listOf(), + "ringOn" to (alarm.ringOn == 1), + "isEnabled" to (alarm.isEnabled == 1), + ) + + val finalAlarmMap = mapOf( + "alarmMap" to alarmMap, + "isNewAlarm" to isNewAlarm + ) + + // val json = Gson().toJson(alarmMap) + Log.d(TAG, "📤 Sending alarm to Flutter: $finalAlarmMap") + + Handler(Looper.getMainLooper()).post { + methodChannel.invokeMethod("onWatchAlarmReceived", finalAlarmMap) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/FullAlarmDTO.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/FullAlarmDTO.kt new file mode 100644 index 000000000..5e43672f9 --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/FullAlarmDTO.kt @@ -0,0 +1,24 @@ +package com.ccextractor.ultimate_alarm_clock.communication + +data class FullAlarmDTO( + val id: Int, + val time: String, + val days: List, + val uniqueSyncId: String, + val isEnabled: Int, + val isOneTime: Int, + val fromWatch: Boolean, + val isLocationEnabled: Boolean, + val locationConditionType: Int, + val location: String, + val isWeatherEnabled: Boolean, + val weatherConditionType: Int, + val weatherTypes: List, + val isActivityEnabled: Boolean, + val activityInterval: Int, + val activityConditionType: Int, + val isGuardian: Boolean, + val guardian: String, + val guardianTimer: Int, + val isCall: Boolean +) \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/PhoneSender.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/PhoneSender.kt new file mode 100644 index 000000000..d2858fed5 --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/PhoneSender.kt @@ -0,0 +1,95 @@ +package com.ccextractor.ultimate_alarm_clock.communication + +import android.content.Context +import android.util.Log +import com.google.android.gms.wearable.* +import com.google.gson.Gson +import com.ccextractor.ultimate_alarm_clock.AlarmModel +// import com.ccextractor.ultimate_alarm_clock.communication.MinimalAlarmDTO + +object PhoneSender { + private const val TAG = "UAC_PhoneListSender" +// private const val PATH_ALARM_LIST_SYNC = "/uac_phone_alarm_list_sync"; +// private const val PATH_ACTION = "/uac_alarm_sync/action" + private const val PATH_ACTION_PHONE_TO_WATCH = "/uac_phone_to_watch/action" + private val PATH_ALARM_PHONE_TO_WATCH = "/uac_phone_to_watch/alarm" + +private fun anyToBoolean(value: Any?): Boolean { + return when (value) { + is Boolean -> value + is Number -> value.toInt() == 1 + else -> false + } +} + +fun sendAlarmToWatch(context: Context, alarmDataFromFlutter: Map) { + val watchDataMap = mutableMapOf() + + watchDataMap["uniqueSyncId"] = alarmDataFromFlutter["alarmID"] + watchDataMap["id"] = alarmDataFromFlutter["isarId"] + watchDataMap["time"] = alarmDataFromFlutter["alarmTime"] + watchDataMap["isOneTime"] = if (anyToBoolean(alarmDataFromFlutter["isOneTime"])) 1 else 0 + watchDataMap["isEnabled"] = if (anyToBoolean(alarmDataFromFlutter["isEnabled"])) 1 else 0 + + val daysBoolList = alarmDataFromFlutter["days"] as? List ?: emptyList() + val daysIntList = daysBoolList.mapIndexedNotNull { index, isSet -> if (isSet) index + 1 else null } + watchDataMap["days"] = daysIntList + + watchDataMap["isLocationEnabled"] = anyToBoolean(alarmDataFromFlutter["isLocationEnabled"]) + watchDataMap["location"] = alarmDataFromFlutter["location"] + watchDataMap["locationConditionType"] = alarmDataFromFlutter["locationConditionType"] + + watchDataMap["isWeatherEnabled"] = anyToBoolean(alarmDataFromFlutter["isWeatherEnabled"]) + watchDataMap["weatherTypes"] = alarmDataFromFlutter["weatherTypes"] + watchDataMap["weatherConditionType"] = alarmDataFromFlutter["weatherConditionType"] + + watchDataMap["isActivityEnabled"] = anyToBoolean(alarmDataFromFlutter["isActivityEnabled"]) + watchDataMap["activityInterval"] = alarmDataFromFlutter["activityInterval"] + watchDataMap["activityConditionType"] = alarmDataFromFlutter["activityConditionType"] + + watchDataMap["isGuardian"] = anyToBoolean(alarmDataFromFlutter["isGuardian"]) + watchDataMap["guardian"] = alarmDataFromFlutter["guardian"] + watchDataMap["guardianTimer"] = alarmDataFromFlutter["guardianTimer"] + watchDataMap["isCall"] = anyToBoolean(alarmDataFromFlutter["isCall"]) + + val alarmJson = Gson().toJson(watchDataMap) + Log.d(TAG, "Sending transformed alarm to watch: $alarmJson") + + val putDataMapRequest = PutDataMapRequest.create(PATH_ALARM_PHONE_TO_WATCH) + putDataMapRequest.dataMap.putString("alarm_json", alarmJson) + putDataMapRequest.dataMap.putLong("timestamp", System.currentTimeMillis()) + + val request = putDataMapRequest.asPutDataRequest().setUrgent() + + Wearable.getDataClient(context).putDataItem(request) + .addOnSuccessListener { + Log.d(TAG, "Alarm sent to watch successfully") + } + .addOnFailureListener { + Log.e(TAG, "Failed to send alarm to watch", it) + } +} + + fun sendActionToWatch(context: Context, action: String, alarmId: String) { + Log.d(TAG, "Attempting to send action '$action' for alarm ID $alarmId to watch") + + try { + val dataMap = PutDataMapRequest.create(PATH_ACTION_PHONE_TO_WATCH) + dataMap.dataMap.putString("action", action) + dataMap.dataMap.putString("alarm_id", alarmId) + dataMap.dataMap.putLong("timestamp", System.currentTimeMillis()) + + val request = dataMap.asPutDataRequest().setUrgent() + + Wearable.getDataClient(context).putDataItem(request) + .addOnSuccessListener { + Log.d(TAG, "✅ Successfully sent action '$action' to watch with PATH -> $PATH_ACTION_PHONE_TO_WATCH") + } + .addOnFailureListener { e -> + Log.e(TAG, "❌ Failed to send action to watch.", e) + } + } catch (e: Exception) { + Log.e(TAG, "❌ Exception while sending action to watch.", e) + } + } +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/UACDataLayerListenerService.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/UACDataLayerListenerService.kt new file mode 100644 index 000000000..1ba9909b0 --- /dev/null +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/communicatoin/UACDataLayerListenerService.kt @@ -0,0 +1,77 @@ +package com.ccextractor.ultimate_alarm_clock.communication + +import android.os.Handler +import android.os.Looper +import android.util.Log +import com.google.android.gms.wearable.* +import com.google.gson.Gson +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel + +class UACDataLayerListenerService : WearableListenerService() { + private val TAG = "UAC_DataLayerService" + private val CHANNEL_NAME = "com.ccextractor.uac/alarm_actions" + private val PATH_ACTION_WATCH_TO_PHONE = "/uac_watch_to_phone/action" + private val PATH_ALARM_WATCH_TO_PHONE = "/uac_watch_to_phone/alarm" + + private val mainThreadHandler = Handler(Looper.getMainLooper()) + + companion object { + var flutterEngine: FlutterEngine? = null + } + + override fun onCreate() { + super.onCreate() + } + + override fun onDataChanged(dataEvents: DataEventBuffer) { + Log.d(TAG, "📡 Phone receivers triggered") + + for (event in dataEvents) { + if (event.type != DataEvent.TYPE_CHANGED) continue + + val item = event.dataItem + val path = item.uri.path ?: continue + val dataMap = DataMapItem.fromDataItem(item).dataMap + + Log.d(TAG, "➡ Path: $path | data: $dataMap") + + when { + // ! alarm received is send to the flutter side + path == PATH_ALARM_WATCH_TO_PHONE -> { + val json = dataMap.getString("alarm_json") + val isNewAlarm = dataMap.getBoolean("isNewAlarm", true) + val dto = Gson().fromJson(json, FullAlarmDTO::class.java) + val fullAlarm = dto.toAlarmModel() + Log.d(TAG, "Background Alarm Received: isNewAlarm=$isNewAlarm, fullAlarm=$fullAlarm") + + mainThreadHandler.post { + flutterEngine?.let { + ReceivedAlarmModelToDb.sendToFlutter(it, fullAlarm, isNewAlarm) + } + ?: Log.w(TAG, "⚠️ flutterEngine not available for alarm sync.") + } + } + + path == PATH_ACTION_WATCH_TO_PHONE -> { + val action = dataMap.getString("action") + val alarmId = dataMap.getString("uniqueSyncId") + val timestamp = dataMap.getLong("timestamp") + + Log.d(TAG, "Received Action: '$action' for alarmId: $alarmId at $timestamp") + + mainThreadHandler.post { + flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger -> + MethodChannel(messenger, CHANNEL_NAME) + .invokeMethod( + "handleReceivedAction", + mapOf("action" to action, "alarmId" to alarmId) + ) + } + ?: Log.w(TAG, "⚠️ flutterEngine not available for action sync.") + } + } + } + } + } +} \ No newline at end of file diff --git a/lib/app/communication/communication.dart b/lib/app/communication/communication.dart new file mode 100644 index 000000000..46602e4f9 --- /dev/null +++ b/lib/app/communication/communication.dart @@ -0,0 +1,33 @@ +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:ultimate_alarm_clock/app/data/models/alarm_model.dart'; +import 'package:ultimate_alarm_clock/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart'; +import 'package:ultimate_alarm_clock/app/modules/home/controllers/home_controller.dart'; + +class AlarmSyncHandler { + static const MethodChannel _channel = + MethodChannel("com.ccextractor.alarm_channel"); + + static void initListener() { + _channel.setMethodCallHandler((call) async { + if (call.method == "onWatchAlarmReceived") { + try { + final Map payload = call.arguments; + final Map alarmMap = Map.from(payload['alarmMap']); + final bool isNewAlarm = payload['isNewAlarm']; + final AlarmModel alarm = AlarmModel.fromMap(alarmMap); + + if (isNewAlarm) { + print("AlarmSyncHandler: Calling static create handler..."); + await AddOrUpdateAlarmController.handleWatchCreate(alarm); + } else { + print("AlarmSyncHandler: Calling static update handler..."); + await AddOrUpdateAlarmController.handleWatchUpdate(alarm); + } + } catch (e, st) { + print("AlarmSyncHandler: Failed to process incoming alarm: $e\n$st"); + } + } + }); + } +} \ No newline at end of file diff --git a/lib/app/communication/native_action_handler.dart b/lib/app/communication/native_action_handler.dart new file mode 100644 index 000000000..b4fe5237b --- /dev/null +++ b/lib/app/communication/native_action_handler.dart @@ -0,0 +1,51 @@ +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:ultimate_alarm_clock/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart'; +import 'package:ultimate_alarm_clock/app/modules/alarmRing/controllers/alarm_ring_controller.dart'; +import 'package:ultimate_alarm_clock/app/modules/home/controllers/home_controller.dart'; + +class NativeActionHandler extends GetxService { + static const _channel = MethodChannel('com.ccextractor.uac/alarm_actions'); + Future init() async { + _channel.setMethodCallHandler(_handleNativeActions); + return this; + } + + Future _handleNativeActions(MethodCall call) async { + if (call.method == 'handleReceivedAction') { + final action = call.arguments['action'] as String?; + final uniqueSyncId = call.arguments['alarmId'] as String?; + + if (uniqueSyncId == null) { + print("NativeActionHandler: Received action '$action' but alarmId was null. Aborting."); + return; + } + + print("NativeActionHandler: Received '$action' action for $uniqueSyncId"); + + switch (action) { + case 'delete alarm': + await AddOrUpdateAlarmController.handleWatchDelete(uniqueSyncId); + // Refresh the UI if the HomeController is active + if (Get.isRegistered()) { + final HomeController homeController = Get.find(); + await Future.delayed(const Duration(milliseconds: 200)); + await homeController.refreshUpcomingAlarms(); + } + break; + + case 'snooze': + if (Get.isRegistered()) { + Get.find().startSnooze(); + } + break; + + case 'dismiss': + if (Get.isRegistered()) { + await Get.find().dismissAlarm(); + } + break; + } + } + } +} \ No newline at end of file diff --git a/lib/app/data/providers/isar_provider.dart b/lib/app/data/providers/isar_provider.dart index d48b1f7e3..88533f71f 100644 --- a/lib/app/data/providers/isar_provider.dart +++ b/lib/app/data/providers/isar_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:isar/isar.dart'; import 'package:path_provider/path_provider.dart'; @@ -11,6 +12,7 @@ import 'package:ultimate_alarm_clock/app/data/models/saved_emails.dart'; import 'package:ultimate_alarm_clock/app/data/models/timer_model.dart'; import 'package:ultimate_alarm_clock/app/data/providers/firestore_provider.dart'; import 'package:ultimate_alarm_clock/app/data/providers/get_storage_provider.dart'; +import 'package:ultimate_alarm_clock/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart'; import 'package:ultimate_alarm_clock/app/utils/utils.dart'; import 'package:sqflite/sqflite.dart'; @@ -519,6 +521,12 @@ class IsarDb { where: 'alarmID = ?', whereArgs: [alarmRecord.alarmID], ); + try { + await AddOrUpdateAlarmController().syncAlarmToWatch(alarmRecord); + debugPrint("Successfully sent toggled alarm state to the watch: $alarmRecord"); + } catch (e) { + debugPrint("Failed to send toggled alarm state to the watch: $e"); + } } } @@ -631,6 +639,27 @@ class IsarDb { ); } } + + // need this for companion app // UniqueId = alarmID on phone. + static Future deleteAlarmByUniqueId(String alarmID) async { + final isarProvider = IsarDb(); + final db = await isarProvider.db; + + // Find the alarm in the database using the unique alarmID + final alarmToDelete = await db.alarmModels + .where() + .filter() + .alarmIDEqualTo(alarmID) + .findFirst(); + + // If an alarm is found, use its isarId to delete it + if (alarmToDelete != null) { + print("Found alarm with alarmID $alarmID. Deleting it now."); + await IsarDb.deleteAlarm(alarmToDelete.isarId); + } else { + print("Could not find alarm with alarmID $alarmID to delete."); + } + } // Timer Functions diff --git a/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart b/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart index 3d68c84b6..dd4238568 100644 --- a/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart +++ b/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -7,6 +8,7 @@ import 'package:collection/collection.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; import 'package:fl_location/fl_location.dart'; +import 'package:isar/isar.dart'; import 'package:latlong2/latlong.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:path_provider/path_provider.dart'; @@ -17,6 +19,7 @@ import 'package:ultimate_alarm_clock/app/data/models/user_model.dart'; import 'package:ultimate_alarm_clock/app/data/providers/firestore_provider.dart'; import 'package:ultimate_alarm_clock/app/data/providers/get_storage_provider.dart'; import 'package:ultimate_alarm_clock/app/data/providers/isar_provider.dart' as isar; +import 'package:ultimate_alarm_clock/app/data/providers/isar_provider.dart'; import 'package:ultimate_alarm_clock/app/data/providers/push_notifications.dart'; import 'package:ultimate_alarm_clock/app/data/providers/secure_storage_provider.dart'; import 'package:ultimate_alarm_clock/app/data/models/ringtone_model.dart'; @@ -36,6 +39,16 @@ class AddOrUpdateAlarmController extends GetxController { final labelController = TextEditingController(); ThemeController themeController = Get.find(); SettingsController settingsController = Get.find(); + + static const MethodChannel watchSyncChannel = MethodChannel('watch_action_channel'); + Future syncAlarmToWatch(AlarmModel alarm) async { + try { + await watchSyncChannel.invokeMethod('sendAlarmToWatch', {...alarm.toSQFliteMap(), 'isarId': alarm.isarId}); + debugPrint('Successfully requested sync for alarm: ${alarm.toSQFliteMap()}'); + } on PlatformException catch (e) { + debugPrint("Failed to sync alarm to watch: ${e.message}"); + } + } final Rx userModel = Rx(null); var alarmID = const Uuid().v4(); @@ -447,7 +460,7 @@ class AddOrUpdateAlarmController extends GetxController { return true; } - createAlarm(AlarmModel alarmData) async { + Future createAlarm(AlarmModel alarmData) async { if (isSharedAlarmEnabled.value == true) { alarmRecord.value = await FirestoreDb.addAlarm(userModel.value, alarmData); @@ -460,6 +473,9 @@ class AddOrUpdateAlarmController extends GetxController { alarmRecord: alarmData, ); }); + debugPrint("Created alarm with ID: ${alarmData.isarId}"); + + await syncAlarmToWatch(alarmData); } showQRDialog() { @@ -661,6 +677,63 @@ class AddOrUpdateAlarmController extends GetxController { detectedQrValue.value = retake ? '' : qrValue.value; } + static Future handleWatchCreate(AlarmModel alarm) async { + await isar.IsarDb.addAlarm(alarm); + print("AddOrUpdateAlarmController: Static handler created new alarm."); + } + + static Future handleWatchUpdate(AlarmModel alarmFromWatch) async { + final db = await isar.IsarDb().db; + final homeController = Get.find(); + + final existingAlarm = await db.alarmModels + .where() + .filter() + .alarmIDEqualTo(alarmFromWatch.alarmID) + .findFirst(); + + if (existingAlarm != null) { + print("AddOrUpdateAlarmController: Found existing alarm. Proceeding with update."); + alarmFromWatch.isarId = existingAlarm.isarId; + + try { + await homeController.alarmChannel.invokeMethod('cancelAlarmById', { + 'alarmID': existingAlarm.alarmID, + 'isSharedAlarm': false, + }); + print('Canceled existing local alarm before update: ${existingAlarm.alarmID}'); + } catch (e) { + print('Error canceling existing alarm during watch sync: $e'); + } + + await isar.IsarDb.updateAlarm(alarmFromWatch); + print("AddOrUpdateAlarmController: Static handler updated existing alarm in DB."); + await homeController.refreshUpcomingAlarms(); + } else { + print("AddOrUpdateAlarmController: Could not find alarm to update. Creating it instead."); + await isar.IsarDb.addAlarm(alarmFromWatch); + await homeController.refreshUpcomingAlarms(); + } + } + + static Future handleWatchDelete(String uniqueSyncId) async { + final homeController = Get.find(); + + // CANCEL the scheduled native alarm first + try { + debugPrint('handleWatchDelete called'); + await homeController.alarmChannel.invokeMethod('cancelAlarmById', { + 'alarmID': uniqueSyncId, + 'isSharedAlarm': false, + }); + print('Canceled native alarm via watch sync: $uniqueSyncId'); + } catch (e) { + print('Error canceling native alarm during watch delete sync: $e'); + } + await isar.IsarDb.deleteAlarmByUniqueId(uniqueSyncId); + print("AddOrUpdateAlarmController: Static handler deleted alarm."); + } + updateAlarm(AlarmModel alarmData) async { // Adding the ID's so it can update depending on the db if (isSharedAlarmEnabled.value == true) { @@ -759,10 +832,11 @@ class AddOrUpdateAlarmController extends GetxController { alarmRecord.value = await isar.IsarDb.addAlarm(alarmData); + await syncAlarmToWatch(alarmRecord.value); + debugPrint('✅ Created new normal alarm in local database: ${alarmRecord.value.alarmID}'); - - } else if (await isar.IsarDb.doesAlarmExist(alarmRecord.value.alarmID) == true) { - + } else if (await isar.IsarDb.doesAlarmExist(alarmRecord.value.alarmID) == + true) { debugPrint('📝 Updating existing normal alarm: ${alarmRecord.value.alarmID}'); alarmData.isarId = alarmRecord.value.isarId; @@ -780,8 +854,7 @@ class AddOrUpdateAlarmController extends GetxController { await isar.IsarDb.updateAlarm(alarmData); - - + await syncAlarmToWatch(alarmData); homeController.forceRefreshAfterAlarmUpdate(alarmData.alarmID, false); } else { diff --git a/lib/app/modules/alarmRing/controllers/alarm_ring_controller.dart b/lib/app/modules/alarmRing/controllers/alarm_ring_controller.dart index 017e12df9..7e3f0301b 100644 --- a/lib/app/modules/alarmRing/controllers/alarm_ring_controller.dart +++ b/lib/app/modules/alarmRing/controllers/alarm_ring_controller.dart @@ -30,6 +30,7 @@ import 'package:screen_brightness/screen_brightness.dart'; import '../../home/controllers/home_controller.dart'; class AlarmControlController extends GetxController { + static const MethodChannel watchSyncChannel = MethodChannel('watch_action_channel'); MethodChannel alarmChannel = MethodChannel('ulticlock'); RxString note = ''.obs; Timer? vibrationTimer; @@ -174,6 +175,62 @@ class AlarmControlController extends GetxController { }); } + Future sendToWatch(String action) async { + try { + await watchSyncChannel.invokeMethod('sendActionToWatch', { + 'action': action, + 'id': currentlyRingingAlarm.value.alarmID, + }); + + print('✅ Sent $action action to watch'); + } catch (e) { + print('Failed to send $action to watch: $e'); + } + } + + Future dismissAlarm() async { + Utils.hapticFeedback(); + await sendToWatch('dismiss'); + + debugPrint('🔔 Dismissing alarm via controller method'); + + if (isPreviewMode.value) { + debugPrint('🔔 Preview mode - simple navigation back'); + Get.offAllNamed('/bottom-navigation-bar'); + return; + } + + if (currentlyRingingAlarm.value.isGuardian) { + guardianTimer.cancel(); + debugPrint('🔔 Guardian timer canceled'); + } + + if (currentlyRingingAlarm.value.isSharedAlarmEnabled) { + rememberDismissedAlarm(); + debugPrint('🔔 Blocked shared alarm: ${currentlyRingingAlarm.value.alarmTime}, ID: ${currentlyRingingAlarm.value.firestoreId}'); + } + + await homeController.clearLastScheduledAlarm(); + debugPrint('🔔 Cleared all scheduled alarms'); + + homeController.refreshTimer = true; + debugPrint('🔔 Set refresh flag for alarm scheduling'); + + if (Utils.isChallengeEnabled(currentlyRingingAlarm.value)) { + debugPrint('🔔 Navigating to challenge screen'); + Get.toNamed( + '/alarm-challenge', + arguments: currentlyRingingAlarm.value, + ); + } else { + debugPrint('🔔 Navigating to home screen'); + Get.offAllNamed( + '/bottom-navigation-bar', + arguments: currentlyRingingAlarm.value, + ); + } + } + Future _fadeInAlarmVolume() async { await FlutterVolumeController.setVolume( currentlyRingingAlarm.value.volMin / 10.0, diff --git a/lib/app/modules/alarmRing/views/alarm_ring_view.dart b/lib/app/modules/alarmRing/views/alarm_ring_view.dart index d08b2b059..a93a98e50 100644 --- a/lib/app/modules/alarmRing/views/alarm_ring_view.dart +++ b/lib/app/modules/alarmRing/views/alarm_ring_view.dart @@ -18,6 +18,7 @@ class AlarmControlView extends GetView { AlarmControlView({Key? key}) : super(key: key); ThemeController themeController = Get.find(); + static const MethodChannel watchSyncChannel = MethodChannel('watch_action_channel'); Obx getAddSnoozeButtons( BuildContext context, int snoozeMinutes, String title) { @@ -175,9 +176,10 @@ class AlarmControlView extends GetView { fontWeight: FontWeight.w600, ), ), - onPressed: () { + onPressed: () async { Utils.hapticFeedback(); controller.startSnooze(); + await controller.sendToWatch('snooze'); }, ), ), @@ -218,51 +220,8 @@ class AlarmControlView extends GetView { ), ), onPressed: () async { - Utils.hapticFeedback(); - debugPrint('🔔 Dismiss button pressed'); - - // Handle preview mode differently - if (controller.isPreviewMode.value) { - debugPrint('🔔 Preview mode - simple navigation back'); - Get.offAllNamed('/bottom-navigation-bar'); - return; - } - - if (controller.currentlyRingingAlarm.value.isGuardian) { - controller.guardianTimer.cancel(); - debugPrint('🔔 Guardian timer canceled'); - } - - - if (controller.currentlyRingingAlarm.value.isSharedAlarmEnabled) { - controller.rememberDismissedAlarm(); - debugPrint('🔔 Blocked shared alarm: ${controller.currentlyRingingAlarm.value.alarmTime}, ID: ${controller.currentlyRingingAlarm.value.firestoreId}'); - } - - - await controller.homeController.clearLastScheduledAlarm(); - debugPrint('🔔 Cleared all scheduled alarms'); - - - controller.homeController.refreshTimer = true; - debugPrint('🔔 Set refresh flag for alarm scheduling'); - - - if (Utils.isChallengeEnabled( - controller.currentlyRingingAlarm.value, - )) { - debugPrint('🔔 Navigating to challenge screen'); - Get.toNamed( - '/alarm-challenge', - arguments: controller.currentlyRingingAlarm.value, - ); - } else { - debugPrint('🔔 Navigating to home screen'); - Get.offAllNamed( - '/bottom-navigation-bar', - arguments: controller.currentlyRingingAlarm.value, - ); - } + await controller.dismissAlarm(); + // removed the controller logic from view :) }, child: Text( Utils.isChallengeEnabled( diff --git a/lib/app/modules/home/bindings/home_binding.dart b/lib/app/modules/home/bindings/home_binding.dart index 76ec7ba28..599b748f2 100644 --- a/lib/app/modules/home/bindings/home_binding.dart +++ b/lib/app/modules/home/bindings/home_binding.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import 'package:ultimate_alarm_clock/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart'; import 'package:ultimate_alarm_clock/app/modules/bottomNavigationBar/controllers/bottom_navigation_bar_controller.dart'; import '../controllers/home_controller.dart'; @@ -10,6 +11,7 @@ class HomeBinding extends Bindings { HomeController(), ); + Get.lazyPut(()=>AddOrUpdateAlarmController()); Get.lazyPut( () => BottomNavigationBarController(), diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 5b291a270..359e32c83 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -34,6 +34,7 @@ class Pair { class HomeController extends GetxController { MethodChannel alarmChannel = const MethodChannel('ulticlock'); + static const MethodChannel watchSyncChannel = MethodChannel('watch_action_channel'); Stream? firestoreStreamAlarms; Stream? sharedAlarmsStream; @@ -418,6 +419,7 @@ class HomeController extends GetxController { @override void onInit() async { super.onInit(); + refreshUpcomingAlarms(); // Clear all alarm tracking on init to ensure clean startup recentlyDismissedAlarmIds.clear(); @@ -1330,6 +1332,13 @@ class HomeController extends GetxController { } await FirestoreDb.deleteAlarm(user, alarm.firestoreId!); + + if (alarmToDelete != null) { + await watchSyncChannel.invokeMethod('sendActionToWatch', { + 'action': 'delete alarm', + 'id': alarmToDelete.alarmID, + }); + } } else { alarmToDelete = await IsarDb.getAlarm(alarm.isarId); @@ -1345,6 +1354,12 @@ class HomeController extends GetxController { } await IsarDb.deleteAlarm(alarm.isarId); + if (alarmToDelete != null) { + await watchSyncChannel.invokeMethod('sendActionToWatch', { + 'action': 'delete alarm', + 'id': alarmToDelete.alarmID, + }); + } } if (Get.isSnackbarOpen) { diff --git a/lib/app/modules/home/views/home_view.dart b/lib/app/modules/home/views/home_view.dart index 2f2cf85ca..d8b446fa9 100644 --- a/lib/app/modules/home/views/home_view.dart +++ b/lib/app/modules/home/views/home_view.dart @@ -115,7 +115,7 @@ class HomeView extends GetView { .withOpacity( 0.75, ), - fontSize: 14 * + fontSize: 13 * controller .scalingFactor .value, diff --git a/lib/app/modules/splashScreen/controllers/splash_screen_controller.dart b/lib/app/modules/splashScreen/controllers/splash_screen_controller.dart index b2e7e10e8..c2549759f 100644 --- a/lib/app/modules/splashScreen/controllers/splash_screen_controller.dart +++ b/lib/app/modules/splashScreen/controllers/splash_screen_controller.dart @@ -15,6 +15,7 @@ import '../../home/controllers/home_controller.dart'; class SplashScreenController extends GetxController { MethodChannel alarmChannel = const MethodChannel('ulticlock'); MethodChannel timerChannel = const MethodChannel('timer'); + static const MethodChannel watchChannel = MethodChannel('com.ccextractor.uac/alarm_actions'); bool shouldAlarmRing = true; bool shouldNavigate = true; @@ -30,6 +31,19 @@ class SplashScreenController extends GetxController { return latestAlarm; } + // void initWatchActionListener() { + // watchChannel.setMethodCallHandler((call) async { + // if (call.method == "handleReceivedAction") { + // final Map args = call.arguments as Map; + // final String action = args["action"]; + // final int alarmId = args["id"]; + // if (action == "delete") { + // debugPrint("Alarm deleted from watch: $alarmId"); + // } + // } + // }); + // } + getNextAlarm() async { UserModel? _userModel = await SecureStorageProvider().retrieveUserModel(); AlarmModel _alarmRecord = homeController.genFakeAlarmModel(); diff --git a/lib/main.dart b/lib/main.dart index d995478b3..586878b75 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,11 +6,14 @@ import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:ultimate_alarm_clock/app/data/providers/get_storage_provider.dart'; import 'package:ultimate_alarm_clock/app/data/providers/push_notifications.dart'; +import 'package:ultimate_alarm_clock/app/modules/debug/controllers/debug_controller.dart'; import 'package:ultimate_alarm_clock/app/modules/settings/controllers/theme_controller.dart'; import 'package:sqflite/sqflite.dart'; import 'package:ultimate_alarm_clock/app/utils/language.dart'; import 'package:ultimate_alarm_clock/app/utils/constants.dart'; import 'package:ultimate_alarm_clock/app/utils/custom_error_screen.dart'; +import 'package:ultimate_alarm_clock/app/communication/communication.dart'; +import 'package:ultimate_alarm_clock/app/communication/native_action_handler.dart'; import 'firebase_options.dart'; import 'app/routes/app_pages.dart'; @@ -18,6 +21,7 @@ Locale? loc; void main() async { WidgetsFlutterBinding.ensureInitialized(); + AlarmSyncHandler.initListener(); await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); @@ -30,11 +34,13 @@ void main() async { await Get.putAsync(() => GetStorageProvider().init()); + await Get.putAsync(() => NativeActionHandler().init()); final storage = Get.find(); loc = await storage.readLocale(); final ThemeController themeController = Get.put(ThemeController()); + Get.put(ThemeController()); AudioPlayer.global.setAudioContext( const AudioContext(