diff --git a/CHANGELOG.md b/CHANGELOG.md index fcfca102d..748b070ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,18 @@ ### Changelog -#### Version 0.36.3 -* Basic Makibes HR3 support +#### Version 0.37.0 +* Initial Makibes HR3 support +* Amazfit Bip Lite: Inittal working support, firmware update is disabled for now (we do not have any firmware for testing) +* Amazfit Cor 2: Enable Emoji Font setting and 3rd party HR access +* Find Phone now also vibration in addition to playing the ring tone +* ID115: All settings are now per-device +* Time format settings are now per-device for all supported devices +* Wrist location settings are now per-device for all supported devices +* Work around broken layout in database management activity +* Show toast in case no app is installed which can handle GPX files +* Mi Band 4/Amazfit Bip Lite: Trim white spaces and new lines from auth key +* Mi Band 4/Amazfit Bip Lite: Display a toast and do not try to pair if there was no auth key supplied +* Skip service scan if supported device could be recognized without uuids during discovery #### Version 0.36.2 * Amazfit Bip: Untested support for Lite variant diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 3a55d6e58..13868d787 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - Schema schema = new Schema(20, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(21, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -60,6 +60,7 @@ public class GBDaoGenerator { Entity tag = addTag(schema); Entity userDefinedActivityOverlay = addActivityDescription(schema, tag, user); + addMakibesHR3ActivitySample(schema, user, device); addMiBandActivitySample(schema, user, device); addPebbleHealthActivitySample(schema, user, device); addPebbleHealthActivityKindOverlay(schema, user, device); @@ -186,6 +187,16 @@ public class GBDaoGenerator { return deviceAttributes; } + private static Entity addMakibesHR3ActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "MakibesHR3ActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); + return activitySample; + } + private static Entity addMiBandActivitySample(Schema schema, Entity user, Entity device) { Entity activitySample = addEntity(schema, "MiBandActivitySample"); activitySample.implementsSerializable(); @@ -363,7 +374,7 @@ public class GBDaoGenerator { alarm.addBooleanProperty("smartWakeup").notNull(); alarm.addIntProperty("repetition").notNull().codeBeforeGetter( "public boolean isRepetitive() { return getRepetition() != ALARM_ONCE; } " + - "public boolean getRepetition(int dow) { return (this.repetition & dow) > 0; }" + "public boolean getRepetition(int dow) { return (this.repetition & dow) > 0; }" ); alarm.addIntProperty("hour").notNull(); alarm.addIntProperty("minute").notNull(); diff --git a/README.md b/README.md index bec3d8186..ca48beca8 100644 --- a/README.md +++ b/README.md @@ -30,28 +30,30 @@ vendor's servers. [List of changes](https://codeberg.org/Freeyourgadget/Gadgetbridge/src/master/CHANGELOG.md) -## Supported Devices +## Supported Devices (Some of them WIP and some of them without maintainer) * Amazfit Bip [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip) +* Amazfit Bip Lite (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Bip-Lite) * Amazfit Cor [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor) * Amazfit Cor 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Amazfit-Cor-2) * BFH-16 -* Casio GB-6900B (WIP) +* Casio GB-6900B * HPlus Devices (e.g. ZeBand) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/HPlus) -* ID115 (WIP) -* Lenovo Watch 9 (WIP) -* Liveview (WIP) +* ID115 +* Lenovo Watch 9 +* Liveview +* Makibes HR3 * Mi Band, Mi Band 1A, Mi Band 1S [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band) * Mi Band 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-2) * Mi Band 3 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-3) -* Mi Band 4 (WIP, NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4) +* Mi Band 4 (NOT RECOMMENDED, NEEDS MI FIT WITH ACCOUNT AND ROOT ONCE) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Mi-Band-4) * Mi Scale 2 (currently only displays a toast after stepping on the scale) -* NO.1 F1 (WIP) +* NO.1 F1 * Pebble, Pebble Steel, Pebble Time, Pebble Time Steel, Pebble Time Round [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble) * Pebble 2 [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Pebble) -* Teclast H10, H30 (WIP) +* Teclast H10, H30 * XWatch (Affordable Chinese Casio-like smartwatches) * Vibratissimo (experimental) -* ZeTime (WIP) [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime) +* ZeTime [Wiki](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/MyKronoz-ZeTime) * Fossil Q Hybrid * Skagen Connected @@ -113,6 +115,7 @@ Please [this wiki article](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki * Andreas Böhler (Casio GB-6900B) * Jean-François Greffier (Mi Scale 2) * Johannes Schmitt (BFH-16) +* Lukas Schwichtenberg (Makibes HR3) ## Contribute diff --git a/app/build.gradle b/app/build.gradle index f3f530079..d90123492 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -25,7 +25,7 @@ android { targetSdkVersion 27 // Note: always bump BOTH versionCode and versionName! - versionName "0.36.3" + versionName "0.37.0" versionCode 158 vectorDrawables.useSupportLibrary = true } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 88327acd4..e7585523d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java index 1183a67ce..6293cb03c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureAlarms.java @@ -80,6 +80,7 @@ public class ConfigureAlarms extends AbstractGBActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQ_CONFIGURE_ALARM) { avoidSendAlarmsToDevice = false; updateAlarmsFromDB(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java index 65c956388..101154439 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DiscoveryActivity.java @@ -35,6 +35,7 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; @@ -195,8 +196,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView private final BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { - LOG.warn(device.getName() + ": " + ((scanRecord != null) ? scanRecord.length : -1)); - logMessageContent(scanRecord); + //logMessageContent(scanRecord); handleDeviceFound(device, (short) rssi); } }; @@ -338,6 +338,12 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } private void handleDeviceFound(BluetoothDevice device, short rssi) { + if (device.getName() != null) { + if (handleDeviceFound(device,rssi, null)) { + LOG.info("found supported device " + device.getName() + " without scanning services, skipping service scan."); + return; + } + } ParcelUuid[] uuids = device.getUuids(); if (uuids == null) { if (device.fetchUuidsWithSdp()) { @@ -349,7 +355,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } - private void handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) { + private boolean handleDeviceFound(BluetoothDevice device, short rssi, ParcelUuid[] uuids) { LOG.debug("found device: " + device.getName() + ", " + device.getAddress()); if (LOG.isDebugEnabled()) { if (uuids != null && uuids.length > 0) { @@ -359,7 +365,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView } } if (device.getBondState() == BluetoothDevice.BOND_BONDED) { - return; // ignore already bonded devices + return true; // ignore already bonded devices } GBDeviceCandidate candidate = new GBDeviceCandidate(device, rssi, uuids); @@ -374,7 +380,9 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView deviceCandidates.add(candidate); } cadidateListAdapter.notifyDataSetChanged(); + return true; } + return false; } /** @@ -620,6 +628,17 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView stopDiscovery(); DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(deviceCandidate); LOG.info("Using device candidate " + deviceCandidate + " with coordinator: " + coordinator.getClass()); + + if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_REQUIRE_KEY) { + SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress()); + + String authKey = sharedPrefs.getString("authkey", null); + if (authKey == null || authKey.isEmpty() || authKey.getBytes().length < 34 || !authKey.substring(0, 2).equals("0x")) { + GB.toast(DiscoveryActivity.this, getString(R.string.discovery_need_to_enter_authkey), Toast.LENGTH_LONG, GB.WARN); + return; + } + } + Class pairingActivity = coordinator.getPairingActivity(); if (pairingActivity != null) { Intent intent = new Intent(this, pairingActivity); @@ -627,7 +646,7 @@ public class DiscoveryActivity extends AbstractGBActivity implements AdapterView startActivity(intent); } else { GBDevice device = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate); - int bondingStyle = coordinator.getBondingStyle(device); + int bondingStyle = coordinator.getBondingStyle(); if (bondingStyle == DeviceCoordinator.BONDING_STYLE_NONE) { LOG.info("No bonding needed, according to coordinator, so connecting right away"); connectAndFinish(device); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FindPhoneActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FindPhoneActivity.java index dc47d2c2c..cb2a69d6e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FindPhoneActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/FindPhoneActivity.java @@ -25,7 +25,10 @@ import android.media.AudioManager; import android.media.MediaPlayer; import android.media.RingtoneManager; import android.net.Uri; +import android.os.Build; import android.os.Bundle; +import android.os.VibrationEffect; +import android.os.Vibrator; import android.view.View; import android.widget.Button; @@ -58,6 +61,7 @@ public class FindPhoneActivity extends AbstractGBActivity { } }; + Vibrator mVibrator; AudioManager mAudioManager; int userVolume; MediaPlayer mp; @@ -79,10 +83,26 @@ public class FindPhoneActivity extends AbstractGBActivity { finish(); } }); + + vibrate(); playRingtone(); } - public void playRingtone(){ + private void vibrate(){ + mVibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE); + + long[] vibrationPattern = new long[]{ 1000, 1000 }; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + VibrationEffect vibrationEffect = VibrationEffect.createWaveform(vibrationPattern, 0); + + mVibrator.vibrate(vibrationEffect); + } else { + mVibrator.vibrate(vibrationPattern, 0); + } + } + + private void playRingtone(){ mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE); if (mAudioManager != null) { userVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_ALARM); @@ -107,7 +127,11 @@ public class FindPhoneActivity extends AbstractGBActivity { } } - public void stopSound() { + private void stopVibration() { + mVibrator.cancel(); + } + + private void stopSound() { mAudioManager.setStreamVolume(AudioManager.STREAM_ALARM, userVolume, AudioManager.FLAG_PLAY_SOUND); mp.stop(); mp.reset(); @@ -116,7 +140,10 @@ public class FindPhoneActivity extends AbstractGBActivity { @Override protected void onDestroy() { super.onDestroy(); + + stopVibration(); stopSound(); + LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); unregisterReceiver(mReceiver); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java index c2ab8b94a..bdb095c2a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/LiveActivityFragment.java @@ -346,7 +346,7 @@ public class LiveActivityFragment extends AbstractChartFragment { renderCharts(); - // have to enable it again and again to keep it measureing + // have to enable it again and again to keep it measuring GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index cb1fe6c63..90d75ef58 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory; import java.util.Objects; import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference; @@ -369,15 +370,25 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat { }); } - EditTextPreference pref = findPreference(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS); - if (pref != null) { - pref.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() { + EditTextPreference mibandTimeOffset = findPreference(MiBandConst.PREF_MIBAND_DEVICE_TIME_OFFSET_HOURS); + if (mibandTimeOffset != null) { + mibandTimeOffset.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() { @Override public void onBindEditText(@NonNull EditText editText) { editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_SIGNED); } }); } + + EditTextPreference findPhoneDuration = findPreference(MakibesHR3Constants.PREF_FIND_PHONE_DURATION); + if (findPhoneDuration != null) { + findPhoneDuration.setOnBindEditTextListener(new EditTextPreference.OnBindEditTextListener() { + @Override + public void onBindEditText(@NonNull EditText editText) { + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + } + }); + } } static DeviceSpecificSettingsFragment newInstance(String settingsFileSuffix, @NonNull int[] supportedSettings) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index 6d8a030f3..46bd46344 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -136,7 +136,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { } @Override - public int getBondingStyle(GBDevice device) { + public int getBondingStyle() { return BONDING_STYLE_ASK; } @@ -159,6 +159,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return false; } + @NonNull @Override public int[] getColorPresets() { return new int[0]; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index a9d7fd7e7..567fa5b8d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -29,7 +29,6 @@ import java.util.Collection; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import nodomain.freeyourgadget.gadgetbridge.GBException; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsFragment; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; @@ -63,6 +62,11 @@ public interface DeviceCoordinator { */ int BONDING_STYLE_ASK = 2; + /** + * A secret key has to be entered before connecting + */ + int BONDING_STYLE_REQUIRE_KEY = 3; + /** * Checks whether this coordinator handles the given candidate. * Returns the supported device type for the given candidate or @@ -224,9 +228,8 @@ public interface DeviceCoordinator { /** * Returns how/if the given device should be bonded before connecting to it. - * @param device */ - int getBondingStyle(GBDevice device); + int getBondingStyle(); /** * Indicates whether the device has some kind of calender we can sync to. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java index df6fc0596..255cacdd7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/UnknownDeviceCoordinator.java @@ -93,6 +93,7 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { sampleProvider = new UnknownSampleProvider(); } + @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { return DeviceType.UNKNOWN; @@ -197,6 +198,7 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator { return false; } + @NonNull @Override public int[] getColorPresets() { return new int[0]; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casiogb6900/CasioGB6900DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casiogb6900/CasioGB6900DeviceCoordinator.java index cb9dc6cad..3bd12815a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casiogb6900/CasioGB6900DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/casiogb6900/CasioGB6900DeviceCoordinator.java @@ -58,7 +58,7 @@ public class CasioGB6900DeviceCoordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice deviceCandidate){ + public int getBondingStyle(){ return BONDING_STYLE_BOND; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java index 621ae54d2..b9e3326d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/hplus/HPlusCoordinator.java @@ -84,7 +84,7 @@ public class HPlusCoordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice deviceCandidate){ + public int getBondingStyle(){ return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipLiteCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipLiteCoordinator.java index d25067b77..b2ce87a0b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipLiteCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbip/AmazfitBipLiteCoordinator.java @@ -57,4 +57,9 @@ public class AmazfitBipLiteCoordinator extends AmazfitBipCoordinator { public InstallHandler findInstallHandler(Uri uri, Context context) { return null; } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_REQUIRE_KEY; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor2/AmazfitCor2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor2/AmazfitCor2Coordinator.java index eab4dce79..3f59b5287 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor2/AmazfitCor2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitcor2/AmazfitCor2Coordinator.java @@ -87,8 +87,11 @@ public class AmazfitCor2Coordinator extends HuamiCoordinator { return new int[]{ R.xml.devicesettings_amazfitcor, R.xml.devicesettings_wearlocation, + R.xml.devicesettings_custom_emoji_font, R.xml.devicesettings_liftwrist_display, R.xml.devicesettings_disconnectnotification, - R.xml.devicesettings_pairingkey}; + R.xml.devicesettings_expose_hr_thirdparty, + R.xml.devicesettings_pairingkey + }; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java index 52d22230a..b087dbe1d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband2/MiBand2Coordinator.java @@ -46,11 +46,6 @@ public class MiBand2Coordinator extends HuamiCoordinator { @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { - if (candidate.supportsService(HuamiService.UUID_SERVICE_MIBAND2_SERVICE)) { - return DeviceType.MIBAND2; - } - - // and a heuristic for now try { BluetoothDevice device = candidate.getDevice(); String name = device.getName(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java index 68ce12840..c451e8d49 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband4/MiBand4Coordinator.java @@ -97,4 +97,9 @@ public class MiBand4Coordinator extends HuamiCoordinator { R.xml.devicesettings_pairingkey }; } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_REQUIRE_KEY; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Coordinator.java index f60532eaf..fb8717b6d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/id115/ID115Coordinator.java @@ -66,7 +66,7 @@ public class ID115Coordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice deviceCandidate){ + public int getBondingStyle(){ return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/BFH16DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/BFH16DeviceCoordinator.java index 020a38485..dd90e41a4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/BFH16DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/BFH16DeviceCoordinator.java @@ -70,6 +70,7 @@ public class BFH16DeviceCoordinator extends AbstractDeviceCoordinator return Collections.singletonList(filter); } + @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { @@ -85,7 +86,7 @@ public class BFH16DeviceCoordinator extends AbstractDeviceCoordinator } @Override - public int getBondingStyle(GBDevice deviceCandidate){ + public int getBondingStyle(){ return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java index 4c994bf1f..68a464af4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java @@ -82,7 +82,7 @@ public class TeclastH30Coordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice deviceCandidate){ + public int getBondingStyle(){ return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java index 7805d8ac7..7a717c18b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Constants.java @@ -21,18 +21,19 @@ import java.util.UUID; public final class MakibesHR3Constants { + // TODO: This doesn't belong here, but I don't want to touch other files to avoid + // TODO: breaking someone else's device support. + public static final String PREF_HEADS_UP_SCREEN = "activate_display_on_lift_wrist"; + public static final String PREF_LOST_REMINDER = "disconnect_notification"; + public static final String PREF_DO_NOT_DISTURB = "do_not_disturb_no_auto"; + public static final String PREF_DO_NOT_DISTURB_START = "do_not_disturb_no_auto_start"; + public static final String PREF_DO_NOT_DISTURB_END = "do_not_disturb_no_auto_end"; + public static final String PREF_FIND_PHONE = "prefs_find_phone"; + public static final String PREF_FIND_PHONE_DURATION = "prefs_find_phone_duration"; public static final UUID UUID_SERVICE = UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e"); public static final UUID UUID_CHARACTERISTIC_CONTROL = UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e"); - - // time - // mode ab:00:04:ff:7c:80:** (00: 24h, 01: 12h) - - // confirm write? - // ab:00:09:ff:52:80:00:13:06:09:0f:0b - - // disconnect? - // ab:00:03:ff:ff:80 + public static final UUID UUID_CHARACTERISTIC_REPORT = UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e"); // Services and Characteristics // 00001801-0000-1000-8000-00805f9b34fb @@ -44,8 +45,8 @@ public final class MakibesHR3Constants { // 00002a04-0000-1000-8000-00805f9b34fb // 00002aa6-0000-1000-8000-00805f9b34fb // 6e400001-b5a3-f393-e0a9-e50e24dcca9e // Nordic UART Service - // 6e400002-b5a3-f393-e0a9-e50e24dcca9e // control - // 6e400003-b5a3-f393-e0a9-e50e24dcca9e + // 6e400002-b5a3-f393-e0a9-e50e24dcca9e // control (RX) + // 6e400003-b5a3-f393-e0a9-e50e24dcca9e // report // 0000fee7-0000-1000-8000-00805f9b34fb // 0000fec9-0000-1000-8000-00805f9b34fb // 0000fea1-0000-1000-8000-00805f9b34fb @@ -54,11 +55,8 @@ public final class MakibesHR3Constants { // Command structure // ab 00 [argument_count] ff [command] 80 [arguments] // where [argument_count] is [arguments].length + 3 + // 80 might by different. - // refresh sends - // 51 - // 52 - // 93 (CMD_SET_DATE_TIME) public static final byte[] DATA_TEMPLATE = { (byte) 0xab, @@ -74,37 +72,137 @@ public final class MakibesHR3Constants { public static final int DATA_COMMAND_INDEX = 4; public static final int DATA_ARGUMENTS_INDEX = 6; + // blood oxygen percentage + public static final byte[] RPRT_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 }; + + + // blood oxygen percentage + // blood oxygen percentage + public static final byte[] RPRT_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 }; + + + // steps might take up more bytes. I don't know which ones and I won't walk that much. + // Only sent after we send CMD_51 + // 00 (maybe also used for steps) + // [steps hi] + // [steps lo] + // 00 + // 00 + // 01 (also was 0b. Maybe minutes of activity.) + // 00 + // 00 + // 00 + // 00 + // 00 + public static final byte[] RPRT_FITNESS = new byte[]{ (byte) 0x51, 0x08 }; + + + // year (+2000) + // month + // day + // hour + // minute + // heart rate + // heart rate + public static final byte[] RPRT_HEART_RATE_SAMPLE = new byte[]{ (byte) 0x51, (byte) 0x11 }; + + + // WearFit says "walking" in the step details. This is probably also in here, but + // I don't run :O + // year (+2000) + // month + // day + // hour (start of measurement. interval is 1h. Might be longer when running.) + // 00 (either used for steps or minute) + // accumulated steps (hi) + // accumulated steps (lo) + // 00 + // 00 + // ?? (changes whenever steps change. Ranges from 00 to 16.) + // 00 + // 00 + // 00 + // 00 + public static final byte[] RPRT_STEPS_SAMPLE = new byte[]{ (byte) 0x51, (byte) 0x20 }; + + + // enable (00/01) + public static final byte RPRT_REVERSE_FIND_DEVICE = (byte) 0x7d; + + + // The proximity sensor sees air.. + public static final byte ARG_HEARTRATE_NO_TARGET = (byte) 0xff; + // The hr sensor didn't find the heart rate yet. + public static final byte ARG_HEARTRATE_NO_READING = (byte) 0x00; + + // heart rate + public static final byte RPRT_HEARTRATE = (byte) 0x84; + + + // charging (00/01) + // battery percentage (step size is 20). + public static final byte RPRT_BATTERY = (byte) 0x91; + + // firmware_major + // firmware_minor + // 37 + // 00 + // 00 + // 00 + // 00 + // 00 + // 00 + // 20 + // 0e + public static final byte RPRT_SOFTWARE = (byte) 0x92; // 00 public static final byte CMD_FACTORY_RESET = (byte) 0x23; - // 00 - // year (+2000) - // month - // day - // 0b - // 00 - // year (+2000) - // month - // day - // 0b - // 19 - public static final byte CMD_UNKNOWN_51 = (byte) 0x51; + // enable (00/01) + public static final byte[] CMD_SET_REAL_TIME_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x12 }; - // this is the last command sent on sync + + // After disabling, the watch replies with RPRT_SINGLE_BLOOD_OXYGEN + // enable (00/01) + public static final byte[] CMD_SET_SINGLE_BLOOD_OXYGEN = new byte[]{ (byte) 0x31, (byte) 0x11 }; + + // device replies with + // {@link MakibesHR3Constants#RPRT_HEART_RATE_SAMPLE} + // {@link MakibesHR3Constants#RPRT_STEPS_SAMPLE} (Only if steps are non-zero) + // {@link MakibesHR3Constants#RPRT_FITNESS} + // there are also multiple 6 * 00 reports // 00 - // year (+2000) - // month - // 14 this isn't the current day + // year (+2000) steps after + // month steps after + // day steps after + // hour steps after + // minute steps after + // year (+2000) heart rate after + // month heart rate after + // day heart rate after + // hour heart rate after + // minute heart rate after + public static final byte CMD_REQUEST_FITNESS = (byte) 0x51; + + + // Manually sending this doesn't yield a reply. The heart rate history is sent in response to + // CMD_CMD_REQUEST_FITNESS. + // 00 + // year (+2000) (probably not current) + // month (not current!) + // day (not current!) // hour (current) // minute (current) - public static final byte CMD_UNKNOWN_52 = (byte) 0x52; + public static final byte CMD_52 = (byte) 0x52; + // vibrates 6 times public static final byte CMD_FIND_DEVICE = (byte) 0x71; + // WearFit writes uses other sources as well. They don't do anything though. public static final byte ARG_SEND_NOTIFICATION_SOURCE_CALL = (byte) 0x01; public static final byte ARG_SEND_NOTIFICATION_SOURCE_STOP_CALL = (byte) 0x02; public static final byte ARG_SEND_NOTIFICATION_SOURCE_MESSAGE = (byte) 0x03; @@ -118,27 +216,33 @@ public final class MakibesHR3Constants { public static final byte ARG_SEND_NOTIFICATION_SOURCE_WEIBO = (byte) 0x13; public static final byte ARG_SEND_NOTIFICATION_SOURCE_KAKOTALK = (byte) 0x14; // ARG_SET_NOTIFICATION_SOURCE_* - // 02 + // 02 (This is 00 and 01 during connection. I don't know what it does. Maybe clears notifications?) // ASCII public static final byte CMD_SEND_NOTIFICATION = (byte) 0x72; public static final byte ARG_SET_ALARM_REMINDER_REPEAT_WEEKDAY = (byte) 0x1F; - public static final byte ARG_SET_ALARM_REMINDER_REPEAT_CUSTOM = (byte) 0x40; public static final byte ARG_SET_ALARM_REMINDER_REPEAT_EVERY_DAY = (byte) 0x7F; public static final byte ARG_SET_ALARM_REMINDER_REPEAT_ONE_TIME = (byte) 0x80; + public static final byte ARG_SET_ALARM_REMINDER_REPEAT_MONDAY = (byte) 0x01; + public static final byte ARG_SET_ALARM_REMINDER_REPEAT_TUESDAY = (byte) 0x02; + public static final byte ARG_SET_ALARM_REMINDER_REPEAT_WEDNESDAY = (byte) 0x04; + public static final byte ARG_SET_ALARM_REMINDER_REPEAT_THURSDAY = (byte) 0x08; + public static final byte ARG_SET_ALARM_REMINDER_REPEAT_FRIDAY = (byte) 0x10; + public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SATURDAY = (byte) 0x20; + public static final byte ARG_SET_ALARM_REMINDER_REPEAT_SUNDAY = (byte) 0x40; + // reminder id starting at 0 // enable (00/01) // hour // minute - // ARG_SET_ALARM_REMINDER_REPEAT_* + // bit field of ARG_SET_ALARM_REMINDER_REPEAT_* public static final byte CMD_SET_ALARM_REMINDER = (byte) 0x73; public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_MILES = (byte) 0x00; public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_KILOMETERS = (byte) 0x01; - public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_LENGTH_INCHES = (byte) 0x00; - public static final byte ARG_SET_PERSONAL_INFORMATION_UNIT_LENGTH_CENTIMETERS = (byte) 0x01; + // step length (in/cm) // step length (in/cm) // age (years) // height (in/cm) @@ -177,6 +281,10 @@ public final class MakibesHR3Constants { public static final byte CMD_SET_HEADS_UP_SCREEN = (byte) 0x77; + // Looks like enable/disable. + public static final byte CMD_78 = (byte) 0x78; + + // The watch enters photograph mode, but doesn't appear to send a trigger signal. // enable (00/01) public static final byte CMD_SET_PHOTOGRAPH_MODE = (byte) 0x79; @@ -188,14 +296,32 @@ public final class MakibesHR3Constants { // 7b has 1 argument. Looks like enable/disable. - // 7e has 14 arguments. - public static final byte ARG_SET_TIMEMODE_24H = 0x00; public static final byte ARG_SET_TIMEMODE_12H = 0x01; // ARG_SET_TIMEMODE_* public static final byte CMD_SET_TIMEMODE = (byte) 0x7c; + // 14 arguments. Watch might reply with RPRT_BATTERY. + public static final byte CMD_7e = (byte) 0x7e; + + + // 01 + // fall hour + // fall minute + // awake hour + // awake minute + public static final byte CMD_SET_SLEEP_TIME = (byte) 0x7f; + + + // enable (00/01) + public static final byte CMD_SET_REAL_TIME_HEART_RATE = (byte) 0x84; + + + // looks like enable/disable. + public static final byte CMD_85 = (byte) 0x85; + + // 00 // year hi // year lo @@ -206,6 +332,16 @@ public final class MakibesHR3Constants { // second public static final byte CMD_SET_DATE_TIME = (byte) 0x93; + // 3 arguments. Sent when saving personal information. + public static final byte CMD_95 = (byte) 0x95; + + // looks like enable/disable. + public static final byte CMD_96 = (byte) 0x96; + + + // looks like enable/disable. + public static final byte CMD_e5 = (byte) 0xe5; + // If this is sent after {@link CMD_FACTORY_RESET}, it's a shutdown, not a reboot. // Rebooting resets the watch face and wallpaper. diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java index a81c789a9..d17f535e1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3Coordinator.java @@ -16,6 +16,10 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; + import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; @@ -27,7 +31,7 @@ import androidx.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import de.greenrobot.dao.query.QueryBuilder; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; @@ -36,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -46,28 +51,101 @@ import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext; public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { + public static final int FindPhone_ON = -1; + public static final int FindPhone_OFF = 0; + private static final Logger LOG = LoggerFactory.getLogger(MakibesHR3Coordinator.class); - public static byte getTimeMode(String deviceAddress) { - SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceAddress); - String tmode = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h)); + public static boolean shouldEnableHeadsUpScreen(SharedPreferences sharedPrefs) { + String liftMode = sharedPrefs.getString(MakibesHR3Constants.PREF_HEADS_UP_SCREEN, getContext().getString(R.string.p_on)); - LOG.debug("tmode is " + tmode); + // Makibes HR3 doesn't support scheduled intervals. Treat it as "on". + return !liftMode.equals(getContext().getString(R.string.p_off)); + } - if (getContext().getString(R.string.p_timeformat_24h).equals(tmode)) { + public static boolean shouldEnableLostReminder(SharedPreferences sharedPrefs) { + String lostReminder = sharedPrefs.getString(MakibesHR3Constants.PREF_LOST_REMINDER, getContext().getString(R.string.p_on)); + + // Makibes HR3 doesn't support scheduled intervals. Treat it as "on". + return !lostReminder.equals(getContext().getString(R.string.p_off)); + } + + public static byte getTimeMode(SharedPreferences sharedPrefs) { + String timeMode = sharedPrefs.getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, getContext().getString(R.string.p_timeformat_24h)); + + if (timeMode.equals(getContext().getString(R.string.p_timeformat_24h))) { return MakibesHR3Constants.ARG_SET_TIMEMODE_24H; } else { return MakibesHR3Constants.ARG_SET_TIMEMODE_12H; } } + /** + * @param startOut out Only hour/minute are used. + * @param endOut out Only hour/minute are used. + * @return True if quite hours are enabled. + */ + public static boolean getQuiteHours(SharedPreferences sharedPrefs, Calendar startOut, Calendar endOut) { + String doNotDisturb = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB, getContext().getString(R.string.p_off)); + + if (doNotDisturb.equals(getContext().getString(R.string.p_off))) { + return false; + } else { + String start = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB_START, "00:00"); + String end = sharedPrefs.getString(MakibesHR3Constants.PREF_DO_NOT_DISTURB_END, "00:00"); + + DateFormat df = new SimpleDateFormat("HH:mm"); + + try { + startOut.setTime(df.parse(start)); + endOut.setTime(df.parse(end)); + + return true; + } catch (Exception e) { + LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage()); + return false; + } + } + } + + /** + * @return {@link #FindPhone_OFF}, {@link #FindPhone_ON}, or the duration + */ + public static int getFindPhone(SharedPreferences sharedPrefs) { + String findPhone = sharedPrefs.getString(MakibesHR3Constants.PREF_FIND_PHONE, getContext().getString(R.string.p_off)); + + if (findPhone.equals(getContext().getString(R.string.p_off))) { + return FindPhone_OFF; + } else if (findPhone.equals(getContext().getString(R.string.p_on))) { + return FindPhone_ON; + } else { // Duration + String duration = sharedPrefs.getString(MakibesHR3Constants.PREF_FIND_PHONE_DURATION, "0"); + + try { + int iDuration; + + try { + iDuration = Integer.valueOf(duration); + } catch (Exception ex) { + LOG.warn(ex.getMessage()); + iDuration = 60; + } + + return iDuration; + } catch (Exception e) { + LOG.error("Unexpected exception in MiBand2Coordinator.getTime: " + e.getMessage()); + return FindPhone_ON; + } + } + } + @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { String name = candidate.getDevice().getName(); - // TODO: + // TODO: Device discovery if ((name != null) && name.equals("Y808")) { return DeviceType.MAKIBESHR3; } @@ -77,11 +155,13 @@ public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { @Override protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { - + Long deviceId = device.getId(); + QueryBuilder qb = session.getMakibesHR3ActivitySampleDao().queryBuilder(); + qb.where(MakibesHR3ActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities(); } @Override - public int getBondingStyle(GBDevice deviceCandidate) { + public int getBondingStyle() { return BONDING_STYLE_NONE; } @@ -92,7 +172,7 @@ public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { @Override public boolean supportsRealtimeData() { - return false; + return true; } @Override @@ -123,12 +203,12 @@ public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { @Override public boolean supportsActivityTracking() { - return false; + return true; } @Override public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { - return null; + return new MakibesHR3SampleProvider(device, session); } @Override @@ -143,8 +223,7 @@ public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { @Override public int getAlarmSlotCount() { - // TODO: - return 5; + return 8; } @Override @@ -175,7 +254,11 @@ public class MakibesHR3Coordinator extends AbstractDeviceCoordinator { @Override public int[] getSupportedDeviceSpecificSettings(GBDevice device) { return new int[]{ - R.xml.devicesettings_timeformat + R.xml.devicesettings_timeformat, + R.xml.devicesettings_liftwrist_display, + R.xml.devicesettings_disconnectnotification, + R.xml.devicesettings_donotdisturb_no_auto, + R.xml.devicesettings_find_phone }; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3SampleProvider.java new file mode 100644 index 000000000..fd6f4ee01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/makibeshr3/MakibesHR3SampleProvider.java @@ -0,0 +1,84 @@ +/* Copyright (C) 2018-2019 Daniele Gobbetti, Sebastian Kranz + + 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.makibeshr3; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class MakibesHR3SampleProvider extends AbstractSampleProvider { + + private GBDevice mDevice; + private DaoSession mSession; + + public MakibesHR3SampleProvider(GBDevice device, DaoSession session) { + super(device, session); + + mSession = session; + mDevice = device; + } + + @Override + public int normalizeType(int rawType) { + return rawType; + } + + @Override + public int toRawActivityKind(int activityKind) { + return activityKind; + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity; + } + + @Override + public MakibesHR3ActivitySample createActivitySample() { + return new MakibesHR3ActivitySample(); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getMakibesHR3ActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return MakibesHR3ActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return MakibesHR3ActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return MakibesHR3ActivitySampleDao.Properties.DeviceId; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/mijia_lywsd02/MijiaLywsd02Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/mijia_lywsd02/MijiaLywsd02Coordinator.java index d0bcdee58..76e05fdba 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/mijia_lywsd02/MijiaLywsd02Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/mijia_lywsd02/MijiaLywsd02Coordinator.java @@ -49,7 +49,7 @@ public class MijiaLywsd02Coordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice deviceCandidate) { + public int getBondingStyle() { return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miscale2/MiScale2DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miscale2/MiScale2DeviceCoordinator.java index c2308026f..c042e04d4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miscale2/MiScale2DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miscale2/MiScale2DeviceCoordinator.java @@ -85,7 +85,7 @@ public class MiScale2DeviceCoordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice device) { + public int getBondingStyle() { return super.BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java index 4affefa09..79c5943a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/no1f1/No1F1Coordinator.java @@ -71,7 +71,7 @@ public class No1F1Coordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice deviceCandidate) { + public int getBondingStyle() { return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java index 1aa75024d..574c2f4fe 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/roidmi/RoidmiCoordinator.java @@ -44,7 +44,7 @@ public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice device) { + public int getBondingStyle() { return BONDING_STYLE_BOND; } @@ -133,6 +133,7 @@ public abstract class RoidmiCoordinator extends AbstractDeviceCoordinator { return true; } + @NonNull @Override public int[] getColorPresets() { return RoidmiConst.COLOR_PRESETS; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java index 878076f94..18b0a8844 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9DeviceCoordinator.java @@ -77,7 +77,7 @@ public class Watch9DeviceCoordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice deviceCandidate) { + public int getBondingStyle() { return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java index 3f80a8b53..3ddc4cdc3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java @@ -153,7 +153,7 @@ public class ZeTimeCoordinator extends AbstractDeviceCoordinator { } @Override - public int getBondingStyle(GBDevice device) { + public int getBondingStyle() { return BONDING_STYLE_NONE; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java index c262b00fe..70d39648d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/BluetoothPairingRequestReceiver.java @@ -61,7 +61,7 @@ public class BluetoothPairingRequestReceiver extends BroadcastReceiver { DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice); try { - if (coordinator.getBondingStyle(gbDevice) == DeviceCoordinator.BONDING_STYLE_NONE) { + if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE) { LOG.info("Aborting unwanted pairing request"); abortBroadcast(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java index 52b6e96d3..92b56bb33 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BtLEQueue.java @@ -242,6 +242,7 @@ public final class BtLEQueue { mBluetoothGattServer.addService(service); } } + synchronized (mGattMonitor) { // connectGatt with true doesn't really work ;( too often connection problems if (GBApplication.isRunningMarshmallowOrLater()) { @@ -259,6 +260,7 @@ public final class BtLEQueue { private void setDeviceConnectionState(State newState) { LOG.debug("new device connection state: " + newState); + mGbDevice.setState(newState); mGbDevice.sendDeviceUpdateIntent(mContext); if (mConnectionLatch != null && newState == State.CONNECTED) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipLiteSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipLiteSupport.java index 4db0af32b..fb6c6bb61 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipLiteSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitbip/AmazfitBipLiteSupport.java @@ -20,8 +20,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitbip; import android.content.Context; import android.net.Uri; -import java.io.IOException; - import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; public class AmazfitBipLiteSupport extends AmazfitBipSupport { @@ -37,7 +35,7 @@ public class AmazfitBipLiteSupport extends AmazfitBipSupport { } @Override - public HuamiFWHelper createFWHelper(Uri uri, Context context) throws IOException { + public HuamiFWHelper createFWHelper(Uri uri, Context context) { return null; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java index b655a8527..974b72128 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation.java @@ -94,7 +94,7 @@ public class InitOperation extends AbstractBTLEOperation { String authKey = sharedPrefs.getString("authkey", null); if (authKey != null && !authKey.isEmpty()) { - byte[] srcBytes = authKey.getBytes(); + byte[] srcBytes = authKey.trim().getBytes(); if (authKey.length() == 34 && authKey.substring(0, 2).equals("0x")) { srcBytes = GB.hexStringToByteArray(authKey.substring(2)); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java index 6a782481e..bc6830e25 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/makibeshr3/MakibesHR3DeviceSupport.java @@ -1,22 +1,66 @@ +// TODO: GB sometimes fails to connect until a connection with WearFit was made. This must be caused +// TODO: by GB, not by Makibes hr3 support. Charging the watch, attempting to pair, delete and +// TODO: re-add, scan for devices and go back, might also help. This needs further research. + +// TODO: All the commands that aren't supported by GB should be added to device specific settings. + +// TODO: It'd be cool if we could change the language. There's no official way to do so, but the +// TODO: watch is sold as chinese/english. Screen-on-time would be nice too. + +// TODO: Firmware upgrades. WearFit tries to connect to Wake up Technology at +// TODO: http://47.112.119.52/app.php/Api/hardUpdate/type/55 +// TODO: But that server resets the connection. +// TODO: The host is supposed to be www.iwhop.com, but that domain no longer exists. +// TODO: I think /app.php is missing a closing php tag. + package nodomain.freeyourgadget.gadgetbridge.service.devices.makibeshr3; +import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; +import android.content.SharedPreferences; import android.net.Uri; +import android.os.CountDownTimer; +import android.os.Handler; +import android.widget.Toast; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Arrays; import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants; import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; 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; @@ -24,12 +68,32 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { +public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport implements SharedPreferences.OnSharedPreferenceChangeListener { private static final Logger LOG = LoggerFactory.getLogger(MakibesHR3DeviceSupport.class); - private BluetoothGattCharacteristic ctrlCharacteristic = null; + // The delay must be at least as long as it takes the watch to respond. + // Reordering the requests could maybe reduce the delay, but this works fine too. + private CountDownTimer mFetchCountDown = new CountDownTimer(2000, 2000) { + @Override + public void onTick(long millisUntilFinished) { + + } + + @Override + public void onFinish() { + LOG.debug("download finished"); + GB.updateTransferNotification(null, "", false, 100, getContext()); + } + }; + + private Handler mFindPhoneHandler = new Handler(); + + private BluetoothGattCharacteristic mControlCharacteristic = null; + private BluetoothGattCharacteristic mReportCharacteristic = null; + public MakibesHR3DeviceSupport() { super(LOG); @@ -37,11 +101,82 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { addSupportedService(MakibesHR3Constants.UUID_SERVICE); } + /** + * Called whenever data is received to postpone the removing of the progress notification. + * + * @param start Start showing the notification + */ + private void fetch(boolean start) { + if (start) { + // We don't know how long the watch is going to take to reply. Keep progress at 0. + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 0, getContext()); + } + + this.mFetchCountDown.cancel(); + this.mFetchCountDown.start(); + } + @Override public boolean useAutoConnect() { return false; } + /** + * @param timeStamp seconds + */ + private void getDayStartEnd(int timeStamp, Calendar start, Calendar end) { + final int DAY = (24 * 60 * 60); + + int timeStampStart = ((timeStamp / DAY) * DAY); + int timeStampEnd = (timeStampStart + DAY); + + start.setTimeInMillis(timeStampStart * 1000L); + end.setTimeInMillis(timeStampEnd * 1000L); + } + + /** + * @param timeStamp Time stamp at some point during the requested day. + */ + private int getStepsOnDay(int timeStamp) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + + Calendar dayStart = new GregorianCalendar(); + Calendar dayEnd = new GregorianCalendar(); + + this.getDayStartEnd(timeStamp, dayStart, dayEnd); + + MakibesHR3SampleProvider provider = new MakibesHR3SampleProvider(this.getDevice(), dbHandler.getDaoSession()); + + List samples = provider.getAllActivitySamples( + (int) (dayStart.getTimeInMillis() / 1000L), + (int) (dayEnd.getTimeInMillis() / 1000L)); + + int totalSteps = 0; + + for (MakibesHR3ActivitySample sample : samples) { + totalSteps += sample.getSteps(); + } + + return totalSteps; + + } catch (Exception ex) { + LOG.error(ex.getMessage()); + + return 0; + } + } + + public MakibesHR3ActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) { + MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample(); + sample.setDevice(device); + sample.setUser(user); + sample.setTimestamp(timestampInSeconds); + sample.setProvider(provider); + + return sample; + } + + @Override public void onNotification(NotificationSpec notificationSpec) { TransactionBuilder transactionBuilder = this.createTransactionBuilder("onnotificaiton"); @@ -77,8 +212,16 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { break; } + String message = ""; + + if (notificationSpec.title != null) { + message += (notificationSpec.title + ": "); + } + + message += notificationSpec.body; + this.sendNotification(transactionBuilder, - sender, notificationSpec.title + ": " + notificationSpec.body); + sender, message); try { this.performConnected(transactionBuilder.getTransaction()); @@ -113,6 +256,34 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { for (int i = 0; i < alarms.size(); ++i) { Alarm alarm = alarms.get(i); + byte repetition = 0x00; + + switch (alarm.getRepetition()) { + case Alarm.ALARM_ONCE: + repetition = MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_ONE_TIME; + break; + + case Alarm.ALARM_MON: + repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_MONDAY; + case Alarm.ALARM_TUE: + repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_TUESDAY; + case Alarm.ALARM_WED: + repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_WEDNESDAY; + case Alarm.ALARM_THU: + repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_THURSDAY; + case Alarm.ALARM_FRI: + repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_FRIDAY; + case Alarm.ALARM_SAT: + repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_SATURDAY; + case Alarm.ALARM_SUN: + repetition |= MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_SUNDAY; + break; + + default: + LOG.warn("invalid alarm repetition " + alarm.getRepetition()); + break; + } + // Should we use @alarm.getPosition() rather than @i? this.setAlarmReminder( transactionBuilder, @@ -120,7 +291,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { alarm.getEnabled(), alarm.getHour(), alarm.getMinute(), - MakibesHR3Constants.ARG_SET_ALARM_REMINDER_REPEAT_CUSTOM); + repetition); } try { @@ -137,7 +308,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { if (callSpec.command == CallSpec.CALL_INCOMING) { this.sendNotification(transactionBuilder, MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_CALL, callSpec.name); } else { - this.sendNotification(transactionBuilder, MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_STOP_CALL, callSpec.name); + this.sendNotification(transactionBuilder, MakibesHR3Constants.ARG_SEND_NOTIFICATION_SOURCE_STOP_CALL, ""); } try { @@ -164,32 +335,26 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onEnableRealtimeSteps(boolean enable) { - } @Override public void onInstallApp(Uri uri) { - } @Override public void onAppInfoReq() { - } @Override public void onAppStart(UUID uuid, boolean start) { - } @Override public void onAppDelete(UUID uuid) { - } @Override public void onAppConfiguration(UUID appUuid, String config, Integer id) { - } @Override @@ -199,7 +364,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onFetchRecordedData(int dataTypes) { - + // what is this? } @Override @@ -228,12 +393,52 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { @Override public void onHeartRateTest() { - } @Override public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + TransactionBuilder transactionBuilder = this.createTransactionBuilder("finddevice"); + this.setEnableRealTimeHeartRate(transactionBuilder, enable); + + try { + this.performConnected(transactionBuilder.getTransaction()); + } catch (Exception e) { + LOG.debug("ERROR"); + } + } + + private void onReverseFindDevice(boolean start) { + if (start) { + SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs( + this.getDevice().getAddress()); + + int findPhone = MakibesHR3Coordinator.getFindPhone(sharedPreferences); + + if (findPhone != MakibesHR3Coordinator.FindPhone_OFF) { + GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone(); + + findPhoneEvent.event = GBDeviceEventFindPhone.Event.START; + + evaluateGBDeviceEvent(findPhoneEvent); + + if (findPhone > 0) { + this.mFindPhoneHandler.postDelayed(new Runnable() { + @Override + public void run() { + onReverseFindDevice(false); + } + }, findPhone * 1000); + } + } + } else { + // Always send stop, ignore preferences. + GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone(); + + findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP; + + evaluateGBDeviceEvent(findPhoneEvent); + } } @Override @@ -303,51 +508,102 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { } - private MakibesHR3DeviceSupport sendUserInfo(TransactionBuilder builder) { - // builder.write(ctrlCharacteristic, MakibesHR3Constants.CMD_SET_PREF_START); - // builder.write(ctrlCharacteristic, MakibesHR3Constants.CMD_SET_PREF_START1); + private void syncPreferences(TransactionBuilder transaction) { - syncPreferences(builder); + SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress()); - // builder.write(ctrlCharacteristic, new byte[]{MakibesHR3Constants.CMD_SET_CONF_END}); - return this; + this.setTimeMode(transaction, sharedPreferences); + this.setDateTime(transaction); + this.setQuiteHours(transaction, sharedPreferences); + + this.setHeadsUpScreen(transaction, sharedPreferences); + this.setLostReminder(transaction, sharedPreferences); + + ActivityUser activityUser = new ActivityUser(); + + this.setPersonalInformation(transaction, + (byte) Math.round(activityUser.getHeightCm() * 0.43), // Thanks no1f1 + activityUser.getAge(), + activityUser.getHeightCm(), + activityUser.getWeightKg(), + activityUser.getStepsGoal() / 1000); + + this.fetch(true); } - private MakibesHR3DeviceSupport syncPreferences(TransactionBuilder transaction) { + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + LOG.debug(key + " changed"); - this.setTimeMode(transaction); - this.setDateTime(transaction); - // setDayOfWeek(transaction); - // setTimeMode(transaction); + if (!this.isConnected()) { + LOG.debug("ignoring change, we're disconnected"); + return; + } - // setGender(transaction); - // setAge(transaction); - // setWeight(transaction); - // setHeight(transaction); + TransactionBuilder transactionBuilder = this.createTransactionBuilder("onSharedPreferenceChanged"); - // setGoal(transaction); - // setLanguage(transaction); - // setScreenTime(transaction); - // setUnit(transaction); - // setAllDayHeart(transaction); + if (key.equals(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT)) { + this.setTimeMode(transactionBuilder, sharedPreferences); + } else if (key.equals(MakibesHR3Constants.PREF_HEADS_UP_SCREEN)) { + this.setHeadsUpScreen(transactionBuilder, sharedPreferences); + } else if (key.equals(MakibesHR3Constants.PREF_LOST_REMINDER)) { + this.setLostReminder(transactionBuilder, sharedPreferences); + } else if (key.equals(MakibesHR3Constants.PREF_DO_NOT_DISTURB) || + key.equals(MakibesHR3Constants.PREF_DO_NOT_DISTURB_START) || + key.equals(MakibesHR3Constants.PREF_DO_NOT_DISTURB_END)) { + this.setQuiteHours(transactionBuilder, sharedPreferences); + } else if (key.equals(MakibesHR3Constants.PREF_FIND_PHONE) || + key.equals(MakibesHR3Constants.PREF_FIND_PHONE_DURATION)) { + // No action, we check the shared preferences when the device tries to ring the phone. + } else { + return; + } - return this; + try { + this.performConnected(transactionBuilder.getTransaction()); + } catch (Exception ex) { + LOG.warn(ex.getMessage()); + } + } + + /** + * Use to show the battery icon in the device card. + * If the icon shows up later, the user might be trying to tap one thing but the battery icon + * will shift everything. + * This is hacky. There should be a "supportsBattery" function in the coordinator that displays + * the battery icon before the battery level is received. + */ + private void fakeBattery() { + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + + batteryInfo.level = 100; + batteryInfo.state = BatteryState.UNKNOWN; + + this.handleGBDeviceEvent(batteryInfo); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + this.fakeBattery(); + + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, 0, getContext()); + gbDevice.setState(GBDevice.State.INITIALIZING); gbDevice.sendDeviceUpdateIntent(getContext()); - this.ctrlCharacteristic = getCharacteristic(MakibesHR3Constants.UUID_CHARACTERISTIC_CONTROL); + this.mControlCharacteristic = getCharacteristic(MakibesHR3Constants.UUID_CHARACTERISTIC_CONTROL); + this.mReportCharacteristic = getCharacteristic(MakibesHR3Constants.UUID_CHARACTERISTIC_REPORT); + builder.notify(this.mReportCharacteristic, true); builder.setGattCallback(this); + // Allow modifications - builder.write(this.ctrlCharacteristic, new byte[]{0x01, 0x00}); + builder.write(this.mControlCharacteristic, new byte[]{0x01, 0x00}); // Initialize device - sendUserInfo(builder); //Sync preferences + this.syncPreferences(builder); + + this.requestFitness(builder); gbDevice.setState(GBDevice.State.INITIALIZED); gbDevice.sendDeviceUpdateIntent(getContext()); @@ -355,27 +611,233 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { getDevice().setFirmwareVersion("N/A"); getDevice().setFirmwareVersion2("N/A"); + SharedPreferences preferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress()); + + preferences.registerOnSharedPreferenceChangeListener(this); + return builder; } + private void addGBActivitySamples(MakibesHR3ActivitySample[] samples) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(this.getDevice(), dbHandler.getDaoSession()); + + MakibesHR3SampleProvider provider = new MakibesHR3SampleProvider(this.getDevice(), dbHandler.getDaoSession()); + + for (MakibesHR3ActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setProvider(provider); + + sample.setRawIntensity(ActivitySample.NOT_MEASURED); + + provider.addGBActivitySample(sample); + } + + } catch (Exception ex) { + // Why is this a toast? The user doesn't care about the error. + GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + + LOG.error(ex.getMessage()); + } + } + + private void addGBActivitySample(MakibesHR3ActivitySample sample) { + this.addGBActivitySamples(new MakibesHR3ActivitySample[]{sample}); + } + /** - * @param command - * @param data - * @return + * Should only be called after the sample has been populated by + * {@link MakibesHR3DeviceSupport#addGBActivitySample} or + * {@link MakibesHR3DeviceSupport#addGBActivitySamples} */ - private byte[] craftData(byte command, byte[] data) { + private void broadcastSample(MakibesHR3ActivitySample sample) { + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) + .putExtra(DeviceService.EXTRA_TIMESTAMP, sample.getTimestamp()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + + private void onReceiveFitness(int steps) { + LOG.info("steps: " + steps); + + this.onReceiveStepsSample(steps); + } + + private void onReceiveHeartRate(int heartRate) { + LOG.info("heart rate: " + heartRate); + + MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample(); + + if (heartRate > 0) { + sample.setHeartRate(heartRate); + sample.setTimestamp((int) (System.currentTimeMillis() / 1000)); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + } else { + if (heartRate == MakibesHR3Constants.ARG_HEARTRATE_NO_TARGET) { + sample.setRawKind(ActivityKind.TYPE_NOT_WORN); + } else if (heartRate == MakibesHR3Constants.ARG_HEARTRATE_NO_READING) { + sample.setRawKind(ActivityKind.TYPE_NOT_MEASURED); + } else { + LOG.warn("invalid heart rate reading: " + heartRate); + return; + } + } + + this.addGBActivitySample(sample); + this.broadcastSample(sample); + } + + private void onReceiveHeartRateSample(int year, int month, int day, int hour, int minute, int heartRate) { + LOG.debug("received heart rate sample " + year + "-" + month + "-" + day + " " + hour + ":" + minute + " " + heartRate); + + MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample(); + + Calendar calendar = new GregorianCalendar(year, month - 1, day, hour, minute); + + int timeStamp = (int) (calendar.getTimeInMillis() / 1000); + + sample.setHeartRate(heartRate); + sample.setTimestamp(timeStamp); + + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + + this.addGBActivitySample(sample); + } + + private void onReceiveStepsSample(int timeStamp, int steps) { + MakibesHR3ActivitySample sample = new MakibesHR3ActivitySample(); + + // We need to subtract the day's total step count thus far. + int dayStepCount = this.getStepsOnDay(timeStamp); + + int newSteps = (steps - dayStepCount); + + if (newSteps > 0) { + LOG.debug("adding " + newSteps + " steps"); + + sample.setSteps(steps - dayStepCount); + sample.setTimestamp(timeStamp); + + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + + this.addGBActivitySample(sample); + } + } + + /** + * The time is the start of the measurement. Each measurement lasts 1h. + */ + private void onReceiveStepsSample(int year, int month, int day, int hour, int minute, int steps) { + LOG.debug("received steps sample " + year + "-" + month + "-" + day + " " + hour + ":" + minute + " " + steps); + + Calendar calendar = new GregorianCalendar(year, month - 1, day, hour + 1, minute); + + int timeStamp = (int) (calendar.getTimeInMillis() / 1000); + + this.onReceiveStepsSample(timeStamp, steps); + } + + private void onReceiveStepsSample(int steps) { + this.onReceiveStepsSample((int) (Calendar.getInstance().getTimeInMillis() / 1000l), steps); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + if (super.onCharacteristicChanged(gatt, characteristic)) { + return true; + } + + byte[] data = characteristic.getValue(); + if (data.length < 6) + return true; + + this.fetch(false); + + UUID characteristicUuid = characteristic.getUuid(); + + if (characteristicUuid.equals(mReportCharacteristic.getUuid())) { + byte[] value = characteristic.getValue(); + byte[] arguments = new byte[value.length - 6]; + + for (int i = 0; i < arguments.length; ++i) { + arguments[i] = value[i + 6]; + } + + byte[] report = new byte[]{value[4], value[5]}; + + switch (report[0]) { + case MakibesHR3Constants.RPRT_REVERSE_FIND_DEVICE: + this.onReverseFindDevice(arguments[0] == 0x01); + break; + case MakibesHR3Constants.RPRT_HEARTRATE: + if (value.length == 7) { + this.onReceiveHeartRate((int) arguments[0]); + } + break; + case MakibesHR3Constants.RPRT_BATTERY: + if (arguments.length == 2) { + GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + + batteryInfo.level = (short) (arguments[1] & 0xff); + batteryInfo.state = ((arguments[0] == 0x01) ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL); + + this.handleGBDeviceEvent(batteryInfo); + } + break; + case MakibesHR3Constants.RPRT_SOFTWARE: + if (arguments.length == 11) { + this.getDevice().setFirmwareVersion(((int) (arguments[0] & 0xff)) + "." + (arguments[1] & 0xff)); + } + break; + default: // Non-80 reports + if (Arrays.equals(report, MakibesHR3Constants.RPRT_FITNESS)) { + int steps = (arguments[1] & 0xff) * 0x100 + (arguments[2] & 0xff); + this.onReceiveFitness( + steps + ); + } else if (Arrays.equals(report, MakibesHR3Constants.RPRT_HEART_RATE_SAMPLE)) { + this.onReceiveHeartRateSample( + (arguments[0] & 0xff) + 2000, (arguments[1] & 0xff), (arguments[2] & 0xff), + (arguments[3] & 0xff), (arguments[4] & 0xff), + (arguments[5] & 0xff)); + } else if (Arrays.equals(report, MakibesHR3Constants.RPRT_STEPS_SAMPLE)) { + this.onReceiveStepsSample( + (arguments[0] & 0xff) + 2000, (arguments[1] & 0xff), (arguments[2] & 0xff), + (arguments[3] & 0xff), 0, + ((arguments[5] & 0xff) * 0x100) + (arguments[6] & 0xff)); + } + break; + } + } + + return false; + } + + private byte[] craftData(byte[] command, byte[] data) { byte[] result = new byte[MakibesHR3Constants.DATA_TEMPLATE.length + data.length]; System.arraycopy(MakibesHR3Constants.DATA_TEMPLATE, 0, result, 0, MakibesHR3Constants.DATA_TEMPLATE.length); result[MakibesHR3Constants.DATA_ARGUMENT_COUNT_INDEX] = (byte) (data.length + 3); - result[MakibesHR3Constants.DATA_COMMAND_INDEX] = command; + + for (int i = 0; i < command.length; ++i) { + result[MakibesHR3Constants.DATA_COMMAND_INDEX + i] = command[i]; + } System.arraycopy(data, 0, result, 6, data.length); return result; } + private byte[] craftData(byte command, byte[] data) { + return this.craftData(new byte[]{command}, data); + } + private byte[] craftData(byte command) { return this.craftData(command, new byte[]{}); @@ -385,7 +847,11 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { final int maxMessageLength = 20; // For every split, we need 1 byte extra. - int extraBytes = (((data.length - maxMessageLength) / maxMessageLength) + 1); + int extraBytes = 0; + + if (data.length > 20) { + extraBytes = (((data.length - maxMessageLength) / maxMessageLength) + 1); + } int totalDataLength = (data.length + extraBytes); @@ -422,13 +888,88 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { } private MakibesHR3DeviceSupport factoryReset(TransactionBuilder transaction) { - transaction.write(this.ctrlCharacteristic, this.craftData(MakibesHR3Constants.CMD_FACTORY_RESET)); + transaction.write(this.mControlCharacteristic, this.craftData(MakibesHR3Constants.CMD_FACTORY_RESET)); return this.reboot(transaction); } + /** + * Ugly because I don't like Date. + * All non-zero records after the given times will be returned via + * {@link MakibesHR3Constants#RPRT_HEART_RATE_SAMPLE}, + * {@link MakibesHR3Constants#RPRT_STEPS_SAMPLE}, + * {@link MakibesHR3Constants#RPRT_FITNESS} + */ + private MakibesHR3DeviceSupport requestFitness(TransactionBuilder transaction, + int yearStepsAfter, int monthStepsAfter, int dayStepsAfter, + int hourStepsAfter, int minuteStepsAfter, + int yearHeartRateAfter, int monthHeartRateAfter, int dayHeartRateAfter, + int hourHeartRateAfter, int minuteHeartRateAfter) { + + byte[] data = this.craftData(MakibesHR3Constants.CMD_REQUEST_FITNESS, + new byte[]{ + (byte) 0x00, + (byte) (yearStepsAfter - 2000), + (byte) monthStepsAfter, + (byte) dayStepsAfter, + (byte) hourStepsAfter, + (byte) minuteStepsAfter, + (byte) (yearHeartRateAfter - 2000), + (byte) monthHeartRateAfter, + (byte) dayHeartRateAfter, + (byte) hourHeartRateAfter, + (byte) minuteHeartRateAfter + }); + + transaction.write(this.mControlCharacteristic, data); + + this.fetch(true); + + return this; + } + + /** + * Ugly because I don't like Date. + * All non-zero records after the given times will be returned via + * {@link MakibesHR3Constants#RPRT_HEART_RATE_SAMPLE}, + * {@link MakibesHR3Constants#RPRT_STEPS_SAMPLE}, + * {@link MakibesHR3Constants#RPRT_FITNESS} + */ + private MakibesHR3DeviceSupport requestFitness(TransactionBuilder transaction) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + + MakibesHR3SampleProvider provider = new MakibesHR3SampleProvider(this.getDevice(), dbHandler.getDaoSession()); + + MakibesHR3ActivitySample latestSample = provider.getLatestActivitySample(); + + if (latestSample == null) { + this.requestFitness(transaction, + 2000, 0, 0, 0, 0, + 2000, 0, 0, 0, 0); + } else { + Calendar calendar = new GregorianCalendar(); + calendar.setTime(new Date(latestSample.getTimestamp() * 1000l)); + + int year = calendar.get(Calendar.YEAR); + int month = (calendar.get(Calendar.MONTH) + 1); + int day = calendar.get(Calendar.DAY_OF_MONTH); + int hour = calendar.get(Calendar.HOUR_OF_DAY); + int minute = calendar.get(Calendar.MINUTE); + + this.requestFitness(transaction, + year, month, day, hour, minute, + year, month, day, hour, minute); + } + + } catch (Exception ex) { + LOG.error(ex.getMessage()); + } + + return this; + } + private MakibesHR3DeviceSupport findDevice(TransactionBuilder transaction) { - transaction.write(this.ctrlCharacteristic, this.craftData(MakibesHR3Constants.CMD_FIND_DEVICE)); + transaction.write(this.mControlCharacteristic, this.craftData(MakibesHR3Constants.CMD_FIND_DEVICE)); return this; } @@ -444,7 +985,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { } this.writeSafe( - this.ctrlCharacteristic, + this.mControlCharacteristic, transaction, this.craftData(MakibesHR3Constants.CMD_SEND_NOTIFICATION, data)); @@ -453,7 +994,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { private MakibesHR3DeviceSupport setAlarmReminder(TransactionBuilder transaction, int id, boolean enable, int hour, int minute, byte repeat) { - transaction.write(this.ctrlCharacteristic, + transaction.write(this.mControlCharacteristic, this.craftData(MakibesHR3Constants.CMD_SET_ALARM_REMINDER, new byte[]{ (byte) id, (byte) (enable ? 0x01 : 0x00), @@ -465,12 +1006,112 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { return this; } - private MakibesHR3DeviceSupport setTimeMode(TransactionBuilder transaction) { - byte value = MakibesHR3Coordinator.getTimeMode(getDevice().getAddress()); + /** + * @param transactionBuilder + * @param stepLength cm + * @param age years + * @param height cm + * @param weight kg + * @param stepGoal kilo + */ + private MakibesHR3DeviceSupport setPersonalInformation(TransactionBuilder transactionBuilder, + int stepLength, int age, int height, int weight, int stepGoal) { + byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_PERSONAL_INFORMATION, + new byte[]{ + (byte) stepLength, + (byte) age, + (byte) height, + (byte) weight, + MakibesHR3Constants.ARG_SET_PERSONAL_INFORMATION_UNIT_DISTANCE_KILOMETERS, + (byte) stepGoal, + (byte) 0x5a, + (byte) 0x82, + (byte) 0x3c, + (byte) 0x5a, + (byte) 0x28, + (byte) 0xb4, + (byte) 0x5d, + (byte) 0x64, - byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_TIMEMODE, new byte[]{value}); + }); - transaction.write(this.ctrlCharacteristic, data); + transactionBuilder.write(this.mControlCharacteristic, data); + + return this; + } + + private MakibesHR3DeviceSupport setHeadsUpScreen(TransactionBuilder transactionBuilder, boolean enable) { + byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_HEADS_UP_SCREEN, + new byte[]{(byte) (enable ? 0x01 : 0x00)}); + + transactionBuilder.write(this.mControlCharacteristic, data); + + return this; + } + + private MakibesHR3DeviceSupport setQuiteHours(TransactionBuilder transactionBuilder, + boolean enable, + int hourStart, int minuteStart, + int hourEnd, int minuteEnd) { + byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_QUITE_HOURS, new byte[]{ + (byte) (enable ? 0x01 : 0x00), + (byte) hourStart, (byte) minuteStart, + (byte) hourEnd, (byte) minuteEnd + }); + + transactionBuilder.write(this.mControlCharacteristic, data); + + return this; + } + + private MakibesHR3DeviceSupport setQuiteHours(TransactionBuilder transactionBuilder, + SharedPreferences sharedPreferences) { + + Calendar start = new GregorianCalendar(); + Calendar end = new GregorianCalendar(); + boolean enable = MakibesHR3Coordinator.getQuiteHours(sharedPreferences, start, end); + + return this.setQuiteHours(transactionBuilder, enable, + start.get(Calendar.HOUR_OF_DAY), start.get(Calendar.MINUTE), + end.get(Calendar.HOUR_OF_DAY), end.get(Calendar.MINUTE)); + } + + private MakibesHR3DeviceSupport setHeadsUpScreen(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) { + return this.setHeadsUpScreen(transactionBuilder, + MakibesHR3Coordinator.shouldEnableHeadsUpScreen(sharedPreferences)); + } + + private MakibesHR3DeviceSupport setLostReminder(TransactionBuilder transactionBuilder, boolean enable) { + byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_LOST_REMINDER, + new byte[]{(byte) (enable ? 0x01 : 0x00)}); + + transactionBuilder.write(this.mControlCharacteristic, data); + + return this; + } + + private MakibesHR3DeviceSupport setLostReminder(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) { + return this.setLostReminder(transactionBuilder, + MakibesHR3Coordinator.shouldEnableLostReminder(sharedPreferences)); + } + + private MakibesHR3DeviceSupport setTimeMode(TransactionBuilder transactionBuilder, byte timeMode) { + byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_TIMEMODE, new byte[]{timeMode}); + + transactionBuilder.write(this.mControlCharacteristic, data); + + return this; + } + + private MakibesHR3DeviceSupport setTimeMode(TransactionBuilder transactionBuilder, SharedPreferences sharedPreferences) { + return this.setTimeMode(transactionBuilder, + MakibesHR3Coordinator.getTimeMode(sharedPreferences)); + } + + private MakibesHR3DeviceSupport setEnableRealTimeHeartRate(TransactionBuilder transaction, boolean enable) { + byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_REAL_TIME_HEART_RATE, new byte[]{(byte) (enable ? 0x01 : 0x00)}); + + transaction.write(this.mControlCharacteristic, data); return this; } @@ -486,7 +1127,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { byte[] data = this.craftData(MakibesHR3Constants.CMD_SET_DATE_TIME, new byte[]{ (byte) 0x00, - (byte) (year & 0xff00), + (byte) ((year & 0xff00) >> 8), (byte) (year & 0x00ff), (byte) month, (byte) day, @@ -495,7 +1136,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { (byte) second }); - transaction.write(this.ctrlCharacteristic, data); + transaction.write(this.mControlCharacteristic, data); return this; } @@ -515,7 +1156,7 @@ public class MakibesHR3DeviceSupport extends AbstractBTLEDeviceSupport { } private MakibesHR3DeviceSupport reboot(TransactionBuilder transaction) { - transaction.write(this.ctrlCharacteristic, this.craftData(MakibesHR3Constants.CMD_REBOOT)); + transaction.write(this.mControlCharacteristic, this.craftData(MakibesHR3Constants.CMD_REBOOT)); return this; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index 10889f683..14863e36d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -83,14 +83,13 @@ public class DeviceHelper { private static final Logger LOG = LoggerFactory.getLogger(DeviceHelper.class); private static final DeviceHelper instance = new DeviceHelper(); + // lazily created + private List coordinators; public static DeviceHelper getInstance() { return instance; } - // lazily created - private List coordinators; - public DeviceType getSupportedType(GBDeviceCandidate candidate) { for (DeviceCoordinator coordinator : getAllCoordinators()) { DeviceType deviceType = coordinator.getSupportedType(candidate); @@ -204,15 +203,15 @@ public class DeviceHelper { private List createCoordinators() { List result = new ArrayList<>(); - result.add(new MiScale2DeviceCoordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new AmazfitBipCoordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new AmazfitBipLiteCoordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new AmazfitCorCoordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new AmazfitCor2Coordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new MiBand3Coordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new MiBand4Coordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new MiBand2HRXCoordinator()); // Note: must come before MiBand2 because detection is hacky, atm - result.add(new MiBand2Coordinator()); // Note: MiBand2 must come before MiBand because detection is hacky, atm + result.add(new MiScale2DeviceCoordinator()); + result.add(new AmazfitBipCoordinator()); + result.add(new AmazfitBipLiteCoordinator()); + result.add(new AmazfitCorCoordinator()); + result.add(new AmazfitCor2Coordinator()); + result.add(new MiBand3Coordinator()); + result.add(new MiBand4Coordinator()); + result.add(new MiBand2HRXCoordinator()); + result.add(new MiBand2Coordinator()); // Note: MiBand2 and all of the above must come before MiBand because detection is hacky, atm result.add(new MiBandCoordinator()); result.add(new PebbleCoordinator()); result.add(new VibratissimoCoordinator()); diff --git a/app/src/main/res/drawable/ic_find_lost_phone.xml b/app/src/main/res/drawable/ic_find_lost_phone.xml new file mode 100644 index 000000000..2740ef687 --- /dev/null +++ b/app/src/main/res/drawable/ic_find_lost_phone.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index ae9d69f43..0f4602f2d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -747,4 +747,18 @@ Tlačítko Připojit nové zařízení Vždy viditelné Viditelné pouze pokud není přidáno žádné zařízení + + %d hodina + %d hodiny + %d hodin + + Pro zobrazení trasy aktivity nainstalujte aplikaci, která umí zobrazit GPX soubory. + Nastavení Makibes HR3 + Makibes HR3 + Amazfit Bip Lite + Vyhledat telefon + Povolit vyhledání telefonu + Použijte náramek/hodinky k prozvonění telefonu. + Délka zvonění ve vteřinách + Délka \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5ab9ec23a..8af947c36 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -770,4 +770,10 @@ Makibes HR3 Einstellungen Makibes HR3 + Amazfit Bip Lite + Telefon finden + Telefon finden aktivieren + Verwende dein Band, um den Klingelton deines Handys wiederzugeben. + Klingeldauer in Sekunden + Dauer \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 748f75f9e..d60ff5c45 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -753,4 +753,10 @@ כדי לצפות בעקבות פעילות, עליך להתקין יישומון שיכול לטפל בקובצי GPX. הגדרות של Makibes HR3 Makibes HR3 + Amazfit Bip Lite + איתור הטלפון + הפעלת איתור הטלפון + ניתן להשתמש בצמיד כדי שהטלפון שלך ישמיע צלצול. + משך זמן הצלצול בשניות + משך \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e6775eefb..379394634 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -17,7 +17,7 @@ ナビゲーションドロワーを閉じる カードを長押しすると切断します 切断中 - 接続中 + 接続中… デバイスのスクリーンショットを取得中 デバッグ @@ -35,7 +35,7 @@ HRM を非アクティベート システムの天気アプリを有効にする システムの天気アプリを無効にする - 天気予報アプリをインストール + 天気予報通知アプリをインストール 設定 先頭に移動 @@ -44,17 +44,17 @@ ブラックリストにしたカレンダー ファームウェア・アプリのインストール - お使いのMi Bandに、現在のファームウェアの代わりに%sをインストールしようとしています。 + %s をインストールしようとしています。 お使いのMi Bandに、現在のファームウェアの代わりに %1$s および %2$s をインストールしようとしています。 このファームウェアはテスト済で、ガジェットブリッジと互換性があることがわかっています。 - このファームウェアは未テストで、Gadgetbridge と互換性がない可能性があります。 -\n -\nお使いのMi Bandにフラッシュすることは推奨されません! - それでも続行して、正しく動作した場合は、Gadgetbridge開発者にホワイトリストの %s ファームウェア バージョンを教えてください + このファームウェアは未テストで、Gadgetbridge と互換性がない可能性があります。 +\n +\nフラッシュすることは推奨されません! + それでも続行して、正しく動作した場合は、Gadgetbridge開発者にホワイトリストの %s ファームウェア バージョンを教えてください。 設定 全般設定 - Bluetoothがオンになったときにデバイスに接続 + Bluetoothがオンになったときに Gadgetbridge デバイスに接続 自動的に開始 自動的に再接続 お好みのオーディオプレイヤー @@ -427,14 +427,14 @@ 電話で開く ミュート 返信 - 接続 + 接続… Amazfit Cor に %s のファームウェアをインストールしようとしています。 \n -\n.fw ファイルをインストールし、その後 .res ファイルをインストールしください。お使いのウォッチは、.fw ファイルをインストールした後に再起動します。 +\n.fw ファイルをインストールし、その後 .res ファイルをインストールしください。お使いの band は、.fw ファイルをインストールした後に再起動します。 \n \n注: 以前にインストールされたものと同じ場合は、.res をインストールする必要はありません。 \n -\nテストされていません。デバイスが文鎮化する可能性があります。ご自身の責任で行って下さい! +\nご自身の責任で行って下さい! バックグラウンド JS を有効にします 有効にすると、ウォッチフェースに天気、バッテリー情報等を表示することができます。 Web View アクティビティ @@ -520,13 +520,13 @@ 工場出荷値にリセットすると、(サポートされている場合) 接続されているデバイスからすべてのデータを削除します。Xiaomi/Huamiデバイスでは、BluetoothのMACアドレスも変更するので、GadgetBligeに新しいデバイスとして表示されます。 すべて通知のブラックリストにする すべて通知のホワイトリストにする - Mi Band 3 に %s ファームウェアをインストールします。 -\n -\n.fw ファイルをインストールして、その後 .res ファイルをインストールするようにしてください。 .fw ファイルをインストールした後、ウォッチは再起動します。 -\n -\n注意: .res が以前にインストールしたものと全く同じ場合は、インストールする必要はありません。 -\n -\nテストされていません。お使いのデバイスが文鎮化する可能性があります。ご自身の責任で行ってください! + Mi Band 3 に %s ファームウェアをインストールします。 +\n +\n.fw ファイルをインストールして、その後 .res ファイルをインストールするようにしてください。 .fw ファイルをインストールした後、お使いの band は再起動します。 +\n +\n注意: .res が以前にインストールしたものと全く同じ場合は、インストールする必要はありません。 +\n +\nご自身の責任で行ってください! 通知の間の最小時間 右から左へ お使いのデバイスが右から左の言語を表示できない場合はこれを有効にします @@ -627,4 +627,19 @@ \nあなた自身のリスクで続行してください! \n  \n完全にはテストされていません。お使いのデバイス名が \"Amazfit Band 2\" の場合、おそらく BEATS_W ファームウェアをフラッシュする必要があります + VoIP アプリの通話を有効にする + Mi Band 4 に %s ファームウェアをインストールしようとしています。 +\n +\n.fw ファイルをインストールしてから、.res ファイルをインストールしてください。 .fw ファイルをインストールすると、お使いの band が再起動します。 +\n +\n注: 以前にインストールしたものとまったく同じ場合、.res をインストールする必要はありません。 +\n +\nご自身のリスクで進めてください! + Gadgetbridge の接続中に、他のアプリが HR データにリアルタイムでアクセスできるようにします + サードパーティのリアルタイムHRアクセス + カスタムフォントを使用する + お使いのデバイスで絵文字サポートのカスタムフォントファームウェアがある場合は、これを有効にしてください + 新しいデバイスを接続するボタン + 常に表示 + デバイスが追加されていない場合にのみ表示 \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 56075dd5d..2fb2246c2 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -347,7 +347,7 @@ Tijd Tijd & datum Knoppen acties - Specificeer actie voor de Mi Band 2 knopdruk + Specificeer actie voor de Mi Band knopdruk Aantal knopdrukken Aantal knopdrukken nodig om een bericht te sturen Uit te sturen bericht @@ -754,4 +754,8 @@ Verbind nieuw apparaat knop Altijkd zichtbaar Alleen zichtbaar als er geen apparaat toegevoegd is + Om de route van de activiteit te zien: installeer een app die GPX bestanden kan tonen. + Makibes HR3 instellingen + Makibes HR3 + Amazfit Bip Lite \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 23e7c456c..3cf8f2bce 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -763,4 +763,10 @@ Para visualizar o rastreamento de atividade, instale um aplicativo que consegue manipular arquivos GPX. Configurações de Makibes HR3 Makibes HR3 + Amazfit Bip Lite + Encontrar telefone + Ativar encontrar telefone + Use sua pulseira para reproduzir o toque sonoro do seu celular. + Duração do toque sonoro em segundos + Duração \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 06c105690..3bf151cf2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -760,4 +760,10 @@ 若需要查看活动轨迹,请安装一个能查看 GPX 文件的应用。 Makibes HR3 设置 Makibes HR3 + 米动手表青春版 Lite + 查找手机 + 启用查找手机 + 使用您的手环以在手机上播放铃声。 + 铃声将持续数秒 + 持续 \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 326b1f064..6a38a6e9b 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -141,6 +141,18 @@ @string/p_pebble_privacy_mode_complete + + @string/off + @string/on + @string/maximum_duration + + + + @string/p_off + @string/p_on + @string/p_scheduled + + @string/dateformat_time @string/dateformat_date_time @@ -162,6 +174,15 @@ MM/dd/yyyy + + @string/mi2_dnd_off + @string/mi2_dnd_scheduled + + + @string/p_off + @string/p_scheduled + + @string/mi2_dnd_off @string/mi2_dnd_automatic diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3adf3dffb..bb7e648cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -300,6 +300,7 @@ When your Mi Band vibrates and blinks, tap it a few times in a row. Install Make your device discoverable. Currently connected devices will likely not be discovered. Activate location (e.g. GPS) on Android 6+. Disable Privacy Guard for Gadgetbridge, because it may crash and reboot your phone. If no device is found after a few minutes, try again after rebooting your mobile device. + This device needs a secret auth key, long press on the device to enter it. Read the wiki. Note: Device image Name/Alias @@ -445,6 +446,10 @@ Alarms to reserve for upcoming events Use heart rate sensor to improve sleep detection Device time offset in hours (for detecting sleep of shift workers) + Find phone + Enable find phone + Use your band to play your phone\'s ringtone. + Ring duration in seconds Date format Time @@ -600,6 +605,7 @@ At sunset Automatic (sleep detection) Scheduled (time interval) + Duration Attempting to pair with %1$s Bonding with %1$s failed immediately. Trying to connect to: %1$s diff --git a/app/src/main/res/xml/changelog_master.xml b/app/src/main/res/xml/changelog_master.xml index e5d36351c..de4646768 100644 --- a/app/src/main/res/xml/changelog_master.xml +++ b/app/src/main/res/xml/changelog_master.xml @@ -1,5 +1,19 @@ + + Initial Makibes HR3 support + Amazfit Bip Lite: Inittal working support, firmware update is disabled for now (we do not have any firmware for testing) + Amazfit Cor 2: Enable Emoji Font setting and 3rd party HR access + Find Phone now also vibration in addition to playing the ring tone + ID115: All settings are now per-device + Time format settings are now per-device for all supported devices + Wrist location settings are now per-device for all supported devices + Work around broken layout in database management activity + Show toast in case no app is installed which can handle GPX files + Mi Band 4/Amazfit Bip Lite: Trim white spaces and new lines from auth key + Mi Band 4/Amazfit Bip Lite: Display a toast and do not try to pair if there was no auth key supplied + Skip service scan if supported device could be recognized without uuids during discovery + Amazfit Bip: Untested support for Lite variant Force Lineage OS to ask for permission when Trust is used to fix non-working incoming calls diff --git a/app/src/main/res/xml/devicesettings_donotdisturb_no_auto.xml b/app/src/main/res/xml/devicesettings_donotdisturb_no_auto.xml new file mode 100644 index 000000000..f30f0ec42 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_donotdisturb_no_auto.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_find_phone.xml b/app/src/main/res/xml/devicesettings_find_phone.xml new file mode 100644 index 000000000..5baec8c70 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_find_phone.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/158.txt b/fastlane/metadata/android/en-US/changelogs/158.txt new file mode 100644 index 000000000..ea026740a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/158.txt @@ -0,0 +1,12 @@ +* Initial Makibes HR3 support +* Amazfit Bip Lite: Inittal working support, firmware update is disabled for now (we do not have any firmware for testing) +* Amazfit Cor 2: Enable Emoji Font setting and 3rd party HR access +* Find Phone now also vibration in addition to playing the ring tone +* ID115: All settings are now per-device +* Time format settings are now per-device for all supported devices +* Wrist location settings are now per-device for all supported devices +* Work around broken layout in database management activity +* Show toast in case no app is installed which can handle GPX files +* Mi Band 4/Amazfit Bip Lite: Trim white spaces and new lines from auth key +* Mi Band 4/Amazfit Bip Lite: Display a toast and do not try to pair if there was no auth key supplied +* Skip service scan if supported device could be recognized without uuids during discovery