Java tutorial
/* Copyright (C) 2015-2017 Andreas Shimokawa, Carsten Pfeiffer, Christian Fischer, Daniele Gobbetti, JohnnySun, Julien Pivotto, Kasha, Sergey Trofimov, Steffen Liebergeld 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. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.support.v4.content.LocalBroadcastManager; import android.text.format.DateFormat; import android.widget.Toast; import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.DateTimeDisplay; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandService; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBAlarm; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEvents; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.AbortTransactionAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile; import nodomain.freeyourgadget.gadgetbridge.service.devices.common.SimpleNotification; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.NotificationStrategy; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.RealtimeSamplesSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.actions.StopNotificationAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Version; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PROFILE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COLOUR; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_COUNT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_DURATION; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.FLASH_ORIGINAL_COLOUR; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_COUNT; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_DURATION; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PAUSE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.VIBRATION_PROFILE; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefIntValue; import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.getNotificationPrefStringValue; public class MiBand2Support extends AbstractBTLEDeviceSupport { private static final Logger LOG = LoggerFactory.getLogger(MiBand2Support.class); private final DeviceInfoProfile<MiBand2Support> deviceInfoProfile; private final HeartRateProfile<MiBand2Support> heartRateProfile; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String s = intent.getAction(); if (s.equals(DeviceInfoProfile.ACTION_DEVICE_INFO)) { handleDeviceInfo( (nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo) intent .getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); } } }; private boolean needsAuth; private volatile boolean telephoneRinging; private volatile boolean isLocatingDevice; private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); private RealtimeSamplesSupport realtimeSamplesSupport; private boolean alarmClockRinging; public MiBand2Support() { super(LOG); addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); addSupportedService(GattService.UUID_SERVICE_HEART_RATE); addSupportedService(GattService.UUID_SERVICE_IMMEDIATE_ALERT); addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); addSupportedService(GattService.UUID_SERVICE_ALERT_NOTIFICATION); addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE); addSupportedService(MiBand2Service.UUID_SERVICE_FIRMWARE_SERVICE); deviceInfoProfile = new DeviceInfoProfile<>(this); addSupportedProfile(deviceInfoProfile); heartRateProfile = new HeartRateProfile<>(this); addSupportedProfile(heartRateProfile); LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(DeviceInfoProfile.ACTION_DEVICE_INFO); intentFilter.addAction(DeviceService.ACTION_MIBAND2_AUTH); broadcastManager.registerReceiver(mReceiver, intentFilter); } @Override public void dispose() { LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); broadcastManager.unregisterReceiver(mReceiver); super.dispose(); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { try { boolean authenticate = needsAuth; needsAuth = false; new InitOperation(authenticate, this, builder).perform(); } catch (IOException e) { GB.toast(getContext(), "Initializing Mi Band 2 failed", Toast.LENGTH_SHORT, GB.ERROR, e); } return builder; } public byte[] getTimeBytes(Calendar calendar, TimeUnit precision) { byte[] bytes; if (precision == TimeUnit.MINUTES) { bytes = BLETypeConversions.shortCalendarToRawBytes(calendar, true); } else if (precision == TimeUnit.SECONDS) { bytes = BLETypeConversions.calendarToRawBytes(calendar, true); } else { throw new IllegalArgumentException( "Unsupported precision, only MINUTES and SECONDS are supported till now"); } byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar.getTimeZone()) }; // 0 = adjust reason bitflags? or DST offset?? , timezone // byte[] tail = new byte[] { 0x2 }; // reason byte[] all = BLETypeConversions.join(bytes, tail); return all; } public Calendar fromTimeBytes(byte[] bytes) { GregorianCalendar timestamp = BLETypeConversions.rawBytesToCalendar(bytes, true); return timestamp; } public MiBand2Support setCurrentTimeWithService(TransactionBuilder builder) { GregorianCalendar now = BLETypeConversions.createCalendar(); byte[] bytes = getTimeBytes(now, TimeUnit.SECONDS); builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytes); // byte[] localtime = BLETypeConversions.calendarToLocalTimeBytes(now); // builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_LOCAL_TIME_INFORMATION), localtime); // builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), new byte[] {0x2, 0x00}); // builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOQN_CHARACTERISTIC0), new byte[] {0x03,0x00,(byte)0x8e,(byte)0xce,0x5a,0x09,(byte)0xb3,(byte)0xd8,0x55,0x57,0x10,0x2a,(byte)0xed,0x7d,0x6b,0x78,(byte)0xc5,(byte)0xd2}); return this; } public MiBand2Support setLowLatency(TransactionBuilder builder) { // TODO: low latency? return this; } public MiBand2Support setHighLatency(TransactionBuilder builder) { // TODO: high latency? return this; } /** * Last action of initialization sequence. Sets the device to initialized. * It is only invoked if all other actions were successfully run, so the device * must be initialized, then. * * @param builder */ public void setInitialized(TransactionBuilder builder) { builder.add(new SetDeviceStateAction(getDevice(), State.INITIALIZED, getContext())); } // MB2: AVL // TODO: tear down the notifications on quit public MiBand2Support enableNotifications(TransactionBuilder builder, boolean enable) { builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable); builder.notify(getCharacteristic(GattService.UUID_SERVICE_CURRENT_TIME), enable); // Notify CHARACTERISTIC9 to receive random auth code builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_AUTH), enable); return this; } public MiBand2Support enableFurtherNotifications(TransactionBuilder builder, boolean enable) { builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), enable); builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO), enable); builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_10_BUTTON), enable); BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic( GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT); if (heartrateCharacteristic != null) { builder.notify(heartrateCharacteristic, enable); } return this; } @Override public boolean useAutoConnect() { return true; } @Override public void pair() { needsAuth = true; for (int i = 0; i < 5; i++) { if (connect()) { return; } } } private MiBand2Support sendDefaultNotification(TransactionBuilder builder, SimpleNotification simpleNotification, short repeat, BtLEAction extraAction) { LOG.info("Sending notification to MiBand: (" + repeat + " times)"); NotificationStrategy strategy = getNotificationStrategy(); for (short i = 0; i < repeat; i++) { strategy.sendDefaultNotification(builder, simpleNotification, extraAction); } return this; } /** * Adds a custom notification to the given transaction builder * @param vibrationProfile specifies how and how often the Band shall vibrate. * @param simpleNotification * @param flashTimes * @param flashColour * @param originalColour * @param flashDuration * @param extraAction an extra action to be executed after every vibration and flash sequence. Allows to abort the repetition, for example. * @param builder */ private MiBand2Support sendCustomNotification(VibrationProfile vibrationProfile, SimpleNotification simpleNotification, int flashTimes, int flashColour, int originalColour, long flashDuration, BtLEAction extraAction, TransactionBuilder builder) { getNotificationStrategy().sendCustomNotification(vibrationProfile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); LOG.info("Sending notification to MiBand"); return this; } private NotificationStrategy getNotificationStrategy() { String firmwareVersion = getDevice().getFirmwareVersion(); if (firmwareVersion != null) { Version ver = new Version(firmwareVersion); if (MiBandConst.MI2_FW_VERSION_MIN_TEXT_NOTIFICATIONS.compareTo(ver) > 0) { return new Mi2NotificationStrategy(this); } } if (GBApplication.getPrefs().getBoolean(MiBandConst.PREF_MI2_ENABLE_TEXT_NOTIFICATIONS, true)) { return new Mi2TextNotificationStrategy(this); } return new Mi2NotificationStrategy(this); } private static final byte[] startHeartMeasurementManual = new byte[] { 0x15, MiBandService.COMMAND_SET_HR_MANUAL, 1 }; private static final byte[] stopHeartMeasurementManual = new byte[] { 0x15, MiBandService.COMMAND_SET_HR_MANUAL, 0 }; private static final byte[] startHeartMeasurementContinuous = new byte[] { 0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 1 }; private static final byte[] stopHeartMeasurementContinuous = new byte[] { 0x15, MiBandService.COMMAND_SET__HR_CONTINUOUS, 0 }; private MiBand2Support requestBatteryInfo(TransactionBuilder builder) { LOG.debug("Requesting Battery Info!"); BluetoothGattCharacteristic characteristic = getCharacteristic( MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO); builder.read(characteristic); return this; } public MiBand2Support requestDeviceInfo(TransactionBuilder builder) { LOG.debug("Requesting Device Info!"); deviceInfoProfile.requestDeviceInfo(builder); return this; } /** * Part of device initialization process. Do not call manually. * * @param transaction * @return */ private MiBand2Support setFitnessGoal(TransactionBuilder transaction) { LOG.info("Attempting to set Fitness Goal..."); BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8); if (characteristic != null) { int fitnessGoal = GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, 10000); byte[] bytes = ArrayUtils.addAll(MiBand2Service.COMMAND_SET_FITNESS_GOAL_START, BLETypeConversions.fromUint16(fitnessGoal)); bytes = ArrayUtils.addAll(bytes, MiBand2Service.COMMAND_SET_FITNESS_GOAL_END); transaction.write(characteristic, bytes); } else { LOG.info("Unable to set Fitness Goal"); } return this; } /** * Part of device initialization process. Do not call manually. * * @param builder * @return */ private MiBand2Support setWearLocation(TransactionBuilder builder) { LOG.info("Attempting to set wear location..."); BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC8); if (characteristic != null) { builder.notify(characteristic, true); int location = MiBandCoordinator.getWearLocation(getDevice().getAddress()); switch (location) { case 0: // left hand builder.write(characteristic, MiBand2Service.WEAR_LOCATION_LEFT_WRIST); break; case 1: // right hand builder.write(characteristic, MiBand2Service.WEAR_LOCATION_RIGHT_WRIST); break; } builder.notify(characteristic, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. } return this; } @Override public void onEnableHeartRateSleepSupport(boolean enable) { try { TransactionBuilder builder = performInitialized("enable heart rate sleep support: " + enable); setHeartrateSleepSupport(builder); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error toggling heart rate sleep support: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @Override public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { // not supported } @Override public void onDeleteCalendarEvent(byte type, long id) { // not supported } /** * Part of device initialization process. Do not call manually. * * @param builder */ private MiBand2Support setHeartrateSleepSupport(TransactionBuilder builder) { BluetoothGattCharacteristic characteristicHRControlPoint = getCharacteristic( GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT); final boolean enableHrSleepSupport = MiBandCoordinator.getHeartrateSleepSupport(getDevice().getAddress()); if (characteristicHRControlPoint != null) { builder.notify(characteristicHRControlPoint, true); if (enableHrSleepSupport) { LOG.info("Enabling heartrate sleep support..."); builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_ENABLE_HR_SLEEP_MEASUREMENT); } else { LOG.info("Disabling heartrate sleep support..."); builder.write(characteristicHRControlPoint, MiBand2Service.COMMAND_DISABLE_HR_SLEEP_MEASUREMENT); } builder.notify(characteristicHRControlPoint, false); // TODO: this should actually be in some kind of finally-block in the queue. It should also be sent asynchronously after the notifications have completely arrived and processed. } return this; } private void performDefaultNotification(String task, SimpleNotification simpleNotification, short repeat, BtLEAction extraAction) { try { TransactionBuilder builder = performInitialized(task); sendDefaultNotification(builder, simpleNotification, repeat, extraAction); builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to send notification to MI device", ex); } } private void performPreferredNotification(String task, String notificationOrigin, SimpleNotification simpleNotification, int alertLevel, BtLEAction extraAction) { try { TransactionBuilder builder = performInitialized(task); Prefs prefs = GBApplication.getPrefs(); int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs); int vibratePause = getPreferredVibratePause(notificationOrigin, prefs); short vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs); VibrationProfile profile = getPreferredVibrateProfile(notificationOrigin, prefs, vibrateTimes); profile.setAlertLevel(alertLevel); int flashTimes = getPreferredFlashCount(notificationOrigin, prefs); int flashColour = getPreferredFlashColour(notificationOrigin, prefs); int originalColour = getPreferredOriginalColour(notificationOrigin, prefs); int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs); sendCustomNotification(profile, simpleNotification, flashTimes, flashColour, originalColour, flashDuration, extraAction, builder); // sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder); builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to send notification to MI device", ex); } } private int getPreferredFlashDuration(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION); } private int getPreferredOriginalColour(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR); } private int getPreferredFlashColour(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR); } private int getPreferredFlashCount(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT); } private int getPreferredVibratePause(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE); } private short getPreferredVibrateCount(String notificationOrigin, Prefs prefs) { return (short) Math.min(Short.MAX_VALUE, getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT)); } private int getPreferredVibrateDuration(String notificationOrigin, Prefs prefs) { return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION); } private VibrationProfile getPreferredVibrateProfile(String notificationOrigin, Prefs prefs, short repeat) { String profileId = getNotificationPrefStringValue(VIBRATION_PROFILE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PROFILE); return VibrationProfile.getProfile(profileId, repeat); } @Override public void onSetAlarms(ArrayList<? extends Alarm> alarms) { try { BluetoothGattCharacteristic characteristic = getCharacteristic( MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION); TransactionBuilder builder = performInitialized("Set alarm"); boolean anyAlarmEnabled = false; for (Alarm alarm : alarms) { anyAlarmEnabled |= alarm.isEnabled(); queueAlarm(alarm, builder, characteristic); } builder.queue(getQueue()); if (anyAlarmEnabled) { GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_ok), Toast.LENGTH_SHORT, GB.INFO); } else { GB.toast(getContext(), getContext().getString(R.string.user_feedback_all_alarms_disabled), Toast.LENGTH_SHORT, GB.INFO); } } catch (IOException ex) { GB.toast(getContext(), getContext().getString(R.string.user_feedback_miband_set_alarms_failed), Toast.LENGTH_LONG, GB.ERROR, ex); } } @Override public void onNotification(NotificationSpec notificationSpec) { if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) { onAlarmClock(notificationSpec); return; } int alertLevel = MiBand2Service.ALERT_LEVEL_MESSAGE; if (notificationSpec.type == NotificationType.UNKNOWN) { alertLevel = MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY; } String message = NotificationUtils.getPreferredTextFor(notificationSpec, 40, 40, getContext()).trim(); String origin = notificationSpec.type.getGenericType(); SimpleNotification simpleNotification = new SimpleNotification(message, BLETypeConversions.toAlertCategory(notificationSpec.type)); performPreferredNotification(origin + " received", origin, simpleNotification, alertLevel, null); } private void onAlarmClock(NotificationSpec notificationSpec) { alarmClockRinging = true; AbortTransactionAction abortAction = new StopNotificationAction( getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL)) { @Override protected boolean shouldAbort() { return !isAlarmClockRinging(); } }; String message = NotificationUtils.getPreferredTextFor(notificationSpec, 40, 40, getContext()); SimpleNotification simpleNotification = new SimpleNotification(message, AlertCategory.HighPriorityAlert); performPreferredNotification("alarm clock ringing", MiBandConst.ORIGIN_ALARM_CLOCK, simpleNotification, MiBand2Service.ALERT_LEVEL_VIBRATE_ONLY, abortAction); } @Override public void onDeleteNotification(int id) { alarmClockRinging = false; // we should have the notificationtype at least to check } @Override public void onSetTime() { try { TransactionBuilder builder = performInitialized("Set date and time"); setCurrentTimeWithService(builder); //TODO: once we have a common strategy for sending events (e.g. EventHandler), remove this call from here. Meanwhile it does no harm. sendCalendarEvents(builder); builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to set time on MI device", ex); } } @Override public void onSetCallState(CallSpec callSpec) { if (callSpec.command == CallSpec.CALL_INCOMING) { telephoneRinging = true; AbortTransactionAction abortAction = new StopNotificationAction( getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL)) { @Override protected boolean shouldAbort() { return !isTelephoneRinging(); } }; String message = NotificationUtils.getPreferredTextFor(callSpec); SimpleNotification simpleNotification = new SimpleNotification(message, AlertCategory.IncomingCall); performPreferredNotification("incoming call", MiBandConst.ORIGIN_INCOMING_CALL, simpleNotification, MiBand2Service.ALERT_LEVEL_PHONE_CALL, abortAction); } else if ((callSpec.command == CallSpec.CALL_START) || (callSpec.command == CallSpec.CALL_END)) { telephoneRinging = false; stopCurrentNotification(); } } private void stopCurrentNotification() { try { TransactionBuilder builder = performInitialized("stop notification"); getNotificationStrategy().stopCurrentNotification(builder); builder.queue(getQueue()); } catch (IOException e) { LOG.error("Error stopping notification"); } } @Override public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { } private boolean isAlarmClockRinging() { // don't synchronize, this is not really important return alarmClockRinging; } private boolean isTelephoneRinging() { // don't synchronize, this is not really important return telephoneRinging; } @Override public void onSetMusicState(MusicStateSpec stateSpec) { // not supported } @Override public void onSetMusicInfo(MusicSpec musicSpec) { // not supported } @Override public void onReboot() { try { TransactionBuilder builder = performInitialized("Reboot"); builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE), new byte[] { MiBand2Service.COMMAND_FIRMWARE_REBOOT }); builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to reboot MI", ex); } } @Override public void onHeartRateTest() { try { TransactionBuilder builder = performInitialized("HeartRateTest"); builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementManual); builder.queue(getQueue()); } catch (IOException ex) { LOG.error("Unable to read HearRate with MI2", ex); } } @Override public void onEnableRealtimeHeartRateMeasurement(boolean enable) { try { TransactionBuilder builder = performInitialized("Enable realtime heart rateM measurement"); if (enable) { builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementManual); builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), startHeartMeasurementContinuous); } else { builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT), stopHeartMeasurementContinuous); } builder.queue(getQueue()); enableRealtimeSamplesTimer(enable); } catch (IOException ex) { LOG.error("Unable to enable realtime heart rate measurement in MI1S", ex); } } @Override public void onFindDevice(boolean start) { isLocatingDevice = start; if (start) { AbortTransactionAction abortAction = new AbortTransactionAction() { @Override protected boolean shouldAbort() { return !isLocatingDevice; } }; SimpleNotification simpleNotification = new SimpleNotification( getContext().getString(R.string.find_device_you_found_it), AlertCategory.HighPriorityAlert); performDefaultNotification("locating device", simpleNotification, (short) 255, abortAction); } } @Override public void onSetConstantVibration(int intensity) { } @Override public void onFetchActivityData() { try { new FetchActivityOperation(this).perform(); } catch (IOException ex) { LOG.error("Unable to fetch MI activity data", ex); } } @Override public void onEnableRealtimeSteps(boolean enable) { try { TransactionBuilder builder = performInitialized( enable ? "Enabling realtime steps notifications" : "Disabling realtime steps notifications"); if (enable) { builder.read(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS)); } builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS), enable); builder.queue(getQueue()); enableRealtimeSamplesTimer(enable); } catch (IOException e) { LOG.error("Unable to change realtime steps notification to: " + enable, e); } } private byte[] getHighLatency() { int minConnectionInterval = 460; int maxConnectionInterval = 500; int latency = 0; int timeout = 500; int advertisementInterval = 0; return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval); } private byte[] getLatency(int minConnectionInterval, int maxConnectionInterval, int latency, int timeout, int advertisementInterval) { byte result[] = new byte[12]; result[0] = (byte) (minConnectionInterval & 0xff); result[1] = (byte) (0xff & minConnectionInterval >> 8); result[2] = (byte) (maxConnectionInterval & 0xff); result[3] = (byte) (0xff & maxConnectionInterval >> 8); result[4] = (byte) (latency & 0xff); result[5] = (byte) (0xff & latency >> 8); result[6] = (byte) (timeout & 0xff); result[7] = (byte) (0xff & timeout >> 8); result[8] = 0; result[9] = 0; result[10] = (byte) (advertisementInterval & 0xff); result[11] = (byte) (0xff & advertisementInterval >> 8); return result; } private byte[] getLowLatency() { int minConnectionInterval = 39; int maxConnectionInterval = 49; int latency = 0; int timeout = 500; int advertisementInterval = 0; return getLatency(minConnectionInterval, maxConnectionInterval, latency, timeout, advertisementInterval); } @Override public void onInstallApp(Uri uri) { try { new UpdateFirmwareOperation(uri, this).perform(); } catch (IOException ex) { GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); } } @Override public void onAppInfoReq() { // not supported } @Override public void onAppStart(UUID uuid, boolean start) { // not supported } @Override public void onAppDelete(UUID uuid) { // not supported } @Override public void onAppConfiguration(UUID uuid, String config) { // not supported } @Override public void onAppReorder(UUID[] uuids) { // not supported } @Override public void onScreenshotReq() { // not supported } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); UUID characteristicUUID = characteristic.getUuid(); if (MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS); return true; } else if (MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS.equals(characteristicUUID)) { handleRealtimeSteps(characteristic.getValue()); return true; } else if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { handleHeartrate(characteristic.getValue()); return true; } else if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { LOG.info("AUTHENTICATION?? " + characteristicUUID); logMessageContent(characteristic.getValue()); return true; } else if (MiBand2Service.UUID_CHARACTERISTIC_10_BUTTON.equals(characteristicUUID)) { handleButtonPressed(characteristic.getValue()); return true; } else if (MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) { handleRealtimeSteps(characteristic.getValue()); return true; } else { LOG.info("Unhandled characteristic changed: " + characteristicUUID); logMessageContent(characteristic.getValue()); } return false; } private void handleButtonPressed(byte[] value) { LOG.info("Button pressed: " + value); logMessageContent(value); } private void handleUnknownCharacteristic(byte[] value) { } @Override public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicRead(gatt, characteristic, status); UUID characteristicUUID = characteristic.getUuid(); if (GattCharacteristic.UUID_CHARACTERISTIC_GAP_DEVICE_NAME.equals(characteristicUUID)) { handleDeviceName(characteristic.getValue(), status); return true; } else if (MiBand2Service.UUID_CHARACTERISTIC_6_BATTERY_INFO.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), status); return true; } else if (GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT.equals(characteristicUUID)) { logHeartrate(characteristic.getValue(), status); return true; } else if (MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS.equals(characteristicUUID)) { handleRealtimeSteps(characteristic.getValue()); return true; } else if (MiBand2Service.UUID_CHARACTERISTIC_10_BUTTON.equals(characteristicUUID)) { handleButtonPressed(characteristic.getValue()); return true; } else { LOG.info("Unhandled characteristic read: " + characteristicUUID); logMessageContent(characteristic.getValue()); } return false; } @Override public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { UUID characteristicUUID = characteristic.getUuid(); if (MiBand2Service.UUID_CHARACTERISTIC_AUTH.equals(characteristicUUID)) { LOG.info("KEY AES SEND"); logMessageContent(characteristic.getValue()); return true; } return false; } public void logHeartrate(byte[] value, int status) { if (status == BluetoothGatt.GATT_SUCCESS && value != null) { LOG.info("Got heartrate:"); if (value.length == 2 && value[0] == 0) { int hrValue = (value[1] & 0xff); GB.toast(getContext(), "Heart Rate measured: " + hrValue, Toast.LENGTH_LONG, GB.INFO); } return; } logMessageContent(value); } private void handleHeartrate(byte[] value) { if (value.length == 2 && value[0] == 0) { int hrValue = (value[1] & 0xff); if (LOG.isDebugEnabled()) { LOG.debug("heart rate: " + hrValue); } RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport(); realtimeSamplesSupport.setHeartrateBpm(hrValue); if (!realtimeSamplesSupport.isRunning()) { // single shot measurement, manually invoke storage and result publishing realtimeSamplesSupport.triggerCurrentSample(); } } } private void handleRealtimeSteps(byte[] value) { if (value == null) { LOG.error("realtime steps: value is null"); return; } if (value.length == 13) { byte[] stepsValue = new byte[] { value[1], value[2] }; int steps = BLETypeConversions.toUint16(stepsValue); if (LOG.isDebugEnabled()) { LOG.debug("realtime steps: " + steps); } getRealtimeSamplesSupport().setSteps(steps); } else { LOG.warn("Unrecognized realtime steps value: " + Logging.formatBytes(value)); } } private void enableRealtimeSamplesTimer(boolean enable) { if (enable) { getRealtimeSamplesSupport().start(); } else { if (realtimeSamplesSupport != null) { realtimeSamplesSupport.stop(); } } } public MiBandActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) { MiBandActivitySample sample = new MiBandActivitySample(); sample.setDevice(device); sample.setUser(user); sample.setTimestamp(timestampInSeconds); sample.setProvider(provider); return sample; } private RealtimeSamplesSupport getRealtimeSamplesSupport() { if (realtimeSamplesSupport == null) { realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) { @Override public void doCurrentSample() { try (DBHandler handler = GBApplication.acquireDB()) { DaoSession session = handler.getDaoSession(); Device device = DBHelper.getDevice(getDevice(), session); User user = DBHelper.getUser(session); int ts = (int) (System.currentTimeMillis() / 1000); MiBand2SampleProvider provider = new MiBand2SampleProvider(gbDevice, session); MiBandActivitySample sample = createActivitySample(device, user, ts, provider); sample.setHeartRate(getHeartrateBpm()); sample.setSteps(getSteps()); sample.setRawIntensity(ActivitySample.NOT_MEASURED); sample.setRawKind(MiBand2SampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that? provider.addGBActivitySample(sample); // set the steps only afterwards, since realtime steps are also recorded // in the regular samples and we must not count them twice // Note: we know that the DAO sample is never committed again, so we simply // change the value here in memory. sample.setSteps(getSteps()); if (LOG.isDebugEnabled()) { LOG.debug("realtime sample: " + sample); } Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample); LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); } catch (Exception e) { LOG.warn("Unable to acquire db for saving realtime samples", e); } } }; } return realtimeSamplesSupport; } private void handleDeviceName(byte[] value, int status) { // if (status == BluetoothGatt.GATT_SUCCESS) { // versionCmd.hwVersion = new String(value); // handleGBDeviceEvent(versionCmd); // } } /** * Convert an alarm from the GB internal structure to a Mi Band message and put on the specified * builder queue as a write message for the passed characteristic * * @param alarm * @param builder * @param characteristic */ private void queueAlarm(Alarm alarm, TransactionBuilder builder, BluetoothGattCharacteristic characteristic) { Calendar calendar = alarm.getAlarmCal(); int maxAlarms = 5; // arbitrary at the moment... if (alarm.getIndex() >= maxAlarms) { if (alarm.isEnabled()) { GB.toast(getContext(), "Only 5 alarms are currently supported.", Toast.LENGTH_LONG, GB.WARN); } return; } int base = 0; if (alarm.isEnabled()) { base = 128; } int daysMask = alarm.getRepetitionMask(); if (!alarm.isRepetitive()) { daysMask = 128; } byte[] alarmMessage = new byte[] { (byte) 0x2, // TODO what is this? (byte) (base + alarm.getIndex()), // 128 is the base, alarm slot is added (byte) calendar.get(Calendar.HOUR_OF_DAY), (byte) calendar.get(Calendar.MINUTE), (byte) daysMask, }; builder.write(characteristic, alarmMessage); // TODO: react on 0x10, 0x02, 0x01 on notification (success) } private void handleDeviceInfo( nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { // if (getDeviceInfo().supportsHeartrate()) { // getDevice().addDeviceInfo(new GenericItem( // getContext().getString(R.string.DEVINFO_HR_VER), // info.getSoftwareRevision())); // } LOG.warn("Device info: " + info); versionCmd.hwVersion = info.getHardwareRevision(); // versionCmd.fwVersion = info.getFirmwareRevision(); // always null versionCmd.fwVersion = info.getSoftwareRevision(); if (versionCmd.fwVersion != null && versionCmd.fwVersion.length() > 0 && versionCmd.fwVersion.charAt(0) == 'V') { versionCmd.fwVersion = versionCmd.fwVersion.substring(1); } handleGBDeviceEvent(versionCmd); } private void handleBatteryInfo(byte[] value, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { BatteryInfo info = new BatteryInfo(value); batteryCmd.level = ((short) info.getLevelInPercent()); batteryCmd.state = info.getState(); batteryCmd.lastChargeTime = info.getLastChargeTime(); batteryCmd.numCharges = info.getNumCharges(); handleGBDeviceEvent(batteryCmd); } } /** * Fetch the events from the android device calendars and set the alarms on the miband. * @param builder */ private MiBand2Support sendCalendarEvents(TransactionBuilder builder) { BluetoothGattCharacteristic characteristic = getCharacteristic( MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION); Prefs prefs = GBApplication.getPrefs(); int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0); if (availableSlots > 0) { CalendarEvents upcomingEvents = new CalendarEvents(); List<CalendarEvents.CalendarEvent> mEvents = upcomingEvents.getCalendarEventList(getContext()); int iteration = 0; for (CalendarEvents.CalendarEvent mEvt : mEvents) { if (iteration >= availableSlots || iteration > 2) { break; } int slotToUse = 2 - iteration; Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(mEvt.getBegin()); Alarm alarm = GBAlarm.createSingleShot(slotToUse, false, calendar); queueAlarm(alarm, builder, characteristic); iteration++; } builder.queue(getQueue()); } return this; } @Override public void onSendConfiguration(String config) { TransactionBuilder builder; try { builder = performInitialized("Sending configuration for option: " + config); switch (config) { case MiBandConst.PREF_MI2_DATEFORMAT: setDateDisplay(builder); break; case MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT: setActivateDisplayOnLiftWrist(builder); break; case ActivityUser.PREF_USER_STEPS_GOAL: setFitnessGoal(builder); break; } builder.queue(getQueue()); } catch (IOException e) { GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e); } } @Override public void onTestNewFunction() { try { TransactionBuilder builder = performInitialized("test realtime steps"); builder.read(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_7_REALTIME_STEPS)); builder.queue(getQueue()); } catch (IOException e) { } } @Override public void onSendWeather(WeatherSpec weatherSpec) { } private MiBand2Support setDateDisplay(TransactionBuilder builder) { DateTimeDisplay dateTimeDisplay = MiBand2Coordinator.getDateDisplay(getContext()); LOG.info("Setting date display to " + dateTimeDisplay); switch (dateTimeDisplay) { case TIME: builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME); break; case DATE_TIME: builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_DATE_TIME); break; } return this; } private MiBand2Support setTimeFormat(TransactionBuilder builder) { boolean is24Format = DateFormat.is24HourFormat(getContext()); LOG.info("Setting 24h time format to " + is24Format); if (is24Format) { builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME_24_HOURS); } else { builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME_12_HOURS); } return this; } private MiBand2Support setActivateDisplayOnLiftWrist(TransactionBuilder builder) { boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist(); LOG.info("Setting activate display on lift wrist to " + enable); if (enable) { builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST); } else { builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST); } return this; } public void phase2Initialize(TransactionBuilder builder) { LOG.info("phase2Initialize..."); enableFurtherNotifications(builder, true); requestBatteryInfo(builder); setDateDisplay(builder); setTimeFormat(builder); setWearLocation(builder); setFitnessGoal(builder); setActivateDisplayOnLiftWrist(builder); setHeartrateSleepSupport(builder); } }