/* Copyright (C) 2015-2021 Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti, Dmitry Markin, José Rebelo, Matthieu Baerts, Nephiel, Petr Vaněk, Taavi Eomäe 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 . */ package nodomain.freeyourgadget.gadgetbridge.devices; import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getPrefs; import android.app.Activity; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.le.ScanFilter; import android.content.Context; import android.net.Uri; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.regex.Pattern; import de.greenrobot.dao.query.QueryBuilder; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability; import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl; import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.entities.AlarmDao; import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevelDao; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributesDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.PaiSample; import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { private static final Logger LOG = LoggerFactory.getLogger(AbstractDeviceCoordinator.class); private Pattern supportedDeviceName = null; /** * This method should return a ReGexp pattern that will matched against a found device * to check whether this coordinator supports that device. * If more sophisticated logic is needed to determine device support, the supports(GBDeviceCandidate) * should be overridden. * * @return Pattern * */ protected Pattern getSupportedDeviceName(){ return null; } @Override public boolean supports(GBDeviceCandidate candidate) { if(supportedDeviceName == null){ supportedDeviceName = getSupportedDeviceName(); } if(supportedDeviceName == null){ throw new RuntimeException(getClass() + " should either override getSupportedDeviceName or supports(GBDeviceCandidate)"); } return supportedDeviceName.matcher(candidate.getName()).matches(); } @Override public ConnectionType getConnectionType() { return ConnectionType.BOTH; } @NonNull @Override public Collection createBLEScanFilters() { return Collections.emptyList(); } @Override public GBDevice createDevice(GBDeviceCandidate candidate, DeviceType deviceType) { GBDevice gbDevice = new GBDevice(candidate.getDevice().getAddress(), candidate.getName(), null, null, deviceType); for (BatteryConfig batteryConfig : getBatteryConfig()) { gbDevice.setBatteryIcon(batteryConfig.getBatteryIcon(), batteryConfig.getBatteryIndex()); gbDevice.setBatteryLabel(batteryConfig.getBatteryLabel(), batteryConfig.getBatteryIndex()); } return gbDevice; } @Override public final void deleteDevice(final GBDevice gbDevice) throws GBException { LOG.info("will try to delete device: " + gbDevice.getName()); if (gbDevice.isConnected() || gbDevice.isConnecting()) { GBApplication.deviceService(gbDevice).disconnect(); } Prefs prefs = getPrefs(); String lastDevice = prefs.getPreferences().getString("last_device_address", ""); if (gbDevice.getAddress().equals(lastDevice)) { LOG.debug("#1605 removing last device"); prefs.getPreferences().edit().remove("last_device_address").apply(); } String macAddress = prefs.getPreferences().getString(MiBandConst.PREF_MIBAND_ADDRESS, ""); if (gbDevice.getAddress().equals(macAddress)) { LOG.debug("#1605 removing devel miband"); prefs.getPreferences().edit().remove(MiBandConst.PREF_MIBAND_ADDRESS).apply(); } GBApplication.deleteDeviceSpecificSharedPrefs(gbDevice.getAddress()); try (DBHandler dbHandler = GBApplication.acquireDB()) { DaoSession session = dbHandler.getDaoSession(); Device device = DBHelper.findDevice(gbDevice, session); if (device != null) { deleteDevice(gbDevice, device, session); QueryBuilder qb = session.getDeviceAttributesDao().queryBuilder(); qb.where(DeviceAttributesDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities(); QueryBuilder batteryLevelQueryBuilder = session.getBatteryLevelDao().queryBuilder(); batteryLevelQueryBuilder.where(BatteryLevelDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities(); QueryBuilder alarmDeviceQueryBuilder = session.getAlarmDao().queryBuilder(); alarmDeviceQueryBuilder.where(AlarmDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities(); session.getDeviceDao().delete(device); } else { LOG.info("device to delete not found in db: " + gbDevice); } } catch (Exception e) { throw new GBException("Error deleting device: " + e.getMessage(), e); } } /** * Hook for subclasses to perform device-specific deletion logic, e.g. db cleanup. * * @param gbDevice the GBDevice * @param device the corresponding database Device * @param session the session to use * @throws GBException */ protected abstract void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException; @Override public boolean allowFetchActivityData(GBDevice device) { return device.isInitialized() && !device.isBusy() && supportsActivityDataFetching(); } @Override public SampleProvider getSampleProvider(final GBDevice device, final DaoSession session) { return null; } @Override public TimeSampleProvider getStressSampleProvider(GBDevice device, DaoSession session) { return null; } @Override public TimeSampleProvider getTemperatureSampleProvider(GBDevice device, DaoSession session) { return null; } @Override public TimeSampleProvider getSpo2SampleProvider(GBDevice device, DaoSession session) { return null; } @Override public TimeSampleProvider getHeartRateMaxSampleProvider(GBDevice device, DaoSession session) { return null; } @Override public TimeSampleProvider getHeartRateRestingSampleProvider(GBDevice device, DaoSession session) { return null; } @Override public TimeSampleProvider getHeartRateManualSampleProvider(GBDevice device, DaoSession session) { return null; } @Override public TimeSampleProvider getPaiSampleProvider(GBDevice device, DaoSession session) { return null; } @Override public TimeSampleProvider getSleepRespiratoryRateSampleProvider(GBDevice device, DaoSession session) { return null; } @Override @Nullable public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) { return null; } public boolean isHealthWearable(BluetoothDevice device) { BluetoothClass bluetoothClass; try { bluetoothClass = device.getBluetoothClass(); } catch (SecurityException se) { LOG.warn("missing bluetooth permission: ", se); return false; } if (bluetoothClass == null) { LOG.warn("unable to determine bluetooth device class of " + device); return false; } if (bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.WEARABLE || bluetoothClass.getMajorDeviceClass() == BluetoothClass.Device.Major.UNCATEGORIZED) { int deviceClasses = BluetoothClass.Device.HEALTH_BLOOD_PRESSURE | BluetoothClass.Device.HEALTH_DATA_DISPLAY | BluetoothClass.Device.HEALTH_PULSE_RATE | BluetoothClass.Device.HEALTH_WEIGHING | BluetoothClass.Device.HEALTH_UNCATEGORIZED | BluetoothClass.Device.HEALTH_PULSE_OXIMETER | BluetoothClass.Device.HEALTH_GLUCOSE; return (bluetoothClass.getDeviceClass() & deviceClasses) != 0; } return false; } @Override public File getAppCacheDir() throws IOException { return null; } @Override public String getAppCacheSortFilename() { return null; } @Override public String getAppFileExtension() { return null; } @Override public boolean supportsAppListFetching() { return false; } @Override public boolean supportsFlashing() { return false; } @Nullable @Override public InstallHandler findInstallHandler(final Uri uri, final Context context) { return null; } @Override public boolean supportsScreenshots() { return false; } @Override public int getAlarmSlotCount(final GBDevice device) { return 0; } @Override public boolean supportsSmartWakeup(final GBDevice device) { return false; } @Override public boolean supportsAppReordering() { return false; } @Override public boolean supportsAppsManagement(final GBDevice device) { return false; } @Override public boolean supportsCachedAppManagement(final GBDevice device) { try { return supportsAppsManagement(device) && getAppCacheDir() != null; } catch (final Exception e) { // we failed, but still tried, so it's supported.. LOG.error("Failed to get app cache dir", e); return true; } } @Override public boolean supportsInstalledAppManagement(final GBDevice device) { return supportsAppsManagement(device); } @Override public boolean supportsWatchfaceManagement(final GBDevice device) { return supportsAppsManagement(device); } @Nullable @Override public Class getAppsManagementActivity() { return null; } @Nullable @Override public Class getWatchfaceDesignerActivity() { return null; } @Override public int getBondingStyle() { return BONDING_STYLE_ASK; } @Override public boolean isExperimental() { return false; } @Override public boolean supportsCalendarEvents() { return false; } @Override public boolean supportsActivityDataFetching() { return false; } @Override public boolean supportsActivityTracking() { return false; } @Override public boolean supportsActivityTracks() { return false; } @Override public boolean supportsStressMeasurement() { return false; } @Override public boolean supportsSpo2() { return false; } @Override public boolean supportsHeartRateStats() { return false; } @Override public boolean supportsPai() { return false; } @Override public boolean supportsSleepRespiratoryRate() { return false; } @Override public boolean supportsAlarmSnoozing() { return false; } @Override public boolean supportsAlarmDescription(GBDevice device) { return false; } @Override public boolean supportsMusicInfo() { return false; } @Override public boolean supportsLedColor() { return false; } @Override public int getMaximumReminderMessageLength() { return 0; } @Override public int getReminderSlotCount(final GBDevice device) { return 0; } @Override public int getCannedRepliesSlotCount(final GBDevice device) { return 0; } @Override public int getWorldClocksSlotCount() { return 0; } @Override public int getWorldClocksLabelLength() { return 10; } @Override public boolean supportsDisabledWorldClocks() { return false; } @Override public int getContactsSlotCount(final GBDevice device) { return 0; } @Override public boolean supportsRgbLedColor() { return false; } @NonNull @Override public int[] getColorPresets() { return new int[0]; } @Override public boolean supportsHeartRateMeasurement(final GBDevice device) { return false; } @Override public boolean supportsManualHeartRateMeasurement(final GBDevice device) { return supportsHeartRateMeasurement(device); } @Override public boolean supportsRealtimeData() { return false; } @Override public boolean supportsRemSleep() { return false; } @Override public boolean supportsWeather() { return false; } @Override public boolean supportsFindDevice() { return false; } @Override public boolean supportsUnicodeEmojis() { return false; } @Override public int[] getSupportedDeviceSpecificConnectionSettings() { int[] settings = new int[0]; ConnectionType connectionType = getConnectionType(); if(connectionType.usesBluetoothLE()){ settings = ArrayUtils.insert(0, settings, R.xml.devicesettings_reconnect_ble); } if(connectionType.usesBluetoothClassic()){ settings = ArrayUtils.insert(0, settings, R.xml.devicesettings_reconnect_bl_classic); } return settings; } @Override public int[] getSupportedDeviceSpecificApplicationSettings() { return new int[0]; } @Override public int[] getSupportedDeviceSpecificSettings(GBDevice device) { return new int[0]; } @Override public int[] getSupportedDeviceSpecificAuthenticationSettings() { return new int[0]; } @Override public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(GBDevice device) { return null; } @Override public String[] getSupportedLanguageSettings(GBDevice device) { return null; } @Nullable @Override public Class getPairingActivity() { return null; } @Nullable @Override public Class getCalibrationActivity() { return null; } @Override public int getBatteryCount() { return 1; } //multiple battery support, default is 1, maximum is 3, 0 will disable the battery in UI @Override public BatteryConfig[] getBatteryConfig() { return new BatteryConfig[0]; } @Override public boolean supportsPowerOff() { return false; } @Override public PasswordCapabilityImpl.Mode getPasswordCapability() { return PasswordCapabilityImpl.Mode.NONE; } @Override public List getHeartRateMeasurementIntervals() { return Arrays.asList( HeartRateCapability.MeasurementInterval.OFF, HeartRateCapability.MeasurementInterval.MINUTES_1, HeartRateCapability.MeasurementInterval.MINUTES_5, HeartRateCapability.MeasurementInterval.MINUTES_10, HeartRateCapability.MeasurementInterval.MINUTES_30, HeartRateCapability.MeasurementInterval.HOUR_1 ); } @Override public boolean supportsWidgets(final GBDevice device) { return false; } @Nullable @Override public WidgetManager getWidgetManager(final GBDevice device) { return null; } public boolean supportsNavigation() { return false; } @Override public int getOrderPriority(){ return 0; } @Override public EnumSet getInitialFlags() { return EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING); } @Override @DrawableRes public int getDefaultIconResource() { return R.drawable.ic_device_default; } @Override @DrawableRes public int getDisabledIconResource() { return R.drawable.ic_device_default_disabled; } @Override public boolean supportsNotificationVibrationPatterns() { return false; } @Override public boolean supportsNotificationVibrationRepetitionPatterns() { return false; } @Override public boolean supportsNotificationLedPatterns() { return false; } @Override public AbstractNotificationPattern[] getNotificationVibrationPatterns() { return new AbstractNotificationPattern[0]; } @Override public AbstractNotificationPattern[] getNotificationVibrationRepetitionPatterns() { return new AbstractNotificationPattern[0]; } @Override public AbstractNotificationPattern[] getNotificationLedPatterns() { return new AbstractNotificationPattern[0]; } @Override public boolean validateAuthKey(final String authKey) { return !(authKey.getBytes().length < 34 || !authKey.startsWith("0x")); } }