Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<application
android:label="UAC Companion"
Expand All @@ -17,6 +18,17 @@
<receiver android:name=".AlarmBroadcastReceiver" android:exported="true" />
<receiver android:name=".AlarmDismissReceiver" android:exported="true"/>
<receiver android:name=".AlarmSnoozeReceiver" android:exported="true"/>

<!-- Boot receiver to reschedule alarms after device reboot -->
<receiver
android:name=".BootBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>

<uses-library
android:name="com.google.android.wearable"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.ccextractor.uac_companion

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

/**
* BootBroadcastReceiver listens for device boot completion.
* When the device restarts, all pending alarms are cleared by the Android system.
* This receiver reschedules all active alarms from the database.
*/
class BootBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "BootBroadcastReceiver"
}

override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
Log.d(TAG, "Device boot completed. Rescheduling alarms...")
Comment on lines +16 to +20
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent filter in AndroidManifest.xml includes both BOOT_COMPLETED and QUICKBOOT_POWERON actions, but this receiver only handles BOOT_COMPLETED. The QUICKBOOT_POWERON action (used by some manufacturers for quick boot) will not trigger alarm rescheduling. Consider checking for both actions or using a more general approach.

Suggested change
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
Log.d(TAG, "Device boot completed. Rescheduling alarms...")
private const val ACTION_QUICKBOOT_POWERON = "android.intent.action.QUICKBOOT_POWERON"
private const val ACTION_HTC_QUICKBOOT_POWERON = "com.htc.intent.action.QUICKBOOT_POWERON"
}
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action
if (action == Intent.ACTION_BOOT_COMPLETED ||
action == ACTION_QUICKBOOT_POWERON ||
action == ACTION_HTC_QUICKBOOT_POWERON) {
Log.d(TAG, "Device boot completed or quick boot power on. Rescheduling alarms...")

Copilot uses AI. Check for mistakes.

try {
// Get all enabled alarms from the database
val alarms = AlarmUtils.getAllAlarmsFromDb(context)
val enabledAlarms = alarms.filter { it.isEnabled == 1 }

Log.d(TAG, "Found ${enabledAlarms.size} enabled alarms to reschedule")

// Reschedule each alarm using the AlarmScheduler
// The scheduler will automatically pick the next upcoming alarm
if (enabledAlarms.isNotEmpty()) {
AlarmScheduler.scheduleNextAlarm(context)
Comment on lines +29 to +32
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 29 states "Reschedule each alarm using the AlarmScheduler," but the code actually only schedules the next upcoming alarm via scheduleNextAlarm(context). Based on the AlarmScheduler implementation, this only schedules one alarm at a time (the next upcoming one). The comment should be updated to reflect this behavior, or if all enabled alarms should be scheduled, the implementation needs to be changed.

Copilot uses AI. Check for mistakes.
Log.d(TAG, "Successfully rescheduled alarms after boot")
} else {
Log.d(TAG, "No active alarms to reschedule")
}
Comment on lines +21 to +36
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The enabledAlarms variable is filtered but never used. Lines 24-27 filter for enabled alarms and log the count, but line 32 calls scheduleNextAlarm which internally filters again for enabled alarms. Consider removing the unused variable or using it to avoid redundant filtering.

Suggested change
try {
// Get all enabled alarms from the database
val alarms = AlarmUtils.getAllAlarmsFromDb(context)
val enabledAlarms = alarms.filter { it.isEnabled == 1 }
Log.d(TAG, "Found ${enabledAlarms.size} enabled alarms to reschedule")
// Reschedule each alarm using the AlarmScheduler
// The scheduler will automatically pick the next upcoming alarm
if (enabledAlarms.isNotEmpty()) {
AlarmScheduler.scheduleNextAlarm(context)
Log.d(TAG, "Successfully rescheduled alarms after boot")
} else {
Log.d(TAG, "No active alarms to reschedule")
}
try {
// Delegate rescheduling to AlarmScheduler; it will handle enabled alarms internally
AlarmScheduler.scheduleNextAlarm(context)
Log.d(TAG, "Reschedule request sent to AlarmScheduler after boot")

Copilot uses AI. Check for mistakes.
} catch (e: Exception) {
Log.e(TAG, "Error rescheduling alarms after boot: ${e.message}", e)
}
}
}
}
86 changes: 82 additions & 4 deletions lib/app/modules/smart_control/controllers/location_controller.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:fl_location/fl_location.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:get/get.dart';
import 'package:latlong2/latlong.dart';
Expand All @@ -15,10 +16,83 @@ class LocationController extends GetxController {
{"label": "Cancel Away from Location", "type": 4},
];

//! need to change the defaultLatLng as user's current locaiton
static final LatLng defaultLatLng = LatLng(28.6139, 77.2090);
// Fallback location (New Delhi) if user location is unavailable
static final LatLng fallbackLatLng = LatLng(28.6139, 77.2090);
final MapController mapController = MapController();
var pickerLatLng = defaultLatLng.obs;
var pickerLatLng = fallbackLatLng.obs;
var currentUserLocation = Rxn<LatLng>();
var isLoadingLocation = false.obs;

/// Fetch user's current location
Future<void> fetchCurrentLocation() async {
isLoadingLocation.value = true;

try {
// Check if location service is enabled
if (!await FlLocation.isLocationServicesEnabled) {
Get.snackbar(
'Location Services Disabled',
'Please enable location services',
snackPosition: SnackPosition.BOTTOM,
);
currentUserLocation.value = fallbackLatLng;
isLoadingLocation.value = false;
return;
}

// Check location permission
var permission = await FlLocation.checkLocationPermission();

if (permission == LocationPermission.deniedForever) {
Get.snackbar(
'Location Permission Denied',
'Using default location (New Delhi)',
snackPosition: SnackPosition.BOTTOM,
);
currentUserLocation.value = fallbackLatLng;
isLoadingLocation.value = false;
return;
}

if (permission == LocationPermission.denied) {
// Request permission
permission = await FlLocation.requestLocationPermission();

if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
Get.snackbar(
'Location Permission Denied',
'Using default location (New Delhi)',
snackPosition: SnackPosition.BOTTOM,
);
currentUserLocation.value = fallbackLatLng;
isLoadingLocation.value = false;
return;
}
}

// Get current location
final location = await FlLocation.getLocation(
accuracy: LocationAccuracy.best,
timeLimit: const Duration(seconds: 10),
);

if (location.latitude != 0.0 && location.longitude != 0.0) {
currentUserLocation.value = LatLng(location.latitude, location.longitude);
} else {
currentUserLocation.value = fallbackLatLng;
}
} catch (e) {
Get.snackbar(
'Location Error',
'Could not fetch location. Using default location.',
snackPosition: SnackPosition.BOTTOM,
);
currentUserLocation.value = fallbackLatLng;
} finally {
isLoadingLocation.value = false;
}
}

void onPickerScreenReady() {
if (mapController.camera.center != pickerLatLng.value) {
Expand All @@ -27,7 +101,11 @@ class LocationController extends GetxController {
}

Future<void> onSelectCondition(int index) async {
pickerLatLng.value = defaultLatLng;
// Fetch current location before opening picker
await fetchCurrentLocation();

// Use current user location or fallback
pickerLatLng.value = currentUserLocation.value ?? fallbackLatLng;

final result = await Get.toNamed(AppRoutes.locationPicker);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,45 +65,61 @@ class LocationConditionScreen extends StatelessWidget {
required bool isRound,
required bool isSelected,
}) {
return GestureDetector(
onTap: () => LocationController.to.onSelectCondition(index),
child: Container(
margin: EdgeInsets.symmetric(vertical: isRound ? 4 : 6),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: isSelected
? uac_colors.AppColors.green.withOpacity(0.2)
: uac_colors.AppColors.grayBlack,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment:
isRound ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
Icon(
_getLocationConditionIcon(type),
size: isRound ? 16 : 18,
color: isSelected ? uac_colors.AppColors.green : Colors.white,
),
const SizedBox(width: 8),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize:
isSelected ? (isRound ? 13 : 15) : (isRound ? 12 : 14),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color:
isSelected ? uac_colors.AppColors.green : Colors.white,
return Obx(() {
final isLoading = LocationController.to.isLoadingLocation.value;

return GestureDetector(
onTap: isLoading ? null : () => LocationController.to.onSelectCondition(index),
child: Container(
margin: EdgeInsets.symmetric(vertical: isRound ? 4 : 6),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: isSelected
? uac_colors.AppColors.green.withOpacity(0.2)
: uac_colors.AppColors.grayBlack,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisAlignment:
isRound ? MainAxisAlignment.center : MainAxisAlignment.start,
children: [
if (isLoading && isSelected)
SizedBox(
width: isRound ? 16 : 18,
height: isRound ? 16 : 18,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
uac_colors.AppColors.green,
),
),
)
else
Icon(
_getLocationConditionIcon(type),
size: isRound ? 16 : 18,
color: isSelected ? uac_colors.AppColors.green : Colors.white,
),
const SizedBox(width: 8),
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize:
isSelected ? (isRound ? 13 : 15) : (isRound ? 12 : 14),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
color:
isSelected ? uac_colors.AppColors.green : Colors.white,
),
),
),
),
],
],
),
),
),
);
);
});
}

IconData _getLocationConditionIcon(int type) {
Expand Down
Loading