mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Add Third Party Alarm Intent API: Set and Dismiss alarms for your device from any app (#6088)
Adds the capability to set and dismiss alarms from third party apps through an Intent API, fixes #5553 **This PR was co-authored by ChatGPT and José Rebelo (passively):** - Scaffold for the core component `DeviceAlarmReceiver` was taken from José's `DeviceSettingsReceiver` - The core implementation was written by me by hand, ChatGPT was used to review and find potential issues and bugs - The `DeviceAlarmReceiverTest` unit tests were written by ChatGPT and reviewed by me **About these changes:** - Adds a new developer settings entry for the device that the user needs to enable, so other apps can use the Intent API for such device - Supports app blacklisting as it is done in the `DeviceSettingsReceiver` - `ConfigureAlarms.addMissingAlarms()` was migrated to `DBHelper.fillMissingAlarms()` to provide this abstraction globally - `AlarmReceiver` was migrated to `SunriseSunsetAlarmReceiver` to avoid ambiguity with this feature; the formerly called `AlarmReceiver` was related to setting calendar events based on the sunrise/sunset via an Intent API - The API is registered in the `DeviceCommunicationService` - The `ConfigureAlarms` activity updates its list after setting alarms from a third party app by broadcasting `ACTION_SAVE_ALARMS` - It is possible to set alarms of a device that is offline, it should sync when the device connects next time, unless it downloads the alarms from the device when connecting - However, this device-to-GadgetBridge alarm sync API is not commonly abstracted, so we cannot respect existing alarms on the devices when changing alarms within the new Intent API; the database is the single source of truth and connecting the device after using the API will reset alarms - ~~The API was designed to align with the `AlarmClock` API from Android, but~~ there is no feature parity to improve the design and interaction; after all the `AlarmClock` API is interacted with `startActivity`, but `GadgetBridge` uses `sendBroadcast` receivers, which is recommend for background tasks - Errors are not raised to the caller (i.e. providing unknown or invalid Mac Address of device or trying to set an alarm on a nonexisting slot); these errors are logged though **EDIT: Additional changes during the development:** - Since slot IDs are unpredictable, the flow is title-driven (there is no way to provide an ID) - Dismissing an alarm through the API removes its title - Only alarms without a title can be overwritten and a title should be set through this API to retain the ability to dismiss it without dismissing all alarms of the user - Dismissing all alarms regardless of the title is still possible - The title is exposed in the UI not just when the device supports it, but also when this API is enabled in the developer settings of the device itself **Example usage:** - https://gitlab.com/martin-braun/warpclock-plus/-/blob/feat/gb/app/src/main/java/com/antonok/warpclock/AlarmIntentService.kt demonstrates dismissing all alarms and setting multiple alarms (search for `sendBroadcast`) - `DeviceAlarmReceiverTest` should cover all cases for a more complete overview of the capabilities of the API Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6088
This commit is contained in:
committed by
José Rebelo
parent
0c11ba44f8
commit
b8a85e03ea
@@ -1,5 +1,6 @@
|
||||
/* Copyright (C) 2015-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniel
|
||||
Dakhno, Daniele Gobbetti, Dmitry Markin, Lem Dulfo, Taavi Eomäe, Martin.JM
|
||||
Dakhno, Daniele Gobbetti, Dmitry Markin, Lem Dulfo, Martin.JM, Martin Braun,
|
||||
Taavi Eomäe
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -167,7 +168,7 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
binding.soundModeSpinner.setAdapter(adapter);
|
||||
}
|
||||
|
||||
binding.title.setVisibility(supportsTitle() ? View.VISIBLE : View.GONE);
|
||||
binding.title.setVisibility(shouldShowTitle() ? View.VISIBLE : View.GONE);
|
||||
binding.title.setText(alarm.getTitle());
|
||||
|
||||
final int titleLimit = getAlarmTitleLimit();
|
||||
@@ -202,8 +203,10 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
return device.getDeviceCoordinator().forcedSmartWakeup(device, position);
|
||||
}
|
||||
|
||||
private boolean supportsTitle() {
|
||||
return device.getDeviceCoordinator().supportsAlarmTitle(device);
|
||||
private boolean shouldShowTitle() {
|
||||
return device.getDeviceCoordinator().supportsAlarmTitle(device) ||
|
||||
GBApplication.getDeviceSpecificSharedPrefs(device.getAddress())
|
||||
.getBoolean("third_party_apps_set_alarms", false);
|
||||
}
|
||||
|
||||
private int getAlarmTitleLimit() {
|
||||
|
||||
+2
-27
@@ -1,6 +1,6 @@
|
||||
/* Copyright (C) 2015-2024 Andreas Shimokawa, Carsten Pfeiffer, Damien
|
||||
Gaignon, Daniel Dakhno, Daniele Gobbetti, Dmitry Markin, José Rebelo,
|
||||
Lem Dulfo, Petr Vaněk
|
||||
Lem Dulfo, Martin Braun, Petr Vaněk
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ConfigureAlarms extends AbstractGBActivity {
|
||||
alarms = AlarmUtils.readAlarmsFromPrefs(getGbDevice());
|
||||
storeMigratedAlarms(alarms);
|
||||
}
|
||||
addMissingAlarms(alarms);
|
||||
DBHelper.fillMissingAlarms(gbDevice, alarms);
|
||||
|
||||
mGBAlarmListAdapter.setAlarmList(alarms);
|
||||
mGBAlarmListAdapter.notifyDataSetChanged();
|
||||
@@ -115,31 +115,6 @@ public class ConfigureAlarms extends AbstractGBActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void addMissingAlarms(List<Alarm> alarms) {
|
||||
DeviceCoordinator coordinator = getGbDevice().getDeviceCoordinator();
|
||||
int supportedNumAlarms = coordinator.getAlarmSlotCount(getGbDevice());
|
||||
if (supportedNumAlarms > alarms.size()) {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
DaoSession daoSession = db.getDaoSession();
|
||||
for (int position = 0; position < supportedNumAlarms; position++) {
|
||||
boolean found = false;
|
||||
for (Alarm alarm : alarms) {
|
||||
if (alarm.getPosition() == position) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LOG.info("adding missing alarm at position {}", position);
|
||||
alarms.add(position, AlarmUtils.createDefaultAlarm(daoSession, getGbDevice(), position));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error accessing database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
|
||||
+6
@@ -82,6 +82,12 @@ public class DeviceSpecificSettings implements Parcelable {
|
||||
return subScreenScreens;
|
||||
}
|
||||
|
||||
public List<Integer> addRootScreen(final DeviceSpecificSettingsScreen screen, final List<Integer> subScreens) {
|
||||
final List<Integer> subScreenScreens = addRootScreen(screen);
|
||||
subScreenScreens.addAll(subScreens);
|
||||
return subScreenScreens;
|
||||
}
|
||||
|
||||
public void addRootScreen(final int index, @XmlRes final int screen) {
|
||||
rootScreens.add(index, screen);
|
||||
}
|
||||
|
||||
+12
-2
@@ -65,6 +65,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -1664,11 +1665,20 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
R.xml.devicesettings_device_support_can_reconnect
|
||||
);
|
||||
|
||||
deviceSpecificSettings.addRootScreen(
|
||||
DeviceSpecificSettingsScreen.DEVELOPER,
|
||||
final List<Integer> intentApiSubScreens = new ArrayList<>();
|
||||
Collections.addAll(
|
||||
intentApiSubScreens,
|
||||
R.xml.devicesettings_header_intent_api,
|
||||
R.xml.devicesettings_settings_third_party_apps
|
||||
);
|
||||
if (coordinator.getAlarmSlotCount(device) > 0) {
|
||||
intentApiSubScreens.add(R.xml.devicesettings_alarms_third_party_apps);
|
||||
}
|
||||
deviceSpecificSettings.addRootScreen(
|
||||
DeviceSpecificSettingsScreen.DEVELOPER,
|
||||
intentApiSubScreens
|
||||
);
|
||||
|
||||
if (coordinator.getConnectionType().usesBluetoothLE()) {
|
||||
deviceSpecificSettings.addRootScreen(
|
||||
DeviceSpecificSettingsScreen.DEVELOPER,
|
||||
|
||||
+34
-1
@@ -1,5 +1,5 @@
|
||||
/* Copyright (C) 2015-2024 Andreas Shimokawa, Arjan Schrijver, Carsten
|
||||
Pfeiffer
|
||||
Pfeiffer, Martin Braun
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -17,11 +17,19 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ItemWithDetails;
|
||||
|
||||
/**
|
||||
@@ -52,4 +60,29 @@ public class ItemWithDetailsAdapter extends AbstractItemAdapter<ItemWithDetails>
|
||||
protected Bitmap getPreview(ItemWithDetails item) {
|
||||
return item.getPreview();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public View getView(final int position, final View view, final ViewGroup parent) {
|
||||
final View itemView = super.getView(position, view, parent);
|
||||
final ItemWithDetails item = getItem(position);
|
||||
|
||||
if (item != null && item.getDetails() != null && !item.getDetails().isEmpty()) {
|
||||
itemView.setOnClickListener(v -> {
|
||||
final ClipboardManager clipboard = (ClipboardManager)
|
||||
getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboard != null) {
|
||||
clipboard.setPrimaryClip(
|
||||
ClipData.newPlainText(item.getName(), item.getDetails())
|
||||
);
|
||||
Toast.makeText(getContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
|
||||
return itemView;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ValidByDate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
|
||||
@@ -558,6 +559,52 @@ public class DBHelper {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all user-configurable alarms for the given user and device filled with default alarms
|
||||
* for unoccupied slots. The list is sorted by {@link Alarm#getPosition()}. Calendar events that
|
||||
* may also be modeled as alarms are not stored in the database and hence not returned by this
|
||||
* method.
|
||||
* @param gbDevice the device for which the alarms shall be loaded
|
||||
* @return the list of alarms for the given device
|
||||
*/
|
||||
@NonNull
|
||||
public static List<Alarm> getAlarmsWithDefaults(@NonNull GBDevice gbDevice) {
|
||||
List<Alarm> alarms = getAlarms(gbDevice);
|
||||
fillMissingAlarms(gbDevice, alarms);
|
||||
return alarms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills default alarms in the alarm list for unoccupied slots of the device.
|
||||
* @param gbDevice the device for which the alarms shall be loaded
|
||||
* @param alarms list of alarms to fill
|
||||
*/
|
||||
public static void fillMissingAlarms(@NonNull GBDevice gbDevice, List<Alarm> alarms) {
|
||||
DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
|
||||
int supportedNumAlarms = coordinator.getAlarmSlotCount(gbDevice);
|
||||
if (supportedNumAlarms > alarms.size()) {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
DaoSession daoSession = db.getDaoSession();
|
||||
for (int position = 0; position < supportedNumAlarms; position++) {
|
||||
boolean found = false;
|
||||
for (Alarm alarm : alarms) {
|
||||
if (alarm.getPosition() == position) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LOG.info("adding missing alarm at position {}", position);
|
||||
alarms.add(position, AlarmUtils.createDefaultAlarm(daoSession, gbDevice, position));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error accessing database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void store(Alarm alarm) {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
DaoSession daoSession = db.getDaoSession();
|
||||
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
/* Copyright (C) 2022-2026 José Rebelo, Martin Braun
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
|
||||
public class DeviceAlarmReceiver extends BroadcastReceiver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DeviceAlarmReceiver.class);
|
||||
|
||||
private static final String macAddrPattern = "^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$";
|
||||
|
||||
public static final String COMMAND_SET_ALARM = "nodomain.freeyourgadget.gadgetbridge.command.SET_ALARM";
|
||||
public static final String COMMAND_DISMISS_ALARM = "nodomain.freeyourgadget.gadgetbridge.command.DISMISS_ALARM";
|
||||
|
||||
public static final String EXTRA_MAC_ADDR = "device";
|
||||
public static final String EXTRA_DAYS = "days";
|
||||
public static final String EXTRA_HOUR = "hour";
|
||||
public static final String EXTRA_MINUTES = "minutes";
|
||||
public static final String EXTRA_TITLE = "title";
|
||||
public static final String EXTRA_ALARM_SEARCH_MODE = "mode";
|
||||
public static final String ALARM_SEARCH_MODE_ALL = "all";
|
||||
public static final String ALARM_SEARCH_MODE_TIME = "time";
|
||||
public static final String ALARM_SEARCH_MODE_TITLE = "title";
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
|
||||
if (!COMMAND_SET_ALARM.equals(action) && !COMMAND_DISMISS_ALARM.equals(action)) {
|
||||
LOG.warn("Unexpected action {}", intent.getAction());
|
||||
}
|
||||
LOG.debug("Got third party alarm action: {}", action);
|
||||
|
||||
final String deviceAddress = intent.getStringExtra(EXTRA_MAC_ADDR);
|
||||
|
||||
if (deviceAddress == null) {
|
||||
LOG.warn("Missing device address");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deviceAddress.matches(macAddrPattern)) {
|
||||
LOG.warn("Device address '{}' does not match '{}'", deviceAddress, macAddrPattern);
|
||||
return;
|
||||
}
|
||||
|
||||
final GBDevice targetDevice = GBApplication.app()
|
||||
.getDeviceManager()
|
||||
.getDeviceByAddress(deviceAddress);
|
||||
|
||||
if (targetDevice == null) {
|
||||
LOG.warn("Unknown device {}", deviceAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(targetDevice.getAddress());
|
||||
|
||||
if (!prefs.getBoolean("third_party_apps_set_alarms", false)) {
|
||||
LOG.warn("Setting alarms from 3rd party apps not allowed for {}", deviceAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: sync alarms back from device to DB to avoid losing device-exclusive changes
|
||||
// (this requires a common interface for back-syncing alarms first)
|
||||
final ArrayList<Alarm> alarms = (ArrayList<Alarm>) DBHelper.getAlarmsWithDefaults(targetDevice);
|
||||
if (alarms.isEmpty()) {
|
||||
LOG.error("Alarms are not supported on this device");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
final String mode = intent.getStringExtra(EXTRA_ALARM_SEARCH_MODE);
|
||||
final String title = intent.getStringExtra(EXTRA_TITLE);
|
||||
if (COMMAND_SET_ALARM.equals(action)) {
|
||||
final int hour = intent.getIntExtra(EXTRA_HOUR, -1);
|
||||
final int minute = intent.getIntExtra(EXTRA_MINUTES, -1);
|
||||
if ((hour == -1 || minute == -1)) {
|
||||
LOG.error("Both hour and minutes have to be provided when creating an alarm");
|
||||
return;
|
||||
}
|
||||
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||||
LOG.error("Invalid time provided");
|
||||
return;
|
||||
}
|
||||
final ArrayList<Integer> days = intent.getIntegerArrayListExtra(EXTRA_DAYS); // Calendar.SUNDAY, Calendar.MONDAY, Cal..;
|
||||
int repetition = convertRepetitionMask(days);
|
||||
// find the next free slot: a dismissed alarm without any title
|
||||
// (this way the user can protect dismissed alarms by naming them)
|
||||
for (Alarm alarm : alarms) {
|
||||
if (!alarm.getEnabled() && (
|
||||
alarm.getTitle() == null || alarm.getTitle().isEmpty())) {
|
||||
updateAlarm(
|
||||
alarm, true, hour, minute, repetition,
|
||||
title
|
||||
);
|
||||
DBHelper.store(alarm);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
LOG.error("No free alarm slot was found; A slot is free when it's disabled and has no title");
|
||||
return;
|
||||
}
|
||||
} else if (COMMAND_DISMISS_ALARM.equals(action)) {
|
||||
if (!Objects.equals(mode, ALARM_SEARCH_MODE_TIME) &&
|
||||
!Objects.equals(mode, ALARM_SEARCH_MODE_ALL) &&
|
||||
!Objects.equals(mode, ALARM_SEARCH_MODE_TITLE)) {
|
||||
LOG.error("Unknown mode");
|
||||
return;
|
||||
}
|
||||
final int hour = intent.getIntExtra(EXTRA_HOUR, -1);
|
||||
final int minutes = intent.getIntExtra(EXTRA_MINUTES, -1);
|
||||
if (Objects.equals(mode, ALARM_SEARCH_MODE_TIME) && hour == -1) {
|
||||
LOG.error("Hour has to be provided when dismissing an alarm by time");
|
||||
return;
|
||||
}
|
||||
if (Objects.equals(mode, ALARM_SEARCH_MODE_TITLE)
|
||||
&& (title == null || title.isEmpty())) {
|
||||
LOG.error("Title has to be provided when dismissing an alarm by title");
|
||||
return;
|
||||
}
|
||||
for (Alarm alarm : alarms) {
|
||||
if (Objects.equals(mode, ALARM_SEARCH_MODE_ALL) ||
|
||||
(Objects.equals(mode, ALARM_SEARCH_MODE_TITLE)
|
||||
&& alarm.getTitle() != null
|
||||
&& alarm.getTitle().contains(title)
|
||||
) ||
|
||||
(Objects.equals(mode, ALARM_SEARCH_MODE_TIME)
|
||||
&& alarm.getHour() == hour
|
||||
&& (minutes == -1 || alarm.getMinute() == minutes)
|
||||
)
|
||||
) {
|
||||
// dismiss the alarm and clear its title, so it can be set again
|
||||
updateAlarm(alarm, false, alarm.getHour(), alarm.getMinute(), 0, "");
|
||||
DBHelper.store(alarm);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (!changed) {
|
||||
LOG.warn("No alarm to dismiss was found");
|
||||
}
|
||||
} else {
|
||||
LOG.error("Unknown action");
|
||||
return;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(
|
||||
new Intent(DeviceService.ACTION_SAVE_ALARMS)
|
||||
);
|
||||
GBApplication.deviceService(targetDevice).onSetAlarms(alarms);
|
||||
}
|
||||
}
|
||||
|
||||
private static int convertRepetitionMask(ArrayList<Integer> days) {
|
||||
int repetitionMask = 0;
|
||||
if (days != null && !days.isEmpty()) {
|
||||
for (int day : days) {
|
||||
switch (day) {
|
||||
case Calendar.MONDAY:
|
||||
repetitionMask |= Alarm.ALARM_MON;
|
||||
break;
|
||||
case Calendar.TUESDAY:
|
||||
repetitionMask |= Alarm.ALARM_TUE;
|
||||
break;
|
||||
case Calendar.WEDNESDAY:
|
||||
repetitionMask |= Alarm.ALARM_WED;
|
||||
break;
|
||||
case Calendar.THURSDAY:
|
||||
repetitionMask |= Alarm.ALARM_THU;
|
||||
break;
|
||||
case Calendar.FRIDAY:
|
||||
repetitionMask |= Alarm.ALARM_FRI;
|
||||
break;
|
||||
case Calendar.SATURDAY:
|
||||
repetitionMask |= Alarm.ALARM_SAT;
|
||||
break;
|
||||
case Calendar.SUNDAY:
|
||||
repetitionMask |= Alarm.ALARM_SUN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return repetitionMask;
|
||||
}
|
||||
|
||||
private static void updateAlarm(Alarm alarm, boolean enable, int hour, int minute, int repetition, String title) {
|
||||
alarm.setHour(hour);
|
||||
alarm.setMinute(minute);
|
||||
alarm.setRepetition(repetition);
|
||||
alarm.setTitle(title);
|
||||
alarm.setEnabled(enable);
|
||||
}
|
||||
|
||||
public IntentFilter buildFilter() {
|
||||
final IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(COMMAND_SET_ALARM);
|
||||
intentFilter.addAction(COMMAND_DISMISS_ALARM);
|
||||
return intentFilter;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -39,10 +39,10 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
|
||||
|
||||
public class AlarmReceiver extends BroadcastReceiver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AlarmReceiver.class);
|
||||
public class SunriseSunsetAlarmReceiver extends BroadcastReceiver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SunriseSunsetAlarmReceiver.class);
|
||||
|
||||
public AlarmReceiver() {
|
||||
public SunriseSunsetAlarmReceiver() {
|
||||
Context context = GBApplication.getContext();
|
||||
Intent intent = new Intent("DAILY_ALARM");
|
||||
intent.setPackage(BuildConfig.APPLICATION_ID);
|
||||
+10
-5
@@ -1,11 +1,11 @@
|
||||
/* Copyright (C) 2015-2025 Andreas Böhler, Andreas Shimokawa, Arjan
|
||||
/* Copyright (C) 2015-2026 Andreas Böhler, Andreas Shimokawa, Arjan
|
||||
Schrijver, Avamander, Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti,
|
||||
Daniel Hauck, Davis Mosenkovs, Dikay900, Dmitriy Bogdanov, Frank Slezak,
|
||||
Gabriele Monaco, Gordon Williams, ivanovlev, João Paulo Barraca, José
|
||||
Rebelo, Julien Pivotto, Kasha, keeshii, Martin, Matthieu Baerts, mvn23,
|
||||
NekoBox, Nephiel, Petr Vaněk, Sebastian Kranz, Sergey Trofimov, Steffen
|
||||
Liebergeld, Taavi Eomäe, TylerWilliamson, Uwe Hermann, Yoran Vulker,
|
||||
Thomas Kuehne
|
||||
Rebelo, Julien Pivotto, Kasha, keeshii, Martin, Martin Braun, Matthieu
|
||||
Baerts, mvn23, NekoBox, Nephiel, Petr Vaněk, Sebastian Kranz, Sergey
|
||||
Trofimov, Steffen Liebergeld, Taavi Eomäe, TylerWilliamson, Uwe Hermann,
|
||||
Yoran Vulker, Thomas Kuehne
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -79,6 +79,7 @@ import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCar
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.DeviceAlarmReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothPairingRequestReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.CMWeatherReceiver;
|
||||
@@ -553,6 +554,10 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
ContextCompat.registerReceiver(this, deviceSettingsReceiver, deviceSettingsIntentFilter, ContextCompat.RECEIVER_EXPORTED);
|
||||
globalReceivers.add(deviceSettingsReceiver);
|
||||
|
||||
final DeviceAlarmReceiver deviceAlarmReceiver = new DeviceAlarmReceiver();
|
||||
ContextCompat.registerReceiver(this, deviceAlarmReceiver, deviceAlarmReceiver.buildFilter(), ContextCompat.RECEIVER_EXPORTED);
|
||||
globalReceivers.add(deviceAlarmReceiver);
|
||||
|
||||
final IntentApiReceiver intentApiReceiver = new IntentApiReceiver();
|
||||
ContextCompat.registerReceiver(this, intentApiReceiver, intentApiReceiver.buildFilter(), ContextCompat.RECEIVER_EXPORTED);
|
||||
globalReceivers.add(intentApiReceiver);
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@ import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.SunriseSunsetAlarmReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@@ -52,7 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
|
||||
public class PebbleSupport extends AbstractSerialDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PebbleSupport.class);
|
||||
private AlarmReceiver mAlarmReceiver = null;
|
||||
private SunriseSunsetAlarmReceiver mAlarmReceiver = null;
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
@@ -69,7 +69,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport {
|
||||
}
|
||||
unregisterSunriseSunsetAlarmReceiver();
|
||||
LOG.info("registering sunrise and sunset receiver");
|
||||
this.mAlarmReceiver = new AlarmReceiver();
|
||||
this.mAlarmReceiver = new SunriseSunsetAlarmReceiver();
|
||||
ContextCompat.registerReceiver(GBApplication.getContext(), mAlarmReceiver, new IntentFilter("DAILY_ALARM"), ContextCompat.RECEIVER_EXPORTED);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/activatedBackgroundIndicator"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/activatedBackgroundIndicator"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:paddingStart="1dp">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/activatedBackgroundIndicator"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageView
|
||||
|
||||
@@ -1141,6 +1141,8 @@
|
||||
<string name="pref_summary_low_latency_fw_update">This might help on devices where firmware flashing fails.</string>
|
||||
<string name="pref_title_third_party_app_device_settings">Allow 3rd party apps to change settings</string>
|
||||
<string name="pref_summary_third_party_app_device_settings">Allow other installed 3rd party apps to set device settings through intents.</string>
|
||||
<string name="pref_title_third_party_app_device_alarms">Allow 3rd party apps to set alarms</string>
|
||||
<string name="pref_summary_third_party_app_device_alarms">Allow other installed 3rd party apps to set device alarms through intents.</string>
|
||||
<string name="live_activity_steps_history">Steps history</string>
|
||||
<string name="live_activity_current_steps_per_minute">Current steps/min</string>
|
||||
<string name="live_activity_total_steps">Total steps</string>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<SwitchPreferenceCompat
|
||||
android:icon="@drawable/ic_developer_mode"
|
||||
android:defaultValue="false"
|
||||
android:key="third_party_apps_set_alarms"
|
||||
android:layout="@layout/preference_checkbox"
|
||||
android:summary="@string/pref_summary_third_party_app_device_alarms"
|
||||
android:title="@string/pref_title_third_party_app_device_alarms" />
|
||||
</androidx.preference.PreferenceScreen>
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.robolectric.Shadows.shadowOf;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TestName;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
|
||||
public class DeviceAlarmReceiverTest extends TestBase {
|
||||
@Rule
|
||||
public final TestName testName = new TestName();
|
||||
|
||||
private final DeviceAlarmReceiver receiver = new DeviceAlarmReceiver();
|
||||
|
||||
private GBDevice device;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
device = createThreeSlotDevice();
|
||||
device.setState(GBDevice.State.INITIALIZED);
|
||||
DBHelper.getDevice(device, daoSession);
|
||||
registerDeviceWithManager(device);
|
||||
|
||||
final SharedPreferences devicePrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
assertNotNull(devicePrefs);
|
||||
devicePrefs.edit()
|
||||
.clear()
|
||||
.putBoolean("third_party_apps_set_alarms", true)
|
||||
.commit();
|
||||
|
||||
drainStartedServices();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_rejectsMissingDeviceAddress() {
|
||||
receiver.onReceive(getContext(), new Intent(DeviceAlarmReceiver.COMMAND_SET_ALARM));
|
||||
|
||||
assertNull(getNextStartedService());
|
||||
assertEquals(0, DBHelper.getAlarms(device).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_rejectsInvalidMacAddress() {
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_SET_ALARM);
|
||||
intent.putExtra(DeviceAlarmReceiver.EXTRA_MAC_ADDR, "invalid");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertNull(getNextStartedService());
|
||||
assertEquals(0, DBHelper.getAlarms(device).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_rejectsUnknownDevice() {
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_SET_ALARM);
|
||||
intent.putExtra(DeviceAlarmReceiver.EXTRA_MAC_ADDR, "00:00:00:00:00:00");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertNull(getNextStartedService());
|
||||
assertEquals(0, DBHelper.getAlarms(device).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_rejectsWhenThirdPartyAlarmsAreDisabled() {
|
||||
GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).edit()
|
||||
.putBoolean("third_party_apps_set_alarms", false)
|
||||
.commit();
|
||||
|
||||
final Intent intent = setAlarmIntent(7, 30, "Morning run");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertNull(getNextStartedService());
|
||||
assertEquals(0, DBHelper.getAlarms(device).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_usesFirstUntitledDisabledSlotWithRepetition() {
|
||||
storeAlarm(0, true, 6, 15, 0, "personal");
|
||||
storeAlarm(1, false, 6, 30, 0, "");
|
||||
storeAlarm(2, false, 8, 45, 0, "");
|
||||
|
||||
final Intent intent = setAlarmIntent(7, 30, "Morning run");
|
||||
intent.putIntegerArrayListExtra(
|
||||
DeviceAlarmReceiver.EXTRA_DAYS,
|
||||
new ArrayList<>(Arrays.asList(Calendar.MONDAY, Calendar.WEDNESDAY, Calendar.SUNDAY))
|
||||
);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
final Alarm alarm = getAlarm(1);
|
||||
assertTrue(alarm.getEnabled());
|
||||
assertEquals(7, alarm.getHour());
|
||||
assertEquals(30, alarm.getMinute());
|
||||
assertEquals("Morning run", alarm.getTitle());
|
||||
assertEquals(
|
||||
Alarm.ALARM_MON + Alarm.ALARM_WED + Alarm.ALARM_SUN,
|
||||
alarm.getRepetition()
|
||||
);
|
||||
|
||||
final Intent forwardedIntent = getNextStartedService();
|
||||
assertForwardedAlarmUpdate(forwardedIntent, 3);
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_skipsDismissedAlarmWithTitleAndUsesUntitledSlot() {
|
||||
storeAlarm(0, false, 6, 30, 0, "protected personal alarm");
|
||||
storeAlarm(1, false, 6, 30, 0, "");
|
||||
storeAlarm(2, false, 6, 30, 0, "second free");
|
||||
|
||||
final Intent intent = setAlarmIntent(9, 45, "Auto slot");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
final Alarm protectedAlarm = getAlarm(0);
|
||||
assertFalse(protectedAlarm.getEnabled());
|
||||
assertEquals("protected personal alarm", protectedAlarm.getTitle());
|
||||
|
||||
final Alarm reused = getAlarm(1);
|
||||
assertTrue(reused.getEnabled());
|
||||
assertEquals(9, reused.getHour());
|
||||
assertEquals(45, reused.getMinute());
|
||||
assertEquals("Auto slot", reused.getTitle());
|
||||
|
||||
final Alarm untouched2 = getAlarm(2);
|
||||
assertFalse(untouched2.getEnabled());
|
||||
assertEquals("second free", untouched2.getTitle());
|
||||
|
||||
assertForwardedAlarmUpdate(getNextStartedService(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_rejectsWhenNoFreeSlotIsAvailable() {
|
||||
storeAlarm(0, true, 6, 30, 0, "slot 1");
|
||||
storeAlarm(1, false, 7, 30, 0, "protected dismissed slot");
|
||||
storeAlarm(2, true, 8, 30, 0, "slot 3");
|
||||
|
||||
final Intent intent = setAlarmIntent(9, 45, "No space");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertNull(getNextStartedService());
|
||||
assertEquals(3, DBHelper.getAlarms(device).size());
|
||||
assertEquals("slot 1", getAlarm(0).getTitle());
|
||||
assertEquals("protected dismissed slot", getAlarm(1).getTitle());
|
||||
assertEquals("slot 3", getAlarm(2).getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_allowsMissingLabel() {
|
||||
storeAlarm(0, false, 6, 30, 0, "");
|
||||
storeAlarm(1, true, 7, 45, 0, "personal");
|
||||
storeAlarm(2, false, 8, 15, 0, "reserved");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_SET_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, 5)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MINUTES, 50);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
final Alarm alarm = getAlarm(0);
|
||||
assertTrue(alarm.getEnabled());
|
||||
assertEquals(5, alarm.getHour());
|
||||
assertEquals(50, alarm.getMinute());
|
||||
assertNull(alarm.getTitle());
|
||||
assertForwardedAlarmUpdate(getNextStartedService(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_rejectsOutOfBoundsHour() {
|
||||
storeAlarm(0, false, 6, 30, 0, "");
|
||||
|
||||
final Intent intent = setAlarmIntent(24, 15, "Morning run");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
final Alarm alarm = getAlarm(0);
|
||||
assertFalse(alarm.getEnabled());
|
||||
assertEquals(6, alarm.getHour());
|
||||
assertEquals(30, alarm.getMinute());
|
||||
assertEquals("", alarm.getTitle());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setAlarm_rejectsOutOfBoundsMinute() {
|
||||
storeAlarm(0, false, 6, 30, 0, "");
|
||||
|
||||
final Intent intent = setAlarmIntent(7, 60, "Morning run");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
final Alarm alarm = getAlarm(0);
|
||||
assertFalse(alarm.getEnabled());
|
||||
assertEquals(6, alarm.getHour());
|
||||
assertEquals(30, alarm.getMinute());
|
||||
assertEquals("", alarm.getTitle());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarm_rejectsUnknownMode() {
|
||||
storeAlarm(0, true, 6, 30, 0, "slot 0");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, "bogus");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarm_rejectsLabelModeWithoutMessage() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TITLE);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByAll_disablesEveryStoredAlarm() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
storeAlarm(1, true, 7, 45, 0, "Standup");
|
||||
storeAlarm(2, true, 8, 15, 0, "School run");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_ALL);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertFalse(getAlarm(0).getEnabled());
|
||||
assertFalse(getAlarm(1).getEnabled());
|
||||
assertFalse(getAlarm(2).getEnabled());
|
||||
assertEquals("", getAlarm(0).getTitle());
|
||||
assertEquals("", getAlarm(1).getTitle());
|
||||
assertEquals("", getAlarm(2).getTitle());
|
||||
assertForwardedAlarmUpdate(getNextStartedService(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_rejectsWhenHourIsMissing() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_matchesProvidedHour() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
storeAlarm(1, true, 6, 45, 0, "Standup");
|
||||
storeAlarm(2, true, 8, 15, 0, "School run");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, 6);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertFalse(getAlarm(0).getEnabled());
|
||||
assertFalse(getAlarm(1).getEnabled());
|
||||
assertTrue(getAlarm(2).getEnabled());
|
||||
assertEquals("", getAlarm(0).getTitle());
|
||||
assertEquals("", getAlarm(1).getTitle());
|
||||
assertEquals("School run", getAlarm(2).getTitle());
|
||||
assertForwardedAlarmUpdate(getNextStartedService(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_rejectsWhenOnlyMinuteIsProvided() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
storeAlarm(1, true, 7, 30, 0, "Standup");
|
||||
storeAlarm(2, true, 8, 15, 0, "School run");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MINUTES, 30);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertTrue(getAlarm(1).getEnabled());
|
||||
assertTrue(getAlarm(2).getEnabled());
|
||||
assertEquals("Wake up", getAlarm(0).getTitle());
|
||||
assertEquals("Standup", getAlarm(1).getTitle());
|
||||
assertEquals("School run", getAlarm(2).getTitle());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_matchesHourAndMinuteWhenBothProvided() {
|
||||
storeAlarm(0, true, 6, 10, 0, "Hour match");
|
||||
storeAlarm(1, true, 9, 30, 0, "Minute match");
|
||||
storeAlarm(2, true, 6, 30, 0, "Both match");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, 6)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MINUTES, 30);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertTrue(getAlarm(1).getEnabled());
|
||||
assertFalse(getAlarm(2).getEnabled());
|
||||
assertEquals("Hour match", getAlarm(0).getTitle());
|
||||
assertEquals("Minute match", getAlarm(1).getTitle());
|
||||
assertEquals("", getAlarm(2).getTitle());
|
||||
assertForwardedAlarmUpdate(getNextStartedService(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_doesNothingWhenBothProvidedButNoExactMatchExists() {
|
||||
storeAlarm(0, true, 6, 10, 0, "Hour match");
|
||||
storeAlarm(1, true, 9, 30, 0, "Minute match");
|
||||
storeAlarm(2, true, 8, 15, 0, "No match");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, 6)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MINUTES, 30);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertTrue(getAlarm(1).getEnabled());
|
||||
assertTrue(getAlarm(2).getEnabled());
|
||||
assertEquals("Hour match", getAlarm(0).getTitle());
|
||||
assertEquals("Minute match", getAlarm(1).getTitle());
|
||||
assertEquals("No match", getAlarm(2).getTitle());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_doesNothingWhenNoAlarmMatches() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
storeAlarm(1, true, 7, 45, 0, "Standup");
|
||||
storeAlarm(2, true, 8, 15, 0, "School run");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, 11)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MINUTES, 59);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertTrue(getAlarm(1).getEnabled());
|
||||
assertTrue(getAlarm(2).getEnabled());
|
||||
assertEquals("Wake up", getAlarm(0).getTitle());
|
||||
assertEquals("Standup", getAlarm(1).getTitle());
|
||||
assertEquals("School run", getAlarm(2).getTitle());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_outOfBoundsHourDoesNothing() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
storeAlarm(1, true, 7, 45, 0, "Standup");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, 24);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertTrue(getAlarm(1).getEnabled());
|
||||
assertEquals("Wake up", getAlarm(0).getTitle());
|
||||
assertEquals("Standup", getAlarm(1).getTitle());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByTime_outOfBoundsMinuteDoesNothing() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Wake up");
|
||||
storeAlarm(1, true, 7, 45, 0, "Standup");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TIME)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, 6)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MINUTES, 60);
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertTrue(getAlarm(1).getEnabled());
|
||||
assertEquals("Wake up", getAlarm(0).getTitle());
|
||||
assertEquals("Standup", getAlarm(1).getTitle());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByLabel_matchesContainedTitle() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Morning workout");
|
||||
storeAlarm(1, true, 7, 45, 0, "Workout cooldown");
|
||||
storeAlarm(2, true, 8, 15, 0, "School run");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TITLE)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_TITLE, "workout");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertFalse(getAlarm(0).getEnabled());
|
||||
assertTrue(getAlarm(1).getEnabled());
|
||||
assertTrue(getAlarm(2).getEnabled());
|
||||
assertEquals("", getAlarm(0).getTitle());
|
||||
assertEquals("Workout cooldown", getAlarm(1).getTitle());
|
||||
assertForwardedAlarmUpdate(getNextStartedService(), 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dismissAlarmByLabel_isCaseSensitive() {
|
||||
storeAlarm(0, true, 6, 30, 0, "Morning workout");
|
||||
|
||||
final Intent intent = baseIntent(DeviceAlarmReceiver.COMMAND_DISMISS_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_ALARM_SEARCH_MODE, DeviceAlarmReceiver.ALARM_SEARCH_MODE_TITLE)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_TITLE, "Workout");
|
||||
|
||||
receiver.onReceive(getContext(), intent);
|
||||
|
||||
assertTrue(getAlarm(0).getEnabled());
|
||||
assertNull(getNextStartedService());
|
||||
}
|
||||
|
||||
private GBDevice createThreeSlotDevice() {
|
||||
final int suffix = Math.abs(testName.getMethodName().hashCode()) % 256;
|
||||
final String address = String.format("AA:BB:CC:DD:EE:%02X", suffix);
|
||||
return new ThreeSlotTestDevice(address);
|
||||
}
|
||||
|
||||
private void registerDeviceWithManager(final GBDevice device) {
|
||||
try {
|
||||
final DeviceManager deviceManager = GBApplication.app().getDeviceManager();
|
||||
final Field field = DeviceManager.class.getDeclaredField("deviceList");
|
||||
field.setAccessible(true);
|
||||
final List<GBDevice> deviceList = (List<GBDevice>) field.get(deviceManager);
|
||||
deviceList.clear();
|
||||
deviceList.add(device);
|
||||
} catch (final Exception e) {
|
||||
throw new AssertionError("Failed to register test device", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Intent setAlarmIntent(final int hour, final int minutes, final String message) {
|
||||
return baseIntent(DeviceAlarmReceiver.COMMAND_SET_ALARM)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_HOUR, hour)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MINUTES, minutes)
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_TITLE, message);
|
||||
}
|
||||
|
||||
private Intent baseIntent(final String action) {
|
||||
return new Intent(action)
|
||||
.setPackage("nodomain.freeyourgadget.gadgetbridge")
|
||||
.putExtra(DeviceAlarmReceiver.EXTRA_MAC_ADDR, device.getAddress());
|
||||
}
|
||||
|
||||
private void storeAlarm(final int position, final boolean enabled, final int hour, final int minute, final int repetition, final String title) {
|
||||
final Alarm alarm = AlarmUtils.createDefaultAlarm(daoSession, device, position);
|
||||
assertNotNull(alarm);
|
||||
alarm.setEnabled(enabled);
|
||||
alarm.setHour(hour);
|
||||
alarm.setMinute(minute);
|
||||
alarm.setRepetition(repetition);
|
||||
alarm.setTitle(title);
|
||||
DBHelper.store(alarm);
|
||||
}
|
||||
|
||||
private Alarm getAlarm(final int position) {
|
||||
final List<Alarm> alarms = DBHelper.getAlarmsWithDefaults(device);
|
||||
for (final Alarm alarm : alarms) {
|
||||
if (alarm.getPosition() == position) {
|
||||
return alarm;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Alarm at position " + position + " not found");
|
||||
}
|
||||
|
||||
private void assertForwardedAlarmUpdate(final Intent intent, final int expectedAlarmCount) {
|
||||
assertNotNull(intent);
|
||||
assertEquals(DeviceCommunicationService.class.getName(), intent.getComponent().getClassName());
|
||||
assertEquals(DeviceService.ACTION_SET_ALARMS, intent.getAction());
|
||||
|
||||
final ArrayList<? extends Alarm> alarms = (ArrayList<? extends Alarm>) intent.getSerializableExtra(DeviceService.EXTRA_ALARMS);
|
||||
assertNotNull(alarms);
|
||||
assertEquals(expectedAlarmCount, alarms.size());
|
||||
}
|
||||
|
||||
private void drainStartedServices() {
|
||||
while (getNextStartedService() != null) {
|
||||
// Drain app startup noise so each test can assert only its own service invocation.
|
||||
}
|
||||
}
|
||||
|
||||
private Intent getNextStartedService() {
|
||||
return shadowOf((Application) app).getNextStartedService();
|
||||
}
|
||||
|
||||
private static final class ThreeSlotTestDevice extends GBDevice {
|
||||
private static final DeviceCoordinator COORDINATOR = new ThreeSlotTestCoordinator();
|
||||
|
||||
private ThreeSlotTestDevice(final String address) {
|
||||
super(address, "Testie", "Test Alias", "Test Folder", DeviceType.TEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceCoordinator getDeviceCoordinator() {
|
||||
return COORDINATOR;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ThreeSlotTestCoordinator extends TestDeviceCoordinator {
|
||||
@Override
|
||||
public int getAlarmSlotCount(final GBDevice device) {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user