From b259425361d11bf1de3f1a4797488a73ea494d2d Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 4 Aug 2023 14:59:08 +0200 Subject: [PATCH 01/18] Add Custom Watchface --- _docs/icons/HourHand.svg | 9 + _docs/icons/MinuteHand.svg | 9 + _docs/icons/SecondHand.svg | 11 + _docs/icons/SimplifiedDial.svg | 18 + _docs/icons/background.svg | 8 + _docs/icons/detailed_dial.svg | 70 +++ _docs/icons/export_custom.svg | 38 ++ _docs/icons/load_custom.svg | 38 ++ _docs/icons/send_custom.svg | 16 + _docs/icons/set_default.svg | 20 + app-wear-shared/shared/build.gradle | 1 + .../rx/events/EventMobileDataToWear.kt | 5 + .../rx/events/EventWearCwfExported.kt | 5 + .../rx/events/EventWearDataToMobile.kt | 5 + .../rx/events/EventWearUpdateGui.kt | 3 + .../rx/weardata/CustomWatchfaceFormat.kt | 140 ++++++ .../info/nightscout/rx/weardata/EventData.kt | 26 ++ .../info/nightscout/shared/utils/DateUtil.kt | 4 + .../src/main/res/drawable/background.xml | 9 + .../src/main/res/drawable/detailed_dial.xml | 183 ++++++++ .../src/main/res/drawable/hour_hand.xml | 4 + .../src/main/res/drawable/minute_hand.xml | 4 + .../src/main/res/drawable/second_hand.xml | 4 + .../src/main/res/drawable/simplified_dial.xml | 39 ++ .../main/res/drawable/watchface_custom.png | Bin 0 -> 41646 bytes .../shared/src/main/res/values/strings.xml | 8 + .../shared/src/main/res/values/wear_paths.xml | 1 + .../maintenance/CustomWatchfaceFile.kt | 21 + .../maintenance/ImportExportPrefs.kt | 4 + .../maintenance/PrefFileListProvider.kt | 2 + .../src/main/AndroidManifest.xml | 4 + .../DaggerAppCompatActivityWithResult.kt | 3 + .../configuration/di/ConfigurationModule.kt | 4 + .../CustomWatchfaceFileContract.kt | 24 ++ .../maintenance/ImportExportPrefsImpl.kt | 26 +- .../maintenance/PrefFileListProviderImpl.kt | 21 + .../CustomWatchfaceImportListActivity.kt | 112 +++++ .../formats/ZipCustomWatchfaceFormat.kt | 128 ++++++ .../custom_watchface_import_list_activity.xml | 19 + .../custom_watchface_import_list_item.xml | 142 +++++++ .../src/main/res/values/strings.xml | 5 + plugins/main/src/main/AndroidManifest.xml | 9 + .../plugins/general/wear/WearFragment.kt | 75 +++- .../plugins/general/wear/WearPlugin.kt | 2 + .../wear/wearintegration/DataHandlerMobile.kt | 28 +- .../DataLayerListenerServiceMobile.kt | 16 +- .../src/main/res/drawable/export_custom.xml | 80 ++++ .../src/main/res/drawable/load_custom.xml | 42 ++ .../src/main/res/drawable/send_custom.xml | 7 + .../src/main/res/drawable/set_default.xml | 7 + .../src/main/res/layout/wear_fragment.xml | 213 ++++++++-- plugins/main/src/main/res/values/strings.xml | 5 + wear/src/main/AndroidManifest.xml | 35 ++ .../androidaps/comm/DataHandlerWear.kt | 24 ++ .../comm/DataLayerListenerServiceWear.kt | 16 + .../androidaps/di/WearServicesModule.kt | 1 + .../interaction/utils/Persistence.kt | 29 ++ .../androidaps/watchfaces/CustomWatchface.kt | 398 ++++++++++++++++++ .../watchfaces/utils/BaseWatchFace.kt | 31 +- .../watchfaces/utils/WatchfaceViewAdapter.kt | 58 +-- wear/src/main/res/layout/activity_custom.xml | 379 +++++++++++++++++ wear/src/main/res/values/strings.xml | 10 + .../xml/watch_face_configuration_custom.xml | 27 ++ 63 files changed, 2619 insertions(+), 66 deletions(-) create mode 100644 _docs/icons/HourHand.svg create mode 100644 _docs/icons/MinuteHand.svg create mode 100644 _docs/icons/SecondHand.svg create mode 100644 _docs/icons/SimplifiedDial.svg create mode 100644 _docs/icons/background.svg create mode 100644 _docs/icons/detailed_dial.svg create mode 100644 _docs/icons/export_custom.svg create mode 100644 _docs/icons/load_custom.svg create mode 100644 _docs/icons/send_custom.svg create mode 100644 _docs/icons/set_default.svg create mode 100644 app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventMobileDataToWear.kt create mode 100644 app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearCwfExported.kt create mode 100644 app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearDataToMobile.kt create mode 100644 app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt create mode 100644 app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt create mode 100644 app-wear-shared/shared/src/main/res/drawable/background.xml create mode 100644 app-wear-shared/shared/src/main/res/drawable/detailed_dial.xml create mode 100644 app-wear-shared/shared/src/main/res/drawable/hour_hand.xml create mode 100644 app-wear-shared/shared/src/main/res/drawable/minute_hand.xml create mode 100644 app-wear-shared/shared/src/main/res/drawable/second_hand.xml create mode 100644 app-wear-shared/shared/src/main/res/drawable/simplified_dial.xml create mode 100644 app-wear-shared/shared/src/main/res/drawable/watchface_custom.png create mode 100644 core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt create mode 100644 plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/CustomWatchfaceFileContract.kt create mode 100644 plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt create mode 100644 plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt create mode 100644 plugins/configuration/src/main/res/layout/custom_watchface_import_list_activity.xml create mode 100644 plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml create mode 100644 plugins/main/src/main/res/drawable/export_custom.xml create mode 100644 plugins/main/src/main/res/drawable/load_custom.xml create mode 100644 plugins/main/src/main/res/drawable/send_custom.xml create mode 100644 plugins/main/src/main/res/drawable/set_default.xml create mode 100644 wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt create mode 100644 wear/src/main/res/layout/activity_custom.xml create mode 100644 wear/src/main/res/xml/watch_face_configuration_custom.xml diff --git a/_docs/icons/HourHand.svg b/_docs/icons/HourHand.svg new file mode 100644 index 0000000000..421b2d37a7 --- /dev/null +++ b/_docs/icons/HourHand.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/_docs/icons/MinuteHand.svg b/_docs/icons/MinuteHand.svg new file mode 100644 index 0000000000..07e4f81fb1 --- /dev/null +++ b/_docs/icons/MinuteHand.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/_docs/icons/SecondHand.svg b/_docs/icons/SecondHand.svg new file mode 100644 index 0000000000..0d66040491 --- /dev/null +++ b/_docs/icons/SecondHand.svg @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/_docs/icons/SimplifiedDial.svg b/_docs/icons/SimplifiedDial.svg new file mode 100644 index 0000000000..fbce895e01 --- /dev/null +++ b/_docs/icons/SimplifiedDial.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_docs/icons/background.svg b/_docs/icons/background.svg new file mode 100644 index 0000000000..ea30c0c8a5 --- /dev/null +++ b/_docs/icons/background.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/_docs/icons/detailed_dial.svg b/_docs/icons/detailed_dial.svg new file mode 100644 index 0000000000..3c079740f0 --- /dev/null +++ b/_docs/icons/detailed_dial.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_docs/icons/export_custom.svg b/_docs/icons/export_custom.svg new file mode 100644 index 0000000000..b4c4d12ae8 --- /dev/null +++ b/_docs/icons/export_custom.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_docs/icons/load_custom.svg b/_docs/icons/load_custom.svg new file mode 100644 index 0000000000..90fe7bfe37 --- /dev/null +++ b/_docs/icons/load_custom.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_docs/icons/send_custom.svg b/_docs/icons/send_custom.svg new file mode 100644 index 0000000000..70168f1472 --- /dev/null +++ b/_docs/icons/send_custom.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/_docs/icons/set_default.svg b/_docs/icons/set_default.svg new file mode 100644 index 0000000000..90e36585a2 --- /dev/null +++ b/_docs/icons/set_default.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/app-wear-shared/shared/build.gradle b/app-wear-shared/shared/build.gradle index 5e24d768e0..e69dd02310 100644 --- a/app-wear-shared/shared/build.gradle +++ b/app-wear-shared/shared/build.gradle @@ -34,6 +34,7 @@ dependencies { api 'com.github.tony19:logback-android:2.0.0' api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_version" + api "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinx_serialization_version" api "org.apache.commons:commons-lang3:$commonslang3_version" //RxBus diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventMobileDataToWear.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventMobileDataToWear.kt new file mode 100644 index 0000000000..9cc327ae92 --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventMobileDataToWear.kt @@ -0,0 +1,5 @@ +package info.nightscout.rx.events + +import info.nightscout.rx.weardata.EventData + +class EventMobileDataToWear(val payload: EventData.ActionSetCustomWatchface) : Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearCwfExported.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearCwfExported.kt new file mode 100644 index 0000000000..1d95780241 --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearCwfExported.kt @@ -0,0 +1,5 @@ +package info.nightscout.rx.events + +import info.nightscout.rx.weardata.EventData + +class EventWearCwfExported(val payload: EventData.ActionSetCustomWatchface): Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearDataToMobile.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearDataToMobile.kt new file mode 100644 index 0000000000..c26c8fd8b6 --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearDataToMobile.kt @@ -0,0 +1,5 @@ +package info.nightscout.rx.events + +import info.nightscout.rx.weardata.EventData + +class EventWearDataToMobile(val payload: EventData) : Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt new file mode 100644 index 0000000000..7aae63204a --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt @@ -0,0 +1,3 @@ +package info.nightscout.rx.events + +class EventWearUpdateGui : Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt new file mode 100644 index 0000000000..cef6aa181a --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt @@ -0,0 +1,140 @@ +package info.nightscout.rx.weardata + +import android.content.Context +import android.content.res.Resources +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Parcelable +import android.util.Xml +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import info.nightscout.shared.R +import kotlinx.serialization.Serializable +import org.json.JSONObject +import java.io.File + +enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon: Int?, val fileName: String) { + UNKNOWN("unknown", null,"Unknown"), + CUSTOM_WATCHFACE("customWatchface", R.drawable.watchface_custom, "CustomWatchface"), + BACKGROUND("background", R.drawable.background, "Background"), + COVERCHART("cover_chart", null,"CoverChart"), + COVERPLATE("cover_plate", R.drawable.simplified_dial, "CoverPlate"), + HOURHAND("hour_hand", R.drawable.hour_hand,"HourHand"), + MINUTEHAND("minute_hand", R.drawable.minute_hand,"MinuteHand"), + SECONDHAND("second_hand", R.drawable.second_hand, "SecondHand"); + + companion object { + + private val keyToEnumMap = HashMap() + private val fileNameToEnumMap = HashMap() + + init { + for (value in values()) keyToEnumMap[value.key] = value + for (value in values()) fileNameToEnumMap[value.fileName] = value + } + + fun fromKey(key: String): CustomWatchfaceDrawableDataKey = + if (keyToEnumMap.containsKey(key)) { + keyToEnumMap[key] ?:UNKNOWN + } else { + UNKNOWN + } + + fun fromFileName(file: String): CustomWatchfaceDrawableDataKey = + if (fileNameToEnumMap.containsKey(file.substringBeforeLast("."))) { + fileNameToEnumMap[file.substringBeforeLast(".")] ?:UNKNOWN + } else { + UNKNOWN + } + } + +} + +enum class DrawableFormat(val extension: String) { + UNKNOWN(""), + //XML("xml"), + //svg("svg"), + JPG("jpg"), + PNG("png"); + + companion object { + + private val extensionToEnumMap = HashMap() + + init { + for (value in values()) extensionToEnumMap[value.extension] = value + } + + fun fromFileName(fileName: String): DrawableFormat = + if (extensionToEnumMap.containsKey(fileName.substringAfterLast("."))) { + extensionToEnumMap[fileName.substringAfterLast(".")] ?:UNKNOWN + } else { + UNKNOWN + } + } +} + +@Serializable +data class DrawableData(val value: ByteArray, val format: DrawableFormat) { + + fun toDrawable(resources: Resources): Drawable? { + try { + return when (format) { + DrawableFormat.PNG, DrawableFormat.JPG -> { + val bitmap = BitmapFactory.decodeByteArray(value, 0, value.size) + BitmapDrawable(resources, bitmap) + } +/* + DrawableFormat.XML -> { + val xmlInputStream = ByteArrayInputStream(value) + val xmlPullParser = Xml.newPullParser() + xmlPullParser.setInput(xmlInputStream, null) + Drawable.createFromXml(resources, xmlPullParser) + } +*/ + else -> null + } + } catch (e: Exception) { + return null + } + } +} + +typealias CustomWatchfaceDrawableDataMap = MutableMap +typealias CustomWatchfaceMetadataMap = MutableMap + +data class CustomWatchface(val json: String, var metadata: CustomWatchfaceMetadataMap, val drawableDatas: CustomWatchfaceDrawableDataMap) + +interface CustomWatchfaceFormat { + + fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface) + fun loadCustomWatchface(cwfFile: File): CustomWatchface? + fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap +} + +enum class CustomWatchfaceMetadataKey(val key: String, @StringRes val label: Int) { + + CWF_NAME("name", R.string.metadata_label_watchface_name), + CWF_AUTHOR("author", R.string.metadata_label_watchface_author), + CWF_CREATED_AT("created_at", R.string.metadata_label_watchface_created_at), + CWF_VERSION("cwf_version", R.string.metadata_label_watchface_version); + + companion object { + + private val keyToEnumMap = HashMap() + + init { + for (value in values()) keyToEnumMap[value.key] = value + } + + fun fromKey(key: String): CustomWatchfaceMetadataKey? = + if (keyToEnumMap.containsKey(key)) { + keyToEnumMap[key] + } else { + null + } + + } + +} \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt index 59c8bc6fa7..88bf4fa1bc 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt @@ -3,6 +3,7 @@ package info.nightscout.rx.weardata import info.nightscout.rx.events.Event import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.protobuf.ProtoBuf import org.joda.time.DateTime import java.util.Objects @@ -13,6 +14,7 @@ sealed class EventData : Event() { fun serialize() = Json.encodeToString(serializer(), this) + fun serializeByte() = ProtoBuf.encodeToByteArray(serializer(), this) companion object { fun deserialize(json: String) = try { @@ -20,6 +22,12 @@ sealed class EventData : Event() { } catch (ignored: Exception) { Error(System.currentTimeMillis()) } + + fun deserializeByte(byteArray: ByteArray) = try { + ProtoBuf.decodeFromByteArray(serializer(), byteArray) + } catch (ignored: Exception) { + Error(System.currentTimeMillis()) + } } // Mobile <- Wear @@ -142,6 +150,12 @@ sealed class EventData : Event() { @Serializable data class CancelNotification(val timeStamp: Long) : EventData() + @Serializable + data class ActionGetCustomWatchface( + val customWatchface: ActionSetCustomWatchface, + val exportFile: Boolean + ) : EventData() + @Serializable data class ActionPing(val timeStamp: Long) : EventData() @@ -267,6 +281,18 @@ sealed class EventData : Event() { val validTo: Int ) : EventData() } + @Serializable + data class ActionSetCustomWatchface( + val name: String, + val json: String, + val drawableDataMap: CustomWatchfaceDrawableDataMap + ) : EventData() + + @Serializable + data class ActionrequestCustomWatchface(val exportFile: Boolean) : EventData() + + @Serializable + data class ActionrequestSetDefaultWatchface(val timeStamp: Long) : EventData() @Serializable data class ActionProfileSwitchOpenActivity(val timeShift: Int, val percentage: Int) : EventData() diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt b/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt index 1ed083888d..7958f7403a 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt @@ -150,6 +150,10 @@ class DateUtil @Inject constructor(private val context: Context) { return DateTime(mills).toString(DateTimeFormat.forPattern(format)) } + fun secondString(): String = secondString(now()) + fun secondString(mills: Long): String = + DateTime(mills).toString(DateTimeFormat.forPattern("ss")) + fun minuteString(): String = minuteString(now()) fun minuteString(mills: Long): String = DateTime(mills).toString(DateTimeFormat.forPattern("mm")) diff --git a/app-wear-shared/shared/src/main/res/drawable/background.xml b/app-wear-shared/shared/src/main/res/drawable/background.xml new file mode 100644 index 0000000000..76226923e6 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/detailed_dial.xml b/app-wear-shared/shared/src/main/res/drawable/detailed_dial.xml new file mode 100644 index 0000000000..fa5c92bde2 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/detailed_dial.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/hour_hand.xml b/app-wear-shared/shared/src/main/res/drawable/hour_hand.xml new file mode 100644 index 0000000000..c29364421b --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/hour_hand.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/minute_hand.xml b/app-wear-shared/shared/src/main/res/drawable/minute_hand.xml new file mode 100644 index 0000000000..ae37534d34 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/minute_hand.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/second_hand.xml b/app-wear-shared/shared/src/main/res/drawable/second_hand.xml new file mode 100644 index 0000000000..96464ba7a8 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/second_hand.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/simplified_dial.xml b/app-wear-shared/shared/src/main/res/drawable/simplified_dial.xml new file mode 100644 index 0000000000..63cca01bfb --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/simplified_dial.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/watchface_custom.png b/app-wear-shared/shared/src/main/res/drawable/watchface_custom.png new file mode 100644 index 0000000000000000000000000000000000000000..9678177c64ab2fbd0001fc2e4743df1eb3199c24 GIT binary patch literal 41646 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 Waiting for disconnection + + key_custom_watchface + Created at: %1$s + Author: %1$s + Name: %1$s + Watchface version: %1$s + Default Watchface + \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/res/values/wear_paths.xml b/app-wear-shared/shared/src/main/res/values/wear_paths.xml index 36d7325849..377785cc8e 100644 --- a/app-wear-shared/shared/src/main/res/values/wear_paths.xml +++ b/app-wear-shared/shared/src/main/res/values/wear_paths.xml @@ -1,4 +1,5 @@ /rx_bridge + /rx_data_bridge \ No newline at end of file diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt new file mode 100644 index 0000000000..5019ae87e4 --- /dev/null +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt @@ -0,0 +1,21 @@ +package info.nightscout.interfaces.maintenance + +import android.os.Parcelable +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap +import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap +import info.nightscout.rx.weardata.DrawableData +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import java.io.File + +data class CustomWatchfaceFile( + val name: String, + val file: File, + val baseDir: File, + val json: String, + + val metadata: @RawValue CustomWatchfaceMetadataMap, + val drawableFiles: @RawValue CustomWatchfaceDrawableDataMap + +) \ No newline at end of file diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt index 18ea0f32d3..99657fe7c3 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt @@ -2,12 +2,16 @@ package info.nightscout.interfaces.maintenance import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import info.nightscout.rx.weardata.EventData interface ImportExportPrefs { fun importSharedPreferences(activity: FragmentActivity, importFile: PrefsFile) fun importSharedPreferences(activity: FragmentActivity) fun importSharedPreferences(fragment: Fragment) + fun importCustomWatchface(activity: FragmentActivity) + fun importCustomWatchface(fragment: Fragment) + fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface) fun prefsFileExists(): Boolean fun verifyStoragePermissions(fragment: Fragment, onGranted: Runnable) fun exportSharedPreferences(f: Fragment) diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt index 8bf0d497dd..9d131e9fdb 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt @@ -10,7 +10,9 @@ interface PrefFileListProvider { fun ensureExtraDirExists(): File fun newExportFile(): File fun newExportCsvFile(): File + fun newCwfFile(filename: String): File fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList + fun listCustomWatchfaceFiles(): MutableList fun checkMetadata(metadata: Map): Map fun formatExportedAgo(utcTime: String): String } \ No newline at end of file diff --git a/plugins/configuration/src/main/AndroidManifest.xml b/plugins/configuration/src/main/AndroidManifest.xml index 2d2811718b..700ebdafae 100644 --- a/plugins/configuration/src/main/AndroidManifest.xml +++ b/plugins/configuration/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ android:name=".maintenance.activities.PrefImportListActivity" android:exported="false" android:theme="@style/AppTheme" /> + () { + + companion object { + const val OUTPUT_PARAM = "custom_file" + } + + override fun parseResult(resultCode: Int, intent: Intent?): Unit? { + return when (resultCode) { + FragmentActivity.RESULT_OK -> Unit + else -> null + } + } + + override fun createIntent(context: Context, input: Void?): Intent { + return Intent(context, info.nightscout.configuration.maintenance.activities.CustomWatchfaceImportListActivity::class.java) + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt index 941a5b9cb3..567e61fca7 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt @@ -22,6 +22,7 @@ import dagger.android.HasAndroidInjector import info.nightscout.configuration.R import info.nightscout.configuration.activities.DaggerAppCompatActivityWithResult import info.nightscout.configuration.maintenance.dialogs.PrefImportSummaryDialog +import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat import info.nightscout.core.ui.dialogs.OKDialog import info.nightscout.core.ui.dialogs.TwoMessagesAlertDialog @@ -55,6 +56,7 @@ import info.nightscout.rx.events.EventAppExit import info.nightscout.rx.events.EventDiaconnG8PumpLogReset import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag +import info.nightscout.rx.weardata.EventData import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil @@ -84,7 +86,8 @@ class ImportExportPrefsImpl @Inject constructor( private val prefFileList: PrefFileListProvider, private val uel: UserEntryLogger, private val dateUtil: DateUtil, - private val uiInteraction: UiInteraction + private val uiInteraction: UiInteraction, + private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat ) : ImportExportPrefs { override fun prefsFileExists(): Boolean { @@ -297,6 +300,27 @@ class ImportExportPrefsImpl @Inject constructor( } } + override fun importCustomWatchface(fragment: Fragment) { + fragment.activity?.let { importCustomWatchface(it) } + } + override fun importCustomWatchface(activity: FragmentActivity) { + try { + if (activity is DaggerAppCompatActivityWithResult) + activity.callForCustomWatchfaceFile.launch(null) + } catch (e: IllegalArgumentException) { + // this exception happens on some early implementations of ActivityResult contracts + // when registered and called for the second time + ToastUtils.errorToast(activity, rh.gs(R.string.goto_main_try_again)) + log.error(LTag.CORE, "Internal android framework exception", e) + } + } + + override fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface) { + prefFileList.ensureExportDirExists() + val newFile = prefFileList.newCwfFile(customWatchface.name) + customWatchfaceCWFFormat.saveCustomWatchface(newFile,customWatchface) + } + override fun importSharedPreferences(activity: FragmentActivity, importFile: PrefsFile) { askToConfirmImport(activity, importFile) { password -> diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt index d060dc1304..b8d66e3bd5 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt @@ -6,8 +6,10 @@ import dagger.Lazy import dagger.Reusable import info.nightscout.androidaps.annotations.OpenForTesting import info.nightscout.configuration.R +import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat import info.nightscout.interfaces.Config +import info.nightscout.interfaces.maintenance.CustomWatchfaceFile import info.nightscout.interfaces.maintenance.PrefFileListProvider import info.nightscout.interfaces.maintenance.PrefMetadata import info.nightscout.interfaces.maintenance.PrefMetadataMap @@ -34,6 +36,7 @@ class PrefFileListProviderImpl @Inject constructor( private val rh: ResourceHelper, private val config: Lazy, private val encryptedPrefsFormat: EncryptedPrefsFormat, + private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat, private val storage: Storage, private val versionCheckerUtils: VersionCheckerUtils, context: Context @@ -88,6 +91,20 @@ class PrefFileListProviderImpl @Inject constructor( return prefFiles } + override fun listCustomWatchfaceFiles(): MutableList { + val customWatchfaceFiles = mutableListOf() + + // searching dedicated dir, only for new CWF format + exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION) }.forEach { file -> + // Here loadCustomWatchface will unzip, check and load CustomWatchface + customWatchfaceCWFFormat.loadCustomWatchface(file)?.also { customWatchface -> + customWatchfaceFiles.add(CustomWatchfaceFile(file.name, file, exportsPath, customWatchface.json, customWatchface.metadata, customWatchface.drawableDatas)) + } + } + + return customWatchfaceFiles + } + private fun metadataFor(loadMetadata: Boolean, contents: String): PrefMetadataMap { if (!loadMetadata) { return mapOf() @@ -128,6 +145,10 @@ class PrefFileListProviderImpl @Inject constructor( val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss")) return File(exportsPath, timeLocal + "_UserEntry.csv") } + override fun newCwfFile(filename: String): File { + val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss")) + return File(exportsPath, "${filename}_$timeLocal${ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION}") + } // check metadata for known issues, change their status and add info with explanations override fun checkMetadata(metadata: Map): Map { diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt new file mode 100644 index 0000000000..d225ae790d --- /dev/null +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt @@ -0,0 +1,112 @@ +package info.nightscout.configuration.maintenance.activities + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.ViewGroup + +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import info.nightscout.core.ui.activities.TranslatedDaggerAppCompatActivity +import info.nightscout.interfaces.maintenance.CustomWatchfaceFile +import info.nightscout.interfaces.maintenance.PrefFileListProvider +import info.nightscout.configuration.databinding.CustomWatchfaceImportListActivityBinding +import info.nightscout.configuration.R +import info.nightscout.configuration.databinding.CustomWatchfaceImportListItemBinding +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventMobileDataToWear +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey.* +import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP +import javax.inject.Inject + +class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { + + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var sp: SP + @Inject lateinit var prefFileListProvider: PrefFileListProvider + @Inject lateinit var rxBus: RxBus + @Inject lateinit var aapsLogger: AAPSLogger + + private lateinit var binding: CustomWatchfaceImportListActivityBinding + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = CustomWatchfaceImportListActivityBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + title = rh.gs(R.string.wear_import_custom_watchface_title) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(true) + + binding.recyclerview.layoutManager = LinearLayoutManager(this) + binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles()) + } + + inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { + + inner class PrefFileViewHolder(val customWatchfaceImportListItemBinding: CustomWatchfaceImportListItemBinding) : RecyclerView.ViewHolder(customWatchfaceImportListItemBinding.root) { + + init { + with(customWatchfaceImportListItemBinding) { + root.isClickable = true + customWatchfaceImportListItemBinding.root.setOnClickListener { + val customWatchfaceFile = filelistName.tag as CustomWatchfaceFile + val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile.metadata[CWF_NAME] ?:"", customWatchfaceFile.json, customWatchfaceFile.drawableFiles) + sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWF.serialize()) + val i = Intent() + setResult(FragmentActivity.RESULT_OK, i) + rxBus.send(EventMobileDataToWear(customWF)) + aapsLogger.debug("XXXXX EventMobileDataToWear sent") + + finish() + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PrefFileViewHolder { + val binding = CustomWatchfaceImportListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PrefFileViewHolder(binding) + } + + override fun getItemCount(): Int { + return customWatchfaceFileList.size + } + + override fun onBindViewHolder(holder: PrefFileViewHolder, position: Int) { + val customWatchfaceFile = customWatchfaceFileList[position] + val metadata = customWatchfaceFile.metadata + val drawable = customWatchfaceFile.drawableFiles[CustomWatchfaceDrawableDataKey + .CUSTOM_WATCHFACE]?.toDrawable(resources) + with(holder.customWatchfaceImportListItemBinding) { + filelistName.text = rh.gs(R.string.wear_import_filename, customWatchfaceFile.file.name) + filelistName.tag = customWatchfaceFile + customWatchface.setImageDrawable(drawable) + filelistDir.text = rh.gs(R.string.wear_import_directory, customWatchfaceFile.file.parentFile?.absolutePath) + customName.text = rh.gs(CWF_NAME.label, metadata[CWF_NAME]) + author.text = rh.gs(CWF_AUTHOR.label, metadata[CWF_AUTHOR] ?:"") + createdAt.text = rh.gs(CWF_CREATED_AT.label, metadata[CWF_CREATED_AT] ?:"") + cwfVersion.text = rh.gs(CWF_VERSION.label, metadata[CWF_VERSION] ?:"") + + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finish() + return true + } + return super.onOptionsItemSelected(item) + } + +} \ No newline at end of file diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt new file mode 100644 index 0000000000..6414187d1a --- /dev/null +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt @@ -0,0 +1,128 @@ +package info.nightscout.configuration.maintenance.formats + +import info.nightscout.core.utils.CryptoUtil +import info.nightscout.interfaces.storage.Storage +import info.nightscout.rx.weardata.CustomWatchface +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap +import info.nightscout.rx.weardata.CustomWatchfaceFormat +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey +import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap +import info.nightscout.rx.weardata.DrawableData +import info.nightscout.rx.weardata.DrawableFormat +import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.interfaces.ResourceHelper +import org.json.JSONObject +import java.io.BufferedOutputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ZipCustomWatchfaceFormat @Inject constructor( + private var rh: ResourceHelper, + private var cryptoUtil: CryptoUtil, + private var storage: Storage +) : CustomWatchfaceFormat { + + companion object { + + const val CUSTOM_WF_EXTENTION = ".zip" + const val CUSTOM_JSON_FILE = "CustomWatchface.json" + } + + override fun loadCustomWatchface(cwfFile: File): CustomWatchface? { + var json = JSONObject() + var metadata: CustomWatchfaceMetadataMap = mutableMapOf() + val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf() + + try { + val zipInputStream = ZipInputStream(cwfFile.inputStream()) + var zipEntry: ZipEntry? = zipInputStream.nextEntry + while (zipEntry != null) { + val entryName = zipEntry.name + + val buffer = ByteArray(2048) + val byteArrayOutputStream = ByteArrayOutputStream() + var count = zipInputStream.read(buffer) + while (count != -1) { + byteArrayOutputStream.write(buffer, 0, count) + count = zipInputStream.read(buffer) + } + zipInputStream.closeEntry() + + if (entryName == CUSTOM_JSON_FILE) { + val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8) + json = JSONObject(jsonString) + metadata = loadMetadata(json) + } else { + val customWatchfaceDrawableDataKey = CustomWatchfaceDrawableDataKey.fromFileName(entryName) + val drawableFormat = DrawableFormat.fromFileName(entryName) + if (customWatchfaceDrawableDataKey != CustomWatchfaceDrawableDataKey.UNKNOWN && drawableFormat != DrawableFormat.UNKNOWN) { + drawableDatas[customWatchfaceDrawableDataKey] = DrawableData(byteArrayOutputStream.toByteArray(),drawableFormat) + } + } + zipEntry = zipInputStream.nextEntry + } + + // Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image + if (metadata.containsKey(CustomWatchfaceMetadataKey.CWF_NAME) && drawableDatas.containsKey(CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE)) + return CustomWatchface(json.toString(4), metadata, drawableDatas) + else + return null + + } catch (e: Exception) { + return null + } + } + + + override fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface) { + + try { + val outputStream = FileOutputStream(file) + val zipOutputStream = ZipOutputStream(BufferedOutputStream(outputStream)) + + // Ajouter le fichier JSON au ZIP + val jsonEntry = ZipEntry(CUSTOM_JSON_FILE) + zipOutputStream.putNextEntry(jsonEntry) + zipOutputStream.write(customWatchface.json.toByteArray()) + zipOutputStream.closeEntry() + + // Ajouter les fichiers divers au ZIP + for (drawableData in customWatchface.drawableDataMap) { + val fileEntry = ZipEntry("${drawableData.key.fileName}.${drawableData.value.format.extension}") + zipOutputStream.putNextEntry(fileEntry) + zipOutputStream.write(drawableData.value.value) + zipOutputStream.closeEntry() + } + zipOutputStream.close() + outputStream.close() + } catch (e: Exception) { + + } + + } + + + override fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap { + val metadata: CustomWatchfaceMetadataMap = mutableMapOf() + + if (contents.has("metadata")) { + val meta = contents.getJSONObject("metadata") + for (key in meta.keys()) { + val metaKey = CustomWatchfaceMetadataKey.fromKey(key) + if (metaKey != null) { + metadata[metaKey] = meta.getString(key) + } + } + } + return metadata + } + +} \ No newline at end of file diff --git a/plugins/configuration/src/main/res/layout/custom_watchface_import_list_activity.xml b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_activity.xml new file mode 100644 index 0000000000..5095328ce9 --- /dev/null +++ b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_activity.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml new file mode 100644 index 0000000000..dfd624b429 --- /dev/null +++ b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/configuration/src/main/res/values/strings.xml b/plugins/configuration/src/main/res/values/strings.xml index 1cec48dfba..15e68dacd6 100644 --- a/plugins/configuration/src/main/res/values/strings.xml +++ b/plugins/configuration/src/main/res/values/strings.xml @@ -164,6 +164,11 @@ Missing encryption configuration, settings format is invalid! Unsupported or not specified encryption algorithm! + + Select Custom Watchface + Directory: %1$s + File name: %1$s + Please reboot your phone or restart AAPS from the System Settings \notherwise Android APS will not have logging (important to track and verify that the algorithms are working correctly)! diff --git a/plugins/main/src/main/AndroidManifest.xml b/plugins/main/src/main/AndroidManifest.xml index 6d24217b2e..75ecc0da85 100644 --- a/plugins/main/src/main/AndroidManifest.xml +++ b/plugins/main/src/main/AndroidManifest.xml @@ -51,6 +51,15 @@ android:pathPrefix="@string/path_rx_bridge" android:scheme="wear" /> + + + + + + diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt index 57cd89ec9d..848facfbec 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt @@ -5,13 +5,21 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import dagger.android.support.DaggerFragment +import info.nightscout.core.ui.toast.ToastUtils import info.nightscout.core.utils.fabric.FabricPrivacy +import info.nightscout.interfaces.maintenance.ImportExportPrefs +import info.nightscout.plugins.R import info.nightscout.plugins.databinding.WearFragmentBinding -import info.nightscout.plugins.general.wear.events.EventWearUpdateGui import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventMobileDataToWear import info.nightscout.rx.events.EventMobileToWear +import info.nightscout.rx.events.EventWearUpdateGui +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign @@ -24,9 +32,12 @@ class WearFragment : DaggerFragment() { @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var importExportPrefs: ImportExportPrefs + @Inject lateinit var sp:SP + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var aapsLogger: AAPSLogger private var _binding: WearFragmentBinding? = null - private val disposable = CompositeDisposable() // This property is only valid between onCreateView and @@ -43,6 +54,25 @@ class WearFragment : DaggerFragment() { super.onViewCreated(view, savedInstanceState) binding.resend.setOnClickListener { rxBus.send(EventData.ActionResendData("WearFragment")) } binding.openSettings.setOnClickListener { rxBus.send(EventMobileToWear(EventData.OpenSettings(dateUtil.now()))) } + + binding.loadCustom.setOnClickListener { + importExportPrefs.verifyStoragePermissions(this) { + importExportPrefs.importCustomWatchface(this) + } + } + binding.defaultCustom.setOnClickListener { + sp.remove(info.nightscout.shared.R.string.key_custom_watchface) + wearPlugin.savedCustomWatchface = null + rxBus.send(EventMobileToWear(EventData.ActionrequestSetDefaultWatchface(dateUtil.now()))) + updateGui() + } + binding.sendCustom.setOnClickListener { + wearPlugin.savedCustomWatchface?.let { cwf -> rxBus.send(EventMobileDataToWear(cwf)) } + } + binding.exportCustom.setOnClickListener { + wearPlugin.savedCustomWatchface?.let { importExportPrefs.exportCustomWatchface(it) } + ?: apply { rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(true)))} + } } override fun onResume() { @@ -51,6 +81,19 @@ class WearFragment : DaggerFragment() { .toObservable(EventWearUpdateGui::class.java) .observeOn(aapsSchedulers.main) .subscribe({ updateGui() }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventMobileDataToWear::class.java) + .observeOn(aapsSchedulers.main) + .subscribe({ + loadCustom(it.payload) + wearPlugin.customWatchfaceSerialized = "" + wearPlugin.savedCustomWatchface = null + updateGui() + ToastUtils.okToast(context,rh.gs(R.string.wear_new_custom_watchface_received)) + }, fabricPrivacy::logException) + if (wearPlugin.savedCustomWatchface == null) + rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(false))) + //EventMobileDataToWear updateGui() } @@ -67,6 +110,34 @@ class WearFragment : DaggerFragment() { private fun updateGui() { _binding ?: return + sp.getString(info.nightscout.shared.R.string.key_custom_watchface, "").let { + if (it != wearPlugin.customWatchfaceSerialized && it != "") { + aapsLogger.debug("XXXXX Serialisation: ${it.length}") + try { + wearPlugin.savedCustomWatchface = (EventData.deserialize(it) as EventData.ActionSetCustomWatchface) + wearPlugin.customWatchfaceSerialized = it + } + catch(e: Exception) { + wearPlugin.customWatchfaceSerialized = "" + wearPlugin.savedCustomWatchface = null + } + } + sp.remove(info.nightscout.shared.R.string.key_custom_watchface) + } + wearPlugin.savedCustomWatchface?.let { + binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.name) + binding.sendCustom.visibility = View.VISIBLE + binding.coverChart.setImageDrawable(it.drawableDataMap[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE]?.toDrawable(resources)) + } ?:apply { + binding.customName.text = rh.gs(R.string.wear_custom_watchface, rh.gs(info.nightscout.shared.R.string.wear_default_watchface)) + binding.sendCustom.visibility = View.INVISIBLE + binding.coverChart.setImageDrawable(null) + } binding.connectedDevice.text = wearPlugin.connectedDevice } + + private fun loadCustom(cwf: EventData.ActionSetCustomWatchface) { + aapsLogger.debug("XXXXX EventWearCwfExported received") + wearPlugin.savedCustomWatchface = cwf + } } \ No newline at end of file diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt index a17e63134e..21516d6d1e 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt @@ -54,6 +54,8 @@ class WearPlugin @Inject constructor( private val disposable = CompositeDisposable() var connectedDevice = "---" + var customWatchfaceSerialized = "" + var savedCustomWatchface: EventData.ActionSetCustomWatchface? = null override fun onStart() { super.onStart() diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt index 066e49bf2b..a1fe993f56 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt @@ -41,6 +41,7 @@ import info.nightscout.interfaces.iob.GlucoseStatusProvider import info.nightscout.interfaces.iob.InMemoryGlucoseValue import info.nightscout.interfaces.iob.IobCobCalculator import info.nightscout.interfaces.logging.UserEntryLogger +import info.nightscout.interfaces.maintenance.ImportExportPrefs import info.nightscout.interfaces.nsclient.ProcessedDeviceStatusData import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.plugin.PluginBase @@ -59,6 +60,8 @@ import info.nightscout.plugins.R import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventMobileToWear +import info.nightscout.rx.events.EventWearCwfExported +import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag import info.nightscout.rx.weardata.EventData @@ -107,7 +110,8 @@ class DataHandlerMobile @Inject constructor( private val commandQueue: CommandQueue, private val fabricPrivacy: FabricPrivacy, private val uiInteraction: UiInteraction, - private val persistenceLayer: PersistenceLayer + private val persistenceLayer: PersistenceLayer, + private val importExportPrefs: ImportExportPrefs ) { private val disposable = CompositeDisposable() @@ -314,6 +318,13 @@ class DataHandlerMobile @Inject constructor( .toObservable(EventData.ActionHeartRate::class.java) .observeOn(aapsSchedulers.io) .subscribe({ handleHeartRate(it) }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventData.ActionGetCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ + aapsLogger.debug(LTag.WEAR, "Custom Watch face ${it.customWatchface} received from ${it.sourceNodeId}") + handleGetCustomWatchface(it) + }, fabricPrivacy::logException) } private fun handleTddStatus() { @@ -1247,4 +1258,19 @@ class DataHandlerMobile @Inject constructor( device = actionHeartRate.device) repository.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait() } + + + private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) { + val customWatchface = command.customWatchface + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.json}") + //Update Wear Fragment + sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWatchface.serialize()) + //rxBus.send(EventWearCwfExported(customWatchface)) + rxBus.send(EventWearUpdateGui()) + if (command.exportFile) + importExportPrefs.exportCustomWatchface(customWatchface) + //Implement here a record within SP and a save within exports subfolder as zipFile + + } + } diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt index 13a8ff47f9..dac0274681 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -26,9 +26,10 @@ import info.nightscout.interfaces.profile.ProfileFunction import info.nightscout.interfaces.receivers.ReceiverStatusStore import info.nightscout.plugins.R import info.nightscout.plugins.general.wear.WearPlugin -import info.nightscout.plugins.general.wear.events.EventWearUpdateGui +import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventMobileDataToWear import info.nightscout.rx.events.EventMobileToWear import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -80,6 +81,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private val disposable = CompositeDisposable() private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge) + private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge) override fun onCreate() { AndroidInjection.inject(this) @@ -90,6 +92,10 @@ class DataLayerListenerServiceMobile : WearableListenerService() { .toObservable(EventMobileToWear::class.java) .observeOn(aapsSchedulers.io) .subscribe { sendMessage(rxPath, it.payload.serialize()) } + disposable += rxBus + .toObservable(EventMobileDataToWear::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { sendMessage(rxDataPath, it.payload.serializeByte()) } } override fun onCapabilityChanged(p0: CapabilityInfo) { @@ -136,6 +142,11 @@ class DataLayerListenerServiceMobile : WearableListenerService() { val command = EventData.deserialize(String(messageEvent.data)) rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) } + rxDataPath -> { + aapsLogger.debug(LTag.WEAR, "onMessageReceived rxDataPath: ${String(messageEvent.data)}") + val command = EventData.deserializeByte(messageEvent.data) + rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + } } } } @@ -164,7 +175,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private fun pickBestNodeId(nodes: Set): Node? = nodes.firstOrNull { it.isNearby } ?: nodes.firstOrNull() - @Suppress("unused") + //@Suppress("unused") private fun sendData(path: String, vararg params: DataMap) { if (wearPlugin.isEnabled()) { scope.launch { @@ -201,7 +212,6 @@ class DataLayerListenerServiceMobile : WearableListenerService() { } } - @Suppress("unused") private fun sendMessage(path: String, data: ByteArray) { aapsLogger.debug(LTag.WEAR, "sendMessage: $path") transcriptionNodeId?.also { nodeId -> diff --git a/plugins/main/src/main/res/drawable/export_custom.xml b/plugins/main/src/main/res/drawable/export_custom.xml new file mode 100644 index 0000000000..3779b637df --- /dev/null +++ b/plugins/main/src/main/res/drawable/export_custom.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + diff --git a/plugins/main/src/main/res/drawable/load_custom.xml b/plugins/main/src/main/res/drawable/load_custom.xml new file mode 100644 index 0000000000..26a7dda859 --- /dev/null +++ b/plugins/main/src/main/res/drawable/load_custom.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + diff --git a/plugins/main/src/main/res/drawable/send_custom.xml b/plugins/main/src/main/res/drawable/send_custom.xml new file mode 100644 index 0000000000..2602e2953d --- /dev/null +++ b/plugins/main/src/main/res/drawable/send_custom.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/plugins/main/src/main/res/drawable/set_default.xml b/plugins/main/src/main/res/drawable/set_default.xml new file mode 100644 index 0000000000..ff43d0d255 --- /dev/null +++ b/plugins/main/src/main/res/drawable/set_default.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/plugins/main/src/main/res/layout/wear_fragment.xml b/plugins/main/src/main/res/layout/wear_fragment.xml index cfd22a1f4f..1eb374529d 100644 --- a/plugins/main/src/main/res/layout/wear_fragment.xml +++ b/plugins/main/src/main/res/layout/wear_fragment.xml @@ -1,42 +1,201 @@ - + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="4dp" + app:cardCornerRadius="4dp" + app:contentPadding="2dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="false" + android:layout_gravity="center"> - + - + + + + + + + + + + + + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="4dp" + app:cardCornerRadius="4dp" + app:contentPadding="2dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="false" + android:layout_gravity="center"> + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index c7c4789b6f..b8d2b4eccf 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -361,6 +361,11 @@ Show SMB on the watch like a standard bolus. Show the predictions on the watchface. Predictions + Custom Watchface: %1$s + Load Watchface + Send Watchface + Export Watchface + New watchface received from watch Resend All Data Open Settings on Wear diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 2a08a7f623..ab50e950d1 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -241,6 +241,31 @@ + + + + + + + + + + + + + + @@ -267,6 +292,15 @@ android:pathPrefix="@string/path_rx_bridge" android:scheme="wear" /> + + + + + + + diff --git a/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt b/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt index 389ee261d9..5d5e197670 100644 --- a/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt +++ b/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt @@ -25,6 +25,7 @@ import info.nightscout.androidaps.tile.QuickWizardTileService import info.nightscout.androidaps.tile.TempTargetTileService import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventWearDataToMobile import info.nightscout.rx.events.EventWearToMobile import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -179,6 +180,29 @@ class DataHandlerWear @Inject constructor( TileService.getUpdater(context).requestUpdate(QuickWizardTileService::class.java) } } + disposable += rxBus + .toObservable(EventData.ActionSetCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}") + persistence.store(it) + } + disposable += rxBus + .toObservable(EventData.ActionrequestSetDefaultWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + aapsLogger.debug(LTag.WEAR, "Set Default Watchface received from ${it.sourceNodeId}") + persistence.setDefaultWatchface() + } + disposable += rxBus + .toObservable(EventData.ActionrequestCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { eventData -> + aapsLogger.debug(LTag.WEAR, "Custom Watchface requested from ${eventData.sourceNodeId}") + persistence.readCustomWatchface()?.let { + rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, eventData.exportFile))) + } + } } private fun handleBolusProgress(bolusProgress: EventData.BolusProgress) { diff --git a/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt b/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt index fabc65b8fb..e4c62dd83d 100644 --- a/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt @@ -12,6 +12,7 @@ import info.nightscout.androidaps.interaction.utils.Persistence import info.nightscout.androidaps.interaction.utils.WearUtil import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventWearDataToMobile import info.nightscout.rx.events.EventWearToMobile import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -43,6 +44,7 @@ class DataLayerListenerServiceWear : WearableListenerService() { private val disposable = CompositeDisposable() private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge) + private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge) override fun onCreate() { AndroidInjection.inject(this) @@ -54,6 +56,12 @@ class DataLayerListenerServiceWear : WearableListenerService() { .subscribe { sendMessage(rxPath, it.payload.serialize()) } + disposable += rxBus + .toObservable(EventWearDataToMobile::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + sendMessage(rxDataPath, it.payload.serializeByte()) + } } override fun onCapabilityChanged(p0: CapabilityInfo) { @@ -100,6 +108,14 @@ class DataLayerListenerServiceWear : WearableListenerService() { transcriptionNodeId = messageEvent.sourceNodeId aapsLogger.debug(LTag.WEAR, "Updated node: $transcriptionNodeId") } + rxDataPath -> { + aapsLogger.debug(LTag.WEAR, "onMessageReceived: ${messageEvent.data}") + val command = EventData.deserializeByte(messageEvent.data) + rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + // Use this sender + transcriptionNodeId = messageEvent.sourceNodeId + aapsLogger.debug(LTag.WEAR, "Updated node: $transcriptionNodeId") + } } } diff --git a/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt b/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt index 909c0aa21a..3cc8baa387 100644 --- a/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt +++ b/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt @@ -40,6 +40,7 @@ abstract class WearServicesModule { @ContributesAndroidInjector abstract fun contributesBIGChart(): BigChartWatchface @ContributesAndroidInjector abstract fun contributesNOChart(): NoChartWatchface @ContributesAndroidInjector abstract fun contributesCircleWatchface(): CircleWatchface + @ContributesAndroidInjector abstract fun contributesCustomWatchface(): CustomWatchface @ContributesAndroidInjector abstract fun contributesTileBase(): TileBase @ContributesAndroidInjector abstract fun contributesQuickWizardTileService(): QuickWizardTileService diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt index 557300ca6d..ef97aa4270 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt @@ -36,6 +36,8 @@ open class Persistence @Inject constructor( const val KEY_STALE_REPORTED = "staleReported" const val KEY_DATA_UPDATED = "data_updated_at" + const val CUSTOM_WATCHFACE = "custom_watchface" + const val CUSTOM_DEFAULT_WATCHFACE = "custom_default_watchface" } fun getString(key: String, defaultValue: String): String { @@ -130,6 +132,23 @@ open class Persistence @Inject constructor( return null } + fun readCustomWatchface(isDefault: Boolean = false): EventData.ActionSetCustomWatchface? { + try { + var s = sp.getStringOrNull(if (isDefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, null) + if (s != null) { + return deserialize(s) as EventData.ActionSetCustomWatchface + } else { + s = sp.getStringOrNull(CUSTOM_DEFAULT_WATCHFACE, null) + if (s != null) { + return deserialize(s) as EventData.ActionSetCustomWatchface + } + } + } catch (exception: Exception) { + aapsLogger.error(LTag.WEAR, exception.toString()) + } + return null + } + fun store(singleBg: SingleBg) { putString(BG_DATA_PERSISTENCE_KEY, singleBg.serialize()) aapsLogger.debug(LTag.WEAR, "Stored BG data: $singleBg") @@ -151,6 +170,16 @@ open class Persistence @Inject constructor( aapsLogger.debug(LTag.WEAR, "Stored Status data: $status") } + fun store(customWatchface: EventData.ActionSetCustomWatchface, isdefault: Boolean = false) { + putString(if (isdefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, customWatchface.serialize()) + aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.name} ${isdefault}: $customWatchface") + } + + fun setDefaultWatchface() { + readCustomWatchface(true)?.let {store(it)} + aapsLogger.debug(LTag.WEAR, "Custom Watchface reset to default") + } + fun joinSet(set: Set, separator: String?): String { val sb = StringBuilder() var i = 0 diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt new file mode 100644 index 0000000000..f01f5de785 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -0,0 +1,398 @@ +@file:Suppress("DEPRECATION") + +package info.nightscout.androidaps.watchfaces + +import android.app.ActionBar.LayoutParams +import android.content.Context +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Point +import android.graphics.Typeface +import android.support.wearable.watchface.WatchFaceStyle +import android.util.TypedValue +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.forEach +import androidx.viewbinding.ViewBinding +import info.nightscout.androidaps.R +import info.nightscout.androidaps.databinding.ActivityCustomBinding +import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey +import info.nightscout.rx.weardata.DrawableData +import info.nightscout.rx.weardata.DrawableFormat +import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.extensions.toVisibility +import org.joda.time.TimeOfDay +import org.json.JSONObject +import java.io.ByteArrayOutputStream +import javax.inject.Inject + +class CustomWatchface : BaseWatchFace() { + + @Inject lateinit var context: Context + private lateinit var binding: ActivityCustomBinding + private var zoomFactor = 1.0 + private val displaySize = Point() + private val TEMPLATE_RESOLUTION = 400 + private var lowBatColor = Color.RED + private var bgColor = Color.WHITE + + val CUSTOM_VERSION = "v0.1" + + @Suppress("DEPRECATION") + override fun inflateLayout(inflater: LayoutInflater): ViewBinding { + binding = ActivityCustomBinding.inflate(inflater) + setDefaultColors() + persistence.store(defaultWatchface(), true) + (context.getSystemService(WINDOW_SERVICE) as WindowManager).defaultDisplay.getSize(displaySize) + zoomFactor = (displaySize.x).toDouble() / TEMPLATE_RESOLUTION.toDouble() + return binding + } + + override fun getWatchFaceStyle(): WatchFaceStyle { + return WatchFaceStyle.Builder(this) + .setAcceptsTapEvents(true) + .setHideNotificationIndicator(false) + .setShowUnreadCountIndicator(true) + .build() + } + + override fun setColorDark() { + setWatchfaceStyle() + //@ColorInt val batteryOkColor = ContextCompat.getColor(this, R.color.dark_midColor) + binding.mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.dark_background)) + binding.sgv.setTextColor(bgColor) + binding.direction.setTextColor(bgColor) + + if (ageLevel != 1) + binding.timestamp.setTextColor(ContextCompat.getColor(this, R.color.dark_TimestampOld)) + if (status.batteryLevel != 1) + binding.uploaderBattery.setTextColor(lowBatColor) + when (loopLevel) { + -1 -> binding.loop.setBackgroundResource(R.drawable.loop_grey_25) + 1 -> binding.loop.setBackgroundResource(R.drawable.loop_green_25) + else -> binding.loop.setBackgroundResource(R.drawable.loop_red_25) + } + + basalBackgroundColor = ContextCompat.getColor(this, R.color.basal_dark) + basalCenterColor = ContextCompat.getColor(this, R.color.basal_light) + + // rotate the second hand. + binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f + // rotate the minute hand. + binding.minuteHand.rotation = TimeOfDay().minuteOfHour * 6f + // rotate the hour hand. + binding.hourHand.rotation = TimeOfDay().hourOfDay * 30f + TimeOfDay().minuteOfHour * 0.5f + + setupCharts() + } + + override fun setColorBright() { + setColorDark() + } + + override fun setColorLowRes() { + setColorDark() + } + + override fun setSecond() { + binding.time.text = "${dateUtil.hourString()}:${dateUtil.minuteString()}" + if (enableSecond) ":${dateUtil.secondString()}" else "" + binding.second.text = dateUtil.secondString() + // rotate the second hand. + binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f + //aapsLogger.debug("XXXXXX Setsecond calles:") + } + + private fun setWatchfaceStyle() { + bgColor = when (singleBg.sgvLevel) { + 1L -> highColor + 0L -> midColor + -1L -> lowColor + else -> midColor + } + val customWatchface = persistence.readCustomWatchface() ?: persistence.readCustomWatchface(true) + //aapsLogger.debug("XXXXX + setWatchfaceStyle Json ${customWatchface?.json}") + customWatchface?.let { customWatchface -> + val json = JSONObject(customWatchface.json) + val drawableDataMap = customWatchface.drawableDataMap + enableSecond = (if (json.has("enableSecond")) json.getBoolean("enableSecond") else false) && sp.getBoolean(R.string.key_show_seconds, true) + //aapsLogger.debug("XXXXXX json File (beginning):" + customWatchface.json) + + highColor = if (json.has("highColor")) Color.parseColor(json.getString("highColor")) else ContextCompat.getColor(this, R.color.dark_highColor) + midColor = if (json.has("midColor")) Color.parseColor(json.getString("midColor")) else ContextCompat.getColor(this, R.color.inrange) + lowColor = if (json.has("lowColor")) Color.parseColor(json.getString("lowColor")) else ContextCompat.getColor(this, R.color.low) + lowBatColor = if (json.has("lowBatColor")) Color.parseColor(json.getString("lowBatColor")) else ContextCompat.getColor(this, R.color.dark_uploaderBatteryEmpty) + carbColor = if (json.has("carbColor")) Color.parseColor(json.getString("carbColor")) else ContextCompat.getColor(this, R.color.carbs) + gridColor = if (json.has("gridColor")) Color.parseColor(json.getString("gridColor")) else ContextCompat.getColor(this, R.color.carbs) + pointSize = if (json.has("pointSize")) json.getInt("pointSize") else 2 + aapsLogger.debug("XXXXXX enableSecond $enableSecond ${sp.getBoolean(R.string.key_show_seconds, false)} pointSize $pointSize") + binding.mainLayout.forEach { view -> + //aapsLogger.debug("XXXXXX view:" + view.tag.toString()) + view.tag?.let { tag -> + if (json.has(tag.toString())) { + var viewjson = json.getJSONObject(tag.toString()) + //aapsLogger.debug("XXXXXX \"" + tag.toString() + "\": " + viewjson.toString(4)) + var wrapContent = LayoutParams.WRAP_CONTENT + val width = if (viewjson.has("width")) (viewjson.getInt("width") * zoomFactor).toInt() else wrapContent + val height = if (viewjson.has("height")) (viewjson.getInt("height") * zoomFactor).toInt() else wrapContent + var params = FrameLayout.LayoutParams(width, height) + params.topMargin = if (viewjson.has("topmargin")) (viewjson.getInt("topmargin") * zoomFactor).toInt() else 0 + params.leftMargin = if (viewjson.has("leftmargin")) (viewjson.getInt("leftmargin") * zoomFactor).toInt() else 0 + view.setLayoutParams(params) + view.visibility = if (viewjson.has("visibility")) setVisibility(viewjson.getString("visibility")) else View.GONE + + if (view is TextView) { + view.rotation = if (viewjson.has("rotation")) viewjson.getInt("rotation").toFloat() else 0F + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((if (viewjson.has("textsize")) viewjson.getInt("textsize") else 22) * zoomFactor).toFloat()) + view.gravity = setGravity(if (viewjson.has("gravity")) viewjson.getString("gravity") else "center") + view.setTypeface( + setFont(if (viewjson.has("font")) viewjson.getString("font") else "sans-serif"), + Style.fromKey( viewjson.getString("fontStyle")).typeface + ) + if (viewjson.has("fontColor")) + view.setTextColor(getColor(viewjson.getString("fontColor"))) + } + + if (view is ImageView) { + view.clearColorFilter() + drawableDataMap[CustomWatchfaceDrawableDataKey.fromKey(tag.toString())]?.toDrawable(resources)?.also { + if (viewjson.has("color")) + it.colorFilter = changeDrawableColor(getColor(viewjson.getString("color"))) + else + it.clearColorFilter() + view.setImageDrawable(it) + } ?: apply { + view.setImageDrawable(CustomWatchfaceDrawableDataKey.fromKey(tag.toString()).icon?.let { context.getDrawable(it) }) + if (viewjson.has("color")) + view.setColorFilter(getColor(viewjson.getString("color"))) + else + view.clearColorFilter() + } + } + } + } + } + binding.second.visibility= ((binding.second.visibility==View.VISIBLE) && enableSecond).toVisibility() + binding.secondHand.visibility= ((binding.secondHand.visibility==View.VISIBLE) && enableSecond).toVisibility() + } + } + + private fun defaultWatchface(): EventData.ActionSetCustomWatchface { + val metadata = JSONObject() + .put(CustomWatchfaceMetadataKey.CWF_NAME.key, getString(info.nightscout.shared.R.string.wear_default_watchface)) + .put(CustomWatchfaceMetadataKey.CWF_AUTHOR.key, "Philoul") + .put(CustomWatchfaceMetadataKey.CWF_CREATED_AT.key, dateUtil.dateString(dateUtil.now())) + .put(CustomWatchfaceMetadataKey.CWF_VERSION.key, CUSTOM_VERSION) + val json = JSONObject() + .put("metadata", metadata) + .put("highColor", String.format("#%06X", 0xFFFFFF and highColor)) + .put("midColor", String.format("#%06X", 0xFFFFFF and midColor)) + .put("lowColor", String.format("#%06X", 0xFFFFFF and lowColor)) + .put("lowBatColor", String.format("#%06X", 0xFFFFFF and lowBatColor)) + .put("carbColor", String.format("#%06X", 0xFFFFFF and carbColor)) + .put("gridColor", String.format("#%06X", 0xFFFFFF and Color.WHITE)) + .put("pointSize",2) + .put("enableSecond", true) + + binding.mainLayout.forEach { view -> + val params = view.layoutParams as FrameLayout.LayoutParams + if (view is TextView) { + json.put( + view.tag.toString(), + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("rotation", view.rotation.toInt()) + .put("visibility", getVisibility(view.visibility)) + .put("textsize", view.textSize.toInt()) + .put("gravity", getGravity(view.gravity)) + .put("font", getFont(view.typeface)) + .put("fontStyle", Style.fromTypeface(view.typeface.style).key) + .put("fontColor", String.format("#%06X", 0xFFFFFF and view.currentTextColor)) + ) + } + if (view is ImageView) { + //view.backgroundTintList = + json.put( + view.tag.toString(), + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("visibility", getVisibility(view.visibility)) + ) + } + if (view is lecho.lib.hellocharts.view.LineChartView) { + json.put( + view.tag.toString(), + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("visibility", getVisibility(view.visibility)) + ) + } + } + val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf() + getResourceByteArray(info.nightscout.shared.R.drawable.watchface_custom)?.let { + val drawableDataMap = DrawableData(it,DrawableFormat.PNG) + drawableDatas[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE] = drawableDataMap + } + return EventData.ActionSetCustomWatchface(getString(info.nightscout.shared.R.string.wear_default_watchface),json.toString(4),drawableDatas) + } + + private fun setDefaultColors() { + highColor = Color.parseColor("#FFFF00") + midColor = Color.parseColor("#00FF00") + lowColor = Color.parseColor("#FF0000") + carbColor = ContextCompat.getColor(this, R.color.carbs) + lowBatColor = ContextCompat.getColor(this, R.color.dark_uploaderBatteryEmpty) + gridColor = Color.WHITE + } + + private fun setVisibility(visibility: String): Int = when (visibility) { + "visible" -> View.VISIBLE + "invisible" -> View.INVISIBLE + "gone" -> View.GONE + else -> View.GONE + } + + private fun getVisibility(visibility: Int): String = when (visibility) { + View.VISIBLE -> "visible" + View.INVISIBLE -> "invisible" + View.GONE -> "gone" + else -> "gone" + } + + private fun setGravity(gravity: String): Int = when (gravity) { + "center" -> Gravity.CENTER + "left" -> Gravity.LEFT + "right" -> Gravity.RIGHT + else -> Gravity.CENTER + } + + private fun getGravity(gravity: Int): String = when (gravity) { + Gravity.CENTER -> "center" + Gravity.LEFT -> "left" + Gravity.RIGHT -> "right" + else -> "center" + } + + private fun setFont(font: String): Typeface = when (font) { + "sans-serif" -> Typeface.SANS_SERIF + "default" -> Typeface.DEFAULT + "default-bold" -> Typeface.DEFAULT_BOLD + "monospace" -> Typeface.MONOSPACE + "serif" -> Typeface.SERIF + "roboto-condensed-bold" -> ResourcesCompat.getFont(context, R.font.roboto_condensed_bold)!! + "roboto-condensed-light" -> ResourcesCompat.getFont(context, R.font.roboto_condensed_light)!! + "roboto-condensed-regular" -> ResourcesCompat.getFont(context, R.font.roboto_condensed_regular)!! + "roboto-slab-light" -> ResourcesCompat.getFont(context, R.font.roboto_slab_light)!! + else -> Typeface.DEFAULT + } + + private fun getFont(font: Typeface): String = when (font) { + Typeface.SANS_SERIF -> "sans-serif" + Typeface.DEFAULT -> "default" + Typeface.DEFAULT_BOLD -> "default-bold" + Typeface.MONOSPACE -> "monospace" + Typeface.SERIF -> "serif" + ResourcesCompat.getFont(context, R.font.roboto_condensed_bold)!! -> "roboto-condensed-bold" + ResourcesCompat.getFont(context, R.font.roboto_condensed_light)!! -> "roboto-condensed-light" + ResourcesCompat.getFont(context, R.font.roboto_condensed_regular)!! -> "roboto-condensed-regular" + ResourcesCompat.getFont(context, R.font.roboto_slab_light)!! -> "roboto-slab-light" + else -> "default" + } + + enum class Style(val key: String, val typeface: Int) { + NORMAL("normal", Typeface.NORMAL), + BOLD("bold", Typeface.BOLD), + BOLD_ITALIC("bold-italic", Typeface.BOLD_ITALIC), + ITALIC("italic", Typeface.ITALIC); + companion object { + private val keyToEnumMap = HashMap() + private val typefaceToEnumMap = HashMap() + init { + for (value in values()) keyToEnumMap[value.key] = value + for (value in values()) typefaceToEnumMap[value.typeface] = value + } + fun fromKey(key: String?): Style = + if (keyToEnumMap.containsKey(key)) { + keyToEnumMap[key] ?:NORMAL + } else { + NORMAL + } + fun fromTypeface(typeface: Int?): Style = + if (typefaceToEnumMap.containsKey(typeface)) { + typefaceToEnumMap[typeface] ?:NORMAL + } else { + NORMAL + } + } + } + fun getResourceByteArray(resourceId: Int): ByteArray? { + val inputStream = resources.openRawResource(resourceId) + val byteArrayOutputStream = ByteArrayOutputStream() + + try { + val buffer = ByteArray(1024) + var count: Int + while (inputStream.read(buffer).also { count = it } != -1) { + byteArrayOutputStream.write(buffer, 0, count) + } + byteArrayOutputStream.close() + inputStream.close() + + return byteArrayOutputStream.toByteArray() + } catch (e: Exception) { + } + return null + } + + private fun changeDrawableColor(color: Int): ColorFilter { + val colorMatrix = ColorMatrix() + colorMatrix.setSaturation(0f) // 0 désature l'image, 1 la laisse inchangée. + + // Modifier la teinte de couleur (couleur de fond) + colorMatrix.postConcat( + ColorMatrix( + floatArrayOf( + Color.red(color) / 255f, 0f, 0f, 0f, 0f, + 0f, Color.green(color) / 255f, 0f, 0f, 0f, + 0f, 0f, Color.blue(color) / 255f, 0f, 0f, + 0f, 0f, 0f, Color.alpha(color) / 255f, 0f + ) + ) + ) + + // Appliquer la matrice de couleur au ColorFilter + return ColorMatrixColorFilter(colorMatrix) + + // Appliquer le ColorFilter au Drawable + //drawable.colorFilter = colorFilter + //return drawable + } + + private fun getColor(color: String): Int { + if (color == "bgColor") + return bgColor + else + return Color.parseColor(color) + } + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt index a1cc53eab9..4a7fd3a5cf 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt @@ -36,6 +36,7 @@ import info.nightscout.shared.extensions.toVisibilityKeepSpace import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.plusAssign import javax.inject.Inject import kotlin.math.floor @@ -78,12 +79,15 @@ abstract class BaseWatchFace : WatchFace() { var gridColor = Color.WHITE var basalBackgroundColor = Color.BLUE var basalCenterColor = Color.BLUE + var carbColor = Color.GREEN private var bolusColor = Color.MAGENTA private var lowResMode = false private var layoutSet = false var bIsRound = false var dividerMatchesBg = false var pointSize = 2 + var enableSecond = false + var updateSecond: Disposable? = null // Tapping times private var sgvTapTime: Long = 0 @@ -245,12 +249,13 @@ abstract class BaseWatchFace : WatchFace() { override fun onDestroy() { disposable.clear() + updateSecond?.dispose() simpleUi.onDestroy() super.onDestroy() } override fun getInteractiveModeUpdateRate(): Long { - return 60 * 1000L // Only call onTimeChanged every 60 seconds + return if (enableSecond) 1000L else 60 * 1000L // Only call onTimeChanged every 60 seconds } override fun onDraw(canvas: Canvas) { @@ -271,6 +276,8 @@ abstract class BaseWatchFace : WatchFace() { missedReadingAlert() checkVibrateHourly(oldTime, newTime) if (!simpleUi.isEnabled(currentWatchMode)) setDataFields() + } else if (layoutSet && !simpleUi.isEnabled(currentWatchMode) && enableSecond && newTime.hasSecondChanged(oldTime)) { + setSecond() } } @@ -349,9 +356,10 @@ abstract class BaseWatchFace : WatchFace() { } private fun setDateAndTime() { - binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + if (enableSecond) ":" + dateUtil.secondString() else "" binding.hour?.text = dateUtil.hourString() binding.minute?.text = dateUtil.minuteString() + binding.second?.text = dateUtil.secondString() binding.dateTime?.visibility = sp.getBoolean(R.string.key_show_date, false).toVisibility() binding.dayName?.text = dateUtil.dayNameString() binding.day?.text = dateUtil.dayString() @@ -360,7 +368,12 @@ abstract class BaseWatchFace : WatchFace() { binding.timePeriod?.text = dateUtil.amPm() } - private fun setColor() { + open fun setSecond() { + binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + if (enableSecond) ":" + dateUtil.secondString() else "" + binding.second?.text = dateUtil.secondString() + } + + fun setColor() { dividerMatchesBg = sp.getBoolean(R.string.key_match_divider, false) when { lowResMode -> setColorLowRes() @@ -382,6 +395,14 @@ abstract class BaseWatchFace : WatchFace() { if (simpleUi.isEnabled(currentWatchMode)) simpleUi.setAntiAlias(currentWatchMode) else setDataFields() invalidate() + /* + if (enableSecond) + if (updateSecond == null) + updateSecond = aapsSchedulers.io.schedulePeriodicallyDirect( + ::setSecond, 1000L, 1000L, TimeUnit.MILLISECONDS) + else + updateSecond?.dispose() + */ } private fun isLowRes(watchMode: WatchMode): Boolean { @@ -409,12 +430,12 @@ abstract class BaseWatchFace : WatchFace() { if (lowResMode) BgGraphBuilder( sp, dateUtil, graphData.entries, treatmentData.predictions, treatmentData.temps, treatmentData.basals, treatmentData.boluses, pointSize, - midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe + midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, carbColor, timeframe ) else BgGraphBuilder( sp, dateUtil, graphData.entries, treatmentData.predictions, treatmentData.temps, treatmentData.basals, treatmentData.boluses, - pointSize, highColor, lowColor, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe + pointSize, highColor, lowColor, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, carbColor, timeframe ) binding.chart?.lineChartData = bgGraphBuilder.lineData() binding.chart?.isViewportCalculationEnabled = true diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt index 6a2f78d954..61333ad1e1 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt @@ -6,6 +6,7 @@ import info.nightscout.androidaps.databinding.ActivityHome2Binding import info.nightscout.androidaps.databinding.ActivityHomeBinding import info.nightscout.androidaps.databinding.ActivityBigchartBinding import info.nightscout.androidaps.databinding.ActivityCockpitBinding +import info.nightscout.androidaps.databinding.ActivityCustomBinding import info.nightscout.androidaps.databinding.ActivityDigitalstyleBinding import info.nightscout.androidaps.databinding.ActivityNochartBinding import info.nightscout.androidaps.databinding.ActivitySteampunkBinding @@ -22,11 +23,12 @@ class WatchfaceViewAdapter( cp: ActivityCockpitBinding? = null, ds: ActivityDigitalstyleBinding? = null, nC: ActivityNochartBinding? = null, - sP: ActivitySteampunkBinding? = null + sP: ActivitySteampunkBinding? = null, + cU: ActivityCustomBinding? = null ) { init { - if (aL == null && a2 == null && aa == null && bC == null && cp == null && ds == null && nC == null && sP == null) { + if (aL == null && a2 == null && aa == null && bC == null && cp == null && ds == null && nC == null && sP == null && cU == null) { throw IllegalArgumentException("Require at least on Binding parameter") } } @@ -34,39 +36,40 @@ class WatchfaceViewAdapter( private val errorMessage = "Missing require View Binding parameter" // Required attributes val mainLayout = - aL?.mainLayout ?: a2?.mainLayout ?: aa?.mainLayout ?: bC?.mainLayout ?: bC?.mainLayout ?: cp?.mainLayout ?: ds?.mainLayout ?: nC?.mainLayout ?: sP?.mainLayout + aL?.mainLayout ?: a2?.mainLayout ?: aa?.mainLayout ?: bC?.mainLayout ?: bC?.mainLayout ?: cp?.mainLayout ?: ds?.mainLayout ?: nC?.mainLayout ?: sP?.mainLayout ?: cU?.mainLayout ?: throw IllegalArgumentException(errorMessage) val timestamp = - aL?.timestamp ?: a2?.timestamp ?: aa?.timestamp ?: bC?.timestamp ?: bC?.timestamp ?: cp?.timestamp ?: ds?.timestamp ?: nC?.timestamp ?: sP?.timestamp + aL?.timestamp ?: a2?.timestamp ?: aa?.timestamp ?: bC?.timestamp ?: bC?.timestamp ?: cp?.timestamp ?: ds?.timestamp ?: nC?.timestamp ?: sP?.timestamp ?: cU?.timestamp ?: throw IllegalArgumentException(errorMessage) val root = - aL?.root ?: a2?.root ?: aa?.root ?: bC?.root ?: bC?.root ?: cp?.root ?: ds?.root ?: nC?.root ?: sP?.root + aL?.root ?: a2?.root ?: aa?.root ?: bC?.root ?: bC?.root ?: cp?.root ?: ds?.root ?: nC?.root ?: sP?.root ?: cU?.root ?: throw IllegalArgumentException(errorMessage) // Optional attributes - val sgv = aL?.sgv ?: a2?.sgv ?: aa?.sgv ?: bC?.sgv ?: bC?.sgv ?: cp?.sgv ?: ds?.sgv ?: nC?.sgv - val direction = aL?.direction ?: a2?.direction ?: aa?.direction ?: cp?.direction ?: ds?.direction - val loop = a2?.loop ?: cp?.loop ?: sP?.loop - val delta = aL?.delta ?: a2?.delta ?: aa?.delta ?: bC?.delta ?: bC?.delta ?: cp?.delta ?: ds?.delta ?: nC?.delta - val avgDelta = a2?.avgDelta ?: bC?.avgDelta ?: bC?.avgDelta ?: cp?.avgDelta ?: ds?.avgDelta ?: nC?.avgDelta - val uploaderBattery = aL?.uploaderBattery ?: a2?.uploaderBattery ?: aa?.uploaderBattery ?: cp?.uploaderBattery ?: ds?.uploaderBattery ?: sP?.uploaderBattery - val rigBattery = a2?.rigBattery ?: cp?.rigBattery ?: ds?.rigBattery ?: sP?.rigBattery - val basalRate = a2?.basalRate ?: cp?.basalRate ?: ds?.basalRate ?: sP?.basalRate - val bgi = a2?.bgi ?: ds?.bgi - val AAPSv2 = a2?.AAPSv2 ?: cp?.AAPSv2 ?: ds?.AAPSv2 ?: sP?.AAPSv2 - val cob1 = a2?.cob1 ?: ds?.cob1 - val cob2 = a2?.cob2 ?: cp?.cob2 ?: ds?.cob2 ?: sP?.cob2 - val time = aL?.time ?: a2?.time ?: aa?.time ?: bC?.time ?: bC?.time ?: cp?.time ?: nC?.time - val minute = ds?.minute - val hour = ds?.hour - val day = a2?.day ?: ds?.day - val month = a2?.month ?: ds?.month - val iob1 = a2?.iob1 ?: ds?.iob1 - val iob2 = a2?.iob2 ?: cp?.iob2 ?: ds?.iob2 ?: sP?.iob2 - val chart = a2?.chart ?: aa?.chart ?: bC?.chart ?: bC?.chart ?: ds?.chart ?: sP?.chart + val sgv = aL?.sgv ?: a2?.sgv ?: aa?.sgv ?: bC?.sgv ?: bC?.sgv ?: cp?.sgv ?: ds?.sgv ?: nC?.sgv ?: cU?.sgv + val direction = aL?.direction ?: a2?.direction ?: aa?.direction ?: cp?.direction ?: ds?.direction ?: cU?.direction + val loop = a2?.loop ?: cp?.loop ?: sP?.loop ?: cU?.loop + val delta = aL?.delta ?: a2?.delta ?: aa?.delta ?: bC?.delta ?: bC?.delta ?: cp?.delta ?: ds?.delta ?: nC?.delta ?: cU?.delta + val avgDelta = a2?.avgDelta ?: bC?.avgDelta ?: bC?.avgDelta ?: cp?.avgDelta ?: ds?.avgDelta ?: nC?.avgDelta ?: cU?.avgDelta + val uploaderBattery = aL?.uploaderBattery ?: a2?.uploaderBattery ?: aa?.uploaderBattery ?: cp?.uploaderBattery ?: ds?.uploaderBattery ?: sP?.uploaderBattery ?: cU?.uploaderBattery + val rigBattery = a2?.rigBattery ?: cp?.rigBattery ?: ds?.rigBattery ?: sP?.rigBattery ?: cU?.rigBattery + val basalRate = a2?.basalRate ?: cp?.basalRate ?: ds?.basalRate ?: sP?.basalRate ?: cU?.basalRate + val bgi = a2?.bgi ?: ds?.bgi ?: cU?.bgi + val AAPSv2 = a2?.AAPSv2 ?: cp?.AAPSv2 ?: ds?.AAPSv2 ?: sP?.AAPSv2 ?: cU?.AAPSv2 + val cob1 = a2?.cob1 ?: ds?.cob1 ?: cU?.cob1 + val cob2 = a2?.cob2 ?: cp?.cob2 ?: ds?.cob2 ?: sP?.cob2 ?: cU?.cob2 + val time = aL?.time ?: a2?.time ?: aa?.time ?: bC?.time ?: bC?.time ?: cp?.time ?: nC?.time ?: cU?.time + val second = cU?.second + val minute = ds?.minute ?: cU?.minute + val hour = ds?.hour ?: cU?.hour + val day = a2?.day ?: ds?.day ?: cU?.day + val month = a2?.month ?: ds?.month ?: cU?.month + val iob1 = a2?.iob1 ?: ds?.iob1 ?: cU?.iob1 + val iob2 = a2?.iob2 ?: cp?.iob2 ?: ds?.iob2 ?: sP?.iob2 ?: cU?.iob2 + val chart = a2?.chart ?: aa?.chart ?: bC?.chart ?: bC?.chart ?: ds?.chart ?: sP?.chart ?: cU?.chart val status = aL?.status ?: aa?.status ?: bC?.status ?: bC?.status ?: nC?.status - val timePeriod = ds?.timePeriod ?: aL?.timePeriod ?: nC?.timePeriod ?: bC?.timePeriod - val dayName = ds?.dayName + val timePeriod = ds?.timePeriod ?: aL?.timePeriod ?: nC?.timePeriod ?: bC?.timePeriod ?: cU?.timePeriod + val dayName = ds?.dayName ?: cU?.dayName val mainMenuTap = ds?.mainMenuTap ?: sP?.mainMenuTap val chartZoomTap = ds?.chartZoomTap ?: sP?.chartZoomTap val dateTime = ds?.dateTime ?: a2?.dateTime @@ -91,6 +94,7 @@ class WatchfaceViewAdapter( is ActivityDigitalstyleBinding -> WatchfaceViewAdapter(null, null, null, null, null, bindLayout) is ActivityNochartBinding -> WatchfaceViewAdapter(null, null, null, null, null, null, bindLayout) is ActivitySteampunkBinding -> WatchfaceViewAdapter(null, null, null, null, null, null, null, bindLayout) + is ActivityCustomBinding -> WatchfaceViewAdapter(null, null, null, null, null, null, null, null, bindLayout) else -> throw IllegalArgumentException("ViewBinding is not implement in WatchfaceViewAdapter") } } diff --git a/wear/src/main/res/layout/activity_custom.xml b/wear/src/main/res/layout/activity_custom.xml new file mode 100644 index 0000000000..a1172a3c3f --- /dev/null +++ b/wear/src/main/res/layout/activity_custom.xml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index a9f53bd230..b29ad82733 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ AAPS(Cockpit) AAPS(Steampunk) AAPS(DigitalStyle) + AAPS(Custom) AAPS(Actions) AAPS(Temp Target) AAPS(Quick Wizard) @@ -143,6 +144,7 @@ Only show time and BG Vibrate hourly Show Week number + Show seconds Your style: no style minimal style @@ -208,6 +210,14 @@ complication_tap_action carbs_button_increment_1 carbs_button_increment_2 + enable_custom_setting + digital_watchface + digital_timing + analog_watchface + show_second + highcolor + midcolor + lowcolor increment decrement H diff --git a/wear/src/main/res/xml/watch_face_configuration_custom.xml b/wear/src/main/res/xml/watch_face_configuration_custom.xml new file mode 100644 index 0000000000..ff998be701 --- /dev/null +++ b/wear/src/main/res/xml/watch_face_configuration_custom.xml @@ -0,0 +1,27 @@ + + + + + + + + + + From 26dff28e1c6ac2bcb1236f9afd69dd489768d6f4 Mon Sep 17 00:00:00 2001 From: Philoul Date: Sun, 6 Aug 2023 09:58:52 +0200 Subject: [PATCH 02/18] Refactor Custom Watchface code (simplified) --- .../rx/events/EventWearUpdateGui.kt | 4 +- .../rx/weardata/CustomWatchfaceFormat.kt | 156 ++++++++++++++---- .../info/nightscout/rx/weardata/EventData.kt | 10 +- .../shared/src/main/res/values/strings.xml | 2 +- .../maintenance/CustomWatchfaceFile.kt | 21 --- .../maintenance/ImportExportPrefs.kt | 4 +- .../maintenance/PrefFileListProvider.kt | 3 +- .../configuration/di/ConfigurationModule.kt | 2 - .../maintenance/ImportExportPrefsImpl.kt | 14 +- .../maintenance/PrefFileListProviderImpl.kt | 17 +- .../CustomWatchfaceImportListActivity.kt | 17 +- .../formats/ZipCustomWatchfaceFormat.kt | 128 -------------- .../custom_watchface_import_list_item.xml | 17 -- .../src/main/res/values/strings.xml | 2 - .../plugins/general/wear/WearFragment.kt | 37 +---- .../plugins/general/wear/WearPlugin.kt | 3 +- .../wear/wearintegration/DataHandlerMobile.kt | 11 +- .../DataLayerListenerServiceMobile.kt | 5 +- .../androidaps/comm/DataHandlerWear.kt | 6 + .../comm/DataLayerListenerServiceWear.kt | 5 +- .../interaction/utils/Persistence.kt | 2 +- .../androidaps/watchfaces/CustomWatchface.kt | 34 ++-- 22 files changed, 204 insertions(+), 296 deletions(-) delete mode 100644 core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt delete mode 100644 plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt index 7aae63204a..ba643694a3 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt @@ -1,3 +1,5 @@ package info.nightscout.rx.events -class EventWearUpdateGui : Event() \ No newline at end of file +import info.nightscout.rx.weardata.CustomWatchfaceData + +class EventWearUpdateGui(val customWatchfaceData: CustomWatchfaceData? = null, val exportFile: Boolean = false) : Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt index cef6aa181a..3cc98232ac 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt @@ -1,27 +1,30 @@ package info.nightscout.rx.weardata -import android.content.Context import android.content.res.Resources import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import android.os.Parcelable -import android.util.Xml import androidx.annotation.DrawableRes import androidx.annotation.StringRes import info.nightscout.shared.R import kotlinx.serialization.Serializable import org.json.JSONObject +import java.io.BufferedOutputStream +import java.io.ByteArrayOutputStream import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon: Int?, val fileName: String) { - UNKNOWN("unknown", null,"Unknown"), + UNKNOWN("unknown", null, "Unknown"), CUSTOM_WATCHFACE("customWatchface", R.drawable.watchface_custom, "CustomWatchface"), BACKGROUND("background", R.drawable.background, "Background"), - COVERCHART("cover_chart", null,"CoverChart"), + COVERCHART("cover_chart", null, "CoverChart"), COVERPLATE("cover_plate", R.drawable.simplified_dial, "CoverPlate"), - HOURHAND("hour_hand", R.drawable.hour_hand,"HourHand"), - MINUTEHAND("minute_hand", R.drawable.minute_hand,"MinuteHand"), + HOURHAND("hour_hand", R.drawable.hour_hand, "HourHand"), + MINUTEHAND("minute_hand", R.drawable.minute_hand, "MinuteHand"), SECONDHAND("second_hand", R.drawable.second_hand, "SecondHand"); companion object { @@ -36,14 +39,14 @@ enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon fun fromKey(key: String): CustomWatchfaceDrawableDataKey = if (keyToEnumMap.containsKey(key)) { - keyToEnumMap[key] ?:UNKNOWN + keyToEnumMap[key] ?: UNKNOWN } else { UNKNOWN } fun fromFileName(file: String): CustomWatchfaceDrawableDataKey = if (fileNameToEnumMap.containsKey(file.substringBeforeLast("."))) { - fileNameToEnumMap[file.substringBeforeLast(".")] ?:UNKNOWN + fileNameToEnumMap[file.substringBeforeLast(".")] ?: UNKNOWN } else { UNKNOWN } @@ -53,8 +56,9 @@ enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon enum class DrawableFormat(val extension: String) { UNKNOWN(""), + //XML("xml"), - //svg("svg"), + //SVG("svg"), JPG("jpg"), PNG("png"); @@ -68,7 +72,7 @@ enum class DrawableFormat(val extension: String) { fun fromFileName(fileName: String): DrawableFormat = if (extensionToEnumMap.containsKey(fileName.substringAfterLast("."))) { - extensionToEnumMap[fileName.substringAfterLast(".")] ?:UNKNOWN + extensionToEnumMap[fileName.substringAfterLast(".")] ?: UNKNOWN } else { UNKNOWN } @@ -85,15 +89,20 @@ data class DrawableData(val value: ByteArray, val format: DrawableFormat) { val bitmap = BitmapFactory.decodeByteArray(value, 0, value.size) BitmapDrawable(resources, bitmap) } -/* - DrawableFormat.XML -> { - val xmlInputStream = ByteArrayInputStream(value) - val xmlPullParser = Xml.newPullParser() - xmlPullParser.setInput(xmlInputStream, null) - Drawable.createFromXml(resources, xmlPullParser) - } -*/ - else -> null + /* + DrawableFormat.SVG -> { + //TODO: include svg to Drawable convertor here + null + } + DrawableFormat.XML -> { + // Always return a null Drawable, even if xml file is a valid xml vector file + val xmlInputStream = ByteArrayInputStream(value) + val xmlPullParser = Xml.newPullParser() + xmlPullParser.setInput(xmlInputStream, null) + Drawable.createFromXml(resources, xmlPullParser) + } + */ + else -> null } } catch (e: Exception) { return null @@ -104,18 +113,13 @@ data class DrawableData(val value: ByteArray, val format: DrawableFormat) { typealias CustomWatchfaceDrawableDataMap = MutableMap typealias CustomWatchfaceMetadataMap = MutableMap -data class CustomWatchface(val json: String, var metadata: CustomWatchfaceMetadataMap, val drawableDatas: CustomWatchfaceDrawableDataMap) - -interface CustomWatchfaceFormat { - - fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface) - fun loadCustomWatchface(cwfFile: File): CustomWatchface? - fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap -} +@Serializable +data class CustomWatchfaceData(val json: String, var metadata: CustomWatchfaceMetadataMap, val drawableDatas: CustomWatchfaceDrawableDataMap) enum class CustomWatchfaceMetadataKey(val key: String, @StringRes val label: Int) { CWF_NAME("name", R.string.metadata_label_watchface_name), + CWF_FILENAME("filename", R.string.metadata_wear_import_filename), CWF_AUTHOR("author", R.string.metadata_label_watchface_author), CWF_CREATED_AT("created_at", R.string.metadata_label_watchface_created_at), CWF_VERSION("cwf_version", R.string.metadata_label_watchface_version); @@ -137,4 +141,100 @@ enum class CustomWatchfaceMetadataKey(val key: String, @StringRes val label: Int } +} + +class ZipWatchfaceFormat { + companion object { + + const val CUSTOM_WF_EXTENTION = ".zip" + const val CUSTOM_JSON_FILE = "CustomWatchface.json" + + fun loadCustomWatchface(cwfFile: File): CustomWatchfaceData? { + var json = JSONObject() + var metadata: CustomWatchfaceMetadataMap = mutableMapOf() + val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf() + + try { + val zipInputStream = ZipInputStream(cwfFile.inputStream()) + var zipEntry: ZipEntry? = zipInputStream.nextEntry + while (zipEntry != null) { + val entryName = zipEntry.name + + val buffer = ByteArray(2048) + val byteArrayOutputStream = ByteArrayOutputStream() + var count = zipInputStream.read(buffer) + while (count != -1) { + byteArrayOutputStream.write(buffer, 0, count) + count = zipInputStream.read(buffer) + } + zipInputStream.closeEntry() + + if (entryName == CUSTOM_JSON_FILE) { + val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8) + json = JSONObject(jsonString) + metadata = loadMetadata(json) + metadata[CustomWatchfaceMetadataKey.CWF_FILENAME] = cwfFile.name + } else { + val customWatchfaceDrawableDataKey = CustomWatchfaceDrawableDataKey.fromFileName(entryName) + val drawableFormat = DrawableFormat.fromFileName(entryName) + if (customWatchfaceDrawableDataKey != CustomWatchfaceDrawableDataKey.UNKNOWN && drawableFormat != DrawableFormat.UNKNOWN) { + drawableDatas[customWatchfaceDrawableDataKey] = DrawableData(byteArrayOutputStream.toByteArray(), drawableFormat) + } + } + zipEntry = zipInputStream.nextEntry + } + + // Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image + if (metadata.containsKey(CustomWatchfaceMetadataKey.CWF_NAME) && drawableDatas.containsKey(CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE)) + return CustomWatchfaceData(json.toString(4), metadata, drawableDatas) + else + return null + + } catch (e: Exception) { + return null + } + } + + fun saveCustomWatchface(file: File, customWatchface: CustomWatchfaceData) { + + try { + val outputStream = FileOutputStream(file) + val zipOutputStream = ZipOutputStream(BufferedOutputStream(outputStream)) + + // Ajouter le fichier JSON au ZIP + val jsonEntry = ZipEntry(CUSTOM_JSON_FILE) + zipOutputStream.putNextEntry(jsonEntry) + zipOutputStream.write(customWatchface.json.toByteArray()) + zipOutputStream.closeEntry() + + // Ajouter les fichiers divers au ZIP + for (drawableData in customWatchface.drawableDatas) { + val fileEntry = ZipEntry("${drawableData.key.fileName}.${drawableData.value.format.extension}") + zipOutputStream.putNextEntry(fileEntry) + zipOutputStream.write(drawableData.value.value) + zipOutputStream.closeEntry() + } + zipOutputStream.close() + outputStream.close() + } catch (_: Exception) { + } + + } + + fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap { + val metadata: CustomWatchfaceMetadataMap = mutableMapOf() + + if (contents.has("metadata")) { + val meta = contents.getJSONObject("metadata") + for (key in meta.keys()) { + val metaKey = CustomWatchfaceMetadataKey.fromKey(key) + if (metaKey != null) { + metadata[metaKey] = meta.getString(key) + } + } + } + return metadata + } + } + } \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt index 88bf4fa1bc..4bfb867ef9 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt @@ -14,15 +14,15 @@ sealed class EventData : Event() { fun serialize() = Json.encodeToString(serializer(), this) + @ExperimentalSerializationApi fun serializeByte() = ProtoBuf.encodeToByteArray(serializer(), this) companion object { - fun deserialize(json: String) = try { Json.decodeFromString(serializer(), json) } catch (ignored: Exception) { Error(System.currentTimeMillis()) } - + @ExperimentalSerializationApi fun deserializeByte(byteArray: ByteArray) = try { ProtoBuf.decodeFromByteArray(serializer(), byteArray) } catch (ignored: Exception) { @@ -153,7 +153,7 @@ sealed class EventData : Event() { @Serializable data class ActionGetCustomWatchface( val customWatchface: ActionSetCustomWatchface, - val exportFile: Boolean + val exportFile: Boolean = false ) : EventData() @Serializable @@ -283,9 +283,7 @@ sealed class EventData : Event() { } @Serializable data class ActionSetCustomWatchface( - val name: String, - val json: String, - val drawableDataMap: CustomWatchfaceDrawableDataMap + val customWatchfaceData: CustomWatchfaceData ) : EventData() @Serializable diff --git a/app-wear-shared/shared/src/main/res/values/strings.xml b/app-wear-shared/shared/src/main/res/values/strings.xml index df0c6b7bfd..b153a87526 100644 --- a/app-wear-shared/shared/src/main/res/values/strings.xml +++ b/app-wear-shared/shared/src/main/res/values/strings.xml @@ -40,10 +40,10 @@ Waiting for disconnection - key_custom_watchface Created at: %1$s Author: %1$s Name: %1$s + File name: %1$s Watchface version: %1$s Default Watchface diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt deleted file mode 100644 index 5019ae87e4..0000000000 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt +++ /dev/null @@ -1,21 +0,0 @@ -package info.nightscout.interfaces.maintenance - -import android.os.Parcelable -import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey -import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap -import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap -import info.nightscout.rx.weardata.DrawableData -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue -import java.io.File - -data class CustomWatchfaceFile( - val name: String, - val file: File, - val baseDir: File, - val json: String, - - val metadata: @RawValue CustomWatchfaceMetadataMap, - val drawableFiles: @RawValue CustomWatchfaceDrawableDataMap - -) \ No newline at end of file diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt index 99657fe7c3..e9193347a7 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt @@ -2,7 +2,7 @@ package info.nightscout.interfaces.maintenance import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import info.nightscout.rx.weardata.EventData +import info.nightscout.rx.weardata.CustomWatchfaceData interface ImportExportPrefs { @@ -11,7 +11,7 @@ interface ImportExportPrefs { fun importSharedPreferences(fragment: Fragment) fun importCustomWatchface(activity: FragmentActivity) fun importCustomWatchface(fragment: Fragment) - fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface) + fun exportCustomWatchface(customWatchface: CustomWatchfaceData) fun prefsFileExists(): Boolean fun verifyStoragePermissions(fragment: Fragment, onGranted: Runnable) fun exportSharedPreferences(f: Fragment) diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt index 9d131e9fdb..c97465e3fd 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt @@ -1,5 +1,6 @@ package info.nightscout.interfaces.maintenance +import info.nightscout.rx.weardata.CustomWatchfaceData import java.io.File interface PrefFileListProvider { @@ -12,7 +13,7 @@ interface PrefFileListProvider { fun newExportCsvFile(): File fun newCwfFile(filename: String): File fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList - fun listCustomWatchfaceFiles(): MutableList + fun listCustomWatchfaceFiles(): MutableList fun checkMetadata(metadata: Map): Map fun formatExportedAgo(utcTime: String): String } \ No newline at end of file diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/di/ConfigurationModule.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/di/ConfigurationModule.kt index 48b06b4560..bc85399a39 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/di/ConfigurationModule.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/di/ConfigurationModule.kt @@ -14,7 +14,6 @@ import info.nightscout.configuration.maintenance.PrefFileListProviderImpl import info.nightscout.configuration.maintenance.activities.CustomWatchfaceImportListActivity import info.nightscout.configuration.maintenance.activities.LogSettingActivity import info.nightscout.configuration.maintenance.activities.PrefImportListActivity -import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat import info.nightscout.interfaces.AndroidPermission import info.nightscout.interfaces.ConfigBuilder @@ -37,7 +36,6 @@ abstract class ConfigurationModule { @ContributesAndroidInjector abstract fun contributesCsvExportWorker(): ImportExportPrefsImpl.CsvExportWorker @ContributesAndroidInjector abstract fun contributesPrefImportListActivity(): PrefImportListActivity @ContributesAndroidInjector abstract fun contributesCustomWatchfaceImportListActivity(): CustomWatchfaceImportListActivity - @ContributesAndroidInjector abstract fun contributesZipCustomWatchfaceFormat(): ZipCustomWatchfaceFormat @ContributesAndroidInjector abstract fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat @ContributesAndroidInjector abstract fun prefImportListProviderInjector(): PrefFileListProvider diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt index 567e61fca7..a752a55eb1 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt @@ -22,7 +22,6 @@ import dagger.android.HasAndroidInjector import info.nightscout.configuration.R import info.nightscout.configuration.activities.DaggerAppCompatActivityWithResult import info.nightscout.configuration.maintenance.dialogs.PrefImportSummaryDialog -import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat import info.nightscout.core.ui.dialogs.OKDialog import info.nightscout.core.ui.dialogs.TwoMessagesAlertDialog @@ -56,7 +55,9 @@ import info.nightscout.rx.events.EventAppExit import info.nightscout.rx.events.EventDiaconnG8PumpLogReset import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag -import info.nightscout.rx.weardata.EventData +import info.nightscout.rx.weardata.CustomWatchfaceData +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey +import info.nightscout.rx.weardata.ZipWatchfaceFormat import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil @@ -86,8 +87,7 @@ class ImportExportPrefsImpl @Inject constructor( private val prefFileList: PrefFileListProvider, private val uel: UserEntryLogger, private val dateUtil: DateUtil, - private val uiInteraction: UiInteraction, - private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat + private val uiInteraction: UiInteraction ) : ImportExportPrefs { override fun prefsFileExists(): Boolean { @@ -315,10 +315,10 @@ class ImportExportPrefsImpl @Inject constructor( } } - override fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface) { + override fun exportCustomWatchface(customWatchface: CustomWatchfaceData) { prefFileList.ensureExportDirExists() - val newFile = prefFileList.newCwfFile(customWatchface.name) - customWatchfaceCWFFormat.saveCustomWatchface(newFile,customWatchface) + val newFile = prefFileList.newCwfFile(customWatchface.metadata[CustomWatchfaceMetadataKey.CWF_FILENAME] ?:"") + ZipWatchfaceFormat.saveCustomWatchface(newFile, customWatchface) } override fun importSharedPreferences(activity: FragmentActivity, importFile: PrefsFile) { diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt index b8d66e3bd5..8c546c0bd1 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt @@ -6,10 +6,8 @@ import dagger.Lazy import dagger.Reusable import info.nightscout.androidaps.annotations.OpenForTesting import info.nightscout.configuration.R -import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat import info.nightscout.interfaces.Config -import info.nightscout.interfaces.maintenance.CustomWatchfaceFile import info.nightscout.interfaces.maintenance.PrefFileListProvider import info.nightscout.interfaces.maintenance.PrefMetadata import info.nightscout.interfaces.maintenance.PrefMetadataMap @@ -19,6 +17,8 @@ import info.nightscout.interfaces.maintenance.PrefsMetadataKey import info.nightscout.interfaces.maintenance.PrefsStatus import info.nightscout.interfaces.storage.Storage import info.nightscout.interfaces.versionChecker.VersionCheckerUtils +import info.nightscout.rx.weardata.CustomWatchfaceData +import info.nightscout.rx.weardata.ZipWatchfaceFormat import info.nightscout.shared.interfaces.ResourceHelper import org.joda.time.DateTime import org.joda.time.Days @@ -36,7 +36,6 @@ class PrefFileListProviderImpl @Inject constructor( private val rh: ResourceHelper, private val config: Lazy, private val encryptedPrefsFormat: EncryptedPrefsFormat, - private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat, private val storage: Storage, private val versionCheckerUtils: VersionCheckerUtils, context: Context @@ -91,14 +90,14 @@ class PrefFileListProviderImpl @Inject constructor( return prefFiles } - override fun listCustomWatchfaceFiles(): MutableList { - val customWatchfaceFiles = mutableListOf() + override fun listCustomWatchfaceFiles(): MutableList { + val customWatchfaceFiles = mutableListOf() // searching dedicated dir, only for new CWF format - exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION) }.forEach { file -> + exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipWatchfaceFormat.CUSTOM_WF_EXTENTION) }.forEach { file -> // Here loadCustomWatchface will unzip, check and load CustomWatchface - customWatchfaceCWFFormat.loadCustomWatchface(file)?.also { customWatchface -> - customWatchfaceFiles.add(CustomWatchfaceFile(file.name, file, exportsPath, customWatchface.json, customWatchface.metadata, customWatchface.drawableDatas)) + ZipWatchfaceFormat.loadCustomWatchface(file)?.also { customWatchface -> + customWatchfaceFiles.add(customWatchface) } } @@ -147,7 +146,7 @@ class PrefFileListProviderImpl @Inject constructor( } override fun newCwfFile(filename: String): File { val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss")) - return File(exportsPath, "${filename}_$timeLocal${ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION}") + return File(exportsPath, "${filename}_$timeLocal${ZipWatchfaceFormat.CUSTOM_WF_EXTENTION}") } // check metadata for known issues, change their status and add info with explanations diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt index d225ae790d..d0d07de34b 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt @@ -10,7 +10,6 @@ import androidx.fragment.app.FragmentActivity import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import info.nightscout.core.ui.activities.TranslatedDaggerAppCompatActivity -import info.nightscout.interfaces.maintenance.CustomWatchfaceFile import info.nightscout.interfaces.maintenance.PrefFileListProvider import info.nightscout.configuration.databinding.CustomWatchfaceImportListActivityBinding import info.nightscout.configuration.R @@ -18,6 +17,7 @@ import info.nightscout.configuration.databinding.CustomWatchfaceImportListItemBi import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventMobileDataToWear import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CustomWatchfaceData import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey.* import info.nightscout.rx.weardata.EventData @@ -51,7 +51,7 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles()) } - inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { + inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { inner class PrefFileViewHolder(val customWatchfaceImportListItemBinding: CustomWatchfaceImportListItemBinding) : RecyclerView.ViewHolder(customWatchfaceImportListItemBinding.root) { @@ -59,14 +59,12 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { with(customWatchfaceImportListItemBinding) { root.isClickable = true customWatchfaceImportListItemBinding.root.setOnClickListener { - val customWatchfaceFile = filelistName.tag as CustomWatchfaceFile - val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile.metadata[CWF_NAME] ?:"", customWatchfaceFile.json, customWatchfaceFile.drawableFiles) - sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWF.serialize()) + val customWatchfaceFile = filelistName.tag as CustomWatchfaceData + val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile) val i = Intent() setResult(FragmentActivity.RESULT_OK, i) + //rxBus.send(EventWearUpdateGui(customWatchfaceFile)) rxBus.send(EventMobileDataToWear(customWF)) - aapsLogger.debug("XXXXX EventMobileDataToWear sent") - finish() } } @@ -85,13 +83,12 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { override fun onBindViewHolder(holder: PrefFileViewHolder, position: Int) { val customWatchfaceFile = customWatchfaceFileList[position] val metadata = customWatchfaceFile.metadata - val drawable = customWatchfaceFile.drawableFiles[CustomWatchfaceDrawableDataKey + val drawable = customWatchfaceFile.drawableDatas[CustomWatchfaceDrawableDataKey .CUSTOM_WATCHFACE]?.toDrawable(resources) with(holder.customWatchfaceImportListItemBinding) { - filelistName.text = rh.gs(R.string.wear_import_filename, customWatchfaceFile.file.name) + filelistName.text = rh.gs(info.nightscout.shared.R.string.metadata_wear_import_filename, metadata[CWF_FILENAME]) filelistName.tag = customWatchfaceFile customWatchface.setImageDrawable(drawable) - filelistDir.text = rh.gs(R.string.wear_import_directory, customWatchfaceFile.file.parentFile?.absolutePath) customName.text = rh.gs(CWF_NAME.label, metadata[CWF_NAME]) author.text = rh.gs(CWF_AUTHOR.label, metadata[CWF_AUTHOR] ?:"") createdAt.text = rh.gs(CWF_CREATED_AT.label, metadata[CWF_CREATED_AT] ?:"") diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt deleted file mode 100644 index 6414187d1a..0000000000 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt +++ /dev/null @@ -1,128 +0,0 @@ -package info.nightscout.configuration.maintenance.formats - -import info.nightscout.core.utils.CryptoUtil -import info.nightscout.interfaces.storage.Storage -import info.nightscout.rx.weardata.CustomWatchface -import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey -import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap -import info.nightscout.rx.weardata.CustomWatchfaceFormat -import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey -import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap -import info.nightscout.rx.weardata.DrawableData -import info.nightscout.rx.weardata.DrawableFormat -import info.nightscout.rx.weardata.EventData -import info.nightscout.shared.interfaces.ResourceHelper -import org.json.JSONObject -import java.io.BufferedOutputStream -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.FileOutputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream -import java.util.zip.ZipOutputStream -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ZipCustomWatchfaceFormat @Inject constructor( - private var rh: ResourceHelper, - private var cryptoUtil: CryptoUtil, - private var storage: Storage -) : CustomWatchfaceFormat { - - companion object { - - const val CUSTOM_WF_EXTENTION = ".zip" - const val CUSTOM_JSON_FILE = "CustomWatchface.json" - } - - override fun loadCustomWatchface(cwfFile: File): CustomWatchface? { - var json = JSONObject() - var metadata: CustomWatchfaceMetadataMap = mutableMapOf() - val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf() - - try { - val zipInputStream = ZipInputStream(cwfFile.inputStream()) - var zipEntry: ZipEntry? = zipInputStream.nextEntry - while (zipEntry != null) { - val entryName = zipEntry.name - - val buffer = ByteArray(2048) - val byteArrayOutputStream = ByteArrayOutputStream() - var count = zipInputStream.read(buffer) - while (count != -1) { - byteArrayOutputStream.write(buffer, 0, count) - count = zipInputStream.read(buffer) - } - zipInputStream.closeEntry() - - if (entryName == CUSTOM_JSON_FILE) { - val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8) - json = JSONObject(jsonString) - metadata = loadMetadata(json) - } else { - val customWatchfaceDrawableDataKey = CustomWatchfaceDrawableDataKey.fromFileName(entryName) - val drawableFormat = DrawableFormat.fromFileName(entryName) - if (customWatchfaceDrawableDataKey != CustomWatchfaceDrawableDataKey.UNKNOWN && drawableFormat != DrawableFormat.UNKNOWN) { - drawableDatas[customWatchfaceDrawableDataKey] = DrawableData(byteArrayOutputStream.toByteArray(),drawableFormat) - } - } - zipEntry = zipInputStream.nextEntry - } - - // Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image - if (metadata.containsKey(CustomWatchfaceMetadataKey.CWF_NAME) && drawableDatas.containsKey(CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE)) - return CustomWatchface(json.toString(4), metadata, drawableDatas) - else - return null - - } catch (e: Exception) { - return null - } - } - - - override fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface) { - - try { - val outputStream = FileOutputStream(file) - val zipOutputStream = ZipOutputStream(BufferedOutputStream(outputStream)) - - // Ajouter le fichier JSON au ZIP - val jsonEntry = ZipEntry(CUSTOM_JSON_FILE) - zipOutputStream.putNextEntry(jsonEntry) - zipOutputStream.write(customWatchface.json.toByteArray()) - zipOutputStream.closeEntry() - - // Ajouter les fichiers divers au ZIP - for (drawableData in customWatchface.drawableDataMap) { - val fileEntry = ZipEntry("${drawableData.key.fileName}.${drawableData.value.format.extension}") - zipOutputStream.putNextEntry(fileEntry) - zipOutputStream.write(drawableData.value.value) - zipOutputStream.closeEntry() - } - zipOutputStream.close() - outputStream.close() - } catch (e: Exception) { - - } - - } - - - override fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap { - val metadata: CustomWatchfaceMetadataMap = mutableMapOf() - - if (contents.has("metadata")) { - val meta = contents.getJSONObject("metadata") - for (key in meta.keys()) { - val metaKey = CustomWatchfaceMetadataKey.fromKey(key) - if (metaKey != null) { - metadata[metaKey] = meta.getString(key) - } - } - } - return metadata - } - -} \ No newline at end of file diff --git a/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml index dfd624b429..be5a37623d 100644 --- a/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml +++ b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml @@ -59,23 +59,6 @@ android:textColor="?attr/importListFileNameColor" tools:ignore="HardcodedText" /> - - Select Custom Watchface - Directory: %1$s - File name: %1$s Please reboot your phone or restart AAPS from the System Settings \notherwise Android APS will not have logging (important to track and verify that the algorithms are working correctly)! diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt index 848facfbec..2a11088864 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt @@ -17,6 +17,7 @@ import info.nightscout.rx.events.EventMobileToWear import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey import info.nightscout.rx.weardata.EventData import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.sharedPreferences.SP @@ -61,13 +62,11 @@ class WearFragment : DaggerFragment() { } } binding.defaultCustom.setOnClickListener { - sp.remove(info.nightscout.shared.R.string.key_custom_watchface) - wearPlugin.savedCustomWatchface = null rxBus.send(EventMobileToWear(EventData.ActionrequestSetDefaultWatchface(dateUtil.now()))) updateGui() } binding.sendCustom.setOnClickListener { - wearPlugin.savedCustomWatchface?.let { cwf -> rxBus.send(EventMobileDataToWear(cwf)) } + wearPlugin.savedCustomWatchface?.let { cwf -> rxBus.send(EventMobileDataToWear(EventData.ActionSetCustomWatchface(cwf))) } } binding.exportCustom.setOnClickListener { wearPlugin.savedCustomWatchface?.let { importExportPrefs.exportCustomWatchface(it) } @@ -80,16 +79,11 @@ class WearFragment : DaggerFragment() { disposable += rxBus .toObservable(EventWearUpdateGui::class.java) .observeOn(aapsSchedulers.main) - .subscribe({ updateGui() }, fabricPrivacy::logException) - disposable += rxBus - .toObservable(EventMobileDataToWear::class.java) - .observeOn(aapsSchedulers.main) .subscribe({ - loadCustom(it.payload) - wearPlugin.customWatchfaceSerialized = "" - wearPlugin.savedCustomWatchface = null + it.customWatchfaceData?.let { wearPlugin.savedCustomWatchface = it } + if (it.exportFile) + ToastUtils.okToast(context,rh.gs(R.string.wear_new_custom_watchface_received)) updateGui() - ToastUtils.okToast(context,rh.gs(R.string.wear_new_custom_watchface_received)) }, fabricPrivacy::logException) if (wearPlugin.savedCustomWatchface == null) rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(false))) @@ -110,24 +104,10 @@ class WearFragment : DaggerFragment() { private fun updateGui() { _binding ?: return - sp.getString(info.nightscout.shared.R.string.key_custom_watchface, "").let { - if (it != wearPlugin.customWatchfaceSerialized && it != "") { - aapsLogger.debug("XXXXX Serialisation: ${it.length}") - try { - wearPlugin.savedCustomWatchface = (EventData.deserialize(it) as EventData.ActionSetCustomWatchface) - wearPlugin.customWatchfaceSerialized = it - } - catch(e: Exception) { - wearPlugin.customWatchfaceSerialized = "" - wearPlugin.savedCustomWatchface = null - } - } - sp.remove(info.nightscout.shared.R.string.key_custom_watchface) - } wearPlugin.savedCustomWatchface?.let { - binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.name) + binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.metadata[CustomWatchfaceMetadataKey.CWF_NAME]) binding.sendCustom.visibility = View.VISIBLE - binding.coverChart.setImageDrawable(it.drawableDataMap[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE]?.toDrawable(resources)) + binding.coverChart.setImageDrawable(it.drawableDatas[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE]?.toDrawable(resources)) } ?:apply { binding.customName.text = rh.gs(R.string.wear_custom_watchface, rh.gs(info.nightscout.shared.R.string.wear_default_watchface)) binding.sendCustom.visibility = View.INVISIBLE @@ -137,7 +117,6 @@ class WearFragment : DaggerFragment() { } private fun loadCustom(cwf: EventData.ActionSetCustomWatchface) { - aapsLogger.debug("XXXXX EventWearCwfExported received") - wearPlugin.savedCustomWatchface = cwf + wearPlugin.savedCustomWatchface = cwf.customWatchfaceData } } \ No newline at end of file diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt index 21516d6d1e..156f791da6 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt @@ -18,6 +18,7 @@ import info.nightscout.rx.events.EventMobileToWear import info.nightscout.rx.events.EventOverviewBolusProgress import info.nightscout.rx.events.EventPreferenceChange import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CustomWatchfaceData import info.nightscout.rx.weardata.EventData import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.sharedPreferences.SP @@ -55,7 +56,7 @@ class WearPlugin @Inject constructor( var connectedDevice = "---" var customWatchfaceSerialized = "" - var savedCustomWatchface: EventData.ActionSetCustomWatchface? = null + var savedCustomWatchface: CustomWatchfaceData? = null override fun onStart() { super.onStart() diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt index a1fe993f56..465a76f3d6 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt @@ -60,7 +60,6 @@ import info.nightscout.plugins.R import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventMobileToWear -import info.nightscout.rx.events.EventWearCwfExported import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -1262,14 +1261,10 @@ class DataHandlerMobile @Inject constructor( private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) { val customWatchface = command.customWatchface - aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.json}") - //Update Wear Fragment - sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWatchface.serialize()) - //rxBus.send(EventWearCwfExported(customWatchface)) - rxBus.send(EventWearUpdateGui()) + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.customWatchfaceData.json}") + rxBus.send(EventWearUpdateGui(customWatchface.customWatchfaceData, command.exportFile)) if (command.exportFile) - importExportPrefs.exportCustomWatchface(customWatchface) - //Implement here a record within SP and a save within exports subfolder as zipFile + importExportPrefs.exportCustomWatchface(customWatchface.customWatchfaceData) } diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt index dac0274681..0593e79845 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -45,6 +45,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await +import kotlinx.serialization.ExperimentalSerializationApi import javax.inject.Inject class DataLayerListenerServiceMobile : WearableListenerService() { @@ -82,7 +83,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge) private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge) - + @ExperimentalSerializationApi override fun onCreate() { AndroidInjection.inject(this) super.onCreate() @@ -131,7 +132,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { } super.onDataChanged(dataEvents) } - + @ExperimentalSerializationApi override fun onMessageReceived(messageEvent: MessageEvent) { super.onMessageReceived(messageEvent) diff --git a/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt b/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt index 5d5e197670..fb16c6a5e6 100644 --- a/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt +++ b/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt @@ -186,6 +186,9 @@ class DataHandlerWear @Inject constructor( .subscribe { aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}") persistence.store(it) + persistence.readCustomWatchface()?.let { + rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false))) + } } disposable += rxBus .toObservable(EventData.ActionrequestSetDefaultWatchface::class.java) @@ -193,6 +196,9 @@ class DataHandlerWear @Inject constructor( .subscribe { aapsLogger.debug(LTag.WEAR, "Set Default Watchface received from ${it.sourceNodeId}") persistence.setDefaultWatchface() + persistence.readCustomWatchface()?.let { + rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false))) + } } disposable += rxBus .toObservable(EventData.ActionrequestCustomWatchface::class.java) diff --git a/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt b/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt index e4c62dd83d..7a1c894b49 100644 --- a/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt @@ -22,6 +22,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign import kotlinx.coroutines.* import kotlinx.coroutines.tasks.await +import kotlinx.serialization.ExperimentalSerializationApi import javax.inject.Inject class DataLayerListenerServiceWear : WearableListenerService() { @@ -45,7 +46,7 @@ class DataLayerListenerServiceWear : WearableListenerService() { private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge) private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge) - + @ExperimentalSerializationApi override fun onCreate() { AndroidInjection.inject(this) super.onCreate() @@ -95,7 +96,7 @@ class DataLayerListenerServiceWear : WearableListenerService() { } super.onDataChanged(dataEvents) } - + @ExperimentalSerializationApi override fun onMessageReceived(messageEvent: MessageEvent) { super.onMessageReceived(messageEvent) diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt index ef97aa4270..26b85433be 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt @@ -172,7 +172,7 @@ open class Persistence @Inject constructor( fun store(customWatchface: EventData.ActionSetCustomWatchface, isdefault: Boolean = false) { putString(if (isdefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, customWatchface.serialize()) - aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.name} ${isdefault}: $customWatchface") + aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.customWatchfaceData} ${isdefault}: $customWatchface") } fun setDefaultWatchface() { diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index f01f5de785..cbf70ee8d5 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -26,12 +26,14 @@ import androidx.viewbinding.ViewBinding import info.nightscout.androidaps.R import info.nightscout.androidaps.databinding.ActivityCustomBinding import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace +import info.nightscout.rx.weardata.CustomWatchfaceData import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey import info.nightscout.rx.weardata.DrawableData import info.nightscout.rx.weardata.DrawableFormat import info.nightscout.rx.weardata.EventData +import info.nightscout.rx.weardata.ZipWatchfaceFormat import info.nightscout.shared.extensions.toVisibility import org.joda.time.TimeOfDay import org.json.JSONObject @@ -111,23 +113,14 @@ class CustomWatchface : BaseWatchFace() { binding.second.text = dateUtil.secondString() // rotate the second hand. binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f - //aapsLogger.debug("XXXXXX Setsecond calles:") } private fun setWatchfaceStyle() { - bgColor = when (singleBg.sgvLevel) { - 1L -> highColor - 0L -> midColor - -1L -> lowColor - else -> midColor - } val customWatchface = persistence.readCustomWatchface() ?: persistence.readCustomWatchface(true) - //aapsLogger.debug("XXXXX + setWatchfaceStyle Json ${customWatchface?.json}") customWatchface?.let { customWatchface -> - val json = JSONObject(customWatchface.json) - val drawableDataMap = customWatchface.drawableDataMap + val json = JSONObject(customWatchface.customWatchfaceData.json) + val drawableDataMap = customWatchface.customWatchfaceData.drawableDatas enableSecond = (if (json.has("enableSecond")) json.getBoolean("enableSecond") else false) && sp.getBoolean(R.string.key_show_seconds, true) - //aapsLogger.debug("XXXXXX json File (beginning):" + customWatchface.json) highColor = if (json.has("highColor")) Color.parseColor(json.getString("highColor")) else ContextCompat.getColor(this, R.color.dark_highColor) midColor = if (json.has("midColor")) Color.parseColor(json.getString("midColor")) else ContextCompat.getColor(this, R.color.inrange) @@ -136,13 +129,16 @@ class CustomWatchface : BaseWatchFace() { carbColor = if (json.has("carbColor")) Color.parseColor(json.getString("carbColor")) else ContextCompat.getColor(this, R.color.carbs) gridColor = if (json.has("gridColor")) Color.parseColor(json.getString("gridColor")) else ContextCompat.getColor(this, R.color.carbs) pointSize = if (json.has("pointSize")) json.getInt("pointSize") else 2 - aapsLogger.debug("XXXXXX enableSecond $enableSecond ${sp.getBoolean(R.string.key_show_seconds, false)} pointSize $pointSize") + bgColor = when (singleBg.sgvLevel) { + 1L -> highColor + 0L -> midColor + -1L -> lowColor + else -> midColor + } binding.mainLayout.forEach { view -> - //aapsLogger.debug("XXXXXX view:" + view.tag.toString()) view.tag?.let { tag -> if (json.has(tag.toString())) { var viewjson = json.getJSONObject(tag.toString()) - //aapsLogger.debug("XXXXXX \"" + tag.toString() + "\": " + viewjson.toString(4)) var wrapContent = LayoutParams.WRAP_CONTENT val width = if (viewjson.has("width")) (viewjson.getInt("width") * zoomFactor).toInt() else wrapContent val height = if (viewjson.has("height")) (viewjson.getInt("height") * zoomFactor).toInt() else wrapContent @@ -191,6 +187,7 @@ class CustomWatchface : BaseWatchFace() { private fun defaultWatchface(): EventData.ActionSetCustomWatchface { val metadata = JSONObject() .put(CustomWatchfaceMetadataKey.CWF_NAME.key, getString(info.nightscout.shared.R.string.wear_default_watchface)) + .put(CustomWatchfaceMetadataKey.CWF_FILENAME.key, getString(R.string.wear_default_watchface)) .put(CustomWatchfaceMetadataKey.CWF_AUTHOR.key, "Philoul") .put(CustomWatchfaceMetadataKey.CWF_CREATED_AT.key, dateUtil.dateString(dateUtil.now())) .put(CustomWatchfaceMetadataKey.CWF_VERSION.key, CUSTOM_VERSION) @@ -248,12 +245,13 @@ class CustomWatchface : BaseWatchFace() { ) } } - val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf() + val metadataMap = ZipWatchfaceFormat.loadMetadata(json) + val drawableDataMap: CustomWatchfaceDrawableDataMap = mutableMapOf() getResourceByteArray(info.nightscout.shared.R.drawable.watchface_custom)?.let { - val drawableDataMap = DrawableData(it,DrawableFormat.PNG) - drawableDatas[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE] = drawableDataMap + val drawableData = DrawableData(it,DrawableFormat.PNG) + drawableDataMap[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE] = drawableData } - return EventData.ActionSetCustomWatchface(getString(info.nightscout.shared.R.string.wear_default_watchface),json.toString(4),drawableDatas) + return EventData.ActionSetCustomWatchface(CustomWatchfaceData(json.toString(4), metadataMap, drawableDataMap)) } private fun setDefaultColors() { From 54e2a3f72c23add394ad33cb536a9e8697221494 Mon Sep 17 00:00:00 2001 From: Philoul Date: Sun, 6 Aug 2023 10:29:07 +0200 Subject: [PATCH 03/18] Remove shadow warning --- .../info/nightscout/plugins/general/wear/WearFragment.kt | 2 +- plugins/main/src/main/res/values/strings.xml | 2 +- .../nightscout/androidaps/watchfaces/CustomWatchface.kt | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt index 2a11088864..7798e89b37 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt @@ -82,7 +82,7 @@ class WearFragment : DaggerFragment() { .subscribe({ it.customWatchfaceData?.let { wearPlugin.savedCustomWatchface = it } if (it.exportFile) - ToastUtils.okToast(context,rh.gs(R.string.wear_new_custom_watchface_received)) + ToastUtils.okToast(activity, rh.gs(R.string.wear_new_custom_watchface_exported)) updateGui() }, fabricPrivacy::logException) if (wearPlugin.savedCustomWatchface == null) diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index b8d2b4eccf..0cb11d304e 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -365,7 +365,7 @@ Load Watchface Send Watchface Export Watchface - New watchface received from watch + Custom watchface exported Resend All Data Open Settings on Wear diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index cbf70ee8d5..d96410b81e 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -117,9 +117,9 @@ class CustomWatchface : BaseWatchFace() { private fun setWatchfaceStyle() { val customWatchface = persistence.readCustomWatchface() ?: persistence.readCustomWatchface(true) - customWatchface?.let { customWatchface -> - val json = JSONObject(customWatchface.customWatchfaceData.json) - val drawableDataMap = customWatchface.customWatchfaceData.drawableDatas + customWatchface?.let { + val json = JSONObject(it.customWatchfaceData.json) + val drawableDataMap = it.customWatchfaceData.drawableDatas enableSecond = (if (json.has("enableSecond")) json.getBoolean("enableSecond") else false) && sp.getBoolean(R.string.key_show_seconds, true) highColor = if (json.has("highColor")) Color.parseColor(json.getString("highColor")) else ContextCompat.getColor(this, R.color.dark_highColor) From 04c654c8c671e4446e83e93a61f30ccd2a807277 Mon Sep 17 00:00:00 2001 From: Philoul Date: Sun, 6 Aug 2023 23:48:11 +0200 Subject: [PATCH 04/18] Fix Default Layout (remove all wrapcontent) --- .../src/main/res/layout/wear_fragment.xml | 16 ++-- .../androidaps/watchfaces/CustomWatchface.kt | 12 +-- wear/src/main/res/layout/activity_custom.xml | 94 ++++++++++--------- 3 files changed, 61 insertions(+), 61 deletions(-) diff --git a/plugins/main/src/main/res/layout/wear_fragment.xml b/plugins/main/src/main/res/layout/wear_fragment.xml index 1eb374529d..a8c23efcd7 100644 --- a/plugins/main/src/main/res/layout/wear_fragment.xml +++ b/plugins/main/src/main/res/layout/wear_fragment.xml @@ -121,11 +121,11 @@ android:paddingEnd="0dp" android:text="@string/wear_load_watchface" android:textSize="11sp" - app:layout_column="0" app:layout_columnWeight="1" app:layout_gravity="fill" - app:layout_row="0" /> - + app:layout_row="0" + app:layout_column="0" /> + + app:layout_row="0" + app:layout_column="1" /> + app:layout_row="1" + app:layout_column="0" /> diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index d96410b81e..f556f6f114 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -72,7 +72,6 @@ class CustomWatchface : BaseWatchFace() { override fun setColorDark() { setWatchfaceStyle() - //@ColorInt val batteryOkColor = ContextCompat.getColor(this, R.color.dark_midColor) binding.mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.dark_background)) binding.sgv.setTextColor(bgColor) binding.direction.setTextColor(bgColor) @@ -135,6 +134,7 @@ class CustomWatchface : BaseWatchFace() { -1L -> lowColor else -> midColor } + binding.mainLayout.forEach { view -> view.tag?.let { tag -> if (json.has(tag.toString())) { @@ -147,7 +147,6 @@ class CustomWatchface : BaseWatchFace() { params.leftMargin = if (viewjson.has("leftmargin")) (viewjson.getInt("leftmargin") * zoomFactor).toInt() else 0 view.setLayoutParams(params) view.visibility = if (viewjson.has("visibility")) setVisibility(viewjson.getString("visibility")) else View.GONE - if (view is TextView) { view.rotation = if (viewjson.has("rotation")) viewjson.getInt("rotation").toFloat() else 0F view.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((if (viewjson.has("textsize")) viewjson.getInt("textsize") else 22) * zoomFactor).toFloat()) @@ -364,9 +363,8 @@ class CustomWatchface : BaseWatchFace() { private fun changeDrawableColor(color: Int): ColorFilter { val colorMatrix = ColorMatrix() - colorMatrix.setSaturation(0f) // 0 désature l'image, 1 la laisse inchangée. + colorMatrix.setSaturation(0f) - // Modifier la teinte de couleur (couleur de fond) colorMatrix.postConcat( ColorMatrix( floatArrayOf( @@ -377,13 +375,7 @@ class CustomWatchface : BaseWatchFace() { ) ) ) - - // Appliquer la matrice de couleur au ColorFilter return ColorMatrixColorFilter(colorMatrix) - - // Appliquer le ColorFilter au Drawable - //drawable.colorFilter = colorFilter - //return drawable } private fun getColor(color: String): Int { diff --git a/wear/src/main/res/layout/activity_custom.xml b/wear/src/main/res/layout/activity_custom.xml index a1172a3c3f..aa8eca5d47 100644 --- a/wear/src/main/res/layout/activity_custom.xml +++ b/wear/src/main/res/layout/activity_custom.xml @@ -30,8 +30,8 @@ @@ -129,11 +132,12 @@ android:id="@+id/rig_battery" android:tag="rig_battery" android:layout_width="60px" - android:layout_height="wrap_content" + android:layout_height="32px" android:gravity="center" android:layout_marginTop="133px" android:layout_marginLeft="189px" android:textSize="23px" + android:fontFamily="@font/roboto_condensed_bold" android:textStyle="bold" android:textColor="@color/light_grey" android:visibility="visible" @@ -143,10 +147,11 @@ android:id="@+id/basalRate" android:tag="basalRate" android:layout_width="91px" - android:layout_height="wrap_content" - android:gravity="center" + android:layout_height="32px" android:layout_marginTop="133px" android:layout_marginLeft="249px" + android:gravity="center" + android:fontFamily="@font/roboto_condensed_bold" android:textSize="23px" android:textStyle="bold" android:textColor="@color/light_grey" @@ -155,10 +160,11 @@ Date: Mon, 7 Aug 2023 01:15:40 +0200 Subject: [PATCH 05/18] Fix build --- .../src/main/java/info/nightscout/rx/weardata/EventData.kt | 1 + core/main/src/main/res/drawable/ic_watch.xml | 4 ++-- .../info/nightscout/plugins/general/wear/WearFragment.kt | 5 ----- .../info/nightscout/androidaps/watchfaces/CustomWatchface.kt | 2 +- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt index 4bfb867ef9..16e80764af 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt @@ -1,6 +1,7 @@ package info.nightscout.rx.weardata import info.nightscout.rx.events.Event +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.protobuf.ProtoBuf diff --git a/core/main/src/main/res/drawable/ic_watch.xml b/core/main/src/main/res/drawable/ic_watch.xml index bd2628f464..3f12b9288d 100644 --- a/core/main/src/main/res/drawable/ic_watch.xml +++ b/core/main/src/main/res/drawable/ic_watch.xml @@ -1,6 +1,6 @@ rxBus.send(EventMobileDataToWear(EventData.ActionSetCustomWatchface(cwf))) } - } binding.exportCustom.setOnClickListener { wearPlugin.savedCustomWatchface?.let { importExportPrefs.exportCustomWatchface(it) } ?: apply { rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(true)))} @@ -106,11 +103,9 @@ class WearFragment : DaggerFragment() { _binding ?: return wearPlugin.savedCustomWatchface?.let { binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.metadata[CustomWatchfaceMetadataKey.CWF_NAME]) - binding.sendCustom.visibility = View.VISIBLE binding.coverChart.setImageDrawable(it.drawableDatas[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE]?.toDrawable(resources)) } ?:apply { binding.customName.text = rh.gs(R.string.wear_custom_watchface, rh.gs(info.nightscout.shared.R.string.wear_default_watchface)) - binding.sendCustom.visibility = View.INVISIBLE binding.coverChart.setImageDrawable(null) } binding.connectedDevice.text = wearPlugin.connectedDevice diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index f556f6f114..42eaf43606 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -186,7 +186,7 @@ class CustomWatchface : BaseWatchFace() { private fun defaultWatchface(): EventData.ActionSetCustomWatchface { val metadata = JSONObject() .put(CustomWatchfaceMetadataKey.CWF_NAME.key, getString(info.nightscout.shared.R.string.wear_default_watchface)) - .put(CustomWatchfaceMetadataKey.CWF_FILENAME.key, getString(R.string.wear_default_watchface)) + .put(CustomWatchfaceMetadataKey.CWF_FILENAME.key, getString(info.nightscout.shared.R.string.wear_default_watchface)) .put(CustomWatchfaceMetadataKey.CWF_AUTHOR.key, "Philoul") .put(CustomWatchfaceMetadataKey.CWF_CREATED_AT.key, dateUtil.dateString(dateUtil.now())) .put(CustomWatchfaceMetadataKey.CWF_VERSION.key, CUSTOM_VERSION) From b3091090907c0c0c43dfb0332f9a6a97d9886042 Mon Sep 17 00:00:00 2001 From: Philoul Date: Mon, 7 Aug 2023 12:54:19 +0200 Subject: [PATCH 06/18] Delete Old EventWearUpdateGui (moved to Shared) --- .../plugins/general/wear/events/EventWearUpdateGui.kt | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 plugins/main/src/main/java/info/nightscout/plugins/general/wear/events/EventWearUpdateGui.kt diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/events/EventWearUpdateGui.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/events/EventWearUpdateGui.kt deleted file mode 100644 index b8c4761358..0000000000 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/events/EventWearUpdateGui.kt +++ /dev/null @@ -1,5 +0,0 @@ -package info.nightscout.plugins.general.wear.events - -import info.nightscout.rx.events.Event - -class EventWearUpdateGui : Event() \ No newline at end of file From 9bf6545f5db78659fa4f20935914c0d0786c7798 Mon Sep 17 00:00:00 2001 From: Philoul Date: Mon, 7 Aug 2023 14:17:34 +0200 Subject: [PATCH 07/18] Show custom watchface only if Wear Watch connected --- .../plugins/general/wear/WearFragment.kt | 2 + .../plugins/general/wear/WearPlugin.kt | 1 - .../src/main/res/layout/wear_fragment.xml | 228 +++++++++--------- 3 files changed, 115 insertions(+), 116 deletions(-) diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt index 85c620eb94..b3bc0008b8 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt @@ -19,6 +19,7 @@ import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.extensions.toVisibility import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil @@ -109,6 +110,7 @@ class WearFragment : DaggerFragment() { binding.coverChart.setImageDrawable(null) } binding.connectedDevice.text = wearPlugin.connectedDevice + binding.customWatchfaceLayout.visibility = (wearPlugin.connectedDevice != rh.gs(R.string.no_watch_connected)).toVisibility() } private fun loadCustom(cwf: EventData.ActionSetCustomWatchface) { diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt index 156f791da6..9b7bd88101 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt @@ -55,7 +55,6 @@ class WearPlugin @Inject constructor( private val disposable = CompositeDisposable() var connectedDevice = "---" - var customWatchfaceSerialized = "" var savedCustomWatchface: CustomWatchfaceData? = null override fun onStart() { diff --git a/plugins/main/src/main/res/layout/wear_fragment.xml b/plugins/main/src/main/res/layout/wear_fragment.xml index a8c23efcd7..4730a02599 100644 --- a/plugins/main/src/main/res/layout/wear_fragment.xml +++ b/plugins/main/src/main/res/layout/wear_fragment.xml @@ -73,128 +73,126 @@ - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + android:padding="20dp" + android:orientation="vertical"> + + + + From 363877175b496f298425121b1a4adddefa0b139d Mon Sep 17 00:00:00 2001 From: Philoul Date: Tue, 8 Aug 2023 12:12:27 +0200 Subject: [PATCH 08/18] Fix Amient mode for second --- .../androidaps/watchfaces/CustomWatchface.kt | 1 + .../watchfaces/utils/BaseWatchFace.kt | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index 42eaf43606..b164077c4e 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -112,6 +112,7 @@ class CustomWatchface : BaseWatchFace() { binding.second.text = dateUtil.secondString() // rotate the second hand. binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f + //aapsLogger.debug("XXXXX SetSecond") } private fun setWatchfaceStyle() { diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt index 4a7fd3a5cf..af6a0b463a 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt @@ -36,8 +36,10 @@ import info.nightscout.shared.extensions.toVisibilityKeepSpace import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.plusAssign +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.floor @@ -87,7 +89,14 @@ abstract class BaseWatchFace : WatchFace() { var dividerMatchesBg = false var pointSize = 2 var enableSecond = false - var updateSecond: Disposable? = null + + // SecondUpdater (Ambient mode) + private val mScheduledSecondUpdaterPool = Executors.newScheduledThreadPool(2) + private var mScheduledSecondUpdater: ScheduledFuture<*>? = null + private val mSecondUpdater = Runnable { + setSecond() + invalidate() + } // Tapping times private var sgvTapTime: Long = 0 @@ -249,8 +258,9 @@ abstract class BaseWatchFace : WatchFace() { override fun onDestroy() { disposable.clear() - updateSecond?.dispose() simpleUi.onDestroy() + cancelSecondUpdater() + mScheduledSecondUpdaterPool.shutdown() super.onDestroy() } @@ -394,17 +404,17 @@ abstract class BaseWatchFace : WatchFace() { lowResMode = isLowRes(watchMode) if (simpleUi.isEnabled(currentWatchMode)) simpleUi.setAntiAlias(currentWatchMode) else setDataFields() + if (watchMode == WatchMode.AMBIENT && enableSecond) { + val initialDelay = 1000L - System.currentTimeMillis() % 1000L + mScheduledSecondUpdater = mScheduledSecondUpdaterPool.scheduleAtFixedRate(mSecondUpdater, initialDelay, 1000L, TimeUnit.MILLISECONDS) + } else { + cancelSecondUpdater() + } invalidate() - /* - if (enableSecond) - if (updateSecond == null) - updateSecond = aapsSchedulers.io.schedulePeriodicallyDirect( - ::setSecond, 1000L, 1000L, TimeUnit.MILLISECONDS) - else - updateSecond?.dispose() - */ } + private fun cancelSecondUpdater() = mScheduledSecondUpdater?.cancel(true) + private fun isLowRes(watchMode: WatchMode): Boolean { return watchMode == WatchMode.LOW_BIT || watchMode == WatchMode.LOW_BIT_BURN_IN } From dd24dd5051e0e7dfffd0c60f3628cc188162de9a Mon Sep 17 00:00:00 2001 From: Philoul Date: Tue, 8 Aug 2023 12:21:26 +0200 Subject: [PATCH 09/18] Load CustomWatchface in dedicated function (for future updates) --- .../info/nightscout/plugins/general/wear/WearFragment.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt index b3bc0008b8..64a386379e 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt @@ -16,6 +16,7 @@ import info.nightscout.rx.events.EventMobileDataToWear import info.nightscout.rx.events.EventMobileToWear import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CustomWatchfaceData import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey import info.nightscout.rx.weardata.EventData @@ -78,7 +79,7 @@ class WearFragment : DaggerFragment() { .toObservable(EventWearUpdateGui::class.java) .observeOn(aapsSchedulers.main) .subscribe({ - it.customWatchfaceData?.let { wearPlugin.savedCustomWatchface = it } + it.customWatchfaceData?.let { loadCustom(it) } if (it.exportFile) ToastUtils.okToast(activity, rh.gs(R.string.wear_new_custom_watchface_exported)) updateGui() @@ -113,7 +114,7 @@ class WearFragment : DaggerFragment() { binding.customWatchfaceLayout.visibility = (wearPlugin.connectedDevice != rh.gs(R.string.no_watch_connected)).toVisibility() } - private fun loadCustom(cwf: EventData.ActionSetCustomWatchface) { - wearPlugin.savedCustomWatchface = cwf.customWatchfaceData + private fun loadCustom(cwf: CustomWatchfaceData) { + wearPlugin.savedCustomWatchface = cwf } } \ No newline at end of file From f8dbcf2edd8eac1286cb49631979753f49b7e41f Mon Sep 17 00:00:00 2001 From: Philoul Date: Thu, 10 Aug 2023 22:25:52 +0200 Subject: [PATCH 10/18] Hide seconds in Ambient mode --- .../androidaps/watchfaces/CustomWatchface.kt | 12 ++++-- .../watchfaces/utils/BaseWatchFace.kt | 43 +++++++------------ 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index b164077c4e..587abd4f11 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -108,11 +108,16 @@ class CustomWatchface : BaseWatchFace() { } override fun setSecond() { - binding.time.text = "${dateUtil.hourString()}:${dateUtil.minuteString()}" + if (enableSecond) ":${dateUtil.secondString()}" else "" + binding.time.text = "${dateUtil.hourString()}:${dateUtil.minuteString()}" + if (showSecond) ":${dateUtil.secondString()}" else "" binding.second.text = dateUtil.secondString() // rotate the second hand. binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f - //aapsLogger.debug("XXXXX SetSecond") + //aapsLogger.debug("XXXXX SetSecond $watchModeString") + } + + override fun updateSecondVisibility() { + binding.second.visibility = (enableSecond && showSecond).toVisibility() + binding.secondHand.visibility = (enableSecond && showSecond).toVisibility() } private fun setWatchfaceStyle() { @@ -179,8 +184,7 @@ class CustomWatchface : BaseWatchFace() { } } } - binding.second.visibility= ((binding.second.visibility==View.VISIBLE) && enableSecond).toVisibility() - binding.secondHand.visibility= ((binding.secondHand.visibility==View.VISIBLE) && enableSecond).toVisibility() + updateSecondVisibility() } } diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt index af6a0b463a..656e14cbe4 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt @@ -37,9 +37,6 @@ import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.floor @@ -89,14 +86,8 @@ abstract class BaseWatchFace : WatchFace() { var dividerMatchesBg = false var pointSize = 2 var enableSecond = false - - // SecondUpdater (Ambient mode) - private val mScheduledSecondUpdaterPool = Executors.newScheduledThreadPool(2) - private var mScheduledSecondUpdater: ScheduledFuture<*>? = null - private val mSecondUpdater = Runnable { - setSecond() - invalidate() - } + val showSecond: Boolean + get() = enableSecond && currentWatchMode == WatchMode.INTERACTIVE // Tapping times private var sgvTapTime: Long = 0 @@ -259,13 +250,11 @@ abstract class BaseWatchFace : WatchFace() { override fun onDestroy() { disposable.clear() simpleUi.onDestroy() - cancelSecondUpdater() - mScheduledSecondUpdaterPool.shutdown() super.onDestroy() } override fun getInteractiveModeUpdateRate(): Long { - return if (enableSecond) 1000L else 60 * 1000L // Only call onTimeChanged every 60 seconds + return if (showSecond) 1000L else 60 * 1000L // Only call onTimeChanged every 60 seconds } override fun onDraw(canvas: Canvas) { @@ -286,7 +275,7 @@ abstract class BaseWatchFace : WatchFace() { missedReadingAlert() checkVibrateHourly(oldTime, newTime) if (!simpleUi.isEnabled(currentWatchMode)) setDataFields() - } else if (layoutSet && !simpleUi.isEnabled(currentWatchMode) && enableSecond && newTime.hasSecondChanged(oldTime)) { + } else if (layoutSet && !simpleUi.isEnabled(currentWatchMode) && showSecond && newTime.hasSecondChanged(oldTime)) { setSecond() } } @@ -366,23 +355,28 @@ abstract class BaseWatchFace : WatchFace() { } private fun setDateAndTime() { - binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + if (enableSecond) ":" + dateUtil.secondString() else "" + binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() binding.hour?.text = dateUtil.hourString() binding.minute?.text = dateUtil.minuteString() - binding.second?.text = dateUtil.secondString() binding.dateTime?.visibility = sp.getBoolean(R.string.key_show_date, false).toVisibility() binding.dayName?.text = dateUtil.dayNameString() binding.day?.text = dateUtil.dayString() binding.month?.text = dateUtil.monthString() binding.timePeriod?.visibility = android.text.format.DateFormat.is24HourFormat(this).not().toVisibility() binding.timePeriod?.text = dateUtil.amPm() + if (showSecond) + setSecond() } open fun setSecond() { - binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + if (enableSecond) ":" + dateUtil.secondString() else "" + binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + if (showSecond) ":" + dateUtil.secondString() else "" binding.second?.text = dateUtil.secondString() } + open fun updateSecondVisibility() { + binding.second?.visibility = showSecond.toVisibility() + } + fun setColor() { dividerMatchesBg = sp.getBoolean(R.string.key_match_divider, false) when { @@ -401,20 +395,15 @@ abstract class BaseWatchFace : WatchFace() { } override fun onWatchModeChanged(watchMode: WatchMode) { + updateSecondVisibility() // will show second if enabledSecond and Interactive mode, hide in other situation + setSecond() // will remove second from main date and time if not in Interactive mode lowResMode = isLowRes(watchMode) if (simpleUi.isEnabled(currentWatchMode)) simpleUi.setAntiAlias(currentWatchMode) - else setDataFields() - if (watchMode == WatchMode.AMBIENT && enableSecond) { - val initialDelay = 1000L - System.currentTimeMillis() % 1000L - mScheduledSecondUpdater = mScheduledSecondUpdaterPool.scheduleAtFixedRate(mSecondUpdater, initialDelay, 1000L, TimeUnit.MILLISECONDS) - } else { - cancelSecondUpdater() - } + else + setDataFields() invalidate() } - private fun cancelSecondUpdater() = mScheduledSecondUpdater?.cancel(true) - private fun isLowRes(watchMode: WatchMode): Boolean { return watchMode == WatchMode.LOW_BIT || watchMode == WatchMode.LOW_BIT_BURN_IN } From af2f18acd93af573c6d3784ae7b5e1d2caa0f0e9 Mon Sep 17 00:00:00 2001 From: Philoul Date: Fri, 11 Aug 2023 22:00:19 +0200 Subject: [PATCH 11/18] Avoid crash of Watchface (even on wrong type of data within json file) --- .../plugins/general/wear/WearPlugin.kt | 5 + .../src/main/res/layout/wear_fragment.xml | 2 +- .../androidaps/watchfaces/CustomWatchface.kt | 164 +++++++++--------- 3 files changed, 87 insertions(+), 84 deletions(-) diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt index 9b7bd88101..731290df49 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt @@ -17,6 +17,7 @@ import info.nightscout.rx.events.EventLoopUpdateGui import info.nightscout.rx.events.EventMobileToWear import info.nightscout.rx.events.EventOverviewBolusProgress import info.nightscout.rx.events.EventPreferenceChange +import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.weardata.CustomWatchfaceData import info.nightscout.rx.weardata.EventData @@ -91,6 +92,10 @@ class WearPlugin @Inject constructor( .toObservable(EventLoopUpdateGui::class.java) .observeOn(aapsSchedulers.io) .subscribe({ dataHandlerMobile.resendData("EventLoopUpdateGui") }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventWearUpdateGui::class.java) + .observeOn(aapsSchedulers.main) + .subscribe({ it.customWatchfaceData?.let { cwf -> savedCustomWatchface = cwf } }, fabricPrivacy::logException) } override fun onStop() { diff --git a/plugins/main/src/main/res/layout/wear_fragment.xml b/plugins/main/src/main/res/layout/wear_fragment.xml index 4730a02599..8e8b421c3d 100644 --- a/plugins/main/src/main/res/layout/wear_fragment.xml +++ b/plugins/main/src/main/res/layout/wear_fragment.xml @@ -106,7 +106,7 @@ diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index 587abd4f11..3814c8c003 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -116,75 +116,79 @@ class CustomWatchface : BaseWatchFace() { } override fun updateSecondVisibility() { - binding.second.visibility = (enableSecond && showSecond).toVisibility() - binding.secondHand.visibility = (enableSecond && showSecond).toVisibility() + binding.second.visibility = showSecond.toVisibility() + binding.secondHand.visibility = showSecond.toVisibility() } private fun setWatchfaceStyle() { val customWatchface = persistence.readCustomWatchface() ?: persistence.readCustomWatchface(true) customWatchface?.let { - val json = JSONObject(it.customWatchfaceData.json) - val drawableDataMap = it.customWatchfaceData.drawableDatas - enableSecond = (if (json.has("enableSecond")) json.getBoolean("enableSecond") else false) && sp.getBoolean(R.string.key_show_seconds, true) + try { + val json = JSONObject(it.customWatchfaceData.json) + val drawableDataMap = it.customWatchfaceData.drawableDatas + enableSecond = (if (json.has("enableSecond")) json.getBoolean("enableSecond") else false) && sp.getBoolean(R.string.key_show_seconds, true) - highColor = if (json.has("highColor")) Color.parseColor(json.getString("highColor")) else ContextCompat.getColor(this, R.color.dark_highColor) - midColor = if (json.has("midColor")) Color.parseColor(json.getString("midColor")) else ContextCompat.getColor(this, R.color.inrange) - lowColor = if (json.has("lowColor")) Color.parseColor(json.getString("lowColor")) else ContextCompat.getColor(this, R.color.low) - lowBatColor = if (json.has("lowBatColor")) Color.parseColor(json.getString("lowBatColor")) else ContextCompat.getColor(this, R.color.dark_uploaderBatteryEmpty) - carbColor = if (json.has("carbColor")) Color.parseColor(json.getString("carbColor")) else ContextCompat.getColor(this, R.color.carbs) - gridColor = if (json.has("gridColor")) Color.parseColor(json.getString("gridColor")) else ContextCompat.getColor(this, R.color.carbs) - pointSize = if (json.has("pointSize")) json.getInt("pointSize") else 2 - bgColor = when (singleBg.sgvLevel) { - 1L -> highColor - 0L -> midColor - -1L -> lowColor - else -> midColor - } + highColor = if (json.has("highColor")) Color.parseColor(json.getString("highColor")) else ContextCompat.getColor(this, R.color.dark_highColor) + midColor = if (json.has("midColor")) Color.parseColor(json.getString("midColor")) else ContextCompat.getColor(this, R.color.inrange) + lowColor = if (json.has("lowColor")) Color.parseColor(json.getString("lowColor")) else ContextCompat.getColor(this, R.color.low) + lowBatColor = if (json.has("lowBatColor")) Color.parseColor(json.getString("lowBatColor")) else ContextCompat.getColor(this, R.color.dark_uploaderBatteryEmpty) + carbColor = if (json.has("carbColor")) Color.parseColor(json.getString("carbColor")) else ContextCompat.getColor(this, R.color.carbs) + gridColor = if (json.has("gridColor")) Color.parseColor(json.getString("gridColor")) else ContextCompat.getColor(this, R.color.carbs) + pointSize = if (json.has("pointSize")) json.getInt("pointSize") else 2 + bgColor = when (singleBg.sgvLevel) { + 1L -> highColor + 0L -> midColor + -1L -> lowColor + else -> midColor + } - binding.mainLayout.forEach { view -> - view.tag?.let { tag -> - if (json.has(tag.toString())) { - var viewjson = json.getJSONObject(tag.toString()) - var wrapContent = LayoutParams.WRAP_CONTENT - val width = if (viewjson.has("width")) (viewjson.getInt("width") * zoomFactor).toInt() else wrapContent - val height = if (viewjson.has("height")) (viewjson.getInt("height") * zoomFactor).toInt() else wrapContent - var params = FrameLayout.LayoutParams(width, height) - params.topMargin = if (viewjson.has("topmargin")) (viewjson.getInt("topmargin") * zoomFactor).toInt() else 0 - params.leftMargin = if (viewjson.has("leftmargin")) (viewjson.getInt("leftmargin") * zoomFactor).toInt() else 0 - view.setLayoutParams(params) - view.visibility = if (viewjson.has("visibility")) setVisibility(viewjson.getString("visibility")) else View.GONE - if (view is TextView) { - view.rotation = if (viewjson.has("rotation")) viewjson.getInt("rotation").toFloat() else 0F - view.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((if (viewjson.has("textsize")) viewjson.getInt("textsize") else 22) * zoomFactor).toFloat()) - view.gravity = setGravity(if (viewjson.has("gravity")) viewjson.getString("gravity") else "center") - view.setTypeface( - setFont(if (viewjson.has("font")) viewjson.getString("font") else "sans-serif"), - Style.fromKey( viewjson.getString("fontStyle")).typeface - ) - if (viewjson.has("fontColor")) - view.setTextColor(getColor(viewjson.getString("fontColor"))) - } + binding.mainLayout.forEach { view -> + view.tag?.let { tag -> + if (json.has(tag.toString())) { + var viewjson = json.getJSONObject(tag.toString()) + var wrapContent = LayoutParams.WRAP_CONTENT + val width = if (viewjson.has("width")) (viewjson.getInt("width") * zoomFactor).toInt() else wrapContent + val height = if (viewjson.has("height")) (viewjson.getInt("height") * zoomFactor).toInt() else wrapContent + var params = FrameLayout.LayoutParams(width, height) + params.topMargin = if (viewjson.has("topmargin")) (viewjson.getInt("topmargin") * zoomFactor).toInt() else 0 + params.leftMargin = if (viewjson.has("leftmargin")) (viewjson.getInt("leftmargin") * zoomFactor).toInt() else 0 + view.setLayoutParams(params) + view.visibility = if (viewjson.has("visibility")) setVisibility(viewjson.getString("visibility")) else View.GONE + if (view is TextView) { + view.rotation = if (viewjson.has("rotation")) viewjson.getInt("rotation").toFloat() else 0F + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((if (viewjson.has("textsize")) viewjson.getInt("textsize") else 22) * zoomFactor).toFloat()) + view.gravity = setGravity(if (viewjson.has("gravity")) viewjson.getString("gravity") else "center") + view.setTypeface( + setFont(if (viewjson.has("font")) viewjson.getString("font") else "sans-serif"), + setStyle(if (viewjson.has("fontStyle")) viewjson.getString("fontStyle") else "normal") + ) + if (viewjson.has("fontColor")) + view.setTextColor(getColor(viewjson.getString("fontColor"))) + } - if (view is ImageView) { - view.clearColorFilter() - drawableDataMap[CustomWatchfaceDrawableDataKey.fromKey(tag.toString())]?.toDrawable(resources)?.also { - if (viewjson.has("color")) - it.colorFilter = changeDrawableColor(getColor(viewjson.getString("color"))) - else - it.clearColorFilter() - view.setImageDrawable(it) - } ?: apply { - view.setImageDrawable(CustomWatchfaceDrawableDataKey.fromKey(tag.toString()).icon?.let { context.getDrawable(it) }) - if (viewjson.has("color")) - view.setColorFilter(getColor(viewjson.getString("color"))) - else - view.clearColorFilter() + if (view is ImageView) { + view.clearColorFilter() + drawableDataMap[CustomWatchfaceDrawableDataKey.fromKey(tag.toString())]?.toDrawable(resources)?.also { + if (viewjson.has("color")) + it.colorFilter = changeDrawableColor(getColor(viewjson.getString("color"))) + else + it.clearColorFilter() + view.setImageDrawable(it) + } ?: apply { + view.setImageDrawable(CustomWatchfaceDrawableDataKey.fromKey(tag.toString()).icon?.let { context.getDrawable(it) }) + if (viewjson.has("color")) + view.setColorFilter(getColor(viewjson.getString("color"))) + else + view.clearColorFilter() + } } } } } + updateSecondVisibility() + } catch (e:Exception) { + persistence.store(defaultWatchface(), false) // relaod correct values to avoid crash of watchface } - updateSecondVisibility() } } @@ -221,7 +225,7 @@ class CustomWatchface : BaseWatchFace() { .put("textsize", view.textSize.toInt()) .put("gravity", getGravity(view.gravity)) .put("font", getFont(view.typeface)) - .put("fontStyle", Style.fromTypeface(view.typeface.style).key) + .put("fontStyle", getStyle(view.typeface.style)) .put("fontColor", String.format("#%06X", 0xFFFFFF and view.currentTextColor)) ) } @@ -321,32 +325,22 @@ class CustomWatchface : BaseWatchFace() { else -> "default" } - enum class Style(val key: String, val typeface: Int) { - NORMAL("normal", Typeface.NORMAL), - BOLD("bold", Typeface.BOLD), - BOLD_ITALIC("bold-italic", Typeface.BOLD_ITALIC), - ITALIC("italic", Typeface.ITALIC); - companion object { - private val keyToEnumMap = HashMap() - private val typefaceToEnumMap = HashMap() - init { - for (value in values()) keyToEnumMap[value.key] = value - for (value in values()) typefaceToEnumMap[value.typeface] = value - } - fun fromKey(key: String?): Style = - if (keyToEnumMap.containsKey(key)) { - keyToEnumMap[key] ?:NORMAL - } else { - NORMAL - } - fun fromTypeface(typeface: Int?): Style = - if (typefaceToEnumMap.containsKey(typeface)) { - typefaceToEnumMap[typeface] ?:NORMAL - } else { - NORMAL - } - } + private fun setStyle(style: String): Int = when (style) { + "normal" -> Typeface.NORMAL + "bold" -> Typeface.BOLD + "bold-italic" -> Typeface.BOLD_ITALIC + "italic" -> Typeface.ITALIC + else -> Typeface.NORMAL } + + private fun getStyle(style: Int): String = when (style) { + Typeface.NORMAL -> "normal" + Typeface.BOLD -> "bold" + Typeface.BOLD_ITALIC -> "bold-italic" + Typeface.ITALIC -> "italic" + else -> "normal" + } + fun getResourceByteArray(resourceId: Int): ByteArray? { val inputStream = resources.openRawResource(resourceId) val byteArrayOutputStream = ByteArrayOutputStream() @@ -387,7 +381,11 @@ class CustomWatchface : BaseWatchFace() { if (color == "bgColor") return bgColor else - return Color.parseColor(color) + return try { + Color.parseColor(color) + } catch (e: Exception) { + Color.GRAY + } } } From f2e92a1b1933580dbe137d00ae13a111726d46ae Mon Sep 17 00:00:00 2001 From: Philoul Date: Sat, 12 Aug 2023 11:52:07 +0200 Subject: [PATCH 12/18] Replace tag by enum and sync with AAPS preferences --- .../androidaps/watchfaces/CustomWatchface.kt | 162 ++++++++++++------ wear/src/main/res/layout/activity_custom.xml | 29 ---- 2 files changed, 114 insertions(+), 77 deletions(-) diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index 3814c8c003..ffdaba882a 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -19,6 +19,8 @@ import android.view.WindowManager import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.annotation.IdRes +import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.core.view.forEach @@ -35,6 +37,8 @@ import info.nightscout.rx.weardata.DrawableFormat import info.nightscout.rx.weardata.EventData import info.nightscout.rx.weardata.ZipWatchfaceFormat import info.nightscout.shared.extensions.toVisibility +import info.nightscout.shared.extensions.toVisibilityKeepSpace +import info.nightscout.shared.sharedPreferences.SP import org.joda.time.TimeOfDay import org.json.JSONObject import java.io.ByteArrayOutputStream @@ -143,9 +147,9 @@ class CustomWatchface : BaseWatchFace() { } binding.mainLayout.forEach { view -> - view.tag?.let { tag -> - if (json.has(tag.toString())) { - var viewjson = json.getJSONObject(tag.toString()) + CustomViews.fromId(view.id)?.let { id -> + if (json.has(id.key)) { + var viewjson = json.getJSONObject(id.key) var wrapContent = LayoutParams.WRAP_CONTENT val width = if (viewjson.has("width")) (viewjson.getInt("width") * zoomFactor).toInt() else wrapContent val height = if (viewjson.has("height")) (viewjson.getInt("height") * zoomFactor).toInt() else wrapContent @@ -153,7 +157,7 @@ class CustomWatchface : BaseWatchFace() { params.topMargin = if (viewjson.has("topmargin")) (viewjson.getInt("topmargin") * zoomFactor).toInt() else 0 params.leftMargin = if (viewjson.has("leftmargin")) (viewjson.getInt("leftmargin") * zoomFactor).toInt() else 0 view.setLayoutParams(params) - view.visibility = if (viewjson.has("visibility")) setVisibility(viewjson.getString("visibility")) else View.GONE + view.visibility = if (viewjson.has("visibility")) setVisibility(viewjson.getString("visibility"), id.visibility(sp)) else View.GONE if (view is TextView) { view.rotation = if (viewjson.has("rotation")) viewjson.getInt("rotation").toFloat() else 0F view.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((if (viewjson.has("textsize")) viewjson.getInt("textsize") else 22) * zoomFactor).toFloat()) @@ -168,14 +172,14 @@ class CustomWatchface : BaseWatchFace() { if (view is ImageView) { view.clearColorFilter() - drawableDataMap[CustomWatchfaceDrawableDataKey.fromKey(tag.toString())]?.toDrawable(resources)?.also { + drawableDataMap[CustomWatchfaceDrawableDataKey.fromKey(id.key)]?.toDrawable(resources)?.also { if (viewjson.has("color")) it.colorFilter = changeDrawableColor(getColor(viewjson.getString("color"))) else it.clearColorFilter() view.setImageDrawable(it) } ?: apply { - view.setImageDrawable(CustomWatchfaceDrawableDataKey.fromKey(tag.toString()).icon?.let { context.getDrawable(it) }) + view.setImageDrawable(CustomWatchfaceDrawableDataKey.fromKey(id.key).icon?.let { context.getDrawable(it) }) if (viewjson.has("color")) view.setColorFilter(getColor(viewjson.getString("color"))) else @@ -212,45 +216,47 @@ class CustomWatchface : BaseWatchFace() { binding.mainLayout.forEach { view -> val params = view.layoutParams as FrameLayout.LayoutParams - if (view is TextView) { - json.put( - view.tag.toString(), - JSONObject() - .put("width", (params.width / zoomFactor).toInt()) - .put("height", (params.height / zoomFactor).toInt()) - .put("topmargin", (params.topMargin / zoomFactor).toInt()) - .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) - .put("rotation", view.rotation.toInt()) - .put("visibility", getVisibility(view.visibility)) - .put("textsize", view.textSize.toInt()) - .put("gravity", getGravity(view.gravity)) - .put("font", getFont(view.typeface)) - .put("fontStyle", getStyle(view.typeface.style)) - .put("fontColor", String.format("#%06X", 0xFFFFFF and view.currentTextColor)) - ) - } - if (view is ImageView) { - //view.backgroundTintList = - json.put( - view.tag.toString(), - JSONObject() - .put("width", (params.width / zoomFactor).toInt()) - .put("height", (params.height / zoomFactor).toInt()) - .put("topmargin", (params.topMargin / zoomFactor).toInt()) - .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) - .put("visibility", getVisibility(view.visibility)) - ) - } - if (view is lecho.lib.hellocharts.view.LineChartView) { - json.put( - view.tag.toString(), - JSONObject() - .put("width", (params.width / zoomFactor).toInt()) - .put("height", (params.height / zoomFactor).toInt()) - .put("topmargin", (params.topMargin / zoomFactor).toInt()) - .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) - .put("visibility", getVisibility(view.visibility)) - ) + CustomViews.fromId(view.id)?.let { + if (view is TextView) { + json.put( + it.key, + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("rotation", view.rotation.toInt()) + .put("visibility", getVisibility(view.visibility)) + .put("textsize", view.textSize.toInt()) + .put("gravity", getGravity(view.gravity)) + .put("font", getFont(view.typeface)) + .put("fontStyle", getStyle(view.typeface.style)) + .put("fontColor", String.format("#%06X", 0xFFFFFF and view.currentTextColor)) + ) + } + if (view is ImageView) { + //view.backgroundTintList = + json.put( + it.key, + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("visibility", getVisibility(view.visibility)) + ) + } + if (view is lecho.lib.hellocharts.view.LineChartView) { + json.put( + it.key, + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("visibility", getVisibility(view.visibility)) + ) + } } } val metadataMap = ZipWatchfaceFormat.loadMetadata(json) @@ -271,9 +277,9 @@ class CustomWatchface : BaseWatchFace() { gridColor = Color.WHITE } - private fun setVisibility(visibility: String): Int = when (visibility) { - "visible" -> View.VISIBLE - "invisible" -> View.INVISIBLE + private fun setVisibility(visibility: String, pref: Boolean = true): Int = when (visibility) { + "visible" -> pref.toVisibility() + "invisible" -> pref.toVisibilityKeepSpace() "gone" -> View.GONE else -> View.GONE } @@ -388,4 +394,64 @@ class CustomWatchface : BaseWatchFace() { } } + enum class CustomViews(val key: String, @IdRes val id: Int, @StringRes val pref: Int?) { + + BACKGROUND(CustomWatchfaceDrawableDataKey.BACKGROUND.key, R.id.background, null), + CHART("chart", R.id.chart, null), + COVER_CHART(CustomWatchfaceDrawableDataKey.COVERCHART.key, R.id.cover_chart, null), + IOB1("iob1", R.id.iob1, R.string.key_show_iob), + IOB2("iob2", R.id.iob2, R.string.key_show_iob), + COB1("cob1", R.id.cob1, R.string.key_show_cob), + COB2("cob2", R.id.cob2, R.string.key_show_cob), + DELTA("delta", R.id.delta, R.string.key_show_delta), + AVG_DELTA("avg_delta", R.id.avg_delta, R.string.key_show_avg_delta), + UPLOADER_BATTERY("uploader_battery", R.id.uploader_battery, R.string.key_show_uploader_battery), + RIG_BATTERY("rig_battery", R.id.rig_battery, R.string.key_show_rig_battery), + BASALRATE("basalRate", R.id.basalRate, R.string.key_show_temp_basal), + BGI("bgi", R.id.bgi, null), + TIME("time", R.id.time, null), + HOUR("hour", R.id.hour, null), + MINUTE("minute", R.id.minute, null), + SECOND("second", R.id.second, R.string.key_show_seconds), + TIMEPERIOD("timePeriod", R.id.timePeriod, null), + DAY_NAME("day_name", R.id.day_name, null), + DAY("day", R.id.day, null), + MONTH("month", R.id.month, null), + LOOP("loop", R.id.loop, R.string.key_show_external_status), + DIRECTION("direction", R.id.direction, R.string.key_show_direction), + TIMESTAMP("timestamp", R.id.timestamp, R.string.key_show_ago), + SGV("sgv", R.id.sgv, R.string.key_show_bg), + COVER_PLATE(CustomWatchfaceDrawableDataKey.COVERPLATE.key, R.id.cover_plate, null), + HOUR_HABD(CustomWatchfaceDrawableDataKey.HOURHAND.key, R.id.hour_hand, null), + MINUTE_HAND(CustomWatchfaceDrawableDataKey.MINUTEHAND.key, R.id.minute_hand, null), + SECOND_HAND(CustomWatchfaceDrawableDataKey.SECONDHAND.key, R.id.second_hand, R.string.key_show_seconds); + + companion object { + + private val keyToEnumMap = HashMap() + private val idToEnumMap = HashMap() + + init { + for (value in values()) keyToEnumMap[value.key] = value + for (value in values()) idToEnumMap[value.id] = value + } + + fun fromKey(key: String): CustomViews? = + if (keyToEnumMap.containsKey(key)) { + keyToEnumMap[key] + } else { + null + } + fun fromId(id: Int): CustomViews? = + if (idToEnumMap.containsKey(id)) { + idToEnumMap[id] + } else { + null + } + } + + fun visibility(sp: SP): Boolean = this.pref?.let { sp.getBoolean(it, true) } + ?: true + } + } diff --git a/wear/src/main/res/layout/activity_custom.xml b/wear/src/main/res/layout/activity_custom.xml index aa8eca5d47..3a7466f97b 100644 --- a/wear/src/main/res/layout/activity_custom.xml +++ b/wear/src/main/res/layout/activity_custom.xml @@ -9,7 +9,6 @@ Date: Sat, 12 Aug 2023 17:41:44 +0200 Subject: [PATCH 13/18] Add 2 freetext views in CustomWatchface --- .../androidaps/watchfaces/CustomWatchface.kt | 4 ++++ wear/src/main/res/layout/activity_custom.xml | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index ffdaba882a..5cb6f497d0 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -168,6 +168,8 @@ class CustomWatchface : BaseWatchFace() { ) if (viewjson.has("fontColor")) view.setTextColor(getColor(viewjson.getString("fontColor"))) + if (viewjson.has("textvalue")) + view.text = viewjson.getString("textvalue") } if (view is ImageView) { @@ -399,6 +401,8 @@ class CustomWatchface : BaseWatchFace() { BACKGROUND(CustomWatchfaceDrawableDataKey.BACKGROUND.key, R.id.background, null), CHART("chart", R.id.chart, null), COVER_CHART(CustomWatchfaceDrawableDataKey.COVERCHART.key, R.id.cover_chart, null), + FREETEXT1("freetext1", R.id.freetext1, null), + FREETEXT2("freetext2", R.id.freetext2, null), IOB1("iob1", R.id.iob1, R.string.key_show_iob), IOB2("iob2", R.id.iob2, R.string.key_show_iob), COB1("cob1", R.id.cob1, R.string.key_show_cob), diff --git a/wear/src/main/res/layout/activity_custom.xml b/wear/src/main/res/layout/activity_custom.xml index 3a7466f97b..553dc1fdae 100644 --- a/wear/src/main/res/layout/activity_custom.xml +++ b/wear/src/main/res/layout/activity_custom.xml @@ -34,6 +34,28 @@ android:visibility="gone" android:orientation="vertical" /> + + + + Date: Sat, 12 Aug 2023 18:15:02 +0200 Subject: [PATCH 14/18] Update CustomWatchface version to v0.2 --- .../info/nightscout/androidaps/watchfaces/CustomWatchface.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index 5cb6f497d0..5df2e0799b 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -54,7 +54,7 @@ class CustomWatchface : BaseWatchFace() { private var lowBatColor = Color.RED private var bgColor = Color.WHITE - val CUSTOM_VERSION = "v0.1" + val CUSTOM_VERSION = "v0.2" @Suppress("DEPRECATION") override fun inflateLayout(inflater: LayoutInflater): ViewBinding { From d06a3c3e534063b67b68e6e98b3b7a494939aa57 Mon Sep 17 00:00:00 2001 From: Philoul Date: Sat, 12 Aug 2023 20:02:13 +0200 Subject: [PATCH 15/18] Fix remaining freetext in v0.1 watchfaces --- .../info/nightscout/rx/weardata/CustomWatchfaceFormat.kt | 1 + .../nightscout/androidaps/watchfaces/CustomWatchface.kt | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt index 3cc98232ac..15f3c79e7f 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt @@ -17,6 +17,7 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream +val CUSTOM_VERSION = "v0.2" enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon: Int?, val fileName: String) { UNKNOWN("unknown", null, "Unknown"), CUSTOM_WATCHFACE("customWatchface", R.drawable.watchface_custom, "CustomWatchface"), diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index 5df2e0799b..d68ca3a619 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -28,6 +28,7 @@ import androidx.viewbinding.ViewBinding import info.nightscout.androidaps.R import info.nightscout.androidaps.databinding.ActivityCustomBinding import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace +import info.nightscout.rx.weardata.CUSTOM_VERSION import info.nightscout.rx.weardata.CustomWatchfaceData import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap @@ -54,7 +55,6 @@ class CustomWatchface : BaseWatchFace() { private var lowBatColor = Color.RED private var bgColor = Color.WHITE - val CUSTOM_VERSION = "v0.2" @Suppress("DEPRECATION") override fun inflateLayout(inflater: LayoutInflater): ViewBinding { @@ -168,6 +168,7 @@ class CustomWatchface : BaseWatchFace() { ) if (viewjson.has("fontColor")) view.setTextColor(getColor(viewjson.getString("fontColor"))) + if (viewjson.has("textvalue")) view.text = viewjson.getString("textvalue") } @@ -188,6 +189,11 @@ class CustomWatchface : BaseWatchFace() { view.clearColorFilter() } } + } else { + view.visibility = View.GONE + if (view is TextView) { + view.text = "" + } } } } From 3349017ae07ea52c7d362d8fdbb213e83a0a6cec Mon Sep 17 00:00:00 2001 From: Philoul Date: Sun, 13 Aug 2023 14:42:07 +0200 Subject: [PATCH 16/18] Check Custom Watchface version --- .../rx/weardata/CustomWatchfaceFormat.kt | 2 +- .../CustomWatchfaceImportListActivity.kt | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt index 15f3c79e7f..bfe8982526 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt @@ -17,7 +17,7 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -val CUSTOM_VERSION = "v0.2" +val CUSTOM_VERSION = "0.2" enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon: Int?, val fileName: String) { UNKNOWN("unknown", null, "Unknown"), CUSTOM_WATCHFACE("customWatchface", R.drawable.watchface_custom, "CustomWatchface"), diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt index d0d07de34b..0a900c8561 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt @@ -14,12 +14,15 @@ import info.nightscout.interfaces.maintenance.PrefFileListProvider import info.nightscout.configuration.databinding.CustomWatchfaceImportListActivityBinding import info.nightscout.configuration.R import info.nightscout.configuration.databinding.CustomWatchfaceImportListItemBinding +import info.nightscout.interfaces.versionChecker.VersionCheckerUtils import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventMobileDataToWear import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CUSTOM_VERSION import info.nightscout.rx.weardata.CustomWatchfaceData import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey.* +import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap import info.nightscout.rx.weardata.EventData import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.sharedPreferences.SP @@ -32,10 +35,10 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { @Inject lateinit var prefFileListProvider: PrefFileListProvider @Inject lateinit var rxBus: RxBus @Inject lateinit var aapsLogger: AAPSLogger + @Inject lateinit var versionCheckerUtils: VersionCheckerUtils private lateinit var binding: CustomWatchfaceImportListActivityBinding - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = CustomWatchfaceImportListActivityBinding.inflate(layoutInflater) @@ -63,7 +66,6 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile) val i = Intent() setResult(FragmentActivity.RESULT_OK, i) - //rxBus.send(EventWearUpdateGui(customWatchfaceFile)) rxBus.send(EventMobileDataToWear(customWF)) finish() } @@ -93,7 +95,8 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { author.text = rh.gs(CWF_AUTHOR.label, metadata[CWF_AUTHOR] ?:"") createdAt.text = rh.gs(CWF_CREATED_AT.label, metadata[CWF_CREATED_AT] ?:"") cwfVersion.text = rh.gs(CWF_VERSION.label, metadata[CWF_VERSION] ?:"") - + val colorAttr = if (checkCustomVersion(metadata)) info.nightscout.core.ui.R.attr.metadataTextOkColor else info.nightscout.core.ui.R.attr.metadataTextWarningColor + cwfVersion.setTextColor(rh.gac(cwfVersion.context, colorAttr)) } } } @@ -106,4 +109,13 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { return super.onOptionsItemSelected(item) } + private fun checkCustomVersion(metadata: CustomWatchfaceMetadataMap): Boolean { + metadata[CWF_VERSION]?.let { version -> + val currentAppVer = versionCheckerUtils.versionDigits(CUSTOM_VERSION) + val metadataVer = versionCheckerUtils.versionDigits(version) + //Only check that Loaded Watchface version is lower or equal to Wear CustomWatchface version + return ((currentAppVer.size >= 2) && (metadataVer.size >= 2) && (currentAppVer[0] >= metadataVer[0])) + } + return false + } } \ No newline at end of file From f4f79b12205b863c8930cb28c5d41a6497d950e3 Mon Sep 17 00:00:00 2001 From: Philoul Date: Mon, 14 Aug 2023 16:51:51 +0200 Subject: [PATCH 17/18] Add vector arrows for precise direction positioning --- .../androidaps/watchfaces/CustomWatchface.kt | 28 +++++++++++++++++++ wear/src/main/res/drawable/ic_doubledown.xml | 12 ++++++++ wear/src/main/res/drawable/ic_doubleup.xml | 12 ++++++++ wear/src/main/res/drawable/ic_flat.xml | 9 ++++++ .../main/res/drawable/ic_fortyfivedown.xml | 9 ++++++ wear/src/main/res/drawable/ic_fortyfiveup.xml | 9 ++++++ wear/src/main/res/drawable/ic_invalid.xml | 18 ++++++++++++ wear/src/main/res/drawable/ic_singledown.xml | 9 ++++++ wear/src/main/res/drawable/ic_singleup.xml | 9 ++++++ wear/src/main/res/layout/activity_custom.xml | 11 ++++++++ 10 files changed, 126 insertions(+) create mode 100644 wear/src/main/res/drawable/ic_doubledown.xml create mode 100644 wear/src/main/res/drawable/ic_doubleup.xml create mode 100644 wear/src/main/res/drawable/ic_flat.xml create mode 100644 wear/src/main/res/drawable/ic_fortyfivedown.xml create mode 100644 wear/src/main/res/drawable/ic_fortyfiveup.xml create mode 100644 wear/src/main/res/drawable/ic_invalid.xml create mode 100644 wear/src/main/res/drawable/ic_singledown.xml create mode 100644 wear/src/main/res/drawable/ic_singleup.xml diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt index d68ca3a619..a5c8be2026 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -19,6 +19,7 @@ import android.view.WindowManager import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.annotation.DrawableRes import androidx.annotation.IdRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat @@ -74,11 +75,16 @@ class CustomWatchface : BaseWatchFace() { .build() } + override fun setDataFields() { + super.setDataFields() + binding.direction2.setImageDrawable(resources.getDrawable(TrendArrow.fromSymbol(singleBg.slopeArrow).icon)) + } override fun setColorDark() { setWatchfaceStyle() binding.mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.dark_background)) binding.sgv.setTextColor(bgColor) binding.direction.setTextColor(bgColor) + binding.direction2.colorFilter = changeDrawableColor(bgColor) if (ageLevel != 1) binding.timestamp.setTextColor(ContextCompat.getColor(this, R.color.dark_TimestampOld)) @@ -429,6 +435,7 @@ class CustomWatchface : BaseWatchFace() { MONTH("month", R.id.month, null), LOOP("loop", R.id.loop, R.string.key_show_external_status), DIRECTION("direction", R.id.direction, R.string.key_show_direction), + DIRECTION2("direction2", R.id.direction2, R.string.key_show_direction), TIMESTAMP("timestamp", R.id.timestamp, R.string.key_show_ago), SGV("sgv", R.id.sgv, R.string.key_show_bg), COVER_PLATE(CustomWatchfaceDrawableDataKey.COVERPLATE.key, R.id.cover_plate, null), @@ -460,8 +467,29 @@ class CustomWatchface : BaseWatchFace() { } } + fun visibility(sp: SP): Boolean = this.pref?.let { sp.getBoolean(it, true) } ?: true } + + enum class TrendArrow(val text: String, val symbol: String,@DrawableRes val icon: Int) { + NONE("NONE", "??", R.drawable.ic_invalid), + TRIPLE_UP("TripleUp", "X", R.drawable.ic_invalid), + DOUBLE_UP("DoubleUp", "\u21c8", R.drawable.ic_doubleup), + SINGLE_UP("SingleUp", "\u2191", R.drawable.ic_singleup), + FORTY_FIVE_UP("FortyFiveUp", "\u2197", R.drawable.ic_fortyfiveup), + FLAT("Flat", "\u2192", R.drawable.ic_flat), + FORTY_FIVE_DOWN("FortyFiveDown", "\u2198",R.drawable.ic_fortyfivedown), + SINGLE_DOWN("SingleDown", "\u2193", R.drawable.ic_singledown), + DOUBLE_DOWN("DoubleDown", "\u21ca", R.drawable.ic_doubledown), + TRIPLE_DOWN("TripleDown", "X",R.drawable.ic_invalid) + ; + + companion object { + fun fromSymbol(direction: String?) = + values().firstOrNull { it.symbol == direction } ?: NONE + } + } + } diff --git a/wear/src/main/res/drawable/ic_doubledown.xml b/wear/src/main/res/drawable/ic_doubledown.xml new file mode 100644 index 0000000000..6bccc9cb47 --- /dev/null +++ b/wear/src/main/res/drawable/ic_doubledown.xml @@ -0,0 +1,12 @@ + + + + diff --git a/wear/src/main/res/drawable/ic_doubleup.xml b/wear/src/main/res/drawable/ic_doubleup.xml new file mode 100644 index 0000000000..9c56d4cf85 --- /dev/null +++ b/wear/src/main/res/drawable/ic_doubleup.xml @@ -0,0 +1,12 @@ + + + + diff --git a/wear/src/main/res/drawable/ic_flat.xml b/wear/src/main/res/drawable/ic_flat.xml new file mode 100644 index 0000000000..487a647f10 --- /dev/null +++ b/wear/src/main/res/drawable/ic_flat.xml @@ -0,0 +1,9 @@ + + + diff --git a/wear/src/main/res/drawable/ic_fortyfivedown.xml b/wear/src/main/res/drawable/ic_fortyfivedown.xml new file mode 100644 index 0000000000..673a965fe4 --- /dev/null +++ b/wear/src/main/res/drawable/ic_fortyfivedown.xml @@ -0,0 +1,9 @@ + + + diff --git a/wear/src/main/res/drawable/ic_fortyfiveup.xml b/wear/src/main/res/drawable/ic_fortyfiveup.xml new file mode 100644 index 0000000000..930857ec66 --- /dev/null +++ b/wear/src/main/res/drawable/ic_fortyfiveup.xml @@ -0,0 +1,9 @@ + + + diff --git a/wear/src/main/res/drawable/ic_invalid.xml b/wear/src/main/res/drawable/ic_invalid.xml new file mode 100644 index 0000000000..bdda131e7d --- /dev/null +++ b/wear/src/main/res/drawable/ic_invalid.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/wear/src/main/res/drawable/ic_singledown.xml b/wear/src/main/res/drawable/ic_singledown.xml new file mode 100644 index 0000000000..bc3a0e7501 --- /dev/null +++ b/wear/src/main/res/drawable/ic_singledown.xml @@ -0,0 +1,9 @@ + + + diff --git a/wear/src/main/res/drawable/ic_singleup.xml b/wear/src/main/res/drawable/ic_singleup.xml new file mode 100644 index 0000000000..6dda71fc74 --- /dev/null +++ b/wear/src/main/res/drawable/ic_singleup.xml @@ -0,0 +1,9 @@ + + + diff --git a/wear/src/main/res/layout/activity_custom.xml b/wear/src/main/res/layout/activity_custom.xml index 553dc1fdae..a746ba2a8a 100644 --- a/wear/src/main/res/layout/activity_custom.xml +++ b/wear/src/main/res/layout/activity_custom.xml @@ -306,12 +306,23 @@ android:layout_height="52px" android:layout_marginTop="26px" android:layout_marginLeft="291px" + android:visibility="gone" android:gravity="left" android:textSize="39px" android:textStyle="bold" android:textColor="@color/light_grey" tools:text="--" /> + + Date: Mon, 14 Aug 2023 16:52:38 +0200 Subject: [PATCH 18/18] CustomWatchface version 0.3 --- .../java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt index bfe8982526..7c855468bb 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt @@ -17,7 +17,7 @@ import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -val CUSTOM_VERSION = "0.2" +val CUSTOM_VERSION = "0.3" enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon: Int?, val fileName: String) { UNKNOWN("unknown", null, "Unknown"), CUSTOM_WATCHFACE("customWatchface", R.drawable.watchface_custom, "CustomWatchface"),