/* Copyright (C) 2024 José Rebelo 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.cmfwatchpro; import android.app.Activity; import android.bluetooth.le.ScanFilter; import android.content.Context; import android.net.Uri; import android.os.ParcelUuid; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.apache.commons.lang3.ArrayUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfActivitySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfStressSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfWorkoutSummaryParser; import nodomain.freeyourgadget.gadgetbridge.entities.CmfActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.CmfHeartRateSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.CmfSleepSessionSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.CmfSleepStageSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.CmfSpo2SampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.CmfStressSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.cmfwatchpro.CmfInstallHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.cmfwatchpro.CmfWatchProSupport; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class CmfWatchProCoordinator extends AbstractBLEDeviceCoordinator { @Override protected Pattern getSupportedDeviceName() { return Pattern.compile("^Watch Pro$"); } @NonNull @Override public Collection createBLEScanFilters() { final ParcelUuid casioService = new ParcelUuid(CmfWatchProSupport.UUID_SERVICE_CMF_CMD); final ScanFilter filter = new ScanFilter.Builder().setServiceUuid(casioService).build(); return Collections.singletonList(filter); } @Nullable @Override public InstallHandler findInstallHandler(final Uri uri, final Context context) { final CmfInstallHandler handler = new CmfInstallHandler(uri, context); return handler.isValid() ? handler : null; } @Override public boolean suggestUnbindBeforePair() { return false; } @Override protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException { final Long deviceId = device.getId(); session.getCmfActivitySampleDao().queryBuilder() .where(CmfActivitySampleDao.Properties.DeviceId.eq(deviceId)) .buildDelete().executeDeleteWithoutDetachingEntities(); session.getCmfStressSampleDao().queryBuilder() .where(CmfStressSampleDao.Properties.DeviceId.eq(deviceId)) .buildDelete().executeDeleteWithoutDetachingEntities(); session.getCmfHeartRateSampleDao().queryBuilder() .where(CmfHeartRateSampleDao.Properties.DeviceId.eq(deviceId)) .buildDelete().executeDeleteWithoutDetachingEntities(); session.getCmfSleepSessionSampleDao().queryBuilder() .where(CmfSleepSessionSampleDao.Properties.DeviceId.eq(deviceId)) .buildDelete().executeDeleteWithoutDetachingEntities(); session.getCmfSleepStageSampleDao().queryBuilder() .where(CmfSleepStageSampleDao.Properties.DeviceId.eq(deviceId)) .buildDelete().executeDeleteWithoutDetachingEntities(); session.getCmfSpo2SampleDao().queryBuilder() .where(CmfSpo2SampleDao.Properties.DeviceId.eq(deviceId)) .buildDelete().executeDeleteWithoutDetachingEntities(); } @Override public String getManufacturer() { return "Nothing"; } @Override public int getDeviceNameResource() { return R.string.devicetype_nothing_cmf_watch_pro; } @Override public int getDefaultIconResource() { return R.drawable.ic_device_amazfit_bip; } @Override public int getDisabledIconResource() { return R.drawable.ic_device_amazfit_bip_disabled; } @NonNull @Override public Class getDeviceSupportClass() { return CmfWatchProSupport.class; } @Override public int getBondingStyle() { return BONDING_STYLE_REQUIRE_KEY; } @Override public boolean validateAuthKey(final String authKey) { final byte[] authKeyBytes = authKey.trim().getBytes(); return authKeyBytes.length == 32 || (authKey.startsWith("0x") && authKeyBytes.length == 34); } @Override public int[] getSupportedDeviceSpecificAuthenticationSettings() { return new int[]{R.xml.devicesettings_pairingkey}; } @Override public SampleProvider getSampleProvider(final GBDevice device, DaoSession session) { return new CmfActivitySampleProvider(device, session); } @Override public TimeSampleProvider getStressSampleProvider(final GBDevice device, final DaoSession session) { return new CmfStressSampleProvider(device, session); } @Override public int[] getStressRanges() { // 1-29 = relaxed // 30-59 = normal // 60-79 = medium // 80-99 = high return new int[]{1, 30, 60, 80}; } @Override public TimeSampleProvider getSpo2SampleProvider(final GBDevice device, final DaoSession session) { return new CmfSpo2SampleProvider(device, session); } @Nullable @Override public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) { return new CmfWorkoutSummaryParser(device); } @Override public boolean supportsFlashing() { return true; } @Override public int getAlarmSlotCount(final GBDevice device) { return 5; } @Override public boolean supportsAlarmTitle(final GBDevice device) { return true; } @Override public int getAlarmTitleLimit(final GBDevice device) { return 8; } @Override public boolean supportsAppsManagement(final GBDevice device) { return false; // TODO for watchface management } @Override public boolean supportsCachedAppManagement(GBDevice device) { return false; } @Override public boolean supportsInstalledAppManagement(GBDevice device) { return false; } @Override public boolean supportsWatchfaceManagement(GBDevice device) { return supportsAppsManagement(device); } @Override public Class getAppsManagementActivity() { return AppManagerActivity.class; } @Override public boolean supportsAppListFetching() { return false; // TODO it does not, but we can fake it for watchfaces } @Override public boolean supportsActivityDataFetching() { return true; } @Override public boolean supportsActivityTracking() { return true; } @Override public boolean supportsActivityTracks() { return true; } @Override public boolean supportsStressMeasurement() { return true; } @Override public boolean supportsSpo2(GBDevice device) { return true; } @Override public boolean supportsMusicInfo() { return true; } @Override public int getContactsSlotCount(final GBDevice device) { return 20; } @Override public boolean supportsHeartRateMeasurement(final GBDevice device) { return true; } @Override public boolean supportsManualHeartRateMeasurement(final GBDevice device) { return false; } @Override public boolean supportsRemSleep() { return true; } @Override public boolean supportsWeather() { return true; } @Override public boolean supportsFindDevice() { return true; } @Override public int[] getSupportedDeviceSpecificSettings(final GBDevice device) { final List settings = new ArrayList<>(); settings.add(R.xml.devicesettings_header_time); settings.add(R.xml.devicesettings_timeformat); settings.add(R.xml.devicesettings_header_display); settings.add(R.xml.devicesettings_workout_activity_types); settings.add(R.xml.devicesettings_liftwrist_display_noshed); settings.add(R.xml.devicesettings_header_health); settings.add(R.xml.devicesettings_heartrate_sleep_alert_activity_stress_spo2); settings.add(R.xml.devicesettings_inactivity_dnd); settings.add(R.xml.devicesettings_hydration_reminder_dnd); settings.add(R.xml.devicesettings_header_notifications); settings.add(R.xml.devicesettings_send_app_notifications); settings.add(R.xml.devicesettings_transliteration); settings.add(R.xml.devicesettings_header_other); if (getContactsSlotCount(device) > 0) { settings.add(R.xml.devicesettings_contacts); } return ArrayUtils.toPrimitive(settings.toArray(new Integer[0])); } @Override public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { return new CmfWatchProSettingsCustomizer(); } @Override public String[] getSupportedLanguageSettings(final GBDevice device) { return null; // FIXME language setting does not seem to work from phone //return new String[]{ // "auto", // "ar_SA", // "de_DE", // "en_US", // "es_ES", // "fr_FR", // "hi_IN", // "id_ID", // "it_IT", // "ja_JP", // "ko_KO", // "zh_CN", // "zh_HK", //}; } @Override public List getHeartRateMeasurementIntervals() { return Arrays.asList( HeartRateCapability.MeasurementInterval.OFF, HeartRateCapability.MeasurementInterval.SMART ); } protected static Prefs getPrefs(final GBDevice device) { return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress())); } public boolean supportsSunriseSunset() { return false; } }