Refactor Custom Watchface code (simplified)

This commit is contained in:
Philoul 2023-08-06 09:58:52 +02:00
parent b259425361
commit 26dff28e1c
22 changed files with 204 additions and 296 deletions

View file

@ -1,3 +1,5 @@
package info.nightscout.rx.events
class EventWearUpdateGui : Event()
import info.nightscout.rx.weardata.CustomWatchfaceData
class EventWearUpdateGui(val customWatchfaceData: CustomWatchfaceData? = null, val exportFile: Boolean = false) : Event()

View file

@ -1,27 +1,30 @@
package info.nightscout.rx.weardata
import android.content.Context
import android.content.res.Resources
import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Parcelable
import android.util.Xml
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import info.nightscout.shared.R
import kotlinx.serialization.Serializable
import org.json.JSONObject
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon: Int?, val fileName: String) {
UNKNOWN("unknown", null,"Unknown"),
UNKNOWN("unknown", null, "Unknown"),
CUSTOM_WATCHFACE("customWatchface", R.drawable.watchface_custom, "CustomWatchface"),
BACKGROUND("background", R.drawable.background, "Background"),
COVERCHART("cover_chart", null,"CoverChart"),
COVERCHART("cover_chart", null, "CoverChart"),
COVERPLATE("cover_plate", R.drawable.simplified_dial, "CoverPlate"),
HOURHAND("hour_hand", R.drawable.hour_hand,"HourHand"),
MINUTEHAND("minute_hand", R.drawable.minute_hand,"MinuteHand"),
HOURHAND("hour_hand", R.drawable.hour_hand, "HourHand"),
MINUTEHAND("minute_hand", R.drawable.minute_hand, "MinuteHand"),
SECONDHAND("second_hand", R.drawable.second_hand, "SecondHand");
companion object {
@ -36,14 +39,14 @@ enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon
fun fromKey(key: String): CustomWatchfaceDrawableDataKey =
if (keyToEnumMap.containsKey(key)) {
keyToEnumMap[key] ?:UNKNOWN
keyToEnumMap[key] ?: UNKNOWN
} else {
UNKNOWN
}
fun fromFileName(file: String): CustomWatchfaceDrawableDataKey =
if (fileNameToEnumMap.containsKey(file.substringBeforeLast("."))) {
fileNameToEnumMap[file.substringBeforeLast(".")] ?:UNKNOWN
fileNameToEnumMap[file.substringBeforeLast(".")] ?: UNKNOWN
} else {
UNKNOWN
}
@ -53,8 +56,9 @@ enum class CustomWatchfaceDrawableDataKey(val key: String, @DrawableRes val icon
enum class DrawableFormat(val extension: String) {
UNKNOWN(""),
//XML("xml"),
//svg("svg"),
//SVG("svg"),
JPG("jpg"),
PNG("png");
@ -68,7 +72,7 @@ enum class DrawableFormat(val extension: String) {
fun fromFileName(fileName: String): DrawableFormat =
if (extensionToEnumMap.containsKey(fileName.substringAfterLast("."))) {
extensionToEnumMap[fileName.substringAfterLast(".")] ?:UNKNOWN
extensionToEnumMap[fileName.substringAfterLast(".")] ?: UNKNOWN
} else {
UNKNOWN
}
@ -85,15 +89,20 @@ data class DrawableData(val value: ByteArray, val format: DrawableFormat) {
val bitmap = BitmapFactory.decodeByteArray(value, 0, value.size)
BitmapDrawable(resources, bitmap)
}
/*
DrawableFormat.XML -> {
val xmlInputStream = ByteArrayInputStream(value)
val xmlPullParser = Xml.newPullParser()
xmlPullParser.setInput(xmlInputStream, null)
Drawable.createFromXml(resources, xmlPullParser)
}
*/
else -> null
/*
DrawableFormat.SVG -> {
//TODO: include svg to Drawable convertor here
null
}
DrawableFormat.XML -> {
// Always return a null Drawable, even if xml file is a valid xml vector file
val xmlInputStream = ByteArrayInputStream(value)
val xmlPullParser = Xml.newPullParser()
xmlPullParser.setInput(xmlInputStream, null)
Drawable.createFromXml(resources, xmlPullParser)
}
*/
else -> null
}
} catch (e: Exception) {
return null
@ -104,18 +113,13 @@ data class DrawableData(val value: ByteArray, val format: DrawableFormat) {
typealias CustomWatchfaceDrawableDataMap = MutableMap<CustomWatchfaceDrawableDataKey, DrawableData>
typealias CustomWatchfaceMetadataMap = MutableMap<CustomWatchfaceMetadataKey, String>
data class CustomWatchface(val json: String, var metadata: CustomWatchfaceMetadataMap, val drawableDatas: CustomWatchfaceDrawableDataMap)
interface CustomWatchfaceFormat {
fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface)
fun loadCustomWatchface(cwfFile: File): CustomWatchface?
fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap
}
@Serializable
data class CustomWatchfaceData(val json: String, var metadata: CustomWatchfaceMetadataMap, val drawableDatas: CustomWatchfaceDrawableDataMap)
enum class CustomWatchfaceMetadataKey(val key: String, @StringRes val label: Int) {
CWF_NAME("name", R.string.metadata_label_watchface_name),
CWF_FILENAME("filename", R.string.metadata_wear_import_filename),
CWF_AUTHOR("author", R.string.metadata_label_watchface_author),
CWF_CREATED_AT("created_at", R.string.metadata_label_watchface_created_at),
CWF_VERSION("cwf_version", R.string.metadata_label_watchface_version);
@ -137,4 +141,100 @@ enum class CustomWatchfaceMetadataKey(val key: String, @StringRes val label: Int
}
}
class ZipWatchfaceFormat {
companion object {
const val CUSTOM_WF_EXTENTION = ".zip"
const val CUSTOM_JSON_FILE = "CustomWatchface.json"
fun loadCustomWatchface(cwfFile: File): CustomWatchfaceData? {
var json = JSONObject()
var metadata: CustomWatchfaceMetadataMap = mutableMapOf()
val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf()
try {
val zipInputStream = ZipInputStream(cwfFile.inputStream())
var zipEntry: ZipEntry? = zipInputStream.nextEntry
while (zipEntry != null) {
val entryName = zipEntry.name
val buffer = ByteArray(2048)
val byteArrayOutputStream = ByteArrayOutputStream()
var count = zipInputStream.read(buffer)
while (count != -1) {
byteArrayOutputStream.write(buffer, 0, count)
count = zipInputStream.read(buffer)
}
zipInputStream.closeEntry()
if (entryName == CUSTOM_JSON_FILE) {
val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8)
json = JSONObject(jsonString)
metadata = loadMetadata(json)
metadata[CustomWatchfaceMetadataKey.CWF_FILENAME] = cwfFile.name
} else {
val customWatchfaceDrawableDataKey = CustomWatchfaceDrawableDataKey.fromFileName(entryName)
val drawableFormat = DrawableFormat.fromFileName(entryName)
if (customWatchfaceDrawableDataKey != CustomWatchfaceDrawableDataKey.UNKNOWN && drawableFormat != DrawableFormat.UNKNOWN) {
drawableDatas[customWatchfaceDrawableDataKey] = DrawableData(byteArrayOutputStream.toByteArray(), drawableFormat)
}
}
zipEntry = zipInputStream.nextEntry
}
// Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image
if (metadata.containsKey(CustomWatchfaceMetadataKey.CWF_NAME) && drawableDatas.containsKey(CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE))
return CustomWatchfaceData(json.toString(4), metadata, drawableDatas)
else
return null
} catch (e: Exception) {
return null
}
}
fun saveCustomWatchface(file: File, customWatchface: CustomWatchfaceData) {
try {
val outputStream = FileOutputStream(file)
val zipOutputStream = ZipOutputStream(BufferedOutputStream(outputStream))
// Ajouter le fichier JSON au ZIP
val jsonEntry = ZipEntry(CUSTOM_JSON_FILE)
zipOutputStream.putNextEntry(jsonEntry)
zipOutputStream.write(customWatchface.json.toByteArray())
zipOutputStream.closeEntry()
// Ajouter les fichiers divers au ZIP
for (drawableData in customWatchface.drawableDatas) {
val fileEntry = ZipEntry("${drawableData.key.fileName}.${drawableData.value.format.extension}")
zipOutputStream.putNextEntry(fileEntry)
zipOutputStream.write(drawableData.value.value)
zipOutputStream.closeEntry()
}
zipOutputStream.close()
outputStream.close()
} catch (_: Exception) {
}
}
fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap {
val metadata: CustomWatchfaceMetadataMap = mutableMapOf()
if (contents.has("metadata")) {
val meta = contents.getJSONObject("metadata")
for (key in meta.keys()) {
val metaKey = CustomWatchfaceMetadataKey.fromKey(key)
if (metaKey != null) {
metadata[metaKey] = meta.getString(key)
}
}
}
return metadata
}
}
}

View file

@ -14,15 +14,15 @@ sealed class EventData : Event() {
fun serialize() = Json.encodeToString(serializer(), this)
@ExperimentalSerializationApi
fun serializeByte() = ProtoBuf.encodeToByteArray(serializer(), this)
companion object {
fun deserialize(json: String) = try {
Json.decodeFromString(serializer(), json)
} catch (ignored: Exception) {
Error(System.currentTimeMillis())
}
@ExperimentalSerializationApi
fun deserializeByte(byteArray: ByteArray) = try {
ProtoBuf.decodeFromByteArray(serializer(), byteArray)
} catch (ignored: Exception) {
@ -153,7 +153,7 @@ sealed class EventData : Event() {
@Serializable
data class ActionGetCustomWatchface(
val customWatchface: ActionSetCustomWatchface,
val exportFile: Boolean
val exportFile: Boolean = false
) : EventData()
@Serializable
@ -283,9 +283,7 @@ sealed class EventData : Event() {
}
@Serializable
data class ActionSetCustomWatchface(
val name: String,
val json: String,
val drawableDataMap: CustomWatchfaceDrawableDataMap
val customWatchfaceData: CustomWatchfaceData
) : EventData()
@Serializable

View file

@ -40,10 +40,10 @@
<string name="waiting_for_disconnection">Waiting for disconnection</string>
<!-- Custom Watchface -->
<string name="key_custom_watchface" translatable="false">key_custom_watchface</string>
<string name="metadata_label_watchface_created_at">Created at: %1$s</string>
<string name="metadata_label_watchface_author">Author: %1$s</string>
<string name="metadata_label_watchface_name">Name: %1$s</string>
<string name="metadata_wear_import_filename">File name: %1$s</string>
<string name="metadata_label_watchface_version">Watchface version: %1$s</string>
<string name="wear_default_watchface">Default Watchface</string>

View file

@ -1,21 +0,0 @@
package info.nightscout.interfaces.maintenance
import android.os.Parcelable
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap
import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap
import info.nightscout.rx.weardata.DrawableData
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import java.io.File
data class CustomWatchfaceFile(
val name: String,
val file: File,
val baseDir: File,
val json: String,
val metadata: @RawValue CustomWatchfaceMetadataMap,
val drawableFiles: @RawValue CustomWatchfaceDrawableDataMap
)

View file

@ -2,7 +2,7 @@ package info.nightscout.interfaces.maintenance
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import info.nightscout.rx.weardata.EventData
import info.nightscout.rx.weardata.CustomWatchfaceData
interface ImportExportPrefs {
@ -11,7 +11,7 @@ interface ImportExportPrefs {
fun importSharedPreferences(fragment: Fragment)
fun importCustomWatchface(activity: FragmentActivity)
fun importCustomWatchface(fragment: Fragment)
fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface)
fun exportCustomWatchface(customWatchface: CustomWatchfaceData)
fun prefsFileExists(): Boolean
fun verifyStoragePermissions(fragment: Fragment, onGranted: Runnable)
fun exportSharedPreferences(f: Fragment)

View file

@ -1,5 +1,6 @@
package info.nightscout.interfaces.maintenance
import info.nightscout.rx.weardata.CustomWatchfaceData
import java.io.File
interface PrefFileListProvider {
@ -12,7 +13,7 @@ interface PrefFileListProvider {
fun newExportCsvFile(): File
fun newCwfFile(filename: String): File
fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList<PrefsFile>
fun listCustomWatchfaceFiles(): MutableList<CustomWatchfaceFile>
fun listCustomWatchfaceFiles(): MutableList<CustomWatchfaceData>
fun checkMetadata(metadata: Map<PrefsMetadataKey, PrefMetadata>): Map<PrefsMetadataKey, PrefMetadata>
fun formatExportedAgo(utcTime: String): String
}

View file

@ -14,7 +14,6 @@ import info.nightscout.configuration.maintenance.PrefFileListProviderImpl
import info.nightscout.configuration.maintenance.activities.CustomWatchfaceImportListActivity
import info.nightscout.configuration.maintenance.activities.LogSettingActivity
import info.nightscout.configuration.maintenance.activities.PrefImportListActivity
import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat
import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat
import info.nightscout.interfaces.AndroidPermission
import info.nightscout.interfaces.ConfigBuilder
@ -37,7 +36,6 @@ abstract class ConfigurationModule {
@ContributesAndroidInjector abstract fun contributesCsvExportWorker(): ImportExportPrefsImpl.CsvExportWorker
@ContributesAndroidInjector abstract fun contributesPrefImportListActivity(): PrefImportListActivity
@ContributesAndroidInjector abstract fun contributesCustomWatchfaceImportListActivity(): CustomWatchfaceImportListActivity
@ContributesAndroidInjector abstract fun contributesZipCustomWatchfaceFormat(): ZipCustomWatchfaceFormat
@ContributesAndroidInjector abstract fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat
@ContributesAndroidInjector abstract fun prefImportListProviderInjector(): PrefFileListProvider

View file

@ -22,7 +22,6 @@ import dagger.android.HasAndroidInjector
import info.nightscout.configuration.R
import info.nightscout.configuration.activities.DaggerAppCompatActivityWithResult
import info.nightscout.configuration.maintenance.dialogs.PrefImportSummaryDialog
import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat
import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat
import info.nightscout.core.ui.dialogs.OKDialog
import info.nightscout.core.ui.dialogs.TwoMessagesAlertDialog
@ -56,7 +55,9 @@ import info.nightscout.rx.events.EventAppExit
import info.nightscout.rx.events.EventDiaconnG8PumpLogReset
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
import info.nightscout.rx.weardata.EventData
import info.nightscout.rx.weardata.CustomWatchfaceData
import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey
import info.nightscout.rx.weardata.ZipWatchfaceFormat
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
import info.nightscout.shared.utils.DateUtil
@ -86,8 +87,7 @@ class ImportExportPrefsImpl @Inject constructor(
private val prefFileList: PrefFileListProvider,
private val uel: UserEntryLogger,
private val dateUtil: DateUtil,
private val uiInteraction: UiInteraction,
private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat
private val uiInteraction: UiInteraction
) : ImportExportPrefs {
override fun prefsFileExists(): Boolean {
@ -315,10 +315,10 @@ class ImportExportPrefsImpl @Inject constructor(
}
}
override fun exportCustomWatchface(customWatchface: EventData.ActionSetCustomWatchface) {
override fun exportCustomWatchface(customWatchface: CustomWatchfaceData) {
prefFileList.ensureExportDirExists()
val newFile = prefFileList.newCwfFile(customWatchface.name)
customWatchfaceCWFFormat.saveCustomWatchface(newFile,customWatchface)
val newFile = prefFileList.newCwfFile(customWatchface.metadata[CustomWatchfaceMetadataKey.CWF_FILENAME] ?:"")
ZipWatchfaceFormat.saveCustomWatchface(newFile, customWatchface)
}
override fun importSharedPreferences(activity: FragmentActivity, importFile: PrefsFile) {

View file

@ -6,10 +6,8 @@ import dagger.Lazy
import dagger.Reusable
import info.nightscout.androidaps.annotations.OpenForTesting
import info.nightscout.configuration.R
import info.nightscout.configuration.maintenance.formats.ZipCustomWatchfaceFormat
import info.nightscout.configuration.maintenance.formats.EncryptedPrefsFormat
import info.nightscout.interfaces.Config
import info.nightscout.interfaces.maintenance.CustomWatchfaceFile
import info.nightscout.interfaces.maintenance.PrefFileListProvider
import info.nightscout.interfaces.maintenance.PrefMetadata
import info.nightscout.interfaces.maintenance.PrefMetadataMap
@ -19,6 +17,8 @@ import info.nightscout.interfaces.maintenance.PrefsMetadataKey
import info.nightscout.interfaces.maintenance.PrefsStatus
import info.nightscout.interfaces.storage.Storage
import info.nightscout.interfaces.versionChecker.VersionCheckerUtils
import info.nightscout.rx.weardata.CustomWatchfaceData
import info.nightscout.rx.weardata.ZipWatchfaceFormat
import info.nightscout.shared.interfaces.ResourceHelper
import org.joda.time.DateTime
import org.joda.time.Days
@ -36,7 +36,6 @@ class PrefFileListProviderImpl @Inject constructor(
private val rh: ResourceHelper,
private val config: Lazy<Config>,
private val encryptedPrefsFormat: EncryptedPrefsFormat,
private val customWatchfaceCWFFormat: ZipCustomWatchfaceFormat,
private val storage: Storage,
private val versionCheckerUtils: VersionCheckerUtils,
context: Context
@ -91,14 +90,14 @@ class PrefFileListProviderImpl @Inject constructor(
return prefFiles
}
override fun listCustomWatchfaceFiles(): MutableList<CustomWatchfaceFile> {
val customWatchfaceFiles = mutableListOf<CustomWatchfaceFile>()
override fun listCustomWatchfaceFiles(): MutableList<CustomWatchfaceData> {
val customWatchfaceFiles = mutableListOf<CustomWatchfaceData>()
// searching dedicated dir, only for new CWF format
exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION) }.forEach { file ->
exportsPath.walk().filter { it.isFile && it.name.endsWith(ZipWatchfaceFormat.CUSTOM_WF_EXTENTION) }.forEach { file ->
// Here loadCustomWatchface will unzip, check and load CustomWatchface
customWatchfaceCWFFormat.loadCustomWatchface(file)?.also { customWatchface ->
customWatchfaceFiles.add(CustomWatchfaceFile(file.name, file, exportsPath, customWatchface.json, customWatchface.metadata, customWatchface.drawableDatas))
ZipWatchfaceFormat.loadCustomWatchface(file)?.also { customWatchface ->
customWatchfaceFiles.add(customWatchface)
}
}
@ -147,7 +146,7 @@ class PrefFileListProviderImpl @Inject constructor(
}
override fun newCwfFile(filename: String): File {
val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss"))
return File(exportsPath, "${filename}_$timeLocal${ZipCustomWatchfaceFormat.CUSTOM_WF_EXTENTION}")
return File(exportsPath, "${filename}_$timeLocal${ZipWatchfaceFormat.CUSTOM_WF_EXTENTION}")
}
// check metadata for known issues, change their status and add info with explanations

View file

@ -10,7 +10,6 @@ import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import info.nightscout.core.ui.activities.TranslatedDaggerAppCompatActivity
import info.nightscout.interfaces.maintenance.CustomWatchfaceFile
import info.nightscout.interfaces.maintenance.PrefFileListProvider
import info.nightscout.configuration.databinding.CustomWatchfaceImportListActivityBinding
import info.nightscout.configuration.R
@ -18,6 +17,7 @@ import info.nightscout.configuration.databinding.CustomWatchfaceImportListItemBi
import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventMobileDataToWear
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.weardata.CustomWatchfaceData
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey
import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey.*
import info.nightscout.rx.weardata.EventData
@ -51,7 +51,7 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() {
binding.recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listCustomWatchfaceFiles())
}
inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List<CustomWatchfaceFile>) : RecyclerView.Adapter<RecyclerViewAdapter.PrefFileViewHolder>() {
inner class RecyclerViewAdapter internal constructor(private var customWatchfaceFileList: List<CustomWatchfaceData>) : RecyclerView.Adapter<RecyclerViewAdapter.PrefFileViewHolder>() {
inner class PrefFileViewHolder(val customWatchfaceImportListItemBinding: CustomWatchfaceImportListItemBinding) : RecyclerView.ViewHolder(customWatchfaceImportListItemBinding.root) {
@ -59,14 +59,12 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() {
with(customWatchfaceImportListItemBinding) {
root.isClickable = true
customWatchfaceImportListItemBinding.root.setOnClickListener {
val customWatchfaceFile = filelistName.tag as CustomWatchfaceFile
val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile.metadata[CWF_NAME] ?:"", customWatchfaceFile.json, customWatchfaceFile.drawableFiles)
sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWF.serialize())
val customWatchfaceFile = filelistName.tag as CustomWatchfaceData
val customWF = EventData.ActionSetCustomWatchface(customWatchfaceFile)
val i = Intent()
setResult(FragmentActivity.RESULT_OK, i)
//rxBus.send(EventWearUpdateGui(customWatchfaceFile))
rxBus.send(EventMobileDataToWear(customWF))
aapsLogger.debug("XXXXX EventMobileDataToWear sent")
finish()
}
}
@ -85,13 +83,12 @@ class CustomWatchfaceImportListActivity: TranslatedDaggerAppCompatActivity() {
override fun onBindViewHolder(holder: PrefFileViewHolder, position: Int) {
val customWatchfaceFile = customWatchfaceFileList[position]
val metadata = customWatchfaceFile.metadata
val drawable = customWatchfaceFile.drawableFiles[CustomWatchfaceDrawableDataKey
val drawable = customWatchfaceFile.drawableDatas[CustomWatchfaceDrawableDataKey
.CUSTOM_WATCHFACE]?.toDrawable(resources)
with(holder.customWatchfaceImportListItemBinding) {
filelistName.text = rh.gs(R.string.wear_import_filename, customWatchfaceFile.file.name)
filelistName.text = rh.gs(info.nightscout.shared.R.string.metadata_wear_import_filename, metadata[CWF_FILENAME])
filelistName.tag = customWatchfaceFile
customWatchface.setImageDrawable(drawable)
filelistDir.text = rh.gs(R.string.wear_import_directory, customWatchfaceFile.file.parentFile?.absolutePath)
customName.text = rh.gs(CWF_NAME.label, metadata[CWF_NAME])
author.text = rh.gs(CWF_AUTHOR.label, metadata[CWF_AUTHOR] ?:"")
createdAt.text = rh.gs(CWF_CREATED_AT.label, metadata[CWF_CREATED_AT] ?:"")

View file

@ -1,128 +0,0 @@
package info.nightscout.configuration.maintenance.formats
import info.nightscout.core.utils.CryptoUtil
import info.nightscout.interfaces.storage.Storage
import info.nightscout.rx.weardata.CustomWatchface
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap
import info.nightscout.rx.weardata.CustomWatchfaceFormat
import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey
import info.nightscout.rx.weardata.CustomWatchfaceMetadataMap
import info.nightscout.rx.weardata.DrawableData
import info.nightscout.rx.weardata.DrawableFormat
import info.nightscout.rx.weardata.EventData
import info.nightscout.shared.interfaces.ResourceHelper
import org.json.JSONObject
import java.io.BufferedOutputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ZipCustomWatchfaceFormat @Inject constructor(
private var rh: ResourceHelper,
private var cryptoUtil: CryptoUtil,
private var storage: Storage
) : CustomWatchfaceFormat {
companion object {
const val CUSTOM_WF_EXTENTION = ".zip"
const val CUSTOM_JSON_FILE = "CustomWatchface.json"
}
override fun loadCustomWatchface(cwfFile: File): CustomWatchface? {
var json = JSONObject()
var metadata: CustomWatchfaceMetadataMap = mutableMapOf()
val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf()
try {
val zipInputStream = ZipInputStream(cwfFile.inputStream())
var zipEntry: ZipEntry? = zipInputStream.nextEntry
while (zipEntry != null) {
val entryName = zipEntry.name
val buffer = ByteArray(2048)
val byteArrayOutputStream = ByteArrayOutputStream()
var count = zipInputStream.read(buffer)
while (count != -1) {
byteArrayOutputStream.write(buffer, 0, count)
count = zipInputStream.read(buffer)
}
zipInputStream.closeEntry()
if (entryName == CUSTOM_JSON_FILE) {
val jsonString = byteArrayOutputStream.toByteArray().toString(Charsets.UTF_8)
json = JSONObject(jsonString)
metadata = loadMetadata(json)
} else {
val customWatchfaceDrawableDataKey = CustomWatchfaceDrawableDataKey.fromFileName(entryName)
val drawableFormat = DrawableFormat.fromFileName(entryName)
if (customWatchfaceDrawableDataKey != CustomWatchfaceDrawableDataKey.UNKNOWN && drawableFormat != DrawableFormat.UNKNOWN) {
drawableDatas[customWatchfaceDrawableDataKey] = DrawableData(byteArrayOutputStream.toByteArray(),drawableFormat)
}
}
zipEntry = zipInputStream.nextEntry
}
// Valid CWF file must contains a valid json file with a name within metadata and a custom watchface image
if (metadata.containsKey(CustomWatchfaceMetadataKey.CWF_NAME) && drawableDatas.containsKey(CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE))
return CustomWatchface(json.toString(4), metadata, drawableDatas)
else
return null
} catch (e: Exception) {
return null
}
}
override fun saveCustomWatchface(file: File, customWatchface: EventData.ActionSetCustomWatchface) {
try {
val outputStream = FileOutputStream(file)
val zipOutputStream = ZipOutputStream(BufferedOutputStream(outputStream))
// Ajouter le fichier JSON au ZIP
val jsonEntry = ZipEntry(CUSTOM_JSON_FILE)
zipOutputStream.putNextEntry(jsonEntry)
zipOutputStream.write(customWatchface.json.toByteArray())
zipOutputStream.closeEntry()
// Ajouter les fichiers divers au ZIP
for (drawableData in customWatchface.drawableDataMap) {
val fileEntry = ZipEntry("${drawableData.key.fileName}.${drawableData.value.format.extension}")
zipOutputStream.putNextEntry(fileEntry)
zipOutputStream.write(drawableData.value.value)
zipOutputStream.closeEntry()
}
zipOutputStream.close()
outputStream.close()
} catch (e: Exception) {
}
}
override fun loadMetadata(contents: JSONObject): CustomWatchfaceMetadataMap {
val metadata: CustomWatchfaceMetadataMap = mutableMapOf()
if (contents.has("metadata")) {
val meta = contents.getJSONObject("metadata")
for (key in meta.keys()) {
val metaKey = CustomWatchfaceMetadataKey.fromKey(key)
if (metaKey != null) {
metadata[metaKey] = meta.getString(key)
}
}
}
return metadata
}
}

View file

@ -59,23 +59,6 @@
android:textColor="?attr/importListFileNameColor"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/filelist_dir"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="0dp"
android:ellipsize="none"
android:maxLines="2"
android:paddingStart="0dp"
android:paddingEnd="10dp"
android:scrollHorizontally="false"
android:text="File dir here"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?attr/importListFileNameColor"
android:textSize="11sp"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/filelist_name"
android:layout_width="wrap_content"

View file

@ -166,8 +166,6 @@
<!-- Custom Watchface -->
<string name="wear_import_custom_watchface_title">Select Custom Watchface</string>
<string name="wear_import_directory" comment="placeholder is for imported watchface path">Directory: %1$s</string>
<string name="wear_import_filename">File name: %1$s</string>
<!-- Permissions -->
<string name="alert_dialog_storage_permission_text">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)!</string>

View file

@ -17,6 +17,7 @@ import info.nightscout.rx.events.EventMobileToWear
import info.nightscout.rx.events.EventWearUpdateGui
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey
import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey
import info.nightscout.rx.weardata.EventData
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
@ -61,13 +62,11 @@ class WearFragment : DaggerFragment() {
}
}
binding.defaultCustom.setOnClickListener {
sp.remove(info.nightscout.shared.R.string.key_custom_watchface)
wearPlugin.savedCustomWatchface = null
rxBus.send(EventMobileToWear(EventData.ActionrequestSetDefaultWatchface(dateUtil.now())))
updateGui()
}
binding.sendCustom.setOnClickListener {
wearPlugin.savedCustomWatchface?.let { cwf -> rxBus.send(EventMobileDataToWear(cwf)) }
wearPlugin.savedCustomWatchface?.let { cwf -> rxBus.send(EventMobileDataToWear(EventData.ActionSetCustomWatchface(cwf))) }
}
binding.exportCustom.setOnClickListener {
wearPlugin.savedCustomWatchface?.let { importExportPrefs.exportCustomWatchface(it) }
@ -80,16 +79,11 @@ class WearFragment : DaggerFragment() {
disposable += rxBus
.toObservable(EventWearUpdateGui::class.java)
.observeOn(aapsSchedulers.main)
.subscribe({ updateGui() }, fabricPrivacy::logException)
disposable += rxBus
.toObservable(EventMobileDataToWear::class.java)
.observeOn(aapsSchedulers.main)
.subscribe({
loadCustom(it.payload)
wearPlugin.customWatchfaceSerialized = ""
wearPlugin.savedCustomWatchface = null
it.customWatchfaceData?.let { wearPlugin.savedCustomWatchface = it }
if (it.exportFile)
ToastUtils.okToast(context,rh.gs(R.string.wear_new_custom_watchface_received))
updateGui()
ToastUtils.okToast(context,rh.gs(R.string.wear_new_custom_watchface_received))
}, fabricPrivacy::logException)
if (wearPlugin.savedCustomWatchface == null)
rxBus.send(EventMobileToWear(EventData.ActionrequestCustomWatchface(false)))
@ -110,24 +104,10 @@ class WearFragment : DaggerFragment() {
private fun updateGui() {
_binding ?: return
sp.getString(info.nightscout.shared.R.string.key_custom_watchface, "").let {
if (it != wearPlugin.customWatchfaceSerialized && it != "") {
aapsLogger.debug("XXXXX Serialisation: ${it.length}")
try {
wearPlugin.savedCustomWatchface = (EventData.deserialize(it) as EventData.ActionSetCustomWatchface)
wearPlugin.customWatchfaceSerialized = it
}
catch(e: Exception) {
wearPlugin.customWatchfaceSerialized = ""
wearPlugin.savedCustomWatchface = null
}
}
sp.remove(info.nightscout.shared.R.string.key_custom_watchface)
}
wearPlugin.savedCustomWatchface?.let {
binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.name)
binding.customName.text = rh.gs(R.string.wear_custom_watchface, it.metadata[CustomWatchfaceMetadataKey.CWF_NAME])
binding.sendCustom.visibility = View.VISIBLE
binding.coverChart.setImageDrawable(it.drawableDataMap[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE]?.toDrawable(resources))
binding.coverChart.setImageDrawable(it.drawableDatas[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE]?.toDrawable(resources))
} ?:apply {
binding.customName.text = rh.gs(R.string.wear_custom_watchface, rh.gs(info.nightscout.shared.R.string.wear_default_watchface))
binding.sendCustom.visibility = View.INVISIBLE
@ -137,7 +117,6 @@ class WearFragment : DaggerFragment() {
}
private fun loadCustom(cwf: EventData.ActionSetCustomWatchface) {
aapsLogger.debug("XXXXX EventWearCwfExported received")
wearPlugin.savedCustomWatchface = cwf
wearPlugin.savedCustomWatchface = cwf.customWatchfaceData
}
}

View file

@ -18,6 +18,7 @@ import info.nightscout.rx.events.EventMobileToWear
import info.nightscout.rx.events.EventOverviewBolusProgress
import info.nightscout.rx.events.EventPreferenceChange
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.weardata.CustomWatchfaceData
import info.nightscout.rx.weardata.EventData
import info.nightscout.shared.interfaces.ResourceHelper
import info.nightscout.shared.sharedPreferences.SP
@ -55,7 +56,7 @@ class WearPlugin @Inject constructor(
var connectedDevice = "---"
var customWatchfaceSerialized = ""
var savedCustomWatchface: EventData.ActionSetCustomWatchface? = null
var savedCustomWatchface: CustomWatchfaceData? = null
override fun onStart() {
super.onStart()

View file

@ -60,7 +60,6 @@ import info.nightscout.plugins.R
import info.nightscout.rx.AapsSchedulers
import info.nightscout.rx.bus.RxBus
import info.nightscout.rx.events.EventMobileToWear
import info.nightscout.rx.events.EventWearCwfExported
import info.nightscout.rx.events.EventWearUpdateGui
import info.nightscout.rx.logging.AAPSLogger
import info.nightscout.rx.logging.LTag
@ -1262,14 +1261,10 @@ class DataHandlerMobile @Inject constructor(
private fun handleGetCustomWatchface(command: EventData.ActionGetCustomWatchface) {
val customWatchface = command.customWatchface
aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.json}")
//Update Wear Fragment
sp.putString(info.nightscout.shared.R.string.key_custom_watchface, customWatchface.serialize())
//rxBus.send(EventWearCwfExported(customWatchface))
rxBus.send(EventWearUpdateGui())
aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${command.sourceNodeId}: ${customWatchface.customWatchfaceData.json}")
rxBus.send(EventWearUpdateGui(customWatchface.customWatchfaceData, command.exportFile))
if (command.exportFile)
importExportPrefs.exportCustomWatchface(customWatchface)
//Implement here a record within SP and a save within exports subfolder as zipFile
importExportPrefs.exportCustomWatchface(customWatchface.customWatchfaceData)
}

View file

@ -45,6 +45,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.tasks.await
import kotlinx.serialization.ExperimentalSerializationApi
import javax.inject.Inject
class DataLayerListenerServiceMobile : WearableListenerService() {
@ -82,7 +83,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() {
private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge)
private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge)
@ExperimentalSerializationApi
override fun onCreate() {
AndroidInjection.inject(this)
super.onCreate()
@ -131,7 +132,7 @@ class DataLayerListenerServiceMobile : WearableListenerService() {
}
super.onDataChanged(dataEvents)
}
@ExperimentalSerializationApi
override fun onMessageReceived(messageEvent: MessageEvent) {
super.onMessageReceived(messageEvent)

View file

@ -186,6 +186,9 @@ class DataHandlerWear @Inject constructor(
.subscribe {
aapsLogger.debug(LTag.WEAR, "Custom Watchface received from ${it.sourceNodeId}")
persistence.store(it)
persistence.readCustomWatchface()?.let {
rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false)))
}
}
disposable += rxBus
.toObservable(EventData.ActionrequestSetDefaultWatchface::class.java)
@ -193,6 +196,9 @@ class DataHandlerWear @Inject constructor(
.subscribe {
aapsLogger.debug(LTag.WEAR, "Set Default Watchface received from ${it.sourceNodeId}")
persistence.setDefaultWatchface()
persistence.readCustomWatchface()?.let {
rxBus.send(EventWearDataToMobile(EventData.ActionGetCustomWatchface(it, false)))
}
}
disposable += rxBus
.toObservable(EventData.ActionrequestCustomWatchface::class.java)

View file

@ -22,6 +22,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import kotlinx.coroutines.*
import kotlinx.coroutines.tasks.await
import kotlinx.serialization.ExperimentalSerializationApi
import javax.inject.Inject
class DataLayerListenerServiceWear : WearableListenerService() {
@ -45,7 +46,7 @@ class DataLayerListenerServiceWear : WearableListenerService() {
private val rxPath get() = getString(info.nightscout.shared.R.string.path_rx_bridge)
private val rxDataPath get() = getString(info.nightscout.shared.R.string.path_rx_data_bridge)
@ExperimentalSerializationApi
override fun onCreate() {
AndroidInjection.inject(this)
super.onCreate()
@ -95,7 +96,7 @@ class DataLayerListenerServiceWear : WearableListenerService() {
}
super.onDataChanged(dataEvents)
}
@ExperimentalSerializationApi
override fun onMessageReceived(messageEvent: MessageEvent) {
super.onMessageReceived(messageEvent)

View file

@ -172,7 +172,7 @@ open class Persistence @Inject constructor(
fun store(customWatchface: EventData.ActionSetCustomWatchface, isdefault: Boolean = false) {
putString(if (isdefault) CUSTOM_DEFAULT_WATCHFACE else CUSTOM_WATCHFACE, customWatchface.serialize())
aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.name} ${isdefault}: $customWatchface")
aapsLogger.debug(LTag.WEAR, "Stored Custom Watchface ${customWatchface.customWatchfaceData} ${isdefault}: $customWatchface")
}
fun setDefaultWatchface() {

View file

@ -26,12 +26,14 @@ import androidx.viewbinding.ViewBinding
import info.nightscout.androidaps.R
import info.nightscout.androidaps.databinding.ActivityCustomBinding
import info.nightscout.androidaps.watchfaces.utils.BaseWatchFace
import info.nightscout.rx.weardata.CustomWatchfaceData
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataKey
import info.nightscout.rx.weardata.CustomWatchfaceDrawableDataMap
import info.nightscout.rx.weardata.CustomWatchfaceMetadataKey
import info.nightscout.rx.weardata.DrawableData
import info.nightscout.rx.weardata.DrawableFormat
import info.nightscout.rx.weardata.EventData
import info.nightscout.rx.weardata.ZipWatchfaceFormat
import info.nightscout.shared.extensions.toVisibility
import org.joda.time.TimeOfDay
import org.json.JSONObject
@ -111,23 +113,14 @@ class CustomWatchface : BaseWatchFace() {
binding.second.text = dateUtil.secondString()
// rotate the second hand.
binding.secondHand.rotation = TimeOfDay().secondOfMinute * 6f
//aapsLogger.debug("XXXXXX Setsecond calles:")
}
private fun setWatchfaceStyle() {
bgColor = when (singleBg.sgvLevel) {
1L -> highColor
0L -> midColor
-1L -> lowColor
else -> midColor
}
val customWatchface = persistence.readCustomWatchface() ?: persistence.readCustomWatchface(true)
//aapsLogger.debug("XXXXX + setWatchfaceStyle Json ${customWatchface?.json}")
customWatchface?.let { customWatchface ->
val json = JSONObject(customWatchface.json)
val drawableDataMap = customWatchface.drawableDataMap
val json = JSONObject(customWatchface.customWatchfaceData.json)
val drawableDataMap = customWatchface.customWatchfaceData.drawableDatas
enableSecond = (if (json.has("enableSecond")) json.getBoolean("enableSecond") else false) && sp.getBoolean(R.string.key_show_seconds, true)
//aapsLogger.debug("XXXXXX json File (beginning):" + customWatchface.json)
highColor = if (json.has("highColor")) Color.parseColor(json.getString("highColor")) else ContextCompat.getColor(this, R.color.dark_highColor)
midColor = if (json.has("midColor")) Color.parseColor(json.getString("midColor")) else ContextCompat.getColor(this, R.color.inrange)
@ -136,13 +129,16 @@ class CustomWatchface : BaseWatchFace() {
carbColor = if (json.has("carbColor")) Color.parseColor(json.getString("carbColor")) else ContextCompat.getColor(this, R.color.carbs)
gridColor = if (json.has("gridColor")) Color.parseColor(json.getString("gridColor")) else ContextCompat.getColor(this, R.color.carbs)
pointSize = if (json.has("pointSize")) json.getInt("pointSize") else 2
aapsLogger.debug("XXXXXX enableSecond $enableSecond ${sp.getBoolean(R.string.key_show_seconds, false)} pointSize $pointSize")
bgColor = when (singleBg.sgvLevel) {
1L -> highColor
0L -> midColor
-1L -> lowColor
else -> midColor
}
binding.mainLayout.forEach { view ->
//aapsLogger.debug("XXXXXX view:" + view.tag.toString())
view.tag?.let { tag ->
if (json.has(tag.toString())) {
var viewjson = json.getJSONObject(tag.toString())
//aapsLogger.debug("XXXXXX \"" + tag.toString() + "\": " + viewjson.toString(4))
var wrapContent = LayoutParams.WRAP_CONTENT
val width = if (viewjson.has("width")) (viewjson.getInt("width") * zoomFactor).toInt() else wrapContent
val height = if (viewjson.has("height")) (viewjson.getInt("height") * zoomFactor).toInt() else wrapContent
@ -191,6 +187,7 @@ class CustomWatchface : BaseWatchFace() {
private fun defaultWatchface(): EventData.ActionSetCustomWatchface {
val metadata = JSONObject()
.put(CustomWatchfaceMetadataKey.CWF_NAME.key, getString(info.nightscout.shared.R.string.wear_default_watchface))
.put(CustomWatchfaceMetadataKey.CWF_FILENAME.key, getString(R.string.wear_default_watchface))
.put(CustomWatchfaceMetadataKey.CWF_AUTHOR.key, "Philoul")
.put(CustomWatchfaceMetadataKey.CWF_CREATED_AT.key, dateUtil.dateString(dateUtil.now()))
.put(CustomWatchfaceMetadataKey.CWF_VERSION.key, CUSTOM_VERSION)
@ -248,12 +245,13 @@ class CustomWatchface : BaseWatchFace() {
)
}
}
val drawableDatas: CustomWatchfaceDrawableDataMap = mutableMapOf()
val metadataMap = ZipWatchfaceFormat.loadMetadata(json)
val drawableDataMap: CustomWatchfaceDrawableDataMap = mutableMapOf()
getResourceByteArray(info.nightscout.shared.R.drawable.watchface_custom)?.let {
val drawableDataMap = DrawableData(it,DrawableFormat.PNG)
drawableDatas[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE] = drawableDataMap
val drawableData = DrawableData(it,DrawableFormat.PNG)
drawableDataMap[CustomWatchfaceDrawableDataKey.CUSTOM_WATCHFACE] = drawableData
}
return EventData.ActionSetCustomWatchface(getString(info.nightscout.shared.R.string.wear_default_watchface),json.toString(4),drawableDatas)
return EventData.ActionSetCustomWatchface(CustomWatchfaceData(json.toString(4), metadataMap, drawableDataMap))
}
private fun setDefaultColors() {