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 @@ + + + + + + + + + +