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 46894fbecd..233b7dd1e1 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..ba643694a3
--- /dev/null
+++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt
@@ -0,0 +1,5 @@
+package info.nightscout.rx.events
+
+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
new file mode 100644
index 0000000000..7c855468bb
--- /dev/null
+++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt
@@ -0,0 +1,241 @@
+package info.nightscout.rx.weardata
+
+import android.content.res.Resources
+import android.graphics.BitmapFactory
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+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
+
+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"),
+ 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.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
+ }
+ }
+}
+
+typealias CustomWatchfaceDrawableDataMap = MutableMap
+typealias CustomWatchfaceMetadataMap = MutableMap
+
+@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);
+
+ 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
+ }
+
+ }
+
+}
+
+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 59c8bc6fa7..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,8 +1,10 @@
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
import org.joda.time.DateTime
import java.util.Objects
@@ -13,13 +15,20 @@ 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) {
+ Error(System.currentTimeMillis())
+ }
}
// Mobile <- Wear
@@ -142,6 +151,12 @@ sealed class EventData : Event() {
@Serializable
data class CancelNotification(val timeStamp: Long) : EventData()
+ @Serializable
+ data class ActionGetCustomWatchface(
+ val customWatchface: ActionSetCustomWatchface,
+ val exportFile: Boolean = false
+ ) : EventData()
+
@Serializable
data class ActionPing(val timeStamp: Long) : EventData()
@@ -267,6 +282,16 @@ sealed class EventData : Event() {
val validTo: Int
) : EventData()
}
+ @Serializable
+ data class ActionSetCustomWatchface(
+ val customWatchfaceData: CustomWatchfaceData
+ ) : 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 6fcef97f4e..c24523b3c6 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 0000000000..9678177c64
Binary files /dev/null and b/app-wear-shared/shared/src/main/res/drawable/watchface_custom.png differ
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 9f363f5cfc..b153a87526 100644
--- a/app-wear-shared/shared/src/main/res/values/strings.xml
+++ b/app-wear-shared/shared/src/main/res/values/strings.xml
@@ -39,4 +39,12 @@
Disconnecting
Waiting for disconnection
+
+ Created at: %1$s
+ Author: %1$s
+ Name: %1$s
+ File 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/ImportExportPrefs.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt
index 18ea0f32d3..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,12 +2,16 @@ package info.nightscout.interfaces.maintenance
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
+import info.nightscout.rx.weardata.CustomWatchfaceData
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: 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 8bf0d497dd..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 {
@@ -10,7 +11,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/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 @@
+
() {
+
+ 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..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
@@ -55,6 +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.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
@@ -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: CustomWatchfaceData) {
+ prefFileList.ensureExportDirExists()
+ val newFile = prefFileList.newCwfFile(customWatchface.metadata[CustomWatchfaceMetadataKey.CWF_FILENAME] ?:"")
+ ZipWatchfaceFormat.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..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
@@ -17,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
@@ -88,6 +90,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(ZipWatchfaceFormat.CUSTOM_WF_EXTENTION) }.forEach { file ->
+ // Here loadCustomWatchface will unzip, check and load CustomWatchface
+ ZipWatchfaceFormat.loadCustomWatchface(file)?.also { customWatchface ->
+ customWatchfaceFiles.add(customWatchface)
+ }
+ }
+
+ return customWatchfaceFiles
+ }
+
private fun metadataFor(loadMetadata: Boolean, contents: String): PrefMetadataMap {
if (!loadMetadata) {
return mapOf()
@@ -128,6 +144,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${ZipWatchfaceFormat.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..0a900c8561
--- /dev/null
+++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt
@@ -0,0 +1,121 @@
+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.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
+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
+ @Inject lateinit var versionCheckerUtils: VersionCheckerUtils
+
+ 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 CustomWatchfaceData
+ val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile)
+ val i = Intent()
+ setResult(FragmentActivity.RESULT_OK, i)
+ rxBus.send(EventMobileDataToWear(customWF))
+ 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.drawableDatas[CustomWatchfaceDrawableDataKey
+ .CUSTOM_WATCHFACE]?.toDrawable(resources)
+ with(holder.customWatchfaceImportListItemBinding) {
+ filelistName.text = rh.gs(info.nightscout.shared.R.string.metadata_wear_import_filename, metadata[CWF_FILENAME])
+ filelistName.tag = customWatchfaceFile
+ customWatchface.setImageDrawable(drawable)
+ 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] ?:"")
+ 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))
+ }
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ finish()
+ return true
+ }
+ 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
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..be5a37623d
--- /dev/null
+++ b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/configuration/src/main/res/values/strings.xml b/plugins/configuration/src/main/res/values/strings.xml
index 1cec48dfba..3ada4ae626 100644
--- a/plugins/configuration/src/main/res/values/strings.xml
+++ b/plugins/configuration/src/main/res/values/strings.xml
@@ -164,6 +164,9 @@
Missing encryption configuration, settings format is invalid!
Unsupported or not specified encryption algorithm!
+
+ Select Custom Watchface
+
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..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
@@ -5,13 +5,24 @@ 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.CustomWatchfaceData
+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
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
@@ -24,9 +35,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 +57,20 @@ 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 {
+ rxBus.send(EventMobileToWear(EventData.ActionrequestSetDefaultWatchface(dateUtil.now())))
+ updateGui()
+ }
+ binding.exportCustom.setOnClickListener {
+ wearPlugin.savedCustomWatchface?.let { importExportPrefs.exportCustomWatchface(it) }
+ ?: apply { rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(true)))}
+ }
}
override fun onResume() {
@@ -50,7 +78,15 @@ class WearFragment : DaggerFragment() {
disposable += rxBus
.toObservable(EventWearUpdateGui::class.java)
.observeOn(aapsSchedulers.main)
- .subscribe({ updateGui() }, fabricPrivacy::logException)
+ .subscribe({
+ it.customWatchfaceData?.let { loadCustom(it) }
+ if (it.exportFile)
+ ToastUtils.okToast(activity, rh.gs(R.string.wear_new_custom_watchface_exported))
+ updateGui()
+ }, fabricPrivacy::logException)
+ if (wearPlugin.savedCustomWatchface == null)
+ rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(false)))
+ //EventMobileDataToWear
updateGui()
}
@@ -67,6 +103,18 @@ class WearFragment : DaggerFragment() {
private fun updateGui() {
_binding ?: return
+ wearPlugin.savedCustomWatchface?.let {
+ binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.metadata[CustomWatchfaceMetadataKey.CWF_NAME])
+ 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.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: CustomWatchfaceData) {
+ 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..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,7 +17,9 @@ 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
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
@@ -54,6 +56,7 @@ class WearPlugin @Inject constructor(
private val disposable = CompositeDisposable()
var connectedDevice = "---"
+ var savedCustomWatchface: CustomWatchfaceData? = null
override fun onStart() {
super.onStart()
@@ -89,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/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
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..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
@@ -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,7 @@ 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.EventWearUpdateGui
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.rx.weardata.EventData
@@ -107,7 +109,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 +317,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 +1257,15 @@ 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.customWatchfaceData.json}")
+ rxBus.send(EventWearUpdateGui(customWatchface.customWatchfaceData, command.exportFile))
+ if (command.exportFile)
+ 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 13a8ff47f9..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
@@ -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
@@ -44,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() {
@@ -80,7 +82,8 @@ 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)
+ @ExperimentalSerializationApi
override fun onCreate() {
AndroidInjection.inject(this)
super.onCreate()
@@ -90,6 +93,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) {
@@ -125,7 +132,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() {
}
super.onDataChanged(dataEvents)
}
-
+ @ExperimentalSerializationApi
override fun onMessageReceived(messageEvent: MessageEvent) {
super.onMessageReceived(messageEvent)
@@ -136,6 +143,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 +176,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 +213,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..8e8b421c3d 100644
--- a/plugins/main/src/main/res/layout/wear_fragment.xml
+++ b/plugins/main/src/main/res/layout/wear_fragment.xml
@@ -1,42 +1,199 @@
-
+ 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_height="match_parent"
+ android:orientation="vertical">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..0cb11d304e 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
+ Custom watchface exported
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..fb16c6a5e6 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,35 @@ 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)
+ persistence.readCustomWatchface()?.let {
+ rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false)))
+ }
+ }
+ disposable += rxBus
+ .toObservable(EventData.ActionrequestSetDefaultWatchface::class.java)
+ .observeOn(aapsSchedulers.io)
+ .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)
+ .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..7a1c894b49 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
@@ -21,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() {
@@ -43,7 +45,8 @@ 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)
+ @ExperimentalSerializationApi
override fun onCreate() {
AndroidInjection.inject(this)
super.onCreate()
@@ -54,6 +57,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) {
@@ -87,7 +96,7 @@ class DataLayerListenerServiceWear : WearableListenerService() {
}
super.onDataChanged(dataEvents)
}
-
+ @ExperimentalSerializationApi
override fun onMessageReceived(messageEvent: MessageEvent) {
super.onMessageReceived(messageEvent)
@@ -100,6 +109,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..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
@@ -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.customWatchfaceData} ${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..a5c8be2026
--- /dev/null
+++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt
@@ -0,0 +1,495 @@
+@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.annotation.DrawableRes
+import androidx.annotation.IdRes
+import androidx.annotation.StringRes
+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.CUSTOM_VERSION
+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 info.nightscout.shared.extensions.toVisibilityKeepSpace
+import info.nightscout.shared.sharedPreferences.SP
+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
+
+
+ @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 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))
+ 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 (showSecond) ":${dateUtil.secondString()}" else ""
+ binding.second.text = dateUtil.secondString()
+ // rotate the second hand.
+ binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f
+ //aapsLogger.debug("XXXXX SetSecond $watchModeString")
+ }
+
+ override fun updateSecondVisibility() {
+ binding.second.visibility = showSecond.toVisibility()
+ binding.secondHand.visibility = showSecond.toVisibility()
+ }
+
+ private fun setWatchfaceStyle() {
+ val customWatchface = persistence.readCustomWatchface() ?: persistence.readCustomWatchface(true)
+ customWatchface?.let {
+ 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
+ }
+
+ binding.mainLayout.forEach { view ->
+ 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
+ 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"), 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())
+ 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 (viewjson.has("textvalue"))
+ view.text = viewjson.getString("textvalue")
+ }
+
+ if (view is ImageView) {
+ view.clearColorFilter()
+ 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(id.key).icon?.let { context.getDrawable(it) })
+ if (viewjson.has("color"))
+ view.setColorFilter(getColor(viewjson.getString("color")))
+ else
+ view.clearColorFilter()
+ }
+ }
+ } else {
+ view.visibility = View.GONE
+ if (view is TextView) {
+ view.text = ""
+ }
+ }
+ }
+ }
+ updateSecondVisibility()
+ } catch (e:Exception) {
+ persistence.store(defaultWatchface(), false) // relaod correct values to avoid crash of watchface
+ }
+ }
+ }
+
+ 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(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
+ 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)
+ val drawableDataMap: CustomWatchfaceDrawableDataMap = mutableMapOf()
+ getResourceByteArray(info.nightscout.shared.R.drawable.watchface_custom)?.let {
+ val drawableData = DrawableData(it,DrawableFormat.PNG)
+ drawableDataMap[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE] = drawableData
+ }
+ return EventData.ActionSetCustomWatchface(CustomWatchfaceData(json.toString(4), metadataMap, drawableDataMap))
+ }
+
+ 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, pref: Boolean = true): Int = when (visibility) {
+ "visible" -> pref.toVisibility()
+ "invisible" -> pref.toVisibilityKeepSpace()
+ "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"
+ }
+
+ 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()
+
+ 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)
+
+ 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
+ )
+ )
+ )
+ return ColorMatrixColorFilter(colorMatrix)
+ }
+
+ private fun getColor(color: String): Int {
+ if (color == "bgColor")
+ return bgColor
+ else
+ return try {
+ Color.parseColor(color)
+ } catch (e: Exception) {
+ Color.GRAY
+ }
+ }
+
+ 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),
+ 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),
+ 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),
+ 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),
+ 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
+ }
+
+
+ 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/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt
index a1cc53eab9..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
@@ -78,12 +78,16 @@ 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
+ val showSecond: Boolean
+ get() = enableSecond && currentWatchMode == WatchMode.INTERACTIVE
// Tapping times
private var sgvTapTime: Long = 0
@@ -250,7 +254,7 @@ abstract class BaseWatchFace : WatchFace() {
}
override fun getInteractiveModeUpdateRate(): Long {
- return 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) {
@@ -271,6 +275,8 @@ abstract class BaseWatchFace : WatchFace() {
missedReadingAlert()
checkVibrateHourly(oldTime, newTime)
if (!simpleUi.isEnabled(currentWatchMode)) setDataFields()
+ } else if (layoutSet && !simpleUi.isEnabled(currentWatchMode) && showSecond && newTime.hasSecondChanged(oldTime)) {
+ setSecond()
}
}
@@ -358,9 +364,20 @@ abstract class BaseWatchFace : WatchFace() {
binding.month?.text = dateUtil.monthString()
binding.timePeriod?.visibility = android.text.format.DateFormat.is24HourFormat(this).not().toVisibility()
binding.timePeriod?.text = dateUtil.amPm()
+ if (showSecond)
+ setSecond()
}
- private fun setColor() {
+ open fun setSecond() {
+ 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 {
lowResMode -> setColorLowRes()
@@ -378,9 +395,12 @@ 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()
+ else
+ setDataFields()
invalidate()
}
@@ -409,12 +429,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/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
new file mode 100644
index 0000000000..a746ba2a8a
--- /dev/null
+++ b/wear/src/main/res/layout/activity_custom.xml
@@ -0,0 +1,391 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+