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:
Martin Braun
2026-05-21 19:57:07 +02:00
committed by José Rebelo
parent 0c11ba44f8
commit b8a85e03ea
16 changed files with 940 additions and 51 deletions
@@ -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() {
@@ -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();
@@ -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);
}
@@ -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,
@@ -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();
@@ -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;
}
}
@@ -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);
@@ -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);
@@ -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">
@@ -67,4 +67,4 @@
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
@@ -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
@@ -48,4 +48,4 @@
android:text="Item Description" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
@@ -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
@@ -47,4 +47,4 @@
android:text="Item Description" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
+2
View File
@@ -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>
@@ -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;
}
}
}