From 11665a1d89c46f4cdc478a55150d48091a4f5569 Mon Sep 17 00:00:00 2001 From: Dominik Dzienia Date: Thu, 7 Nov 2019 22:39:29 +0100 Subject: [PATCH 1/7] [#728] Complication support - Watch OS selection of Short, Long and Image complications with current AAPS status (SGV, trend, delta, BR, IoB, CoB, phone/uploader battery) --- .../battery-charging-wireless-10-burnin.svg | 4 + .../battery-charging-wireless-20-burnin.svg | 4 + .../battery-charging-wireless-30-burnin.svg | 4 + .../battery-charging-wireless-40-burnin.svg | 4 + .../battery-charging-wireless-50-burnin.svg | 4 + .../battery-charging-wireless-60-burnin.svg | 4 + .../battery-charging-wireless-70-burnin.svg | 4 + .../battery-charging-wireless-80-burnin.svg | 4 + .../battery-charging-wireless-90-burnin.svg | 4 + .../battery-charging-wireless-burnin.svg | 4 + .../battery-burnin/battery-unknown-burnin.svg | 4 + .../mask-burnin-battery-raw.svg | 55 +++ icons/battery-source/mask-burnin-battery.svg | 223 ++++++++++ .../battery/battery-charging-wireless-10.svg | 1 + .../battery/battery-charging-wireless-20.svg | 1 + .../battery/battery-charging-wireless-30.svg | 1 + .../battery/battery-charging-wireless-40.svg | 1 + .../battery/battery-charging-wireless-50.svg | 1 + .../battery/battery-charging-wireless-60.svg | 1 + .../battery/battery-charging-wireless-70.svg | 1 + .../battery/battery-charging-wireless-80.svg | 1 + .../battery/battery-charging-wireless-90.svg | 1 + icons/battery/battery-charging-wireless.svg | 1 + icons/battery/battery-outline.svg | 1 + icons/battery/battery-unknown.svg | 1 + .../ic_br_cob_iob_orig.svg | 93 ++++ .../ic_cob_detailed_orig.svg | 83 ++++ .../complications-source/ic_cob_iob_orig.svg | 83 ++++ .../ic_ins_burnin_orig.svg | 57 +++ icons/complications-source/ic_ins_orig.svg | 56 +++ .../ic_iob_detailed_orig.svg | 88 ++++ icons/complications/ic_aaps_full.svg | 36 ++ icons/complications/ic_basal.svg | 35 ++ icons/complications/ic_br_cob_iob.svg | 40 ++ icons/complications/ic_carbs.svg | 35 ++ icons/complications/ic_cob_detailed.svg | 36 ++ icons/complications/ic_cob_iob.svg | 36 ++ icons/complications/ic_ins.svg | 31 ++ icons/complications/ic_ins_burnin.svg | 31 ++ icons/complications/ic_iob_detailed.svg | 36 ++ icons/complications/ic_sgv.svg | 35 ++ wear/build.gradle | 4 + wear/src/main/AndroidManifest.xml | 224 ++++++++++ wear/src/main/assets/watch_dark.jpg | Bin 0 -> 21801 bytes wear/src/main/assets/watch_gray.jpg | Bin 0 -> 19759 bytes wear/src/main/assets/watch_light.jpg | Bin 0 -> 19708 bytes .../java/info/nightscout/androidaps/aaps.java | 75 ++++ .../BaseComplicationProviderService.java | 415 ++++++++++++++++++ .../complications/BrCobIobComplication.java | 49 +++ .../CobDetailedComplication.java | 52 +++ .../complications/CobIconComplication.java | 51 +++ .../complications/CobIobComplication.java | 46 ++ .../complications/ComplicationAction.java | 12 + .../ComplicationTapBroadcastReceiver.java | 167 +++++++ .../IobDetailedComplication.java | 52 +++ .../complications/IobIconComplication.java | 56 +++ .../complications/LongStatusComplication.java | 52 +++ .../LongStatusFlippedComplication.java | 53 +++ .../complications/SgvComplication.java | 48 ++ .../complications/UploaderBattery.java | 114 +++++ .../complications/WallpaperComplication.java | 62 +++ .../WallpaperDarkComplication.java | 22 + .../WallpaperGrayComplication.java | 22 + .../WallpaperLightComplication.java | 22 + .../androidaps/data/DisplayRawData.java | 269 ++++++++++++ .../androidaps/data/ListenerService.java | 4 + .../interaction/utils/Constants.java | 14 + .../interaction/utils/DisplayFormat.java | 129 ++++++ .../interaction/utils/Inevitable.java | 117 +++++ .../interaction/utils/Persistence.java | 94 ++++ .../utils/SmallestDoubleString.java | 130 ++++++ .../interaction/utils/WearUtil.java | 110 ++++- .../androidaps/watchfaces/BaseWatchFace.java | 226 ++-------- .../androidaps/watchfaces/BgGraphBuilder.java | 37 ++ .../androidaps/watchfaces/Cockpit.java | 8 +- .../androidaps/watchfaces/Home.java | 16 +- .../androidaps/watchfaces/Home2.java | 18 +- .../androidaps/watchfaces/LargeHome.java | 22 +- .../androidaps/watchfaces/Steampunk.java | 34 +- wear/src/main/res/drawable/ic_aaps_dark.xml | 12 + wear/src/main/res/drawable/ic_aaps_full.xml | 12 + wear/src/main/res/drawable/ic_aaps_gray.xml | 12 + wear/src/main/res/drawable/ic_aaps_light.xml | 12 + wear/src/main/res/drawable/ic_alert.xml | 9 + .../src/main/res/drawable/ic_alert_burnin.xml | 9 + .../ic_battery_alert_variant_outline.xml | 9 + .../drawable/ic_battery_charging_wireless.xml | 9 + .../ic_battery_charging_wireless_10.xml | 9 + ...ic_battery_charging_wireless_10_burnin.xml | 10 + .../ic_battery_charging_wireless_20.xml | 9 + ...ic_battery_charging_wireless_20_burnin.xml | 10 + .../ic_battery_charging_wireless_30.xml | 9 + ...ic_battery_charging_wireless_30_burnin.xml | 10 + .../ic_battery_charging_wireless_40.xml | 9 + ...ic_battery_charging_wireless_40_burnin.xml | 10 + .../ic_battery_charging_wireless_50.xml | 9 + ...ic_battery_charging_wireless_50_burnin.xml | 10 + .../ic_battery_charging_wireless_60.xml | 9 + ...ic_battery_charging_wireless_60_burnin.xml | 10 + .../ic_battery_charging_wireless_70.xml | 9 + ...ic_battery_charging_wireless_70_burnin.xml | 10 + .../ic_battery_charging_wireless_80.xml | 9 + ...ic_battery_charging_wireless_80_burnin.xml | 10 + .../ic_battery_charging_wireless_90.xml | 9 + ...ic_battery_charging_wireless_90_burnin.xml | 10 + .../ic_battery_charging_wireless_burnin.xml | 10 + .../ic_battery_charging_wireless_outline.xml | 9 + .../main/res/drawable/ic_battery_outline.xml | 9 + .../main/res/drawable/ic_battery_unknown.xml | 9 + .../drawable/ic_battery_unknown_burnin.xml | 10 + wear/src/main/res/drawable/ic_br_cob_iob.xml | 11 + wear/src/main/res/drawable/ic_carbs.xml | 11 + .../src/main/res/drawable/ic_cob_detailed.xml | 11 + wear/src/main/res/drawable/ic_cob_iob.xml | 11 + wear/src/main/res/drawable/ic_ins.xml | 11 + wear/src/main/res/drawable/ic_ins_burnin.xml | 11 + .../src/main/res/drawable/ic_iob_detailed.xml | 11 + wear/src/main/res/drawable/ic_sgv.xml | 13 + wear/src/main/res/drawable/ic_sync_alert.xml | 9 + wear/src/main/res/values/strings.xml | 28 ++ wear/src/main/res/xml/preferences.xml | 14 + 121 files changed, 4181 insertions(+), 233 deletions(-) create mode 100644 icons/battery-burnin/battery-charging-wireless-10-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-20-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-30-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-40-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-50-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-60-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-70-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-80-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-90-burnin.svg create mode 100644 icons/battery-burnin/battery-charging-wireless-burnin.svg create mode 100644 icons/battery-burnin/battery-unknown-burnin.svg create mode 100644 icons/battery-source/mask-burnin-battery-raw.svg create mode 100644 icons/battery-source/mask-burnin-battery.svg create mode 100644 icons/battery/battery-charging-wireless-10.svg create mode 100644 icons/battery/battery-charging-wireless-20.svg create mode 100644 icons/battery/battery-charging-wireless-30.svg create mode 100644 icons/battery/battery-charging-wireless-40.svg create mode 100644 icons/battery/battery-charging-wireless-50.svg create mode 100644 icons/battery/battery-charging-wireless-60.svg create mode 100644 icons/battery/battery-charging-wireless-70.svg create mode 100644 icons/battery/battery-charging-wireless-80.svg create mode 100644 icons/battery/battery-charging-wireless-90.svg create mode 100644 icons/battery/battery-charging-wireless.svg create mode 100644 icons/battery/battery-outline.svg create mode 100644 icons/battery/battery-unknown.svg create mode 100644 icons/complications-source/ic_br_cob_iob_orig.svg create mode 100644 icons/complications-source/ic_cob_detailed_orig.svg create mode 100644 icons/complications-source/ic_cob_iob_orig.svg create mode 100644 icons/complications-source/ic_ins_burnin_orig.svg create mode 100644 icons/complications-source/ic_ins_orig.svg create mode 100644 icons/complications-source/ic_iob_detailed_orig.svg create mode 100644 icons/complications/ic_aaps_full.svg create mode 100644 icons/complications/ic_basal.svg create mode 100644 icons/complications/ic_br_cob_iob.svg create mode 100644 icons/complications/ic_carbs.svg create mode 100644 icons/complications/ic_cob_detailed.svg create mode 100644 icons/complications/ic_cob_iob.svg create mode 100644 icons/complications/ic_ins.svg create mode 100644 icons/complications/ic_ins_burnin.svg create mode 100644 icons/complications/ic_iob_detailed.svg create mode 100644 icons/complications/ic_sgv.svg create mode 100644 wear/src/main/assets/watch_dark.jpg create mode 100644 wear/src/main/assets/watch_gray.jpg create mode 100644 wear/src/main/assets/watch_light.jpg create mode 100644 wear/src/main/java/info/nightscout/androidaps/aaps.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/ComplicationAction.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/ComplicationTapBroadcastReceiver.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/WallpaperDarkComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/WallpaperGrayComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/complications/WallpaperLightComplication.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/Constants.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java create mode 100644 wear/src/main/res/drawable/ic_aaps_dark.xml create mode 100644 wear/src/main/res/drawable/ic_aaps_full.xml create mode 100644 wear/src/main/res/drawable/ic_aaps_gray.xml create mode 100644 wear/src/main/res/drawable/ic_aaps_light.xml create mode 100644 wear/src/main/res/drawable/ic_alert.xml create mode 100644 wear/src/main/res/drawable/ic_alert_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_alert_variant_outline.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_10.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_10_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_20.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_20_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_30.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_30_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_40.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_40_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_50.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_50_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_60.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_60_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_70.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_70_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_80.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_80_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_90.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_90_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_battery_charging_wireless_outline.xml create mode 100644 wear/src/main/res/drawable/ic_battery_outline.xml create mode 100644 wear/src/main/res/drawable/ic_battery_unknown.xml create mode 100644 wear/src/main/res/drawable/ic_battery_unknown_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_br_cob_iob.xml create mode 100644 wear/src/main/res/drawable/ic_carbs.xml create mode 100644 wear/src/main/res/drawable/ic_cob_detailed.xml create mode 100644 wear/src/main/res/drawable/ic_cob_iob.xml create mode 100644 wear/src/main/res/drawable/ic_ins.xml create mode 100644 wear/src/main/res/drawable/ic_ins_burnin.xml create mode 100644 wear/src/main/res/drawable/ic_iob_detailed.xml create mode 100644 wear/src/main/res/drawable/ic_sgv.xml create mode 100644 wear/src/main/res/drawable/ic_sync_alert.xml diff --git a/icons/battery-burnin/battery-charging-wireless-10-burnin.svg b/icons/battery-burnin/battery-charging-wireless-10-burnin.svg new file mode 100644 index 0000000000..2888abf209 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-10-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-20-burnin.svg b/icons/battery-burnin/battery-charging-wireless-20-burnin.svg new file mode 100644 index 0000000000..77d52751d7 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-20-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-30-burnin.svg b/icons/battery-burnin/battery-charging-wireless-30-burnin.svg new file mode 100644 index 0000000000..9ffa356436 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-30-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-40-burnin.svg b/icons/battery-burnin/battery-charging-wireless-40-burnin.svg new file mode 100644 index 0000000000..d9ccc46027 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-40-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-50-burnin.svg b/icons/battery-burnin/battery-charging-wireless-50-burnin.svg new file mode 100644 index 0000000000..4d4905046c --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-50-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-60-burnin.svg b/icons/battery-burnin/battery-charging-wireless-60-burnin.svg new file mode 100644 index 0000000000..e3c4ba5c44 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-60-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-70-burnin.svg b/icons/battery-burnin/battery-charging-wireless-70-burnin.svg new file mode 100644 index 0000000000..fe26b5e93d --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-70-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-80-burnin.svg b/icons/battery-burnin/battery-charging-wireless-80-burnin.svg new file mode 100644 index 0000000000..c949afb1c8 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-80-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-90-burnin.svg b/icons/battery-burnin/battery-charging-wireless-90-burnin.svg new file mode 100644 index 0000000000..13677dc328 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-90-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-charging-wireless-burnin.svg b/icons/battery-burnin/battery-charging-wireless-burnin.svg new file mode 100644 index 0000000000..f074b89d34 --- /dev/null +++ b/icons/battery-burnin/battery-charging-wireless-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-burnin/battery-unknown-burnin.svg b/icons/battery-burnin/battery-unknown-burnin.svg new file mode 100644 index 0000000000..b0cc3bba40 --- /dev/null +++ b/icons/battery-burnin/battery-unknown-burnin.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/battery-source/mask-burnin-battery-raw.svg b/icons/battery-source/mask-burnin-battery-raw.svg new file mode 100644 index 0000000000..e2ab79f2a1 --- /dev/null +++ b/icons/battery-source/mask-burnin-battery-raw.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/battery-source/mask-burnin-battery.svg b/icons/battery-source/mask-burnin-battery.svg new file mode 100644 index 0000000000..09be5da8a9 --- /dev/null +++ b/icons/battery-source/mask-burnin-battery.svg @@ -0,0 +1,223 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icons/battery/battery-charging-wireless-10.svg b/icons/battery/battery-charging-wireless-10.svg new file mode 100644 index 0000000000..d6dbd7febc --- /dev/null +++ b/icons/battery/battery-charging-wireless-10.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-20.svg b/icons/battery/battery-charging-wireless-20.svg new file mode 100644 index 0000000000..9e4badedc9 --- /dev/null +++ b/icons/battery/battery-charging-wireless-20.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-30.svg b/icons/battery/battery-charging-wireless-30.svg new file mode 100644 index 0000000000..7da87ce966 --- /dev/null +++ b/icons/battery/battery-charging-wireless-30.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-40.svg b/icons/battery/battery-charging-wireless-40.svg new file mode 100644 index 0000000000..b9aaad2b0f --- /dev/null +++ b/icons/battery/battery-charging-wireless-40.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-50.svg b/icons/battery/battery-charging-wireless-50.svg new file mode 100644 index 0000000000..705a61c55b --- /dev/null +++ b/icons/battery/battery-charging-wireless-50.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-60.svg b/icons/battery/battery-charging-wireless-60.svg new file mode 100644 index 0000000000..b2cd9f7734 --- /dev/null +++ b/icons/battery/battery-charging-wireless-60.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-70.svg b/icons/battery/battery-charging-wireless-70.svg new file mode 100644 index 0000000000..608a404882 --- /dev/null +++ b/icons/battery/battery-charging-wireless-70.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-80.svg b/icons/battery/battery-charging-wireless-80.svg new file mode 100644 index 0000000000..c604743cc3 --- /dev/null +++ b/icons/battery/battery-charging-wireless-80.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless-90.svg b/icons/battery/battery-charging-wireless-90.svg new file mode 100644 index 0000000000..246886ad08 --- /dev/null +++ b/icons/battery/battery-charging-wireless-90.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-charging-wireless.svg b/icons/battery/battery-charging-wireless.svg new file mode 100644 index 0000000000..b36143c4c2 --- /dev/null +++ b/icons/battery/battery-charging-wireless.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-outline.svg b/icons/battery/battery-outline.svg new file mode 100644 index 0000000000..e05e71b288 --- /dev/null +++ b/icons/battery/battery-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/battery/battery-unknown.svg b/icons/battery/battery-unknown.svg new file mode 100644 index 0000000000..8e117be5cb --- /dev/null +++ b/icons/battery/battery-unknown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/complications-source/ic_br_cob_iob_orig.svg b/icons/complications-source/ic_br_cob_iob_orig.svg new file mode 100644 index 0000000000..f650e12d99 --- /dev/null +++ b/icons/complications-source/ic_br_cob_iob_orig.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/icons/complications-source/ic_cob_detailed_orig.svg b/icons/complications-source/ic_cob_detailed_orig.svg new file mode 100644 index 0000000000..e599774c5d --- /dev/null +++ b/icons/complications-source/ic_cob_detailed_orig.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/complications-source/ic_cob_iob_orig.svg b/icons/complications-source/ic_cob_iob_orig.svg new file mode 100644 index 0000000000..98c1764554 --- /dev/null +++ b/icons/complications-source/ic_cob_iob_orig.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/complications-source/ic_ins_burnin_orig.svg b/icons/complications-source/ic_ins_burnin_orig.svg new file mode 100644 index 0000000000..c5bcdfe0ff --- /dev/null +++ b/icons/complications-source/ic_ins_burnin_orig.svg @@ -0,0 +1,57 @@ + + + + + + + + image/svg+xml + + + + + + + diff --git a/icons/complications-source/ic_ins_orig.svg b/icons/complications-source/ic_ins_orig.svg new file mode 100644 index 0000000000..7601867f58 --- /dev/null +++ b/icons/complications-source/ic_ins_orig.svg @@ -0,0 +1,56 @@ + + + + + + + + image/svg+xml + + + + + + + diff --git a/icons/complications-source/ic_iob_detailed_orig.svg b/icons/complications-source/ic_iob_detailed_orig.svg new file mode 100644 index 0000000000..3479a78565 --- /dev/null +++ b/icons/complications-source/ic_iob_detailed_orig.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/complications/ic_aaps_full.svg b/icons/complications/ic_aaps_full.svg new file mode 100644 index 0000000000..4e005a8228 --- /dev/null +++ b/icons/complications/ic_aaps_full.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/complications/ic_basal.svg b/icons/complications/ic_basal.svg new file mode 100644 index 0000000000..0491265177 --- /dev/null +++ b/icons/complications/ic_basal.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/complications/ic_br_cob_iob.svg b/icons/complications/ic_br_cob_iob.svg new file mode 100644 index 0000000000..b340c42187 --- /dev/null +++ b/icons/complications/ic_br_cob_iob.svg @@ -0,0 +1,40 @@ + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/complications/ic_carbs.svg b/icons/complications/ic_carbs.svg new file mode 100644 index 0000000000..0785741d47 --- /dev/null +++ b/icons/complications/ic_carbs.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/complications/ic_cob_detailed.svg b/icons/complications/ic_cob_detailed.svg new file mode 100644 index 0000000000..5132260c3c --- /dev/null +++ b/icons/complications/ic_cob_detailed.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/complications/ic_cob_iob.svg b/icons/complications/ic_cob_iob.svg new file mode 100644 index 0000000000..81a2e9250f --- /dev/null +++ b/icons/complications/ic_cob_iob.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/complications/ic_ins.svg b/icons/complications/ic_ins.svg new file mode 100644 index 0000000000..e5d1b3e147 --- /dev/null +++ b/icons/complications/ic_ins.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons/complications/ic_ins_burnin.svg b/icons/complications/ic_ins_burnin.svg new file mode 100644 index 0000000000..623db41e5b --- /dev/null +++ b/icons/complications/ic_ins_burnin.svg @@ -0,0 +1,31 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/icons/complications/ic_iob_detailed.svg b/icons/complications/ic_iob_detailed.svg new file mode 100644 index 0000000000..8bd9aabc0e --- /dev/null +++ b/icons/complications/ic_iob_detailed.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/icons/complications/ic_sgv.svg b/icons/complications/ic_sgv.svg new file mode 100644 index 0000000000..1dc46d403b --- /dev/null +++ b/icons/complications/ic_sgv.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/wear/build.gradle b/wear/build.gradle index 7e6500fe56..8d4dd71af3 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -92,6 +92,10 @@ dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') //implementation files("libs/hellocharts-library-1.5.5.jar") //compile "com.ustwo.android:clockwise-wearable:1.0.2" + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.legacy:legacy-support-v13:1.0.0' + compileOnly "com.google.android.wearable:wearable:${wearableVersion}" implementation "com.google.android.support:wearable:${wearableVersion}" implementation "com.google.android.gms:play-services-wearable:${playServicesWearable}" diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 874a9c8763..2f89781293 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wear/src/main/assets/watch_dark.jpg b/wear/src/main/assets/watch_dark.jpg new file mode 100644 index 0000000000000000000000000000000000000000..54cd4dc1dc79f34c55857323696c048e33613414 GIT binary patch literal 21801 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

v;t;JEa-hkx&vXO2_SjwYqYCpDLvKj zQ(j>(Fc<<=U4E+E(apyQo$JWGTY!Nc01Vc3H){;Tp^3bue$8%vJuZGsJsLb!ZwTGx z=WVpXFaUtd4jEN5>M5%2uWkuwR8aJFweU)5mrS#e z+Wo%%b2q;QW|E-(?_J@c*E7#j$?I3&IHZ`WCy64MLA4$b9mv=bUMHixo)^430F{&t z|137hJ97A4VqC>rW>+jy3F2sS4FA6mlzn)0g?f$nNKXXS^t&D4LYc^x58<|25{ze!`~nE1T%P zqa0@d|8e@@OibY#T%RFxVE=O71cIWB@VaBoYG z&5?csSh+yPQ7d^4o$I}Le8=Ha#A9B1ZSB?$&8*WsAQ@B4BYewULR0qr-Niq*LoV9x?f)d9mg|_c{youk$l$L6eWQKY^u$9U(1R?N+Vc@3i za_QYX-@aRFt~1E^yD@Nh6|OLSmyH!2^;TmAp0o6?|3v*6x?{5iru26!wa~i1#CAfs7d8K_YYt@vQ#BF=i z_-OmuM_*{Dkghe^!F3n;xTs=XXtOV&GHn5Q%E9o_f{=uo>sb?Z=1`pszpDIV)sksE zYp=Nu9_>f+_9%FoYFHWkz^YVNZE6l%`>JI~t^pd=AVV|#=C2?^O*7x&pB3{Y3K1h* z?8}!c3nag=p@BRFDPwdBQksF|XyJ9|z8p!z_|?6X#FSo|-YR3ZFV(Ar@h9L>em``i zPWDftTh!mO=<_=9@!PNL74G!$Uy%x_>=ImsF-9RxwVf*J-F(JZTPGP3JXJLG+L?*z zp7h>4hRy=%pvdC%2pN&Y+oqTqG$Qk_6AH|zt`wF3^=VfGk=*e$>?7;eG2}srZQ`93 zKQwg2&DGKlz1GKnLt<(S&)$^IGl!FL0Jan;_^evEaXbs_S7@S5ExOz)lhk}swF;BF zu{ySLM{-Cq=IpPd%uufht&LgbvXg6YI$9;LIQ!Flf#ky5o(^x5BGSZ%l7x??6cp0jKXmv-F!Nk{&u9(0`~R=QtIL7(Htk0)G2P&!e7V- z`$84lkPIOJ7hc%#`OXaV{L6Nh5lVY|;dW$+g;ymUfwtaYcPbF>4Ae>8slvHn4li6- zS&}yuLp7-V2|BOc{=B<$r!Z!yVJOJalsSv#Obx@wa${bm51sv1;M<2X3wh!K(fc)` z*sw?jp%IG8#!f;mAP9!qs(W3SJ%!qO%12Ca#RMDiIE0Zcqj0)NVl1an0}ZH?*|dC` zAqJoZ_>WtQ3gyrEnrUHs`(x%ppC0{*zQj6N#K-+Y?Y6a21K`ze9r|suq#wF$W#Ht= z1PY#GueRTWW+oW1C!Y{QHI$51xvnj`uzH4#vsaW3PQ5#yB%@LlR;=9QiUWH9$^3Hv zbm6m2>t2quduCM+^Lbl$`03RVn^N;efdDF#-OqClYzC6%0*)% zWV$vgs)$b=jb%yGsJe_#qbgyMGvApUvg&xgqu!57Sj)7=bLF&4-|<*DI%^}qc^UQ1$nUm%oDtNKD+cimeC`69cd;7dVMz=n`q^i6zQQ+ z^$a)k@s;)0kZplMF8&Ae!QbN4sw|uMS?DhBdgGC*%W7dM*wDhxA}F}R!`TsBvvKFP z=C&#L)=7ODpR|6bnND*)q#=4zq6GZcq@eGP^q7+z3bM>p*_dTqRBG>WZaRhDZ%`Yp z=jc1lAE~(|zq77(j%faNo2#4UnA&Ehdie0yNZ)4@l$w9h%zVu>)ZLA(By$Cls}(JN zl$gGqCw5aqEZ3Xj8G8>27b{2}e!fBrZ2h{SlM|WzD|HzzBuE}MQF$JKR0w&9`aZPm ziGcrdDP`NGeq0#D!XwH6o|m7PUF*%!>HV-;_s>1!1lGn~UQT75%1DG6*f(L27w$G>K5dTX@JI|JKBS`C& zs4}X7-D@i|16PVnrn#{Z+TkglHfb@6iJg*J4S^rOBUlEo7=-PUggjiB%t81?=qfJNUVm8Y4kC{?EGw3nx~B2z_m2iZ>~Zr|4da{ z<5GvxFzrMmU8U;!!_U(UrNG7y!{?oVP$I=5(Y zq(0fDIWJovMQ?JHb?FXRPnAktNxyx8ZqPM~5(GdNhua&EFaQZiMOxglyYWTLm zC5}l?0EUi&0u3F493pz z(6-NFH-&@wSqdL99v*le^@s%6eE|?8jva1Xz%?ad774&HJl1dts3HIY*rRybw(o(w zVfhqXKBM2Dk;S|tdR%r_14k{=uj>uW=YvU94oJZ5L*&db)8`*i#Y=^nd=qGgLt^p4 z7_UAqfaPgCc2|a(P*`L_1AN%X!1?(D^x>hsJ@U9Rf|B7J(adVPSaKd6o%bCYDM9Ux z_B8~bse+9$uI!9>QVGH08{h|4%fT;g>uKo>pTfVJBV-$sBucbiS0*q{RS@U}>DS$O zzV;;g7B=9XN_N->tK`aQwnnVM#%7^^eM{yDL8s#>;xq|!8gT*HX(e7nqGy*F9*$*} z=Zf!j%xb`e-KJY(_T-IBx9etJ8y)0Nv`LI{ILg(Ps!-|}yuZI?*dXZFG2Sw*A-|XS zFC3w$5oiRDEy^Ck3bl&u{c|va5m9zJm>%{f4rxtVL|{%m=4jLS8@pi!dFl{!cOyf3W>b&3 z=e0#`68_ZP@+z9JnOWy4qS?wr*)5=oD<~p-gS;4mJSK!{WrNCw7ls_&_PvSfVyAF{ z^25C^c~?WMO^dw0ufXa%ugQ&tyNqx>Y}2~?`8$;N7G+#w0vN;*Wz#l?wtO#ZSOZY2z8YHC(CbN zgR>SMX}i{{8rJI&TTP@0a&zPXu~Geieb;I%bZ^6(=(74Hx|Pn7v`qK`mCX~4j!cB;zk4l^#T_=|aM)bZmb4$_^1rZI)me!A57)hp4LcYfMTFjrE2>EL0(2nXVhR zhfoWv`*m3qH6jH7s=V29h2VKv13iU>#%N|`&SAY#|9a@T^5J{sNUM!TcD5%=FlY=5 zy4h|Y@6^J#xr&aY8_45|t!*!TAd>sNY#h(1Z;#(o+!&o&ZE1$O(uP=+b?EvBFb6nwH5iY_HwaJ`gyWAz2UB zXvW>r!S}yqH-T=V+46MS182x%ETDB$Ay_Ttn~OVW!8*C(Io zo6nWm^`teq>cLH_3V^)dxiI_l1y0lV&Ojb_3QRlBBOjhW7>?!aLcsu5r2eo1JIR!6 z0qMd%^g^K4n+B-2wclR3*fXWyAF$f3PRo!OmuPFy?g$4GI!t3e!o?a+2hdYA;D=4r z2zN)z59m6kxrdyqyyn8}rq7MpCU{Jo@g|1jVmE+1(sRi)jH(4C+$V|L9I$f_We}5@*M(hD(SCO=T&hK=A{~2w=^M6G!Ud zFj!;1E}@xWRSo@KXF)a4g>|&g{&Oos3VdTJB_P;*}EO~!9m*5e#u?J9_+NYDT48OfmBZ-_#{ zQsN+A-!GZ7kTfO^IKsR7F|fol`hnP}$^3?ULlopkd0s-(Ng{>TkDYxW8+N*-JRX>i zGV}TJ7YM`53oab7We_wL?vQ}mUW>mpN7MpXrD^bld}U=E!`|RV;6o$y#ti-qBb@T{ z*&5CM+WG<32?j-r(R?r;J_~1EeO|ctyOg4NEX)>I=kRjJ(UTpAO8E zexF09Po}XkZDMJdBMj5UrYM?cR$$SzyWLzwnC92g>z3W=KEk8go5&h1eci|FgY{J( z_IbV<37f%r2W1+t2D!j{164N}Vga+9{d*9`4&-$M2GIjA?iNixO5Q7I?c3%uW&_iL z5~_DKBG=>pqU>`=JjV29h^4yNG;~vkI$1m)@n08hv#`R9+kL-IRp^F)x&aAF}|4j zlK;XB?ih%!{!6S8eF6yUM(poU6Gwl2%5St=>K&q2?%QtcLJJ?K3trC`W9&WYYm(h} zr!*J!!Ws`E`8?{E9O1aJu>0lfS7V@@Pe~%UA>kVH2-q=6irwDZ{F00^s5;}#WL(4n z87!Y6P<>*Ia=~{dLsD__RwNE$-n*Dm7?j8(=UdM5s%@;TlC2h-Z6Ue z$D*Vi;L;gvF2H0F@fX9l)$^bCMG|hj{gb}mUXsg`B*xvRG*F*9$xr8;!|Mc}!Nv%d z)vUBljP9F1BUle*F}yZee-e>*)ih-VrJ6ooKIiIH2piNdURs*xWvtQY>f{bOYn&pri0?2 z1X-ltMCPAYPta=er-Q9YQekz33_f?LkZ}c>c8)L@kOkAE{5DM? zyBY{OFKM`p`D!^q@~V4L8B#C@`6EtzpJ-tJgm3MaBYnIm7QH2zDtY|w88Y8;X;|m6 z*l^_v>?M8fnsF4);;qgKjgkHE?l6u5+%K;GLV}J$1@c&@d1r-bZ_XnP)Z}Jc;VQ)- ziX)}>chV{>$i@Bo-WY>9`h^|qnpsMbbB)M;dt3x+QnE3i+k?gR?toM?Rgi^k`}*0;4ap8WheyieYIVB;75#; zq!lZ%)L+c<+l3(M!LI8ny9;(0X!>Q(eb2Olu}T!)ZUkTbN|PZO<&p6xr$ zo+I3XHFj0#Hpl`NR_T^t{L*FZD90P$iICBSx>Q0!lIvl`uU%p%Ccf-oGb?m^WPka4NIiy0#7}{xPOSW7;WF^T0DP@bj)hpfq1%>)ZIZAk1NAws>qs>;U5=dI z{8YE&1l;wDW)haD%{X0b-8s#Y_FdmS$n|F32Hp>Wg*S+AYxK@w^%!sBQPUPjn$M-f z+T{!jD%jV*p2#zioPe!jgi6tD7Hk;U@Bjw@8072tli+k~-M>!Ey6kyMH{agm0WkvfdSjwpl^lFNrG9dH=s6uyBR#Sb4NNxd+D6df!% zTs*t&hqb^bQ>A9to(t%0Dzu7BIg3>zX^1CQzvGG#1wwSPdb>N zWyyb|xWQOyZJWnp2N$$~U^~Q!SGY!z z8jM?F=dmsp+#nph%`&NiJQV&haL5J85IMsl)$lnid>Ao)edq!z%iT%)i!|~^%%A>==9e?Dn&<1-yP*SDf#p;HXwEI%G!mF zy3>^v0%;2PHQd9)n6eG9mnLRtII%%VMdX+rwnvQTxTR(^&xurrqXa|k4AEA7C!R{P zH#iOM*(5>V!qZ6@e%L;syMy8;nJ$fJWe-QR)5RGI)b`P&%J=tY;)L(!{u=`wOQFA_ zsNc>%%RE)4;7xQaLoo6K^x8T&;_X7NU`n)GYBKxvGVEfiO4+Ss;=)oYK3qDPA-X_( z5n_GeD0y+OTZt<(6|-L)ZYbQXDupAV5|Z#7a@fOdGV4URm=Kzp*-O)Y8-&~o|4sd& zlaxwtnz8+4AMT>)+}#!Ud|GKWoR^Sr$ss@A5tojy!hM&Mf&$TU!Ef0a;z(*rB+XnM zy;swe&*9gWvCj}UF~#_OdGG^&8f6=#*_~765DrJb1+#6xdj`woqz#hQwZq{!ae}l_ z4eph11VhbI73+(-8gz^J4tA6MZ7lWX z2ptTSCy!cTCCt(YE5$|A>NE}){m$PNRHZWeS8hq-V_V=LOIjjB@cPj%y7I>dP=jG% z<_tmb?(Bd?v`ZY~+53!FZ{$-fL=$$VXtd61jJY9lE;9t%taNi4`M^P%2Ck%82X7^y zwS*hA%yOq?h?zPw1tkR-T2EUV0BVz`QYT8MJ99J&$8k1spKv#SFcpgU)!U|6;)M}O z__RreGUiVJ3KzSBL}77Q^%2%IMELqugC(}J?gwNR8RfzZSA5xsrZXgBnE!6KX12Kx z@>Dy}l%F4tVg3pzJ*{Fc}MFb3dhN6x4~A z2@0~+ks==DXrzwp7o8zQqe@5m@bp@^gObyNDiA`^dip1f``7F#vaFIK~JD zi3r0?80&se#}%^;nSz**KwiR)sTM7BF!#fK_v8c9V~o>K+tPEYmd7WG#Ccj5N(0Uo zrv^oWp7Ac9dx?GOpfJTZfE=gkzuP2!~>~vQNY6mIv0-97Nqk~v zwHacLF>Nsrjn7c)J5(}u=uzt`mJlJ8I4!~q+5b_DCPjro0h+CGXJ{G$y!J++wGMb% zEsv^2SuVqGkk{$awHX4yiskD0caa*7QY?b$uNHQZI72KPgUa+$9?e{u-740OCQ%`f zK`y*VMnLMRNfuvcha!u|7;8>jfZb`8`fb1*b5aCU=3Qc{dT(z*1b}~ENH@+V^f^BoMe&X1Sy~j z;KJXrj*)3PI4jdew8{-BF;&(=un_5N*@DliWY_@I9j({Di}zaxh3cpw0Q2@xH!+d6bLW_Jt1*a;M@Ng@Wz#p!t}mIr|W_CsG_n$msQD4z8c+EgQG{ zJm@{^#v6Srwj8yCap5@yBiMGiFlX#tvG{C6(PfDtXr(@dgi&`YP`kH;W7zd)VERMg z2v556Jl-z`@?g;QpYb@-c3aQm#Stn{Ao)TJu}&;fC|Fvl;fkX(xUj;EO(lnXW)#OB zrr=A#ccz{>MgHehlBgEW845qgjhLf@qS(TvX~&$tgIgnL=L{Xi)>yYrqO@C(y)$ym z2HGhMn=j!K7BD!W1F`FbVrGb$d#lf2dT27tJNn_h)h?K3UKp%#tsbfgCj>fUmQyJW z-X)UJY_SD27d>QY2oMIqnuvI4iTT(zAACMT4Y$9Ws~}j-3^jZUxkkk@DL}Y$1TtE} z^{cW-najA^BH_PTP6V4pDkdS*d4lB$5wb%M5R(l=F&Nne{l9X5ly}9%ESH zfNAY1;*}$5P9()s!|?v7K+f(uEJFF3s|MkxJ`zN3sxIO7E^3VV&Jo0N<_YdJSEhm0 z$6IU6f(;*n-qjj8dp1nvBGy7pg2ER|(T7xGMUhZ2Lwz4Eq?^=-&+p?#?#|( zPz>19i<{}xg8gR51w5u&gsX)`Vh%Nv3{~u%kxH#c>=oRwd?iHpBQLGMG9RW?IlD03 zo+Uq;!M~5;l!KmM!R_zkK0=e^C3L3K{)B@uZ68~tOrm2Sv0 zSl}h7#nBX#_yBM(M@yQgj2Npua-RpwYz9w=h*uKM4{{8o-DkSZ zwX3QPN);+%6+Pq_9*_7Al7gk7QuNk-tdF*kq3{xQtOL9d__jS{tjXaS>E%2QXLbxf z70kntM-J+;$lqDm7Z3&p^rR8_0fuw{@xwgc_8BqD6>(ocT@i|CIzu%8cEJMLwLY%e zZro=`LARNqyj?pAjqKy%U^oveCSZ<7BU||3oAPn{qM{h`Lorm*LS@D6)&~S&srNr1 zPQ`vq1VX`r8#Xy8W(!!?FW3$^3M{e<;?pe*aLOEobMv?cEL-j15V}eJ0F-a{`HI9K zdFjX=%=TGi&v|?e7A?0SEasqiEJ;zocKa@XWHW>Yp%2)MVb!#GfMpDYTMS9;QP{R9 zYz|P}?RpOYK7fe*3HJe*A;-{W5x)(>dvI|sU?GXH3L!zD5#3&#M+|;MK$n4EgM0uE z`#g4kY#lJbjBJNA3`S$`d&C6#fT!s!Z9fnz`7pF|X%=c#I8%VN8F0%(r*M8l*j^Vt zgo3e3nih;TG;g9b`ZGAqSW?IZsa%HEpr=;6wa`=ml7KUHA|ktT&}8#q%F->CFii@$ z3_;@asKp4tVOwknx&KcU*B;x{b;iGQeEhg^92}3!#L_4I?}PgQOU_{ezX+_;!4 zDSI-opyX!t{;auW?Ru9 zje!Zx$9bt^)CD@X5A*Z&&8(9-D+al{y7(~F48_WC#&UP9w{Iwu(qdNLjt-+vtnP-B zILCORVnCRYUT)OZOufjet?;!;!ej;c#teU}UyIGSOKd`Xd?MR*y2pi}Lx!DO)Up&} z9Qbs+op+p3MgKN`+nM5*uCqRJWVD#yyD`H$;{jWSZ_sD#U8B0`=fFWoqHmaA^lBrz ze$|xNUE7lt1#YpW<#q#m^KIuHeD1!%hG?C3hyUzr3iJ4$r)_CcD~uUdtNbxuizKQdfc0 zqEm4y=}?h(x|JQHJqeaU$~0^jYRhZix=_*$h!nd;Og7so{!vh8-JmGsQ9nLEL!qAJCu2S)= z;F0-48p{js^UmnXBxy_j!4{ONNXp_OszBS2+?W@$C0r8-W8pGE%Sx`(-{f=7-S_GI z1+AnpD0Yr!nfRi6XIOw`g>2^OemsANnWDDGt|oH9GJ3+SLP*tOuY{wb{jj>|Y+wHQ z4>_wiioZVzetgDUe^Bb2|M;IRZrS_Z{WJ8-aiQL+G<)(-_jqXBR<8$@-^I-c3y3;A z=g>-bSS10f!U8KB*;vatwb}LoT9=&pNv&DDC~L1q@4SQEf~VN_VJqsa~>Ys3q$P89{LfX_>5b-+w*2T6O`X>$jM1ewx<(o)&Q} z1{D^c;z&(0%5f_o4UWo$>A-GuKWC~36=1v&`5Nlj>3)H$F7gtK6?VuU7?ejc?6P9& zYkhes*9xiAvAf)SvUagi(g7PaxM#3c%h)|vheSXeXR)G`v)O!m#H#=AR*@Fi(1efS zmXw&%=8*{EmFY*un=og39p<8Dj%-6-5?UU{Lr?|jLbY0ZQFq_rWu#t_&R8y6PvTE9 z+XYK0HgSVqD0;ceeOoW%r%rvCWy*yEFQI$&+5K^d5yI|v{_1UZ`JwtAMi_MwQ>0_#LriK?XSM!8KE$8u#@h(??+$%QZy0}U!UWQ2s6 z-OAq>gfkFOhlcL*r`NGj)TndreR9LL{GkO(&I@K9J46rE26+5AX^T7pn^WX$L;E#N zur{kZ$l2y4yJ-M(R>Ra{GYsFVNVFvGnBZ)_S!K-v^=^k|+$I`spp)i~eOJxr+u6za zq7I1FmD`oKJUw}Hn-L1qB~#Fm3b>pcejxTY!=l8LI)RnI zRe}`?+~y17wzXsYU#;BG9Y^{wEfQ`$Z^%SyV^>X;)||*?=T>WfH2hv;-sgN(6~EYG zlauF0|2;vE3Gsl7@qfeRF!3u5q~_r70r)GiT)swG)@yf~g0z3P5q2dT)uF?~(a=*f zgZ7Q9=r(^u{VG3N~)2A;p%=c_fe!=oD0)|q+d|Ehgvt>Z{Pa$_vXfZ$*l z*317!5KJ-PAzcR+d9!)ilVUM1jGpSz%FJ(f`F?bHjQGZQ{s>B(DJ?p^`Kqc=zbZSp zDVE3I+w+hW48SGnzQvzUvNZ(1WEwPUNs}(;Cfz$+qSO?^zjl2ynC^F@ z(Vna}ze=+fX&oEf$!w}(3z-U?aUYb?x& zF3(FJq1}hBeSuZ$Lt?;IAA0;VG!_%nr>84%*jTF^A~`(|1Bdz;2uor%p x>V&&H-%<_;H&h&~Mb&@~jH@@WA+@XAsLE>Ux0kBHIe!lY>@UpY`Y*n|^dH*5KIH%a literal 0 HcmV?d00001 diff --git a/wear/src/main/assets/watch_light.jpg b/wear/src/main/assets/watch_light.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a8183304d62c37b7c04ac51111c2005a756694da GIT binary patch literal 19708 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 23; + } + + public static String gs(@StringRes final int id) { + return getAppContext().getString(id); + } + + public static String gs(@StringRes final int id, String... args) { + return getAppContext().getString(id, (Object[]) args); + } + + public static Boolean areComplicationsUnicode() { + return unicodeComplications; + } + + public static String getComplicationTapAction() { + return complicationTapAction; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + updatePrefs(sharedPrefs); + + // we trigger update on Complications + Intent messageIntent = new Intent(); + messageIntent.setAction(Intent.ACTION_SEND); + LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java b/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java new file mode 100644 index 0000000000..c1f33e41c5 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java @@ -0,0 +1,415 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.drawable.Icon; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationManager; +import android.support.wearable.complications.ComplicationProviderService; +import android.support.wearable.complications.ComplicationText; +import android.support.wearable.complications.ProviderUpdateRequester; +import android.util.Log; + +import java.util.HashSet; +import java.util.Set; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.ListenerService; +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; +import info.nightscout.androidaps.interaction.utils.Inevitable; +import info.nightscout.androidaps.interaction.utils.Persistence; +import info.nightscout.androidaps.interaction.utils.WearUtil; + +/** + * Base class for all complications + * + * Created by dlvoy on 2019-11-12 + */ +public abstract class BaseComplicationProviderService extends ComplicationProviderService { + + private static final String TAG = BaseComplicationProviderService.class.getSimpleName(); + + private static final String KEY_COMPLICATIONS = "complications"; + private static final String KEY_LAST_SINCE = "lastSince"; + private static final String KEY_STALE_REPORTED = "staleReported"; + private static final String TASK_ID_REFRESH_COMPLICATION = "refresh-complication"; + + + private LocalBroadcastManager localBroadcastManager; + private MessageReceiver messageReceiver; + + public static void turnOff() { + Log.d(TAG, "TURNING OFF all active complications"); + final Persistence persistence = new Persistence(); + persistence.putString(KEY_COMPLICATIONS, ""); + } + + //============================================================================================== + // ABSTRACT COMPLICATION INTERFACE + //============================================================================================== + + public abstract ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent); + public abstract String getProviderCanonicalName(); + + public ComplicationAction getComplicationAction() { return ComplicationAction.MENU; }; + + //---------------------------------------------------------------------------------------------- + // DEFAULT BEHAVIOURS + //---------------------------------------------------------------------------------------------- + + public ComplicationData buildNoSyncComplicationData(int dataType, + DisplayRawData raw, + PendingIntent complicationPendingIntent, + PendingIntent exceptionalPendingIntent, + long since) { + ComplicationData complicationData = null; + + final ComplicationData.Builder builder = new ComplicationData.Builder(dataType); + if (dataType != ComplicationData.TYPE_LARGE_IMAGE) { + builder.setIcon(Icon.createWithResource(this, R.drawable.ic_sync_alert)); + } + + if (dataType == ComplicationData.TYPE_RANGED_VALUE) { + builder.setMinValue(0); + builder.setMaxValue(100); + builder.setValue(0); + } + + switch (dataType) { + case ComplicationData.TYPE_ICON: + case ComplicationData.TYPE_SHORT_TEXT: + case ComplicationData.TYPE_RANGED_VALUE: + if (since > 0) { + builder.setShortText(ComplicationText.plainText(DisplayFormat.shortTimeSince(since) + " old")); + } else { + builder.setShortText(ComplicationText.plainText("!err!")); + } + break; + case ComplicationData.TYPE_LONG_TEXT: + builder.setLongTitle(ComplicationText.plainText(aaps.gs(R.string.label_warning_sync))); + if (since > 0) { + builder.setLongText(ComplicationText.plainText(String.format(aaps.gs(R.string.label_warning_since), DisplayFormat.shortTimeSince(since)))); + } else { + builder.setLongText(ComplicationText.plainText(aaps.gs(R.string.label_warning_sync_aaps))); + } + break; + case ComplicationData.TYPE_LARGE_IMAGE: + return buildComplicationData(dataType, raw, complicationPendingIntent); + default: + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + break; + } + + builder.setTapAction(exceptionalPendingIntent); + complicationData = builder.build(); + return complicationData; + } + + public ComplicationData buildOutdatedComplicationData(int dataType, + DisplayRawData raw, + PendingIntent complicationPendingIntent, + PendingIntent exceptionalPendingIntent, + long since) { + ComplicationData complicationData = null; + + final ComplicationData.Builder builder = new ComplicationData.Builder(dataType); + if (dataType != ComplicationData.TYPE_LARGE_IMAGE) { + builder.setIcon(Icon.createWithResource(this, R.drawable.ic_alert)); + builder.setBurnInProtectionIcon(Icon.createWithResource(this, R.drawable.ic_alert_burnin)); + } + + if (dataType == ComplicationData.TYPE_RANGED_VALUE) { + builder.setMinValue(0); + builder.setMaxValue(100); + builder.setValue(0); + } + + switch (dataType) { + case ComplicationData.TYPE_ICON: + case ComplicationData.TYPE_SHORT_TEXT: + case ComplicationData.TYPE_RANGED_VALUE: + if (since > 0) { + builder.setShortText(ComplicationText.plainText(DisplayFormat.shortTimeSince(since) + " old")); + } else { + builder.setShortText(ComplicationText.plainText("!old!")); + } + break; + case ComplicationData.TYPE_LONG_TEXT: + builder.setLongTitle(ComplicationText.plainText(aaps.gs(R.string.label_warning_old))); + if (since > 0) { + builder.setLongText(ComplicationText.plainText(String.format(aaps.gs(R.string.label_warning_since), DisplayFormat.shortTimeSince(since)))); + } else { + builder.setLongText(ComplicationText.plainText(aaps.gs(R.string.label_warning_sync_aaps))); + } + break; + case ComplicationData.TYPE_LARGE_IMAGE: + return buildComplicationData(dataType, raw, complicationPendingIntent); + default: + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + break; + } + + builder.setTapAction(exceptionalPendingIntent); + complicationData = builder.build(); + return complicationData; + } + + /** + * If Complication depend on "since" field and need to be updated every minute or not + * and need only update when new DisplayRawData arrive + */ + protected boolean usesSinceField() { + return false; + } + + //============================================================================================== + // COMPLICATION LIFECYCLE + //============================================================================================== + + /* + * Called when a complication has been activated. The method is for any one-time + * (per complication) set-up. + * + * You can continue sending data for the active complicationId until onComplicationDeactivated() + * is called. + */ + @Override + public void onComplicationActivated( + int complicationId, int dataType, ComplicationManager complicationManager) { + Log.d(TAG, "onComplicationActivated(): " + complicationId + " of kind: "+getProviderCanonicalName()); + + Persistence persistence = new Persistence(); + persistence.putString("complication_"+complicationId, getProviderCanonicalName()); + persistence.putBoolean("complication_"+complicationId+"_since", usesSinceField()); + persistence.addToSet(KEY_COMPLICATIONS, "complication_"+complicationId); + + IntentFilter messageFilter = new IntentFilter(Intent.ACTION_SEND); + + messageReceiver = new BaseComplicationProviderService.MessageReceiver(); + localBroadcastManager = LocalBroadcastManager.getInstance(this); + localBroadcastManager.registerReceiver(messageReceiver, messageFilter); + + ListenerService.requestData(this); + checkIfUpdateNeeded(); + } + + /* + * Called when the complication needs updated data from your provider. There are four scenarios + * when this will happen: + * + * 1. An active watch face complication is changed to use this provider + * 2. A complication using this provider becomes active + * 3. The period of time you specified in the manifest has elapsed (UPDATE_PERIOD_SECONDS) + * 4. You triggered an update from your own class via the + * ProviderUpdateRequester.requestUpdate() method. + */ + @Override + public void onComplicationUpdate( + int complicationId, int dataType, ComplicationManager complicationManager) { + Log.d(TAG, "onComplicationUpdate() id: " + complicationId + " of class: "+getProviderCanonicalName()); + + // Create Tap Action so that the user can checkIfUpdateNeeded an update by tapping the complication. + final ComponentName thisProvider = new ComponentName(this, getProviderCanonicalName()); + + // We pass the complication id, so we can only update the specific complication tapped. + final PendingIntent complicationPendingIntent = + ComplicationTapBroadcastReceiver.getTapActionIntent( + aaps.getAppContext(), thisProvider, complicationId, getComplicationAction()); + + final Persistence persistence = new Persistence(); + + final DisplayRawData raw = new DisplayRawData(); + raw.partialUpdateFromPersistence(persistence); + Log.d(TAG, "Complication data: " + raw.toDebugString()); + + // store what is currently rendered since field, to detect it need update + persistence.putString(KEY_LAST_SINCE, DisplayFormat.shortTimeSince(raw.datetime)); + + // by each render we clear stale flag to ensure it is re-rendered at next refresh detection round + persistence.putBoolean(KEY_STALE_REPORTED, false); + + ComplicationData complicationData; + + if (WearUtil.msSince(persistence.whenDataUpdated()) > Constants.STALE_MS) { + // no new data arrived - probably configuration or connection error + final PendingIntent infoToast = ComplicationTapBroadcastReceiver.getTapWarningSinceIntent( + aaps.getAppContext(), thisProvider, complicationId, ComplicationAction.WARNING_SYNC, persistence.whenDataUpdated()); + complicationData = buildNoSyncComplicationData(dataType, raw, complicationPendingIntent, infoToast, persistence.whenDataUpdated()); + } else if (WearUtil.msSince(raw.datetime) > Constants.STALE_MS) { + // data arriving from phone AAPS, but it is outdated (uploader/NS/xDrip/Sensor error) + final PendingIntent infoToast = ComplicationTapBroadcastReceiver.getTapWarningSinceIntent( + aaps.getAppContext(), thisProvider, complicationId, ComplicationAction.WARNING_OLD, raw.datetime); + complicationData = buildOutdatedComplicationData(dataType, raw, complicationPendingIntent, infoToast, raw.datetime); + } else { + // data is up-to-date, we can render standard complication + complicationData = buildComplicationData(dataType, raw, complicationPendingIntent); + } + + if (complicationData != null) { + complicationManager.updateComplicationData(complicationId, complicationData); + + } else { + // If no data is sent, we still need to inform the ComplicationManager, so the update + // job can finish and the wake lock isn't held any longer than necessary. + complicationManager.noUpdateRequired(complicationId); + } + } + + /* + * Called when the complication has been deactivated. + */ + @Override + public void onComplicationDeactivated(int complicationId) { + Log.d(TAG, "onComplicationDeactivated(): " + complicationId); + + Persistence persistence = new Persistence(); + persistence.removeFromSet(KEY_COMPLICATIONS, "complication_"+complicationId); + + if (localBroadcastManager != null && messageReceiver != null) { + localBroadcastManager.unregisterReceiver(messageReceiver); + } + Inevitable.kill(TASK_ID_REFRESH_COMPLICATION); + } + + //============================================================================================== + // UPDATE AND REFRESH LOGIC + //============================================================================================== + + /* + * Schedule check for field update + */ + public static void checkIfUpdateNeeded() { + + Persistence p = new Persistence(); + + Log.d(TAG, "Pending check if update needed - "+p.getString(KEY_COMPLICATIONS, "")); + + Inevitable.task(TASK_ID_REFRESH_COMPLICATION, 15 * Constants.SECOND_IN_MS, () -> { + if (WearUtil.rateLimit("complication-checkIfUpdateNeeded", 5)) { + Log.d(TAG, "Checking if update needed"); + requestUpdateIfSinceChanged(); + // We reschedule need for check - to make sure next check will Inevitable go in next 15s + checkIfUpdateNeeded(); + } + }); + + } + + /* + * Check if displayed since field (field that shows how old, in minutes, is reading) + * is up-to-date or need to be changed (a minute or more elapsed) + */ + private static void requestUpdateIfSinceChanged() { + final Persistence persistence = new Persistence(); + + final DisplayRawData raw = new DisplayRawData(); + raw.partialUpdateFromPersistence(persistence); + + final String lastSince = persistence.getString(KEY_LAST_SINCE, "-"); + final String calcSince = DisplayFormat.shortTimeSince(raw.datetime); + final boolean isStale = (WearUtil.msSince(persistence.whenDataUpdated()) > Constants.STALE_MS) + ||(WearUtil.msSince(raw.datetime) > Constants.STALE_MS); + + final boolean staleWasRefreshed = persistence.getBoolean(KEY_STALE_REPORTED, false); + final boolean sinceWasChanged = !lastSince.equals(calcSince); + + if (sinceWasChanged|| (isStale && !staleWasRefreshed)) { + persistence.putString(KEY_LAST_SINCE, calcSince); + persistence.putBoolean(KEY_STALE_REPORTED, isStale); + + Log.d(TAG, "Detected refresh of time needed! Reason: " + + (isStale ? "- stale detected": "") + + (sinceWasChanged ? "- since changed from: "+lastSince+" to: "+calcSince : "")); + + if (isStale) { + // all complications should update to show offline/old warning + requestUpdate(getActiveProviderClasses()); + } else { + // ... but only some require update due to 'since' field change + requestUpdate(getSinceDependingProviderClasses()); + } + } + } + + /* + * Request update for specified list of providers + */ + private static void requestUpdate(Set providers) { + for (String provider: providers) { + Log.d(TAG, "Pending update of "+provider); + // We wait with updating allowing all request, from various sources, to arrive + Inevitable.task("update-req-"+provider, 700, () -> { + if (WearUtil.rateLimit("update-req-"+provider, 2)) { + Log.d(TAG, "Requesting update of "+provider); + final ComponentName componentName = new ComponentName(aaps.getAppContext(), provider); + final ProviderUpdateRequester providerUpdateRequester = new ProviderUpdateRequester(aaps.getAppContext(), componentName); + providerUpdateRequester.requestUpdateAll(); + } + }); + } + } + + /* + * List all Complication providing classes that have active (registered) providers + */ + private static Set getActiveProviderClasses() { + Persistence persistence = new Persistence(); + Set providers = new HashSet<>(); + Set complications = persistence.getSetOf(KEY_COMPLICATIONS); + for (String complication: complications) { + final String providerClass = persistence.getString(complication, ""); + if (providerClass.length() > 0) { + providers.add(providerClass); + } + } + return providers; + } + + /* + * List all Complication providing classes that have active (registered) providers + * and additionally they depend on "since" field + * == they need to be updated not only on data broadcasts, but every minute or so + */ + private static Set getSinceDependingProviderClasses() { + Persistence persistence = new Persistence(); + Set providers = new HashSet<>(); + Set complications = persistence.getSetOf(KEY_COMPLICATIONS); + for (String complication: complications) { + final String providerClass = persistence.getString(complication, ""); + final boolean dependOnSince = persistence.getBoolean(complication+"_since", false); + if ((providerClass.length() > 0)&&(dependOnSince)) { + providers.add(providerClass); + } + } + return providers; + } + + /* + * Listen to broadcast --> new data was stored by ListenerService to Persistence + */ + public class MessageReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Set complications = Persistence.setOf(KEY_COMPLICATIONS); + if (complications.size() > 0) { + checkIfUpdateNeeded(); + // We request all active providers + requestUpdate(getActiveProviderClasses()); + } + } + } + + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java new file mode 100644 index 0000000000..e9f05c5f79 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java @@ -0,0 +1,49 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; +import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; + +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_SHORT_FIELD; +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MIN_COB_FIELD; +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MIN_IOB_FIELD; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class BrCobIobComplication extends BaseComplicationProviderService { + + private static final String TAG = BrCobIobComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + if (dataType == ComplicationData.TYPE_SHORT_TEXT) { + final String cob = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE).minimise(MIN_COB_FIELD); + final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_IOB_FIELD, (MAX_SHORT_FIELD-1) - cob.length())); + + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(DisplayFormat.basalRateSymbol()+raw.sBasalRate)) + .setShortTitle(ComplicationText.plainText(cob + " " + iob)) + .setTapAction(complicationPendingIntent); + + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return BrCobIobComplication.class.getCanonicalName(); + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java new file mode 100644 index 0000000000..48341b53ef --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; +import android.util.Pair; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class CobDetailedComplication extends BaseComplicationProviderService { + + private static final String TAG = CobDetailedComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + if (dataType == ComplicationData.TYPE_SHORT_TEXT) { + + Pair cob = DisplayFormat.detailedCob(raw); + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(cob.first)) + .setTapAction(complicationPendingIntent); + + if (cob.second.length() > 0) { + builder.setShortTitle(ComplicationText.plainText(cob.second)); + } + + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return CobDetailedComplication.class.getCanonicalName(); + } + + @Override + public ComplicationAction getComplicationAction() { + return ComplicationAction.WIZARD; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java new file mode 100644 index 0000000000..40824fd313 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java @@ -0,0 +1,51 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.graphics.drawable.Icon; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.DisplayRawData; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class CobIconComplication extends BaseComplicationProviderService { + + private static final String TAG = CobIconComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + if (dataType == ComplicationData.TYPE_SHORT_TEXT) { + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(raw.sCOB2)) + .setIcon( + Icon.createWithResource( + this, R.drawable.ic_carbs)) + .setBurnInProtectionIcon( + Icon.createWithResource( + this, R.drawable.ic_carbs)) + .setTapAction(complicationPendingIntent); + + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return CobIconComplication.class.getCanonicalName(); + } + + public ComplicationAction getComplicationAction() { + return ComplicationAction.WIZARD; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java new file mode 100644 index 0000000000..1f7dff7ceb --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java @@ -0,0 +1,46 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; + +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_SHORT_FIELD; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class CobIobComplication extends BaseComplicationProviderService { + + private static final String TAG = CobIobComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + if (dataType == ComplicationData.TYPE_SHORT_TEXT) { + final String cob = raw.sCOB2; + final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_SHORT_FIELD); + + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(cob)) + .setShortTitle(ComplicationText.plainText(iob)) + .setTapAction(complicationPendingIntent); + + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return CobIobComplication.class.getCanonicalName(); + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/ComplicationAction.java b/wear/src/main/java/info/nightscout/androidaps/complications/ComplicationAction.java new file mode 100644 index 0000000000..9ca1e9c456 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/ComplicationAction.java @@ -0,0 +1,12 @@ +package info.nightscout.androidaps.complications; + +public enum ComplicationAction { + NONE, + MENU, + WIZARD, + BOLUS, + ECARB, + STATUS, + WARNING_SYNC, + WARNING_OLD +} \ No newline at end of file diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/ComplicationTapBroadcastReceiver.java b/wear/src/main/java/info/nightscout/androidaps/complications/ComplicationTapBroadcastReceiver.java new file mode 100644 index 0000000000..61dc8d4dde --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/ComplicationTapBroadcastReceiver.java @@ -0,0 +1,167 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.wearable.complications.ProviderUpdateRequester; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.StringRes; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.interaction.actions.BolusActivity; +import info.nightscout.androidaps.interaction.actions.ECarbActivity; +import info.nightscout.androidaps.interaction.actions.WizardActivity; +import info.nightscout.androidaps.interaction.menus.MainMenuActivity; +import info.nightscout.androidaps.interaction.menus.StatusMenuActivity; +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; +import info.nightscout.androidaps.interaction.utils.WearUtil; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class ComplicationTapBroadcastReceiver extends BroadcastReceiver { + + private static final String TAG = ComplicationTapBroadcastReceiver.class.getSimpleName(); + + private static final String EXTRA_PROVIDER_COMPONENT = + "info.nightscout.androidaps.complications.action.PROVIDER_COMPONENT"; + private static final String EXTRA_COMPLICATION_ID = + "info.nightscout.androidaps.complications.action.COMPLICATION_ID"; + private static final String EXTRA_COMPLICATION_ACTION = + "info.nightscout.androidaps.complications.action.COMPLICATION_ACTION"; + private static final String EXTRA_COMPLICATION_SINCE = + "info.nightscout.androidaps.complications.action.COMPLICATION_SINCE"; + + @Override + public void onReceive(Context context, Intent intent) { + Bundle extras = intent.getExtras(); + ComponentName provider = extras.getParcelable(EXTRA_PROVIDER_COMPONENT); + int complicationId = extras.getInt(EXTRA_COMPLICATION_ID); + String complicationAction = extras.getString(EXTRA_COMPLICATION_ACTION, ComplicationAction.MENU.toString()); + + ComplicationAction action = ComplicationAction.MENU; + try { + action = ComplicationAction.valueOf(ComplicationAction.class, complicationAction); + } catch (IllegalArgumentException | NullPointerException ex) { + // but how? + Log.e(TAG, "Cannot interpret complication action: "+complicationAction); + } + + action = remapActionWithUserPreferences(action); + + // Request an update for the complication that has just been tapped. + ProviderUpdateRequester requester = new ProviderUpdateRequester(context, provider); + requester.requestUpdate(complicationId); + + Intent intentOpen = null; + + switch (action) { + case NONE: + // do nothing + return; + case WIZARD: + intentOpen = new Intent(aaps.getAppContext(), WizardActivity.class); + break; + case BOLUS: + intentOpen = new Intent(aaps.getAppContext(), BolusActivity.class); + break; + case ECARB: + intentOpen = new Intent(aaps.getAppContext(), ECarbActivity.class); + break; + case STATUS: + intentOpen = new Intent(aaps.getAppContext(), StatusMenuActivity.class); + break; + case WARNING_OLD: + case WARNING_SYNC: + long oneAndHalfMinuteAgo = WearUtil.timestamp() - (Constants.MINUTE_IN_MS+Constants.SECOND_IN_MS * 30); + long since = extras.getLong(EXTRA_COMPLICATION_SINCE, oneAndHalfMinuteAgo); + @StringRes int labelId = (action == ComplicationAction.WARNING_SYNC) ? + R.string.msg_warning_sync : R.string.msg_warning_old; + String msg = String.format(aaps.gs(labelId), DisplayFormat.shortTimeSince(since)); + Toast.makeText(aaps.getAppContext(), msg, Toast.LENGTH_LONG).show(); + break; + case MENU: + default: + intentOpen = new Intent(aaps.getAppContext(), MainMenuActivity.class); + } + + if (intentOpen != null) { + // Perform intent - open dialog + intentOpen.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + aaps.getAppContext().startActivity(intentOpen); + } + } + + private ComplicationAction remapActionWithUserPreferences(ComplicationAction originalAction) { + final String userPrefAction = aaps.getComplicationTapAction(); + switch (originalAction) { + case WARNING_OLD: + case WARNING_SYNC: + // warnings cannot be reconfigured by user + return originalAction; + default: + switch (userPrefAction) { + case "menu": + return ComplicationAction.MENU; + case "wizard": + return ComplicationAction.WIZARD; + case "bolus": + return ComplicationAction.BOLUS; + case "ecarb": + return ComplicationAction.ECARB; + case "status": + return ComplicationAction.STATUS; + case "none": + return ComplicationAction.NONE; + case "default": + default: + return originalAction; + } + } + } + + /** + * Returns a pending intent, suitable for use as a tap intent, that causes a complication to be + * toggled and updated. + */ + static PendingIntent getTapActionIntent( + Context context, ComponentName provider, int complicationId, ComplicationAction action) { + Intent intent = new Intent(context, ComplicationTapBroadcastReceiver.class); + intent.putExtra(EXTRA_PROVIDER_COMPONENT, provider); + intent.putExtra(EXTRA_COMPLICATION_ID, complicationId); + intent.putExtra(EXTRA_COMPLICATION_ACTION, action.toString()); + + + // Pass complicationId as the requestCode to ensure that different complications get + // different intents. + return PendingIntent.getBroadcast( + context, complicationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Returns a pending intent, suitable for use as a tap intent, that causes a complication to be + * toggled and updated. + */ + static PendingIntent getTapWarningSinceIntent( + Context context, ComponentName provider, int complicationId, ComplicationAction action, long since) { + Intent intent = new Intent(context, ComplicationTapBroadcastReceiver.class); + intent.putExtra(EXTRA_PROVIDER_COMPONENT, provider); + intent.putExtra(EXTRA_COMPLICATION_ID, complicationId); + intent.putExtra(EXTRA_COMPLICATION_ACTION, action.toString()); + intent.putExtra(EXTRA_COMPLICATION_SINCE, since); + + + // Pass complicationId as the requestCode to ensure that different complications get + // different intents. + return PendingIntent.getBroadcast( + context, complicationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java new file mode 100644 index 0000000000..d9ee729874 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; +import android.util.Pair; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class IobDetailedComplication extends BaseComplicationProviderService { + + private static final String TAG = IobDetailedComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + if (dataType == ComplicationData.TYPE_SHORT_TEXT) { + + Pair iob = DisplayFormat.detailedIob(raw); + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(iob.first)) + .setTapAction(complicationPendingIntent); + + if (iob.second.length() > 0) { + builder.setShortTitle(ComplicationText.plainText(iob.second)); + } + + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return IobDetailedComplication.class.getCanonicalName(); + } + + @Override + public ComplicationAction getComplicationAction() { + return ComplicationAction.BOLUS; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java new file mode 100644 index 0000000000..ba253a048d --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java @@ -0,0 +1,56 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.graphics.drawable.Icon; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; + +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_SHORT_FIELD; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class IobIconComplication extends BaseComplicationProviderService { + + private static final String TAG = IobIconComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + if (dataType == ComplicationData.TYPE_SHORT_TEXT) { + final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_SHORT_FIELD); + + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(iob)) + .setIcon(Icon.createWithResource( + this, R.drawable.ic_ins)) + .setBurnInProtectionIcon( + Icon.createWithResource( + this, R.drawable.ic_ins_burnin)) + .setTapAction(complicationPendingIntent); + + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return IobIconComplication.class.getCanonicalName(); + } + + @Override + public ComplicationAction getComplicationAction() { + return ComplicationAction.BOLUS; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java new file mode 100644 index 0000000000..834f40b0a7 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java @@ -0,0 +1,52 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class LongStatusComplication extends BaseComplicationProviderService { + + private static final String TAG = LongStatusComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + switch (dataType) { + case ComplicationData.TYPE_LONG_TEXT: + + final String glucoseLine = DisplayFormat.longGlucoseLine(raw); + final String detailsLine = DisplayFormat.longDetailsLine(raw); + + final ComplicationData.Builder builderLong = new ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT) + .setLongTitle(ComplicationText.plainText(glucoseLine)) + .setLongText(ComplicationText.plainText(detailsLine)) + .setTapAction(complicationPendingIntent); + complicationData = builderLong.build(); + + break; + default: + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return LongStatusComplication.class.getCanonicalName(); + } + + @Override + protected boolean usesSinceField() { + return true; + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java new file mode 100644 index 0000000000..da3384d358 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java @@ -0,0 +1,53 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; +import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class LongStatusFlippedComplication extends BaseComplicationProviderService { + + private static final String TAG = LongStatusFlippedComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + switch (dataType) { + case ComplicationData.TYPE_LONG_TEXT: + + final String glucoseLine = DisplayFormat.longGlucoseLine(raw); + final String detailsLine = DisplayFormat.longDetailsLine(raw); + + final ComplicationData.Builder builderLong = new ComplicationData.Builder(ComplicationData.TYPE_LONG_TEXT) + .setLongTitle(ComplicationText.plainText(detailsLine)) + .setLongText(ComplicationText.plainText(glucoseLine)) + .setTapAction(complicationPendingIntent); + complicationData = builderLong.build(); + + break; + default: + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return LongStatusFlippedComplication.class.getCanonicalName(); + } + + @Override + protected boolean usesSinceField() { + return true; + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java new file mode 100644 index 0000000000..b6f9e94946 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java @@ -0,0 +1,48 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.DisplayFormat; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class SgvComplication extends BaseComplicationProviderService { + + private static final String TAG = SgvComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + switch (dataType) { + case ComplicationData.TYPE_SHORT_TEXT: + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(raw.sSgv + raw.sDirection)) + .setShortTitle(ComplicationText.plainText(DisplayFormat.shortTrend(raw))) + .setTapAction(complicationPendingIntent); + + complicationData = builder.build(); + break; + default: + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return SgvComplication.class.getCanonicalName(); + } + + @Override + protected boolean usesSinceField() { + return true; + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java b/wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java new file mode 100644 index 0000000000..4b268ef9c7 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java @@ -0,0 +1,114 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.graphics.drawable.Icon; +import android.support.wearable.complications.ComplicationData; +import android.support.wearable.complications.ComplicationText; +import android.util.Log; + +import androidx.annotation.DrawableRes; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.DisplayRawData; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class UploaderBattery extends BaseComplicationProviderService { + + private static final String TAG = UploaderBattery.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + @DrawableRes int batteryIcon = R.drawable.ic_battery_unknown; + @DrawableRes int burnInBatteryIcon = R.drawable.ic_battery_unknown_burnin; + int level = 0; + String levelStr = "???"; + + if (raw.sUploaderBattery.matches("^[0-9]+$")) { + try { + level = Integer.parseInt(raw.sUploaderBattery); + level = Math.max(Math.min(level, 100), 0); + levelStr = level + "%"; + int iconNo = (int)Math.floor(level / 10.0); + if (level > 95) { + iconNo = 10; + } + switch (iconNo) { + case 10: batteryIcon = R.drawable.ic_battery_charging_wireless; break; + case 9: batteryIcon = R.drawable.ic_battery_charging_wireless_90; break; + case 8: batteryIcon = R.drawable.ic_battery_charging_wireless_80; break; + case 7: batteryIcon = R.drawable.ic_battery_charging_wireless_70; break; + case 6: batteryIcon = R.drawable.ic_battery_charging_wireless_60; break; + case 5: batteryIcon = R.drawable.ic_battery_charging_wireless_50; break; + case 4: batteryIcon = R.drawable.ic_battery_charging_wireless_40; break; + case 3: batteryIcon = R.drawable.ic_battery_charging_wireless_30; break; + case 2: batteryIcon = R.drawable.ic_battery_charging_wireless_20; break; + case 1: batteryIcon = R.drawable.ic_battery_charging_wireless_10; break; + case 0: batteryIcon = R.drawable.ic_battery_alert_variant_outline; break; + default: batteryIcon = R.drawable.ic_battery_charging_wireless_outline; + } + + switch (iconNo) { + case 10: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_burnin; break; + case 9: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_90_burnin; break; + case 8: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_80_burnin; break; + case 7: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_70_burnin; break; + case 6: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_60_burnin; break; + case 5: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_50_burnin; break; + case 4: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_40_burnin; break; + case 3: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_30_burnin; break; + case 2: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_20_burnin; break; + case 1: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_10_burnin; break; + case 0: burnInBatteryIcon = R.drawable.ic_battery_alert_variant_outline; break; + default: burnInBatteryIcon = R.drawable.ic_battery_charging_wireless_outline; + } + + + } catch (NumberFormatException ex){ + Log.e(TAG, "Cannot parse battery level of: " + raw.sUploaderBattery); + } + } + + if (dataType == ComplicationData.TYPE_RANGED_VALUE) { + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE) + .setMinValue(0) + .setMaxValue(100) + .setValue(level) + .setShortText(ComplicationText.plainText(levelStr)) + .setIcon(Icon.createWithResource(this, batteryIcon)) + .setBurnInProtectionIcon(Icon.createWithResource(this, burnInBatteryIcon)) + .setTapAction(complicationPendingIntent); + complicationData = builder.build(); + } else if (dataType == ComplicationData.TYPE_SHORT_TEXT) { + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) + .setShortText(ComplicationText.plainText(levelStr)) + .setIcon(Icon.createWithResource(this, batteryIcon)) + .setBurnInProtectionIcon(Icon.createWithResource(this, burnInBatteryIcon)) + .setTapAction(complicationPendingIntent); + complicationData = builder.build(); + } else if (dataType == ComplicationData.TYPE_ICON) { + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_ICON) + .setIcon(Icon.createWithResource(this, batteryIcon)) + .setBurnInProtectionIcon(Icon.createWithResource(this, burnInBatteryIcon)) + .setTapAction(complicationPendingIntent); + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } + + @Override + public String getProviderCanonicalName() { + return UploaderBattery.class.getCanonicalName(); + } + + @Override + public ComplicationAction getComplicationAction() { + return ComplicationAction.STATUS; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java new file mode 100644 index 0000000000..2448bebee0 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java @@ -0,0 +1,62 @@ +package info.nightscout.androidaps.complications; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Icon; +import android.support.wearable.complications.ComplicationData; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.WindowManager; + +import java.io.IOException; +import java.io.InputStream; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.data.DisplayRawData; + +/* + * Created by dlvoy on 2019-11-12 + */ +public abstract class WallpaperComplication extends BaseComplicationProviderService { + + public abstract String getWallpaperAssetsFileName(); + + private static final String TAG = WallpaperComplication.class.getSimpleName(); + + public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + + ComplicationData complicationData = null; + + if (dataType == ComplicationData.TYPE_LARGE_IMAGE) { + + DisplayMetrics metrics = new DisplayMetrics(); + WindowManager windowManager = (WindowManager) aaps.getAppContext() + .getSystemService(Context.WINDOW_SERVICE); + windowManager.getDefaultDisplay().getMetrics(metrics); + int width = metrics.widthPixels; + int height = metrics.heightPixels; + + final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_LARGE_IMAGE); + + AssetManager assetManager = getAssets(); + try (InputStream istr = assetManager.open(getWallpaperAssetsFileName())) { + Bitmap bitmap = BitmapFactory.decodeStream(istr); + Bitmap scaled = Bitmap.createScaledBitmap(bitmap, width, height, true); + builder.setLargeImage(Icon.createWithBitmap(scaled)); + } catch (IOException e) { + Log.e(TAG, "Cannot read wallpaper asset: "+e.getMessage(), e); + e.printStackTrace(); + } + + complicationData = builder.build(); + } else { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Unexpected complication type " + dataType); + } + } + return complicationData; + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperDarkComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperDarkComplication.java new file mode 100644 index 0000000000..8c84e1d8c3 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperDarkComplication.java @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.complications; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class WallpaperDarkComplication extends WallpaperComplication { + + @Override + public String getWallpaperAssetsFileName() { + return "watch_dark.jpg"; + } + + @Override + public String getProviderCanonicalName() { + return WallpaperDarkComplication.class.getCanonicalName(); + } + + @Override + public ComplicationAction getComplicationAction() { + return ComplicationAction.NONE; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperGrayComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperGrayComplication.java new file mode 100644 index 0000000000..bec047f323 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperGrayComplication.java @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.complications; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class WallpaperGrayComplication extends WallpaperComplication { + + @Override + public String getWallpaperAssetsFileName() { + return "watch_gray.jpg"; + } + + @Override + public String getProviderCanonicalName() { + return WallpaperGrayComplication.class.getCanonicalName(); + } + + @Override + public ComplicationAction getComplicationAction() { + return ComplicationAction.NONE; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperLightComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperLightComplication.java new file mode 100644 index 0000000000..2d2bbf6f14 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperLightComplication.java @@ -0,0 +1,22 @@ +package info.nightscout.androidaps.complications; + +/* + * Created by dlvoy on 2019-11-12 + */ +public class WallpaperLightComplication extends WallpaperComplication { + + @Override + public String getWallpaperAssetsFileName() { + return "watch_light.jpg"; + } + + @Override + public String getProviderCanonicalName() { + return WallpaperLightComplication.class.getCanonicalName(); + } + + @Override + public ComplicationAction getComplicationAction() { + return ComplicationAction.NONE; + }; +} diff --git a/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java b/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java new file mode 100644 index 0000000000..e9cf997db8 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java @@ -0,0 +1,269 @@ +package info.nightscout.androidaps.data; + +import android.content.Intent; +import android.os.Bundle; +import android.os.PowerManager; + +import com.google.android.gms.wearable.DataMap; + +import java.util.ArrayList; + +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.Persistence; +import info.nightscout.androidaps.interaction.utils.WearUtil; + +/** + * Holds bunch of data model variables and lists that arrive from phone app and are due to be + * displayed on watchface and complications. Keeping them together makes code cleaner and allows + * passing it to complications via persistence layer. + * + * Created by dlvoy on 2019-11-12 + */ +public class DisplayRawData { + + static final String DATA_PERSISTENCE_KEY = "raw_data"; + static final String BASALS_PERSISTENCE_KEY = "raw_basals"; + static final String STATUS_PERSISTENCE_KEY = "raw_status"; + + // data bundle + public long sgvLevel = 0; + public long datetime; + public String sSgv = "---"; + public String sDirection = "--"; + public String sDelta = "--"; + public String sAvgDelta = "--"; + public String sUnits = "-"; + + // status bundle + public String sBasalRate = "-.--U/h"; + public String sUploaderBattery = "--"; + public String sRigBattery = "--"; + public boolean detailedIOB = false; + public String sIOB1 = "IOB"; + public String sIOB2 = "-.--"; + public String sCOB1 = "Carb"; + public String sCOB2= "--g"; + public String sBgi = "--"; + public boolean showBGI = false; + public String externalStatusString = "no status"; + public int batteryLevel = 1; + public long openApsStatus = -1; + + // basals bundle + public ArrayList bgDataList = new ArrayList<>(); + public ArrayList tempWatchDataList = new ArrayList<>(); + public ArrayList basalWatchDataList = new ArrayList<>(); + public ArrayList bolusWatchDataList = new ArrayList<>(); + public ArrayList predictionList = new ArrayList<>(); + + public String toDebugString() { + return "DisplayRawData{" + + "sgvLevel=" + sgvLevel + + ", datetime=" + datetime + + ", sSgv='" + sSgv + '\'' + + ", sDirection='" + sDirection + '\'' + + ", sDelta='" + sDelta + '\'' + + ", sAvgDelta='" + sAvgDelta + '\'' + + ", sUnits='" + sUnits + '\'' + + ", sBasalRate='" + sBasalRate + '\'' + + ", sUploaderBattery='" + sUploaderBattery + '\'' + + ", sRigBattery='" + sRigBattery + '\'' + + ", detailedIOB=" + detailedIOB + + ", sIOB1='" + sIOB1 + '\'' + + ", sIOB2='" + sIOB2 + '\'' + + ", sCOB1='" + sCOB1 + '\'' + + ", sCOB2='" + sCOB2 + '\'' + + ", sBgi='" + sBgi + '\'' + + ", showBGI=" + showBGI + + ", externalStatusString='" + externalStatusString + '\'' + + ", batteryLevel=" + batteryLevel + + ", openApsStatus=" + openApsStatus + + ", bgDataList size=" + bgDataList.size() + + ", tempWatchDataList size=" + tempWatchDataList.size() + + ", basalWatchDataList size=" + basalWatchDataList.size() + + ", bolusWatchDataLis size=" + bolusWatchDataList.size() + + ", predictionList size=" + predictionList.size() + + '}'; + } + + public void updateFromPersistence(Persistence persistence) { + + DataMap dataMapData = persistence.getDataMap(DATA_PERSISTENCE_KEY); + if (dataMapData != null) { + updateData(dataMapData); + } + DataMap dataMapStatus = persistence.getDataMap(STATUS_PERSISTENCE_KEY); + if (dataMapStatus != null) { + updateStatus(dataMapStatus); + } + DataMap dataMapBasals = persistence.getDataMap(BASALS_PERSISTENCE_KEY); + if (dataMapBasals != null) { + updateBasals(dataMapBasals); + } + } + + /* + * Since complications do not need Basals, we skip them for performance + */ + public void partialUpdateFromPersistence(Persistence persistence) { + + DataMap dataMapData = persistence.getDataMap(DATA_PERSISTENCE_KEY); + if (dataMapData != null) { + updateData(dataMapData); + } + DataMap dataMapStatus = persistence.getDataMap(STATUS_PERSISTENCE_KEY); + if (dataMapStatus != null) { + updateStatus(dataMapStatus); + } + } + + public DataMap updateDataFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { + Bundle bundle = intent.getBundleExtra("data"); + if (bundle != null) { + DataMap dataMap = DataMap.fromBundle(bundle); + updateData(dataMap); + return dataMap; + } + return null; + } + + private void updateData(DataMap dataMap) { + WearUtil.getWakeLock("readingPrefs", 50); + sgvLevel = dataMap.getLong("sgvLevel"); + datetime = dataMap.getLong("timestamp"); + sSgv = dataMap.getString("sgvString"); + sDirection = dataMap.getString("slopeArrow"); + sDelta = dataMap.getString("delta"); + sAvgDelta = dataMap.getString("avgDelta"); + sUnits = dataMap.getString("glucoseUnits"); + } + + public DataMap updateStatusFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { + Bundle bundle = intent.getBundleExtra("status"); + if (bundle != null) { + DataMap dataMap = DataMap.fromBundle(bundle); + updateStatus(dataMap); + return dataMap; + } + return null; + } + + private void updateStatus(DataMap dataMap) { + WearUtil.getWakeLock("readingPrefs", 50); + sBasalRate = dataMap.getString("currentBasal"); + sUploaderBattery = dataMap.getString("battery"); + sRigBattery = dataMap.getString("rigBattery"); + detailedIOB = dataMap.getBoolean("detailedIob"); + sIOB1 = dataMap.getString("iobSum") + "U"; + sIOB2 = dataMap.getString("iobDetail"); + sCOB1 = "Carb"; + sCOB2 = dataMap.getString("cob"); + sBgi = dataMap.getString("bgi"); + showBGI = dataMap.getBoolean("showBgi"); + externalStatusString = dataMap.getString("externalStatusString"); + batteryLevel = dataMap.getInt("batteryLevel"); + openApsStatus = dataMap.getLong("openApsStatus"); + } + + public DataMap updateBasalsFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { + Bundle bundle = intent.getBundleExtra("basals"); + if (bundle != null) { + DataMap dataMap = DataMap.fromBundle(bundle); + updateBasals(dataMap); + return dataMap; + } + return null; + } + + private void updateBasals(DataMap dataMap) { + WearUtil.getWakeLock("readingPrefs", 500); + loadBasalsAndTemps(dataMap); + } + + private void loadBasalsAndTemps(DataMap dataMap) { + ArrayList temps = dataMap.getDataMapArrayList("temps"); + if (temps != null) { + tempWatchDataList = new ArrayList<>(); + for (DataMap temp : temps) { + TempWatchData twd = new TempWatchData(); + twd.startTime = temp.getLong("starttime"); + twd.startBasal = temp.getDouble("startBasal"); + twd.endTime = temp.getLong("endtime"); + twd.endBasal = temp.getDouble("endbasal"); + twd.amount = temp.getDouble("amount"); + tempWatchDataList.add(twd); + } + } + ArrayList basals = dataMap.getDataMapArrayList("basals"); + if (basals != null) { + basalWatchDataList = new ArrayList<>(); + for (DataMap basal : basals) { + BasalWatchData bwd = new BasalWatchData(); + bwd.startTime = basal.getLong("starttime"); + bwd.endTime = basal.getLong("endtime"); + bwd.amount = basal.getDouble("amount"); + basalWatchDataList.add(bwd); + } + } + ArrayList boluses = dataMap.getDataMapArrayList("boluses"); + if (boluses != null) { + bolusWatchDataList = new ArrayList<>(); + for (DataMap bolus : boluses) { + BolusWatchData bwd = new BolusWatchData(); + bwd.date = bolus.getLong("date"); + bwd.bolus = bolus.getDouble("bolus"); + bwd.carbs = bolus.getDouble("carbs"); + bwd.isSMB = bolus.getBoolean("isSMB"); + bwd.isValid = bolus.getBoolean("isValid"); + bolusWatchDataList.add(bwd); + } + } + ArrayList predictions = dataMap.getDataMapArrayList("predictions"); + if (boluses != null) { + predictionList = new ArrayList<>(); + for (DataMap prediction : predictions) { + BgWatchData bwd = new BgWatchData(); + bwd.timestamp = prediction.getLong("timestamp"); + bwd.sgv = prediction.getDouble("sgv"); + bwd.color = prediction.getInt("color"); + predictionList.add(bwd); + } + } + } + + public void addToWatchSet(DataMap dataMap) { + ArrayList entries = dataMap.getDataMapArrayList("entries"); + if (entries != null) { + bgDataList = new ArrayList<>(); + for (DataMap entry : entries) { + double sgv = entry.getDouble("sgvDouble"); + double high = entry.getDouble("high"); + double low = entry.getDouble("low"); + long timestamp = entry.getLong("timestamp"); + int color = entry.getInt("color", 0); + bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); + } + } else { + double sgv = dataMap.getDouble("sgvDouble"); + double high = dataMap.getDouble("high"); + double low = dataMap.getDouble("low"); + long timestamp = dataMap.getLong("timestamp"); + int color = dataMap.getInt("color", 0); + + final int size = bgDataList.size(); + if (size > 0) { + if (bgDataList.get(size - 1).timestamp == timestamp) + return; // Ignore duplicates. + } + + bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); + } + + for (int i = 0; i < bgDataList.size(); i++) { + if (bgDataList.get(i).timestamp < (System.currentTimeMillis() - (Constants.HOUR_IN_MS * 5))) { + bgDataList.remove(i); //Get rid of anything more than 5 hours old + break; + } + } + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java index 83c9ad58b3..7bd43a79e1 100644 --- a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java +++ b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java @@ -40,6 +40,7 @@ import info.nightscout.androidaps.interaction.AAPSPreferences; import info.nightscout.androidaps.R; import info.nightscout.androidaps.interaction.actions.AcceptActivity; import info.nightscout.androidaps.interaction.actions.CPPActivity; +import info.nightscout.androidaps.interaction.utils.Persistence; import info.nightscout.androidaps.interaction.utils.SafeParse; import info.nightscout.androidaps.interaction.utils.WearUtil; @@ -512,12 +513,14 @@ public class ListenerService extends WearableListenerService implements GoogleAp Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("status", dataMap.toBundle()); + Persistence.storeDataMap(DisplayRawData.STATUS_PERSISTENCE_KEY, dataMap); LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } else if (path.equals(BASAL_DATA_PATH)){ dataMap = DataMapItem.fromDataItem(event.getDataItem()).getDataMap(); Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("basals", dataMap.toBundle()); + Persistence.storeDataMap(DisplayRawData.BASALS_PERSISTENCE_KEY, dataMap); LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } else if (path.equals(NEW_PREFERENCES_PATH)){ dataMap = DataMapItem.fromDataItem(event.getDataItem()).getDataMap(); @@ -541,6 +544,7 @@ public class ListenerService extends WearableListenerService implements GoogleAp Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("data", dataMap.toBundle()); + Persistence.storeDataMap(DisplayRawData.DATA_PERSISTENCE_KEY, dataMap); LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } } diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Constants.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Constants.java new file mode 100644 index 0000000000..2af3ef34b6 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Constants.java @@ -0,0 +1,14 @@ +package info.nightscout.androidaps.interaction.utils; + +public class Constants { + + public static final long SECOND_IN_MS = 1000; + public static final long MINUTE_IN_MS = 60000; + public static final long HOUR_IN_MS = 3600000; + public static final long DAY_IN_MS = 86400000; + public static final long WEEK_IN_MS = DAY_IN_MS * 7; + public static final long MONTH_IN_MS = DAY_IN_MS * 30; + + public static final long STALE_MS = Constants.MINUTE_IN_MS * 12; + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java new file mode 100644 index 0000000000..366ca28eed --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java @@ -0,0 +1,129 @@ +package info.nightscout.androidaps.interaction.utils; + +import android.util.Pair; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.data.DisplayRawData; + +public class DisplayFormat { + + /** + * Maximal lengths of fields/labels shown in complications + */ + public static final int MAX_LONG_FIELD = 22; // this is empirical, above that many watch faces start to ellipsize + public static final int MAX_SHORT_FIELD = 7; // according to Wear OS docs for TYPE_SHORT_TEXT + public static final int MIN_COB_FIELD = 3; // since carbs are 0..99g + public static final int MIN_IOB_FIELD = 3; // IoB can range from like .1U to 99U + + public static String deltaSymbol() { + return aaps.areComplicationsUnicode() ? "\u0394" : ""; + } + + public static String verticalSeparatorSymbol() { + return aaps.areComplicationsUnicode() ? "\u205E" : "|"; + } + + public static String basalRateSymbol() { + return aaps.areComplicationsUnicode() ? "\u238D\u2006" : ""; + } + + public static String shortTimeSince(final long refTime) { + + long deltaTimeMs = WearUtil.msSince(refTime); + + if (deltaTimeMs < Constants.MINUTE_IN_MS) { + return "0'"; + } else if (deltaTimeMs < Constants.HOUR_IN_MS) { + int minutes = (int) (deltaTimeMs / Constants.MINUTE_IN_MS); + return minutes + "'"; + } else if (deltaTimeMs < Constants.DAY_IN_MS) { + int hours = (int) (deltaTimeMs / Constants.HOUR_IN_MS); + return hours + "h"; + } else { + int days = (int) (deltaTimeMs / Constants.DAY_IN_MS); + if (days < 7) { + return days + "d"; + } else { + int weeks = days / 7; + return weeks + "d"; + } + } + } + + public static String shortTrend(final DisplayRawData raw) { + String minutes = shortTimeSince(raw.datetime); + String delta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_SHORT_FIELD-1); + + if (minutes.length() + delta.length() + 1 < MAX_SHORT_FIELD) { + delta = deltaSymbol() + delta; + } + + return minutes + " " + delta; + } + + public static String longGlucoseLine(final DisplayRawData raw) { + return raw.sSgv + raw.sDirection + " " + deltaSymbol() + (new SmallestDoubleString(raw.sDelta)).minimise(8) + " (" + shortTimeSince(raw.datetime) + ")"; + } + + public static String longDetailsLine(final DisplayRawData raw) { + + final String SEP_LONG = " " + verticalSeparatorSymbol() + " "; + final String SEP_SHORT = " " + verticalSeparatorSymbol() + " "; + final int SEP_SHORT_LEN = SEP_SHORT.length(); + final String SEP_MIN = " "; + + String line = raw.sCOB2 + SEP_LONG + raw.sIOB1 + SEP_LONG + basalRateSymbol()+raw.sBasalRate; + if (line.length() <= MAX_LONG_FIELD) { + return line; + } + line = raw.sCOB2 + SEP_SHORT + raw.sIOB1 + SEP_SHORT + raw.sBasalRate; + if (line.length() <= MAX_LONG_FIELD) { + return line; + } + + int remainingMax = MAX_LONG_FIELD - (raw.sCOB2.length() + raw.sBasalRate.length() + SEP_SHORT_LEN*2); + final String smallestIoB = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_IOB_FIELD, remainingMax)); + line = raw.sCOB2 + SEP_SHORT + smallestIoB + SEP_SHORT + raw.sBasalRate; + if (line.length() <= MAX_LONG_FIELD) { + return line; + } + + remainingMax = MAX_LONG_FIELD - (smallestIoB.length() + raw.sBasalRate.length() + SEP_SHORT_LEN*2); + final String simplifiedCob = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_COB_FIELD, remainingMax)); + + line = simplifiedCob + SEP_SHORT + smallestIoB + SEP_SHORT + raw.sBasalRate; + if (line.length() <= MAX_LONG_FIELD) { + return line; + } + + line = simplifiedCob + SEP_MIN + smallestIoB + SEP_MIN + raw.sBasalRate; + + return line; + } + + public static Pair detailedIob(DisplayRawData raw) { + final String iob1 = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_SHORT_FIELD); + String iob2 = ""; + if (raw.sIOB2.contains("|")) { + + String[] iobs = raw.sIOB2.replace("(", "").replace(")", "").split("\\|"); + if (iobs.length == 2) { + final String iobBolus = new SmallestDoubleString(iobs[0]).minimise(MIN_IOB_FIELD); + final String iobBasal = new SmallestDoubleString(iobs[1]).minimise((MAX_SHORT_FIELD-1) - Math.max(MIN_IOB_FIELD, iobBolus.length())); + iob2 = iobBolus+" "+iobBasal; + } + } + return Pair.create(iob1, iob2); + } + + public static Pair detailedCob(final DisplayRawData raw) { + SmallestDoubleString cobMini = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE); + + String cob2 = ""; + if (cobMini.getExtra().length() > 0) { + cob2 = cobMini.getExtra() + cobMini.getUnits(); + } + final String cob1 = cobMini.minimise(MAX_SHORT_FIELD); + return Pair.create(cob1, cob2); + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java new file mode 100644 index 0000000000..21944da9c7 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java @@ -0,0 +1,117 @@ +package info.nightscout.androidaps.interaction.utils; + +import android.os.PowerManager; +import android.util.Log; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * Created for xDrip by jamorham on 07/03/2018 + * Adapted for AAPS by dlvoy on 2019-11-11 + * + * Tasks which are fired from events can be scheduled here and only execute when they become idle + * and are not being rescheduled within their wait window. + * + */ + +public class Inevitable { + + private static final String TAG = Inevitable.class.getSimpleName(); + private static final int MAX_QUEUE_TIME = (int) Constants.MINUTE_IN_MS * 6; + private static final boolean d = true; + + private static final ConcurrentHashMap tasks = new ConcurrentHashMap<>(); + + public static synchronized void task(final String id, long idle_for, Runnable runnable) { + if (idle_for > MAX_QUEUE_TIME) { + throw new RuntimeException(id + " Requested time: " + idle_for + " beyond max queue time"); + } + final Task task = tasks.get(id); + if (task != null) { + // if it already exists then extend the time + task.extendTime(idle_for); + + if (d) + Log.d(TAG, "Extending time for: " + id + " to " + WearUtil.dateTimeText(task.when)); + } else { + // otherwise create new task + if (runnable == null) return; // extension only if already exists + tasks.put(id, new Task(id, idle_for, runnable)); + + if (d) { + Log.d(TAG, "Creating task: " + id + " due: " + WearUtil.dateTimeText(tasks.get(id).when)); + } + + // create a thread to wait and execute in background + final Thread t = new Thread(() -> { + final PowerManager.WakeLock wl = WearUtil.getWakeLock(id, MAX_QUEUE_TIME + 5000); + try { + boolean running = true; + // wait for task to be due or killed + while (running) { + WearUtil.threadSleep(500); + final Task thisTask = tasks.get(id); + running = thisTask != null && !thisTask.poll(); + } + } finally { + WearUtil.releaseWakeLock(wl); + } + }); + t.setPriority(Thread.MIN_PRIORITY); + //t.setDaemon(true); + t.start(); + } + } + + public static synchronized void stackableTask(String id, long idle_for, Runnable runnable) { + int stack = 0; + while (tasks.get(id = id + "-" + stack) != null) { + stack++; + } + if (stack > 0) { + Log.d(TAG, "Task stacked to: " + id); + } + task(id, idle_for, runnable); + } + + public static void kill(final String id) { + tasks.remove(id); + } + + public static boolean waiting(final String id) { + return tasks.containsKey(id); + } + + private static class Task { + private long when; + private final Runnable what; + private final String id; + + Task(String id, long offset, Runnable what) { + this.what = what; + this.id = id; + extendTime(offset); + } + + public void extendTime(long offset) { + this.when = WearUtil.timestamp() + offset; + } + + public boolean poll() { + final long till = WearUtil.msTill(when); + if (till < 1) { + if (d) Log.d(TAG, "Executing task! " + this.id); + tasks.remove(this.id); // early remove to allow overlapping scheduling + what.run(); + return true; + } else if (till > MAX_QUEUE_TIME) { + Log.wtf(TAG, "Task: " + this.id + " In queue too long: " + till); + tasks.remove(this.id); + return true; + } + return false; + } + + } + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java new file mode 100644 index 0000000000..805aa5aafa --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java @@ -0,0 +1,94 @@ +package info.nightscout.androidaps.interaction.utils; + +import android.content.SharedPreferences; +import android.util.Base64; + +import com.google.android.gms.wearable.DataMap; + +import java.util.Set; + +import info.nightscout.androidaps.aaps; + +/** + * Created by dlvoy on 2019-11-12 + */ +public class Persistence { + + final SharedPreferences preferences; + public static final String COMPLICATION_PROVIDER_PREFERENCES_FILE_KEY = + "info.nightscout.androidaps.complications.COMPLICATION_PROVIDER_PREFERENCES_FILE_KEY"; + + public Persistence() { + preferences = aaps.getAppContext().getSharedPreferences(COMPLICATION_PROVIDER_PREFERENCES_FILE_KEY, 0); + } + + public DataMap getDataMap(String key) { + if (preferences.contains("raw_data")) { + final String rawB64Data = preferences.getString(key, null); + byte[] rawData = Base64.decode(rawB64Data, Base64.DEFAULT); + try { + DataMap dataMap = DataMap.fromByteArray(rawData); + return dataMap; + } catch (IllegalArgumentException ex) { + + } + } + return null; + } + + public void putDataMap(String key, DataMap dataMap) { + preferences.edit().putString(key, Base64.encodeToString(dataMap.toByteArray(), Base64.DEFAULT)).apply(); + } + + public String getString(String key, String defaultValue) { + return preferences.getString(key, defaultValue); + } + + public void putString(String key, String value) { + preferences.edit().putString(key, value).apply(); + } + + public boolean getBoolean(String key, boolean defaultValue) { + return preferences.getBoolean(key, defaultValue); + } + + public void putBoolean(String key, boolean value) { + preferences.edit().putBoolean(key, value).apply(); + } + + public long whenDataUpdated() { + return preferences.getLong("data_updated_at", 0); + } + + private void markDataUpdated() { + preferences.edit().putLong("data_updated_at", WearUtil.timestamp()).apply(); + } + + public Set getSetOf(String key) { + return WearUtil.explodeSet(getString(key, ""), "|"); + } + + public void addToSet(String key, String value) { + final Set set = WearUtil.explodeSet(getString(key, ""), "|"); + set.add(value); + putString(key, WearUtil.joinSet(set, "|")); + } + + public void removeFromSet(String key, String value) { + final Set set = WearUtil.explodeSet(getString(key, ""), "|"); + set.remove(value); + putString(key, WearUtil.joinSet(set, "|")); + } + + public static void storeDataMap(String key, DataMap dataMap) { + Persistence p = new Persistence(); + p.putDataMap(key, dataMap); + p.markDataUpdated(); + } + + public static Set setOf(String key) { + Persistence p = new Persistence(); + return p.getSetOf(key); + } + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java new file mode 100644 index 0000000000..5202daf8e4 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java @@ -0,0 +1,130 @@ +package info.nightscout.androidaps.interaction.utils; + +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper to minimise various floating point values, with or without unit, to fit into specified + * and limited size, scarifying precision (rounding up) and extra characters like leading zero, + * following zero(s) in fractional part, extra plus sign etc. + * + * Created by dlvoy on 2019-11-12 + */ +public class SmallestDoubleString { + + private String sign = ""; + private String decimal = ""; + private String separator = ""; + private String fractional = ""; + private String extra = ""; + private String units = ""; + + private final Units withUnits; + + public enum Units { + SKIP, + USE + } + + private static Pattern pattern = Pattern.compile("^([+-]?)([0-9]*)([,.]?)([0-9]*)(\\([^)]*\\))?(.*?)$", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE ); + + public SmallestDoubleString(String inputString) { + this(inputString, Units.SKIP); + } + + public SmallestDoubleString(String inputString, Units withUnits) { + Matcher matcher = pattern.matcher(inputString); + matcher.matches(); + + sign = matcher.group(1); + decimal = matcher.group(2); + separator = matcher.group(3); + fractional = matcher.group(4); + units = matcher.group(6); + + if (fractional == null || fractional.length() == 0) { + separator = ""; + fractional = ""; + } + if (decimal == null || decimal.length() == 0) { + decimal = ""; + } + if (separator == null || separator.length() == 0) { + separator = ""; + } + if (sign == null || sign.length() == 0) { + sign = ""; + } + + final String extraCandidate = matcher.group(5); + if (extraCandidate != null && extraCandidate.length() > 2) { + extra = extraCandidate.substring(1, extraCandidate.length()-1); + } + + if (units != null) { + units = units.trim(); + } + + this.withUnits = withUnits; + } + + public String minimise(int maxSize) { + if (Integer.parseInt("0"+fractional) == 0) { + separator = ""; + fractional = ""; + } + if (Integer.parseInt("0"+decimal) == 0 && (fractional.length() >0)) { + decimal = ""; + } + if (currentLen() <= maxSize) + return toString(); + + if (sign.equals("+")) { + sign = ""; + } + if (currentLen() <= maxSize) { + return toString(); + } + + while ((fractional.length() > 1)&&(fractional.charAt(fractional.length()-1) == '0')) { + fractional = fractional.substring(0, fractional.length()-1); + } + if (currentLen() <= maxSize) { + return toString(); + } + + if ((fractional.length() > 0)&&(decimal.length() > 0)) { + int remainingForFraction = maxSize-currentLen()+fractional.length(); + String formatCandidate = "#"; + if (remainingForFraction>=1) { + formatCandidate = "#."+("#######".substring(0, remainingForFraction)); + } + DecimalFormat df = new DecimalFormat(formatCandidate); + df.setRoundingMode(RoundingMode.HALF_UP); + + return sign + df.format(Double.parseDouble(decimal+"."+fractional)).replace(".", separator) + + ((withUnits == Units.USE) ? units : ""); + } + return toString(); + } + + private int currentLen() { + return sign.length() + decimal.length() + separator.length() + fractional.length() + + ((withUnits == Units.USE) ? units.length() : 0); + } + + public String toString() { + return sign+decimal+separator+fractional + + ((withUnits == Units.USE) ? units : ""); + } + + public String getExtra() { + return extra; + } + + public String getUnits() { return units; } + + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java index ca63749888..2a400b4fa0 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java @@ -1,19 +1,127 @@ package info.nightscout.androidaps.interaction.utils; -import java.time.LocalDateTime; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.util.Log; + import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.data.DisplayRawData; /** * Created by andy on 3/5/19. + * Adapted by dlvoy on 2019-11-06 using code from jamorham JoH class */ public class WearUtil { + private final static boolean debug_wakelocks = false; + private static final Map rateLimits = new HashMap(); + private static final String TAG = WearUtil.class.getName(); + + //============================================================================================== + // Time related util methods + //============================================================================================== public static String dateTimeText(long timeInMs) { Date d = new Date(timeInMs); return "" + d.getDay() + "." + d.getMonth() + "." + d.getYear() + " " + d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds(); } + public static long timestamp() { + return System.currentTimeMillis(); + } + public static long msSince(long when) { + return (timestamp() - when); + } + + public static long msTill(long when) { + return (when - timestamp()); + } + + //============================================================================================== + // Thread and power management utils + //============================================================================================== + + // return true if below rate limit + public static synchronized boolean rateLimit(String name, int seconds) { + // check if over limit + if ((rateLimits.containsKey(name)) && (timestamp() - rateLimits.get(name) < (seconds * 1000))) { + Log.d(TAG, name + " rate limited: " + seconds + " seconds"); + return false; + } + // not over limit + rateLimits.put(name, timestamp()); + return true; + } + + public static PowerManager.WakeLock getWakeLock(final String name, int millis) { + final PowerManager pm = (PowerManager) aaps.getAppContext().getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "AAPS::"+name); + wl.acquire(millis); + if (debug_wakelocks) Log.d(TAG, "getWakeLock: " + name + " " + wl.toString()); + return wl; + } + + public static void releaseWakeLock(PowerManager.WakeLock wl) { + if (debug_wakelocks) Log.d(TAG, "releaseWakeLock: " + wl.toString()); + if (wl == null) return; + if (wl.isHeld()) wl.release(); + } + + public static void startActivity(Class c) { + aaps.getAppContext().startActivity(getStartActivityIntent(c)); + } + + + public static Intent getStartActivityIntent(Class c) { + return new Intent(aaps.getAppContext(), c).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + + + public static void threadSleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + // + } + } + + public static String joinSet(Set set, String separator) { + StringBuilder sb = new StringBuilder(); + int i = 0; + for (String item : set) { + final String itemToAdd = item.trim(); + if (itemToAdd.length() > 0) { + if (i > 0) { + sb.append(separator); + } + i++; + sb.append(itemToAdd); + } + } + return sb.toString(); + } + + public static Set explodeSet(String joined, String separator) { + // special RegEx literal \\Q starts sequence we escape, \\E ends is + // we use it to escape separator for use in RegEx + String[] items = joined.split("\\Q"+separator+"\\E"); + Set set = new HashSet<>(); + for (String item : items) { + final String itemToAdd = item.trim(); + if (itemToAdd.length() > 0) { + set.add(itemToAdd); + } + } + return set; + } } diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java index 229eb64f1b..222f1eda6e 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java @@ -10,7 +10,6 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; -import android.os.Bundle; import android.os.PowerManager; import android.preference.PreferenceManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -33,20 +32,18 @@ import com.ustwo.clockwise.common.WatchFaceTime; import com.ustwo.clockwise.common.WatchShape; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; -import info.nightscout.androidaps.data.BasalWatchData; -import info.nightscout.androidaps.data.BgWatchData; -import info.nightscout.androidaps.data.BolusWatchData; +import info.nightscout.androidaps.complications.BaseComplicationProviderService; +import info.nightscout.androidaps.data.DisplayRawData; import info.nightscout.androidaps.data.ListenerService; import info.nightscout.androidaps.R; -import info.nightscout.androidaps.data.TempWatchData; import lecho.lib.hellocharts.view.LineChartView; /** * Created by emmablack on 12/29/14. * Updated by andrew-warrington on 02-Jan-2018. + * Refactored by dlvoy on 2019-11-2019 */ public abstract class BaseWatchFace extends WatchFace implements SharedPreferences.OnSharedPreferenceChangeListener { @@ -54,13 +51,10 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen public static final long[] vibratePattern = {0,400,300,400,300,400}; public TextView mTime, mSgv, mDirection, mTimestamp, mUploaderBattery, mRigBattery, mDelta, mAvgDelta, mStatus, mBasalRate, mIOB1, mIOB2, mCOB1, mCOB2, mBgi, mLoop, mDay, mMonth, isAAPSv2, mHighLight, mLowLight; public ImageView mGlucoseDial, mDeltaGauge, mHourHand, mMinuteHand; - public long datetime; public RelativeLayout mRelativeLayout; public LinearLayout mLinearLayout, mLinearLayout2, mDate, mChartTap, mMainMenuTap; - public long sgvLevel = 0; public int ageLevel = 1; public int loopLevel = 1; - public int batteryLevel = 1; public int highColor = Color.YELLOW; public int lowColor = Color.RED; public int midColor = Color.WHITE; @@ -74,11 +68,9 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen public int pointSize = 2; public BgGraphBuilder bgGraphBuilder; public LineChartView chart; - public ArrayList bgDataList = new ArrayList<>(); - public ArrayList tempWatchDataList = new ArrayList<>(); - public ArrayList basalWatchDataList = new ArrayList<>(); - public ArrayList bolusWatchDataList = new ArrayList<>(); - public ArrayList predictionList = new ArrayList<>(); + + + public DisplayRawData rawData = new DisplayRawData(); public PowerManager.WakeLock wakeLock; // related endTime manual layout @@ -90,26 +82,9 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen protected SharedPreferences sharedPrefs; - public boolean detailedIOB = false; - public boolean showBGI = false; public boolean forceSquareCanvas = false; //set to true by the Steampunk watch face. - public long openApsStatus; - public String externalStatusString = "no status"; - public String sSgv = "---"; - public String sDirection = "--"; - public String sUploaderBattery = "--"; - public String sRigBattery = "--"; - public String sDelta = "--"; - public String sAvgDelta = "--"; - public String sBasalRate = "-.--U/h"; - public String sIOB1 = "IOB"; - public String sIOB2 = "-.--"; - public String sCOB1 = "Carb"; - public String sCOB2 = "--g"; - public String sBgi = "--"; public String sMinute = "0"; public String sHour = "0"; - public String sUnits = "-"; @Override public void onCreate() { @@ -126,6 +101,8 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); sharedPrefs.registerOnSharedPreferenceChangeListener(this); + + BaseComplicationProviderService.turnOff(); } @Override @@ -197,11 +174,11 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } public double timeSince() { - return System.currentTimeMillis() - datetime; + return System.currentTimeMillis() - rawData.datetime; } public String readingAge(boolean shortString) { - if (datetime == 0) { return shortString?"--'":"-- Minute ago"; } + if (rawData.datetime == 0) { return shortString?"--'":"-- Minute ago"; } int minutesAgo = (int) Math.floor(timeSince()/(1000*60)); if (minutesAgo == 1) { return minutesAgo + (shortString?"'":" Minute ago"); @@ -266,50 +243,20 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen @Override public void onReceive(Context context, Intent intent) { - Bundle bundle = intent.getBundleExtra("data"); - if (layoutSet && bundle != null) { - DataMap dataMap = DataMap.fromBundle(bundle); - wakeLock.acquire(50); - sgvLevel = dataMap.getLong("sgvLevel"); - datetime = dataMap.getLong("timestamp"); - sSgv = dataMap.getString("sgvString"); - sDirection = dataMap.getString("slopeArrow"); - sDelta = dataMap.getString("delta"); - sAvgDelta = dataMap.getString("avgDelta"); - sUnits = dataMap.getString("glucoseUnits"); - if (chart != null) { - addToWatchSet(dataMap); + if (layoutSet) { + final DataMap dataMap = rawData.updateDataFromMessage(intent, wakeLock); + if (chart != null && dataMap != null) { + rawData.addToWatchSet(dataMap); setupCharts(); } - } - - bundle = intent.getBundleExtra("status"); - if (layoutSet && bundle != null) { - DataMap dataMap = DataMap.fromBundle(bundle); - wakeLock.acquire(50); - sBasalRate = dataMap.getString("currentBasal"); - sUploaderBattery = dataMap.getString("battery"); - sRigBattery = dataMap.getString("rigBattery"); - detailedIOB = dataMap.getBoolean("detailedIob"); - sIOB1 = dataMap.getString("iobSum") + "U"; - sIOB2 = dataMap.getString("iobDetail"); - sCOB1 = "Carb"; - sCOB2 = dataMap.getString("cob"); - sBgi = dataMap.getString("bgi"); - showBGI = dataMap.getBoolean("showBgi"); - externalStatusString = dataMap.getString("externalStatusString"); - batteryLevel = dataMap.getInt("batteryLevel"); - openApsStatus = dataMap.getLong("openApsStatus"); + rawData.updateStatusFromMessage(intent, wakeLock); } setDataFields(); setColor(); - bundle = intent.getBundleExtra("basals"); - if (layoutSet && bundle != null) { - DataMap dataMap = DataMap.fromBundle(bundle); - wakeLock.acquire(500); - loadBasalsAndTemps(dataMap); + if (layoutSet) { + rawData.updateBasalsFromMessage(intent, wakeLock); } mRelativeLayout.measure(specW, specH); @@ -328,7 +275,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mSgv != null) { if (sharedPrefs.getBoolean("showBG", true)) { - mSgv.setText(sSgv); + mSgv.setText(rawData.sSgv); mSgv.setVisibility(View.VISIBLE); } else { //leave the textview there but invisible, as a height holder for the empty space above the white line @@ -341,7 +288,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mDirection != null) { if (sharedPrefs.getBoolean("show_direction", true)) { - mDirection.setText(sDirection); + mDirection.setText(rawData.sDirection); mDirection.setVisibility(View.VISIBLE); } else { mDirection.setVisibility(View.GONE); @@ -350,7 +297,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mDelta != null) { if (sharedPrefs.getBoolean("showDelta", true)) { - mDelta.setText(sDelta); + mDelta.setText(rawData.sDelta); mDelta.setVisibility(View.VISIBLE); } else { mDelta.setVisibility(View.GONE); @@ -359,7 +306,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mAvgDelta != null) { if (sharedPrefs.getBoolean("showAvgDelta", true)) { - mAvgDelta.setText(sAvgDelta); + mAvgDelta.setText(rawData.sAvgDelta); mAvgDelta.setVisibility(View.VISIBLE); } else { mAvgDelta.setVisibility(View.GONE); @@ -367,7 +314,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } if (mCOB1 != null && mCOB2 != null) { - mCOB2.setText(sCOB2); + mCOB2.setText(rawData.sCOB2); if (sharedPrefs.getBoolean("show_cob", true)) { mCOB1.setVisibility(View.VISIBLE); mCOB2.setVisibility(View.VISIBLE); @@ -377,7 +324,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } //deal with cases where there is only the value shown for COB, and not the label } else if (mCOB2 != null) { - mCOB2.setText(sCOB2); + mCOB2.setText(rawData.sCOB2); if (sharedPrefs.getBoolean("show_cob", true)) { mCOB2.setVisibility(View.VISIBLE); } else { @@ -389,12 +336,12 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (sharedPrefs.getBoolean("show_iob", true)) { mIOB1.setVisibility(View.VISIBLE); mIOB2.setVisibility(View.VISIBLE); - if (detailedIOB) { - mIOB1.setText(sIOB1); - mIOB2.setText(sIOB2); + if (rawData.detailedIOB) { + mIOB1.setText(rawData.sIOB1); + mIOB2.setText(rawData.sIOB2); } else { mIOB1.setText("IOB"); - mIOB2.setText(sIOB1); + mIOB2.setText(rawData.sIOB1); } } else { mIOB1.setVisibility(View.GONE); @@ -404,10 +351,10 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } else if (mIOB2 != null) { if (sharedPrefs.getBoolean("show_iob", true)) { mIOB2.setVisibility(View.VISIBLE); - if (detailedIOB) { - mIOB2.setText(sIOB2); + if (rawData.detailedIOB) { + mIOB2.setText(rawData.sIOB2); } else { - mIOB2.setText(sIOB1); + mIOB2.setText(rawData.sIOB1); } } else { mIOB2.setText(""); @@ -434,13 +381,13 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mUploaderBattery != null) { if (sharedPrefs.getBoolean("show_uploader_battery", true)) { if (isAAPSv2 != null) { - mUploaderBattery.setText(sUploaderBattery + "%"); + mUploaderBattery.setText(rawData.sUploaderBattery + "%"); mUploaderBattery.setVisibility(View.VISIBLE); } else { if (sharedPrefs.getBoolean("showExternalStatus", true)) { - mUploaderBattery.setText("U: " + sUploaderBattery + "%"); + mUploaderBattery.setText("U: " + rawData.sUploaderBattery + "%"); } else { - mUploaderBattery.setText("Uploader: " + sUploaderBattery + "%"); + mUploaderBattery.setText("Uploader: " + rawData.sUploaderBattery + "%"); } } } else { @@ -450,7 +397,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mRigBattery != null) { if (sharedPrefs.getBoolean("show_rig_battery", false)) { - mRigBattery.setText(sRigBattery); + mRigBattery.setText(rawData.sRigBattery); mRigBattery.setVisibility(View.VISIBLE); } else { mRigBattery.setVisibility(View.GONE); @@ -459,7 +406,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mBasalRate != null) { if (sharedPrefs.getBoolean("show_temp_basal", true)) { - mBasalRate.setText(sBasalRate); + mBasalRate.setText(rawData.sBasalRate); mBasalRate.setVisibility(View.VISIBLE); } else { mBasalRate.setVisibility(View.GONE); @@ -467,8 +414,8 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } if (mBgi != null) { - if (showBGI) { - mBgi.setText(sBgi); + if (rawData.showBGI) { + mBgi.setText(rawData.sBgi); mBgi.setVisibility(View.VISIBLE); } else { mBgi.setVisibility(View.GONE); @@ -477,7 +424,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mStatus != null) { if (sharedPrefs.getBoolean("showExternalStatus", true)) { - mStatus.setText(externalStatusString); + mStatus.setText(rawData.externalStatusString); mStatus.setVisibility(View.VISIBLE); } else { mStatus.setVisibility(View.GONE); @@ -487,8 +434,8 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen if (mLoop != null) { if (sharedPrefs.getBoolean("showExternalStatus", true)) { mLoop.setVisibility(View.VISIBLE); - if (openApsStatus != -1) { - int mins = (int) ((System.currentTimeMillis() - openApsStatus) / 1000 / 60); + if (rawData.openApsStatus != -1) { + int mins = (int) ((System.currentTimeMillis() - rawData.openApsStatus) / 1000 / 60); mLoop.setText(mins + "'"); if (mins > 14) { loopLevel = 0; @@ -594,50 +541,13 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } } - public void addToWatchSet(DataMap dataMap) { - - ArrayList entries = dataMap.getDataMapArrayList("entries"); - if (entries != null) { - bgDataList = new ArrayList(); - for (DataMap entry : entries) { - double sgv = entry.getDouble("sgvDouble"); - double high = entry.getDouble("high"); - double low = entry.getDouble("low"); - long timestamp = entry.getLong("timestamp"); - int color = entry.getInt("color", 0); - bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); - } - } else { - double sgv = dataMap.getDouble("sgvDouble"); - double high = dataMap.getDouble("high"); - double low = dataMap.getDouble("low"); - long timestamp = dataMap.getLong("timestamp"); - int color = dataMap.getInt("color", 0); - - final int size = bgDataList.size(); - if (size > 0) { - if (bgDataList.get(size - 1).timestamp == timestamp) - return; // Ignore duplicates. - } - - bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); - } - - for (int i = 0; i < bgDataList.size(); i++) { - if (bgDataList.get(i).timestamp < (System.currentTimeMillis() - (1000 * 60 * 60 * 5))) { - bgDataList.remove(i); //Get rid of anything more than 5 hours old - break; - } - } - } - public void setupCharts() { - if(bgDataList.size() > 0) { //Dont crash things just because we dont have values, people dont like crashy things + if(rawData.bgDataList.size() > 0) { //Dont crash things just because we dont have values, people dont like crashy things int timeframe = Integer.parseInt(sharedPrefs.getString("chart_timeframe", "3")); if (lowResMode) { - bgGraphBuilder = new BgGraphBuilder(getApplicationContext(), bgDataList, predictionList, tempWatchDataList, basalWatchDataList, bolusWatchDataList, pointSize, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe); + bgGraphBuilder = new BgGraphBuilder(getApplicationContext(), rawData, pointSize, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe); } else { - bgGraphBuilder = new BgGraphBuilder(getApplicationContext(), bgDataList,predictionList, tempWatchDataList, basalWatchDataList, bolusWatchDataList, pointSize, highColor, lowColor, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe); + bgGraphBuilder = new BgGraphBuilder(getApplicationContext(), rawData, pointSize, highColor, lowColor, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe); } chart.setLineChartData(bgGraphBuilder.lineData()); @@ -646,54 +556,4 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen } } - private void loadBasalsAndTemps(DataMap dataMap) { - ArrayList temps = dataMap.getDataMapArrayList("temps"); - if (temps != null) { - tempWatchDataList = new ArrayList<>(); - for (DataMap temp : temps) { - TempWatchData twd = new TempWatchData(); - twd.startTime = temp.getLong("starttime"); - twd.startBasal = temp.getDouble("startBasal"); - twd.endTime = temp.getLong("endtime"); - twd.endBasal = temp.getDouble("endbasal"); - twd.amount = temp.getDouble("amount"); - tempWatchDataList.add(twd); - } - } - ArrayList basals = dataMap.getDataMapArrayList("basals"); - if (basals != null) { - basalWatchDataList = new ArrayList<>(); - for (DataMap basal : basals) { - BasalWatchData bwd = new BasalWatchData(); - bwd.startTime = basal.getLong("starttime"); - bwd.endTime = basal.getLong("endtime"); - bwd.amount = basal.getDouble("amount"); - basalWatchDataList.add(bwd); - } - } - ArrayList boluses = dataMap.getDataMapArrayList("boluses"); - if (boluses != null) { - bolusWatchDataList = new ArrayList<>(); - for (DataMap bolus : boluses) { - BolusWatchData bwd = new BolusWatchData(); - bwd.date = bolus.getLong("date"); - bwd.bolus = bolus.getDouble("bolus"); - bwd.carbs = bolus.getDouble("carbs"); - bwd.isSMB = bolus.getBoolean("isSMB"); - bwd.isValid = bolus.getBoolean("isValid"); - bolusWatchDataList.add(bwd); - } - } - ArrayList predictions = dataMap.getDataMapArrayList("predictions"); - if (boluses != null) { - predictionList = new ArrayList<>(); - for (DataMap prediction : predictions) { - BgWatchData bwd = new BgWatchData(); - bwd.timestamp = prediction.getLong("timestamp"); - bwd.sgv = prediction.getDouble("sgv"); - bwd.color = prediction.getInt("color"); - predictionList.add(bwd); - } - } - } } diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java index 4a401489d9..b40e15cfd4 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java @@ -18,6 +18,7 @@ import java.util.TimeZone; import info.nightscout.androidaps.data.BasalWatchData; import info.nightscout.androidaps.data.BgWatchData; import info.nightscout.androidaps.data.BolusWatchData; +import info.nightscout.androidaps.data.DisplayRawData; import info.nightscout.androidaps.data.TempWatchData; import lecho.lib.hellocharts.model.Axis; import lecho.lib.hellocharts.model.AxisValue; @@ -115,6 +116,42 @@ public class BgGraphBuilder { this.end_time = (predictionEndTime>end_time)?predictionEndTime:end_time; } + public BgGraphBuilder(Context context, DisplayRawData raw, int aPointSize, int aHighColor, int aLowColor, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int carbsColor, int timespan) { + this(context, + raw.bgDataList, + raw.predictionList, + raw.tempWatchDataList, + raw.basalWatchDataList, + raw.bolusWatchDataList, + aPointSize, + aHighColor, + aLowColor, + aMidColor, + gridColour, + basalBackgroundColor, + basalCenterColor, + bolusInvalidColor, + carbsColor, + timespan); + } + + public BgGraphBuilder(Context context, DisplayRawData raw, int aPointSize, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int carbsColor, int timespan) { + this(context, + raw.bgDataList, + raw.predictionList, + raw.tempWatchDataList, + raw.basalWatchDataList, + raw.bolusWatchDataList, + aPointSize, + aMidColor, + gridColour, + basalBackgroundColor, + basalCenterColor, + bolusInvalidColor, + carbsColor, + timespan); + } + public LineChartData lineData() { LineChartData lineData = new LineChartData(defaultLines()); lineData.setAxisYLeft(yAxis()); diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Cockpit.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Cockpit.java index b949e3832a..31d293929a 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Cockpit.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Cockpit.java @@ -48,13 +48,13 @@ public class Cockpit extends BaseWatchFace { setTextSizes(); if (mHighLight != null && mLowLight != null) { - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mHighLight.setBackgroundResource(R.drawable.airplane_led_yellow_lit); mLowLight.setBackgroundResource(R.drawable.airplane_led_grey_unlit); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mHighLight.setBackgroundResource(R.drawable.airplane_led_grey_unlit); mLowLight.setBackgroundResource(R.drawable.airplane_led_grey_unlit); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mHighLight.setBackgroundResource(R.drawable.airplane_led_grey_unlit); mLowLight.setBackgroundResource(R.drawable.airplane_led_red_lit); } @@ -82,7 +82,7 @@ public class Cockpit extends BaseWatchFace { protected void setTextSizes() { if (mIOB2 != null) { - if (detailedIOB) { + if (rawData.detailedIOB) { if (bIsRound) { mIOB2.setTextSize(10); } else { diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home.java index 430a3feb47..209e6214e7 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home.java @@ -68,15 +68,15 @@ public class Home extends BaseWatchFace { mLinearLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_statusView)); mTime.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_mTime)); mRelativeLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_background)); - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); @@ -88,7 +88,7 @@ public class Home extends BaseWatchFace { mTimestamp.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_TimestampOld)); } - if (batteryLevel == 1) { + if (rawData.batteryLevel == 1) { mUploaderBattery.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_uploaderBattery)); } else { mUploaderBattery.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_uploaderBatteryEmpty)); @@ -133,15 +133,15 @@ public class Home extends BaseWatchFace { if (getCurrentWatchMode() == WatchMode.INTERACTIVE) { mLinearLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.light_stripe_background)); mRelativeLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.light_background)); - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); @@ -153,7 +153,7 @@ public class Home extends BaseWatchFace { mTimestamp.setTextColor(Color.RED); } - if (batteryLevel == 1) { + if (rawData.batteryLevel == 1) { mUploaderBattery.setTextColor(Color.WHITE); } else { mUploaderBattery.setTextColor(Color.RED); diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home2.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home2.java index 41c9fd3029..fbc69f3c1a 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home2.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Home2.java @@ -78,13 +78,13 @@ public class Home2 extends BaseWatchFace { setTextSizes(); - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); } @@ -95,7 +95,7 @@ public class Home2 extends BaseWatchFace { mTimestamp.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_TimestampOld)); } - if (batteryLevel == 1) { + if (rawData.batteryLevel == 1) { mUploaderBattery.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_uploaderBattery)); } else { mUploaderBattery.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_uploaderBatteryEmpty)); @@ -176,13 +176,13 @@ public class Home2 extends BaseWatchFace { setTextSizes(); - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); } @@ -193,7 +193,7 @@ public class Home2 extends BaseWatchFace { mTimestamp.setTextColor(Color.RED); } - if (batteryLevel == 1) { + if (rawData.batteryLevel == 1) { mUploaderBattery.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); } else { mUploaderBattery.setTextColor(Color.RED); @@ -229,7 +229,7 @@ public class Home2 extends BaseWatchFace { if (mIOB1 != null && mIOB2 != null) { - if (detailedIOB) { + if (rawData.detailedIOB) { mIOB1.setTextSize(14); mIOB2.setTextSize(10); } else { diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/LargeHome.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/LargeHome.java index ffb8dce3ad..75b7d995dd 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/LargeHome.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/LargeHome.java @@ -52,15 +52,15 @@ public class LargeHome extends BaseWatchFace { mLinearLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_mLinearLayout)); mTime.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_mTime)); mRelativeLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_background)); - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_highColor)); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_midColor)); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_lowColor)); @@ -72,7 +72,7 @@ public class LargeHome extends BaseWatchFace { mTimestamp.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_TimestampOld)); } - if (batteryLevel == 1) { + if (rawData.batteryLevel == 1) { mUploaderBattery.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_uploaderBattery)); } else { mUploaderBattery.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.dark_uploaderBatteryEmpty)); @@ -86,15 +86,15 @@ public class LargeHome extends BaseWatchFace { if (getCurrentWatchMode() == WatchMode.INTERACTIVE) { mLinearLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.light_stripe_background)); mRelativeLayout.setBackgroundColor(ContextCompat.getColor(getApplicationContext(), R.color.light_background)); - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_highColor)); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_midColor)); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mSgv.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); mDelta.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); mDirection.setTextColor(ContextCompat.getColor(getApplicationContext(), R.color.light_lowColor)); @@ -106,7 +106,7 @@ public class LargeHome extends BaseWatchFace { mTimestamp.setTextColor(Color.RED); } - if (batteryLevel == 1) { + if (rawData.batteryLevel == 1) { mUploaderBattery.setTextColor(Color.WHITE); } else { mUploaderBattery.setTextColor(Color.RED); @@ -116,15 +116,15 @@ public class LargeHome extends BaseWatchFace { } else { mRelativeLayout.setBackgroundColor(Color.BLACK); mLinearLayout.setBackgroundColor(Color.LTGRAY); - if (sgvLevel == 1) { + if (rawData.sgvLevel == 1) { mSgv.setTextColor(Color.YELLOW); mDirection.setTextColor(Color.YELLOW); mDelta.setTextColor(Color.YELLOW); - } else if (sgvLevel == 0) { + } else if (rawData.sgvLevel == 0) { mSgv.setTextColor(Color.WHITE); mDirection.setTextColor(Color.WHITE); mDelta.setTextColor(Color.WHITE); - } else if (sgvLevel == -1) { + } else if (rawData.sgvLevel == -1) { mSgv.setTextColor(Color.RED); mDirection.setTextColor(Color.RED); mDelta.setTextColor(Color.RED); diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Steampunk.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Steampunk.java index c42aff1518..a600290a04 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/Steampunk.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/Steampunk.java @@ -83,24 +83,24 @@ public class Steampunk extends BaseWatchFace { } } - if (!sSgv.equals("---")) { + if (!rawData.sSgv.equals("---")) { float rotationAngle = 0f; //by default, show ? on the dial (? is at 0 degrees on the dial) - if (!sUnits.equals("-")) { + if (!rawData.sUnits.equals("-")) { //ensure the glucose dial is the correct units - if (sUnits.equals("mmol")) { + if (rawData.sUnits.equals("mmol")) { mGlucoseDial.setImageResource(R.drawable.steampunk_dial_mmol); } else { mGlucoseDial.setImageResource(R.drawable.steampunk_dial_mgdl); } //convert the Sgv to degrees of rotation - if (sUnits.equals("mmol")) { - rotationAngle = Float.valueOf(sSgv) * 18f; //convert to mg/dL, which is equivalent to degrees + if (rawData.sUnits.equals("mmol")) { + rotationAngle = Float.valueOf(rawData.sSgv) * 18f; //convert to mg/dL, which is equivalent to degrees } else { - rotationAngle = Float.valueOf(sSgv); //if glucose a value is received, use it to determine the amount of rotation of the dial. + rotationAngle = Float.valueOf(rawData.sSgv); //if glucose a value is received, use it to determine the amount of rotation of the dial. } } @@ -122,36 +122,36 @@ public class Steampunk extends BaseWatchFace { //set the delta gauge and rotate the delta pointer float deltaIsNegative = 1f; //by default go clockwise - if (!sAvgDelta.equals("--")) { //if a legitimate delta value is received, then... - if (sAvgDelta.substring(0,1).equals("-")) deltaIsNegative = -1f; //if the delta is negative, go counter-clockwise + if (!rawData.sAvgDelta.equals("--")) { //if a legitimate delta value is received, then... + if (rawData.sAvgDelta.substring(0,1).equals("-")) deltaIsNegative = -1f; //if the delta is negative, go counter-clockwise //ensure the delta gauge is the right units and granularity - if (!sUnits.equals("-")) { - if (sUnits.equals("mmol")) { + if (!rawData.sUnits.equals("-")) { + if (rawData.sUnits.equals("mmol")) { if (sharedPrefs.getString("delta_granularity", "2").equals("1")) { //low mLinearLayout.setBackgroundResource(R.drawable.steampunk_gauge_mmol_10); - deltaRotationAngle = (Float.valueOf(sAvgDelta.substring(1)) * 30f); //get rid of the sign so it can be converted to float. + deltaRotationAngle = (Float.valueOf(rawData.sAvgDelta.substring(1)) * 30f); //get rid of the sign so it can be converted to float. } if (sharedPrefs.getString("delta_granularity", "2").equals("2")) { //medium mLinearLayout.setBackgroundResource(R.drawable.steampunk_gauge_mmol_05); - deltaRotationAngle = (Float.valueOf(sAvgDelta.substring(1)) * 60f); //get rid of the sign so it can be converted to float. + deltaRotationAngle = (Float.valueOf(rawData.sAvgDelta.substring(1)) * 60f); //get rid of the sign so it can be converted to float. } if (sharedPrefs.getString("delta_granularity", "2").equals("3")) { //high mLinearLayout.setBackgroundResource(R.drawable.steampunk_gauge_mmol_03); - deltaRotationAngle = (Float.valueOf(sAvgDelta.substring(1)) * 100f); //get rid of the sign so it can be converted to float. + deltaRotationAngle = (Float.valueOf(rawData.sAvgDelta.substring(1)) * 100f); //get rid of the sign so it can be converted to float. } } else { if (sharedPrefs.getString("delta_granularity", "2").equals("1")) { //low mLinearLayout.setBackgroundResource(R.drawable.steampunk_gauge_mgdl_20); - deltaRotationAngle = (Float.valueOf(sAvgDelta.substring(1)) * 1.5f); //get rid of the sign so it can be converted to float. + deltaRotationAngle = (Float.valueOf(rawData.sAvgDelta.substring(1)) * 1.5f); //get rid of the sign so it can be converted to float. } if (sharedPrefs.getString("delta_granularity", "2").equals("2")) { //medium mLinearLayout.setBackgroundResource(R.drawable.steampunk_gauge_mgdl_10); - deltaRotationAngle = (Float.valueOf(sAvgDelta.substring(1)) * 3f); //get rid of the sign so it can be converted to float. + deltaRotationAngle = (Float.valueOf(rawData.sAvgDelta.substring(1)) * 3f); //get rid of the sign so it can be converted to float. } if (sharedPrefs.getString("delta_granularity", "2").equals("3")) { //high mLinearLayout.setBackgroundResource(R.drawable.steampunk_gauge_mgdl_5); - deltaRotationAngle = (Float.valueOf(sAvgDelta.substring(1)) * 6f); //get rid of the sign so it can be converted to float. + deltaRotationAngle = (Float.valueOf(rawData.sAvgDelta.substring(1)) * 6f); //get rid of the sign so it can be converted to float. } } } @@ -213,7 +213,7 @@ public class Steampunk extends BaseWatchFace { //top row. large font unless text too big (i.e. detailedIOB) mCOB2.setTextSize(fontLarge); mBasalRate.setTextSize(fontLarge); - if (sIOB2.length() < 7) { + if (rawData.sIOB2.length() < 7) { mIOB2.setTextSize(fontLarge); } else { mIOB2.setTextSize(fontSmall); diff --git a/wear/src/main/res/drawable/ic_aaps_dark.xml b/wear/src/main/res/drawable/ic_aaps_dark.xml new file mode 100644 index 0000000000..4126fec9d9 --- /dev/null +++ b/wear/src/main/res/drawable/ic_aaps_dark.xml @@ -0,0 +1,12 @@ + + + + diff --git a/wear/src/main/res/drawable/ic_aaps_full.xml b/wear/src/main/res/drawable/ic_aaps_full.xml new file mode 100644 index 0000000000..cf9716870c --- /dev/null +++ b/wear/src/main/res/drawable/ic_aaps_full.xml @@ -0,0 +1,12 @@ + + + + diff --git a/wear/src/main/res/drawable/ic_aaps_gray.xml b/wear/src/main/res/drawable/ic_aaps_gray.xml new file mode 100644 index 0000000000..1797ca3c9a --- /dev/null +++ b/wear/src/main/res/drawable/ic_aaps_gray.xml @@ -0,0 +1,12 @@ + + + + diff --git a/wear/src/main/res/drawable/ic_aaps_light.xml b/wear/src/main/res/drawable/ic_aaps_light.xml new file mode 100644 index 0000000000..cf9716870c --- /dev/null +++ b/wear/src/main/res/drawable/ic_aaps_light.xml @@ -0,0 +1,12 @@ + + + + diff --git a/wear/src/main/res/drawable/ic_alert.xml b/wear/src/main/res/drawable/ic_alert.xml new file mode 100644 index 0000000000..d95c322d47 --- /dev/null +++ b/wear/src/main/res/drawable/ic_alert.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_alert_burnin.xml b/wear/src/main/res/drawable/ic_alert_burnin.xml new file mode 100644 index 0000000000..6f7bd23ef0 --- /dev/null +++ b/wear/src/main/res/drawable/ic_alert_burnin.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_alert_variant_outline.xml b/wear/src/main/res/drawable/ic_battery_alert_variant_outline.xml new file mode 100644 index 0000000000..516ae49c17 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_alert_variant_outline.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless.xml new file mode 100644 index 0000000000..bd6bee5fb1 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_10.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_10.xml new file mode 100644 index 0000000000..932c4e5930 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_10.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_10_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_10_burnin.xml new file mode 100644 index 0000000000..a47508a1a9 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_10_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_20.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_20.xml new file mode 100644 index 0000000000..00af480fcd --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_20.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_20_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_20_burnin.xml new file mode 100644 index 0000000000..e08ff88448 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_20_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_30.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_30.xml new file mode 100644 index 0000000000..592d0f380c --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_30.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_30_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_30_burnin.xml new file mode 100644 index 0000000000..eb36861925 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_30_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_40.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_40.xml new file mode 100644 index 0000000000..f94f85a69b --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_40.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_40_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_40_burnin.xml new file mode 100644 index 0000000000..8980db2391 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_40_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_50.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_50.xml new file mode 100644 index 0000000000..f6a4148589 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_50.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_50_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_50_burnin.xml new file mode 100644 index 0000000000..9a9ee645dc --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_50_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_60.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_60.xml new file mode 100644 index 0000000000..29192af0ed --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_60.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_60_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_60_burnin.xml new file mode 100644 index 0000000000..d5191601a6 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_60_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_70.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_70.xml new file mode 100644 index 0000000000..02c0ae7f58 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_70.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_70_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_70_burnin.xml new file mode 100644 index 0000000000..9efc928672 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_70_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_80.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_80.xml new file mode 100644 index 0000000000..21e44f7105 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_80.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_80_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_80_burnin.xml new file mode 100644 index 0000000000..9f212fad87 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_80_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_90.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_90.xml new file mode 100644 index 0000000000..1004964eb2 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_90.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_90_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_90_burnin.xml new file mode 100644 index 0000000000..b6b371fa20 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_90_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_burnin.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_burnin.xml new file mode 100644 index 0000000000..fd11b20ac9 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_battery_charging_wireless_outline.xml b/wear/src/main/res/drawable/ic_battery_charging_wireless_outline.xml new file mode 100644 index 0000000000..5fa535e117 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_charging_wireless_outline.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_outline.xml b/wear/src/main/res/drawable/ic_battery_outline.xml new file mode 100644 index 0000000000..c066289e18 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_outline.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_unknown.xml b/wear/src/main/res/drawable/ic_battery_unknown.xml new file mode 100644 index 0000000000..0ac1d8a740 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_unknown.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/drawable/ic_battery_unknown_burnin.xml b/wear/src/main/res/drawable/ic_battery_unknown_burnin.xml new file mode 100644 index 0000000000..e6271cce70 --- /dev/null +++ b/wear/src/main/res/drawable/ic_battery_unknown_burnin.xml @@ -0,0 +1,10 @@ + + + diff --git a/wear/src/main/res/drawable/ic_br_cob_iob.xml b/wear/src/main/res/drawable/ic_br_cob_iob.xml new file mode 100644 index 0000000000..39ab3af490 --- /dev/null +++ b/wear/src/main/res/drawable/ic_br_cob_iob.xml @@ -0,0 +1,11 @@ + + + diff --git a/wear/src/main/res/drawable/ic_carbs.xml b/wear/src/main/res/drawable/ic_carbs.xml new file mode 100644 index 0000000000..a1ac753064 --- /dev/null +++ b/wear/src/main/res/drawable/ic_carbs.xml @@ -0,0 +1,11 @@ + + + diff --git a/wear/src/main/res/drawable/ic_cob_detailed.xml b/wear/src/main/res/drawable/ic_cob_detailed.xml new file mode 100644 index 0000000000..fc92be1910 --- /dev/null +++ b/wear/src/main/res/drawable/ic_cob_detailed.xml @@ -0,0 +1,11 @@ + + + diff --git a/wear/src/main/res/drawable/ic_cob_iob.xml b/wear/src/main/res/drawable/ic_cob_iob.xml new file mode 100644 index 0000000000..f029cf0d49 --- /dev/null +++ b/wear/src/main/res/drawable/ic_cob_iob.xml @@ -0,0 +1,11 @@ + + + diff --git a/wear/src/main/res/drawable/ic_ins.xml b/wear/src/main/res/drawable/ic_ins.xml new file mode 100644 index 0000000000..9fbb294190 --- /dev/null +++ b/wear/src/main/res/drawable/ic_ins.xml @@ -0,0 +1,11 @@ + + + diff --git a/wear/src/main/res/drawable/ic_ins_burnin.xml b/wear/src/main/res/drawable/ic_ins_burnin.xml new file mode 100644 index 0000000000..8a3a16c64b --- /dev/null +++ b/wear/src/main/res/drawable/ic_ins_burnin.xml @@ -0,0 +1,11 @@ + + + diff --git a/wear/src/main/res/drawable/ic_iob_detailed.xml b/wear/src/main/res/drawable/ic_iob_detailed.xml new file mode 100644 index 0000000000..95974f2fed --- /dev/null +++ b/wear/src/main/res/drawable/ic_iob_detailed.xml @@ -0,0 +1,11 @@ + + + diff --git a/wear/src/main/res/drawable/ic_sgv.xml b/wear/src/main/res/drawable/ic_sgv.xml new file mode 100644 index 0000000000..e7aefdd542 --- /dev/null +++ b/wear/src/main/res/drawable/ic_sgv.xml @@ -0,0 +1,13 @@ + + + diff --git a/wear/src/main/res/drawable/ic_sync_alert.xml b/wear/src/main/res/drawable/ic_sync_alert.xml new file mode 100644 index 0000000000..bd4ca18d5f --- /dev/null +++ b/wear/src/main/res/drawable/ic_sync_alert.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index d50cb015e6..ca24978853 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -46,6 +46,25 @@ 4 + + Default + Menu + Wizard + Bolus + eCarb + Status + None + + + + default + menu + wizard + bolus + ecarb + status + none + AAPS AAPS(Large) @@ -53,4 +72,13 @@ AAPS(NoChart) AAPS(Circle) + + No data! + Old data! + Since %1$s + Sync with AAPS! + + No data received since %1$s! Check if AAPS on the phone sends data to watch + AAPS data is %1$s old! Check your sensor, xDrip+, NS, AAPS config or other! + diff --git a/wear/src/main/res/xml/preferences.xml b/wear/src/main/res/xml/preferences.xml index 0befaddfd0..aa8a3f6ba2 100644 --- a/wear/src/main/res/xml/preferences.xml +++ b/wear/src/main/res/xml/preferences.xml @@ -209,6 +209,20 @@ android:title="Wizard Percentage" app:wear_iconOff="@drawable/settings_off" app:wear_iconOn="@drawable/settings_on"/> + + Date: Tue, 26 Nov 2019 09:14:39 +0100 Subject: [PATCH 2/7] [#2210][#728] - Code coverage config for wear module - Fixed mockito configuration for bug in 2.X --- wear/build.gradle | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/wear/build.gradle b/wear/build.gradle index bb756b2b2c..3a2277e2fe 100644 --- a/wear/build.gradle +++ b/wear/build.gradle @@ -1,4 +1,18 @@ +buildscript { + repositories { + jcenter() + } + + dependencies { + classpath 'com.dicedmelon.gradle:jacoco-android:0.1.4' + } +} apply plugin: 'com.android.application' +apply plugin: 'jacoco-android' + +jacoco { + toolVersion = "0.8.3" +} ext { wearableVersion = "2.4.0" @@ -109,7 +123,16 @@ dependencies { testImplementation "junit:junit:4.12" testImplementation "org.json:json:20140107" - testImplementation "org.mockito:mockito-core:2.8.47" + testImplementation ("org.mockito:mockito-core:2.8.47") { + exclude group: 'net.bytebuddy', module: 'byte-buddy' + exclude group: 'net.bytebuddy', module: 'byte-buddy-android' + exclude group: 'net.bytebuddy', module: 'byte-buddy-agent' + } + // to fix org.mockito:mockito-core dependency issues, fixed in mockito 3+ + testImplementation 'net.bytebuddy:byte-buddy:1.8.22' + testImplementation 'net.bytebuddy:byte-buddy-android:1.8.22' + testImplementation 'net.bytebuddy:byte-buddy-agent:1.8.22' + testImplementation "org.powermock:powermock-api-mockito2:${powermockVersion}" testImplementation "org.powermock:powermock-module-junit4-rule-agent:${powermockVersion}" testImplementation "org.powermock:powermock-module-junit4-rule:${powermockVersion}" From 20eae8eb1207edd5565d95fd8ee918e65564d515 Mon Sep 17 00:00:00 2001 From: dlvoy Date: Tue, 26 Nov 2019 09:16:46 +0100 Subject: [PATCH 3/7] [#728] Fix for complications update period (from @jotomo review) --- wear/src/main/AndroidManifest.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 2f89781293..1097be80be 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -246,7 +246,7 @@ android:value="LONG_TEXT" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> + android:value="300" /> Date: Tue, 26 Nov 2019 09:23:26 +0100 Subject: [PATCH 4/7] [#728] Post review / test related minor improvements: - improving mockability when Android classes are used - POJO Pair instead Java - old BGWatchData cleanup fix - Persistence getDataMap fix - minor fixes and improvements to display formating --- .../CobDetailedComplication.java | 2 +- .../IobDetailedComplication.java | 2 +- .../androidaps/data/DisplayRawData.java | 17 +++++--- .../interaction/utils/DisplayFormat.java | 40 +++++++++++------ .../androidaps/interaction/utils/Pair.java | 43 +++++++++++++++++++ .../interaction/utils/Persistence.java | 2 +- .../utils/SmallestDoubleString.java | 9 +++- .../interaction/utils/WearUtil.java | 11 ++++- 8 files changed, 99 insertions(+), 27 deletions(-) create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/Pair.java diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java index 48341b53ef..ea35b6f45f 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java @@ -4,10 +4,10 @@ import android.app.PendingIntent; import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import android.util.Pair; import info.nightscout.androidaps.data.DisplayRawData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; +import info.nightscout.androidaps.interaction.utils.Pair; /* * Created by dlvoy on 2019-11-12 diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java index d9ee729874..790d63aa67 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java @@ -4,10 +4,10 @@ import android.app.PendingIntent; import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import android.util.Pair; import info.nightscout.androidaps.data.DisplayRawData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; +import info.nightscout.androidaps.interaction.utils.Pair; /* * Created by dlvoy on 2019-11-12 diff --git a/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java b/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java index e9cf997db8..97183cd689 100644 --- a/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java +++ b/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java @@ -7,6 +7,7 @@ import android.os.PowerManager; import com.google.android.gms.wearable.DataMap; import java.util.ArrayList; +import java.util.Iterator; import info.nightscout.androidaps.interaction.utils.Constants; import info.nightscout.androidaps.interaction.utils.Persistence; @@ -120,7 +121,7 @@ public class DisplayRawData { public DataMap updateDataFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { Bundle bundle = intent.getBundleExtra("data"); if (bundle != null) { - DataMap dataMap = DataMap.fromBundle(bundle); + DataMap dataMap = WearUtil.bundleToDataMap(bundle); updateData(dataMap); return dataMap; } @@ -141,7 +142,7 @@ public class DisplayRawData { public DataMap updateStatusFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { Bundle bundle = intent.getBundleExtra("status"); if (bundle != null) { - DataMap dataMap = DataMap.fromBundle(bundle); + DataMap dataMap = WearUtil.bundleToDataMap(bundle); updateStatus(dataMap); return dataMap; } @@ -168,7 +169,7 @@ public class DisplayRawData { public DataMap updateBasalsFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { Bundle bundle = intent.getBundleExtra("basals"); if (bundle != null) { - DataMap dataMap = DataMap.fromBundle(bundle); + DataMap dataMap = WearUtil.bundleToDataMap(bundle); updateBasals(dataMap); return dataMap; } @@ -259,10 +260,12 @@ public class DisplayRawData { bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); } - for (int i = 0; i < bgDataList.size(); i++) { - if (bgDataList.get(i).timestamp < (System.currentTimeMillis() - (Constants.HOUR_IN_MS * 5))) { - bgDataList.remove(i); //Get rid of anything more than 5 hours old - break; + // We use iterator instead for-loop because we iterate and remove on the go + Iterator itr = bgDataList.iterator(); + while (itr.hasNext()) { + BgWatchData entry = (BgWatchData)itr.next(); + if (entry.timestamp < (WearUtil.timestamp() - (Constants.HOUR_IN_MS * 5))) { + itr.remove(); //Get rid of anything more than 5 hours old } } } diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java index 366ca28eed..4925bb1f5c 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java @@ -1,7 +1,5 @@ package info.nightscout.androidaps.interaction.utils; -import android.util.Pair; - import info.nightscout.androidaps.aaps; import info.nightscout.androidaps.data.DisplayRawData; @@ -45,20 +43,30 @@ public class DisplayFormat { return days + "d"; } else { int weeks = days / 7; - return weeks + "d"; + return weeks + "w"; } } } public static String shortTrend(final DisplayRawData raw) { - String minutes = shortTimeSince(raw.datetime); - String delta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_SHORT_FIELD-1); - - if (minutes.length() + delta.length() + 1 < MAX_SHORT_FIELD) { - delta = deltaSymbol() + delta; + String minutes = "--"; + if (raw.datetime > 0) { + minutes = shortTimeSince(raw.datetime); } - return minutes + " " + delta; + if (minutes.length() + raw.sDelta.length() + deltaSymbol().length() + 1 <= MAX_SHORT_FIELD) { + return minutes + " " + deltaSymbol() + raw.sDelta; + } + + // that only optimizes obvious things like 0 before . or at end, + at beginning + String delta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_SHORT_FIELD-1); + if (minutes.length() + delta.length() + deltaSymbol().length() + 1 <= MAX_SHORT_FIELD) { + return minutes + " " + deltaSymbol() + delta; + } + + String shortDelta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_SHORT_FIELD-(1+minutes.length())); + + return minutes + " " + shortDelta; } public static String longGlucoseLine(final DisplayRawData raw) { @@ -105,13 +113,17 @@ public class DisplayFormat { final String iob1 = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_SHORT_FIELD); String iob2 = ""; if (raw.sIOB2.contains("|")) { - String[] iobs = raw.sIOB2.replace("(", "").replace(")", "").split("\\|"); - if (iobs.length == 2) { - final String iobBolus = new SmallestDoubleString(iobs[0]).minimise(MIN_IOB_FIELD); - final String iobBasal = new SmallestDoubleString(iobs[1]).minimise((MAX_SHORT_FIELD-1) - Math.max(MIN_IOB_FIELD, iobBolus.length())); - iob2 = iobBolus+" "+iobBasal; + + String iobBolus = new SmallestDoubleString(iobs[0]).minimise(MIN_IOB_FIELD); + if (iobBolus.trim().length() == 0) { + iobBolus = "--"; } + String iobBasal = new SmallestDoubleString(iobs[1]).minimise((MAX_SHORT_FIELD-1) - Math.max(MIN_IOB_FIELD, iobBolus.length())); + if (iobBasal.trim().length() == 0) { + iobBasal = "--"; + } + iob2 = iobBolus+" "+iobBasal; } return Pair.create(iob1, iob2); } diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Pair.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Pair.java new file mode 100644 index 0000000000..4207cbd743 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Pair.java @@ -0,0 +1,43 @@ +package info.nightscout.androidaps.interaction.utils; + +import java.util.Objects; + +/** + * Same as android Pair, but clean room java class - does not require Android SDK for tests + */ +public class Pair { + + public final F first; + public final S second; + + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + public static Pair create(F f, S s) { + return new Pair<>(f, s); + } + + @Override + public boolean equals(java.lang.Object o) { + if (o instanceof Pair) { + return ((Pair) o).first.equals(first) && ((Pair) o).second.equals(second); + } else { + return false; + } + } + + @Override + public String toString() { + return "First: \""+first.toString()+"\" Second: \""+second.toString()+"\""; + } + + @Override + public int hashCode() { + return Objects.hash(first, second); + } + +} + + diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java index 805aa5aafa..cfc6c2b4b4 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java @@ -23,7 +23,7 @@ public class Persistence { } public DataMap getDataMap(String key) { - if (preferences.contains("raw_data")) { + if (preferences.contains(key)) { final String rawB64Data = preferences.getString(key, null); byte[] rawData = Base64.decode(rawB64Data, Base64.DEFAULT); try { diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java index 5202daf8e4..8361478976 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java @@ -71,6 +71,8 @@ public class SmallestDoubleString { } public String minimise(int maxSize) { + final String originalSeparator = separator; + if (Integer.parseInt("0"+fractional) == 0) { separator = ""; fractional = ""; @@ -95,7 +97,7 @@ public class SmallestDoubleString { return toString(); } - if ((fractional.length() > 0)&&(decimal.length() > 0)) { + if (fractional.length() > 0) { int remainingForFraction = maxSize-currentLen()+fractional.length(); String formatCandidate = "#"; if (remainingForFraction>=1) { @@ -104,8 +106,10 @@ public class SmallestDoubleString { DecimalFormat df = new DecimalFormat(formatCandidate); df.setRoundingMode(RoundingMode.HALF_UP); - return sign + df.format(Double.parseDouble(decimal+"."+fractional)).replace(".", separator) + + final String decimalSup = (decimal.length() > 0) ? decimal : "0"; + String result = sign + df.format(Double.parseDouble(decimalSup+"."+fractional)).replace(",", originalSeparator).replace(".", originalSeparator) + ((withUnits == Units.USE) ? units : ""); + return (decimal.length() > 0) ? result : result.substring(1); } return toString(); } @@ -115,6 +119,7 @@ public class SmallestDoubleString { ((withUnits == Units.USE) ? units.length() : 0); } + @Override public String toString() { return sign+decimal+separator+fractional + ((withUnits == Units.USE) ? units : ""); diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java index 2a400b4fa0..446d0f7d68 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java @@ -2,9 +2,12 @@ package info.nightscout.androidaps.interaction.utils; import android.content.Context; import android.content.Intent; +import android.os.Bundle; import android.os.PowerManager; import android.util.Log; +import com.google.android.gms.wearable.DataMap; + import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -12,7 +15,6 @@ import java.util.Map; import java.util.Set; import info.nightscout.androidaps.aaps; -import info.nightscout.androidaps.data.DisplayRawData; /** * Created by andy on 3/5/19. @@ -124,4 +126,11 @@ public class WearUtil { } return set; } + + /** + * Taken out to helper method to allow testing + */ + public static DataMap bundleToDataMap(Bundle bundle) { + return DataMap.fromBundle(bundle); + } } From 48aa7a887aac41edaf496c4227913ab9c6276bd2 Mon Sep 17 00:00:00 2001 From: dlvoy Date: Tue, 26 Nov 2019 09:24:41 +0100 Subject: [PATCH 5/7] [#2210][#728] Tests for complications related, formating, perisitence, utils and data-model code of wear module --- .../androidaps/data/BgWatchDataTest.java | 113 ++++++++ .../data/DisplayRawDataBasalsTest.java | 265 ++++++++++++++++++ .../data/DisplayRawDataBgEntriesTest.java | 146 ++++++++++ .../data/DisplayRawDataSgvDataTest.java | 148 ++++++++++ .../data/DisplayRawDataStatusTest.java | 176 ++++++++++++ .../interaction/utils/DisplayFormatTest.java | 204 ++++++++++++++ .../interaction/utils/PairTest.java | 68 +++++ .../interaction/utils/PersistenceTest.java | 189 +++++++++++++ .../interaction/utils/WearUtilTest.java | 186 ++++++++++++ .../testing/mockers/AAPSMocker.java | 63 +++++ .../testing/mockers/AndroidMocker.java | 40 +++ .../androidaps/testing/mockers/LogMocker.java | 11 + .../testing/mockers/RawDataMocker.java | 65 +++++ .../testing/mockers/WearUtilMocker.java | 104 +++++++ .../androidaps/testing/mocks/BundleMock.java | 233 +++++++++++++++ .../androidaps/testing/mocks/IntentMock.java | 39 +++ .../testing/mocks/SharedPreferencesMock.java | 158 +++++++++++ .../testing/utils/BasalWatchDataExt.java | 70 +++++ .../testing/utils/BgWatchDataExt.java | 78 ++++++ .../testing/utils/BolusWatchDataExt.java | 76 +++++ .../testing/utils/TempWatchDataExt.java | 78 ++++++ 21 files changed, 2510 insertions(+) create mode 100644 wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBasalsTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBgEntriesTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataSgvDataTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataStatusTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/interaction/utils/PairTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mockers/LogMocker.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mocks/BundleMock.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mocks/IntentMock.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/mocks/SharedPreferencesMock.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java diff --git a/wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java b/wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java new file mode 100644 index 0000000000..fa74d17691 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java @@ -0,0 +1,113 @@ +package info.nightscout.androidaps.data; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.HashSet; +import java.util.Set; + +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.WearUtil; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; + +import static org.hamcrest.number.OrderingComparison.comparesEqualTo; +import static org.hamcrest.number.OrderingComparison.greaterThan; +import static org.hamcrest.number.OrderingComparison.lessThan; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class } ) +public class BgWatchDataTest { + + @Before + public void mock() { + WearUtilMocker.prepareMockNoReal(); + } + + @Test + public void bgWatchDataHashTest() { + // GIVEN + BgWatchData inserted = new BgWatchData( + 88.0, 160.0, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*1, 1 + ); + Set set = new HashSet<>(); + + // THEN + assertFalse(set.contains(inserted)); + set.add(inserted); + assertTrue(set.contains(inserted)); + } + + /** + * BgWatchData has BIZARRE equals - only timestamp and color are checked! + */ + @Test + public void bgWatchDataEqualsTest() { + // GIVEN + BgWatchData item1 = new BgWatchData( + 88.0, 160.0, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS, 1 + ); + + BgWatchData item2sameTimeSameColor = new BgWatchData( + 123.0, 190, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS, 1 + ); + + BgWatchData item3sameTimeSameDiffColor = new BgWatchData( + 96.0, 190, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS, 0 + ); + BgWatchData item4differentTime = new BgWatchData( + 88.0, 160.0, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2, 1 + ); + + // THEN + assertEquals(item1, item2sameTimeSameColor); + assertNotEquals(item1, item3sameTimeSameDiffColor); + assertNotEquals(item1, item4differentTime); + + assertFalse(item1.equals("aa bbb")); + } + + /** + * BgWatchData is ordered by timestamp, reverse order + */ + @Test + public void bgWatchDataCompareTest() { + // GIVEN + BgWatchData item1 = new BgWatchData( + 85, 160.0, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2, 1 + ); + + BgWatchData item2 = new BgWatchData( + 80, 190, 90.0, + WearUtilMocker.REF_NOW, 1 + ); + + BgWatchData item3 = new BgWatchData( + 80, 190, 50.0, + WearUtilMocker.REF_NOW + Constants.MINUTE_IN_MS*5, 0 + ); + + BgWatchData item4 = new BgWatchData( + 160, 140, 70.0, + WearUtilMocker.REF_NOW, 0 + ); + + // THEN + assertThat(item2, lessThan(item1)); + assertThat(item2, greaterThan(item3)); + assertThat(item2, comparesEqualTo(item4)); + } +} diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBasalsTest.java b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBasalsTest.java new file mode 100644 index 0000000000..d3f9dcf48b --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBasalsTest.java @@ -0,0 +1,265 @@ +package info.nightscout.androidaps.data; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.wearable.DataMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.Persistence; +import info.nightscout.androidaps.interaction.utils.WearUtil; +import info.nightscout.androidaps.testing.mockers.AAPSMocker; +import info.nightscout.androidaps.testing.mockers.AndroidMocker; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; +import info.nightscout.androidaps.testing.mocks.BundleMock; +import info.nightscout.androidaps.testing.mocks.IntentMock; +import info.nightscout.androidaps.testing.utils.BasalWatchDataExt; +import info.nightscout.androidaps.testing.utils.BgWatchDataExt; +import info.nightscout.androidaps.testing.utils.BolusWatchDataExt; +import info.nightscout.androidaps.testing.utils.TempWatchDataExt; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class, android.util.Base64.class, Intent.class } ) +public class DisplayRawDataBasalsTest { + + @Before + public void mock() { + AAPSMocker.prepareMock(); + AAPSMocker.resetMockedSharedPrefs(); + AndroidMocker.mockBase64(); + WearUtilMocker.prepareMockNoReal(); + } + + //============================================================================================== + // BASALS for chart + //============================================================================================== + + private DataMap dataMapForBasals() { + + DataMap dataMap = new DataMap(); + + ArrayList temps = new ArrayList<>(); + DataMap temp = new DataMap(); + temp.putLong("starttime", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*20); + temp.putDouble("startBasal", 1.5); + temp.putLong("endtime", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*10); + temp.putDouble("endbasal", 1.5); + temp.putDouble("amount", 1.8); + temps.add(temp); + + DataMap temp2 = new DataMap(); + temp2.putLong("starttime", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*10); + temp2.putDouble("startBasal", 1.3); + temp2.putLong("endtime", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2); + temp2.putDouble("endbasal", 1.3); + temp2.putDouble("amount", 2.3); + temps.add(temp2); + dataMap.putDataMapArrayList("temps", temps); + + ArrayList basals = new ArrayList<>(); + DataMap basal = new DataMap(); + basal.putLong("starttime", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*20); + basal.putLong("endtime", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2); + basal.putDouble("amount", 1.2); + basals.add(basal); + dataMap.putDataMapArrayList("basals", basals); + + ArrayList boluses = new ArrayList<>(); + DataMap bolus = new DataMap(); + bolus.putLong("date", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*17); + bolus.putDouble("bolus", 5.5); + bolus.putDouble("carbs", 20.0); + bolus.putBoolean("isSMB", false); + bolus.putBoolean("isValid", true); + boluses.add(bolus); + + DataMap bolus2 = new DataMap(); + bolus2.putLong("date", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*11); + bolus2.putDouble("bolus", 3.0); + bolus2.putDouble("carbs", 0.0); + bolus2.putBoolean("isSMB", false); + bolus2.putBoolean("isValid", true); + boluses.add(bolus2); + + DataMap bolus3 = new DataMap(); + bolus3.putLong("date", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*3); + bolus3.putDouble("bolus", 0.0); + bolus3.putDouble("carbs", 15.0); + bolus3.putBoolean("isSMB", true); + bolus3.putBoolean("isValid", false); + boluses.add(bolus3); + + dataMap.putDataMapArrayList("boluses", boluses); + + ArrayList predictions = new ArrayList<>(); + for (int i=0; i<10; i++) { + DataMap prediction = new DataMap(); + prediction.putLong("timestamp", WearUtilMocker.REF_NOW + Constants.MINUTE_IN_MS*i); + prediction.putDouble("sgv", 160-4*i); + prediction.putInt("color", 0); + predictions.add(prediction); + } + dataMap.putDataMapArrayList("predictions", predictions); + + return dataMap; + } + + private void assertBasalsEmpty(DisplayRawData newRaw) { + assertThat(newRaw.tempWatchDataList.size(), is(0)); + assertThat(newRaw.basalWatchDataList.size(), is(0)); + assertThat(newRaw.bolusWatchDataList.size(), is(0)); + assertThat(newRaw.predictionList.size(), is(0)); + } + + private void assertBasalsOk(DisplayRawData newRaw) { + assertThat(newRaw.tempWatchDataList.size(), is(2)); + assertThat(newRaw.basalWatchDataList.size(), is(1)); + assertThat(newRaw.bolusWatchDataList.size(), is(3)); + assertThat(newRaw.predictionList.size(), is(10)); + + assertThat(new TempWatchDataExt(newRaw.tempWatchDataList.get(0)), is(TempWatchDataExt.build( + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*20, + 1.5, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*10, + 1.5, + 1.8 + ))); + + assertThat(new TempWatchDataExt(newRaw.tempWatchDataList.get(1)), is(TempWatchDataExt.build( + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*10, + 1.3, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2, + 1.3, + 2.3 + ))); + + assertThat(new BasalWatchDataExt(newRaw.basalWatchDataList.get(0)), is(BasalWatchDataExt.build( + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*20, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2, + 1.2 + ))); + + assertThat(new BolusWatchDataExt(newRaw.bolusWatchDataList.get(0)), is(BolusWatchDataExt.build( + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*17, + 5.5, + 20, + false, + true + ))); + + assertThat(new BolusWatchDataExt(newRaw.bolusWatchDataList.get(1)), is(BolusWatchDataExt.build( + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*11, + 3, + 0, + false, + true + ))); + + assertThat(new BolusWatchDataExt(newRaw.bolusWatchDataList.get(2)), is(BolusWatchDataExt.build( + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*3, + 0, + 15, + true, + false + ))); + + + assertThat(new BgWatchDataExt(newRaw.predictionList.get(3)), is(BgWatchDataExt.build( + 160-4*3, + WearUtilMocker.REF_NOW + Constants.MINUTE_IN_MS*3, + 0 + ))); + + assertThat(new BgWatchDataExt(newRaw.predictionList.get(7)), is(BgWatchDataExt.build( + 160-4*7, + WearUtilMocker.REF_NOW + Constants.MINUTE_IN_MS*7, + 0 + ))); + } + + @Test + public void updateBasalsFromEmptyPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateFromPersistence(persistence); + + // THEN + assertBasalsEmpty(newRaw); + } + + @Test + public void updateBasalsFromPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + Persistence.storeDataMap(DisplayRawData.BASALS_PERSISTENCE_KEY, dataMapForBasals()); + newRaw.updateFromPersistence(persistence); + + // THEN + assertBasalsOk(newRaw); + } + + @Test + public void partialUpdateBasalsFromPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + Persistence.storeDataMap(DisplayRawData.BASALS_PERSISTENCE_KEY, dataMapForBasals()); + newRaw.partialUpdateFromPersistence(persistence); + + // THEN + assertBasalsEmpty(newRaw); + } + + @Test + public void updateBasalsFromMessageTest() { + // GIVEN + Intent intent = IntentMock.mock(); + Bundle bundle = BundleMock.mock(dataMapForBasals()); + + intent.putExtra("basals", bundle); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateBasalsFromMessage(intent, null); + + // THEN + assertBasalsOk(newRaw); + } + + @Test + public void updateBasalsFromEmptyMessageTest() { + // GIVEN + Intent intent = IntentMock.mock(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateBasalsFromMessage(intent, null); + + // THEN + assertBasalsEmpty(newRaw); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBgEntriesTest.java b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBgEntriesTest.java new file mode 100644 index 0000000000..1908e20dd5 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBgEntriesTest.java @@ -0,0 +1,146 @@ +package info.nightscout.androidaps.data; + +import com.google.android.gms.wearable.DataMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; + +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.WearUtil; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; +import info.nightscout.androidaps.testing.utils.BgWatchDataExt; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class } ) +public class DisplayRawDataBgEntriesTest { + + @Before + public void mock() { + WearUtilMocker.prepareMockNoReal(); + } + + //============================================================================================== + // ENTRIES for chart + //============================================================================================== + + private DataMap dataMapForEntries() { + + DataMap dataMap = new DataMap(); + ArrayList entries = new ArrayList<>(); + for (int i=0; i<12; i++) { + DataMap entry = new DataMap(); + entry.putLong("timestamp", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*(16-i)); + entry.putDouble("sgvDouble", 145.0-5*i); + entry.putDouble("high", 170.0); + entry.putDouble("low", 80.0); + entry.putInt("color", 0); + entries.add(entry); + } + dataMap.putDataMapArrayList("entries", entries); + + return dataMap; + } + + private DataMap dataMapForEntries(long timestamp, double sgv) { + DataMap entry = new DataMap(); + entry.putLong("timestamp", timestamp); + entry.putDouble("sgvDouble", sgv); + entry.putDouble("high", 160.0); + entry.putDouble("low", 90.0); + entry.putInt("color", 1); + return entry; + } + + @Test + public void addToWatchSetTest() { + // GIVEN + DisplayRawData newRaw = new DisplayRawData(); + DataMap multipleEntries = dataMapForEntries(); + DataMap singleEntry1 = dataMapForEntries(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*2,92); + DataMap singleEntry2 = dataMapForEntries(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*1,88); + + // WHEN, THEN + // add list + newRaw.addToWatchSet(multipleEntries); + assertThat(newRaw.bgDataList.size(), is(12)); + + assertThat(new BgWatchDataExt(newRaw.bgDataList.get(5)), + is(new BgWatchDataExt(new BgWatchData( + 120.0, 170.0, 80.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*(16-5), 0 + )))); + + assertThat(new BgWatchDataExt(newRaw.bgDataList.get(11)), + is(new BgWatchDataExt(new BgWatchData( + 90.0, 170.0, 80.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*(16-11), 0 + )))); + + // add single entries + newRaw.addToWatchSet(singleEntry1); + newRaw.addToWatchSet(singleEntry2); + assertThat(newRaw.bgDataList.size(), is(14)); + + assertThat(new BgWatchDataExt(newRaw.bgDataList.get(12)), + is(new BgWatchDataExt(new BgWatchData( + 92.0, 160.0, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*2, 1 + )))); + assertThat(new BgWatchDataExt(newRaw.bgDataList.get(13)), + is(new BgWatchDataExt(new BgWatchData( + 88.0, 160.0, 90.0, + WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*1, 1 + )))); + + // ignore duplicates + newRaw.addToWatchSet(singleEntry2); + assertThat(newRaw.bgDataList.size(), is(14)); + } + + @Test + public void addToWatchSetCleanupOldTest() { + DisplayRawData newRaw = new DisplayRawData(); + + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),125)); + assertThat(newRaw.bgDataList.size(), is(1)); + + WearUtilMocker.progressClock(Constants.HOUR_IN_MS*2); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),140)); + assertThat(newRaw.bgDataList.size(), is(2)); + + WearUtilMocker.progressClock(Constants.HOUR_IN_MS*1); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),150)); + WearUtilMocker.progressClock(Constants.HOUR_IN_MS*1 +Constants.MINUTE_IN_MS*30); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),101)); + assertThat(newRaw.bgDataList.size(), is(4)); + + WearUtilMocker.progressClock(Constants.MINUTE_IN_MS*30); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),90)); + assertThat(newRaw.bgDataList.size(), is(5)); + + WearUtilMocker.progressClock(Constants.HOUR_IN_MS*1 +Constants.MINUTE_IN_MS*30); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),80)); + assertThat(newRaw.bgDataList.size(), is(5)); + + WearUtilMocker.progressClock(Constants.HOUR_IN_MS*4); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),92)); + assertThat(newRaw.bgDataList.size(), is(2)); + + WearUtilMocker.progressClock(Constants.HOUR_IN_MS*5 +Constants.MINUTE_IN_MS*30); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),107)); + assertThat(newRaw.bgDataList.size(), is(1)); + + WearUtilMocker.progressClock(Constants.HOUR_IN_MS*6 +Constants.MINUTE_IN_MS*30); + newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp()-Constants.HOUR_IN_MS*6,138)); + assertThat(newRaw.bgDataList.size(), is(0)); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataSgvDataTest.java b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataSgvDataTest.java new file mode 100644 index 0000000000..55ff9bb815 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataSgvDataTest.java @@ -0,0 +1,148 @@ +package info.nightscout.androidaps.data; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.wearable.DataMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.Persistence; +import info.nightscout.androidaps.interaction.utils.WearUtil; +import info.nightscout.androidaps.testing.mockers.AAPSMocker; +import info.nightscout.androidaps.testing.mockers.AndroidMocker; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; +import info.nightscout.androidaps.testing.mocks.BundleMock; +import info.nightscout.androidaps.testing.mocks.IntentMock; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class, android.util.Base64.class, Intent.class } ) +public class DisplayRawDataSgvDataTest { + + @Before + public void mock() { + AAPSMocker.prepareMock(); + AAPSMocker.resetMockedSharedPrefs(); + AndroidMocker.mockBase64(); + WearUtilMocker.prepareMockNoReal(); + } + + //============================================================================================== + // SGV DATA + //============================================================================================== + + private DataMap dataMapForData() { + DataMap dataMap = new DataMap(); + dataMap.putLong("sgvLevel", 1L); + dataMap.putLong("timestamp", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS); + dataMap.putString("sgvString", "106"); + dataMap.putString("slopeArrow", "↗"); + dataMap.putString("delta", "5.4"); + dataMap.putString("avgDelta", "3.7"); + dataMap.putString("glucoseUnits", "mg/dl"); + return dataMap; + } + + private void assertDataEmpty(DisplayRawData newRaw) { + assertThat(newRaw.sgvLevel, is(0L)); + assertThat(newRaw.datetime, is(0L)); + assertThat(newRaw.sSgv, is("---")); + assertThat(newRaw.sDirection, is("--")); + assertThat(newRaw.sDelta, is("--")); + assertThat(newRaw.sAvgDelta, is("--")); + assertThat(newRaw.sUnits, is("-")); + } + + private void assertDataOk(DisplayRawData newRaw) { + assertThat(newRaw.sgvLevel, is(1L)); + assertThat(newRaw.datetime, is(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS)); + assertThat(newRaw.sSgv, is("106")); + assertThat(newRaw.sDirection, is("↗")); + assertThat(newRaw.sDelta, is("5.4")); + assertThat(newRaw.sAvgDelta, is("3.7")); + assertThat(newRaw.sUnits, is("mg/dl")); + } + + @Test + public void updateDataFromEmptyPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateFromPersistence(persistence); + + // THEN + assertDataEmpty(newRaw); + } + + @Test + public void updateDataFromPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + Persistence.storeDataMap(DisplayRawData.DATA_PERSISTENCE_KEY, dataMapForData()); + newRaw.updateFromPersistence(persistence); + + // THEN + assertDataOk(newRaw); + } + + @Test + public void partialUpdateDataFromPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + Persistence.storeDataMap(DisplayRawData.DATA_PERSISTENCE_KEY, dataMapForData()); + newRaw.partialUpdateFromPersistence(persistence); + + // THEN + assertDataOk(newRaw); + } + + @Test + public void updateDataFromMessageTest() { + // GIVEN + Intent intent = IntentMock.mock(); + Bundle bundle = BundleMock.mock(dataMapForData()); + + intent.putExtra("data", bundle); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateDataFromMessage(intent, null); + + // THEN + assertDataOk(newRaw); + } + + @Test + public void updateDataFromEmptyMessageTest() { + // GIVEN + Intent intent = IntentMock.mock(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateDataFromMessage(intent, null); + + // THEN + assertDataEmpty(newRaw); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataStatusTest.java b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataStatusTest.java new file mode 100644 index 0000000000..17ac8fd14b --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataStatusTest.java @@ -0,0 +1,176 @@ +package info.nightscout.androidaps.data; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.wearable.DataMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.Persistence; +import info.nightscout.androidaps.interaction.utils.WearUtil; +import info.nightscout.androidaps.testing.mockers.AAPSMocker; +import info.nightscout.androidaps.testing.mockers.AndroidMocker; +import info.nightscout.androidaps.testing.mockers.RawDataMocker; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; +import info.nightscout.androidaps.testing.mocks.BundleMock; +import info.nightscout.androidaps.testing.mocks.IntentMock; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class, android.util.Base64.class, Intent.class } ) +public class DisplayRawDataStatusTest { + + @Before + public void mock() { + AAPSMocker.prepareMock(); + AAPSMocker.resetMockedSharedPrefs(); + AndroidMocker.mockBase64(); + WearUtilMocker.prepareMockNoReal(); + } + + @Test + public void toDebugStringTest() { + DisplayRawData raw = RawDataMocker.rawDelta(5, "1.5"); + raw.externalStatusString = "placeholder-here"; + + assertThat(raw.datetime, is(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*5)); + assertThat(raw.toDebugString(), containsString("placeholder-here")); + } + + //============================================================================================== + // STATUS + //============================================================================================== + + private DataMap dataMapForStatus() { + DataMap dataMap = new DataMap(); + dataMap.putString("currentBasal", "120%"); + dataMap.putString("battery", "76"); + dataMap.putString("rigBattery", "40%"); + dataMap.putBoolean("detailedIob", true); + dataMap.putString("iobSum", "12.5") ; + dataMap.putString("iobDetail","(11,2|1,3)"); + dataMap.putString("cob","5(10)g"); + dataMap.putString("bgi", "13"); + dataMap.putBoolean("showBgi", false); + dataMap.putString("externalStatusString", ""); + dataMap.putInt("batteryLevel", 1); + dataMap.putLong("openApsStatus", WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2); + return dataMap; + } + + private void assertStatusEmpty(DisplayRawData newRaw) { + assertThat(newRaw.sBasalRate, is("-.--U/h")); + assertThat(newRaw.sUploaderBattery, is("--")); + assertThat(newRaw.sRigBattery, is("--")); + assertThat(newRaw.detailedIOB, is(false)); + assertThat(newRaw.sIOB1, is("IOB")); + assertThat(newRaw.sIOB2, is("-.--")); + assertThat(newRaw.sCOB1, is("Carb")); + assertThat(newRaw.sCOB2, is("--g")); + assertThat(newRaw.sBgi, is("--")); + assertThat(newRaw.showBGI, is(false)); + assertThat(newRaw.externalStatusString, is("no status")); + assertThat(newRaw.batteryLevel, is(1)); + assertThat(newRaw.openApsStatus, is(-1L)); + } + + private void assertStatusOk(DisplayRawData newRaw) { + assertThat(newRaw.sBasalRate, is("120%")); + assertThat(newRaw.sUploaderBattery, is("76")); + assertThat(newRaw.sRigBattery, is("40%")); + assertThat(newRaw.detailedIOB, is(true)); + assertThat(newRaw.sIOB1, is("12.5U")); + assertThat(newRaw.sIOB2, is("(11,2|1,3)")); + assertThat(newRaw.sCOB1, is("Carb")); + assertThat(newRaw.sCOB2, is("5(10)g")); + assertThat(newRaw.sBgi, is("13")); + assertThat(newRaw.showBGI, is(false)); + assertThat(newRaw.externalStatusString, is("")); + assertThat(newRaw.batteryLevel, is(1)); + assertThat(newRaw.openApsStatus, is(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*2)); + } + + @Test + public void updateStatusFromEmptyPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateFromPersistence(persistence); + + // THEN + assertStatusEmpty(newRaw); + } + + @Test + public void updateStatusFromPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + Persistence.storeDataMap(DisplayRawData.STATUS_PERSISTENCE_KEY, dataMapForStatus()); + newRaw.updateFromPersistence(persistence); + + // THEN + assertStatusOk(newRaw); + } + + @Test + public void partialUpdateStatusFromPersistenceTest() { + // GIVEN + Persistence persistence = new Persistence(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + Persistence.storeDataMap(DisplayRawData.STATUS_PERSISTENCE_KEY, dataMapForStatus()); + newRaw.partialUpdateFromPersistence(persistence); + + // THEN + assertStatusOk(newRaw); + } + + @Test + public void updateStatusFromMessageTest() { + // GIVEN + Intent intent = IntentMock.mock(); + Bundle bundle = BundleMock.mock(dataMapForStatus()); + + intent.putExtra("status", bundle); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateStatusFromMessage(intent, null); + + // THEN + assertStatusOk(newRaw); + } + + @Test + public void updateStatusFromEmptyMessageTest() { + // GIVEN + Intent intent = IntentMock.mock(); + DisplayRawData newRaw = new DisplayRawData(); + + // WHEN + newRaw.updateStatusFromMessage(intent, null); + + // THEN + assertStatusEmpty(newRaw); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java new file mode 100644 index 0000000000..3c838f5898 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java @@ -0,0 +1,204 @@ +package info.nightscout.androidaps.interaction.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.testing.mockers.AAPSMocker; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; + +import static info.nightscout.androidaps.testing.mockers.RawDataMocker.rawCob; +import static info.nightscout.androidaps.testing.mockers.RawDataMocker.rawCobIobBr; +import static info.nightscout.androidaps.testing.mockers.RawDataMocker.rawDelta; +import static info.nightscout.androidaps.testing.mockers.RawDataMocker.rawIob; +import static info.nightscout.androidaps.testing.mockers.RawDataMocker.rawSgv; +import static info.nightscout.androidaps.testing.mockers.WearUtilMocker.backInTime; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +/** + * This test covers DisplayFormat class (directly) + * but also SmallestDoubleString - due to carefully chosen input data to format + */ + +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class } ) +public class DisplayFormatTest { + + @Before + public void mock() { + WearUtilMocker.prepareMock(); + AAPSMocker.prepareMock(); + AAPSMocker.resetMockedSharedPrefs(); + } + + @Test + public void shortTimeSinceTest() { + + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,0,0)), is("0'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,0,5)), is("0'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,0,55)), is("0'")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,1,0)), is("1'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,1,59)), is("1'")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,2,0)), is("2'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,3,0)), is("3'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,4,0)), is("4'")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,10,0)), is("10'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,30,0)), is("30'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,59,0)), is("59'")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,0,59,59)), is("59'")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(0,1,0,0)), is("1h")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,1,30,0)), is("1h")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,1,59,59)), is("1h")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(0,2,0,0)), is("2h")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,3,0,0)), is("3h")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,4,0,0)), is("4h")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,5,0,0)), is("5h")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(0,12,0,0)), is("12h")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,18,0,0)), is("18h")); + assertThat(DisplayFormat.shortTimeSince(backInTime(0,23,59,59)), is("23h")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(1,0,0,0)), is("1d")); + assertThat(DisplayFormat.shortTimeSince(backInTime(1,12,0,0)), is("1d")); + assertThat(DisplayFormat.shortTimeSince(backInTime(1,23,59,59)), is("1d")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(2,0,0,0)), is("2d")); + assertThat(DisplayFormat.shortTimeSince(backInTime(3,0,0,0)), is("3d")); + assertThat(DisplayFormat.shortTimeSince(backInTime(4,0,0,0)), is("4d")); + assertThat(DisplayFormat.shortTimeSince(backInTime(5,0,0,0)), is("5d")); + assertThat(DisplayFormat.shortTimeSince(backInTime(6,0,0,0)), is("6d")); + assertThat(DisplayFormat.shortTimeSince(backInTime(6,23,59,59)), is("6d")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(7,0,0,0)), is("1w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(8,0,0,0)), is("1w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(9,0,0,0)), is("1w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(13,23,59,59)), is("1w")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(14,0,0,0)), is("2w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(21,0,0,0)), is("3w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(28,0,0,0)), is("4w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(31,0,0,0)), is("4w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(32,0,0,0)), is("4w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(35,0,0,0)), is("5w")); + + assertThat(DisplayFormat.shortTimeSince(backInTime(100,0,0,0)), is("14w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(200,0,0,0)), is("28w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(365,0,0,0)), is("52w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(366,0,0,0)), is("52w")); + assertThat(DisplayFormat.shortTimeSince(backInTime(367,0,0,0)), is("52w")); + } + + @Test + public void shortTrendTest() { + DisplayRawData raw = new DisplayRawData(); + assertThat(DisplayFormat.shortTrend(raw), is("-- Δ--")); + + raw.datetime = backInTime(0, 0, 2, 0); + assertThat(DisplayFormat.shortTrend(raw), is("2' Δ--")); + + AAPSMocker.setMockedUnicodeComplicationsOn(true); + + // shortening + assertThat(DisplayFormat.shortTrend(rawDelta(2, "1.2")), is("2' Δ1.2")); + assertThat(DisplayFormat.shortTrend(rawDelta(11,"1.2")), is("11' 1.2")); + assertThat(DisplayFormat.shortTrend(rawDelta(12,"0.7")), is("12' Δ.7")); + assertThat(DisplayFormat.shortTrend(rawDelta(10,"1.0")), is("10' Δ1")); + assertThat(DisplayFormat.shortTrend(rawDelta(14,"-5.0")), is("14' Δ-5")); + assertThat(DisplayFormat.shortTrend(rawDelta(13,"-5.1")), is("13' -5")); + assertThat(DisplayFormat.shortTrend(rawDelta(15,"0.87")), is("15' .87")); + assertThat(DisplayFormat.shortTrend(rawDelta(10,"-1.78")), is("10' -2")); + assertThat(DisplayFormat.shortTrend(rawDelta(3, "2.549")), is("3' 2.55")); + assertThat(DisplayFormat.shortTrend(rawDelta(1, "-1.563")), is("1' -1.6")); + + // preserving separator + assertThat(DisplayFormat.shortTrend(rawDelta(2, "1,2")), is("2' Δ1,2")); + assertThat(DisplayFormat.shortTrend(rawDelta(15,"0,87")), is("15' ,87")); + assertThat(DisplayFormat.shortTrend(rawDelta(3, "+2,549")), is("3' 2,55")); + assertThat(DisplayFormat.shortTrend(rawDelta(1, "-1,563")), is("1' -1,6")); + + // UTF-off mode - without delta symbol + AAPSMocker.setMockedUnicodeComplicationsOn(false); + assertThat(DisplayFormat.shortTrend(rawDelta(2, "1.2")), is("2' 1.2")); + assertThat(DisplayFormat.shortTrend(rawDelta(12,"0.7")), is("12' 0.7")); + assertThat(DisplayFormat.shortTrend(rawDelta(10,"1.0")), is("10' 1.0")); + assertThat(DisplayFormat.shortTrend(rawDelta(14,"-5.0")), is("14' -5")); + } + + @Test + public void longGlucoseLine() { + assertThat(DisplayFormat.longGlucoseLine(rawSgv("125",2, "1.2")), is("125→ Δ1.2 (2')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("97",11, "5.2")), is("97↗ Δ5.2 (11')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("110",12,"0.7")), is("110→ Δ.7 (12')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("65",10,"7.0")), is("65↗ Δ7 (10')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("215",14,"-5.0")), is("215↘ Δ-5 (14')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("8.3",13,"-5.1")), is("8.3↘ Δ-5.1 (13')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("6.8",15,"10.83")), is("6.8↑ Δ10.83 (15')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("13.2",10,"-11.78")), is("13.2↓ Δ-11.78 (10')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("3.9",3, "2.549")), is("3.9→ Δ2.549 (3')")); + assertThat(DisplayFormat.longGlucoseLine(rawSgv("11.1",1, "-15.563")), is("11.1↓ Δ-15.563 (1')")); + } + + @Test + public void longDetailsLineTest() { + AAPSMocker.setMockedUnicodeComplicationsOn(true); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("0g", "0U", "3.5U/h")), is("0g ⁞ 0U ⁞ ⎍ 3.5U/h")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("50g", "7.56U", "0%")), is("50g ⁞ 7.56U ⁞ ⎍ 0%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("12g", "3.23U", "120%")), is("12g ⁞ 3.23U ⁞ 120%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("2(40)g", "-1.5U", "0.55U/h")), is("2(40)g ⁞ -2U ⁞ 0.55U/h")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("0(24)g", "0.05U", "160%")), is("0(24)g ⁞ 0.05U ⁞ 160%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("47g", "13.87U", "220%")), is("47g ⁞ 13.87U ⁞ 220%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("13(5)g", "5.90U", "300%")), is("13(5)g ⁞ 5.90U ⁞ 300%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("11(50)g", "0U", "70%")), is("11(50)g ⁞ 0U ⁞ 70%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("7g", "0.54U", "30%")), is("7g ⁞ 0.54U ⁞ ⎍ 30%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("19(38)g", "35.545U", "12.9U/h")), is("19g ⁞ 36U ⁞ 12.9U/h")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("100(1)g", "12.345U", "6.98647U/h")), is("100g 12U 6.98647U/h")); + + AAPSMocker.setMockedUnicodeComplicationsOn(false); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("0g", "0U", "3.5U/h")), is("0g | 0U | 3.5U/h")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("50g", "7.56U", "0%")), is("50g | 7.56U | 0%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("12g", "3.23U", "120%")), is("12g | 3.23U | 120%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("7g", "0.54U", "30%")), is("7g | 0.54U | 30%")); + assertThat(DisplayFormat.longDetailsLine(rawCobIobBr("19(38)g", "35.545U", "12.9U/h")), is("19g | 36U | 12.9U/h")); + } + + @Test + public void detailedIobTest() { + assertThat(DisplayFormat.detailedIob(rawIob("-1.29U", "(0,910|-2,20)")), is(Pair.create("-1.29U", ",91 -2"))); + assertThat(DisplayFormat.detailedIob(rawIob("3.50U", "")), is(Pair.create("3.50U", ""))); + assertThat(DisplayFormat.detailedIob(rawIob("12.5U", "(+1,4|-4.78)")), is(Pair.create("12.5U", "1,4 -5"))); + assertThat(DisplayFormat.detailedIob(rawIob("0.67U", "some junks")), is(Pair.create(".67U", ""))); + assertThat(DisplayFormat.detailedIob(rawIob("-11.0U", "(broken|data)")), is(Pair.create("-11U", "-- --"))); + assertThat(DisplayFormat.detailedIob(rawIob("5.52U", "(0,5439|wrong)")), is(Pair.create("5.52U", ",54 --"))); + assertThat(DisplayFormat.detailedIob(rawIob("-8.1U", "(|-8,1)")), is(Pair.create("-8.1U", "-- -8"))); + assertThat(DisplayFormat.detailedIob(rawIob("-8.1U", "(|-8,1)")), is(Pair.create("-8.1U", "-- -8"))); + assertThat(DisplayFormat.detailedIob(rawIob("7.6U", "(malformed)")), is(Pair.create("7.6U", ""))); + assertThat(DisplayFormat.detailedIob(rawIob("-4.26U", "(6,97|1,3422|too much)")), is(Pair.create("-4.26U", "7 1,3"))); + } + + @Test + public void detailedCobTest() { + assertThat(DisplayFormat.detailedCob(rawCob("0g")), is(Pair.create("0g", ""))); + assertThat(DisplayFormat.detailedCob(rawCob("50g")), is(Pair.create("50g", ""))); + assertThat(DisplayFormat.detailedCob(rawCob("2(40)g")), is(Pair.create("2g", "40g"))); + assertThat(DisplayFormat.detailedCob(rawCob("0(24)g")), is(Pair.create("0g", "24g"))); + assertThat(DisplayFormat.detailedCob(rawCob("13(5)g")), is(Pair.create("13g", "5g"))); + assertThat(DisplayFormat.detailedCob(rawCob("11(50)g")), is(Pair.create("11g", "50g"))); + assertThat(DisplayFormat.detailedCob(rawCob("19(38)g")), is(Pair.create("19g", "38g"))); + assertThat(DisplayFormat.detailedCob(rawCob("100(1)g")), is(Pair.create("100g", "1g"))); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PairTest.java b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PairTest.java new file mode 100644 index 0000000000..ce63330e9c --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PairTest.java @@ -0,0 +1,68 @@ +package info.nightscout.androidaps.interaction.utils; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(PowerMockRunner.class) +public class PairTest { + + @Test + public void pairEqualsTest() { + // GIVEN + Pair left = Pair.create("aa", "bbb"); + Pair right = Pair.create("ccc", "dd"); + Pair another = Pair.create("aa", "bbb"); + Pair samePos1 = Pair.create("aa", "d"); + Pair samePos2 = Pair.create("zzzzz", "bbb"); + Pair no1 = Pair.create(12, 345L); + Pair no2 = Pair.create(-943, 42); + Pair no3 = Pair.create(12L, 345); + Pair no4 = Pair.create(12, 345L); + + // THEN + assertNotEquals(left, right); + assertEquals(left, another); + assertNotEquals(left, samePos1); + assertNotEquals(left, samePos2); + assertNotEquals(no1, no2); + assertNotEquals(no1, no3); + assertEquals(no1, no4); + + assertFalse(left.equals("aa bbb")); + } + + @Test + public void pairHashTest() { + // GIVEN + Pair inserted = Pair.create("aa", "bbb"); + Set set = new HashSet<>(); + + // THEN + assertFalse(set.contains(inserted)); + set.add(inserted); + assertTrue(set.contains(inserted)); + } + + @Test + public void toStringTest() { + // GIVEN + Pair pair = Pair.create("the-first", "2nd"); + + assertThat(pair.toString(), containsString("the-first")); + assertThat(pair+"", containsString("2nd")); + } + + + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java new file mode 100644 index 0000000000..625216aadc --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java @@ -0,0 +1,189 @@ +package info.nightscout.androidaps.interaction.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.gms.wearable.DataMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.Set; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.testing.mockers.AAPSMocker; +import info.nightscout.androidaps.testing.mockers.AndroidMocker; +import info.nightscout.androidaps.testing.mockers.LogMocker; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; + +import static info.nightscout.androidaps.testing.mockers.WearUtilMocker.REF_NOW; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class, android.util.Base64.class} ) +public class PersistenceTest { + + @Before + public void mock() { + WearUtilMocker.prepareMock(); + LogMocker.prepareMock(); + AAPSMocker.prepareMock(); + AAPSMocker.resetMockedSharedPrefs(); + AndroidMocker.mockBase64(); + } + + @Test + public void putStringTest() { + // GIVEN + Persistence persistence = new Persistence(); + + // WHEN + final String emptyGot = persistence.getString("test-key", "default-value"); + persistence.putString("test-key", "newValue"); + final String updatedGot = persistence.getString("test-key", "another-default-value"); + + // THEN + assertThat(emptyGot, is("default-value")); + assertThat(updatedGot, is("newValue")); + } + + @Test + public void putBooleanTest() { + // GIVEN + Persistence persistence = new Persistence(); + + // WHEN + final boolean emptyGot = persistence.getBoolean("test-key", false); + persistence.putBoolean("test-key", true); + final boolean updatedGot = persistence.getBoolean("test-key", false); + + // THEN + assertFalse(emptyGot); + assertTrue(updatedGot); + } + + @Test + public void whenDataUpdatedTest() { + // GIVEN + Persistence persistence = new Persistence(); + DataMap map = new DataMap(); + + // WHEN + final long whenNotUpdated = persistence.whenDataUpdated(); + + Persistence.storeDataMap("data-map", map); + final long whenUpdatedFirst = persistence.whenDataUpdated(); + + WearUtilMocker.progressClock(60000); + Persistence.storeDataMap("data-map", map); + final long whenUpdatedNext = persistence.whenDataUpdated(); + + // THEN + assertThat(0L, is(whenNotUpdated)); + assertThat(REF_NOW, is(whenUpdatedFirst)); + assertThat(REF_NOW + 60000, is(whenUpdatedNext)); + } + + @Test + public void getDataMapTest() { + // GIVEN + Persistence persistence = new Persistence(); + DataMap map = new DataMap(); + map.putByteArray("test-key", new byte[]{9, 42, 127, -5}); + + // WHEN + DataMap notExisting = persistence.getDataMap("not-there"); + Persistence.storeDataMap("data-map", map); + DataMap restoredMap = persistence.getDataMap("data-map"); + byte[] restoredMapContents = restoredMap.getByteArray("test-key"); + + // THEN + assertNull(notExisting); + assertNotNull(restoredMap); + assertTrue(restoredMap.containsKey("test-key")); + + assertThat(restoredMapContents.length, is(4)); + assertThat(restoredMapContents[0], is((byte)9)); + assertThat(restoredMapContents[1], is((byte)42)); + assertThat(restoredMapContents[2], is((byte)127)); + assertThat(restoredMapContents[3], is((byte)-5)); + } + + @Test + public void brokenDataMapTest() { + // GIVEN + Persistence persistence = new Persistence(); + + // WHEN + persistence.putString("data-map", "ZmFrZSBkYXRh"); + DataMap restoredMap = persistence.getDataMap("data-map"); + + // THEN + assertNull(restoredMap); + } + + @Test + public void setsTest() { + // GIVEN + Persistence persistence = new Persistence(); + + // WHEN + Set emptySet = persistence.getSetOf("some fake id"); + + persistence.addToSet("test-set", "element1"); + persistence.addToSet("test-set", "second-elem"); + persistence.addToSet("test-set", "3rd"); + persistence.addToSet("test-set", "czwarty"); + persistence.addToSet("test-set", "V"); + persistence.addToSet("test-set", "6"); + + Set initialSet = persistence.getSetOf("test-set"); + Set sameInitialSet = Persistence.setOf("test-set"); + + persistence.addToSet("test-set", "second-elem"); + persistence.addToSet("test-set", "new-one"); + + Set extendedSet = persistence.getSetOf("test-set"); + + persistence.removeFromSet("test-set", "czwarty"); + persistence.removeFromSet("test-set", "6"); + persistence.removeFromSet("test-set", "3rd"); + + Set reducedSet = persistence.getSetOf("test-set"); + + // THEN + assertThat(emptySet.size(), is(0)); + + assertThat(initialSet.size(), is(6)); + assertTrue(initialSet.contains("element1")); + assertTrue(initialSet.contains("second-elem")); + assertTrue(initialSet.contains("3rd")); + assertTrue(initialSet.contains("czwarty")); + assertTrue(initialSet.contains("V")); + assertTrue(initialSet.contains("6")); + + assertThat(initialSet, is(sameInitialSet)); + + assertThat(extendedSet.size(), is(7)); + assertTrue(extendedSet.contains("new-one")); + + assertThat(reducedSet.size(), is(4)); + assertTrue(reducedSet.contains("element1")); + assertTrue(reducedSet.contains("second-elem")); + assertFalse(reducedSet.contains("3rd")); + assertFalse(reducedSet.contains("czwarty")); + assertTrue(reducedSet.contains("V")); + assertFalse(reducedSet.contains("6")); + assertTrue(reducedSet.contains("new-one")); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java new file mode 100644 index 0000000000..5bb501584d --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java @@ -0,0 +1,186 @@ +package info.nightscout.androidaps.interaction.utils; + +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.wearable.DataMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.HashSet; +import java.util.Set; + +import info.nightscout.androidaps.testing.mockers.LogMocker; +import info.nightscout.androidaps.testing.mockers.WearUtilMocker; +import info.nightscout.androidaps.testing.mocks.BundleMock; + +import static info.nightscout.androidaps.testing.mockers.WearUtilMocker.REF_NOW; +import static org.hamcrest.CoreMatchers.both; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Created by dlvoy on 22.11.2019. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest( { WearUtil.class, Log.class} ) +public class WearUtilTest { + + @Before + public void mock() { + WearUtilMocker.prepareMock(); + LogMocker.prepareMock(); + } + + @Test + public void timestampAndTimeDiffsTest() { + + // smoke for mocks - since we freeze "now" to get stable tests + assertThat(REF_NOW, is(WearUtil.timestamp())); + + assertThat(0L, is(WearUtil.msTill(REF_NOW))); + assertThat(3456L, is(WearUtil.msTill(REF_NOW+3456L))); + assertThat(-6294L, is(WearUtil.msTill(REF_NOW-6294L))); + + assertThat(0L, is(WearUtil.msTill(REF_NOW))); + assertThat(-3456L, is(WearUtil.msSince(REF_NOW+3456L))); + assertThat(6294L, is(WearUtil.msSince(REF_NOW-6294L))); + } + + @Test + public void joinSetTest() { + // GIVEN + Set refSet = new HashSet<>(); + refSet.add("element1"); + refSet.add("second-elem"); + refSet.add("3rd"); + + // WHEN + String joined = WearUtil.joinSet(refSet, "|"); + + // THEN + // we cannot guarantee order of items in joined string + // but all items have to be there + assertThat(joined.length(), is("element1".length() + "second-elem".length() + "3rd".length() + "|".length()*2 )); + + assertThat("|"+joined+"|", containsString("|"+"element1"+"|")); + assertThat("|"+joined+"|", containsString("|"+"second-elem"+"|")); + assertThat("|"+joined+"|", containsString("|"+"3rd"+"|")); + } + + @Test + public void explodeSetTest() { + // GIVEN + String serializedSet = "second-elem:element1:3rd"; + + // WHEN + Set set = WearUtil.explodeSet(serializedSet, ":"); + + // THEN + assertThat(set.size(), is(3)); + + assertTrue(set.contains("element1")); + assertTrue(set.contains("second-elem")); + assertTrue(set.contains("3rd")); + } + + @Test + public void explodeSetEmptyElemsTest() { + // GIVEN + String serializedSet = ",,,,real,,,another,,,"; + + // WHEN + Set set = WearUtil.explodeSet(serializedSet, ","); + + // THEN + assertThat(set.size(), is(2)); + + assertThat(true, is(set.contains("real"))); + assertThat(true, is(set.contains("another"))); + } + + @Test + public void joinExplodeStabilityTest() { + // GIVEN + Set refSet = new HashSet<>(); + refSet.add("element1"); + refSet.add("second-elem"); + refSet.add("3rd"); + refSet.add("czwarty"); + refSet.add("V"); + refSet.add("6"); + + // WHEN + String joinedSet = WearUtil.joinSet(refSet, "#"); + final Set explodedSet = WearUtil.explodeSet(joinedSet, "#"); + + // THEN + assertThat(explodedSet, is(refSet)); + } + + @Test + public void threadSleepTest() { + // GIVEN + final long testStart = System.currentTimeMillis(); + final long requestedSleepDuration = 85L; + final long measuringMargin = 100L; + + // WHEN + WearUtil.threadSleep(requestedSleepDuration); + final long measuredSleepDuration = System.currentTimeMillis() - testStart; + + // THEN + // we cannot guarantee to be exact to the millisecond - we add some margin of error + assertThat(measuredSleepDuration, is(both(greaterThan(60L)).and(lessThan(requestedSleepDuration+measuringMargin)))); + } + + @Test + public void rateLimitTest() { + // WHEN + final boolean firstCall = WearUtil.rateLimit("test-limit", 3); + final boolean callAfterward = WearUtil.rateLimit("test-limit", 3); + WearUtilMocker.progressClock(500L); + final boolean callTooSoon = WearUtil.rateLimit("test-limit", 3); + WearUtilMocker.progressClock(3100L); + final boolean callAfterRateLimit = WearUtil.rateLimit("test-limit", 3); + + // THEN + assertTrue(firstCall); + assertFalse(callAfterward); + assertFalse(callTooSoon); + assertTrue(callAfterRateLimit); + } + + /** + * It tests if mock for bundleToDataMap is sane, + * because original impl. of bundleToDataMap + * uses DataMap.fromBundle which need Android SDK runtime + */ + @Test + public void bundleToDataMapTest() { + // GIVEN + DataMap refMap = new DataMap(); + refMap.putString("ala", "ma kota"); + refMap.putInt("why", 42); + refMap.putFloatArray("list", new float[]{0.45f, 3.2f, 6.8f}); + + // WHEN + WearUtilMocker.prepareMockNoReal(); + Bundle bundle = BundleMock.mock(refMap); + DataMap gotMap = WearUtil.bundleToDataMap(bundle); + + // THEN + assertThat(gotMap, is(refMap)); + } + + +} \ No newline at end of file diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java new file mode 100644 index 0000000000..664b431b5c --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java @@ -0,0 +1,63 @@ +package info.nightscout.androidaps.testing.mockers; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.junit.Assert; +import org.mockito.ArgumentMatchers; +import org.mockito.invocation.InvocationOnMock; +import org.powermock.api.mockito.PowerMockito; + +import java.util.HashMap; +import java.util.Map; + +import info.nightscout.androidaps.aaps; +import info.nightscout.androidaps.testing.mocks.SharedPreferencesMock; + +import static org.mockito.Mockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +public class AAPSMocker { + + private static final Map mockedSharedPrefs = new HashMap<>(); + private static boolean unicodeComplicationsOn = true; + + public static void prepareMock() { + Context mockedContext = mock(Context.class); + mockStatic(aaps.class, InvocationOnMock::callRealMethod); + try { + PowerMockito.when(aaps.class, "getAppContext").thenReturn(mockedContext); + PowerMockito.when(mockedContext, "getSharedPreferences", ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()).thenAnswer(invocation -> { + + final String key = invocation.getArgument(0); + if (mockedSharedPrefs.containsKey(key)) { + return mockedSharedPrefs.get(key); + } else { + SharedPreferencesMock newPrefs = new SharedPreferencesMock(); + mockedSharedPrefs.put(key, newPrefs); + return newPrefs; + } + }); + PowerMockito.when(aaps.class, "areComplicationsUnicode").thenAnswer(invocation -> unicodeComplicationsOn); + + + } catch (Exception e) { + Assert.fail("Unable to mock objects: " + e.getMessage()); + } + + setMockedUnicodeComplicationsOn(true); + resetMockedSharedPrefs(); + } + + public static void resetMockedSharedPrefs() { + mockedSharedPrefs.clear(); + } + + public static void resetMockedSharedPrefs(String forKey) { + mockedSharedPrefs.remove(forKey); + } + + public static void setMockedUnicodeComplicationsOn(boolean setUnicodeOn) { + unicodeComplicationsOn = setUnicodeOn; + } +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java new file mode 100644 index 0000000000..62eb5c6301 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java @@ -0,0 +1,40 @@ +package info.nightscout.androidaps.testing.mockers; + +import org.junit.Assert; +import org.powermock.api.mockito.PowerMockito; + +import java.util.Base64; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +public class AndroidMocker { + + public static void mockBase64() { + mockStatic(android.util.Base64.class); + try { + PowerMockito.when(android.util.Base64.class, "decode", anyString(), anyInt()).thenAnswer(invocation -> { + + final String payload = invocation.getArgument(0); + try { + return Base64.getDecoder().decode(payload); + } catch (java.lang.IllegalArgumentException ex) { + return null; + } + }); + + PowerMockito.when(android.util.Base64.class, "encodeToString", any(), anyInt()).thenAnswer(invocation -> { + + final byte[] payload = invocation.getArgument(0); + return Base64.getEncoder().encodeToString(payload); + + }); + + } catch (Exception e) { + Assert.fail("Unable to mock objects: " + e.getMessage()); + } + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/LogMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/LogMocker.java new file mode 100644 index 0000000000..5c374665bc --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/LogMocker.java @@ -0,0 +1,11 @@ +package info.nightscout.androidaps.testing.mockers; + +import android.util.Log; + +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +public class LogMocker { + public static void prepareMock() { + mockStatic(Log.class); + } +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java new file mode 100644 index 0000000000..832720bbff --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java @@ -0,0 +1,65 @@ +package info.nightscout.androidaps.testing.mockers; + +import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.interaction.utils.SafeParse; + +import static info.nightscout.androidaps.testing.mockers.WearUtilMocker.backInTime; + +public class RawDataMocker { + + public static DisplayRawData rawSgv(String sgv, int m, String deltaString) { + DisplayRawData raw = new DisplayRawData(); + raw.datetime = backInTime(0, 0, m, 0); + raw.sDelta = deltaString; + raw.sSgv = sgv; + + double delta = SafeParse.stringToDouble(deltaString); + + if (delta <= (-3.5 * 5)) { + raw.sDirection = "\u21ca"; + } else if (delta <= (-2 * 5)) { + raw.sDirection = "\u2193"; + } else if (delta <= (-1 * 5)) { + raw.sDirection = "\u2198"; + } else if (delta <= (1 * 5)) { + raw.sDirection = "\u2192"; + } else if (delta <= (2 * 5)) { + raw.sDirection = "\u2197"; + } else if (delta <= (3.5 * 5)) { + raw.sDirection = "\u2191"; + } else { + raw.sDirection = "\u21c8"; + } + + return raw; + } + + public static DisplayRawData rawDelta(int m, String delta) { + DisplayRawData raw = new DisplayRawData(); + raw.datetime = backInTime(0, 0, m, 0); + raw.sDelta = delta; + return raw; + } + + public static DisplayRawData rawCobIobBr(String cob, String iob, String br) { + DisplayRawData raw = new DisplayRawData(); + raw.sCOB2 = cob; + raw.sIOB1 = iob; + raw.sBasalRate = br; + return raw; + } + + public static DisplayRawData rawIob(String iob, String iob2) { + DisplayRawData raw = new DisplayRawData(); + raw.sIOB1 = iob; + raw.sIOB2 = iob2; + return raw; + } + + public static DisplayRawData rawCob(String cob) { + DisplayRawData raw = new DisplayRawData(); + raw.sCOB2 = cob; + return raw; + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java new file mode 100644 index 0000000000..fbe13e52b6 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java @@ -0,0 +1,104 @@ +package info.nightscout.androidaps.testing.mockers; + +import android.os.Bundle; + +import com.google.android.gms.wearable.Asset; +import com.google.android.gms.wearable.DataMap; + +import org.junit.Assert; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; + +import java.util.ArrayList; + +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.WearUtil; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.powermock.api.mockito.PowerMockito.mockStatic; + +public class WearUtilMocker { + + public static final long REF_NOW = 1572610530000L; + private static long clockMsDiff = 0L; + + public static void prepareMock() { + resetClock(); + mockStatic(WearUtil.class, InvocationOnMock::callRealMethod); + try { + // because we cleverly used timestamp() by implementation, we can mock it + // and control the time in tests + PowerMockito.when(WearUtil.class, "timestamp").then(invocation -> (REF_NOW + clockMsDiff)); + } catch (Exception e) { + Assert.fail("Unable to mock the construction of the WearUtil object: " + e.getMessage()); + } + } + + public static void prepareMockNoReal() { + resetClock(); + mockStatic(WearUtil.class); + try { + PowerMockito.when(WearUtil.class, "timestamp").then(invocation -> REF_NOW + clockMsDiff); + PowerMockito.when(WearUtil.class, "getWakeLock", anyString(), anyInt()).then(invocation -> null); + PowerMockito.when(WearUtil.class, "bundleToDataMap", any(Bundle.class)).then(bundleToDataMapMock); + } catch (Exception e) { + Assert.fail("Unable to mock the construction of the WearUtil object: " + e.getMessage()); + } + } + + public static void resetClock() { + clockMsDiff = 0L; + } + + public static void progressClock(long byMilliseconds) { + clockMsDiff = clockMsDiff + byMilliseconds; + } + + public static void setClock(long atMillisecondsSinceEpoch) { + clockMsDiff = atMillisecondsSinceEpoch - REF_NOW; + } + + public static long backInTime(int d, int h, int m, int s) { + return REF_NOW - (Constants.DAY_IN_MS * d + Constants.HOUR_IN_MS * h + Constants.MINUTE_IN_MS * m + Constants.SECOND_IN_MS * s); + } + + private static Answer bundleToDataMapMock = invocation -> { + DataMap map = new DataMap(); + Bundle bundle = invocation.getArgument(0); + for(String key: bundle.keySet()) { + Object v = bundle.get(key); + if (v instanceof Asset) map.putAsset(key, (Asset)v); + if (v instanceof Boolean) map.putBoolean(key, (Boolean)v); + if (v instanceof Byte) map.putByte(key, (Byte)v); + if (v instanceof byte[]) map.putByteArray(key, (byte[])v); + if (v instanceof DataMap) map.putDataMap(key, (DataMap)v); + if (v instanceof Double) map.putDouble(key, (Double)v); + if (v instanceof Float) map.putFloat(key, (Float)v); + if (v instanceof float[]) map.putFloatArray(key, (float[])v); + if (v instanceof Integer) map.putInt(key, (Integer)v); + if (v instanceof Long) map.putLong(key, (Long)v); + if (v instanceof long[]) map.putLongArray(key, (long[])v); + if (v instanceof String) map.putString(key, (String)v); + if (v instanceof String[]) map.putStringArray(key, (String[])v); + + if (v instanceof ArrayList) { + if (!((ArrayList)v).isEmpty()) { + if (((ArrayList) v).get(0) instanceof Integer) { + map.putIntegerArrayList(key, (ArrayList)v); + } + if (((ArrayList) v).get(0) instanceof String) { + map.putStringArrayList(key, (ArrayList)v); + } + if (((ArrayList) v).get(0) instanceof DataMap) { + map.putDataMapArrayList(key, (ArrayList)v); + } + } + } + } + + return map; + }; +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mocks/BundleMock.java b/wear/src/test/java/info/nightscout/androidaps/testing/mocks/BundleMock.java new file mode 100644 index 0000000000..24449dd352 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mocks/BundleMock.java @@ -0,0 +1,233 @@ +package info.nightscout.androidaps.testing.mocks; + +import android.os.Bundle; +import android.os.Parcelable; +import android.util.SparseArray; + +import com.google.android.gms.wearable.DataMap; + +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyByte; +import static org.mockito.Matchers.anyChar; +import static org.mockito.Matchers.anyDouble; +import static org.mockito.Matchers.anyFloat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyShort; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +public final class BundleMock { + + public static Bundle mock() { + return mock(new HashMap()); + } + + public static Bundle mock(DataMap dataMap) { + HashMap hm = new HashMap<>(); + for (String key : dataMap.keySet()) { + hm.put(key, dataMap.get(key)); + } + return mock(hm); + } + + public static Bundle mock(final HashMap map) { + + Answer unsupported = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + throw new UnsupportedOperationException(); + } + }; + Answer put = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + map.put((String)invocation.getArguments()[0], invocation.getArguments()[1]); + return null; + } + }; + Answer get = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return map.get(invocation.getArguments()[0]); + } + }; + Answer getOrDefault = new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Object key = invocation.getArguments()[0]; + return map.containsKey(key) ? map.get(key) : invocation.getArguments()[1]; + } + }; + + Bundle bundle = Mockito.mock(Bundle.class); + + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return map.size(); + } + }).when(bundle).size(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return map.isEmpty(); + } + }).when(bundle).isEmpty(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + map.clear(); + return null; + } + }).when(bundle).clear(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return map.containsKey(invocation.getArguments()[0]); + } + }).when(bundle).containsKey(anyString()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return map.get(invocation.getArguments()[0]); + } + }).when(bundle).get(anyString()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + map.remove(invocation.getArguments()[0]); + return null; + } + }).when(bundle).remove(anyString()); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return map.keySet(); + } + }).when(bundle).keySet(); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return BundleMock.class.getSimpleName() + "{map=" + map.toString() + "}"; + } + }).when(bundle).toString(); + + doAnswer(put).when(bundle).putBoolean(anyString(), anyBoolean()); + when(bundle.getBoolean(anyString())).thenAnswer(get); + when(bundle.getBoolean(anyString(), anyBoolean())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putByte(anyString(), anyByte()); + when(bundle.getByte(anyString())).thenAnswer(get); + when(bundle.getByte(anyString(), anyByte())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putChar(anyString(), anyChar()); + when(bundle.getChar(anyString())).thenAnswer(get); + when(bundle.getChar(anyString(), anyChar())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putInt(anyString(), anyShort()); + when(bundle.getShort(anyString())).thenAnswer(get); + when(bundle.getShort(anyString(), anyShort())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putLong(anyString(), anyLong()); + when(bundle.getLong(anyString())).thenAnswer(get); + when(bundle.getLong(anyString(), anyLong())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putFloat(anyString(), anyFloat()); + when(bundle.getFloat(anyString())).thenAnswer(get); + when(bundle.getFloat(anyString(), anyFloat())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putDouble(anyString(), anyDouble()); + when(bundle.getDouble(anyString())).thenAnswer(get); + when(bundle.getDouble(anyString(), anyDouble())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putString(anyString(), anyString()); + when(bundle.getString(anyString())).thenAnswer(get); + when(bundle.getString(anyString(), anyString())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putBooleanArray(anyString(), any(boolean[].class)); + when(bundle.getBooleanArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putLongArray(anyString(), any(long[].class)); + when(bundle.getLongArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putDoubleArray(anyString(), any(double[].class)); + when(bundle.getDoubleArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putIntArray(anyString(), any(int[].class)); + when(bundle.getIntArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putInt(anyString(), anyInt()); + when(bundle.getInt(anyString())).thenAnswer(get); + when(bundle.getInt(anyString(), anyInt())).thenAnswer(getOrDefault); + + doAnswer(unsupported).when(bundle).putAll(any(Bundle.class)); + when(bundle.hasFileDescriptors()).thenAnswer(unsupported); + + doAnswer(put).when(bundle).putShort(anyString(), anyShort()); + when(bundle.getShort(anyString())).thenAnswer(get); + when(bundle.getShort(anyString(), anyShort())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putFloat(anyString(), anyFloat()); + when(bundle.getFloat(anyString())).thenAnswer(get); + when(bundle.getFloat(anyString(), anyFloat())).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putCharSequence(anyString(), any(CharSequence.class)); + when(bundle.getCharSequence(anyString())).thenAnswer(get); + when(bundle.getCharSequence(anyString(), any(CharSequence.class))).thenAnswer(getOrDefault); + + doAnswer(put).when(bundle).putBundle(anyString(), any(Bundle.class)); + when(bundle.getBundle(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putParcelable(anyString(), any(Parcelable.class)); + when(bundle.getParcelable(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putParcelableArray(anyString(), any(Parcelable[].class)); + when(bundle.getParcelableArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putParcelableArrayList(anyString(), any(ArrayList.class)); + when(bundle.getParcelableArrayList(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putSparseParcelableArray(anyString(), any(SparseArray.class)); + when(bundle.getSparseParcelableArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putSerializable(anyString(), any(Serializable.class)); + when(bundle.getSerializable(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putIntegerArrayList(anyString(), any(ArrayList.class)); + when(bundle.getIntegerArrayList(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putStringArrayList(anyString(), any(ArrayList.class)); + when(bundle.getStringArrayList(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putCharSequenceArrayList(anyString(), any(ArrayList.class)); + when(bundle.getCharSequenceArrayList(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putCharArray(anyString(), any(char[].class)); + when(bundle.getCharArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putByteArray(anyString(), any(byte[].class)); + when(bundle.getByteArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putShortArray(anyString(), any(short[].class)); + when(bundle.getShortArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putFloatArray(anyString(), any(float[].class)); + when(bundle.getFloatArray(anyString())).thenAnswer(get); + + doAnswer(put).when(bundle).putCharSequenceArray(anyString(), any(CharSequence[].class)); + when(bundle.getCharSequenceArray(anyString())).thenAnswer(get); + + return bundle; + } +} \ No newline at end of file diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mocks/IntentMock.java b/wear/src/test/java/info/nightscout/androidaps/testing/mocks/IntentMock.java new file mode 100644 index 0000000000..c82a43fed9 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mocks/IntentMock.java @@ -0,0 +1,39 @@ +package info.nightscout.androidaps.testing.mocks; + +import android.content.Intent; +import android.os.Bundle; + +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import java.util.HashMap; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.when; + +public final class IntentMock { + + public static Intent mock() { + return mock(new HashMap()); + } + + public static Intent mock(final HashMap map) { + + Answer put = invocation -> { + map.put((String)invocation.getArguments()[0], invocation.getArguments()[1]); + return null; + }; + Answer get = invocation -> map.get(invocation.getArguments()[0]); + + Intent intent = Mockito.mock(Intent.class); + + when(intent.putExtra(anyString(), any(Bundle.class))).thenAnswer(put); + when(intent.getBundleExtra(anyString())).thenAnswer(get); + + doAnswer(invocation -> map.containsKey(invocation.getArguments()[0])).when(intent).hasExtra(anyString()); + + return intent; + } +} \ No newline at end of file diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mocks/SharedPreferencesMock.java b/wear/src/test/java/info/nightscout/androidaps/testing/mocks/SharedPreferencesMock.java new file mode 100644 index 0000000000..5b0736a450 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mocks/SharedPreferencesMock.java @@ -0,0 +1,158 @@ +package info.nightscout.androidaps.testing.mocks; + +import android.content.SharedPreferences; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class SharedPreferencesMock implements SharedPreferences { + + private final EditorInternals editor = new EditorInternals(); + + class EditorInternals implements Editor { + + Map innerMap = new HashMap<>(); + + @Override + public Editor putString(String k, @Nullable String v) { + innerMap.put(k, v); + return this; + } + + @Override + public Editor putStringSet(String k, @Nullable Set set) { + innerMap.put(k, set); + return this; + } + + @Override + public Editor putInt(String k, int i) { + innerMap.put(k, i); + return this; + } + + @Override + public Editor putLong(String k, long l) { + innerMap.put(k, l); + return this; + } + + @Override + public Editor putFloat(String k, float v) { + innerMap.put(k, v); + return this; + } + + @Override + public Editor putBoolean(String k, boolean b) { + innerMap.put(k, b); + return this; + } + + @Override + public Editor remove(String k) { + innerMap.remove(k); + return this; + } + + @Override + public Editor clear() { + innerMap.clear(); + return this; + } + + @Override + public boolean commit() { + return true; + } + + @Override + public void apply() { + + } + } + + @Override + public Map getAll() { + return editor.innerMap; + } + + @Nullable + @Override + public String getString(String k, @Nullable String s) { + if (editor.innerMap.containsKey(k)) { + return (String) editor.innerMap.get(k); + } else { + return s; + } + } + + @Nullable + @Override + public Set getStringSet(String k, @Nullable Set set) { + if (editor.innerMap.containsKey(k)) { + return (Set) editor.innerMap.get(k); + } else { + return set; + } + } + + @Override + public int getInt(String k, int i) { + if (editor.innerMap.containsKey(k)) { + return (Integer) editor.innerMap.get(k); + } else { + return i; + } + } + + @Override + public long getLong(String k, long l) { + if (editor.innerMap.containsKey(k)) { + return (Long) editor.innerMap.get(k); + } else { + return l; + } + } + + @Override + public float getFloat(String k, float v) { + if (editor.innerMap.containsKey(k)) { + return (Float) editor.innerMap.get(k); + } else { + return v; + } + } + + @Override + public boolean getBoolean(String k, boolean b) { + if (editor.innerMap.containsKey(k)) { + return (Boolean) editor.innerMap.get(k); + } else { + return b; + } + } + + @Override + public boolean contains(String k) { + return editor.innerMap.containsKey(k); + } + + @Override + public Editor edit() { + return editor; + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) { + + } + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener onSharedPreferenceChangeListener) { + + } +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java new file mode 100644 index 0000000000..0739e8ac9d --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java @@ -0,0 +1,70 @@ +package info.nightscout.androidaps.testing.utils; + +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import info.nightscout.androidaps.data.BasalWatchData; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class BasalWatchDataExt extends BasalWatchData { + + private BasalWatchDataExt() { + super(); + } + + public BasalWatchDataExt(BasalWatchData ref) { + super(); + + Set parentFields = new HashSet<>(); + for (Field f : BasalWatchData.class.getDeclaredFields()) { + parentFields.add(f.getName()); + } + + Set knownFields = new HashSet<>(Arrays.asList("startTime,endTime,amount".split(","))); + + // since we do not want modify BasalWatchData - we use this wrapper class + // but we make sure it has same fields + assertThat(parentFields, is(knownFields)); + + this.startTime = ref.startTime; + this.endTime = ref.endTime; + this.amount = ref.amount; + } + + public static BasalWatchDataExt build(long startTime, long endTime, double amount) { + BasalWatchDataExt bwd = new BasalWatchDataExt(); + bwd.startTime = startTime; + bwd.endTime = endTime; + bwd.amount = amount; + return bwd; + } + + @Override + public boolean equals(@Nullable Object obj) { + if ((obj instanceof BasalWatchData)||(obj instanceof BasalWatchDataExt)) { + return (this.startTime == ((BasalWatchData) obj).startTime) + && (this.endTime == ((BasalWatchData) obj).endTime) + && (this.amount == ((BasalWatchData) obj).amount); + } else { + return false; + } + } + + @Override + public String toString() { + return startTime+", "+endTime+", "+amount; + } + + @Override + public int hashCode() { + return Objects.hash(startTime, endTime, amount); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java new file mode 100644 index 0000000000..0c1d4a0bf3 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java @@ -0,0 +1,78 @@ +package info.nightscout.androidaps.testing.utils; + +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import info.nightscout.androidaps.data.BgWatchData; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class BgWatchDataExt extends BgWatchData { + + private BgWatchDataExt() { + super(); + } + + public BgWatchDataExt(double aSgv, double aHigh, double aLow, long aTimestamp, int aColor) { + super(aSgv, aHigh, aLow, aTimestamp, aColor); + } + + public BgWatchDataExt(BgWatchData ref) { + super(); + + Set parentFields = new HashSet<>(); + for (Field f : BgWatchData.class.getDeclaredFields()) { + parentFields.add(f.getName()); + } + + Set knownFields = new HashSet<>(Arrays.asList("sgv,high,low,timestamp,color".split(","))); + + // since we do not want modify BgWatchDataExt - we use this wrapper class + // but we make sure it has same fields + assertThat(parentFields, is(knownFields)); + + this.sgv = ref.sgv; + this.high = ref.high; + this.low = ref.low; + this.timestamp = ref.timestamp; + this.color = ref.color; + } + + public static BgWatchDataExt build(double sgv, long timestamp, int color) { + BgWatchDataExt twd = new BgWatchDataExt(); + twd.sgv = sgv; + twd.timestamp = timestamp; + twd.color = color; + return twd; + } + + @Override + public boolean equals(@Nullable Object obj) { + if ((obj instanceof BgWatchData)||(obj instanceof BgWatchDataExt)) { + return (this.sgv == ((BgWatchData) obj).sgv) + && (this.high == ((BgWatchData) obj).high) + && (this.low == ((BgWatchData) obj).low) + && (this.timestamp == ((BgWatchData) obj).timestamp) + && (this.color == ((BgWatchData) obj).color); + } else { + return false; + } + } + + @Override + public String toString() { + return sgv+", "+high+", "+low+", "+timestamp+", "+color; + } + + @Override + public int hashCode() { + return Objects.hash(sgv, high, low, timestamp, color); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java new file mode 100644 index 0000000000..13e6f52f8f --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java @@ -0,0 +1,76 @@ +package info.nightscout.androidaps.testing.utils; + +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import info.nightscout.androidaps.data.BolusWatchData; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class BolusWatchDataExt extends BolusWatchData { + + private BolusWatchDataExt() { + super(); + } + + public BolusWatchDataExt(BolusWatchData ref) { + super(); + + Set parentFields = new HashSet<>(); + for (Field f : BolusWatchData.class.getDeclaredFields()) { + parentFields.add(f.getName()); + } + + Set knownFields = new HashSet<>(Arrays.asList("date,bolus,carbs,isSMB,isValid".split(","))); + + // since we do not want modify BolusWatchData - we use this wrapper class + // but we make sure it has same fields + assertThat(parentFields, is(knownFields)); + + this.date = ref.date; + this.bolus = ref.bolus; + this.carbs = ref.carbs; + this.isSMB = ref.isSMB; + this.isValid = ref.isValid; + } + + public static BolusWatchDataExt build(long date, double bolus, double carbs, boolean isSMB, boolean isValid) { + BolusWatchDataExt bwd = new BolusWatchDataExt(); + bwd.date = date; + bwd.bolus = bolus; + bwd.carbs = carbs; + bwd.isSMB = isSMB; + bwd.isValid = isValid; + return bwd; + } + + @Override + public boolean equals(@Nullable Object obj) { + if ((obj instanceof BolusWatchData)||(obj instanceof BolusWatchDataExt)) { + return (this.date == ((BolusWatchData) obj).date) + && (this.bolus == ((BolusWatchData) obj).bolus) + && (this.carbs == ((BolusWatchData) obj).carbs) + && (this.isSMB == ((BolusWatchData) obj).isSMB) + && (this.isValid == ((BolusWatchData) obj).isValid); + } else { + return false; + } + } + + @Override + public String toString() { + return date+", "+bolus+", "+carbs+", "+isSMB+", "+isValid; + } + + @Override + public int hashCode() { + return Objects.hash(date, bolus, carbs, isSMB, isValid); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java new file mode 100644 index 0000000000..171d8ac841 --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java @@ -0,0 +1,78 @@ +package info.nightscout.androidaps.testing.utils; + +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import info.nightscout.androidaps.data.TempWatchData; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + + +public class TempWatchDataExt extends TempWatchData { + + private TempWatchDataExt() { + super(); + } + + public TempWatchDataExt(TempWatchData ref) { + super(); + + Set parentFields = new HashSet<>(); + for (Field f : TempWatchData.class.getDeclaredFields()) { + parentFields.add(f.getName()); + } + + Set knownFields = new HashSet<>(Arrays.asList("startTime,startBasal,endTime,endBasal,amount".split(","))); + + // since we do not want modify TempWatchData - we use this wrapper class + // but we make sure it has same fields + assertThat(parentFields, is(knownFields)); + + this.startTime = ref.startTime; + this.startBasal = ref.startBasal; + this.endTime = ref.endTime; + this.endBasal = ref.endBasal; + this.amount = ref.amount; + } + + public static TempWatchDataExt build(long startTime, double startBasal, long endTime, + double endBasal, double amount) { + TempWatchDataExt twd = new TempWatchDataExt(); + twd.startTime = startTime; + twd.startBasal = startBasal; + twd.endTime = endTime; + twd.endBasal = endBasal; + twd.amount = amount; + return twd; + } + + @Override + public boolean equals(@Nullable Object obj) { + if ((obj instanceof TempWatchData)||(obj instanceof TempWatchDataExt)) { + return (this.startTime == ((TempWatchData) obj).startTime) + && (this.startBasal == ((TempWatchData) obj).startBasal) + && (this.endTime == ((TempWatchData) obj).endTime) + && (this.endBasal == ((TempWatchData) obj).endBasal) + && (this.amount == ((TempWatchData) obj).amount); + } else { + return false; + } + } + + @Override + public String toString() { + return startTime+", "+startBasal+", "+endTime+", "+endBasal+", "+amount; + } + + @Override + public int hashCode() { + return Objects.hash(startTime, startBasal, endTime, endBasal, amount); + } + +} From 0e370fee028381ba68e339ea89d3a8f01b592bef Mon Sep 17 00:00:00 2001 From: dlvoy Date: Tue, 26 Nov 2019 10:49:01 +0100 Subject: [PATCH 6/7] [#2210][#728] Ignoring JaCoCo runtime-injected fields that affected tests --- .../testing/utils/BasalWatchDataExt.java | 16 ++--------- .../testing/utils/BgWatchDataExt.java | 16 ++--------- .../testing/utils/BolusWatchDataExt.java | 16 ++--------- .../androidaps/testing/utils/ExtUtil.java | 28 +++++++++++++++++++ .../testing/utils/TempWatchDataExt.java | 18 ++---------- 5 files changed, 37 insertions(+), 57 deletions(-) create mode 100644 wear/src/test/java/info/nightscout/androidaps/testing/utils/ExtUtil.java diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java index 0739e8ac9d..9a23f491d9 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BasalWatchDataExt.java @@ -2,16 +2,11 @@ package info.nightscout.androidaps.testing.utils; import androidx.annotation.Nullable; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; import info.nightscout.androidaps.data.BasalWatchData; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static info.nightscout.androidaps.testing.utils.ExtUtil.assertClassHaveSameFields; public class BasalWatchDataExt extends BasalWatchData { @@ -22,16 +17,9 @@ public class BasalWatchDataExt extends BasalWatchData { public BasalWatchDataExt(BasalWatchData ref) { super(); - Set parentFields = new HashSet<>(); - for (Field f : BasalWatchData.class.getDeclaredFields()) { - parentFields.add(f.getName()); - } - - Set knownFields = new HashSet<>(Arrays.asList("startTime,endTime,amount".split(","))); - // since we do not want modify BasalWatchData - we use this wrapper class // but we make sure it has same fields - assertThat(parentFields, is(knownFields)); + assertClassHaveSameFields(BasalWatchData.class, "startTime,endTime,amount"); this.startTime = ref.startTime; this.endTime = ref.endTime; diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java index 0c1d4a0bf3..c49125809e 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BgWatchDataExt.java @@ -2,16 +2,11 @@ package info.nightscout.androidaps.testing.utils; import androidx.annotation.Nullable; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; import info.nightscout.androidaps.data.BgWatchData; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static info.nightscout.androidaps.testing.utils.ExtUtil.assertClassHaveSameFields; public class BgWatchDataExt extends BgWatchData { @@ -26,16 +21,9 @@ public class BgWatchDataExt extends BgWatchData { public BgWatchDataExt(BgWatchData ref) { super(); - Set parentFields = new HashSet<>(); - for (Field f : BgWatchData.class.getDeclaredFields()) { - parentFields.add(f.getName()); - } - - Set knownFields = new HashSet<>(Arrays.asList("sgv,high,low,timestamp,color".split(","))); - // since we do not want modify BgWatchDataExt - we use this wrapper class // but we make sure it has same fields - assertThat(parentFields, is(knownFields)); + assertClassHaveSameFields(BgWatchData.class, "sgv,high,low,timestamp,color"); this.sgv = ref.sgv; this.high = ref.high; diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java index 13e6f52f8f..a5553f6a59 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/BolusWatchDataExt.java @@ -2,16 +2,11 @@ package info.nightscout.androidaps.testing.utils; import androidx.annotation.Nullable; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; import info.nightscout.androidaps.data.BolusWatchData; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static info.nightscout.androidaps.testing.utils.ExtUtil.assertClassHaveSameFields; public class BolusWatchDataExt extends BolusWatchData { @@ -22,16 +17,9 @@ public class BolusWatchDataExt extends BolusWatchData { public BolusWatchDataExt(BolusWatchData ref) { super(); - Set parentFields = new HashSet<>(); - for (Field f : BolusWatchData.class.getDeclaredFields()) { - parentFields.add(f.getName()); - } - - Set knownFields = new HashSet<>(Arrays.asList("date,bolus,carbs,isSMB,isValid".split(","))); - // since we do not want modify BolusWatchData - we use this wrapper class // but we make sure it has same fields - assertThat(parentFields, is(knownFields)); + assertClassHaveSameFields(BolusWatchData.class, "date,bolus,carbs,isSMB,isValid"); this.date = ref.date; this.bolus = ref.bolus; diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/ExtUtil.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/ExtUtil.java new file mode 100644 index 0000000000..1559db11ae --- /dev/null +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/ExtUtil.java @@ -0,0 +1,28 @@ +package info.nightscout.androidaps.testing.utils; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +class ExtUtil { + + static void assertClassHaveSameFields(Class checkedClass, String commaSeparatedFieldList) { + Set parentFields = new HashSet<>(); + for (Field f : checkedClass.getDeclaredFields()) { + final String fieldName = f.getName(); + // skip runtime-injected fields like $jacocoData + if (fieldName.startsWith("$")) { + continue; + } + parentFields.add(fieldName); + } + + Set knownFields = new HashSet<>(Arrays.asList(commaSeparatedFieldList.split(","))); + assertThat(parentFields, is(knownFields)); + } + +} diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java b/wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java index 171d8ac841..478c6de966 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/utils/TempWatchDataExt.java @@ -2,16 +2,11 @@ package info.nightscout.androidaps.testing.utils; import androidx.annotation.Nullable; -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.HashSet; import java.util.Objects; -import java.util.Set; import info.nightscout.androidaps.data.TempWatchData; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertThat; +import static info.nightscout.androidaps.testing.utils.ExtUtil.assertClassHaveSameFields; public class TempWatchDataExt extends TempWatchData { @@ -23,16 +18,9 @@ public class TempWatchDataExt extends TempWatchData { public TempWatchDataExt(TempWatchData ref) { super(); - Set parentFields = new HashSet<>(); - for (Field f : TempWatchData.class.getDeclaredFields()) { - parentFields.add(f.getName()); - } - - Set knownFields = new HashSet<>(Arrays.asList("startTime,startBasal,endTime,endBasal,amount".split(","))); - - // since we do not want modify TempWatchData - we use this wrapper class + // since we do not want modify BolusWatchData - we use this wrapper class // but we make sure it has same fields - assertThat(parentFields, is(knownFields)); + assertClassHaveSameFields(TempWatchData.class, "startTime,startBasal,endTime,endBasal,amount"); this.startTime = ref.startTime; this.startBasal = ref.startBasal; From f7b556350cf962ed3af6e16f9bb02dfeb61430ca Mon Sep 17 00:00:00 2001 From: dlvoy Date: Wed, 27 Nov 2019 20:32:42 +0100 Subject: [PATCH 7/7] [#2210][#728] Post-review refactoring (thanks @jotomo !) --- .../BaseComplicationProviderService.java | 40 +- .../complications/BrCobIobComplication.java | 14 +- .../CobDetailedComplication.java | 4 +- .../complications/CobIconComplication.java | 4 +- .../complications/CobIobComplication.java | 8 +- .../IobDetailedComplication.java | 4 +- .../complications/IobIconComplication.java | 8 +- .../complications/LongStatusComplication.java | 4 +- .../LongStatusFlippedComplication.java | 5 +- .../complications/SgvComplication.java | 4 +- .../complications/UploaderBattery.java | 4 +- .../complications/WallpaperComplication.java | 5 +- .../androidaps/data/ListenerService.java | 6 +- ...isplayRawData.java => RawDisplayData.java} | 544 +++++++++--------- .../interaction/utils/DisplayFormat.java | 56 +- .../interaction/utils/Inevitable.java | 11 +- .../interaction/utils/Persistence.java | 8 +- .../interaction/utils/WearUtil.java | 14 +- .../androidaps/watchfaces/BaseWatchFace.java | 4 +- .../androidaps/watchfaces/BgGraphBuilder.java | 7 +- .../androidaps/data/BgWatchDataTest.java | 2 +- ...st.java => RawDataSgvDisplayDataTest.java} | 24 +- ...est.java => RawDisplayDataBasalsTest.java} | 24 +- ....java => RawDisplayDataBgEntriesTest.java} | 8 +- ...est.java => RawDisplayDataStatusTest.java} | 26 +- .../interaction/utils/DisplayFormatTest.java | 6 +- .../interaction/utils/PersistenceTest.java | 8 +- .../interaction/utils/WearUtilTest.java | 12 +- .../testing/mockers/AAPSMocker.java | 31 +- .../testing/mockers/AndroidMocker.java | 30 +- .../testing/mockers/RawDataMocker.java | 22 +- .../testing/mockers/WearUtilMocker.java | 26 +- 32 files changed, 477 insertions(+), 496 deletions(-) rename wear/src/main/java/info/nightscout/androidaps/data/{DisplayRawData.java => RawDisplayData.java} (96%) rename wear/src/test/java/info/nightscout/androidaps/data/{DisplayRawDataSgvDataTest.java => RawDataSgvDisplayDataTest.java} (86%) rename wear/src/test/java/info/nightscout/androidaps/data/{DisplayRawDataBasalsTest.java => RawDisplayDataBasalsTest.java} (93%) rename wear/src/test/java/info/nightscout/androidaps/data/{DisplayRawDataBgEntriesTest.java => RawDisplayDataBgEntriesTest.java} (96%) rename wear/src/test/java/info/nightscout/androidaps/data/{DisplayRawDataStatusTest.java => RawDisplayDataStatusTest.java} (88%) diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java b/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java index c1f33e41c5..49f8f82837 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/BaseComplicationProviderService.java @@ -20,7 +20,7 @@ import java.util.Set; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import info.nightscout.androidaps.R; import info.nightscout.androidaps.aaps; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.data.ListenerService; import info.nightscout.androidaps.interaction.utils.Constants; import info.nightscout.androidaps.interaction.utils.DisplayFormat; @@ -38,7 +38,7 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid private static final String TAG = BaseComplicationProviderService.class.getSimpleName(); private static final String KEY_COMPLICATIONS = "complications"; - private static final String KEY_LAST_SINCE = "lastSince"; + private static final String KEY_LAST_SHOWN_SINCE_VALUE = "lastSince"; private static final String KEY_STALE_REPORTED = "staleReported"; private static final String TASK_ID_REFRESH_COMPLICATION = "refresh-complication"; @@ -56,7 +56,7 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid // ABSTRACT COMPLICATION INTERFACE //============================================================================================== - public abstract ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent); + public abstract ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent); public abstract String getProviderCanonicalName(); public ComplicationAction getComplicationAction() { return ComplicationAction.MENU; }; @@ -66,11 +66,11 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid //---------------------------------------------------------------------------------------------- public ComplicationData buildNoSyncComplicationData(int dataType, - DisplayRawData raw, + RawDisplayData raw, PendingIntent complicationPendingIntent, PendingIntent exceptionalPendingIntent, long since) { - ComplicationData complicationData = null; + final ComplicationData.Builder builder = new ComplicationData.Builder(dataType); if (dataType != ComplicationData.TYPE_LARGE_IMAGE) { @@ -111,16 +111,14 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid } builder.setTapAction(exceptionalPendingIntent); - complicationData = builder.build(); - return complicationData; + return builder.build(); } public ComplicationData buildOutdatedComplicationData(int dataType, - DisplayRawData raw, + RawDisplayData raw, PendingIntent complicationPendingIntent, PendingIntent exceptionalPendingIntent, long since) { - ComplicationData complicationData = null; final ComplicationData.Builder builder = new ComplicationData.Builder(dataType); if (dataType != ComplicationData.TYPE_LARGE_IMAGE) { @@ -162,8 +160,7 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid } builder.setTapAction(exceptionalPendingIntent); - complicationData = builder.build(); - return complicationData; + return builder.build(); } /** @@ -230,12 +227,12 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid final Persistence persistence = new Persistence(); - final DisplayRawData raw = new DisplayRawData(); - raw.partialUpdateFromPersistence(persistence); + final RawDisplayData raw = new RawDisplayData(); + raw.updateForComplicationsFromPersistence(persistence); Log.d(TAG, "Complication data: " + raw.toDebugString()); - // store what is currently rendered since field, to detect it need update - persistence.putString(KEY_LAST_SINCE, DisplayFormat.shortTimeSince(raw.datetime)); + // store what is currently rendered in 'SGV since' field, to detect if it was changed and need update + persistence.putString(KEY_LAST_SHOWN_SINCE_VALUE, DisplayFormat.shortTimeSince(raw.datetime)); // by each render we clear stale flag to ensure it is re-rendered at next refresh detection round persistence.putBoolean(KEY_STALE_REPORTED, false); @@ -259,7 +256,6 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid if (complicationData != null) { complicationManager.updateComplicationData(complicationId, complicationData); - } else { // If no data is sent, we still need to inform the ComplicationManager, so the update // job can finish and the wake lock isn't held any longer than necessary. @@ -297,7 +293,7 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid Log.d(TAG, "Pending check if update needed - "+p.getString(KEY_COMPLICATIONS, "")); Inevitable.task(TASK_ID_REFRESH_COMPLICATION, 15 * Constants.SECOND_IN_MS, () -> { - if (WearUtil.rateLimit("complication-checkIfUpdateNeeded", 5)) { + if (WearUtil.isBelowRateLimit("complication-checkIfUpdateNeeded", 5)) { Log.d(TAG, "Checking if update needed"); requestUpdateIfSinceChanged(); // We reschedule need for check - to make sure next check will Inevitable go in next 15s @@ -314,10 +310,10 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid private static void requestUpdateIfSinceChanged() { final Persistence persistence = new Persistence(); - final DisplayRawData raw = new DisplayRawData(); - raw.partialUpdateFromPersistence(persistence); + final RawDisplayData raw = new RawDisplayData(); + raw.updateForComplicationsFromPersistence(persistence); - final String lastSince = persistence.getString(KEY_LAST_SINCE, "-"); + final String lastSince = persistence.getString(KEY_LAST_SHOWN_SINCE_VALUE, "-"); final String calcSince = DisplayFormat.shortTimeSince(raw.datetime); final boolean isStale = (WearUtil.msSince(persistence.whenDataUpdated()) > Constants.STALE_MS) ||(WearUtil.msSince(raw.datetime) > Constants.STALE_MS); @@ -326,7 +322,7 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid final boolean sinceWasChanged = !lastSince.equals(calcSince); if (sinceWasChanged|| (isStale && !staleWasRefreshed)) { - persistence.putString(KEY_LAST_SINCE, calcSince); + persistence.putString(KEY_LAST_SHOWN_SINCE_VALUE, calcSince); persistence.putBoolean(KEY_STALE_REPORTED, isStale); Log.d(TAG, "Detected refresh of time needed! Reason: " @@ -351,7 +347,7 @@ public abstract class BaseComplicationProviderService extends ComplicationProvid Log.d(TAG, "Pending update of "+provider); // We wait with updating allowing all request, from various sources, to arrive Inevitable.task("update-req-"+provider, 700, () -> { - if (WearUtil.rateLimit("update-req-"+provider, 2)) { + if (WearUtil.isBelowRateLimit("update-req-"+provider, 2)) { Log.d(TAG, "Requesting update of "+provider); final ComponentName componentName = new ComponentName(aaps.getAppContext(), provider); final ProviderUpdateRequester providerUpdateRequester = new ProviderUpdateRequester(aaps.getAppContext(), componentName); diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java index e9f05c5f79..ec862f4f47 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.java @@ -5,13 +5,13 @@ import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; -import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_SHORT_FIELD; -import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MIN_COB_FIELD; -import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MIN_IOB_FIELD; +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_FIELD_LEN_SHORT; +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MIN_FIELD_LEN_COB; +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MIN_FIELD_LEN_IOB; /* * Created by dlvoy on 2019-11-12 @@ -20,13 +20,13 @@ public class BrCobIobComplication extends BaseComplicationProviderService { private static final String TAG = BrCobIobComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; if (dataType == ComplicationData.TYPE_SHORT_TEXT) { - final String cob = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE).minimise(MIN_COB_FIELD); - final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_IOB_FIELD, (MAX_SHORT_FIELD-1) - cob.length())); + final String cob = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE).minimise(MIN_FIELD_LEN_COB); + final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_FIELD_LEN_IOB, (MAX_FIELD_LEN_SHORT -1) - cob.length())); final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) .setShortText(ComplicationText.plainText(DisplayFormat.basalRateSymbol()+raw.sBasalRate)) diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java index ea35b6f45f..350ec9b0ec 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobDetailedComplication.java @@ -5,7 +5,7 @@ import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; import info.nightscout.androidaps.interaction.utils.Pair; @@ -16,7 +16,7 @@ public class CobDetailedComplication extends BaseComplicationProviderService { private static final String TAG = CobDetailedComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java index 40824fd313..e42f46e3a4 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobIconComplication.java @@ -7,7 +7,7 @@ import android.support.wearable.complications.ComplicationText; import android.util.Log; import info.nightscout.androidaps.R; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; /* * Created by dlvoy on 2019-11-12 @@ -16,7 +16,7 @@ public class CobIconComplication extends BaseComplicationProviderService { private static final String TAG = CobIconComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java index 1f7dff7ceb..31ea4dc5f4 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.java @@ -5,10 +5,10 @@ import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; -import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_SHORT_FIELD; +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_FIELD_LEN_SHORT; /* * Created by dlvoy on 2019-11-12 @@ -17,13 +17,13 @@ public class CobIobComplication extends BaseComplicationProviderService { private static final String TAG = CobIobComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; if (dataType == ComplicationData.TYPE_SHORT_TEXT) { final String cob = raw.sCOB2; - final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_SHORT_FIELD); + final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_FIELD_LEN_SHORT); final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) .setShortText(ComplicationText.plainText(cob)) diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java index 790d63aa67..db1d67a66c 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/IobDetailedComplication.java @@ -5,7 +5,7 @@ import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; import info.nightscout.androidaps.interaction.utils.Pair; @@ -16,7 +16,7 @@ public class IobDetailedComplication extends BaseComplicationProviderService { private static final String TAG = IobDetailedComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java index ba253a048d..1dca0f2e4d 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.java @@ -7,10 +7,10 @@ import android.support.wearable.complications.ComplicationText; import android.util.Log; import info.nightscout.androidaps.R; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; -import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_SHORT_FIELD; +import static info.nightscout.androidaps.interaction.utils.DisplayFormat.MAX_FIELD_LEN_SHORT; /* * Created by dlvoy on 2019-11-12 @@ -19,12 +19,12 @@ public class IobIconComplication extends BaseComplicationProviderService { private static final String TAG = IobIconComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; if (dataType == ComplicationData.TYPE_SHORT_TEXT) { - final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_SHORT_FIELD); + final String iob = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_FIELD_LEN_SHORT); final ComplicationData.Builder builder = new ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) .setShortText(ComplicationText.plainText(iob)) diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java index 834f40b0a7..f18ad21662 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusComplication.java @@ -5,7 +5,7 @@ import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; /* @@ -15,7 +15,7 @@ public class LongStatusComplication extends BaseComplicationProviderService { private static final String TAG = LongStatusComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java index da3384d358..7c0b730c76 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/LongStatusFlippedComplication.java @@ -5,9 +5,8 @@ import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; -import info.nightscout.androidaps.interaction.utils.SmallestDoubleString; /* * Created by dlvoy on 2019-11-12 @@ -16,7 +15,7 @@ public class LongStatusFlippedComplication extends BaseComplicationProviderServi private static final String TAG = LongStatusFlippedComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java index b6f9e94946..0296f8bab6 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/SgvComplication.java @@ -5,7 +5,7 @@ import android.support.wearable.complications.ComplicationData; import android.support.wearable.complications.ComplicationText; import android.util.Log; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.DisplayFormat; /* @@ -15,7 +15,7 @@ public class SgvComplication extends BaseComplicationProviderService { private static final String TAG = SgvComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java b/wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java index 4b268ef9c7..a5fcedacdd 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/UploaderBattery.java @@ -8,7 +8,7 @@ import android.util.Log; import androidx.annotation.DrawableRes; import info.nightscout.androidaps.R; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; /* * Created by dlvoy on 2019-11-12 @@ -17,7 +17,7 @@ public class UploaderBattery extends BaseComplicationProviderService { private static final String TAG = UploaderBattery.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java index 2448bebee0..293b1331ca 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java +++ b/wear/src/main/java/info/nightscout/androidaps/complications/WallpaperComplication.java @@ -15,7 +15,7 @@ import java.io.IOException; import java.io.InputStream; import info.nightscout.androidaps.aaps; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; /* * Created by dlvoy on 2019-11-12 @@ -26,7 +26,7 @@ public abstract class WallpaperComplication extends BaseComplicationProviderServ private static final String TAG = WallpaperComplication.class.getSimpleName(); - public ComplicationData buildComplicationData(int dataType, DisplayRawData raw, PendingIntent complicationPendingIntent) { + public ComplicationData buildComplicationData(int dataType, RawDisplayData raw, PendingIntent complicationPendingIntent) { ComplicationData complicationData = null; @@ -48,7 +48,6 @@ public abstract class WallpaperComplication extends BaseComplicationProviderServ builder.setLargeImage(Icon.createWithBitmap(scaled)); } catch (IOException e) { Log.e(TAG, "Cannot read wallpaper asset: "+e.getMessage(), e); - e.printStackTrace(); } complicationData = builder.build(); diff --git a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java index 7bd43a79e1..9e018516ba 100644 --- a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java +++ b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java @@ -513,14 +513,14 @@ public class ListenerService extends WearableListenerService implements GoogleAp Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("status", dataMap.toBundle()); - Persistence.storeDataMap(DisplayRawData.STATUS_PERSISTENCE_KEY, dataMap); + Persistence.storeDataMap(RawDisplayData.STATUS_PERSISTENCE_KEY, dataMap); LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } else if (path.equals(BASAL_DATA_PATH)){ dataMap = DataMapItem.fromDataItem(event.getDataItem()).getDataMap(); Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("basals", dataMap.toBundle()); - Persistence.storeDataMap(DisplayRawData.BASALS_PERSISTENCE_KEY, dataMap); + Persistence.storeDataMap(RawDisplayData.BASALS_PERSISTENCE_KEY, dataMap); LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } else if (path.equals(NEW_PREFERENCES_PATH)){ dataMap = DataMapItem.fromDataItem(event.getDataItem()).getDataMap(); @@ -544,7 +544,7 @@ public class ListenerService extends WearableListenerService implements GoogleAp Intent messageIntent = new Intent(); messageIntent.setAction(Intent.ACTION_SEND); messageIntent.putExtra("data", dataMap.toBundle()); - Persistence.storeDataMap(DisplayRawData.DATA_PERSISTENCE_KEY, dataMap); + Persistence.storeDataMap(RawDisplayData.DATA_PERSISTENCE_KEY, dataMap); LocalBroadcastManager.getInstance(this).sendBroadcast(messageIntent); } } diff --git a/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java b/wear/src/main/java/info/nightscout/androidaps/data/RawDisplayData.java similarity index 96% rename from wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java rename to wear/src/main/java/info/nightscout/androidaps/data/RawDisplayData.java index 97183cd689..d4e3580561 100644 --- a/wear/src/main/java/info/nightscout/androidaps/data/DisplayRawData.java +++ b/wear/src/main/java/info/nightscout/androidaps/data/RawDisplayData.java @@ -1,272 +1,272 @@ -package info.nightscout.androidaps.data; - -import android.content.Intent; -import android.os.Bundle; -import android.os.PowerManager; - -import com.google.android.gms.wearable.DataMap; - -import java.util.ArrayList; -import java.util.Iterator; - -import info.nightscout.androidaps.interaction.utils.Constants; -import info.nightscout.androidaps.interaction.utils.Persistence; -import info.nightscout.androidaps.interaction.utils.WearUtil; - -/** - * Holds bunch of data model variables and lists that arrive from phone app and are due to be - * displayed on watchface and complications. Keeping them together makes code cleaner and allows - * passing it to complications via persistence layer. - * - * Created by dlvoy on 2019-11-12 - */ -public class DisplayRawData { - - static final String DATA_PERSISTENCE_KEY = "raw_data"; - static final String BASALS_PERSISTENCE_KEY = "raw_basals"; - static final String STATUS_PERSISTENCE_KEY = "raw_status"; - - // data bundle - public long sgvLevel = 0; - public long datetime; - public String sSgv = "---"; - public String sDirection = "--"; - public String sDelta = "--"; - public String sAvgDelta = "--"; - public String sUnits = "-"; - - // status bundle - public String sBasalRate = "-.--U/h"; - public String sUploaderBattery = "--"; - public String sRigBattery = "--"; - public boolean detailedIOB = false; - public String sIOB1 = "IOB"; - public String sIOB2 = "-.--"; - public String sCOB1 = "Carb"; - public String sCOB2= "--g"; - public String sBgi = "--"; - public boolean showBGI = false; - public String externalStatusString = "no status"; - public int batteryLevel = 1; - public long openApsStatus = -1; - - // basals bundle - public ArrayList bgDataList = new ArrayList<>(); - public ArrayList tempWatchDataList = new ArrayList<>(); - public ArrayList basalWatchDataList = new ArrayList<>(); - public ArrayList bolusWatchDataList = new ArrayList<>(); - public ArrayList predictionList = new ArrayList<>(); - - public String toDebugString() { - return "DisplayRawData{" + - "sgvLevel=" + sgvLevel + - ", datetime=" + datetime + - ", sSgv='" + sSgv + '\'' + - ", sDirection='" + sDirection + '\'' + - ", sDelta='" + sDelta + '\'' + - ", sAvgDelta='" + sAvgDelta + '\'' + - ", sUnits='" + sUnits + '\'' + - ", sBasalRate='" + sBasalRate + '\'' + - ", sUploaderBattery='" + sUploaderBattery + '\'' + - ", sRigBattery='" + sRigBattery + '\'' + - ", detailedIOB=" + detailedIOB + - ", sIOB1='" + sIOB1 + '\'' + - ", sIOB2='" + sIOB2 + '\'' + - ", sCOB1='" + sCOB1 + '\'' + - ", sCOB2='" + sCOB2 + '\'' + - ", sBgi='" + sBgi + '\'' + - ", showBGI=" + showBGI + - ", externalStatusString='" + externalStatusString + '\'' + - ", batteryLevel=" + batteryLevel + - ", openApsStatus=" + openApsStatus + - ", bgDataList size=" + bgDataList.size() + - ", tempWatchDataList size=" + tempWatchDataList.size() + - ", basalWatchDataList size=" + basalWatchDataList.size() + - ", bolusWatchDataLis size=" + bolusWatchDataList.size() + - ", predictionList size=" + predictionList.size() + - '}'; - } - - public void updateFromPersistence(Persistence persistence) { - - DataMap dataMapData = persistence.getDataMap(DATA_PERSISTENCE_KEY); - if (dataMapData != null) { - updateData(dataMapData); - } - DataMap dataMapStatus = persistence.getDataMap(STATUS_PERSISTENCE_KEY); - if (dataMapStatus != null) { - updateStatus(dataMapStatus); - } - DataMap dataMapBasals = persistence.getDataMap(BASALS_PERSISTENCE_KEY); - if (dataMapBasals != null) { - updateBasals(dataMapBasals); - } - } - - /* - * Since complications do not need Basals, we skip them for performance - */ - public void partialUpdateFromPersistence(Persistence persistence) { - - DataMap dataMapData = persistence.getDataMap(DATA_PERSISTENCE_KEY); - if (dataMapData != null) { - updateData(dataMapData); - } - DataMap dataMapStatus = persistence.getDataMap(STATUS_PERSISTENCE_KEY); - if (dataMapStatus != null) { - updateStatus(dataMapStatus); - } - } - - public DataMap updateDataFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { - Bundle bundle = intent.getBundleExtra("data"); - if (bundle != null) { - DataMap dataMap = WearUtil.bundleToDataMap(bundle); - updateData(dataMap); - return dataMap; - } - return null; - } - - private void updateData(DataMap dataMap) { - WearUtil.getWakeLock("readingPrefs", 50); - sgvLevel = dataMap.getLong("sgvLevel"); - datetime = dataMap.getLong("timestamp"); - sSgv = dataMap.getString("sgvString"); - sDirection = dataMap.getString("slopeArrow"); - sDelta = dataMap.getString("delta"); - sAvgDelta = dataMap.getString("avgDelta"); - sUnits = dataMap.getString("glucoseUnits"); - } - - public DataMap updateStatusFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { - Bundle bundle = intent.getBundleExtra("status"); - if (bundle != null) { - DataMap dataMap = WearUtil.bundleToDataMap(bundle); - updateStatus(dataMap); - return dataMap; - } - return null; - } - - private void updateStatus(DataMap dataMap) { - WearUtil.getWakeLock("readingPrefs", 50); - sBasalRate = dataMap.getString("currentBasal"); - sUploaderBattery = dataMap.getString("battery"); - sRigBattery = dataMap.getString("rigBattery"); - detailedIOB = dataMap.getBoolean("detailedIob"); - sIOB1 = dataMap.getString("iobSum") + "U"; - sIOB2 = dataMap.getString("iobDetail"); - sCOB1 = "Carb"; - sCOB2 = dataMap.getString("cob"); - sBgi = dataMap.getString("bgi"); - showBGI = dataMap.getBoolean("showBgi"); - externalStatusString = dataMap.getString("externalStatusString"); - batteryLevel = dataMap.getInt("batteryLevel"); - openApsStatus = dataMap.getLong("openApsStatus"); - } - - public DataMap updateBasalsFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { - Bundle bundle = intent.getBundleExtra("basals"); - if (bundle != null) { - DataMap dataMap = WearUtil.bundleToDataMap(bundle); - updateBasals(dataMap); - return dataMap; - } - return null; - } - - private void updateBasals(DataMap dataMap) { - WearUtil.getWakeLock("readingPrefs", 500); - loadBasalsAndTemps(dataMap); - } - - private void loadBasalsAndTemps(DataMap dataMap) { - ArrayList temps = dataMap.getDataMapArrayList("temps"); - if (temps != null) { - tempWatchDataList = new ArrayList<>(); - for (DataMap temp : temps) { - TempWatchData twd = new TempWatchData(); - twd.startTime = temp.getLong("starttime"); - twd.startBasal = temp.getDouble("startBasal"); - twd.endTime = temp.getLong("endtime"); - twd.endBasal = temp.getDouble("endbasal"); - twd.amount = temp.getDouble("amount"); - tempWatchDataList.add(twd); - } - } - ArrayList basals = dataMap.getDataMapArrayList("basals"); - if (basals != null) { - basalWatchDataList = new ArrayList<>(); - for (DataMap basal : basals) { - BasalWatchData bwd = new BasalWatchData(); - bwd.startTime = basal.getLong("starttime"); - bwd.endTime = basal.getLong("endtime"); - bwd.amount = basal.getDouble("amount"); - basalWatchDataList.add(bwd); - } - } - ArrayList boluses = dataMap.getDataMapArrayList("boluses"); - if (boluses != null) { - bolusWatchDataList = new ArrayList<>(); - for (DataMap bolus : boluses) { - BolusWatchData bwd = new BolusWatchData(); - bwd.date = bolus.getLong("date"); - bwd.bolus = bolus.getDouble("bolus"); - bwd.carbs = bolus.getDouble("carbs"); - bwd.isSMB = bolus.getBoolean("isSMB"); - bwd.isValid = bolus.getBoolean("isValid"); - bolusWatchDataList.add(bwd); - } - } - ArrayList predictions = dataMap.getDataMapArrayList("predictions"); - if (boluses != null) { - predictionList = new ArrayList<>(); - for (DataMap prediction : predictions) { - BgWatchData bwd = new BgWatchData(); - bwd.timestamp = prediction.getLong("timestamp"); - bwd.sgv = prediction.getDouble("sgv"); - bwd.color = prediction.getInt("color"); - predictionList.add(bwd); - } - } - } - - public void addToWatchSet(DataMap dataMap) { - ArrayList entries = dataMap.getDataMapArrayList("entries"); - if (entries != null) { - bgDataList = new ArrayList<>(); - for (DataMap entry : entries) { - double sgv = entry.getDouble("sgvDouble"); - double high = entry.getDouble("high"); - double low = entry.getDouble("low"); - long timestamp = entry.getLong("timestamp"); - int color = entry.getInt("color", 0); - bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); - } - } else { - double sgv = dataMap.getDouble("sgvDouble"); - double high = dataMap.getDouble("high"); - double low = dataMap.getDouble("low"); - long timestamp = dataMap.getLong("timestamp"); - int color = dataMap.getInt("color", 0); - - final int size = bgDataList.size(); - if (size > 0) { - if (bgDataList.get(size - 1).timestamp == timestamp) - return; // Ignore duplicates. - } - - bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); - } - - // We use iterator instead for-loop because we iterate and remove on the go - Iterator itr = bgDataList.iterator(); - while (itr.hasNext()) { - BgWatchData entry = (BgWatchData)itr.next(); - if (entry.timestamp < (WearUtil.timestamp() - (Constants.HOUR_IN_MS * 5))) { - itr.remove(); //Get rid of anything more than 5 hours old - } - } - } -} +package info.nightscout.androidaps.data; + +import android.content.Intent; +import android.os.Bundle; +import android.os.PowerManager; + +import com.google.android.gms.wearable.DataMap; + +import java.util.ArrayList; +import java.util.Iterator; + +import info.nightscout.androidaps.interaction.utils.Constants; +import info.nightscout.androidaps.interaction.utils.Persistence; +import info.nightscout.androidaps.interaction.utils.WearUtil; + +/** + * Holds bunch of data model variables and lists that arrive from phone app and are due to be + * displayed on watchface and complications. Keeping them together makes code cleaner and allows + * passing it to complications via persistence layer. + * + * Created by dlvoy on 2019-11-12 + */ +public class RawDisplayData { + + static final String DATA_PERSISTENCE_KEY = "raw_data"; + static final String BASALS_PERSISTENCE_KEY = "raw_basals"; + static final String STATUS_PERSISTENCE_KEY = "raw_status"; + + // data bundle + public long sgvLevel = 0; + public long datetime; + public String sSgv = "---"; + public String sDirection = "--"; + public String sDelta = "--"; + public String sAvgDelta = "--"; + public String sUnits = "-"; + + // status bundle + public String sBasalRate = "-.--U/h"; + public String sUploaderBattery = "--"; + public String sRigBattery = "--"; + public boolean detailedIOB = false; + public String sIOB1 = "IOB"; + public String sIOB2 = "-.--"; + public String sCOB1 = "Carb"; + public String sCOB2= "--g"; + public String sBgi = "--"; + public boolean showBGI = false; + public String externalStatusString = "no status"; + public int batteryLevel = 1; + public long openApsStatus = -1; + + // basals bundle + public ArrayList bgDataList = new ArrayList<>(); + public ArrayList tempWatchDataList = new ArrayList<>(); + public ArrayList basalWatchDataList = new ArrayList<>(); + public ArrayList bolusWatchDataList = new ArrayList<>(); + public ArrayList predictionList = new ArrayList<>(); + + public String toDebugString() { + return "DisplayRawData{" + + "sgvLevel=" + sgvLevel + + ", datetime=" + datetime + + ", sSgv='" + sSgv + '\'' + + ", sDirection='" + sDirection + '\'' + + ", sDelta='" + sDelta + '\'' + + ", sAvgDelta='" + sAvgDelta + '\'' + + ", sUnits='" + sUnits + '\'' + + ", sBasalRate='" + sBasalRate + '\'' + + ", sUploaderBattery='" + sUploaderBattery + '\'' + + ", sRigBattery='" + sRigBattery + '\'' + + ", detailedIOB=" + detailedIOB + + ", sIOB1='" + sIOB1 + '\'' + + ", sIOB2='" + sIOB2 + '\'' + + ", sCOB1='" + sCOB1 + '\'' + + ", sCOB2='" + sCOB2 + '\'' + + ", sBgi='" + sBgi + '\'' + + ", showBGI=" + showBGI + + ", externalStatusString='" + externalStatusString + '\'' + + ", batteryLevel=" + batteryLevel + + ", openApsStatus=" + openApsStatus + + ", bgDataList size=" + bgDataList.size() + + ", tempWatchDataList size=" + tempWatchDataList.size() + + ", basalWatchDataList size=" + basalWatchDataList.size() + + ", bolusWatchDataLis size=" + bolusWatchDataList.size() + + ", predictionList size=" + predictionList.size() + + '}'; + } + + public void updateFromPersistence(Persistence persistence) { + + DataMap dataMapData = persistence.getDataMap(DATA_PERSISTENCE_KEY); + if (dataMapData != null) { + updateData(dataMapData); + } + DataMap dataMapStatus = persistence.getDataMap(STATUS_PERSISTENCE_KEY); + if (dataMapStatus != null) { + updateStatus(dataMapStatus); + } + DataMap dataMapBasals = persistence.getDataMap(BASALS_PERSISTENCE_KEY); + if (dataMapBasals != null) { + updateBasals(dataMapBasals); + } + } + + /* + * Since complications do not need Basals, we skip them for performance + */ + public void updateForComplicationsFromPersistence(Persistence persistence) { + + DataMap dataMapData = persistence.getDataMap(DATA_PERSISTENCE_KEY); + if (dataMapData != null) { + updateData(dataMapData); + } + DataMap dataMapStatus = persistence.getDataMap(STATUS_PERSISTENCE_KEY); + if (dataMapStatus != null) { + updateStatus(dataMapStatus); + } + } + + public DataMap updateDataFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { + Bundle bundle = intent.getBundleExtra("data"); + if (bundle != null) { + DataMap dataMap = WearUtil.bundleToDataMap(bundle); + updateData(dataMap); + return dataMap; + } + return null; + } + + private void updateData(DataMap dataMap) { + WearUtil.getWakeLock("readingPrefs", 50); + sgvLevel = dataMap.getLong("sgvLevel"); + datetime = dataMap.getLong("timestamp"); + sSgv = dataMap.getString("sgvString"); + sDirection = dataMap.getString("slopeArrow"); + sDelta = dataMap.getString("delta"); + sAvgDelta = dataMap.getString("avgDelta"); + sUnits = dataMap.getString("glucoseUnits"); + } + + public DataMap updateStatusFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { + Bundle bundle = intent.getBundleExtra("status"); + if (bundle != null) { + DataMap dataMap = WearUtil.bundleToDataMap(bundle); + updateStatus(dataMap); + return dataMap; + } + return null; + } + + private void updateStatus(DataMap dataMap) { + WearUtil.getWakeLock("readingPrefs", 50); + sBasalRate = dataMap.getString("currentBasal"); + sUploaderBattery = dataMap.getString("battery"); + sRigBattery = dataMap.getString("rigBattery"); + detailedIOB = dataMap.getBoolean("detailedIob"); + sIOB1 = dataMap.getString("iobSum") + "U"; + sIOB2 = dataMap.getString("iobDetail"); + sCOB1 = "Carb"; + sCOB2 = dataMap.getString("cob"); + sBgi = dataMap.getString("bgi"); + showBGI = dataMap.getBoolean("showBgi"); + externalStatusString = dataMap.getString("externalStatusString"); + batteryLevel = dataMap.getInt("batteryLevel"); + openApsStatus = dataMap.getLong("openApsStatus"); + } + + public DataMap updateBasalsFromMessage(Intent intent, PowerManager.WakeLock wakeLock) { + Bundle bundle = intent.getBundleExtra("basals"); + if (bundle != null) { + DataMap dataMap = WearUtil.bundleToDataMap(bundle); + updateBasals(dataMap); + return dataMap; + } + return null; + } + + private void updateBasals(DataMap dataMap) { + WearUtil.getWakeLock("readingPrefs", 500); + loadBasalsAndTemps(dataMap); + } + + private void loadBasalsAndTemps(DataMap dataMap) { + ArrayList temps = dataMap.getDataMapArrayList("temps"); + if (temps != null) { + tempWatchDataList = new ArrayList<>(); + for (DataMap temp : temps) { + TempWatchData twd = new TempWatchData(); + twd.startTime = temp.getLong("starttime"); + twd.startBasal = temp.getDouble("startBasal"); + twd.endTime = temp.getLong("endtime"); + twd.endBasal = temp.getDouble("endbasal"); + twd.amount = temp.getDouble("amount"); + tempWatchDataList.add(twd); + } + } + ArrayList basals = dataMap.getDataMapArrayList("basals"); + if (basals != null) { + basalWatchDataList = new ArrayList<>(); + for (DataMap basal : basals) { + BasalWatchData bwd = new BasalWatchData(); + bwd.startTime = basal.getLong("starttime"); + bwd.endTime = basal.getLong("endtime"); + bwd.amount = basal.getDouble("amount"); + basalWatchDataList.add(bwd); + } + } + ArrayList boluses = dataMap.getDataMapArrayList("boluses"); + if (boluses != null) { + bolusWatchDataList = new ArrayList<>(); + for (DataMap bolus : boluses) { + BolusWatchData bwd = new BolusWatchData(); + bwd.date = bolus.getLong("date"); + bwd.bolus = bolus.getDouble("bolus"); + bwd.carbs = bolus.getDouble("carbs"); + bwd.isSMB = bolus.getBoolean("isSMB"); + bwd.isValid = bolus.getBoolean("isValid"); + bolusWatchDataList.add(bwd); + } + } + ArrayList predictions = dataMap.getDataMapArrayList("predictions"); + if (boluses != null) { + predictionList = new ArrayList<>(); + for (DataMap prediction : predictions) { + BgWatchData bwd = new BgWatchData(); + bwd.timestamp = prediction.getLong("timestamp"); + bwd.sgv = prediction.getDouble("sgv"); + bwd.color = prediction.getInt("color"); + predictionList.add(bwd); + } + } + } + + public void addToWatchSet(DataMap dataMap) { + ArrayList entries = dataMap.getDataMapArrayList("entries"); + if (entries != null) { + bgDataList = new ArrayList<>(); + for (DataMap entry : entries) { + double sgv = entry.getDouble("sgvDouble"); + double high = entry.getDouble("high"); + double low = entry.getDouble("low"); + long timestamp = entry.getLong("timestamp"); + int color = entry.getInt("color", 0); + bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); + } + } else { + double sgv = dataMap.getDouble("sgvDouble"); + double high = dataMap.getDouble("high"); + double low = dataMap.getDouble("low"); + long timestamp = dataMap.getLong("timestamp"); + int color = dataMap.getInt("color", 0); + + final int size = bgDataList.size(); + if (size > 0) { + if (bgDataList.get(size - 1).timestamp == timestamp) + return; // Ignore duplicates. + } + + bgDataList.add(new BgWatchData(sgv, high, low, timestamp, color)); + } + + // We use iterator instead for-loop because we iterate and remove on the go + Iterator itr = bgDataList.iterator(); + while (itr.hasNext()) { + BgWatchData entry = (BgWatchData)itr.next(); + if (entry.timestamp < (WearUtil.timestamp() - (Constants.HOUR_IN_MS * 5))) { + itr.remove(); //Get rid of anything more than 5 hours old + } + } + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java index 4925bb1f5c..f2cf06671e 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java @@ -1,17 +1,19 @@ package info.nightscout.androidaps.interaction.utils; import info.nightscout.androidaps.aaps; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; public class DisplayFormat { /** - * Maximal lengths of fields/labels shown in complications + * Maximal and minimal lengths of fields/labels shown in complications, in characters + * For MAX values - above that WearOS and watch faces may start ellipsize (...) contents + * For MIN values - this is minimal length that can hold legible data */ - public static final int MAX_LONG_FIELD = 22; // this is empirical, above that many watch faces start to ellipsize - public static final int MAX_SHORT_FIELD = 7; // according to Wear OS docs for TYPE_SHORT_TEXT - public static final int MIN_COB_FIELD = 3; // since carbs are 0..99g - public static final int MIN_IOB_FIELD = 3; // IoB can range from like .1U to 99U + public static final int MAX_FIELD_LEN_LONG = 22; // this is found out empirical, for TYPE_LONG_TEXT + public static final int MAX_FIELD_LEN_SHORT = 7; // according to Wear OS docs for TYPE_SHORT_TEXT + public static final int MIN_FIELD_LEN_COB = 3; // since carbs are usually 0..99g + public static final int MIN_FIELD_LEN_IOB = 3; // IoB can range from like .1U to 99U public static String deltaSymbol() { return aaps.areComplicationsUnicode() ? "\u0394" : ""; @@ -48,32 +50,32 @@ public class DisplayFormat { } } - public static String shortTrend(final DisplayRawData raw) { + public static String shortTrend(final RawDisplayData raw) { String minutes = "--"; if (raw.datetime > 0) { minutes = shortTimeSince(raw.datetime); } - if (minutes.length() + raw.sDelta.length() + deltaSymbol().length() + 1 <= MAX_SHORT_FIELD) { + if (minutes.length() + raw.sDelta.length() + deltaSymbol().length() + 1 <= MAX_FIELD_LEN_SHORT) { return minutes + " " + deltaSymbol() + raw.sDelta; } // that only optimizes obvious things like 0 before . or at end, + at beginning - String delta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_SHORT_FIELD-1); - if (minutes.length() + delta.length() + deltaSymbol().length() + 1 <= MAX_SHORT_FIELD) { + String delta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_FIELD_LEN_SHORT -1); + if (minutes.length() + delta.length() + deltaSymbol().length() + 1 <= MAX_FIELD_LEN_SHORT) { return minutes + " " + deltaSymbol() + delta; } - String shortDelta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_SHORT_FIELD-(1+minutes.length())); + String shortDelta = (new SmallestDoubleString(raw.sDelta)).minimise(MAX_FIELD_LEN_SHORT -(1+minutes.length())); return minutes + " " + shortDelta; } - public static String longGlucoseLine(final DisplayRawData raw) { + public static String longGlucoseLine(final RawDisplayData raw) { return raw.sSgv + raw.sDirection + " " + deltaSymbol() + (new SmallestDoubleString(raw.sDelta)).minimise(8) + " (" + shortTimeSince(raw.datetime) + ")"; } - public static String longDetailsLine(final DisplayRawData raw) { + public static String longDetailsLine(final RawDisplayData raw) { final String SEP_LONG = " " + verticalSeparatorSymbol() + " "; final String SEP_SHORT = " " + verticalSeparatorSymbol() + " "; @@ -81,26 +83,26 @@ public class DisplayFormat { final String SEP_MIN = " "; String line = raw.sCOB2 + SEP_LONG + raw.sIOB1 + SEP_LONG + basalRateSymbol()+raw.sBasalRate; - if (line.length() <= MAX_LONG_FIELD) { + if (line.length() <= MAX_FIELD_LEN_LONG) { return line; } line = raw.sCOB2 + SEP_SHORT + raw.sIOB1 + SEP_SHORT + raw.sBasalRate; - if (line.length() <= MAX_LONG_FIELD) { + if (line.length() <= MAX_FIELD_LEN_LONG) { return line; } - int remainingMax = MAX_LONG_FIELD - (raw.sCOB2.length() + raw.sBasalRate.length() + SEP_SHORT_LEN*2); - final String smallestIoB = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_IOB_FIELD, remainingMax)); + int remainingMax = MAX_FIELD_LEN_LONG - (raw.sCOB2.length() + raw.sBasalRate.length() + SEP_SHORT_LEN*2); + final String smallestIoB = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_FIELD_LEN_IOB, remainingMax)); line = raw.sCOB2 + SEP_SHORT + smallestIoB + SEP_SHORT + raw.sBasalRate; - if (line.length() <= MAX_LONG_FIELD) { + if (line.length() <= MAX_FIELD_LEN_LONG) { return line; } - remainingMax = MAX_LONG_FIELD - (smallestIoB.length() + raw.sBasalRate.length() + SEP_SHORT_LEN*2); - final String simplifiedCob = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_COB_FIELD, remainingMax)); + remainingMax = MAX_FIELD_LEN_LONG - (smallestIoB.length() + raw.sBasalRate.length() + SEP_SHORT_LEN*2); + final String simplifiedCob = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE).minimise(Math.max(MIN_FIELD_LEN_COB, remainingMax)); line = simplifiedCob + SEP_SHORT + smallestIoB + SEP_SHORT + raw.sBasalRate; - if (line.length() <= MAX_LONG_FIELD) { + if (line.length() <= MAX_FIELD_LEN_LONG) { return line; } @@ -109,17 +111,17 @@ public class DisplayFormat { return line; } - public static Pair detailedIob(DisplayRawData raw) { - final String iob1 = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_SHORT_FIELD); + public static Pair detailedIob(RawDisplayData raw) { + final String iob1 = new SmallestDoubleString(raw.sIOB1, SmallestDoubleString.Units.USE).minimise(MAX_FIELD_LEN_SHORT); String iob2 = ""; if (raw.sIOB2.contains("|")) { String[] iobs = raw.sIOB2.replace("(", "").replace(")", "").split("\\|"); - String iobBolus = new SmallestDoubleString(iobs[0]).minimise(MIN_IOB_FIELD); + String iobBolus = new SmallestDoubleString(iobs[0]).minimise(MIN_FIELD_LEN_IOB); if (iobBolus.trim().length() == 0) { iobBolus = "--"; } - String iobBasal = new SmallestDoubleString(iobs[1]).minimise((MAX_SHORT_FIELD-1) - Math.max(MIN_IOB_FIELD, iobBolus.length())); + String iobBasal = new SmallestDoubleString(iobs[1]).minimise((MAX_FIELD_LEN_SHORT -1) - Math.max(MIN_FIELD_LEN_IOB, iobBolus.length())); if (iobBasal.trim().length() == 0) { iobBasal = "--"; } @@ -128,14 +130,14 @@ public class DisplayFormat { return Pair.create(iob1, iob2); } - public static Pair detailedCob(final DisplayRawData raw) { + public static Pair detailedCob(final RawDisplayData raw) { SmallestDoubleString cobMini = new SmallestDoubleString(raw.sCOB2, SmallestDoubleString.Units.USE); String cob2 = ""; if (cobMini.getExtra().length() > 0) { cob2 = cobMini.getExtra() + cobMini.getUnits(); } - final String cob1 = cobMini.minimise(MAX_SHORT_FIELD); + final String cob1 = cobMini.minimise(MAX_FIELD_LEN_SHORT); return Pair.create(cob1, cob2); } } diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java index 21944da9c7..54361b6678 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Inevitable.java @@ -5,6 +5,8 @@ import android.util.Log; import java.util.concurrent.ConcurrentHashMap; +import info.nightscout.androidaps.BuildConfig; + /** * Created for xDrip by jamorham on 07/03/2018 * Adapted for AAPS by dlvoy on 2019-11-11 @@ -18,7 +20,7 @@ public class Inevitable { private static final String TAG = Inevitable.class.getSimpleName(); private static final int MAX_QUEUE_TIME = (int) Constants.MINUTE_IN_MS * 6; - private static final boolean d = true; + private static final boolean debug = BuildConfig.DEBUG; private static final ConcurrentHashMap tasks = new ConcurrentHashMap<>(); @@ -31,14 +33,14 @@ public class Inevitable { // if it already exists then extend the time task.extendTime(idle_for); - if (d) + if (debug) Log.d(TAG, "Extending time for: " + id + " to " + WearUtil.dateTimeText(task.when)); } else { // otherwise create new task if (runnable == null) return; // extension only if already exists tasks.put(id, new Task(id, idle_for, runnable)); - if (d) { + if (debug) { Log.d(TAG, "Creating task: " + id + " due: " + WearUtil.dateTimeText(tasks.get(id).when)); } @@ -58,7 +60,6 @@ public class Inevitable { } }); t.setPriority(Thread.MIN_PRIORITY); - //t.setDaemon(true); t.start(); } } @@ -100,7 +101,7 @@ public class Inevitable { public boolean poll() { final long till = WearUtil.msTill(when); if (till < 1) { - if (d) Log.d(TAG, "Executing task! " + this.id); + if (debug) Log.d(TAG, "Executing task! " + this.id); tasks.remove(this.id); // early remove to allow overlapping scheduling what.run(); return true; diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java index cfc6c2b4b4..36c0ae76ee 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.java @@ -3,6 +3,8 @@ package info.nightscout.androidaps.interaction.utils; import android.content.SharedPreferences; import android.util.Base64; +import androidx.annotation.Nullable; + import com.google.android.gms.wearable.DataMap; import java.util.Set; @@ -22,15 +24,15 @@ public class Persistence { preferences = aaps.getAppContext().getSharedPreferences(COMPLICATION_PROVIDER_PREFERENCES_FILE_KEY, 0); } + @Nullable public DataMap getDataMap(String key) { if (preferences.contains(key)) { final String rawB64Data = preferences.getString(key, null); byte[] rawData = Base64.decode(rawB64Data, Base64.DEFAULT); try { - DataMap dataMap = DataMap.fromByteArray(rawData); - return dataMap; + return DataMap.fromByteArray(rawData); } catch (IllegalArgumentException ex) { - + // Should never happen, and if it happen - we ignore and fallback to null } } return null; diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java index 446d0f7d68..f8415bdd4a 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.java @@ -4,6 +4,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.PowerManager; +import android.os.SystemClock; import android.util.Log; import com.google.android.gms.wearable.DataMap; @@ -53,14 +54,14 @@ public class WearUtil { //============================================================================================== // return true if below rate limit - public static synchronized boolean rateLimit(String name, int seconds) { + public static synchronized boolean isBelowRateLimit(String named, int onceForSeconds) { // check if over limit - if ((rateLimits.containsKey(name)) && (timestamp() - rateLimits.get(name) < (seconds * 1000))) { - Log.d(TAG, name + " rate limited: " + seconds + " seconds"); + if ((rateLimits.containsKey(named)) && (timestamp() - rateLimits.get(named) < (onceForSeconds * 1000))) { + Log.d(TAG, named + " rate limited to one for " + onceForSeconds + " seconds"); return false; } // not over limit - rateLimits.put(name, timestamp()); + rateLimits.put(named, timestamp()); return true; } @@ -82,18 +83,15 @@ public class WearUtil { aaps.getAppContext().startActivity(getStartActivityIntent(c)); } - public static Intent getStartActivityIntent(Class c) { return new Intent(aaps.getAppContext(), c).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - - public static void threadSleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { - // + // we simply ignore if sleep was interrupted } } diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java index 222f1eda6e..1d1f5758e8 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.java @@ -35,7 +35,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import info.nightscout.androidaps.complications.BaseComplicationProviderService; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.data.ListenerService; import info.nightscout.androidaps.R; import lecho.lib.hellocharts.view.LineChartView; @@ -70,7 +70,7 @@ public abstract class BaseWatchFace extends WatchFace implements SharedPreferen public LineChartView chart; - public DisplayRawData rawData = new DisplayRawData(); + public RawDisplayData rawData = new RawDisplayData(); public PowerManager.WakeLock wakeLock; // related endTime manual layout diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java index b40e15cfd4..ee17d91e76 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BgGraphBuilder.java @@ -1,7 +1,6 @@ package info.nightscout.androidaps.watchfaces; import android.content.Context; -import android.graphics.Color; import android.graphics.DashPathEffect; import android.preference.PreferenceManager; import android.text.format.DateFormat; @@ -18,7 +17,7 @@ import java.util.TimeZone; import info.nightscout.androidaps.data.BasalWatchData; import info.nightscout.androidaps.data.BgWatchData; import info.nightscout.androidaps.data.BolusWatchData; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.data.TempWatchData; import lecho.lib.hellocharts.model.Axis; import lecho.lib.hellocharts.model.AxisValue; @@ -116,7 +115,7 @@ public class BgGraphBuilder { this.end_time = (predictionEndTime>end_time)?predictionEndTime:end_time; } - public BgGraphBuilder(Context context, DisplayRawData raw, int aPointSize, int aHighColor, int aLowColor, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int carbsColor, int timespan) { + public BgGraphBuilder(Context context, RawDisplayData raw, int aPointSize, int aHighColor, int aLowColor, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int carbsColor, int timespan) { this(context, raw.bgDataList, raw.predictionList, @@ -135,7 +134,7 @@ public class BgGraphBuilder { timespan); } - public BgGraphBuilder(Context context, DisplayRawData raw, int aPointSize, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int carbsColor, int timespan) { + public BgGraphBuilder(Context context, RawDisplayData raw, int aPointSize, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int carbsColor, int timespan) { this(context, raw.bgDataList, raw.predictionList, diff --git a/wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java b/wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java index fa74d17691..e920dc3202 100644 --- a/wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/data/BgWatchDataTest.java @@ -27,7 +27,7 @@ import static org.junit.Assert.assertTrue; public class BgWatchDataTest { @Before - public void mock() { + public void mock() throws Exception { WearUtilMocker.prepareMockNoReal(); } diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataSgvDataTest.java b/wear/src/test/java/info/nightscout/androidaps/data/RawDataSgvDisplayDataTest.java similarity index 86% rename from wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataSgvDataTest.java rename to wear/src/test/java/info/nightscout/androidaps/data/RawDataSgvDisplayDataTest.java index 55ff9bb815..fa73a0272e 100644 --- a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataSgvDataTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/data/RawDataSgvDisplayDataTest.java @@ -29,10 +29,10 @@ import static org.junit.Assert.assertThat; @RunWith(PowerMockRunner.class) @PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class, android.util.Base64.class, Intent.class } ) -public class DisplayRawDataSgvDataTest { +public class RawDataSgvDisplayDataTest { @Before - public void mock() { + public void mock() throws Exception { AAPSMocker.prepareMock(); AAPSMocker.resetMockedSharedPrefs(); AndroidMocker.mockBase64(); @@ -55,7 +55,7 @@ public class DisplayRawDataSgvDataTest { return dataMap; } - private void assertDataEmpty(DisplayRawData newRaw) { + private void assertDataEmpty(RawDisplayData newRaw) { assertThat(newRaw.sgvLevel, is(0L)); assertThat(newRaw.datetime, is(0L)); assertThat(newRaw.sSgv, is("---")); @@ -65,7 +65,7 @@ public class DisplayRawDataSgvDataTest { assertThat(newRaw.sUnits, is("-")); } - private void assertDataOk(DisplayRawData newRaw) { + private void assertDataOk(RawDisplayData newRaw) { assertThat(newRaw.sgvLevel, is(1L)); assertThat(newRaw.datetime, is(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS)); assertThat(newRaw.sSgv, is("106")); @@ -79,7 +79,7 @@ public class DisplayRawDataSgvDataTest { public void updateDataFromEmptyPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateFromPersistence(persistence); @@ -92,10 +92,10 @@ public class DisplayRawDataSgvDataTest { public void updateDataFromPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN - Persistence.storeDataMap(DisplayRawData.DATA_PERSISTENCE_KEY, dataMapForData()); + Persistence.storeDataMap(RawDisplayData.DATA_PERSISTENCE_KEY, dataMapForData()); newRaw.updateFromPersistence(persistence); // THEN @@ -106,11 +106,11 @@ public class DisplayRawDataSgvDataTest { public void partialUpdateDataFromPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN - Persistence.storeDataMap(DisplayRawData.DATA_PERSISTENCE_KEY, dataMapForData()); - newRaw.partialUpdateFromPersistence(persistence); + Persistence.storeDataMap(RawDisplayData.DATA_PERSISTENCE_KEY, dataMapForData()); + newRaw.updateForComplicationsFromPersistence(persistence); // THEN assertDataOk(newRaw); @@ -123,7 +123,7 @@ public class DisplayRawDataSgvDataTest { Bundle bundle = BundleMock.mock(dataMapForData()); intent.putExtra("data", bundle); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateDataFromMessage(intent, null); @@ -136,7 +136,7 @@ public class DisplayRawDataSgvDataTest { public void updateDataFromEmptyMessageTest() { // GIVEN Intent intent = IntentMock.mock(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateDataFromMessage(intent, null); diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBasalsTest.java b/wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataBasalsTest.java similarity index 93% rename from wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBasalsTest.java rename to wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataBasalsTest.java index d3f9dcf48b..1245d0fbcb 100644 --- a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBasalsTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataBasalsTest.java @@ -35,10 +35,10 @@ import static org.junit.Assert.assertThat; @RunWith(PowerMockRunner.class) @PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class, android.util.Base64.class, Intent.class } ) -public class DisplayRawDataBasalsTest { +public class RawDisplayDataBasalsTest { @Before - public void mock() { + public void mock() throws Exception { AAPSMocker.prepareMock(); AAPSMocker.resetMockedSharedPrefs(); AndroidMocker.mockBase64(); @@ -119,14 +119,14 @@ public class DisplayRawDataBasalsTest { return dataMap; } - private void assertBasalsEmpty(DisplayRawData newRaw) { + private void assertBasalsEmpty(RawDisplayData newRaw) { assertThat(newRaw.tempWatchDataList.size(), is(0)); assertThat(newRaw.basalWatchDataList.size(), is(0)); assertThat(newRaw.bolusWatchDataList.size(), is(0)); assertThat(newRaw.predictionList.size(), is(0)); } - private void assertBasalsOk(DisplayRawData newRaw) { + private void assertBasalsOk(RawDisplayData newRaw) { assertThat(newRaw.tempWatchDataList.size(), is(2)); assertThat(newRaw.basalWatchDataList.size(), is(1)); assertThat(newRaw.bolusWatchDataList.size(), is(3)); @@ -196,7 +196,7 @@ public class DisplayRawDataBasalsTest { public void updateBasalsFromEmptyPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateFromPersistence(persistence); @@ -209,10 +209,10 @@ public class DisplayRawDataBasalsTest { public void updateBasalsFromPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN - Persistence.storeDataMap(DisplayRawData.BASALS_PERSISTENCE_KEY, dataMapForBasals()); + Persistence.storeDataMap(RawDisplayData.BASALS_PERSISTENCE_KEY, dataMapForBasals()); newRaw.updateFromPersistence(persistence); // THEN @@ -223,11 +223,11 @@ public class DisplayRawDataBasalsTest { public void partialUpdateBasalsFromPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN - Persistence.storeDataMap(DisplayRawData.BASALS_PERSISTENCE_KEY, dataMapForBasals()); - newRaw.partialUpdateFromPersistence(persistence); + Persistence.storeDataMap(RawDisplayData.BASALS_PERSISTENCE_KEY, dataMapForBasals()); + newRaw.updateForComplicationsFromPersistence(persistence); // THEN assertBasalsEmpty(newRaw); @@ -240,7 +240,7 @@ public class DisplayRawDataBasalsTest { Bundle bundle = BundleMock.mock(dataMapForBasals()); intent.putExtra("basals", bundle); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateBasalsFromMessage(intent, null); @@ -253,7 +253,7 @@ public class DisplayRawDataBasalsTest { public void updateBasalsFromEmptyMessageTest() { // GIVEN Intent intent = IntentMock.mock(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateBasalsFromMessage(intent, null); diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBgEntriesTest.java b/wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataBgEntriesTest.java similarity index 96% rename from wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBgEntriesTest.java rename to wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataBgEntriesTest.java index 1908e20dd5..c200288213 100644 --- a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataBgEntriesTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataBgEntriesTest.java @@ -20,10 +20,10 @@ import static org.junit.Assert.assertThat; @RunWith(PowerMockRunner.class) @PrepareForTest( { WearUtil.class } ) -public class DisplayRawDataBgEntriesTest { +public class RawDisplayDataBgEntriesTest { @Before - public void mock() { + public void mock() throws Exception { WearUtilMocker.prepareMockNoReal(); } @@ -62,7 +62,7 @@ public class DisplayRawDataBgEntriesTest { @Test public void addToWatchSetTest() { // GIVEN - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); DataMap multipleEntries = dataMapForEntries(); DataMap singleEntry1 = dataMapForEntries(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*2,92); DataMap singleEntry2 = dataMapForEntries(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*4*1,88); @@ -107,7 +107,7 @@ public class DisplayRawDataBgEntriesTest { @Test public void addToWatchSetCleanupOldTest() { - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); newRaw.addToWatchSet(dataMapForEntries(WearUtil.timestamp(),125)); assertThat(newRaw.bgDataList.size(), is(1)); diff --git a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataStatusTest.java b/wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataStatusTest.java similarity index 88% rename from wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataStatusTest.java rename to wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataStatusTest.java index 17ac8fd14b..da0daea608 100644 --- a/wear/src/test/java/info/nightscout/androidaps/data/DisplayRawDataStatusTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/data/RawDisplayDataStatusTest.java @@ -31,10 +31,10 @@ import static org.junit.Assert.assertThat; @RunWith(PowerMockRunner.class) @PrepareForTest( { WearUtil.class, Log.class, SharedPreferences.class, Context.class, aaps.class, android.util.Base64.class, Intent.class } ) -public class DisplayRawDataStatusTest { +public class RawDisplayDataStatusTest { @Before - public void mock() { + public void mock() throws Exception { AAPSMocker.prepareMock(); AAPSMocker.resetMockedSharedPrefs(); AndroidMocker.mockBase64(); @@ -43,7 +43,7 @@ public class DisplayRawDataStatusTest { @Test public void toDebugStringTest() { - DisplayRawData raw = RawDataMocker.rawDelta(5, "1.5"); + RawDisplayData raw = RawDataMocker.rawDelta(5, "1.5"); raw.externalStatusString = "placeholder-here"; assertThat(raw.datetime, is(WearUtilMocker.REF_NOW - Constants.MINUTE_IN_MS*5)); @@ -71,7 +71,7 @@ public class DisplayRawDataStatusTest { return dataMap; } - private void assertStatusEmpty(DisplayRawData newRaw) { + private void assertStatusEmpty(RawDisplayData newRaw) { assertThat(newRaw.sBasalRate, is("-.--U/h")); assertThat(newRaw.sUploaderBattery, is("--")); assertThat(newRaw.sRigBattery, is("--")); @@ -87,7 +87,7 @@ public class DisplayRawDataStatusTest { assertThat(newRaw.openApsStatus, is(-1L)); } - private void assertStatusOk(DisplayRawData newRaw) { + private void assertStatusOk(RawDisplayData newRaw) { assertThat(newRaw.sBasalRate, is("120%")); assertThat(newRaw.sUploaderBattery, is("76")); assertThat(newRaw.sRigBattery, is("40%")); @@ -107,7 +107,7 @@ public class DisplayRawDataStatusTest { public void updateStatusFromEmptyPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateFromPersistence(persistence); @@ -120,10 +120,10 @@ public class DisplayRawDataStatusTest { public void updateStatusFromPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN - Persistence.storeDataMap(DisplayRawData.STATUS_PERSISTENCE_KEY, dataMapForStatus()); + Persistence.storeDataMap(RawDisplayData.STATUS_PERSISTENCE_KEY, dataMapForStatus()); newRaw.updateFromPersistence(persistence); // THEN @@ -134,11 +134,11 @@ public class DisplayRawDataStatusTest { public void partialUpdateStatusFromPersistenceTest() { // GIVEN Persistence persistence = new Persistence(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN - Persistence.storeDataMap(DisplayRawData.STATUS_PERSISTENCE_KEY, dataMapForStatus()); - newRaw.partialUpdateFromPersistence(persistence); + Persistence.storeDataMap(RawDisplayData.STATUS_PERSISTENCE_KEY, dataMapForStatus()); + newRaw.updateForComplicationsFromPersistence(persistence); // THEN assertStatusOk(newRaw); @@ -151,7 +151,7 @@ public class DisplayRawDataStatusTest { Bundle bundle = BundleMock.mock(dataMapForStatus()); intent.putExtra("status", bundle); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateStatusFromMessage(intent, null); @@ -164,7 +164,7 @@ public class DisplayRawDataStatusTest { public void updateStatusFromEmptyMessageTest() { // GIVEN Intent intent = IntentMock.mock(); - DisplayRawData newRaw = new DisplayRawData(); + RawDisplayData newRaw = new RawDisplayData(); // WHEN newRaw.updateStatusFromMessage(intent, null); diff --git a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java index 3c838f5898..98fb4fd11b 100644 --- a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/DisplayFormatTest.java @@ -11,7 +11,7 @@ import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import info.nightscout.androidaps.aaps; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.testing.mockers.AAPSMocker; import info.nightscout.androidaps.testing.mockers.WearUtilMocker; @@ -34,7 +34,7 @@ import static org.junit.Assert.assertThat; public class DisplayFormatTest { @Before - public void mock() { + public void mock() throws Exception { WearUtilMocker.prepareMock(); AAPSMocker.prepareMock(); AAPSMocker.resetMockedSharedPrefs(); @@ -104,7 +104,7 @@ public class DisplayFormatTest { @Test public void shortTrendTest() { - DisplayRawData raw = new DisplayRawData(); + RawDisplayData raw = new RawDisplayData(); assertThat(DisplayFormat.shortTrend(raw), is("-- Δ--")); raw.datetime = backInTime(0, 0, 2, 0); diff --git a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java index 625216aadc..1faa4ddfe7 100644 --- a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/PersistenceTest.java @@ -33,7 +33,7 @@ import static org.junit.Assert.assertTrue; public class PersistenceTest { @Before - public void mock() { + public void mock() throws Exception { WearUtilMocker.prepareMock(); LogMocker.prepareMock(); AAPSMocker.prepareMock(); @@ -88,9 +88,9 @@ public class PersistenceTest { final long whenUpdatedNext = persistence.whenDataUpdated(); // THEN - assertThat(0L, is(whenNotUpdated)); - assertThat(REF_NOW, is(whenUpdatedFirst)); - assertThat(REF_NOW + 60000, is(whenUpdatedNext)); + assertThat(whenNotUpdated, is(0L)); + assertThat(whenUpdatedFirst, is(REF_NOW)); + assertThat(whenUpdatedNext, is(REF_NOW + 60000)); } @Test diff --git a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java index 5bb501584d..9e60423c6a 100644 --- a/wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java +++ b/wear/src/test/java/info/nightscout/androidaps/interaction/utils/WearUtilTest.java @@ -36,7 +36,7 @@ import static org.junit.Assert.assertTrue; public class WearUtilTest { @Before - public void mock() { + public void mock() throws Exception { WearUtilMocker.prepareMock(); LogMocker.prepareMock(); } @@ -146,12 +146,12 @@ public class WearUtilTest { @Test public void rateLimitTest() { // WHEN - final boolean firstCall = WearUtil.rateLimit("test-limit", 3); - final boolean callAfterward = WearUtil.rateLimit("test-limit", 3); + final boolean firstCall = WearUtil.isBelowRateLimit("test-limit", 3); + final boolean callAfterward = WearUtil.isBelowRateLimit("test-limit", 3); WearUtilMocker.progressClock(500L); - final boolean callTooSoon = WearUtil.rateLimit("test-limit", 3); + final boolean callTooSoon = WearUtil.isBelowRateLimit("test-limit", 3); WearUtilMocker.progressClock(3100L); - final boolean callAfterRateLimit = WearUtil.rateLimit("test-limit", 3); + final boolean callAfterRateLimit = WearUtil.isBelowRateLimit("test-limit", 3); // THEN assertTrue(firstCall); @@ -166,7 +166,7 @@ public class WearUtilTest { * uses DataMap.fromBundle which need Android SDK runtime */ @Test - public void bundleToDataMapTest() { + public void bundleToDataMapTest() throws Exception { // GIVEN DataMap refMap = new DataMap(); refMap.putString("ala", "ma kota"); diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java index 664b431b5c..7684d169f1 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AAPSMocker.java @@ -22,28 +22,23 @@ public class AAPSMocker { private static final Map mockedSharedPrefs = new HashMap<>(); private static boolean unicodeComplicationsOn = true; - public static void prepareMock() { + public static void prepareMock() throws Exception { Context mockedContext = mock(Context.class); mockStatic(aaps.class, InvocationOnMock::callRealMethod); - try { - PowerMockito.when(aaps.class, "getAppContext").thenReturn(mockedContext); - PowerMockito.when(mockedContext, "getSharedPreferences", ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()).thenAnswer(invocation -> { - final String key = invocation.getArgument(0); - if (mockedSharedPrefs.containsKey(key)) { - return mockedSharedPrefs.get(key); - } else { - SharedPreferencesMock newPrefs = new SharedPreferencesMock(); - mockedSharedPrefs.put(key, newPrefs); - return newPrefs; - } - }); - PowerMockito.when(aaps.class, "areComplicationsUnicode").thenAnswer(invocation -> unicodeComplicationsOn); + PowerMockito.when(aaps.class, "getAppContext").thenReturn(mockedContext); + PowerMockito.when(mockedContext, "getSharedPreferences", ArgumentMatchers.anyString(), ArgumentMatchers.anyInt()).thenAnswer(invocation -> { - - } catch (Exception e) { - Assert.fail("Unable to mock objects: " + e.getMessage()); - } + final String key = invocation.getArgument(0); + if (mockedSharedPrefs.containsKey(key)) { + return mockedSharedPrefs.get(key); + } else { + SharedPreferencesMock newPrefs = new SharedPreferencesMock(); + mockedSharedPrefs.put(key, newPrefs); + return newPrefs; + } + }); + PowerMockito.when(aaps.class, "areComplicationsUnicode").thenAnswer(invocation -> unicodeComplicationsOn); setMockedUnicodeComplicationsOn(true); resetMockedSharedPrefs(); diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java index 62eb5c6301..a1addfdc1d 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/AndroidMocker.java @@ -12,29 +12,25 @@ import static org.powermock.api.mockito.PowerMockito.mockStatic; public class AndroidMocker { - public static void mockBase64() { + public static void mockBase64() throws Exception { mockStatic(android.util.Base64.class); - try { - PowerMockito.when(android.util.Base64.class, "decode", anyString(), anyInt()).thenAnswer(invocation -> { - final String payload = invocation.getArgument(0); - try { - return Base64.getDecoder().decode(payload); - } catch (java.lang.IllegalArgumentException ex) { - return null; - } - }); + PowerMockito.when(android.util.Base64.class, "decode", anyString(), anyInt()).thenAnswer(invocation -> { - PowerMockito.when(android.util.Base64.class, "encodeToString", any(), anyInt()).thenAnswer(invocation -> { + final String payload = invocation.getArgument(0); + try { + return Base64.getDecoder().decode(payload); + } catch (java.lang.IllegalArgumentException ex) { + return null; + } + }); - final byte[] payload = invocation.getArgument(0); - return Base64.getEncoder().encodeToString(payload); + PowerMockito.when(android.util.Base64.class, "encodeToString", any(), anyInt()).thenAnswer(invocation -> { - }); + final byte[] payload = invocation.getArgument(0); + return Base64.getEncoder().encodeToString(payload); - } catch (Exception e) { - Assert.fail("Unable to mock objects: " + e.getMessage()); - } + }); } } diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java index 832720bbff..7d06f00c51 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/RawDataMocker.java @@ -1,14 +1,14 @@ package info.nightscout.androidaps.testing.mockers; -import info.nightscout.androidaps.data.DisplayRawData; +import info.nightscout.androidaps.data.RawDisplayData; import info.nightscout.androidaps.interaction.utils.SafeParse; import static info.nightscout.androidaps.testing.mockers.WearUtilMocker.backInTime; public class RawDataMocker { - public static DisplayRawData rawSgv(String sgv, int m, String deltaString) { - DisplayRawData raw = new DisplayRawData(); + public static RawDisplayData rawSgv(String sgv, int m, String deltaString) { + RawDisplayData raw = new RawDisplayData(); raw.datetime = backInTime(0, 0, m, 0); raw.sDelta = deltaString; raw.sSgv = sgv; @@ -34,30 +34,30 @@ public class RawDataMocker { return raw; } - public static DisplayRawData rawDelta(int m, String delta) { - DisplayRawData raw = new DisplayRawData(); + public static RawDisplayData rawDelta(int m, String delta) { + RawDisplayData raw = new RawDisplayData(); raw.datetime = backInTime(0, 0, m, 0); raw.sDelta = delta; return raw; } - public static DisplayRawData rawCobIobBr(String cob, String iob, String br) { - DisplayRawData raw = new DisplayRawData(); + public static RawDisplayData rawCobIobBr(String cob, String iob, String br) { + RawDisplayData raw = new RawDisplayData(); raw.sCOB2 = cob; raw.sIOB1 = iob; raw.sBasalRate = br; return raw; } - public static DisplayRawData rawIob(String iob, String iob2) { - DisplayRawData raw = new DisplayRawData(); + public static RawDisplayData rawIob(String iob, String iob2) { + RawDisplayData raw = new RawDisplayData(); raw.sIOB1 = iob; raw.sIOB2 = iob2; return raw; } - public static DisplayRawData rawCob(String cob) { - DisplayRawData raw = new DisplayRawData(); + public static RawDisplayData rawCob(String cob) { + RawDisplayData raw = new RawDisplayData(); raw.sCOB2 = cob; return raw; } diff --git a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java index fbe13e52b6..11a89f0699 100644 --- a/wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java +++ b/wear/src/test/java/info/nightscout/androidaps/testing/mockers/WearUtilMocker.java @@ -25,28 +25,22 @@ public class WearUtilMocker { public static final long REF_NOW = 1572610530000L; private static long clockMsDiff = 0L; - public static void prepareMock() { + public static void prepareMock() throws Exception { resetClock(); mockStatic(WearUtil.class, InvocationOnMock::callRealMethod); - try { - // because we cleverly used timestamp() by implementation, we can mock it - // and control the time in tests - PowerMockito.when(WearUtil.class, "timestamp").then(invocation -> (REF_NOW + clockMsDiff)); - } catch (Exception e) { - Assert.fail("Unable to mock the construction of the WearUtil object: " + e.getMessage()); - } + + // because we cleverly used timestamp() by implementation, we can mock it + // and control the time in tests + PowerMockito.when(WearUtil.class, "timestamp").then(invocation -> (REF_NOW + clockMsDiff)); } - public static void prepareMockNoReal() { + public static void prepareMockNoReal() throws Exception { resetClock(); mockStatic(WearUtil.class); - try { - PowerMockito.when(WearUtil.class, "timestamp").then(invocation -> REF_NOW + clockMsDiff); - PowerMockito.when(WearUtil.class, "getWakeLock", anyString(), anyInt()).then(invocation -> null); - PowerMockito.when(WearUtil.class, "bundleToDataMap", any(Bundle.class)).then(bundleToDataMapMock); - } catch (Exception e) { - Assert.fail("Unable to mock the construction of the WearUtil object: " + e.getMessage()); - } + + PowerMockito.when(WearUtil.class, "timestamp").then(invocation -> REF_NOW + clockMsDiff); + PowerMockito.when(WearUtil.class, "getWakeLock", anyString(), anyInt()).then(invocation -> null); + PowerMockito.when(WearUtil.class, "bundleToDataMap", any(Bundle.class)).then(bundleToDataMapMock); } public static void resetClock() {