diff --git a/_docs/icons/HourHand.svg b/_docs/icons/HourHand.svg new file mode 100644 index 0000000000..421b2d37a7 --- /dev/null +++ b/_docs/icons/HourHand.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/_docs/icons/MinuteHand.svg b/_docs/icons/MinuteHand.svg new file mode 100644 index 0000000000..07e4f81fb1 --- /dev/null +++ b/_docs/icons/MinuteHand.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/_docs/icons/SecondHand.svg b/_docs/icons/SecondHand.svg new file mode 100644 index 0000000000..0d66040491 --- /dev/null +++ b/_docs/icons/SecondHand.svg @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/_docs/icons/SimplifiedDial.svg b/_docs/icons/SimplifiedDial.svg new file mode 100644 index 0000000000..fbce895e01 --- /dev/null +++ b/_docs/icons/SimplifiedDial.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_docs/icons/background.svg b/_docs/icons/background.svg new file mode 100644 index 0000000000..ea30c0c8a5 --- /dev/null +++ b/_docs/icons/background.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/_docs/icons/detailed_dial.svg b/_docs/icons/detailed_dial.svg new file mode 100644 index 0000000000..3c079740f0 --- /dev/null +++ b/_docs/icons/detailed_dial.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_docs/icons/export_custom.svg b/_docs/icons/export_custom.svg new file mode 100644 index 0000000000..b4c4d12ae8 --- /dev/null +++ b/_docs/icons/export_custom.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_docs/icons/load_custom.svg b/_docs/icons/load_custom.svg new file mode 100644 index 0000000000..90fe7bfe37 --- /dev/null +++ b/_docs/icons/load_custom.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/_docs/icons/send_custom.svg b/_docs/icons/send_custom.svg new file mode 100644 index 0000000000..70168f1472 --- /dev/null +++ b/_docs/icons/send_custom.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/_docs/icons/set_default.svg b/_docs/icons/set_default.svg new file mode 100644 index 0000000000..90e36585a2 --- /dev/null +++ b/_docs/icons/set_default.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/app-wear-shared/shared/build.gradle b/app-wear-shared/shared/build.gradle index 5e24d768e0..e69dd02310 100644 --- a/app-wear-shared/shared/build.gradle +++ b/app-wear-shared/shared/build.gradle @@ -34,6 +34,7 @@ dependencies { api 'com.github.tony19:logback-android:2.0.0' api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_version" + api "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinx_serialization_version" api "org.apache.commons:commons-lang3:$commonslang3_version" //RxBus diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventMobileDataToWear.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventMobileDataToWear.kt new file mode 100644 index 0000000000..9cc327ae92 --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventMobileDataToWear.kt @@ -0,0 +1,5 @@ +package info.nightscout.rx.events + +import info.nightscout.rx.weardata.EventData + +class EventMobileDataToWear(val payload: EventData.ActionSetCustomWatchface) : Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearCwfExported.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearCwfExported.kt new file mode 100644 index 0000000000..1d95780241 --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearCwfExported.kt @@ -0,0 +1,5 @@ +package info.nightscout.rx.events + +import info.nightscout.rx.weardata.EventData + +class EventWearCwfExported(val payload: EventData.ActionSetCustomWatchface): Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearDataToMobile.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearDataToMobile.kt new file mode 100644 index 0000000000..c26c8fd8b6 --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearDataToMobile.kt @@ -0,0 +1,5 @@ +package info.nightscout.rx.events + +import info.nightscout.rx.weardata.EventData + +class EventWearDataToMobile(val payload: EventData) : Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt new file mode 100644 index 0000000000..7aae63204a --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/events/EventWearUpdateGui.kt @@ -0,0 +1,3 @@ +package info.nightscout.rx.events + +class EventWearUpdateGui : Event() \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt new file mode 100644 index 0000000000..cef6aa181a --- /dev/null +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/CustomWatchfaceFormat.kt @@ -0,0 +1,140 @@ +package info.nightscout.rx.weardata + +import android.content.Context +import android.content.res.Resources +import android.graphics.BitmapFactory +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Parcelable +import android.util.Xml +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import info.nightscout.shared.R +import kotlinx.serialization.Serializable +import org.json.JSONObject +import java.io.File + +enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon: Int?, val fileName: String) { + UNKNOWN("unknown", null,"Unknown"), + CUSTOM_WATCHFACE("customWatchface", R.drawable.watchface_custom, "CustomWatchface"), + BACKGROUND("background", R.drawable.background, "Background"), + COVERCHART("cover_chart", null,"CoverChart"), + COVERPLATE("cover_plate", R.drawable.simplified_dial, "CoverPlate"), + HOURHAND("hour_hand", R.drawable.hour_hand,"HourHand"), + MINUTEHAND("minute_hand", R.drawable.minute_hand,"MinuteHand"), + SECONDHAND("second_hand", R.drawable.second_hand, "SecondHand"); + + companion object { + + private val keyToEnumMap = HashMap() + private val fileNameToEnumMap = HashMap() + + init { + for (value in values()) keyToEnumMap[value.key] = value + for (value in values()) fileNameToEnumMap[value.fileName] = value + } + + fun fromKey(key: String): CustomWatchfaceDrawableDataKey = + if (keyToEnumMap.containsKey(key)) { + keyToEnumMap[key] ?:UNKNOWN + } else { + UNKNOWN + } + + fun fromFileName(file: String): CustomWatchfaceDrawableDataKey = + if (fileNameToEnumMap.containsKey(file.substringBeforeLast("."))) { + fileNameToEnumMap[file.substringBeforeLast(".")] ?:UNKNOWN + } else { + UNKNOWN + } + } + +} + +enum class DrawableFormat(val extension: String) { + UNKNOWN(""), + //XML("xml"), + //svg("svg"), + JPG("jpg"), + PNG("png"); + + companion object { + + private val extensionToEnumMap = HashMap() + + init { + for (value in values()) extensionToEnumMap[value.extension] = value + } + + fun fromFileName(fileName: String): DrawableFormat = + if (extensionToEnumMap.containsKey(fileName.substringAfterLast("."))) { + extensionToEnumMap[fileName.substringAfterLast(".")] ?:UNKNOWN + } else { + UNKNOWN + } + } +} + +@Serializable +data class DrawableData(val value: ByteArray, val format: DrawableFormat) { + + fun toDrawable(resources: Resources): Drawable? { + try { + return when (format) { + DrawableFormat.PNG, DrawableFormat.JPG -> { + val bitmap = BitmapFactory.decodeByteArray(value, 0, value.size) + BitmapDrawable(resources, bitmap) + } +/* + DrawableFormat.XML -> { + val xmlInputStream = ByteArrayInputStream(value) + val xmlPullParser = Xml.newPullParser() + xmlPullParser.setInput(xmlInputStream, null) + Drawable.createFromXml(resources, xmlPullParser) + } +*/ + else -> null + } + } catch (e: Exception) { + return null + } + } +} + +typealias CustomWatchfaceDrawableDataMap = MutableMap +typealias CustomWatchfaceMetadataMap = MutableMap + +data class CustomWatchface(val json: String, var metadata: CustomWatchfaceMetadataMap, val drawableDatas: CustomWatchfaceDrawableDataMap) + +interface CustomWatchfaceFormat { + + fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface) + fun loadCustomWatchface(cwfFile: File): CustomWatchface? + fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap +} + +enum class CustomWatchfaceMetadataKey(val key: String, @StringRes val label: Int) { + + CWF_NAME("name", R.string.metadata_label_watchface_name), + CWF_AUTHOR("author", R.string.metadata_label_watchface_author), + CWF_CREATED_AT("created_at", R.string.metadata_label_watchface_created_at), + CWF_VERSION("cwf_version", R.string.metadata_label_watchface_version); + + companion object { + + private val keyToEnumMap = HashMap() + + init { + for (value in values()) keyToEnumMap[value.key] = value + } + + fun fromKey(key: String): CustomWatchfaceMetadataKey? = + if (keyToEnumMap.containsKey(key)) { + keyToEnumMap[key] + } else { + null + } + + } + +} \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt index 59c8bc6fa7..88bf4fa1bc 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/rx/weardata/EventData.kt @@ -3,6 +3,7 @@ package info.nightscout.rx.weardata import info.nightscout.rx.events.Event import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.protobuf.ProtoBuf import org.joda.time.DateTime import java.util.Objects @@ -13,6 +14,7 @@ sealed class EventData : Event() { fun serialize() = Json.encodeToString(serializer(), this) + fun serializeByte() = ProtoBuf.encodeToByteArray(serializer(), this) companion object { fun deserialize(json: String) = try { @@ -20,6 +22,12 @@ sealed class EventData : Event() { } catch (ignored: Exception) { Error(System.currentTimeMillis()) } + + fun deserializeByte(byteArray: ByteArray) = try { + ProtoBuf.decodeFromByteArray(serializer(), byteArray) + } catch (ignored: Exception) { + Error(System.currentTimeMillis()) + } } // Mobile <- Wear @@ -142,6 +150,12 @@ sealed class EventData : Event() { @Serializable data class CancelNotification(val timeStamp: Long) : EventData() + @Serializable + data class ActionGetCustomWatchface( + val customWatchface: ActionSetCustomWatchface, + val exportFile: Boolean + ) : EventData() + @Serializable data class ActionPing(val timeStamp: Long) : EventData() @@ -267,6 +281,18 @@ sealed class EventData : Event() { val validTo: Int ) : EventData() } + @Serializable + data class ActionSetCustomWatchface( + val name: String, + val json: String, + val drawableDataMap: CustomWatchfaceDrawableDataMap + ) : EventData() + + @Serializable + data class ActionrequestCustomWatchface(val exportFile: Boolean) : EventData() + + @Serializable + data class ActionrequestSetDefaultWatchface(val timeStamp: Long) : EventData() @Serializable data class ActionProfileSwitchOpenActivity(val timeShift: Int, val percentage: Int) : EventData() diff --git a/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt b/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt index 1ed083888d..7958f7403a 100644 --- a/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt +++ b/app-wear-shared/shared/src/main/java/info/nightscout/shared/utils/DateUtil.kt @@ -150,6 +150,10 @@ class DateUtil @Inject constructor(private val context: Context) { return DateTime(mills).toString(DateTimeFormat.forPattern(format)) } + fun secondString(): String = secondString(now()) + fun secondString(mills: Long): String = + DateTime(mills).toString(DateTimeFormat.forPattern("ss")) + fun minuteString(): String = minuteString(now()) fun minuteString(mills: Long): String = DateTime(mills).toString(DateTimeFormat.forPattern("mm")) diff --git a/app-wear-shared/shared/src/main/res/drawable/background.xml b/app-wear-shared/shared/src/main/res/drawable/background.xml new file mode 100644 index 0000000000..76226923e6 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/background.xml @@ -0,0 +1,9 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/detailed_dial.xml b/app-wear-shared/shared/src/main/res/drawable/detailed_dial.xml new file mode 100644 index 0000000000..fa5c92bde2 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/detailed_dial.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/hour_hand.xml b/app-wear-shared/shared/src/main/res/drawable/hour_hand.xml new file mode 100644 index 0000000000..c29364421b --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/hour_hand.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/minute_hand.xml b/app-wear-shared/shared/src/main/res/drawable/minute_hand.xml new file mode 100644 index 0000000000..ae37534d34 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/minute_hand.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/second_hand.xml b/app-wear-shared/shared/src/main/res/drawable/second_hand.xml new file mode 100644 index 0000000000..96464ba7a8 --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/second_hand.xml @@ -0,0 +1,4 @@ + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/simplified_dial.xml b/app-wear-shared/shared/src/main/res/drawable/simplified_dial.xml new file mode 100644 index 0000000000..63cca01bfb --- /dev/null +++ b/app-wear-shared/shared/src/main/res/drawable/simplified_dial.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/app-wear-shared/shared/src/main/res/drawable/watchface_custom.png b/app-wear-shared/shared/src/main/res/drawable/watchface_custom.png new file mode 100644 index 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..df0c6b7bfd 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 + + key_custom_watchface + Created at: %1$s + Author: %1$s + Name: %1$s + Watchface version: %1$s + Default Watchface + \ No newline at end of file diff --git a/app-wear-shared/shared/src/main/res/values/wear_paths.xml b/app-wear-shared/shared/src/main/res/values/wear_paths.xml index 36d7325849..377785cc8e 100644 --- a/app-wear-shared/shared/src/main/res/values/wear_paths.xml +++ b/app-wear-shared/shared/src/main/res/values/wear_paths.xml @@ -1,4 +1,5 @@ /rx_bridge + /rx_data_bridge \ No newline at end of file diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt new file mode 100644 index 0000000000..5019ae87e4 --- /dev/null +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/CustomWatchfaceFile.kt @@ -0,0 +1,21 @@ +package info.nightscout.interfaces.maintenance + +import android.os.Parcelable +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap +import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap +import info.nightscout.rx.weardata.DrawableData +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue +import java.io.File + +data class CustomWatchfaceFile( + val name: String, + val file: File, + val baseDir: File, + val json: String, + + val metadata: @RawValue CustomWatchfaceMetadataMap, + val drawableFiles: @RawValue CustomWatchfaceDrawableDataMap + +) \ No newline at end of file diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt index 18ea0f32d3..99657fe7c3 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/ImportExportPrefs.kt @@ -2,12 +2,16 @@ package info.nightscout.interfaces.maintenance import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import info.nightscout.rx.weardata.EventData interface ImportExportPrefs { fun importSharedPreferences(activity: FragmentActivity, importFile: PrefsFile) fun importSharedPreferences(activity: FragmentActivity) fun importSharedPreferences(fragment: Fragment) + fun importCustomWatchface(activity: FragmentActivity) + fun importCustomWatchface(fragment: Fragment) + fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface) fun prefsFileExists(): Boolean fun verifyStoragePermissions(fragment: Fragment, onGranted: Runnable) fun exportSharedPreferences(f: Fragment) diff --git a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt index 8bf0d497dd..9d131e9fdb 100644 --- a/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt +++ b/core/interfaces/src/main/java/info/nightscout/interfaces/maintenance/PrefFileListProvider.kt @@ -10,7 +10,9 @@ interface PrefFileListProvider { fun ensureExtraDirExists(): File fun newExportFile(): File fun newExportCsvFile(): File + fun newCwfFile(filename: String): File fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList + fun listCustomWatchfaceFiles(): MutableList fun checkMetadata(metadata: Map): Map fun formatExportedAgo(utcTime: String): String } \ No newline at end of file diff --git a/plugins/configuration/src/main/AndroidManifest.xml b/plugins/configuration/src/main/AndroidManifest.xml index 2d2811718b..700ebdafae 100644 --- a/plugins/configuration/src/main/AndroidManifest.xml +++ b/plugins/configuration/src/main/AndroidManifest.xml @@ -18,6 +18,10 @@ android:name=".maintenance.activities.PrefImportListActivity" android:exported="false" android:theme="@style/AppTheme" /> + () { + + companion object { + const val OUTPUT_PARAM = "custom_file" + } + + override fun parseResult(resultCode: Int, intent: Intent?): Unit? { + return when (resultCode) { + FragmentActivity.RESULT_OK -> Unit + else -> null + } + } + + override fun createIntent(context: Context, input: Void?): Intent { + return Intent(context, info.nightscout.configuration.maintenance.activities.CustomWatchfaceImportListActivity::class.java) + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt index 941a5b9cb3..567e61fca7 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/ImportExportPrefsImpl.kt @@ -22,6 +22,7 @@ import dagger.android.HasAndroidInjector import info.nightscout.configuration.R import info.nightscout.configuration.activities.DaggerAppCompatActivityWithResult import info.nightscout.configuration.maintenance.dialogs.PrefImportSummaryDialog +import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat import info.nightscout.core.ui.dialogs.OKDialog import info.nightscout.core.ui.dialogs.TwoMessagesAlertDialog @@ -55,6 +56,7 @@ import info.nightscout.rx.events.EventAppExit import info.nightscout.rx.events.EventDiaconnG8PumpLogReset import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag +import info.nightscout.rx.weardata.EventData import info.nightscout.shared.interfaces.ResourceHelper import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil @@ -84,7 +86,8 @@ class ImportExportPrefsImpl @Inject constructor( private val prefFileList: PrefFileListProvider, private val uel: UserEntryLogger, private val dateUtil: DateUtil, - private val uiInteraction: UiInteraction + private val uiInteraction: UiInteraction, + private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat ) : ImportExportPrefs { override fun prefsFileExists(): Boolean { @@ -297,6 +300,27 @@ class ImportExportPrefsImpl @Inject constructor( } } + override fun importCustomWatchface(fragment: Fragment) { + fragment.activity?.let { importCustomWatchface(it) } + } + override fun importCustomWatchface(activity: FragmentActivity) { + try { + if (activity is DaggerAppCompatActivityWithResult) + activity.callForCustomWatchfaceFile.launch(null) + } catch (e: IllegalArgumentException) { + // this exception happens on some early implementations of ActivityResult contracts + // when registered and called for the second time + ToastUtils.errorToast(activity, rh.gs(R.string.goto_main_try_again)) + log.error(LTag.CORE, "Internal android framework exception", e) + } + } + + override fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface) { + prefFileList.ensureExportDirExists() + val newFile = prefFileList.newCwfFile(customWatchface.name) + customWatchfaceCWFFormat.saveCustomWatchface(newFile,customWatchface) + } + override fun importSharedPreferences(activity: FragmentActivity, importFile: PrefsFile) { askToConfirmImport(activity, importFile) { password -> diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt index d060dc1304..b8d66e3bd5 100644 --- a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/PrefFileListProviderImpl.kt @@ -6,8 +6,10 @@ import dagger.Lazy import dagger.Reusable import info.nightscout.androidaps.annotations.OpenForTesting import info.nightscout.configuration.R +import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat import info.nightscout.interfaces.Config +import info.nightscout.interfaces.maintenance.CustomWatchfaceFile import info.nightscout.interfaces.maintenance.PrefFileListProvider import info.nightscout.interfaces.maintenance.PrefMetadata import info.nightscout.interfaces.maintenance.PrefMetadataMap @@ -34,6 +36,7 @@ class PrefFileListProviderImpl @Inject constructor( private val rh: ResourceHelper, private val config: Lazy, private val encryptedPrefsFormat: EncryptedPrefsFormat, + private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat, private val storage: Storage, private val versionCheckerUtils: VersionCheckerUtils, context: Context @@ -88,6 +91,20 @@ class PrefFileListProviderImpl @Inject constructor( return prefFiles } + override fun listCustomWatchfaceFiles(): MutableList { + val customWatchfaceFiles = mutableListOf() + + // searching dedicated dir, only for new CWF format + exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION) }.forEach { file -> + // Here loadCustomWatchface will unzip, check and load CustomWatchface + customWatchfaceCWFFormat.loadCustomWatchface(file)?.also { customWatchface -> + customWatchfaceFiles.add(CustomWatchfaceFile(file.name, file, exportsPath, customWatchface.json, customWatchface.metadata, customWatchface.drawableDatas)) + } + } + + return customWatchfaceFiles + } + private fun metadataFor(loadMetadata: Boolean, contents: String): PrefMetadataMap { if (!loadMetadata) { return mapOf() @@ -128,6 +145,10 @@ class PrefFileListProviderImpl @Inject constructor( val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss")) return File(exportsPath, timeLocal + "_UserEntry.csv") } + override fun newCwfFile(filename: String): File { + val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss")) + return File(exportsPath, "${filename}_$timeLocal${ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION}") + } // check metadata for known issues, change their status and add info with explanations override fun checkMetadata(metadata: Map): Map { diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt new file mode 100644 index 0000000000..d225ae790d --- /dev/null +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/activities/CustomWatchfaceImportListActivity.kt @@ -0,0 +1,112 @@ +package info.nightscout.configuration.maintenance.activities + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.ViewGroup + +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import info.nightscout.core.ui.activities.TranslatedDaggerAppCompatActivity +import info.nightscout.interfaces.maintenance.CustomWatchfaceFile +import info.nightscout.interfaces.maintenance.PrefFileListProvider +import info.nightscout.configuration.databinding.CustomWatchfaceImportListActivityBinding +import info.nightscout.configuration.R +import info.nightscout.configuration.databinding.CustomWatchfaceImportListItemBinding +import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventMobileDataToWear +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey.* +import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP +import javax.inject.Inject + +class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() { + + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var sp: SP + @Inject lateinit var prefFileListProvider: PrefFileListProvider + @Inject lateinit var rxBus: RxBus + @Inject lateinit var aapsLogger: AAPSLogger + + private lateinit var binding: CustomWatchfaceImportListActivityBinding + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = CustomWatchfaceImportListActivityBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + title = rh.gs(R.string.wear_import_custom_watchface_title) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(true) + + binding.recyclerview.layoutManager = LinearLayoutManager(this) + binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles()) + } + + inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List) : RecyclerView.Adapter() { + + inner class PrefFileViewHolder(val customWatchfaceImportListItemBinding: CustomWatchfaceImportListItemBinding) : RecyclerView.ViewHolder(customWatchfaceImportListItemBinding.root) { + + init { + with(customWatchfaceImportListItemBinding) { + root.isClickable = true + customWatchfaceImportListItemBinding.root.setOnClickListener { + val customWatchfaceFile = filelistName.tag as CustomWatchfaceFile + val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile.metadata[CWF_NAME] ?:"", customWatchfaceFile.json, customWatchfaceFile.drawableFiles) + sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWF.serialize()) + val i = Intent() + setResult(FragmentActivity.RESULT_OK, i) + rxBus.send(EventMobileDataToWear(customWF)) + aapsLogger.debug("XXXXX EventMobileDataToWear sent") + + finish() + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PrefFileViewHolder { + val binding = CustomWatchfaceImportListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return PrefFileViewHolder(binding) + } + + override fun getItemCount(): Int { + return customWatchfaceFileList.size + } + + override fun onBindViewHolder(holder: PrefFileViewHolder, position: Int) { + val customWatchfaceFile = customWatchfaceFileList[position] + val metadata = customWatchfaceFile.metadata + val drawable = customWatchfaceFile.drawableFiles[CustomWatchfaceDrawableDataKey + .CUSTOM_WATCHFACE]?.toDrawable(resources) + with(holder.customWatchfaceImportListItemBinding) { + filelistName.text = rh.gs(R.string.wear_import_filename, customWatchfaceFile.file.name) + filelistName.tag = customWatchfaceFile + customWatchface.setImageDrawable(drawable) + filelistDir.text = rh.gs(R.string.wear_import_directory, customWatchfaceFile.file.parentFile?.absolutePath) + customName.text = rh.gs(CWF_NAME.label, metadata[CWF_NAME]) + author.text = rh.gs(CWF_AUTHOR.label, metadata[CWF_AUTHOR] ?:"") + createdAt.text = rh.gs(CWF_CREATED_AT.label, metadata[CWF_CREATED_AT] ?:"") + cwfVersion.text = rh.gs(CWF_VERSION.label, metadata[CWF_VERSION] ?:"") + + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finish() + return true + } + return super.onOptionsItemSelected(item) + } + +} \ No newline at end of file diff --git a/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt new file mode 100644 index 0000000000..6414187d1a --- /dev/null +++ b/plugins/configuration/src/main/java/info/nightscout/configuration/maintenance/formats/ZipCustomWatchfaceFormat.kt @@ -0,0 +1,128 @@ +package info.nightscout.configuration.maintenance.formats + +import info.nightscout.core.utils.CryptoUtil +import info.nightscout.interfaces.storage.Storage +import info.nightscout.rx.weardata.CustomWatchface +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap +import info.nightscout.rx.weardata.CustomWatchfaceFormat +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey +import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap +import info.nightscout.rx.weardata.DrawableData +import info.nightscout.rx.weardata.DrawableFormat +import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.interfaces.ResourceHelper +import org.json.JSONObject +import java.io.BufferedOutputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ZipCustomWatchfaceFormat @Inject constructor( + private var rh: ResourceHelper, + private var cryptoUtil: CryptoUtil, + private var storage: Storage +) : CustomWatchfaceFormat { + + companion object { + + const val CUSTOM_WF_EXTENTION = ".zip" + const val CUSTOM_JSON_FILE = "CustomWatchface.json" + } + + override fun loadCustomWatchface(cwfFile: File): CustomWatchface? { + var json = JSONObject() + var metadata: CustomWatchfaceMetadataMap = mutableMapOf() + val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf() + + try { + val zipInputStream = ZipInputStream(cwfFile.inputStream()) + var zipEntry: ZipEntry? = zipInputStream.nextEntry + while (zipEntry != null) { + val entryName = zipEntry.name + + val buffer = ByteArray(2048) + val byteArrayOutputStream = ByteArrayOutputStream() + var count = zipInputStream.read(buffer) + while (count != -1) { + byteArrayOutputStream.write(buffer, 0, count) + count = zipInputStream.read(buffer) + } + zipInputStream.closeEntry() + + if (entryName == CUSTOM_JSON_FILE) { + val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8) + json = JSONObject(jsonString) + metadata = loadMetadata(json) + } else { + val customWatchfaceDrawableDataKey = CustomWatchfaceDrawableDataKey.fromFileName(entryName) + val drawableFormat = DrawableFormat.fromFileName(entryName) + if (customWatchfaceDrawableDataKey != CustomWatchfaceDrawableDataKey.UNKNOWN && drawableFormat != DrawableFormat.UNKNOWN) { + drawableDatas[customWatchfaceDrawableDataKey] = DrawableData(byteArrayOutputStream.toByteArray(),drawableFormat) + } + } + zipEntry = zipInputStream.nextEntry + } + + // Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image + if (metadata.containsKey(CustomWatchfaceMetadataKey.CWF_NAME) && drawableDatas.containsKey(CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE)) + return CustomWatchface(json.toString(4), metadata, drawableDatas) + else + return null + + } catch (e: Exception) { + return null + } + } + + + override fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface) { + + try { + val outputStream = FileOutputStream(file) + val zipOutputStream = ZipOutputStream(BufferedOutputStream(outputStream)) + + // Ajouter le fichier JSON au ZIP + val jsonEntry = ZipEntry(CUSTOM_JSON_FILE) + zipOutputStream.putNextEntry(jsonEntry) + zipOutputStream.write(customWatchface.json.toByteArray()) + zipOutputStream.closeEntry() + + // Ajouter les fichiers divers au ZIP + for (drawableData in customWatchface.drawableDataMap) { + val fileEntry = ZipEntry("${drawableData.key.fileName}.${drawableData.value.format.extension}") + zipOutputStream.putNextEntry(fileEntry) + zipOutputStream.write(drawableData.value.value) + zipOutputStream.closeEntry() + } + zipOutputStream.close() + outputStream.close() + } catch (e: Exception) { + + } + + } + + + override fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap { + val metadata: CustomWatchfaceMetadataMap = mutableMapOf() + + if (contents.has("metadata")) { + val meta = contents.getJSONObject("metadata") + for (key in meta.keys()) { + val metaKey = CustomWatchfaceMetadataKey.fromKey(key) + if (metaKey != null) { + metadata[metaKey] = meta.getString(key) + } + } + } + return metadata + } + +} \ No newline at end of file diff --git a/plugins/configuration/src/main/res/layout/custom_watchface_import_list_activity.xml b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_activity.xml new file mode 100644 index 0000000000..5095328ce9 --- /dev/null +++ b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_activity.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml new file mode 100644 index 0000000000..dfd624b429 --- /dev/null +++ b/plugins/configuration/src/main/res/layout/custom_watchface_import_list_item.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/configuration/src/main/res/values/strings.xml b/plugins/configuration/src/main/res/values/strings.xml index 1cec48dfba..15e68dacd6 100644 --- a/plugins/configuration/src/main/res/values/strings.xml +++ b/plugins/configuration/src/main/res/values/strings.xml @@ -164,6 +164,11 @@ Missing encryption configuration, settings format is invalid! Unsupported or not specified encryption algorithm! + + Select Custom Watchface + Directory: %1$s + File name: %1$s + Please reboot your phone or restart AAPS from the System Settings \notherwise Android APS will not have logging (important to track and verify that the algorithms are working correctly)! diff --git a/plugins/main/src/main/AndroidManifest.xml b/plugins/main/src/main/AndroidManifest.xml index 6d24217b2e..75ecc0da85 100644 --- a/plugins/main/src/main/AndroidManifest.xml +++ b/plugins/main/src/main/AndroidManifest.xml @@ -51,6 +51,15 @@ android:pathPrefix="@string/path_rx_bridge" android:scheme="wear" /> + + + + + + diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt index 57cd89ec9d..848facfbec 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearFragment.kt @@ -5,13 +5,21 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import dagger.android.support.DaggerFragment +import info.nightscout.core.ui.toast.ToastUtils import info.nightscout.core.utils.fabric.FabricPrivacy +import info.nightscout.interfaces.maintenance.ImportExportPrefs +import info.nightscout.plugins.R import info.nightscout.plugins.databinding.WearFragmentBinding -import info.nightscout.plugins.general.wear.events.EventWearUpdateGui import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventMobileDataToWear import info.nightscout.rx.events.EventMobileToWear +import info.nightscout.rx.events.EventWearUpdateGui +import info.nightscout.rx.logging.AAPSLogger +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.interfaces.ResourceHelper +import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.plusAssign @@ -24,9 +32,12 @@ class WearFragment : DaggerFragment() { @Inject lateinit var aapsSchedulers: AapsSchedulers @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var importExportPrefs: ImportExportPrefs + @Inject lateinit var sp:SP + @Inject lateinit var rh: ResourceHelper + @Inject lateinit var aapsLogger: AAPSLogger private var _binding: WearFragmentBinding? = null - private val disposable = CompositeDisposable() // This property is only valid between onCreateView and @@ -43,6 +54,25 @@ class WearFragment : DaggerFragment() { super.onViewCreated(view, savedInstanceState) binding.resend.setOnClickListener { rxBus.send(EventData.ActionResendData("WearFragment")) } binding.openSettings.setOnClickListener { rxBus.send(EventMobileToWear(EventData.OpenSettings(dateUtil.now()))) } + + binding.loadCustom.setOnClickListener { + importExportPrefs.verifyStoragePermissions(this) { + importExportPrefs.importCustomWatchface(this) + } + } + binding.defaultCustom.setOnClickListener { + sp.remove(info.nightscout.shared.R.string.key_custom_watchface) + wearPlugin.savedCustomWatchface = null + rxBus.send(EventMobileToWear(EventData.ActionrequestSetDefaultWatchface(dateUtil.now()))) + updateGui() + } + binding.sendCustom.setOnClickListener { + wearPlugin.savedCustomWatchface?.let { cwf -> rxBus.send(EventMobileDataToWear(cwf)) } + } + binding.exportCustom.setOnClickListener { + wearPlugin.savedCustomWatchface?.let { importExportPrefs.exportCustomWatchface(it) } + ?: apply { rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(true)))} + } } override fun onResume() { @@ -51,6 +81,19 @@ class WearFragment : DaggerFragment() { .toObservable(EventWearUpdateGui::class.java) .observeOn(aapsSchedulers.main) .subscribe({ updateGui() }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventMobileDataToWear::class.java) + .observeOn(aapsSchedulers.main) + .subscribe({ + loadCustom(it.payload) + wearPlugin.customWatchfaceSerialized = "" + wearPlugin.savedCustomWatchface = null + updateGui() + ToastUtils.okToast(context,rh.gs(R.string.wear_new_custom_watchface_received)) + }, fabricPrivacy::logException) + if (wearPlugin.savedCustomWatchface == null) + rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(false))) + //EventMobileDataToWear updateGui() } @@ -67,6 +110,34 @@ class WearFragment : DaggerFragment() { private fun updateGui() { _binding ?: return + sp.getString(info.nightscout.shared.R.string.key_custom_watchface, "").let { + if (it != wearPlugin.customWatchfaceSerialized && it != "") { + aapsLogger.debug("XXXXX Serialisation: ${it.length}") + try { + wearPlugin.savedCustomWatchface = (EventData.deserialize(it) as EventData.ActionSetCustomWatchface) + wearPlugin.customWatchfaceSerialized = it + } + catch(e: Exception) { + wearPlugin.customWatchfaceSerialized = "" + wearPlugin.savedCustomWatchface = null + } + } + sp.remove(info.nightscout.shared.R.string.key_custom_watchface) + } + wearPlugin.savedCustomWatchface?.let { + binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.name) + binding.sendCustom.visibility = View.VISIBLE + binding.coverChart.setImageDrawable(it.drawableDataMap[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE]?.toDrawable(resources)) + } ?:apply { + binding.customName.text = rh.gs(R.string.wear_custom_watchface, rh.gs(info.nightscout.shared.R.string.wear_default_watchface)) + binding.sendCustom.visibility = View.INVISIBLE + binding.coverChart.setImageDrawable(null) + } binding.connectedDevice.text = wearPlugin.connectedDevice } + + private fun loadCustom(cwf: EventData.ActionSetCustomWatchface) { + aapsLogger.debug("XXXXX EventWearCwfExported received") + wearPlugin.savedCustomWatchface = cwf + } } \ No newline at end of file diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt index a17e63134e..21516d6d1e 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/WearPlugin.kt @@ -54,6 +54,8 @@ class WearPlugin @Inject constructor( private val disposable = CompositeDisposable() var connectedDevice = "---" + var customWatchfaceSerialized = "" + var savedCustomWatchface: EventData.ActionSetCustomWatchface? = null override fun onStart() { super.onStart() diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt index 066e49bf2b..a1fe993f56 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataHandlerMobile.kt @@ -41,6 +41,7 @@ import info.nightscout.interfaces.iob.GlucoseStatusProvider import info.nightscout.interfaces.iob.InMemoryGlucoseValue import info.nightscout.interfaces.iob.IobCobCalculator import info.nightscout.interfaces.logging.UserEntryLogger +import info.nightscout.interfaces.maintenance.ImportExportPrefs import info.nightscout.interfaces.nsclient.ProcessedDeviceStatusData import info.nightscout.interfaces.plugin.ActivePlugin import info.nightscout.interfaces.plugin.PluginBase @@ -59,6 +60,8 @@ import info.nightscout.plugins.R import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus import info.nightscout.rx.events.EventMobileToWear +import info.nightscout.rx.events.EventWearCwfExported +import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag import info.nightscout.rx.weardata.EventData @@ -107,7 +110,8 @@ class DataHandlerMobile @Inject constructor( private val commandQueue: CommandQueue, private val fabricPrivacy: FabricPrivacy, private val uiInteraction: UiInteraction, - private val persistenceLayer: PersistenceLayer + private val persistenceLayer: PersistenceLayer, + private val importExportPrefs: ImportExportPrefs ) { private val disposable = CompositeDisposable() @@ -314,6 +318,13 @@ class DataHandlerMobile @Inject constructor( .toObservable(EventData.ActionHeartRate::class.java) .observeOn(aapsSchedulers.io) .subscribe({ handleHeartRate(it) }, fabricPrivacy::logException) + disposable += rxBus + .toObservable(EventData.ActionGetCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe({ + aapsLogger.debug(LTag.WEAR, "Custom Watch face ${it.customWatchface} received from ${it.sourceNodeId}") + handleGetCustomWatchface(it) + }, fabricPrivacy::logException) } private fun handleTddStatus() { @@ -1247,4 +1258,19 @@ class DataHandlerMobile @Inject constructor( device = actionHeartRate.device) repository.runTransaction(InsertOrUpdateHeartRateTransaction(hr)).blockingAwait() } + + + private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) { + val customWatchface = command.customWatchface + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.json}") + //Update Wear Fragment + sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWatchface.serialize()) + //rxBus.send(EventWearCwfExported(customWatchface)) + rxBus.send(EventWearUpdateGui()) + if (command.exportFile) + importExportPrefs.exportCustomWatchface(customWatchface) + //Implement here a record within SP and a save within exports subfolder as zipFile + + } + } diff --git a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt index 13a8ff47f9..dac0274681 100644 --- a/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt +++ b/plugins/main/src/main/java/info/nightscout/plugins/general/wear/wearintegration/DataLayerListenerServiceMobile.kt @@ -26,9 +26,10 @@ import info.nightscout.interfaces.profile.ProfileFunction import info.nightscout.interfaces.receivers.ReceiverStatusStore import info.nightscout.plugins.R import info.nightscout.plugins.general.wear.WearPlugin -import info.nightscout.plugins.general.wear.events.EventWearUpdateGui +import info.nightscout.rx.events.EventWearUpdateGui import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventMobileDataToWear import info.nightscout.rx.events.EventMobileToWear import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -80,6 +81,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private val disposable = CompositeDisposable() private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge) + private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge) override fun onCreate() { AndroidInjection.inject(this) @@ -90,6 +92,10 @@ class DataLayerListenerServiceMobile : WearableListenerService() { .toObservable(EventMobileToWear::class.java) .observeOn(aapsSchedulers.io) .subscribe { sendMessage(rxPath, it.payload.serialize()) } + disposable += rxBus + .toObservable(EventMobileDataToWear::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { sendMessage(rxDataPath, it.payload.serializeByte()) } } override fun onCapabilityChanged(p0: CapabilityInfo) { @@ -136,6 +142,11 @@ class DataLayerListenerServiceMobile : WearableListenerService() { val command = EventData.deserialize(String(messageEvent.data)) rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) } + rxDataPath -> { + aapsLogger.debug(LTag.WEAR, "onMessageReceived rxDataPath: ${String(messageEvent.data)}") + val command = EventData.deserializeByte(messageEvent.data) + rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + } } } } @@ -164,7 +175,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() { private fun pickBestNodeId(nodes: Set): Node? = nodes.firstOrNull { it.isNearby } ?: nodes.firstOrNull() - @Suppress("unused") + //@Suppress("unused") private fun sendData(path: String, vararg params: DataMap) { if (wearPlugin.isEnabled()) { scope.launch { @@ -201,7 +212,6 @@ class DataLayerListenerServiceMobile : WearableListenerService() { } } - @Suppress("unused") private fun sendMessage(path: String, data: ByteArray) { aapsLogger.debug(LTag.WEAR, "sendMessage: $path") transcriptionNodeId?.also { nodeId -> diff --git a/plugins/main/src/main/res/drawable/export_custom.xml b/plugins/main/src/main/res/drawable/export_custom.xml new file mode 100644 index 0000000000..3779b637df --- /dev/null +++ b/plugins/main/src/main/res/drawable/export_custom.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + diff --git a/plugins/main/src/main/res/drawable/load_custom.xml b/plugins/main/src/main/res/drawable/load_custom.xml new file mode 100644 index 0000000000..26a7dda859 --- /dev/null +++ b/plugins/main/src/main/res/drawable/load_custom.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + diff --git a/plugins/main/src/main/res/drawable/send_custom.xml b/plugins/main/src/main/res/drawable/send_custom.xml new file mode 100644 index 0000000000..2602e2953d --- /dev/null +++ b/plugins/main/src/main/res/drawable/send_custom.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/plugins/main/src/main/res/drawable/set_default.xml b/plugins/main/src/main/res/drawable/set_default.xml new file mode 100644 index 0000000000..ff43d0d255 --- /dev/null +++ b/plugins/main/src/main/res/drawable/set_default.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/plugins/main/src/main/res/layout/wear_fragment.xml b/plugins/main/src/main/res/layout/wear_fragment.xml index cfd22a1f4f..1eb374529d 100644 --- a/plugins/main/src/main/res/layout/wear_fragment.xml +++ b/plugins/main/src/main/res/layout/wear_fragment.xml @@ -1,42 +1,201 @@ - + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="4dp" + app:cardCornerRadius="4dp" + app:contentPadding="2dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="false" + android:layout_gravity="center"> - + - + + + + + + + + + + + + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" + android:layout_marginTop="4dp" + app:cardCornerRadius="4dp" + app:contentPadding="2dp" + app:cardElevation="2dp" + app:cardUseCompatPadding="false" + android:layout_gravity="center"> + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/main/src/main/res/values/strings.xml b/plugins/main/src/main/res/values/strings.xml index c7c4789b6f..b8d2b4eccf 100644 --- a/plugins/main/src/main/res/values/strings.xml +++ b/plugins/main/src/main/res/values/strings.xml @@ -361,6 +361,11 @@ Show SMB on the watch like a standard bolus. Show the predictions on the watchface. Predictions + Custom Watchface: %1$s + Load Watchface + Send Watchface + Export Watchface + New watchface received from watch Resend All Data Open Settings on Wear diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 2a08a7f623..ab50e950d1 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -241,6 +241,31 @@ + + + + + + + + + + + + + + @@ -267,6 +292,15 @@ android:pathPrefix="@string/path_rx_bridge" android:scheme="wear" /> + + + + + + + diff --git a/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt b/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt index 389ee261d9..5d5e197670 100644 --- a/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt +++ b/wear/src/main/java/info/nightscout/androidaps/comm/DataHandlerWear.kt @@ -25,6 +25,7 @@ import info.nightscout.androidaps.tile.QuickWizardTileService import info.nightscout.androidaps.tile.TempTargetTileService import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventWearDataToMobile import info.nightscout.rx.events.EventWearToMobile import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -179,6 +180,29 @@ class DataHandlerWear @Inject constructor( TileService.getUpdater(context).requestUpdate(QuickWizardTileService::class.java) } } + disposable += rxBus + .toObservable(EventData.ActionSetCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}") + persistence.store(it) + } + disposable += rxBus + .toObservable(EventData.ActionrequestSetDefaultWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + aapsLogger.debug(LTag.WEAR, "Set Default Watchface received from ${it.sourceNodeId}") + persistence.setDefaultWatchface() + } + disposable += rxBus + .toObservable(EventData.ActionrequestCustomWatchface::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { eventData -> + aapsLogger.debug(LTag.WEAR, "Custom Watchface requested from ${eventData.sourceNodeId}") + persistence.readCustomWatchface()?.let { + rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, eventData.exportFile))) + } + } } private fun handleBolusProgress(bolusProgress: EventData.BolusProgress) { diff --git a/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt b/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt index fabc65b8fb..e4c62dd83d 100644 --- a/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt +++ b/wear/src/main/java/info/nightscout/androidaps/comm/DataLayerListenerServiceWear.kt @@ -12,6 +12,7 @@ import info.nightscout.androidaps.interaction.utils.Persistence import info.nightscout.androidaps.interaction.utils.WearUtil import info.nightscout.rx.AapsSchedulers import info.nightscout.rx.bus.RxBus +import info.nightscout.rx.events.EventWearDataToMobile import info.nightscout.rx.events.EventWearToMobile import info.nightscout.rx.logging.AAPSLogger import info.nightscout.rx.logging.LTag @@ -43,6 +44,7 @@ class DataLayerListenerServiceWear : WearableListenerService() { private val disposable = CompositeDisposable() private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge) + private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge) override fun onCreate() { AndroidInjection.inject(this) @@ -54,6 +56,12 @@ class DataLayerListenerServiceWear : WearableListenerService() { .subscribe { sendMessage(rxPath, it.payload.serialize()) } + disposable += rxBus + .toObservable(EventWearDataToMobile::class.java) + .observeOn(aapsSchedulers.io) + .subscribe { + sendMessage(rxDataPath, it.payload.serializeByte()) + } } override fun onCapabilityChanged(p0: CapabilityInfo) { @@ -100,6 +108,14 @@ class DataLayerListenerServiceWear : WearableListenerService() { transcriptionNodeId = messageEvent.sourceNodeId aapsLogger.debug(LTag.WEAR, "Updated node: $transcriptionNodeId") } + rxDataPath -> { + aapsLogger.debug(LTag.WEAR, "onMessageReceived: ${messageEvent.data}") + val command = EventData.deserializeByte(messageEvent.data) + rxBus.send(command.also { it.sourceNodeId = messageEvent.sourceNodeId }) + // Use this sender + transcriptionNodeId = messageEvent.sourceNodeId + aapsLogger.debug(LTag.WEAR, "Updated node: $transcriptionNodeId") + } } } diff --git a/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt b/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt index 909c0aa21a..3cc8baa387 100644 --- a/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt +++ b/wear/src/main/java/info/nightscout/androidaps/di/WearServicesModule.kt @@ -40,6 +40,7 @@ abstract class WearServicesModule { @ContributesAndroidInjector abstract fun contributesBIGChart(): BigChartWatchface @ContributesAndroidInjector abstract fun contributesNOChart(): NoChartWatchface @ContributesAndroidInjector abstract fun contributesCircleWatchface(): CircleWatchface + @ContributesAndroidInjector abstract fun contributesCustomWatchface(): CustomWatchface @ContributesAndroidInjector abstract fun contributesTileBase(): TileBase @ContributesAndroidInjector abstract fun contributesQuickWizardTileService(): QuickWizardTileService diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt index 557300ca6d..ef97aa4270 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/Persistence.kt @@ -36,6 +36,8 @@ open class Persistence @Inject constructor( const val KEY_STALE_REPORTED = "staleReported" const val KEY_DATA_UPDATED = "data_updated_at" + const val CUSTOM_WATCHFACE = "custom_watchface" + const val CUSTOM_DEFAULT_WATCHFACE = "custom_default_watchface" } fun getString(key: String, defaultValue: String): String { @@ -130,6 +132,23 @@ open class Persistence @Inject constructor( return null } + fun readCustomWatchface(isDefault: Boolean = false): EventData.ActionSetCustomWatchface? { + try { + var s = sp.getStringOrNull(if (isDefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, null) + if (s != null) { + return deserialize(s) as EventData.ActionSetCustomWatchface + } else { + s = sp.getStringOrNull(CUSTOM_DEFAULT_WATCHFACE, null) + if (s != null) { + return deserialize(s) as EventData.ActionSetCustomWatchface + } + } + } catch (exception: Exception) { + aapsLogger.error(LTag.WEAR, exception.toString()) + } + return null + } + fun store(singleBg: SingleBg) { putString(BG_DATA_PERSISTENCE_KEY, singleBg.serialize()) aapsLogger.debug(LTag.WEAR, "Stored BG data: $singleBg") @@ -151,6 +170,16 @@ open class Persistence @Inject constructor( aapsLogger.debug(LTag.WEAR, "Stored Status data: $status") } + fun store(customWatchface: EventData.ActionSetCustomWatchface, isdefault: Boolean = false) { + putString(if (isdefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, customWatchface.serialize()) + aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.name} ${isdefault}: $customWatchface") + } + + fun setDefaultWatchface() { + readCustomWatchface(true)?.let {store(it)} + aapsLogger.debug(LTag.WEAR, "Custom Watchface reset to default") + } + fun joinSet(set: Set, separator: String?): String { val sb = StringBuilder() var i = 0 diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt new file mode 100644 index 0000000000..f01f5de785 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CustomWatchface.kt @@ -0,0 +1,398 @@ +@file:Suppress("DEPRECATION") + +package info.nightscout.androidaps.watchfaces + +import android.app.ActionBar.LayoutParams +import android.content.Context +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Point +import android.graphics.Typeface +import android.support.wearable.watchface.WatchFaceStyle +import android.util.TypedValue +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.core.view.forEach +import androidx.viewbinding.ViewBinding +import info.nightscout.androidaps.R +import info.nightscout.androidaps.databinding.ActivityCustomBinding +import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey +import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap +import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey +import info.nightscout.rx.weardata.DrawableData +import info.nightscout.rx.weardata.DrawableFormat +import info.nightscout.rx.weardata.EventData +import info.nightscout.shared.extensions.toVisibility +import org.joda.time.TimeOfDay +import org.json.JSONObject +import java.io.ByteArrayOutputStream +import javax.inject.Inject + +class CustomWatchface : BaseWatchFace() { + + @Inject lateinit var context: Context + private lateinit var binding: ActivityCustomBinding + private var zoomFactor = 1.0 + private val displaySize = Point() + private val TEMPLATE_RESOLUTION = 400 + private var lowBatColor = Color.RED + private var bgColor = Color.WHITE + + val CUSTOM_VERSION = "v0.1" + + @Suppress("DEPRECATION") + override fun inflateLayout(inflater: LayoutInflater): ViewBinding { + binding = ActivityCustomBinding.inflate(inflater) + setDefaultColors() + persistence.store(defaultWatchface(), true) + (context.getSystemService(WINDOW_SERVICE) as WindowManager).defaultDisplay.getSize(displaySize) + zoomFactor = (displaySize.x).toDouble() / TEMPLATE_RESOLUTION.toDouble() + return binding + } + + override fun getWatchFaceStyle(): WatchFaceStyle { + return WatchFaceStyle.Builder(this) + .setAcceptsTapEvents(true) + .setHideNotificationIndicator(false) + .setShowUnreadCountIndicator(true) + .build() + } + + override fun setColorDark() { + setWatchfaceStyle() + //@ColorInt val batteryOkColor = ContextCompat.getColor(this, R.color.dark_midColor) + binding.mainLayout.setBackgroundColor(ContextCompat.getColor(this, R.color.dark_background)) + binding.sgv.setTextColor(bgColor) + binding.direction.setTextColor(bgColor) + + if (ageLevel != 1) + binding.timestamp.setTextColor(ContextCompat.getColor(this, R.color.dark_TimestampOld)) + if (status.batteryLevel != 1) + binding.uploaderBattery.setTextColor(lowBatColor) + when (loopLevel) { + -1 -> binding.loop.setBackgroundResource(R.drawable.loop_grey_25) + 1 -> binding.loop.setBackgroundResource(R.drawable.loop_green_25) + else -> binding.loop.setBackgroundResource(R.drawable.loop_red_25) + } + + basalBackgroundColor = ContextCompat.getColor(this, R.color.basal_dark) + basalCenterColor = ContextCompat.getColor(this, R.color.basal_light) + + // rotate the second hand. + binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f + // rotate the minute hand. + binding.minuteHand.rotation = TimeOfDay().minuteOfHour * 6f + // rotate the hour hand. + binding.hourHand.rotation = TimeOfDay().hourOfDay * 30f + TimeOfDay().minuteOfHour * 0.5f + + setupCharts() + } + + override fun setColorBright() { + setColorDark() + } + + override fun setColorLowRes() { + setColorDark() + } + + override fun setSecond() { + binding.time.text = "${dateUtil.hourString()}:${dateUtil.minuteString()}" + if (enableSecond) ":${dateUtil.secondString()}" else "" + binding.second.text = dateUtil.secondString() + // rotate the second hand. + binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f + //aapsLogger.debug("XXXXXX Setsecond calles:") + } + + private fun setWatchfaceStyle() { + bgColor = when (singleBg.sgvLevel) { + 1L -> highColor + 0L -> midColor + -1L -> lowColor + else -> midColor + } + val customWatchface = persistence.readCustomWatchface() ?: persistence.readCustomWatchface(true) + //aapsLogger.debug("XXXXX + setWatchfaceStyle Json ${customWatchface?.json}") + customWatchface?.let { customWatchface -> + val json = JSONObject(customWatchface.json) + val drawableDataMap = customWatchface.drawableDataMap + enableSecond = (if (json.has("enableSecond")) json.getBoolean("enableSecond") else false) && sp.getBoolean(R.string.key_show_seconds, true) + //aapsLogger.debug("XXXXXX json File (beginning):" + customWatchface.json) + + highColor = if (json.has("highColor")) Color.parseColor(json.getString("highColor")) else ContextCompat.getColor(this, R.color.dark_highColor) + midColor = if (json.has("midColor")) Color.parseColor(json.getString("midColor")) else ContextCompat.getColor(this, R.color.inrange) + lowColor = if (json.has("lowColor")) Color.parseColor(json.getString("lowColor")) else ContextCompat.getColor(this, R.color.low) + lowBatColor = if (json.has("lowBatColor")) Color.parseColor(json.getString("lowBatColor")) else ContextCompat.getColor(this, R.color.dark_uploaderBatteryEmpty) + carbColor = if (json.has("carbColor")) Color.parseColor(json.getString("carbColor")) else ContextCompat.getColor(this, R.color.carbs) + gridColor = if (json.has("gridColor")) Color.parseColor(json.getString("gridColor")) else ContextCompat.getColor(this, R.color.carbs) + pointSize = if (json.has("pointSize")) json.getInt("pointSize") else 2 + aapsLogger.debug("XXXXXX enableSecond $enableSecond ${sp.getBoolean(R.string.key_show_seconds, false)} pointSize $pointSize") + binding.mainLayout.forEach { view -> + //aapsLogger.debug("XXXXXX view:" + view.tag.toString()) + view.tag?.let { tag -> + if (json.has(tag.toString())) { + var viewjson = json.getJSONObject(tag.toString()) + //aapsLogger.debug("XXXXXX \"" + tag.toString() + "\": " + viewjson.toString(4)) + var wrapContent = LayoutParams.WRAP_CONTENT + val width = if (viewjson.has("width")) (viewjson.getInt("width") * zoomFactor).toInt() else wrapContent + val height = if (viewjson.has("height")) (viewjson.getInt("height") * zoomFactor).toInt() else wrapContent + var params = FrameLayout.LayoutParams(width, height) + params.topMargin = if (viewjson.has("topmargin")) (viewjson.getInt("topmargin") * zoomFactor).toInt() else 0 + params.leftMargin = if (viewjson.has("leftmargin")) (viewjson.getInt("leftmargin") * zoomFactor).toInt() else 0 + view.setLayoutParams(params) + view.visibility = if (viewjson.has("visibility")) setVisibility(viewjson.getString("visibility")) else View.GONE + + if (view is TextView) { + view.rotation = if (viewjson.has("rotation")) viewjson.getInt("rotation").toFloat() else 0F + view.setTextSize(TypedValue.COMPLEX_UNIT_PX, ((if (viewjson.has("textsize")) viewjson.getInt("textsize") else 22) * zoomFactor).toFloat()) + view.gravity = setGravity(if (viewjson.has("gravity")) viewjson.getString("gravity") else "center") + view.setTypeface( + setFont(if (viewjson.has("font")) viewjson.getString("font") else "sans-serif"), + Style.fromKey( viewjson.getString("fontStyle")).typeface + ) + if (viewjson.has("fontColor")) + view.setTextColor(getColor(viewjson.getString("fontColor"))) + } + + if (view is ImageView) { + view.clearColorFilter() + drawableDataMap[CustomWatchfaceDrawableDataKey.fromKey(tag.toString())]?.toDrawable(resources)?.also { + if (viewjson.has("color")) + it.colorFilter = changeDrawableColor(getColor(viewjson.getString("color"))) + else + it.clearColorFilter() + view.setImageDrawable(it) + } ?: apply { + view.setImageDrawable(CustomWatchfaceDrawableDataKey.fromKey(tag.toString()).icon?.let { context.getDrawable(it) }) + if (viewjson.has("color")) + view.setColorFilter(getColor(viewjson.getString("color"))) + else + view.clearColorFilter() + } + } + } + } + } + binding.second.visibility= ((binding.second.visibility==View.VISIBLE) && enableSecond).toVisibility() + binding.secondHand.visibility= ((binding.secondHand.visibility==View.VISIBLE) && enableSecond).toVisibility() + } + } + + private fun defaultWatchface(): EventData.ActionSetCustomWatchface { + val metadata = JSONObject() + .put(CustomWatchfaceMetadataKey.CWF_NAME.key, getString(info.nightscout.shared.R.string.wear_default_watchface)) + .put(CustomWatchfaceMetadataKey.CWF_AUTHOR.key, "Philoul") + .put(CustomWatchfaceMetadataKey.CWF_CREATED_AT.key, dateUtil.dateString(dateUtil.now())) + .put(CustomWatchfaceMetadataKey.CWF_VERSION.key, CUSTOM_VERSION) + val json = JSONObject() + .put("metadata", metadata) + .put("highColor", String.format("#%06X", 0xFFFFFF and highColor)) + .put("midColor", String.format("#%06X", 0xFFFFFF and midColor)) + .put("lowColor", String.format("#%06X", 0xFFFFFF and lowColor)) + .put("lowBatColor", String.format("#%06X", 0xFFFFFF and lowBatColor)) + .put("carbColor", String.format("#%06X", 0xFFFFFF and carbColor)) + .put("gridColor", String.format("#%06X", 0xFFFFFF and Color.WHITE)) + .put("pointSize",2) + .put("enableSecond", true) + + binding.mainLayout.forEach { view -> + val params = view.layoutParams as FrameLayout.LayoutParams + if (view is TextView) { + json.put( + view.tag.toString(), + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("rotation", view.rotation.toInt()) + .put("visibility", getVisibility(view.visibility)) + .put("textsize", view.textSize.toInt()) + .put("gravity", getGravity(view.gravity)) + .put("font", getFont(view.typeface)) + .put("fontStyle", Style.fromTypeface(view.typeface.style).key) + .put("fontColor", String.format("#%06X", 0xFFFFFF and view.currentTextColor)) + ) + } + if (view is ImageView) { + //view.backgroundTintList = + json.put( + view.tag.toString(), + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("visibility", getVisibility(view.visibility)) + ) + } + if (view is lecho.lib.hellocharts.view.LineChartView) { + json.put( + view.tag.toString(), + JSONObject() + .put("width", (params.width / zoomFactor).toInt()) + .put("height", (params.height / zoomFactor).toInt()) + .put("topmargin", (params.topMargin / zoomFactor).toInt()) + .put("leftmargin", (params.leftMargin / zoomFactor).toInt()) + .put("visibility", getVisibility(view.visibility)) + ) + } + } + val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf() + getResourceByteArray(info.nightscout.shared.R.drawable.watchface_custom)?.let { + val drawableDataMap = DrawableData(it,DrawableFormat.PNG) + drawableDatas[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE] = drawableDataMap + } + return EventData.ActionSetCustomWatchface(getString(info.nightscout.shared.R.string.wear_default_watchface),json.toString(4),drawableDatas) + } + + private fun setDefaultColors() { + highColor = Color.parseColor("#FFFF00") + midColor = Color.parseColor("#00FF00") + lowColor = Color.parseColor("#FF0000") + carbColor = ContextCompat.getColor(this, R.color.carbs) + lowBatColor = ContextCompat.getColor(this, R.color.dark_uploaderBatteryEmpty) + gridColor = Color.WHITE + } + + private fun setVisibility(visibility: String): Int = when (visibility) { + "visible" -> View.VISIBLE + "invisible" -> View.INVISIBLE + "gone" -> View.GONE + else -> View.GONE + } + + private fun getVisibility(visibility: Int): String = when (visibility) { + View.VISIBLE -> "visible" + View.INVISIBLE -> "invisible" + View.GONE -> "gone" + else -> "gone" + } + + private fun setGravity(gravity: String): Int = when (gravity) { + "center" -> Gravity.CENTER + "left" -> Gravity.LEFT + "right" -> Gravity.RIGHT + else -> Gravity.CENTER + } + + private fun getGravity(gravity: Int): String = when (gravity) { + Gravity.CENTER -> "center" + Gravity.LEFT -> "left" + Gravity.RIGHT -> "right" + else -> "center" + } + + private fun setFont(font: String): Typeface = when (font) { + "sans-serif" -> Typeface.SANS_SERIF + "default" -> Typeface.DEFAULT + "default-bold" -> Typeface.DEFAULT_BOLD + "monospace" -> Typeface.MONOSPACE + "serif" -> Typeface.SERIF + "roboto-condensed-bold" -> ResourcesCompat.getFont(context, R.font.roboto_condensed_bold)!! + "roboto-condensed-light" -> ResourcesCompat.getFont(context, R.font.roboto_condensed_light)!! + "roboto-condensed-regular" -> ResourcesCompat.getFont(context, R.font.roboto_condensed_regular)!! + "roboto-slab-light" -> ResourcesCompat.getFont(context, R.font.roboto_slab_light)!! + else -> Typeface.DEFAULT + } + + private fun getFont(font: Typeface): String = when (font) { + Typeface.SANS_SERIF -> "sans-serif" + Typeface.DEFAULT -> "default" + Typeface.DEFAULT_BOLD -> "default-bold" + Typeface.MONOSPACE -> "monospace" + Typeface.SERIF -> "serif" + ResourcesCompat.getFont(context, R.font.roboto_condensed_bold)!! -> "roboto-condensed-bold" + ResourcesCompat.getFont(context, R.font.roboto_condensed_light)!! -> "roboto-condensed-light" + ResourcesCompat.getFont(context, R.font.roboto_condensed_regular)!! -> "roboto-condensed-regular" + ResourcesCompat.getFont(context, R.font.roboto_slab_light)!! -> "roboto-slab-light" + else -> "default" + } + + enum class Style(val key: String, val typeface: Int) { + NORMAL("normal", Typeface.NORMAL), + BOLD("bold", Typeface.BOLD), + BOLD_ITALIC("bold-italic", Typeface.BOLD_ITALIC), + ITALIC("italic", Typeface.ITALIC); + companion object { + private val keyToEnumMap = HashMap() + private val typefaceToEnumMap = HashMap() + init { + for (value in values()) keyToEnumMap[value.key] = value + for (value in values()) typefaceToEnumMap[value.typeface] = value + } + fun fromKey(key: String?): Style = + if (keyToEnumMap.containsKey(key)) { + keyToEnumMap[key] ?:NORMAL + } else { + NORMAL + } + fun fromTypeface(typeface: Int?): Style = + if (typefaceToEnumMap.containsKey(typeface)) { + typefaceToEnumMap[typeface] ?:NORMAL + } else { + NORMAL + } + } + } + fun getResourceByteArray(resourceId: Int): ByteArray? { + val inputStream = resources.openRawResource(resourceId) + val byteArrayOutputStream = ByteArrayOutputStream() + + try { + val buffer = ByteArray(1024) + var count: Int + while (inputStream.read(buffer).also { count = it } != -1) { + byteArrayOutputStream.write(buffer, 0, count) + } + byteArrayOutputStream.close() + inputStream.close() + + return byteArrayOutputStream.toByteArray() + } catch (e: Exception) { + } + return null + } + + private fun changeDrawableColor(color: Int): ColorFilter { + val colorMatrix = ColorMatrix() + colorMatrix.setSaturation(0f) // 0 désature l'image, 1 la laisse inchangée. + + // Modifier la teinte de couleur (couleur de fond) + colorMatrix.postConcat( + ColorMatrix( + floatArrayOf( + Color.red(color) / 255f, 0f, 0f, 0f, 0f, + 0f, Color.green(color) / 255f, 0f, 0f, 0f, + 0f, 0f, Color.blue(color) / 255f, 0f, 0f, + 0f, 0f, 0f, Color.alpha(color) / 255f, 0f + ) + ) + ) + + // Appliquer la matrice de couleur au ColorFilter + return ColorMatrixColorFilter(colorMatrix) + + // Appliquer le ColorFilter au Drawable + //drawable.colorFilter = colorFilter + //return drawable + } + + private fun getColor(color: String): Int { + if (color == "bgColor") + return bgColor + else + return Color.parseColor(color) + } + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt index a1cc53eab9..4a7fd3a5cf 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/BaseWatchFace.kt @@ -36,6 +36,7 @@ import info.nightscout.shared.extensions.toVisibilityKeepSpace import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.utils.DateUtil import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.kotlin.plusAssign import javax.inject.Inject import kotlin.math.floor @@ -78,12 +79,15 @@ abstract class BaseWatchFace : WatchFace() { var gridColor = Color.WHITE var basalBackgroundColor = Color.BLUE var basalCenterColor = Color.BLUE + var carbColor = Color.GREEN private var bolusColor = Color.MAGENTA private var lowResMode = false private var layoutSet = false var bIsRound = false var dividerMatchesBg = false var pointSize = 2 + var enableSecond = false + var updateSecond: Disposable? = null // Tapping times private var sgvTapTime: Long = 0 @@ -245,12 +249,13 @@ abstract class BaseWatchFace : WatchFace() { override fun onDestroy() { disposable.clear() + updateSecond?.dispose() simpleUi.onDestroy() super.onDestroy() } override fun getInteractiveModeUpdateRate(): Long { - return 60 * 1000L // Only call onTimeChanged every 60 seconds + return if (enableSecond) 1000L else 60 * 1000L // Only call onTimeChanged every 60 seconds } override fun onDraw(canvas: Canvas) { @@ -271,6 +276,8 @@ abstract class BaseWatchFace : WatchFace() { missedReadingAlert() checkVibrateHourly(oldTime, newTime) if (!simpleUi.isEnabled(currentWatchMode)) setDataFields() + } else if (layoutSet && !simpleUi.isEnabled(currentWatchMode) && enableSecond && newTime.hasSecondChanged(oldTime)) { + setSecond() } } @@ -349,9 +356,10 @@ abstract class BaseWatchFace : WatchFace() { } private fun setDateAndTime() { - binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + if (enableSecond) ":" + dateUtil.secondString() else "" binding.hour?.text = dateUtil.hourString() binding.minute?.text = dateUtil.minuteString() + binding.second?.text = dateUtil.secondString() binding.dateTime?.visibility = sp.getBoolean(R.string.key_show_date, false).toVisibility() binding.dayName?.text = dateUtil.dayNameString() binding.day?.text = dateUtil.dayString() @@ -360,7 +368,12 @@ abstract class BaseWatchFace : WatchFace() { binding.timePeriod?.text = dateUtil.amPm() } - private fun setColor() { + open fun setSecond() { + binding.time?.text = if(binding.timePeriod == null) dateUtil.timeString() else dateUtil.hourString() + ":" + dateUtil.minuteString() + if (enableSecond) ":" + dateUtil.secondString() else "" + binding.second?.text = dateUtil.secondString() + } + + fun setColor() { dividerMatchesBg = sp.getBoolean(R.string.key_match_divider, false) when { lowResMode -> setColorLowRes() @@ -382,6 +395,14 @@ abstract class BaseWatchFace : WatchFace() { if (simpleUi.isEnabled(currentWatchMode)) simpleUi.setAntiAlias(currentWatchMode) else setDataFields() invalidate() + /* + if (enableSecond) + if (updateSecond == null) + updateSecond = aapsSchedulers.io.schedulePeriodicallyDirect( + ::setSecond, 1000L, 1000L, TimeUnit.MILLISECONDS) + else + updateSecond?.dispose() + */ } private fun isLowRes(watchMode: WatchMode): Boolean { @@ -409,12 +430,12 @@ abstract class BaseWatchFace : WatchFace() { if (lowResMode) BgGraphBuilder( sp, dateUtil, graphData.entries, treatmentData.predictions, treatmentData.temps, treatmentData.basals, treatmentData.boluses, pointSize, - midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe + midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, carbColor, timeframe ) else BgGraphBuilder( sp, dateUtil, graphData.entries, treatmentData.predictions, treatmentData.temps, treatmentData.basals, treatmentData.boluses, - pointSize, highColor, lowColor, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, Color.GREEN, timeframe + pointSize, highColor, lowColor, midColor, gridColor, basalBackgroundColor, basalCenterColor, bolusColor, carbColor, timeframe ) binding.chart?.lineChartData = bgGraphBuilder.lineData() binding.chart?.isViewportCalculationEnabled = true diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt index 6a2f78d954..61333ad1e1 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/utils/WatchfaceViewAdapter.kt @@ -6,6 +6,7 @@ import info.nightscout.androidaps.databinding.ActivityHome2Binding import info.nightscout.androidaps.databinding.ActivityHomeBinding import info.nightscout.androidaps.databinding.ActivityBigchartBinding import info.nightscout.androidaps.databinding.ActivityCockpitBinding +import info.nightscout.androidaps.databinding.ActivityCustomBinding import info.nightscout.androidaps.databinding.ActivityDigitalstyleBinding import info.nightscout.androidaps.databinding.ActivityNochartBinding import info.nightscout.androidaps.databinding.ActivitySteampunkBinding @@ -22,11 +23,12 @@ class WatchfaceViewAdapter( cp: ActivityCockpitBinding? = null, ds: ActivityDigitalstyleBinding? = null, nC: ActivityNochartBinding? = null, - sP: ActivitySteampunkBinding? = null + sP: ActivitySteampunkBinding? = null, + cU: ActivityCustomBinding? = null ) { init { - if (aL == null && a2 == null && aa == null && bC == null && cp == null && ds == null && nC == null && sP == null) { + if (aL == null && a2 == null && aa == null && bC == null && cp == null && ds == null && nC == null && sP == null && cU == null) { throw IllegalArgumentException("Require at least on Binding parameter") } } @@ -34,39 +36,40 @@ class WatchfaceViewAdapter( private val errorMessage = "Missing require View Binding parameter" // Required attributes val mainLayout = - aL?.mainLayout ?: a2?.mainLayout ?: aa?.mainLayout ?: bC?.mainLayout ?: bC?.mainLayout ?: cp?.mainLayout ?: ds?.mainLayout ?: nC?.mainLayout ?: sP?.mainLayout + aL?.mainLayout ?: a2?.mainLayout ?: aa?.mainLayout ?: bC?.mainLayout ?: bC?.mainLayout ?: cp?.mainLayout ?: ds?.mainLayout ?: nC?.mainLayout ?: sP?.mainLayout ?: cU?.mainLayout ?: throw IllegalArgumentException(errorMessage) val timestamp = - aL?.timestamp ?: a2?.timestamp ?: aa?.timestamp ?: bC?.timestamp ?: bC?.timestamp ?: cp?.timestamp ?: ds?.timestamp ?: nC?.timestamp ?: sP?.timestamp + aL?.timestamp ?: a2?.timestamp ?: aa?.timestamp ?: bC?.timestamp ?: bC?.timestamp ?: cp?.timestamp ?: ds?.timestamp ?: nC?.timestamp ?: sP?.timestamp ?: cU?.timestamp ?: throw IllegalArgumentException(errorMessage) val root = - aL?.root ?: a2?.root ?: aa?.root ?: bC?.root ?: bC?.root ?: cp?.root ?: ds?.root ?: nC?.root ?: sP?.root + aL?.root ?: a2?.root ?: aa?.root ?: bC?.root ?: bC?.root ?: cp?.root ?: ds?.root ?: nC?.root ?: sP?.root ?: cU?.root ?: throw IllegalArgumentException(errorMessage) // Optional attributes - val sgv = aL?.sgv ?: a2?.sgv ?: aa?.sgv ?: bC?.sgv ?: bC?.sgv ?: cp?.sgv ?: ds?.sgv ?: nC?.sgv - val direction = aL?.direction ?: a2?.direction ?: aa?.direction ?: cp?.direction ?: ds?.direction - val loop = a2?.loop ?: cp?.loop ?: sP?.loop - val delta = aL?.delta ?: a2?.delta ?: aa?.delta ?: bC?.delta ?: bC?.delta ?: cp?.delta ?: ds?.delta ?: nC?.delta - val avgDelta = a2?.avgDelta ?: bC?.avgDelta ?: bC?.avgDelta ?: cp?.avgDelta ?: ds?.avgDelta ?: nC?.avgDelta - val uploaderBattery = aL?.uploaderBattery ?: a2?.uploaderBattery ?: aa?.uploaderBattery ?: cp?.uploaderBattery ?: ds?.uploaderBattery ?: sP?.uploaderBattery - val rigBattery = a2?.rigBattery ?: cp?.rigBattery ?: ds?.rigBattery ?: sP?.rigBattery - val basalRate = a2?.basalRate ?: cp?.basalRate ?: ds?.basalRate ?: sP?.basalRate - val bgi = a2?.bgi ?: ds?.bgi - val AAPSv2 = a2?.AAPSv2 ?: cp?.AAPSv2 ?: ds?.AAPSv2 ?: sP?.AAPSv2 - val cob1 = a2?.cob1 ?: ds?.cob1 - val cob2 = a2?.cob2 ?: cp?.cob2 ?: ds?.cob2 ?: sP?.cob2 - val time = aL?.time ?: a2?.time ?: aa?.time ?: bC?.time ?: bC?.time ?: cp?.time ?: nC?.time - val minute = ds?.minute - val hour = ds?.hour - val day = a2?.day ?: ds?.day - val month = a2?.month ?: ds?.month - val iob1 = a2?.iob1 ?: ds?.iob1 - val iob2 = a2?.iob2 ?: cp?.iob2 ?: ds?.iob2 ?: sP?.iob2 - val chart = a2?.chart ?: aa?.chart ?: bC?.chart ?: bC?.chart ?: ds?.chart ?: sP?.chart + val sgv = aL?.sgv ?: a2?.sgv ?: aa?.sgv ?: bC?.sgv ?: bC?.sgv ?: cp?.sgv ?: ds?.sgv ?: nC?.sgv ?: cU?.sgv + val direction = aL?.direction ?: a2?.direction ?: aa?.direction ?: cp?.direction ?: ds?.direction ?: cU?.direction + val loop = a2?.loop ?: cp?.loop ?: sP?.loop ?: cU?.loop + val delta = aL?.delta ?: a2?.delta ?: aa?.delta ?: bC?.delta ?: bC?.delta ?: cp?.delta ?: ds?.delta ?: nC?.delta ?: cU?.delta + val avgDelta = a2?.avgDelta ?: bC?.avgDelta ?: bC?.avgDelta ?: cp?.avgDelta ?: ds?.avgDelta ?: nC?.avgDelta ?: cU?.avgDelta + val uploaderBattery = aL?.uploaderBattery ?: a2?.uploaderBattery ?: aa?.uploaderBattery ?: cp?.uploaderBattery ?: ds?.uploaderBattery ?: sP?.uploaderBattery ?: cU?.uploaderBattery + val rigBattery = a2?.rigBattery ?: cp?.rigBattery ?: ds?.rigBattery ?: sP?.rigBattery ?: cU?.rigBattery + val basalRate = a2?.basalRate ?: cp?.basalRate ?: ds?.basalRate ?: sP?.basalRate ?: cU?.basalRate + val bgi = a2?.bgi ?: ds?.bgi ?: cU?.bgi + val AAPSv2 = a2?.AAPSv2 ?: cp?.AAPSv2 ?: ds?.AAPSv2 ?: sP?.AAPSv2 ?: cU?.AAPSv2 + val cob1 = a2?.cob1 ?: ds?.cob1 ?: cU?.cob1 + val cob2 = a2?.cob2 ?: cp?.cob2 ?: ds?.cob2 ?: sP?.cob2 ?: cU?.cob2 + val time = aL?.time ?: a2?.time ?: aa?.time ?: bC?.time ?: bC?.time ?: cp?.time ?: nC?.time ?: cU?.time + val second = cU?.second + val minute = ds?.minute ?: cU?.minute + val hour = ds?.hour ?: cU?.hour + val day = a2?.day ?: ds?.day ?: cU?.day + val month = a2?.month ?: ds?.month ?: cU?.month + val iob1 = a2?.iob1 ?: ds?.iob1 ?: cU?.iob1 + val iob2 = a2?.iob2 ?: cp?.iob2 ?: ds?.iob2 ?: sP?.iob2 ?: cU?.iob2 + val chart = a2?.chart ?: aa?.chart ?: bC?.chart ?: bC?.chart ?: ds?.chart ?: sP?.chart ?: cU?.chart val status = aL?.status ?: aa?.status ?: bC?.status ?: bC?.status ?: nC?.status - val timePeriod = ds?.timePeriod ?: aL?.timePeriod ?: nC?.timePeriod ?: bC?.timePeriod - val dayName = ds?.dayName + val timePeriod = ds?.timePeriod ?: aL?.timePeriod ?: nC?.timePeriod ?: bC?.timePeriod ?: cU?.timePeriod + val dayName = ds?.dayName ?: cU?.dayName val mainMenuTap = ds?.mainMenuTap ?: sP?.mainMenuTap val chartZoomTap = ds?.chartZoomTap ?: sP?.chartZoomTap val dateTime = ds?.dateTime ?: a2?.dateTime @@ -91,6 +94,7 @@ class WatchfaceViewAdapter( is ActivityDigitalstyleBinding -> WatchfaceViewAdapter(null, null, null, null, null, bindLayout) is ActivityNochartBinding -> WatchfaceViewAdapter(null, null, null, null, null, null, bindLayout) is ActivitySteampunkBinding -> WatchfaceViewAdapter(null, null, null, null, null, null, null, bindLayout) + is ActivityCustomBinding -> WatchfaceViewAdapter(null, null, null, null, null, null, null, null, bindLayout) else -> throw IllegalArgumentException("ViewBinding is not implement in WatchfaceViewAdapter") } } diff --git a/wear/src/main/res/layout/activity_custom.xml b/wear/src/main/res/layout/activity_custom.xml new file mode 100644 index 0000000000..a1172a3c3f --- /dev/null +++ b/wear/src/main/res/layout/activity_custom.xml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index a9f53bd230..b29ad82733 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ AAPS(Cockpit) AAPS(Steampunk) AAPS(DigitalStyle) + AAPS(Custom) AAPS(Actions) AAPS(Temp Target) AAPS(Quick Wizard) @@ -143,6 +144,7 @@ Only show time and BG Vibrate hourly Show Week number + Show seconds Your style: no style minimal style @@ -208,6 +210,14 @@ complication_tap_action carbs_button_increment_1 carbs_button_increment_2 + enable_custom_setting + digital_watchface + digital_timing + analog_watchface + show_second + highcolor + midcolor + lowcolor increment decrement H diff --git a/wear/src/main/res/xml/watch_face_configuration_custom.xml b/wear/src/main/res/xml/watch_face_configuration_custom.xml new file mode 100644 index 0000000000..ff998be701 --- /dev/null +++ b/wear/src/main/res/xml/watch_face_configuration_custom.xml @@ -0,0 +1,27 @@ + + + + + + + + + +