From 3c4abe937836a544fbd9fc3bff69da6b4fb92491 Mon Sep 17 00:00:00 2001 From: Andries Smit Date: Wed, 1 Jun 2022 14:29:01 +0200 Subject: [PATCH] chore: wear last java to kotlin --- .../complications/BrCobIobComplication.kt | 7 +- .../complications/CobIobComplication.kt | 5 +- .../complications/IobIconComplication.kt | 5 +- .../interaction/menus/FillMenuActivity.kt | 4 +- .../interaction/menus/MainMenuActivity.kt | 4 +- .../interaction/menus/StatusMenuActivity.kt | 4 +- .../interaction/utils/DisplayFormat.java | 158 ------------ .../interaction/utils/DisplayFormat.kt | 135 ++++++++++ .../interaction/utils/MenuListActivity.java | 160 ------------ .../interaction/utils/MenuListActivity.kt | 129 ++++++++++ .../interaction/utils/Persistence.kt | 3 +- .../interaction/utils/PlusMinusEditText.java | 235 ------------------ .../interaction/utils/PlusMinusEditText.kt | 217 ++++++++++++++++ .../utils/SmallestDoubleString.java | 135 ---------- .../interaction/utils/SmallestDoubleString.kt | 102 ++++++++ .../androidaps/interaction/utils/WearUtil.kt | 4 +- .../androidaps/tile/ActionsTileService.kt | 1 + .../androidaps/tile/QuickWizardTileService.kt | 1 + .../androidaps/tile/TempTargetTileService.kt | 1 + .../tile/{ => source}/ActionSource.kt | 2 +- .../tile/{ => source}/QuickWizardSource.kt | 4 +- .../tile/{ => source}/StaticTileSource.kt | 5 +- .../tile/{ => source}/TempTargetSource.kt | 2 +- .../androidaps/watchfaces/BaseWatchFace.kt | 25 +- .../watchfaces/BigChartWatchface.kt | 4 +- .../androidaps/watchfaces/CircleWatchface.kt | 4 +- .../res/xml/tile_configuration_activity.xml | 27 +- .../main/res/xml/tile_configuration_tempt.xml | 27 +- 28 files changed, 655 insertions(+), 755 deletions(-) delete mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.kt delete mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.kt delete mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.kt delete mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.kt rename wear/src/main/java/info/nightscout/androidaps/tile/{ => source}/ActionSource.kt (95%) rename wear/src/main/java/info/nightscout/androidaps/tile/{ => source}/QuickWizardSource.kt (93%) rename wear/src/main/java/info/nightscout/androidaps/tile/{ => source}/StaticTileSource.kt (90%) rename wear/src/main/java/info/nightscout/androidaps/tile/{ => source}/TempTargetSource.kt (96%) diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.kt b/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.kt index 308aaf2d73..3a6ee0716f 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.kt +++ b/wear/src/main/java/info/nightscout/androidaps/complications/BrCobIobComplication.kt @@ -7,6 +7,7 @@ import android.support.wearable.complications.ComplicationData import android.support.wearable.complications.ComplicationText import dagger.android.AndroidInjection import info.nightscout.androidaps.data.RawDisplayData +import info.nightscout.androidaps.interaction.utils.DisplayFormat import info.nightscout.androidaps.interaction.utils.SmallestDoubleString import info.nightscout.shared.logging.LTag import kotlin.math.max @@ -25,8 +26,8 @@ class BrCobIobComplication : BaseComplicationProviderService() { override fun buildComplicationData(dataType: Int, raw: RawDisplayData, complicationPendingIntent: PendingIntent): ComplicationData? { var complicationData: ComplicationData? = null if (dataType == ComplicationData.TYPE_SHORT_TEXT) { - val cob = SmallestDoubleString(raw.status.cob, SmallestDoubleString.Units.USE).minimise(displayFormat.MIN_FIELD_LEN_COB) - val iob = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(max(displayFormat.MIN_FIELD_LEN_IOB, displayFormat.MAX_FIELD_LEN_SHORT - 1 - cob.length)) + val cob = SmallestDoubleString(raw.status.cob, SmallestDoubleString.Units.USE).minimise(DisplayFormat.MIN_FIELD_LEN_COB) + val iob = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(max(DisplayFormat.MIN_FIELD_LEN_IOB, DisplayFormat.MAX_FIELD_LEN_SHORT - 1 - cob.length)) val builder = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) .setShortText(ComplicationText.plainText(displayFormat.basalRateSymbol() + raw.status.currentBasal)) .setShortTitle(ComplicationText.plainText("$cob $iob")) @@ -39,4 +40,4 @@ class BrCobIobComplication : BaseComplicationProviderService() { } override fun getProviderCanonicalName(): String = BrCobIobComplication::class.java.canonicalName!! -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.kt b/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.kt index 339f2fae85..f4bf3ed413 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.kt +++ b/wear/src/main/java/info/nightscout/androidaps/complications/CobIobComplication.kt @@ -6,6 +6,7 @@ import android.app.PendingIntent import android.support.wearable.complications.ComplicationData import android.support.wearable.complications.ComplicationText import info.nightscout.androidaps.data.RawDisplayData +import info.nightscout.androidaps.interaction.utils.DisplayFormat import info.nightscout.androidaps.interaction.utils.SmallestDoubleString import info.nightscout.shared.logging.LTag @@ -18,7 +19,7 @@ class CobIobComplication : BaseComplicationProviderService() { var complicationData: ComplicationData? = null if (dataType == ComplicationData.TYPE_SHORT_TEXT) { val cob = raw.status.cob - val iob = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(displayFormat.MAX_FIELD_LEN_SHORT) + val iob = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(DisplayFormat.MAX_FIELD_LEN_SHORT) val builder = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) .setShortText(ComplicationText.plainText(cob)) .setShortTitle(ComplicationText.plainText(iob)) @@ -31,4 +32,4 @@ class CobIobComplication : BaseComplicationProviderService() { } override fun getProviderCanonicalName(): String = CobIobComplication::class.java.canonicalName!! -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.kt b/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.kt index 7befcd9858..50742e22d0 100644 --- a/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.kt +++ b/wear/src/main/java/info/nightscout/androidaps/complications/IobIconComplication.kt @@ -8,6 +8,7 @@ import android.support.wearable.complications.ComplicationData import android.support.wearable.complications.ComplicationText import info.nightscout.androidaps.R import info.nightscout.androidaps.data.RawDisplayData +import info.nightscout.androidaps.interaction.utils.DisplayFormat import info.nightscout.androidaps.interaction.utils.SmallestDoubleString import info.nightscout.shared.logging.LTag @@ -19,7 +20,7 @@ class IobIconComplication : BaseComplicationProviderService() { override fun buildComplicationData(dataType: Int, raw: RawDisplayData, complicationPendingIntent: PendingIntent): ComplicationData? { var complicationData: ComplicationData? = null if (dataType == ComplicationData.TYPE_SHORT_TEXT) { - val iob = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(displayFormat.MAX_FIELD_LEN_SHORT) + val iob = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(DisplayFormat.MAX_FIELD_LEN_SHORT) val builder = ComplicationData.Builder(ComplicationData.TYPE_SHORT_TEXT) .setShortText(ComplicationText.plainText(iob)) .setIcon(Icon.createWithResource(this, R.drawable.ic_ins)) @@ -34,4 +35,4 @@ class IobIconComplication : BaseComplicationProviderService() { override fun getProviderCanonicalName(): String = IobIconComplication::class.java.canonicalName!! override fun getComplicationAction(): ComplicationAction = ComplicationAction.BOLUS -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/menus/FillMenuActivity.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/menus/FillMenuActivity.kt index 56889fca22..20ac94ad81 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/menus/FillMenuActivity.kt +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/menus/FillMenuActivity.kt @@ -15,7 +15,7 @@ class FillMenuActivity : MenuListActivity() { super.onCreate(savedInstanceState) } - override fun getElements(): List = + override fun provideElements(): List = ArrayList().apply { add(MenuItem(R.drawable.ic_canula, getString(R.string.action_preset_1))) add(MenuItem(R.drawable.ic_canula, getString(R.string.action_preset_2))) @@ -31,4 +31,4 @@ class FillMenuActivity : MenuListActivity() { getString(R.string.action_free_amount) -> startActivity(Intent(this, FillActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }) } } -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/menus/MainMenuActivity.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/menus/MainMenuActivity.kt index 9cc84c832c..5b24ece99c 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/menus/MainMenuActivity.kt +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/menus/MainMenuActivity.kt @@ -21,7 +21,7 @@ class MainMenuActivity : MenuListActivity() { rxBus.send(EventWearToMobile(ActionResendData("MainMenuListActivity"))) } - override fun getElements(): List = + override fun provideElements(): List = ArrayList().apply { if (!sp.getBoolean(R.string.key_wear_control, false)) { add(MenuItem(R.drawable.ic_settings, getString(R.string.menu_settings))) @@ -53,4 +53,4 @@ class MainMenuActivity : MenuListActivity() { getString(R.string.menu_ecarb) -> startActivity(Intent(this, ECarbActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }) } } -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/menus/StatusMenuActivity.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/menus/StatusMenuActivity.kt index 38ffeef840..f7abab22cc 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/menus/StatusMenuActivity.kt +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/menus/StatusMenuActivity.kt @@ -15,7 +15,7 @@ class StatusMenuActivity : MenuListActivity() { super.onCreate(savedInstanceState) } - override fun getElements(): List = + override fun provideElements(): List = ArrayList().apply { add(MenuItem(R.drawable.ic_status, getString(R.string.status_pump))) add(MenuItem(R.drawable.ic_loop_closed, getString(R.string.status_loop))) @@ -29,4 +29,4 @@ class StatusMenuActivity : MenuListActivity() { getString(R.string.status_tdd) -> rxBus.send(EventWearToMobile(ActionTddStatus(System.currentTimeMillis()))) } } -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java deleted file mode 100644 index 18f2f3ca81..0000000000 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.java +++ /dev/null @@ -1,158 +0,0 @@ -package info.nightscout.androidaps.interaction.utils; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import info.nightscout.androidaps.data.RawDisplayData; -import info.nightscout.shared.sharedPreferences.SP; - -@Singleton -public class DisplayFormat { - - @Inject SP sp; - @Inject WearUtil wearUtil; - - @Inject DisplayFormat() { - } - - /** - * Maximal and minimal lengths of fields/labels shown in complications, in characters - * For MAX values - above that WearOS and watch faces may start ellipsize (...) contents - * For MIN values - this is minimal length that can hold legible data - */ - public final int MAX_FIELD_LEN_LONG = 22; // this is found out empirical, for TYPE_LONG_TEXT - public final int MAX_FIELD_LEN_SHORT = 7; // according to Wear OS docs for TYPE_SHORT_TEXT - public final int MIN_FIELD_LEN_COB = 3; // since carbs are usually 0..99g - public final int MIN_FIELD_LEN_IOB = 3; // IoB can range from like .1U to 99U - - private boolean areComplicationsUnicode() { - return sp.getBoolean("complication_unicode", true); - } - - public String deltaSymbol() { - return areComplicationsUnicode() ? "\u0394" : ""; - } - - public String verticalSeparatorSymbol() { - return areComplicationsUnicode() ? "\u205E" : "|"; - } - - public String basalRateSymbol() { - return areComplicationsUnicode() ? "\u238D\u2006" : ""; - } - - public String shortTimeSince(final long refTime) { - - long deltaTimeMs = wearUtil.msSince(refTime); - - if (deltaTimeMs < Constants.MINUTE_IN_MS) { - return "0'"; - } else if (deltaTimeMs < Constants.HOUR_IN_MS) { - int minutes = (int) (deltaTimeMs / Constants.MINUTE_IN_MS); - return minutes + "'"; - } else if (deltaTimeMs < Constants.DAY_IN_MS) { - int hours = (int) (deltaTimeMs / Constants.HOUR_IN_MS); - return hours + "h"; - } else { - int days = (int) (deltaTimeMs / Constants.DAY_IN_MS); - if (days < 7) { - return days + "d"; - } else { - int weeks = days / 7; - return weeks + "w"; - } - } - } - - public String shortTrend(final RawDisplayData raw) { - String minutes = "--"; - if (raw.getSingleBg().getTimeStamp() > 0) { - minutes = shortTimeSince(raw.getSingleBg().getTimeStamp()); - } - - if (minutes.length() + raw.getSingleBg().getDelta().length() + deltaSymbol().length() + 1 <= MAX_FIELD_LEN_SHORT) { - return minutes + " " + deltaSymbol() + raw.getSingleBg().getDelta(); - } - - // that only optimizes obvious things like 0 before . or at end, + at beginning - String delta = (new SmallestDoubleString(raw.getSingleBg().getDelta())).minimise(MAX_FIELD_LEN_SHORT - 1); - if (minutes.length() + delta.length() + deltaSymbol().length() + 1 <= MAX_FIELD_LEN_SHORT) { - return minutes + " " + deltaSymbol() + delta; - } - - String shortDelta = (new SmallestDoubleString(raw.getSingleBg().getDelta())).minimise(MAX_FIELD_LEN_SHORT - (1 + minutes.length())); - - return minutes + " " + shortDelta; - } - - public String longGlucoseLine(final RawDisplayData raw) { - return raw.getSingleBg().getSgvString() + raw.getSingleBg().getSlopeArrow() + " " + deltaSymbol() + (new SmallestDoubleString(raw.getSingleBg().getDelta())).minimise(8) + " (" + shortTimeSince(raw.getSingleBg().getTimeStamp()) + ")"; - } - - public String longDetailsLine(final RawDisplayData raw) { - - final String SEP_LONG = " " + verticalSeparatorSymbol() + " "; - final String SEP_SHORT = " " + verticalSeparatorSymbol() + " "; - final int SEP_SHORT_LEN = SEP_SHORT.length(); - final String SEP_MIN = " "; - - String line = - raw.getStatus().getCob() + SEP_LONG + raw.getStatus().getIobSum() + SEP_LONG + basalRateSymbol() + raw.getStatus().getCurrentBasal(); - if (line.length() <= MAX_FIELD_LEN_LONG) { - return line; - } - line = raw.getStatus().getCob() + SEP_SHORT + raw.getStatus().getIobSum() + SEP_SHORT + raw.getStatus().getCurrentBasal(); - if (line.length() <= MAX_FIELD_LEN_LONG) { - return line; - } - - int remainingMax = MAX_FIELD_LEN_LONG - (raw.getStatus().getCob().length() + raw.getStatus().getCurrentBasal().length() + SEP_SHORT_LEN * 2); - final String smallestIoB = new SmallestDoubleString(raw.getStatus().getIobSum(), SmallestDoubleString.Units.USE).minimise(Math.max(MIN_FIELD_LEN_IOB, remainingMax)); - line = raw.getStatus().getCob() + SEP_SHORT + smallestIoB + SEP_SHORT + raw.getStatus().getCurrentBasal(); - if (line.length() <= MAX_FIELD_LEN_LONG) { - return line; - } - - remainingMax = MAX_FIELD_LEN_LONG - (smallestIoB.length() + raw.getStatus().getCurrentBasal().length() + SEP_SHORT_LEN * 2); - final String simplifiedCob = new SmallestDoubleString(raw.getStatus().getCob(), SmallestDoubleString.Units.USE).minimise(Math.max(MIN_FIELD_LEN_COB, remainingMax)); - - line = simplifiedCob + SEP_SHORT + smallestIoB + SEP_SHORT + raw.getStatus().getCurrentBasal(); - if (line.length() <= MAX_FIELD_LEN_LONG) { - return line; - } - - line = simplifiedCob + SEP_MIN + smallestIoB + SEP_MIN + raw.getStatus().getCurrentBasal(); - - return line; - } - - public Pair detailedIob(RawDisplayData raw) { - final String iob1 = new SmallestDoubleString(raw.getStatus().getIobSum(), SmallestDoubleString.Units.USE).minimise(MAX_FIELD_LEN_SHORT); - String iob2 = ""; - if (raw.getStatus().getIobDetail().contains("|")) { - String[] iobs = raw.getStatus().getIobDetail().replace("(", "").replace(")", "").split("\\|"); - - String iobBolus = new SmallestDoubleString(iobs[0]).minimise(MIN_FIELD_LEN_IOB); - if (iobBolus.trim().length() == 0) { - iobBolus = "--"; - } - String iobBasal = new SmallestDoubleString(iobs[1]).minimise((MAX_FIELD_LEN_SHORT - 1) - Math.max(MIN_FIELD_LEN_IOB, iobBolus.length())); - if (iobBasal.trim().length() == 0) { - iobBasal = "--"; - } - iob2 = iobBolus + " " + iobBasal; - } - return Pair.Companion.create(iob1, iob2); - } - - public Pair detailedCob(final RawDisplayData raw) { - SmallestDoubleString cobMini = new SmallestDoubleString(raw.getStatus().getCob(), SmallestDoubleString.Units.USE); - - String cob2 = ""; - if (cobMini.getExtra().length() > 0) { - cob2 = cobMini.getExtra() + cobMini.getUnits(); - } - final String cob1 = cobMini.minimise(MAX_FIELD_LEN_SHORT); - return Pair.Companion.create(cob1, cob2); - } -} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.kt new file mode 100644 index 0000000000..43eb8678dd --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/DisplayFormat.kt @@ -0,0 +1,135 @@ +package info.nightscout.androidaps.interaction.utils + + +import info.nightscout.androidaps.interaction.utils.Pair.Companion.create +import javax.inject.Singleton +import javax.inject.Inject +import info.nightscout.shared.sharedPreferences.SP +import info.nightscout.androidaps.data.RawDisplayData +import kotlin.math.max + +@Singleton +class DisplayFormat @Inject internal constructor() { + companion object { + const val MAX_FIELD_LEN_LONG = 22 // this is found out empirical, for TYPE_LONG_TEXT + const val MAX_FIELD_LEN_SHORT = 7 // according to Wear OS docs for TYPE_SHORT_TEXT + const val MIN_FIELD_LEN_COB = 3 // since carbs are usually 0..99g + const val MIN_FIELD_LEN_IOB = 3 // IoB can range from like .1U to 99U + } + @Inject lateinit var sp: SP + @Inject lateinit var wearUtil: WearUtil + + /** + * Maximal and minimal lengths of fields/labels shown in complications, in characters + * For MAX values - above that WearOS and watch faces may start ellipsize (...) contents + * For MIN values - this is minimal length that can hold legible data + */ + + private fun areComplicationsUnicode() = sp.getBoolean("complication_unicode", true) + + private fun deltaSymbol() = if (areComplicationsUnicode()) "\u0394" else "" + + private fun verticalSeparatorSymbol() = if (areComplicationsUnicode()) "\u205E" else "|" + + fun basalRateSymbol() = if (areComplicationsUnicode()) "\u238D\u2006" else "" + + fun shortTimeSince(refTime: Long): String { + val deltaTimeMs = wearUtil.msSince(refTime) + return if (deltaTimeMs < Constants.MINUTE_IN_MS) { + "0'" + } else if (deltaTimeMs < Constants.HOUR_IN_MS) { + val minutes = (deltaTimeMs / Constants.MINUTE_IN_MS).toInt() + "$minutes'" + } else if (deltaTimeMs < Constants.DAY_IN_MS) { + val hours = (deltaTimeMs / Constants.HOUR_IN_MS).toInt() + hours.toString() + "h" + } else { + val days = (deltaTimeMs / Constants.DAY_IN_MS).toInt() + if (days < 7) { + days.toString() + "d" + } else { + val weeks = days / 7 + weeks.toString() + "w" + } + } + } + + fun shortTrend(raw: RawDisplayData): String { + var minutes = "--" + if (raw.singleBg.timeStamp > 0) { + minutes = shortTimeSince(raw.singleBg.timeStamp) + } + if (minutes.length + raw.singleBg.delta.length + deltaSymbol().length + 1 <= MAX_FIELD_LEN_SHORT) { + return minutes + " " + deltaSymbol() + raw.singleBg.delta + } + + // that only optimizes obvious things like 0 before . or at end, + at beginning + val delta = SmallestDoubleString(raw.singleBg.delta).minimise(MAX_FIELD_LEN_SHORT - 1) + if (minutes.length + delta.length + deltaSymbol().length + 1 <= MAX_FIELD_LEN_SHORT) { + return minutes + " " + deltaSymbol() + delta + } + val shortDelta = SmallestDoubleString(raw.singleBg.delta).minimise(MAX_FIELD_LEN_SHORT - (1 + minutes.length)) + return "$minutes $shortDelta" + } + + fun longGlucoseLine(raw: RawDisplayData): String { + return raw.singleBg.sgvString + raw.singleBg.slopeArrow + " " + deltaSymbol() + SmallestDoubleString(raw.singleBg.delta).minimise(8) + " (" + shortTimeSince(raw.singleBg.timeStamp) + ")" + } + + fun longDetailsLine(raw: RawDisplayData): String { + val sepLong = " " + verticalSeparatorSymbol() + " " + val sepShort = " " + verticalSeparatorSymbol() + " " + val sepShortLen = sepShort.length + val sepMin = " " + var line = raw.status.cob + sepLong + raw.status.iobSum + sepLong + basalRateSymbol() + raw.status.currentBasal + if (line.length <= MAX_FIELD_LEN_LONG) { + return line + } + line = raw.status.cob + sepShort + raw.status.iobSum + sepShort + raw.status.currentBasal + if (line.length <= MAX_FIELD_LEN_LONG) { + return line + } + var remainingMax = MAX_FIELD_LEN_LONG - (raw.status.cob.length + raw.status.currentBasal.length + sepShortLen * 2) + val smallestIoB = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(max(MIN_FIELD_LEN_IOB, remainingMax)) + line = raw.status.cob + sepShort + smallestIoB + sepShort + raw.status.currentBasal + if (line.length <= MAX_FIELD_LEN_LONG) { + return line + } + remainingMax = MAX_FIELD_LEN_LONG - (smallestIoB.length + raw.status.currentBasal.length + sepShortLen * 2) + val simplifiedCob = SmallestDoubleString(raw.status.cob, SmallestDoubleString.Units.USE).minimise(max(MIN_FIELD_LEN_COB, remainingMax)) + line = simplifiedCob + sepShort + smallestIoB + sepShort + raw.status.currentBasal + if (line.length <= MAX_FIELD_LEN_LONG) { + return line + } + line = simplifiedCob + sepMin + smallestIoB + sepMin + raw.status.currentBasal + return line + } + + fun detailedIob(raw: RawDisplayData): Pair { + val iob1 = SmallestDoubleString(raw.status.iobSum, SmallestDoubleString.Units.USE).minimise(MAX_FIELD_LEN_SHORT) + var iob2 = "" + if (raw.status.iobDetail.contains("|")) { + val iobs = raw.status.iobDetail.replace("(", "").replace(")", "").split("\\|").toTypedArray() + var iobBolus = SmallestDoubleString(iobs[0]).minimise(MIN_FIELD_LEN_IOB) + if (iobBolus.trim { it <= ' ' }.isEmpty()) { + iobBolus = "--" + } + var iobBasal = SmallestDoubleString(iobs[1]).minimise(MAX_FIELD_LEN_SHORT - 1 - max(MIN_FIELD_LEN_IOB, iobBolus.length)) + if (iobBasal.trim { it <= ' ' }.isEmpty()) { + iobBasal = "--" + } + iob2 = "$iobBolus $iobBasal" + } + return create(iob1, iob2) + } + + fun detailedCob(raw: RawDisplayData): Pair { + val cobMini = SmallestDoubleString(raw.status.cob, SmallestDoubleString.Units.USE) + var cob2 = "" + if (cobMini.extra.isNotEmpty()) { + cob2 = cobMini.extra + cobMini.units + } + val cob1 = cobMini.minimise(MAX_FIELD_LEN_SHORT) + return create(cob1, cob2) + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.java deleted file mode 100644 index 3f55afebd2..0000000000 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.java +++ /dev/null @@ -1,160 +0,0 @@ -package info.nightscout.androidaps.interaction.utils; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import androidx.wear.widget.CurvedTextView; -import androidx.wear.widget.WearableLinearLayoutManager; -import androidx.wear.widget.WearableRecyclerView; - -import java.util.List; - -import javax.inject.Inject; - -import dagger.android.DaggerActivity; -import info.nightscout.androidaps.R; -import info.nightscout.androidaps.plugins.bus.RxBus; -import info.nightscout.shared.sharedPreferences.SP; - -/** - * Created by adrian on 08/02/17. - */ - -public abstract class MenuListActivity extends DaggerActivity { - - @Inject public RxBus rxBus; - @Inject public SP sp; - - List elements; - - protected abstract List getElements(); - - protected abstract void doAction(String position); - - public interface AdapterCallback { - void onItemClicked(MenuAdapter.ItemViewHolder v); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.actions_list_activity); - setTitleBasedOnScreenShape(String.valueOf(getTitle())); - - elements = getElements(); - CustomScrollingLayoutCallback customScrollingLayoutCallback = new CustomScrollingLayoutCallback(); - WearableLinearLayoutManager layoutManager = new WearableLinearLayoutManager(this); - WearableRecyclerView listView = findViewById(R.id.action_list); - boolean isScreenRound = this.getResources().getConfiguration().isScreenRound(); - if (isScreenRound) { - layoutManager.setLayoutCallback(customScrollingLayoutCallback); - listView.setEdgeItemsCenteringEnabled(true); - } else { - // Bug in androidx.wear:wear:1.2.0 - // WearableRecyclerView setEdgeItemsCenteringEnabled requires fix for square screen - listView.setPadding(0, 50, 0, 0); - } - listView.setHasFixedSize(true); - listView.setLayoutManager(layoutManager); - listView.setAdapter(new MenuAdapter(elements, v -> { - String tag = (String) v.itemView.getTag(); - doAction(tag); - })); - } - - private void setTitleBasedOnScreenShape(String title) { - CurvedTextView titleViewCurved = findViewById(R.id.title_curved); - TextView titleView = findViewById(R.id.title); - if (this.getResources().getConfiguration().isScreenRound()) { - titleViewCurved.setText(title); - titleViewCurved.setVisibility(View.VISIBLE); - titleView.setVisibility((View.GONE)); - } else { - titleView.setText(title); - titleView.setVisibility(View.VISIBLE); - titleViewCurved.setVisibility((View.GONE)); - } - } - - private static class MenuAdapter extends RecyclerView.Adapter { - private final List mDataset; - private final AdapterCallback callback; - - public MenuAdapter(List dataset, AdapterCallback callback) { - mDataset = dataset; - this.callback = callback; - } - - public static class ItemViewHolder extends RecyclerView.ViewHolder { - protected final RelativeLayout menuContainer; - protected final TextView actionItem; - protected final ImageView actionIcon; - - public ItemViewHolder(View itemView) { - super(itemView); - menuContainer = itemView.findViewById(R.id.menu_container); - actionItem = itemView.findViewById(R.id.menuItemText); - actionIcon = itemView.findViewById(R.id.menuItemIcon); - } - } - - @NonNull @Override - public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false); - - return new ItemViewHolder(view); - } - - @Override - public void onBindViewHolder(ItemViewHolder holder, final int position) { - MenuItem item = mDataset.get(position); - holder.actionItem.setText(item.actionItem); - holder.actionIcon.setImageResource(item.actionIcon); - holder.itemView.setTag(item.actionItem); - holder.menuContainer.setOnClickListener(v -> callback.onItemClicked(holder)); - } - - @Override - public int getItemCount() { - return mDataset.size(); - } - } - - protected static class MenuItem { - public MenuItem(int actionIcon, String actionItem) { - this.actionIcon = actionIcon; - this.actionItem = actionItem; - } - - public int actionIcon; - public String actionItem; - } - - public static class CustomScrollingLayoutCallback extends WearableLinearLayoutManager.LayoutCallback { - // How much should we scale the icon at most. - private static final float MAX_ICON_PROGRESS = 0.65f; - - @Override - public void onLayoutFinished(View child, RecyclerView parent) { - // Figure out % progress from top to bottom - float centerOffset = ((float) child.getHeight() / 2.0f) / (float) parent.getHeight(); - float yRelativeToCenterOffset = (child.getY() / parent.getHeight()) + centerOffset; - - // Normalize for center - float progressToCenter = Math.abs(0.5f - yRelativeToCenterOffset); - // Adjust to the maximum scale - progressToCenter = Math.min(progressToCenter, MAX_ICON_PROGRESS); - - child.setScaleX(1 - progressToCenter); - child.setScaleY(1 - progressToCenter); - } - } - -} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.kt new file mode 100644 index 0000000000..b299e4f96e --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/MenuListActivity.kt @@ -0,0 +1,129 @@ +package info.nightscout.androidaps.interaction.utils + +import dagger.android.DaggerActivity +import javax.inject.Inject +import info.nightscout.androidaps.plugins.bus.RxBus +import info.nightscout.shared.sharedPreferences.SP +import info.nightscout.androidaps.interaction.utils.MenuListActivity.MenuAdapter.ItemViewHolder +import android.os.Bundle +import info.nightscout.androidaps.R +import androidx.wear.widget.WearableLinearLayoutManager +import androidx.wear.widget.CurvedTextView +import android.widget.TextView +import android.widget.RelativeLayout +import android.view.ViewGroup +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import androidx.wear.widget.WearableLinearLayoutManager.LayoutCallback +import androidx.wear.widget.WearableRecyclerView +import kotlin.math.abs +import kotlin.math.min + +/** + * Created by adrian on 08/02/17. + */ +abstract class MenuListActivity : DaggerActivity() { + + @Inject lateinit var sp: SP + @Inject lateinit var rxBus: RxBus + + private var elements: List = listOf() + protected abstract fun provideElements(): List + protected abstract fun doAction(position: String) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.actions_list_activity) + setTitleBasedOnScreenShape(title.toString()) + elements = provideElements() + val customScrollingLayoutCallback = CustomScrollingLayoutCallback() + val layoutManager = WearableLinearLayoutManager(this) + val listView = findViewById(R.id.action_list) + val isScreenRound = this.resources.configuration.isScreenRound + if (isScreenRound) { + layoutManager.layoutCallback = customScrollingLayoutCallback + listView.isEdgeItemsCenteringEnabled = true + } else { + // Bug in androidx.wear:wear:1.2.0 + // WearableRecyclerView setEdgeItemsCenteringEnabled requires fix for square screen + listView.setPadding(0, 50, 0, 0) + } + listView.setHasFixedSize(true) + listView.layoutManager = layoutManager + listView.adapter = MenuAdapter(elements) { v: ItemViewHolder -> + val tag = v.itemView.tag as String + doAction(tag) + } + } + + private fun setTitleBasedOnScreenShape(title: String) { + val titleViewCurved = findViewById(R.id.title_curved) + val titleView = findViewById(R.id.title) + if (this.resources.configuration.isScreenRound) { + titleViewCurved.text = title + titleViewCurved.visibility = View.VISIBLE + titleView.visibility = View.GONE + } else { + titleView.text = title + titleView.visibility = View.VISIBLE + titleViewCurved.visibility = View.GONE + } + } + + class MenuAdapter(private val mDataset: List, private val callback: (ItemViewHolder) -> Unit) : RecyclerView.Adapter() { + class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + val menuContainer: RelativeLayout + val actionItem: TextView + val actionIcon: ImageView + + init { + menuContainer = itemView.findViewById(R.id.menu_container) + actionItem = itemView.findViewById(R.id.menuItemText) + actionIcon = itemView.findViewById(R.id.menuItemIcon) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item, parent, false) + return ItemViewHolder(view) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val item = mDataset[position] + holder.actionItem.text = item.actionItem + holder.actionIcon.setImageResource(item.actionIcon) + holder.itemView.tag = item.actionItem + holder.menuContainer.setOnClickListener { callback(holder) } + } + + override fun getItemCount(): Int { + return mDataset.size + } + } + + class MenuItem(var actionIcon: Int, var actionItem: String) + class CustomScrollingLayoutCallback : LayoutCallback() { + + override fun onLayoutFinished(child: View, parent: RecyclerView) { + // Figure out % progress from top to bottom + val centerOffset = child.height.toFloat() / 2.0f / parent.height.toFloat() + val yRelativeToCenterOffset = child.y / parent.height + centerOffset + + // Normalize for center + var progressToCenter = abs(0.5f - yRelativeToCenterOffset) + // Adjust to the maximum scale + progressToCenter = min(progressToCenter, MAX_ICON_PROGRESS) + child.scaleX = 1 - progressToCenter + child.scaleY = 1 - progressToCenter + } + + companion object { + + // How much should we scale the icon at most. + private const val MAX_ICON_PROGRESS = 0.65f + } + } +} 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 bfad5b3571..96de0db6d9 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 @@ -1,6 +1,5 @@ package info.nightscout.androidaps.interaction.utils -import android.util.Base64 import info.nightscout.androidaps.utils.DateUtil import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.logging.LTag @@ -182,4 +181,4 @@ class Persistence @Inject constructor( aapsLogger.debug(LTag.WEAR, "TURNING OFF all active complications") putString(KEY_COMPLICATIONS, "") } -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.java deleted file mode 100644 index d5afde2473..0000000000 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.java +++ /dev/null @@ -1,235 +0,0 @@ -package info.nightscout.androidaps.interaction.utils; - -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.core.view.InputDeviceCompat; -import androidx.core.view.MotionEventCompat; - -import java.text.NumberFormat; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -/** - * Created by mike on 28.06.2016. - */ -public class PlusMinusEditText implements View.OnKeyListener, - View.OnTouchListener, View.OnClickListener, View.OnGenericMotionListener { - - public TextView editText; - ImageView minusImage; - ImageView plusImage; - - Double value; - Double minValue; - Double maxValue; - Double step; - NumberFormat formatter; - boolean allowZero; - boolean roundRobin; - - private int mChangeCounter = 0; - private long mLastChange = 0; - private final static int THRESHOLD_COUNTER = 5; - private final static int THRESHOLD_COUNTER_LONG = 10; - private final static int THRESHOLD_TIME = 100; - - private final Handler mHandler; - private ScheduledExecutorService mUpdater; - - private class UpdateCounterTask implements Runnable { - private final boolean mInc; - private int repeated = 0; - private int multiplier = 1; - - public UpdateCounterTask(boolean inc) { - mInc = inc; - } - - public void run() { - Message msg = new Message(); - int doubleLimit = 5; - if (repeated % doubleLimit == 0) multiplier *= 2; - repeated++; - msg.arg1 = multiplier; - msg.arg2 = repeated; - if (mInc) { - msg.what = MSG_INC; - } else { - msg.what = MSG_DEC; - } - mHandler.sendMessage(msg); - } - } - - private static final int MSG_INC = 0; - private static final int MSG_DEC = 1; - - public PlusMinusEditText(View view, int editTextID, int plusID, int minusID, Double initValue, Double minValue, Double maxValue, Double step, NumberFormat formatter, boolean allowZero) { - this(view, editTextID, plusID, minusID, initValue, minValue, maxValue, step, formatter, allowZero, false); - } - - public PlusMinusEditText(View view, int editTextID, int plusID, int minusID, Double initValue, Double minValue, Double maxValue, Double step, NumberFormat formatter, boolean allowZero, boolean roundRobin) { - editText = view.findViewById(editTextID); - minusImage = view.findViewById(minusID); - plusImage = view.findViewById(plusID); - - this.value = initValue; - this.minValue = minValue; - this.maxValue = maxValue; - this.step = step; - this.formatter = formatter; - this.allowZero = allowZero; - this.roundRobin = roundRobin; - - mHandler = new Handler(Looper.getMainLooper()) { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_INC: - inc(msg.arg1); - return; - case MSG_DEC: - dec(msg.arg1); - return; - } - super.handleMessage(msg); - } - }; - - minusImage.setOnTouchListener(this); - minusImage.setOnKeyListener(this); - minusImage.setOnClickListener(this); - plusImage.setOnTouchListener(this); - plusImage.setOnKeyListener(this); - plusImage.setOnClickListener(this); - editText.setOnGenericMotionListener(this); - updateEditText(); - } - - public void setValue(Double value) { - this.value = value; - updateEditText(); - } - - public Double getValue() { - return value; - } - - private void inc(int multiplier) { - value += step * multiplier; - if (value > maxValue) { - if (roundRobin) { - value = minValue; - } else { - value = maxValue; - stopUpdating(); - } - } - updateEditText(); - } - - private void dec(int multiplier) { - value -= step * multiplier; - if (value < minValue) { - if (roundRobin) { - value = maxValue; - } else { - value = minValue; - stopUpdating(); - } - } - updateEditText(); - } - - private void updateEditText() { - if (value == 0d && !allowZero) - editText.setText(""); - else - editText.setText(formatter.format(value)); - } - - private void startUpdating(boolean inc) { - if (mUpdater != null) { - return; - } - mUpdater = Executors.newSingleThreadScheduledExecutor(); - mUpdater.scheduleAtFixedRate(new UpdateCounterTask(inc), 200, 200, - TimeUnit.MILLISECONDS); - } - - private void stopUpdating() { - if (mUpdater != null) { - mUpdater.shutdownNow(); - mUpdater = null; - } - } - - @Override - public void onClick(View v) { - if (mUpdater == null) { - if (v == plusImage) { - inc(1); - } else { - dec(1); - } - } - } - - @Override - public boolean onKey(View v, int keyCode, KeyEvent event) { - boolean isKeyOfInterest = keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER; - boolean isReleased = event.getAction() == KeyEvent.ACTION_UP; - boolean isPressed = event.getAction() == KeyEvent.ACTION_DOWN; - - if (isKeyOfInterest && isReleased) { - stopUpdating(); - } else if (isKeyOfInterest && isPressed) { - startUpdating(v == plusImage); - } - return false; - } - - @Override - public boolean onTouch(View v, MotionEvent event) { - boolean isReleased = event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL; - boolean isPressed = event.getAction() == MotionEvent.ACTION_DOWN; - - if (isReleased) { - stopUpdating(); - } else if (isPressed) { - startUpdating(v == plusImage); - } - return false; - } - - @Override - public boolean onGenericMotion(View v, MotionEvent ev) { - if (ev.getAction() == MotionEvent.ACTION_SCROLL && ev.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) { - long now = System.currentTimeMillis(); - if (now - mLastChange > THRESHOLD_TIME) mChangeCounter = 0; - - int dynamicMultiplier = mChangeCounter < THRESHOLD_COUNTER ? 1 : - mChangeCounter < THRESHOLD_COUNTER_LONG ? 2 : 4; - - float delta = -ev.getAxisValue(MotionEventCompat.AXIS_SCROLL); - if (delta > 0) { - inc(dynamicMultiplier); - } else { - dec(dynamicMultiplier); - } - mLastChange = System.currentTimeMillis(); - mChangeCounter++; - return true; - } - return false; - } - -} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.kt new file mode 100644 index 0000000000..1ee994c9d3 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/PlusMinusEditText.kt @@ -0,0 +1,217 @@ +package info.nightscout.androidaps.interaction.utils + +import android.os.Handler +import kotlin.jvm.JvmOverloads +import android.view.View.OnTouchListener +import android.view.View.OnGenericMotionListener +import android.widget.TextView +import android.view.MotionEvent +import android.os.Looper +import android.os.Message +import android.view.KeyEvent +import android.view.View +import android.widget.ImageView +import androidx.core.view.InputDeviceCompat +import androidx.core.view.MotionEventCompat +import java.text.NumberFormat +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit + +/** + * Created by mike on 28.06.2016. + */ +class PlusMinusEditText @JvmOverloads constructor( + view: View, + editTextID: Int, + plusID: Int, + minusID: Int, + initValue: Double, + minValue: Double, + maxValue: Double, + step: Double, + formatter: NumberFormat, + allowZero: Boolean, + roundRobin: Boolean = false +) : View.OnKeyListener, OnTouchListener, View.OnClickListener, OnGenericMotionListener { + + var editText: TextView + private set + private var minusImage: ImageView + private var plusImage: ImageView + private var value: Double + private var minValue: Double + private var maxValue: Double + private var step: Double + private var formatter: NumberFormat + private var allowZero: Boolean + private var roundRobin: Boolean + private var mChangeCounter = 0 + private var mLastChange: Long = 0 + private val mHandler: Handler + private var mUpdater: ScheduledExecutorService? = null + + private inner class UpdateCounterTask(private val mInc: Boolean) : Runnable { + + private var repeated = 0 + private var multiplier = 1 + override fun run() { + val msg = Message() + val doubleLimit = 5 + if (repeated % doubleLimit == 0) multiplier *= 2 + repeated++ + msg.arg1 = multiplier + msg.arg2 = repeated + if (mInc) { + msg.what = MSG_INC + } else { + msg.what = MSG_DEC + } + mHandler.sendMessage(msg) + } + } + + private fun inc(multiplier: Int) { + value += step * multiplier + if (value > maxValue) { + if (roundRobin) { + value = minValue + } else { + value = maxValue + stopUpdating() + } + } + updateEditText() + } + + private fun dec(multiplier: Int) { + value -= step * multiplier + if (value < minValue) { + if (roundRobin) { + value = maxValue + } else { + value = minValue + stopUpdating() + } + } + updateEditText() + } + + private fun updateEditText() { + if (value == 0.0 && !allowZero) editText.text = "" else editText.text = formatter.format(value) + } + + private fun startUpdating(inc: Boolean) { + if (mUpdater != null) { + return + } + + mUpdater = Executors.newSingleThreadScheduledExecutor() + mUpdater?.scheduleAtFixedRate( + UpdateCounterTask(inc), 200, 200, + TimeUnit.MILLISECONDS + ) + } + + private fun stopUpdating() { + mUpdater?.shutdownNow() + mUpdater = null + } + + override fun onClick(v: View) { + if (mUpdater == null) { + if (v === plusImage) { + inc(1) + } else { + dec(1) + } + } + } + + override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean { + val isKeyOfInterest = keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER + val isReleased = event.action == KeyEvent.ACTION_UP + val isPressed = event.action == KeyEvent.ACTION_DOWN + if (isKeyOfInterest && isReleased) { + stopUpdating() + } else if (isKeyOfInterest && isPressed) { + startUpdating(v === plusImage) + } + return false + } + + override fun onTouch(v: View, event: MotionEvent): Boolean { + val isReleased = event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL + val isPressed = event.action == MotionEvent.ACTION_DOWN + if (isReleased) { + stopUpdating() + } else if (isPressed) { + startUpdating(v === plusImage) + } + return false + } + + override fun onGenericMotion(v: View, ev: MotionEvent): Boolean { + if (ev.action == MotionEvent.ACTION_SCROLL && ev.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) { + val now = System.currentTimeMillis() + if (now - mLastChange > THRESHOLD_TIME) mChangeCounter = 0 + val dynamicMultiplier = if (mChangeCounter < THRESHOLD_COUNTER) 1 else if (mChangeCounter < THRESHOLD_COUNTER_LONG) 2 else 4 + val delta = -ev.getAxisValue(MotionEventCompat.AXIS_SCROLL) + if (delta > 0) { + inc(dynamicMultiplier) + } else { + dec(dynamicMultiplier) + } + mLastChange = System.currentTimeMillis() + mChangeCounter++ + return true + } + return false + } + + companion object { + + private const val THRESHOLD_COUNTER = 5 + private const val THRESHOLD_COUNTER_LONG = 10 + private const val THRESHOLD_TIME = 100 + private const val MSG_INC = 0 + private const val MSG_DEC = 1 + } + + init { + editText = view.findViewById(editTextID) + minusImage = view.findViewById(minusID) + plusImage = view.findViewById(plusID) + value = initValue + this.minValue = minValue + this.maxValue = maxValue + this.step = step + this.formatter = formatter + this.allowZero = allowZero + this.roundRobin = roundRobin + mHandler = object : Handler(Looper.getMainLooper()) { + override fun handleMessage(msg: Message) { + when (msg.what) { + MSG_INC -> { + inc(msg.arg1) + return + } + + MSG_DEC -> { + dec(msg.arg1) + return + } + } + super.handleMessage(msg) + } + } + minusImage.setOnTouchListener(this) + minusImage.setOnKeyListener(this) + minusImage.setOnClickListener(this) + plusImage.setOnTouchListener(this) + plusImage.setOnKeyListener(this) + plusImage.setOnClickListener(this) + editText.setOnGenericMotionListener(this) + updateEditText() + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java deleted file mode 100644 index be59dbc127..0000000000 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.java +++ /dev/null @@ -1,135 +0,0 @@ -package info.nightscout.androidaps.interaction.utils; - -import java.math.RoundingMode; -import java.text.DecimalFormat; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Helper to minimise various floating point values, with or without unit, to fit into specified - * and limited size, scarifying precision (rounding up) and extra characters like leading zero, - * following zero(s) in fractional part, extra plus sign etc. - * - * Created by dlvoy on 2019-11-12 - */ -public class SmallestDoubleString { - - private String sign = ""; - private String decimal = ""; - private String separator = ""; - private String fractional = ""; - private String extra = ""; - private String units = ""; - - private final Units withUnits; - - public enum Units { - SKIP, - USE - } - - private static final Pattern pattern = Pattern.compile("^([+-]?)([0-9]*)([,.]?)([0-9]*)(\\([^)]*\\))?(.*?)$", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE ); - - public SmallestDoubleString(String inputString) { - this(inputString, Units.SKIP); - } - - public SmallestDoubleString(String inputString, Units withUnits) { - Matcher matcher = pattern.matcher(inputString); - matcher.matches(); - - sign = matcher.group(1); - decimal = matcher.group(2); - separator = matcher.group(3); - fractional = matcher.group(4); - units = matcher.group(6); - - if (fractional == null || fractional.length() == 0) { - separator = ""; - fractional = ""; - } - if (decimal == null || decimal.length() == 0) { - decimal = ""; - } - if (separator == null || separator.length() == 0) { - separator = ""; - } - if (sign == null || sign.length() == 0) { - sign = ""; - } - - final String extraCandidate = matcher.group(5); - if (extraCandidate != null && extraCandidate.length() > 2) { - extra = extraCandidate.substring(1, extraCandidate.length()-1); - } - - if (units != null) { - units = units.trim(); - } - - this.withUnits = withUnits; - } - - public String minimise(int maxSize) { - final String originalSeparator = separator; - - if (Integer.parseInt("0"+fractional) == 0) { - separator = ""; - fractional = ""; - } - if (Integer.parseInt("0"+decimal) == 0 && (fractional.length() >0)) { - decimal = ""; - } - if (currentLen() <= maxSize) - return toString(); - - if (sign.equals("+")) { - sign = ""; - } - if (currentLen() <= maxSize) { - return toString(); - } - - while ((fractional.length() > 1)&&(fractional.charAt(fractional.length()-1) == '0')) { - fractional = fractional.substring(0, fractional.length()-1); - } - if (currentLen() <= maxSize) { - return toString(); - } - - if (fractional.length() > 0) { - int remainingForFraction = maxSize-currentLen()+fractional.length(); - String formatCandidate = "#"; - if (remainingForFraction>=1) { - formatCandidate = "#."+("#######".substring(0, remainingForFraction)); - } - DecimalFormat df = new DecimalFormat(formatCandidate); - df.setRoundingMode(RoundingMode.HALF_UP); - - final String decimalSup = (decimal.length() > 0) ? decimal : "0"; - String result = sign + df.format(Double.parseDouble(decimalSup+"."+fractional)).replace(",", originalSeparator).replace(".", originalSeparator) + - ((withUnits == Units.USE) ? units : ""); - return (decimal.length() > 0) ? result : result.substring(1); - } - return toString(); - } - - private int currentLen() { - return sign.length() + decimal.length() + separator.length() + fractional.length() + - ((withUnits == Units.USE) ? units.length() : 0); - } - - @Override - public String toString() { - return sign+decimal+separator+fractional + - ((withUnits == Units.USE) ? units : ""); - } - - public String getExtra() { - return extra; - } - - public String getUnits() { return units; } - - -} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.kt new file mode 100644 index 0000000000..5eb6b8946a --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/SmallestDoubleString.kt @@ -0,0 +1,102 @@ +package info.nightscout.androidaps.interaction.utils + +import kotlin.jvm.JvmOverloads +import java.math.RoundingMode +import java.text.DecimalFormat +import java.util.regex.Pattern + +/** + * Helper to minimise various floating point values, with or without unit, to fit into specified + * and limited size, scarifying precision (rounding up) and extra characters like leading zero, + * following zero(s) in fractional part, extra plus sign etc. + * + * Created by dlvoy on 2019-11-12 + */ +class SmallestDoubleString @JvmOverloads constructor(inputString: String, withUnits: Units = Units.SKIP) { + + private var sign: String + private var decimal: String + private var separator: String + private var fractional: String + var extra = "" + var units: String + private val withUnits: Units + + enum class Units { + SKIP, USE + } + + fun minimise(maxSize: Int): String { + val originalSeparator = separator + if ("0$fractional".toInt() == 0) { + separator = "" + fractional = "" + } + if ("0$decimal".toInt() == 0 && fractional.isNotEmpty()) { + decimal = "" + } + if (currentLen() <= maxSize) return toString() + if (sign == "+") { + sign = "" + } + if (currentLen() <= maxSize) { + return toString() + } + while (fractional.length > 1 && fractional[fractional.length - 1] == '0') { + fractional = fractional.substring(0, fractional.length - 1) + } + if (currentLen() <= maxSize) { + return toString() + } + if (fractional.isNotEmpty()) { + val remainingForFraction = maxSize - currentLen() + fractional.length + var formatCandidate = "#" + if (remainingForFraction >= 1) { + formatCandidate = "#." + "#######".substring(0, remainingForFraction) + } + val df = DecimalFormat(formatCandidate) + df.roundingMode = RoundingMode.HALF_UP + val decimalSup = decimal.ifEmpty { "0" } + val result = sign + df.format("$decimalSup.$fractional".toDouble()).replace(",", originalSeparator).replace(".", originalSeparator) + + if (withUnits == Units.USE) units else "" + return if (decimal.isNotEmpty()) result else result.substring(1) + } + return toString() + } + + private fun currentLen(): Int { + return sign.length + decimal.length + separator.length + fractional.length + + if (withUnits == Units.USE) units.length else 0 + } + + override fun toString(): String { + return sign + decimal + separator + fractional + + if (withUnits == Units.USE) units else "" + } + + companion object { + + private val pattern = Pattern.compile("^([+-]?)([0-9]*)([,.]?)([0-9]*)(\\([^)]*\\))?(.*?)$", Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE) + } + + init { + val matcher = pattern.matcher(inputString) + matcher.matches() + sign = matcher.group(1) ?: "" + decimal = matcher.group(2) ?: "" + separator = matcher.group(3) ?: "" + fractional = matcher.group(4) ?: "" + units = matcher.group(6) ?: "" + if (fractional.isEmpty()) { + separator = "" + fractional = "" + } + val extraCandidate = matcher.group(5) ?: "" + if (extraCandidate.length > 2) { + extra = extraCandidate.substring(1, extraCandidate.length - 1) + } + units = units.trim() + + this.withUnits = withUnits + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.kt index 1311e2dce6..7509cfc0a5 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.kt +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/utils/WearUtil.kt @@ -1,9 +1,7 @@ package info.nightscout.androidaps.interaction.utils import android.content.Context -import android.os.Bundle import android.os.PowerManager -import com.google.android.gms.wearable.DataMap import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.logging.LTag import javax.inject.Inject @@ -72,4 +70,4 @@ class WearUtil @Inject constructor() { // we simply ignore if sleep was interrupted } } -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/ActionsTileService.kt b/wear/src/main/java/info/nightscout/androidaps/tile/ActionsTileService.kt index 959948443a..18fdcace9c 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/ActionsTileService.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/ActionsTileService.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.tile import dagger.android.AndroidInjection +import info.nightscout.androidaps.tile.source.ActionSource import javax.inject.Inject class ActionsTileService : TileBase() { diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt b/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt index 7dfc32a540..23cd00ca27 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.tile import dagger.android.AndroidInjection +import info.nightscout.androidaps.tile.source.QuickWizardSource import javax.inject.Inject class QuickWizardTileService : TileBase() { diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetTileService.kt b/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetTileService.kt index 8b17893ac1..9526b3feba 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetTileService.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetTileService.kt @@ -1,6 +1,7 @@ package info.nightscout.androidaps.tile import dagger.android.AndroidInjection +import info.nightscout.androidaps.tile.source.TempTargetSource import javax.inject.Inject class TempTargetTileService : TileBase() { diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/ActionSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/source/ActionSource.kt similarity index 95% rename from wear/src/main/java/info/nightscout/androidaps/tile/ActionSource.kt rename to wear/src/main/java/info/nightscout/androidaps/tile/source/ActionSource.kt index 492c10d9ec..49a8ac4199 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/ActionSource.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/source/ActionSource.kt @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.tile +package info.nightscout.androidaps.tile.source import android.content.Context import android.content.res.Resources diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/source/QuickWizardSource.kt similarity index 93% rename from wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardSource.kt rename to wear/src/main/java/info/nightscout/androidaps/tile/source/QuickWizardSource.kt index 00993b09bf..6982b2d0c2 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardSource.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/source/QuickWizardSource.kt @@ -1,9 +1,11 @@ -package info.nightscout.androidaps.tile +package info.nightscout.androidaps.tile.source import android.content.Context import android.content.res.Resources import info.nightscout.androidaps.R import info.nightscout.androidaps.interaction.actions.BackgroundActionActivity +import info.nightscout.androidaps.tile.Action +import info.nightscout.androidaps.tile.TileSource import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.logging.LTag import info.nightscout.shared.sharedPreferences.SP diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/StaticTileSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/source/StaticTileSource.kt similarity index 90% rename from wear/src/main/java/info/nightscout/androidaps/tile/StaticTileSource.kt rename to wear/src/main/java/info/nightscout/androidaps/tile/source/StaticTileSource.kt index 93d29d6927..893bcf4c48 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/StaticTileSource.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/source/StaticTileSource.kt @@ -1,9 +1,10 @@ -package info.nightscout.androidaps.tile +package info.nightscout.androidaps.tile.source import android.content.Context -import android.content.SharedPreferences import android.content.res.Resources import androidx.annotation.DrawableRes +import info.nightscout.androidaps.tile.Action +import info.nightscout.androidaps.tile.TileSource import info.nightscout.shared.logging.AAPSLogger import info.nightscout.shared.sharedPreferences.SP import info.nightscout.shared.weardata.EventData diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/source/TempTargetSource.kt similarity index 96% rename from wear/src/main/java/info/nightscout/androidaps/tile/TempTargetSource.kt rename to wear/src/main/java/info/nightscout/androidaps/tile/source/TempTargetSource.kt index 3ebbfdc7e2..ec9a64b50a 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetSource.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/source/TempTargetSource.kt @@ -1,4 +1,4 @@ -package info.nightscout.androidaps.tile +package info.nightscout.androidaps.tile.source import android.content.Context import android.content.res.Resources diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.kt index 07fbc4d63d..23f304a8b5 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BaseWatchFace.kt @@ -66,7 +66,6 @@ abstract class BaseWatchFace : WatchFace() { @Inject lateinit var dateUtil: DateUtil private var disposable = CompositeDisposable() - private val rawData = RawDisplayData() protected val singleBg get() = rawData.singleBg @@ -96,11 +95,11 @@ abstract class BaseWatchFace : WatchFace() { var mCOB2: TextView? = null var mBgi: TextView? = null var mLoop: TextView? = null - var mTimePeriod: TextView? = null + private var mTimePeriod: TextView? = null var mDay: TextView? = null - var mDayName: TextView? = null + private var mDayName: TextView? = null var mMonth: TextView? = null - var isAAPSv2: View? = null + private var isAAPSv2: View? = null var mHighLight: TextView? = null var mLowLight: TextView? = null var mGlucoseDial: ImageView? = null @@ -110,9 +109,9 @@ abstract class BaseWatchFace : WatchFace() { var mRelativeLayout: ViewGroup? = null var mLinearLayout: LinearLayout? = null var mLinearLayout2: LinearLayout? = null - var mDate: LinearLayout? = null - var mChartTap: LinearLayout? = null // Steampunk only - var mMainMenuTap: LinearLayout? = null // Steampunk,Digital only + private var mDate: LinearLayout? = null + private var mChartTap: LinearLayout? = null // Steampunk only + private var mMainMenuTap: LinearLayout? = null // Steampunk,Digital only var chart: LineChartView? = null var ageLevel = 1 @@ -123,7 +122,7 @@ abstract class BaseWatchFace : WatchFace() { var gridColor = Color.WHITE var basalBackgroundColor = Color.BLUE var basalCenterColor = Color.BLUE - var bolusColor = Color.MAGENTA + private var bolusColor = Color.MAGENTA private var lowResMode = false private var layoutSet = false var bIsRound = false @@ -137,8 +136,8 @@ abstract class BaseWatchFace : WatchFace() { // related endTime manual layout var layoutView: View? = null - var specW = 0 - var specH = 0 + private var specW = 0 + private var specH = 0 var forceSquareCanvas = false // Set to true by the Steampunk watch face. private var batteryReceiver: BroadcastReceiver? = null private var colorDarkHigh = 0 @@ -338,7 +337,7 @@ abstract class BaseWatchFace : WatchFace() { return (System.currentTimeMillis() - singleBg.timeStamp).toDouble() } - protected fun readingAge(shortString: Boolean): String { + private fun readingAge(shortString: Boolean): String { if (singleBg.timeStamp == 0L) { return if (shortString) "--" else "-- Minute ago" } @@ -491,7 +490,7 @@ abstract class BaseWatchFace : WatchFace() { invalidate() } - protected fun setDateAndTime() { + private fun setDateAndTime() { mTime?.text = dateUtil.timeString() mHour?.text = dateUtil.hourString() mMinute?.text = dateUtil.minuteString() @@ -512,7 +511,7 @@ abstract class BaseWatchFace : WatchFace() { } } - protected fun strikeThroughSgvIfNeeded() { + private fun strikeThroughSgvIfNeeded() { mSgv?.let { mSgv -> if (ageLevel() <= 0 && singleBg.timeStamp > 0) mSgv.paintFlags = mSgv.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG else mSgv.paintFlags = mSgv.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BigChartWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BigChartWatchface.kt index 8fe6eaae33..698ce41142 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/BigChartWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/BigChartWatchface.kt @@ -2,6 +2,7 @@ package info.nightscout.androidaps.watchfaces +import android.annotation.SuppressLint import androidx.annotation.LayoutRes import androidx.core.content.ContextCompat import com.ustwo.clockwise.common.WatchMode @@ -13,6 +14,7 @@ class BigChartWatchface : BaseWatchFace() { if (resources.displayMetrics.widthPixels < SCREEN_SIZE_SMALL || resources.displayMetrics.heightPixels < SCREEN_SIZE_SMALL) R.layout.activity_bigchart_small else R.layout.activity_bigchart + @SuppressLint("SetTextI18n") override fun setDataFields() { super.setDataFields() mStatus?.text = status.externalStatus + if (sp.getBoolean(R.string.key_show_cob, true)) (" " + this.status.cob) else "" @@ -118,4 +120,4 @@ class BigChartWatchface : BaseWatchFace() { setColorDark() } } -} \ No newline at end of file +} diff --git a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CircleWatchface.kt b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CircleWatchface.kt index 0809275b7e..d24c1826a2 100644 --- a/wear/src/main/java/info/nightscout/androidaps/watchfaces/CircleWatchface.kt +++ b/wear/src/main/java/info/nightscout/androidaps/watchfaces/CircleWatchface.kt @@ -197,7 +197,7 @@ class CircleWatchface : WatchFace() { myLayout?.layout(0, 0, myLayout?.measuredWidth ?: 0, myLayout?.measuredHeight ?: 0) } - val minutes: String + private val minutes: String get() { var minutes = "--'" if (singleBg.timeStamp != 0L) { @@ -421,4 +421,4 @@ class CircleWatchface : WatchFace() { override fun getWatchFaceStyle(): WatchFaceStyle { return WatchFaceStyle.Builder(this).setAcceptsTapEvents(true).build() } -} \ No newline at end of file +} diff --git a/wear/src/main/res/xml/tile_configuration_activity.xml b/wear/src/main/res/xml/tile_configuration_activity.xml index 19abf54a05..e1287e1d9f 100644 --- a/wear/src/main/res/xml/tile_configuration_activity.xml +++ b/wear/src/main/res/xml/tile_configuration_activity.xml @@ -1,33 +1,32 @@ - + + android:entryValues="@array/tile_action_values" + android:key="tile_action_1" + android:title="Action 1" /> + android:entryValues="@array/tile_action_values" + android:key="tile_action_2" + android:title="Action 2" /> + android:entryValues="@array/tile_action_values" + android:key="tile_action_3" + android:title="Action 3" /> + android:entryValues="@array/tile_action_values" + android:key="tile_action_4" + android:title="Action 4" /> diff --git a/wear/src/main/res/xml/tile_configuration_tempt.xml b/wear/src/main/res/xml/tile_configuration_tempt.xml index 44ba856ef0..de687d8f96 100644 --- a/wear/src/main/res/xml/tile_configuration_tempt.xml +++ b/wear/src/main/res/xml/tile_configuration_tempt.xml @@ -1,33 +1,32 @@ - + + android:entryValues="@array/tile_tempt_values" + android:key="tile_tempt_1" + android:title="Target 1" /> + android:entryValues="@array/tile_tempt_values" + android:key="tile_tempt_2" + android:title="Target 2" /> + android:entryValues="@array/tile_tempt_values" + android:key="tile_tempt_3" + android:title="Target 3" /> + android:entryValues="@array/tile_tempt_values" + android:key="tile_tempt_4" + android:title="Target 4" />