From 0233db822624b8e1abbbf27f1928a9a7f2176812 Mon Sep 17 00:00:00 2001 From: Andries Smit Date: Tue, 18 Jan 2022 20:54:39 +0100 Subject: [PATCH] Wear: ad quick wizard tile --- .../activities/QuickWizardListActivity.kt | 106 ++++- .../overview/dialogs/EditQuickWizardDialog.kt | 13 + .../general/wear/ActionStringHandler.kt | 87 +++-- .../wearintegration/WatchUpdaterService.java | 55 ++- .../androidaps/utils/wizard/QuickWizard.kt | 47 +++ .../utils/wizard/QuickWizardEntry.kt | 20 +- .../overview_editquickwizard_dialog.xml | 29 ++ .../layout/overview_quickwizardlist_item.xml | 73 ++-- app/src/main/res/values/strings.xml | 4 + core/src/main/res/drawable/ic_smartphone.xml | 5 + wear/src/main/AndroidManifest.xml | 22 ++ .../androidaps/data/ListenerService.java | 367 ++++-------------- .../actions/BackgroundActionActivity.kt | 29 ++ .../actions/TempTargetActivity.java | 19 +- .../androidaps/tile/ActionSource.kt | 36 +- .../androidaps/tile/ActionsTileService.kt | 6 +- .../androidaps/tile/QuickWizardSource.kt | 79 ++++ .../androidaps/tile/QuickWizardTileService.kt | 8 + .../androidaps/tile/StaticTileSource.kt | 59 +++ .../androidaps/tile/TempTargetSource.kt | 53 +-- .../androidaps/tile/TempTargetTileService.kt | 4 +- .../nightscout/androidaps/tile/TileBase.kt | 149 ++++--- .../quick_wizard_tile_preview.png | Bin 0 -> 13916 bytes .../quick_wizard_tile_preview.png | Bin 0 -> 13916 bytes .../src/main/res/drawable/ic_carbs_orange.xml | 1 - .../src/main/res/drawable/ic_quick_wizard.xml | 12 + wear/src/main/res/values/strings.xml | 6 +- 27 files changed, 790 insertions(+), 499 deletions(-) create mode 100644 core/src/main/res/drawable/ic_smartphone.xml create mode 100644 wear/src/main/java/info/nightscout/androidaps/interaction/actions/BackgroundActionActivity.kt create mode 100644 wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardSource.kt create mode 100644 wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt create mode 100644 wear/src/main/java/info/nightscout/androidaps/tile/StaticTileSource.kt create mode 100644 wear/src/main/res/drawable-notround-nodpi/quick_wizard_tile_preview.png create mode 100644 wear/src/main/res/drawable-round-nodpi/quick_wizard_tile_preview.png create mode 100644 wear/src/main/res/drawable/ic_quick_wizard.xml diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt index eb56d8067e..065a2f1fc3 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/activities/QuickWizardListActivity.kt @@ -1,12 +1,18 @@ package info.nightscout.androidaps.plugins.general.overview.activities +import android.annotation.SuppressLint import android.os.Bundle +import android.util.Log import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.ImageView import android.widget.TextView import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.* import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import info.nightscout.androidaps.R @@ -20,6 +26,8 @@ import info.nightscout.androidaps.utils.FabricPrivacy import io.reactivex.rxkotlin.plusAssign import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.androidaps.utils.wizard.QuickWizard +import info.nightscout.androidaps.utils.wizard.QuickWizardEntry +import info.nightscout.shared.sharedPreferences.SP import io.reactivex.disposables.CompositeDisposable import javax.inject.Inject @@ -30,20 +38,95 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { @Inject lateinit var fabricPrivacy: FabricPrivacy @Inject lateinit var quickWizard: QuickWizard @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var sp: SP private var disposable: CompositeDisposable = CompositeDisposable() private lateinit var binding: OverviewQuickwizardlistActivityBinding + private val itemTouchHelper by lazy { + val simpleItemTouchCallback = object : ItemTouchHelper.SimpleCallback(UP or DOWN or START or END, 0) { + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val adapter = recyclerView.adapter as RecyclerViewAdapter + val from = viewHolder.layoutPosition + val to = target.layoutPosition + adapter.moveItem(from, to) + adapter.notifyItemMoved(from, to) + + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + super.onSelectedChanged(viewHolder, actionState) + + if (actionState == ACTION_STATE_DRAG) { + viewHolder?.itemView?.alpha = 0.5f + } + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + + viewHolder.itemView.alpha = 1.0f + + val adapter = recyclerView.adapter as RecyclerViewAdapter + adapter.onDrop() + } + } + + ItemTouchHelper(simpleItemTouchCallback) + } + + fun startDragging(viewHolder: RecyclerView.ViewHolder) { + itemTouchHelper.startDrag(viewHolder) + } + private inner class RecyclerViewAdapter(var fragmentManager: FragmentManager) : RecyclerView.Adapter() { + @SuppressLint("ClickableViewAccessibility") override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuickWizardEntryViewHolder { - return QuickWizardEntryViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.overview_quickwizardlist_item, parent, false), fragmentManager) + val itemView = LayoutInflater.from(parent.context).inflate(R.layout.overview_quickwizardlist_item, parent, false) + val viewHolder = QuickWizardEntryViewHolder(itemView, fragmentManager) + + viewHolder.handleView.setOnTouchListener { _, event -> + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + startDragging(viewHolder) + } + return@setOnTouchListener true + } + + return viewHolder } override fun onBindViewHolder(holder: QuickWizardEntryViewHolder, position: Int) { holder.from.text = dateUtil.timeString(quickWizard[position].validFromDate()) holder.to.text = dateUtil.timeString(quickWizard[position].validToDate()) + val wearControl = sp.getBoolean(R.string.key_wear_control, false) + + if (wearControl) { + holder.handleView.visibility = View.VISIBLE + } else { + holder.handleView.visibility = View.GONE + } + if (quickWizard[position].device() == QuickWizardEntry.DEVICE_ALL) { + holder.device.visibility = View.GONE + } else { + holder.device.visibility = View.VISIBLE + holder.device.setImageResource( + when (quickWizard[position].device()) { + QuickWizardEntry.DEVICE_WATCH -> R.drawable.ic_watch + else -> R.drawable.ic_smartphone + } + ) + } holder.buttonText.text = quickWizard[position].buttonText() holder.carbs.text = rh.gs(R.string.format_carbs, quickWizard[position].carbs()) } @@ -55,6 +138,8 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { val buttonText: TextView = itemView.findViewById(R.id.overview_quickwizard_item_buttonText) val carbs: TextView = itemView.findViewById(R.id.overview_quickwizard_item_carbs) val from: TextView = itemView.findViewById(R.id.overview_quickwizard_item_from) + val handleView: ImageView = itemView.findViewById(R.id.handleView) + val device: ImageView = itemView.findViewById(R.id.overview_quickwizard_item_device) val to: TextView = itemView.findViewById(R.id.overview_quickwizard_item_to) private val editButton: Button = itemView.findViewById(R.id.overview_quickwizard_item_edit_button) private val removeButton: Button = itemView.findViewById(R.id.overview_quickwizard_item_remove_button) @@ -74,6 +159,16 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { } } } + + fun moveItem(from: Int, to: Int) { + Log.i("QuickWizard", "moveItem") + quickWizard.move(from, to) + } + + fun onDrop() { + Log.i("QuickWizard", "onDrop") + rxBus.send(EventQuickWizardChange()) + } } override fun onCreate(savedInstanceState: Bundle?) { @@ -84,6 +179,7 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { binding.recyclerview.setHasFixedSize(true) binding.recyclerview.layoutManager = LinearLayoutManager(this) binding.recyclerview.adapter = RecyclerViewAdapter(supportFragmentManager) + itemTouchHelper.attachToRecyclerView(binding.recyclerview) binding.addButton.setOnClickListener { val manager = supportFragmentManager @@ -98,13 +194,13 @@ class QuickWizardListActivity : NoSplashAppCompatActivity() { .toObservable(EventQuickWizardChange::class.java) .observeOn(aapsSchedulers.main) .subscribe({ - val adapter = RecyclerViewAdapter(supportFragmentManager) - binding.recyclerview.swapAdapter(adapter, false) - }, fabricPrivacy::logException) + val adapter = RecyclerViewAdapter(supportFragmentManager) + binding.recyclerview.swapAdapter(adapter, false) + }, fabricPrivacy::logException) } override fun onPause() { disposable.clear() super.onPause() } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt index bfe47b2bdf..cb4c3ee652 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/overview/dialogs/EditQuickWizardDialog.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.view.Window import android.view.WindowManager import dagger.android.support.DaggerDialogFragment +import info.nightscout.androidaps.R import info.nightscout.androidaps.databinding.OverviewEditquickwizardDialogBinding import info.nightscout.shared.logging.AAPSLogger import info.nightscout.androidaps.plugins.bus.RxBus @@ -21,6 +22,7 @@ import info.nightscout.androidaps.utils.extensions.setEnableForChildren import info.nightscout.androidaps.utils.extensions.setSelection import info.nightscout.androidaps.utils.wizard.QuickWizard import info.nightscout.androidaps.utils.wizard.QuickWizardEntry +import info.nightscout.shared.sharedPreferences.SP import org.json.JSONException import javax.inject.Inject @@ -30,6 +32,7 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { @Inject lateinit var aapsLogger: AAPSLogger @Inject lateinit var quickWizard: QuickWizard @Inject lateinit var dateUtil: DateUtil + @Inject lateinit var sp: SP var position = -1 @@ -57,6 +60,14 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { position = bundle.getInt("position", -1) } val entry = if (position == -1) quickWizard.newEmptyItem() else quickWizard[position] + if (sp.getBoolean(R.string.key_wear_control, false)) { + binding.deviceLabel.visibility = View.VISIBLE + binding.device.visibility = View.VISIBLE + } else { + binding.deviceLabel.visibility = View.GONE + binding.device.visibility = View.GONE + } + binding.okcancel.ok.setOnClickListener { try { entry.storage.put("buttonText", binding.buttonEdit.text.toString()) @@ -66,6 +77,7 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { entry.storage.put("useBG", binding.useBg.selectedItemPosition) entry.storage.put("useCOB", binding.useCob.selectedItemPosition) entry.storage.put("useBolusIOB", binding.useBolusIob.selectedItemPosition) + entry.storage.put("device", binding.device.selectedItemPosition) entry.storage.put("useBasalIOB", binding.useBasalIob.selectedItemPosition) entry.storage.put("useTrend", binding.useTrend.selectedItemPosition) entry.storage.put("useSuperBolus", binding.useSuperBolus.selectedItemPosition) @@ -122,6 +134,7 @@ class EditQuickWizardDialog : DaggerDialogFragment(), View.OnClickListener { binding.useCob.setSelection(entry.useCOB()) binding.useBolusIob.setSelection(entry.useBolusIOB()) binding.useBasalIob.setSelection(entry.useBasalIOB()) + binding.device.setSelection(entry.device()) binding.useTrend.setSelection(entry.useTrend()) binding.useSuperBolus.setSelection(entry.useSuperBolus()) binding.useTempTarget.setSelection(entry.useTempTarget()) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt index db33722e31..f64eb9430a 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/ActionStringHandler.kt @@ -2,6 +2,7 @@ package info.nightscout.androidaps.plugins.general.wear import android.app.NotificationManager import android.content.Context +import android.util.Log import dagger.android.HasAndroidInjector import info.nightscout.androidaps.Constants import info.nightscout.androidaps.R @@ -39,6 +40,7 @@ import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.rx.AapsSchedulers import info.nightscout.shared.sharedPreferences.SP import info.nightscout.androidaps.utils.wizard.BolusWizard +import info.nightscout.androidaps.utils.wizard.QuickWizard import info.nightscout.shared.SafeParse import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign @@ -71,6 +73,7 @@ class ActionStringHandler @Inject constructor( private val activePlugin: ActivePlugin, private val iobCobCalculator: IobCobCalculator, private val localInsightPlugin: LocalInsightPlugin, + private val quickWizard: QuickWizard, private val danaRPlugin: DanaRPlugin, private val danaRKoreanPlugin: DanaRKoreanPlugin, private val danaRv2Plugin: DanaRv2Plugin, @@ -144,34 +147,39 @@ class ActionStringHandler @Inject constructor( } rAction += "bolus $insulinAfterConstraints $carbsAfterConstraints" } else if ("temptarget" == act[0]) { ///////////////////////////////////////////////////////// TEMPTARGET - aapsLogger.info(LTag.WEAR, "temptarget received:" + act) + aapsLogger.info(LTag.WEAR, "temptarget received: $act") if ("cancel" == act[1]) { rMessage += rh.gs(R.string.wear_action_tempt_cancel_message) rAction = "temptarget true 0 0 0" } else if ("preset" == act[1]) { val presetIsMGDL = profileFunction.getUnits() == GlucoseUnit.MGDL val preset = act[2] - if ("activity" == preset) { - val activityTTDuration = defaultValueHelper.determineActivityTTDuration() - val activityTT = defaultValueHelper.determineActivityTT() - val reason = rh.gs(R.string.activity) - rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, activityTT, activityTTDuration) - rAction = "temptarget $presetIsMGDL $activityTTDuration $activityTT $activityTT" - } else if ("hypo" == preset) { - val hypoTTDuration = defaultValueHelper.determineHypoTTDuration() - val hypoTT = defaultValueHelper.determineHypoTT() - val reason = rh.gs(R.string.hypo) - rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, hypoTT, hypoTTDuration) - rAction = "temptarget $presetIsMGDL $hypoTTDuration $hypoTT $hypoTT" - } else if ("eating" == preset) { - val eatingSoonTTDuration = defaultValueHelper.determineEatingSoonTTDuration() - val eatingSoonTT = defaultValueHelper.determineEatingSoonTT() - val reason = rh.gs(R.string.eatingsoon) - rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, eatingSoonTT, eatingSoonTTDuration) - rAction = "temptarget $presetIsMGDL $eatingSoonTTDuration $eatingSoonTT $eatingSoonTT" - } else { - sendError(rh.gs(R.string.wear_action_tempt_preset_error, preset)) - return + when (preset) { + "activity" -> { + val activityTTDuration = defaultValueHelper.determineActivityTTDuration() + val activityTT = defaultValueHelper.determineActivityTT() + val reason = rh.gs(R.string.activity) + rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, activityTT, activityTTDuration) + rAction = "temptarget $presetIsMGDL $activityTTDuration $activityTT $activityTT" + } + "hypo" -> { + val hypoTTDuration = defaultValueHelper.determineHypoTTDuration() + val hypoTT = defaultValueHelper.determineHypoTT() + val reason = rh.gs(R.string.hypo) + rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, hypoTT, hypoTTDuration) + rAction = "temptarget $presetIsMGDL $hypoTTDuration $hypoTT $hypoTT" + } + "eating" -> { + val eatingSoonTTDuration = defaultValueHelper.determineEatingSoonTTDuration() + val eatingSoonTT = defaultValueHelper.determineEatingSoonTT() + val reason = rh.gs(R.string.eatingsoon) + rMessage += rh.gs(R.string.wear_action_tempt_preset_message, reason, eatingSoonTT, eatingSoonTTDuration) + rAction = "temptarget $presetIsMGDL $eatingSoonTTDuration $eatingSoonTT $eatingSoonTT" + } + else -> { + sendError(rh.gs(R.string.wear_action_tempt_preset_error, preset)) + return + } } } else { val isMGDL = java.lang.Boolean.parseBoolean(act[1]) @@ -281,6 +289,34 @@ class ActionStringHandler @Inject constructor( rMessage += "\nPercentage: " + format.format(bolusWizard.totalBeforePercentageAdjustment) + "U * " + percentage + "% -> ~" + format.format(bolusWizard.calculatedTotalInsulin) + "U" } lastBolusWizard = bolusWizard + } else if ("quick_wizard" == act[0]) { + val guid = act[1] + val actualBg = iobCobCalculator.ads.actualBg() + val profile = profileFunction.getProfile() + val profileName = profileFunction.getProfileName() + val pump = activePlugin.activePump + val quickWizardEntry = quickWizard.get(guid) + Log.i("QuickWizard", "handleInitiate: quick_wizard " + quickWizardEntry?.buttonText() + " c "+ quickWizardEntry?.carbs()) + if (quickWizardEntry != null && actualBg != null && profile != null) { + // Logic related from Overview.kt + val wizard = quickWizardEntry.doCalc(profile, profileName, actualBg, true) + if (wizard.calculatedTotalInsulin > 0.0 && quickWizardEntry.carbs() > 0.0) { + val carbsAfterConstraints = constraintChecker.applyCarbsConstraints(Constraint(quickWizardEntry.carbs())).value() + val insulinAfterConstraints = wizard.insulinAfterConstraints + if (abs(insulinAfterConstraints - wizard.calculatedTotalInsulin) >= pump.pumpDescription.pumpType.determineCorrectBolusStepSize(insulinAfterConstraints) || carbsAfterConstraints != quickWizardEntry.carbs()) { + // TODO check error is correct + sendError(rh.gs(R.string.constraints_violation) + "\n" + rh.gs(R.string.changeyourinput)) + return + } + rMessage = rh.gs(R.string.quick_wizard_message, quickWizardEntry.buttonText(), wizard.calculatedTotalInsulin, quickWizardEntry.carbs()) + rAction = "bolus $insulinAfterConstraints $carbsAfterConstraints" + Log.i("QuickWizard", "handleInitiate: quick_wizard action=$rAction") + } else { + sendError(rh.gs(R.string.quick_wizard_no_action)) + } + } else { + sendError(rh.gs(R.string.quick_wizard_can_not_calculate)) + } } else if ("opencpp" == act[0]) { val activeProfileSwitch = repository.getEffectiveProfileSwitchActiveAt(dateUtil.now()).blockingGet() if (activeProfileSwitch is ValueWrapper.Existing) { // read CPP values @@ -364,7 +400,10 @@ class ActionStringHandler @Inject constructor( rAction = "cancelChangeRequest" wearPlugin.requestNotificationCancel(rAction) return - } else return + } else { + sendError("Unknown action command: " + act[0] ) + return + } // send result wearPlugin.requestActionConfirmation(rTitle, rMessage, rAction) lastSentTimestamp = System.currentTimeMillis() @@ -698,4 +737,4 @@ class ActionStringHandler @Inject constructor( lastConfirmActionString = null lastBolusWizard = null } -} \ No newline at end of file +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java index d6c840625f..9e1e6dd9cf 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/wear/wearintegration/WatchUpdaterService.java @@ -46,6 +46,7 @@ import info.nightscout.androidaps.interfaces.Loop; import info.nightscout.androidaps.interfaces.PluginBase; import info.nightscout.androidaps.interfaces.Profile; import info.nightscout.androidaps.interfaces.ProfileFunction; +import info.nightscout.androidaps.utils.wizard.QuickWizardEntry; import info.nightscout.shared.logging.AAPSLogger; import info.nightscout.shared.logging.LTag; import info.nightscout.androidaps.plugins.aps.loop.LoopPlugin; @@ -62,6 +63,7 @@ import info.nightscout.androidaps.utils.DecimalFormatter; import info.nightscout.androidaps.utils.DefaultValueHelper; import info.nightscout.androidaps.utils.TrendCalculator; import info.nightscout.androidaps.utils.resources.ResourceHelper; +import info.nightscout.androidaps.utils.wizard.QuickWizard; import info.nightscout.shared.sharedPreferences.SP; public class WatchUpdaterService extends WearableListenerService implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { @@ -81,6 +83,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog @Inject ReceiverStatusStore receiverStatusStore; @Inject Config config; @Inject public TrendCalculator trendCalculator; + @Inject public QuickWizard quickWizard; public static final String ACTION_RESEND = WatchUpdaterService.class.getName().concat(".Resend"); public static final String ACTION_OPEN_SETTINGS = WatchUpdaterService.class.getName().concat(".OpenSettings"); @@ -101,12 +104,14 @@ public class WatchUpdaterService extends WearableListenerService implements Goog private static final String OPEN_SETTINGS_PATH = "/openwearsettings"; private static final String NEW_STATUS_PATH = "/sendstatustowear"; private static final String NEW_PREFERENCES_PATH = "/sendpreferencestowear"; + private static final String QUICK_WIZARD_PATH = "/send_quick_wizard"; public static final String BASAL_DATA_PATH = "/nightscout_watch_basal"; public static final String BOLUS_PROGRESS_PATH = "/nightscout_watch_bolusprogress"; public static final String ACTION_CONFIRMATION_REQUEST_PATH = "/nightscout_watch_actionconfirmationrequest"; public static final String ACTION_CHANGECONFIRMATION_REQUEST_PATH = "/nightscout_watch_changeconfirmationrequest"; public static final String ACTION_CANCELNOTIFICATION_REQUEST_PATH = "/nightscout_watch_cancelnotificationrequest"; + String TAG = "WatchUpdateService"; private static boolean lastLoopStatus; @@ -156,7 +161,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog public int onStartCommand(Intent intent, int flags, int startId) { String action = intent != null ? intent.getAction() : null; - // Log.d(TAG, logPrefix + "onStartCommand: " + action); + // Log.d(TAG, "onStartCommand: " + action); if (wearIntegration()) { handler.post(() -> { @@ -235,7 +240,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog super.onPeerConnected(peer); String id = peer.getId(); String name = peer.getDisplayName(); - // Log.d(TAG, logPrefix + "onPeerConnected peer name & ID: " + name + "|" + id); + Log.d(TAG, "onPeerConnected peer name & ID: " + name + "|" + id); } @@ -244,14 +249,14 @@ public class WatchUpdaterService extends WearableListenerService implements Goog super.onPeerDisconnected(peer); String id = peer.getId(); String name = peer.getDisplayName(); - // Log.d(TAG, logPrefix + "onPeerDisconnected peer name & ID: " + name + "|" + id); + Log.d(TAG, "onPeerDisconnected peer name & ID: " + name + "|" + id); } @Override public void onMessageReceived(MessageEvent event) { - // Log.d(TAG, logPrefix + "onMessageRecieved: " + event); + // Log.d(TAG, "onMessageRecieved: " + event); if (wearIntegration()) { if (event != null && event.getPath().equals(WEARABLE_RESEND_PATH)) { @@ -283,7 +288,7 @@ public class WatchUpdaterService extends WearableListenerService implements Goog private void sendData() { GlucoseValue lastBG = iobCobCalculator.getAds().lastBg(); - // Log.d(TAG, logPrefix + "LastBg=" + lastBG); + // Log.d(TAG, "LastBg=" + lastBG); if (lastBG != null) { GlucoseStatus glucoseStatus = glucoseStatusProvider.getGlucoseStatusData(); @@ -364,6 +369,10 @@ public class WatchUpdaterService extends WearableListenerService implements Goog if (googleApiClient != null && !googleApiClient.isConnected() && !googleApiClient.isConnecting()) { googleApiConnect(); } + + sendPreferences(); + sendQuickWizard(); + long startTime = System.currentTimeMillis() - (long) (60000 * 60 * 5.5); GlucoseValue last_bg = iobCobCalculator.getAds().lastBg(); @@ -382,7 +391,6 @@ public class WatchUpdaterService extends WearableListenerService implements Goog entries.putDataMapArrayList("entries", dataMaps); (new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient)).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, entries); } - sendPreferences(); sendBasals(); sendStatus(); } @@ -738,6 +746,41 @@ public class WatchUpdaterService extends WearableListenerService implements Goog } } + private void sendQuickWizard() { + if (googleApiClient != null && googleApiClient.isConnected()) { + int size = quickWizard.size(); + ArrayList entities = new ArrayList<>(); + for(int i=0; i < size; i++) { + QuickWizardEntry q = quickWizard.get(i); + if (q.forDevice(QuickWizardEntry.DEVICE_WATCH)) { + entities.add(quickMap(q)); + } + } + + PutDataMapRequest dataMapRequest = PutDataMapRequest.create(QUICK_WIZARD_PATH); + + DataMap dm = dataMapRequest.getDataMap(); + dm.putLong("timestamp", System.currentTimeMillis()); + dm.putDataMapArrayList("quick_wizard", entities); + + PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest(); + Log.i(TAG, "sendQuickWizard: " + putDataRequest); + Wearable.DataApi.putDataItem(googleApiClient, putDataRequest); + } else { + Log.e("sendQuickWizard", "No connection to wearable available!"); + } + } + + private DataMap quickMap(QuickWizardEntry q) { + DataMap dm = new DataMap(); + dm.putString("guid", q.guid()); + dm.putString("button_text", q.buttonText()); + dm.putInt("carbs", q.carbs()); + dm.putInt("from", q.validFrom()); + dm.putInt("to", q.validTo()); + return dm; + } + @NonNull private String generateStatusString(Profile profile, String currentBasal, String iobSum, String iobDetail, String bgiString) { diff --git a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt index a254831690..b48d44c9cc 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizard.kt @@ -1,10 +1,12 @@ package info.nightscout.androidaps.utils.wizard +import android.util.Log import dagger.android.HasAndroidInjector import info.nightscout.androidaps.R import info.nightscout.shared.sharedPreferences.SP import org.json.JSONArray import org.json.JSONObject +import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -18,6 +20,18 @@ class QuickWizard @Inject constructor( init { setData(JSONArray(sp.getString(R.string.key_quickwizard, "[]"))) + setGuidsForOldEntries() + } + + private fun setGuidsForOldEntries() { + // for migration purposes; guid is a new required property + for (i in 0 until storage.length()) { + val entry = QuickWizardEntry(injector).from(storage.get(i) as JSONObject, i) + if (entry.guid() == "") { + val guid = UUID.randomUUID().toString() + entry.storage.put("guid", guid) + } + } } fun getActive(): QuickWizardEntry? { @@ -41,6 +55,38 @@ class QuickWizard @Inject constructor( operator fun get(position: Int): QuickWizardEntry = QuickWizardEntry(injector).from(storage.get(position) as JSONObject, position) + fun get(guid: String): QuickWizardEntry? { + for (i in 0 until storage.length()) { + val entry = QuickWizardEntry(injector).from(storage.get(i) as JSONObject, i) + if (entry.guid() == guid) { + return entry + } + } + return null + } + + fun move(from: Int, to: Int) { + Log.i("QuickWizard", "moveItem: $from $to") + val fromEntry = storage[from] as JSONObject + storage.remove(from) + addToPos(to, fromEntry, storage) + save() + } + + fun removePos(pos: Int, jsonObj: JSONObject?, jsonArr: JSONArray) { + for (i in jsonArr.length() downTo pos + 1) { + jsonArr.put(i, jsonArr[i - 1]) + } + jsonArr.put(pos, jsonObj) + } + + private fun addToPos(pos: Int, jsonObj: JSONObject?, jsonArr: JSONArray) { + for (i in jsonArr.length() downTo pos + 1) { + jsonArr.put(i, jsonArr[i - 1]) + } + jsonArr.put(pos, jsonObj) + } + fun newEmptyItem(): QuickWizardEntry { return QuickWizardEntry(injector) } @@ -57,4 +103,5 @@ class QuickWizard @Inject constructor( storage.remove(position) save() } + } diff --git a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt index 79ff43bf66..1f3d48fc7a 100644 --- a/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt +++ b/app/src/main/java/info/nightscout/androidaps/utils/wizard/QuickWizardEntry.kt @@ -19,6 +19,7 @@ import info.nightscout.androidaps.utils.JsonHelper.safeGetString import info.nightscout.shared.sharedPreferences.SP import org.json.JSONException import org.json.JSONObject +import java.util.* import javax.inject.Inject class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjector) { @@ -41,11 +42,15 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec const val NO = 1 private const val POSITIVE_ONLY = 2 private const val NEGATIVE_ONLY = 3 + const val DEVICE_ALL = 0 + const val DEVICE_PHONE = 1 + const val DEVICE_WATCH = 2 } init { injector.androidInjector().inject(this) - val emptyData = "{\"buttonText\":\"\",\"carbs\":0,\"validFrom\":0,\"validTo\":86340}" + val guid = UUID.randomUUID().toString() + val emptyData = "{\"guid\": \"$guid\",\"buttonText\":\"\",\"carbs\":0,\"validFrom\":0,\"validTo\":86340, \"device\": \"all\"}" try { storage = JSONObject(emptyData) } catch (e: JSONException) { @@ -55,6 +60,8 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec /* { + guid: string, + device: string, // (phone, watch, all) buttonText: "Meal", carbs: 36, validFrom: 8 * 60 * 60, // seconds from midnight @@ -69,12 +76,13 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec } */ fun from(entry: JSONObject, position: Int): QuickWizardEntry { + // TODO set guid if missing for migration storage = entry this.position = position return this } - fun isActive(): Boolean = profileFunction.secondsFromMidnight() >= validFrom() && profileFunction.secondsFromMidnight() <= validTo() + fun isActive(): Boolean = profileFunction.secondsFromMidnight() >= validFrom() && profileFunction.secondsFromMidnight() <= validTo() && forDevice(DEVICE_PHONE) fun doCalc(profile: Profile, profileName: String, lastBG: GlucoseValue, _synchronized: Boolean): BolusWizard { val dbRecord = repository.getTemporaryTargetActiveAt(dateUtil.now()).blockingGet() @@ -123,6 +131,12 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec return BolusWizard(injector).doCalc(profile, profileName, tempTarget, carbs(), cob, bg, 0.0, percentage, true, useCOB() == YES, bolusIOB, basalIOB, superBolus, useTempTarget() == YES, trend, false, buttonText(), quickWizard = true) //tbc, ok if only quickwizard, but if other sources elsewhere use Sources.QuickWizard } + fun guid(): String = safeGetString(storage, "guid", "") + + fun device(): Int = safeGetInt(storage, "device", DEVICE_ALL) + + fun forDevice(device: Int) = device() == device || device() == DEVICE_ALL + fun buttonText(): String = safeGetString(storage, "buttonText", "") fun carbs(): Int = safeGetInt(storage, "carbs") @@ -148,4 +162,4 @@ class QuickWizardEntry @Inject constructor(private val injector: HasAndroidInjec fun useSuperBolus(): Int = safeGetInt(storage, "useSuperBolus", NO) fun useTempTarget(): Int = safeGetInt(storage, "useTempTarget", NO) -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/overview_editquickwizard_dialog.xml b/app/src/main/res/layout/overview_editquickwizard_dialog.xml index 422b787269..af1eee4f84 100644 --- a/app/src/main/res/layout/overview_editquickwizard_dialog.xml +++ b/app/src/main/res/layout/overview_editquickwizard_dialog.xml @@ -108,6 +108,35 @@ + + + + + + + + + + + + android:layout_height="match_parent" + android:orientation="horizontal"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + - + + + + + + + android:adjustViewBounds="false" + android:cropToPadding="false" + android:paddingRight="10dp" + android:scaleType="fitStart" + card_view:srcCompat="@drawable/ic_smartphone" /> - - + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:adjustViewBounds="false" + android:cropToPadding="false" + android:scaleType="fitStart" + card_view:srcCompat="@drawable/ic_reorder_gray_24dp" /> @@ -88,9 +115,9 @@ android:textStyle="normal|bold" /> + android:layout_height="wrap_content" + android:text="-" /> Temptarget:\nMin: %1$s\nMax: %2$s\nDuration: %3$s Temptarget:\nTarget: %1$s\nDuration: %2$s Temptarget:\Reason: %1$s\nTarget: %2$s\nDuration: %3$s + No insulin needed nor are carbs added. + Can not calculate wizard, requires actual blood glucose and active profile. + Quick Wizard: %1$s\nInsulin: %2$.2fU\nCarbs: %3$dg + Show entry on device: diff --git a/core/src/main/res/drawable/ic_smartphone.xml b/core/src/main/res/drawable/ic_smartphone.xml new file mode 100644 index 0000000000..3d7f349a29 --- /dev/null +++ b/core/src/main/res/drawable/ic_smartphone.xml @@ -0,0 +1,5 @@ + + + diff --git a/wear/src/main/AndroidManifest.xml b/wear/src/main/AndroidManifest.xml index 1f5fc5ae2a..224b6e59f6 100644 --- a/wear/src/main/AndroidManifest.xml +++ b/wear/src/main/AndroidManifest.xml @@ -258,6 +258,10 @@ android:host="*" android:pathPrefix="/openwearsettings" android:scheme="wear" /> + + + + + + + + + + + diff --git a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java index bd64caacc4..42f87f5b4c 100644 --- a/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java +++ b/wear/src/main/java/info/nightscout/androidaps/data/ListenerService.java @@ -13,6 +13,7 @@ import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.preference.PreferenceManager; +import android.util.Base64; import android.util.Log; import androidx.core.app.NotificationCompat; @@ -22,10 +23,6 @@ import androidx.wear.tiles.TileService; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.PendingResult; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.wearable.CapabilityApi; -import com.google.android.gms.wearable.CapabilityInfo; import com.google.android.gms.wearable.ChannelApi; import com.google.android.gms.wearable.DataEvent; import com.google.android.gms.wearable.DataEventBuffer; @@ -36,7 +33,8 @@ import com.google.android.gms.wearable.NodeApi; import com.google.android.gms.wearable.Wearable; import com.google.android.gms.wearable.WearableListenerService; -import java.util.Set; +import org.jetbrains.annotations.NotNull; + import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -47,11 +45,11 @@ import info.nightscout.androidaps.interaction.AAPSPreferences; import info.nightscout.androidaps.interaction.actions.AcceptActivity; import info.nightscout.androidaps.interaction.actions.CPPActivity; import info.nightscout.androidaps.interaction.utils.Persistence; +import info.nightscout.androidaps.interaction.utils.WearUtil; import info.nightscout.androidaps.tile.ActionsTileService; +import info.nightscout.androidaps.tile.QuickWizardTileService; import info.nightscout.androidaps.tile.TempTargetTileService; import info.nightscout.shared.SafeParse; -import info.nightscout.androidaps.interaction.utils.WearUtil; - /** * Created by emmablack on 12/26/14. @@ -62,7 +60,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp @Inject WearUtil wearUtil; @Inject Persistence persistence; - private static final String WEARABLE_DATA_PATH = "/nightscout_watch_data"; private static final String WEARABLE_RESEND_PATH = "/nightscout_watch_data_resend"; private static final String WEARABLE_CANCELBOLUS_PATH = "/nightscout_watch_cancel_bolus"; public static final String WEARABLE_CONFIRM_ACTIONSTRING_PATH = "/nightscout_watch_confirmactionstring"; @@ -71,13 +68,13 @@ public class ListenerService extends WearableListenerService implements GoogleAp private static final String OPEN_SETTINGS = "/openwearsettings"; private static final String NEW_STATUS_PATH = "/sendstatustowear"; private static final String NEW_PREFERENCES_PATH = "/sendpreferencestowear"; + private static final String QUICK_WIZARD_PATH = "/send_quick_wizard"; public static final String BASAL_DATA_PATH = "/nightscout_watch_basal"; public static final String BOLUS_PROGRESS_PATH = "/nightscout_watch_bolusprogress"; public static final String ACTION_CONFIRMATION_REQUEST_PATH = "/nightscout_watch_actionconfirmationrequest"; public static final String NEW_CHANGECONFIRMATIONREQUEST_PATH = "/nightscout_watch_changeconfirmationrequest"; public static final String ACTION_CANCELNOTIFICATION_REQUEST_PATH = "/nightscout_watch_cancelnotificationrequest"; - public static final int BOLUS_PROGRESS_NOTIF_ID = 1; public static final int CONFIRM_NOTIF_ID = 2; public static final int CHANGE_NOTIF_ID = 556677; @@ -88,29 +85,15 @@ public class ListenerService extends WearableListenerService implements GoogleAp private static final String ACTION_CONFIRMCHANGE = "com.dexdrip.stephenblack.nightwatch.CONFIRMCHANGE"; private static final String ACTION_INITIATE_ACTION = "com.dexdrip.stephenblack.nightwatch.INITIATE_ACTION"; - - private static final String ACTION_RESEND_BULK = "com.dexdrip.stephenblack.nightwatch.RESEND_BULK_DATA"; private static final String AAPS_NOTIFY_CHANNEL_ID_OPENLOOP = "AndroidAPS-OpenLoop"; private static final String AAPS_NOTIFY_CHANNEL_ID_BOLUSPROGRESS = "bolus progress vibration"; private static final String AAPS_NOTIFY_CHANNEL_ID_BOLUSPROGRESS_SILENT = "bolus progress silent"; - GoogleApiClient googleApiClient; - private long lastRequest = 0; + private DismissThread bolusprogressThread; private static final String TAG = "ListenerService"; - private DataRequester mDataRequester = null; - private static final int GET_CAPABILITIES_TIMEOUT_MS = 5000; - - // Phone - private static final String CAPABILITY_PHONE_APP = "phone_app_sync_bgs"; - private static final String MESSAGE_PATH_PHONE = "/phone_message_path"; - // Wear - private static final String CAPABILITY_WEAR_APP = "wear_app_sync_bgs"; - private static final String MESSAGE_PATH_WEAR = "/wear_message_path"; - private final String mPhoneNodeId = null; - private String localnode = null; private final String logPrefix = ""; // "WR: " // Not derived from DaggerService, do injection here @@ -120,143 +103,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp super.onCreate(); } - public class DataRequester extends AsyncTask { - Context mContext; - String path; - byte[] payload; - - - DataRequester(Context context, String thispath, byte[] thispayload) { - path = thispath; - payload = thispayload; - // Log.d(TAG, logPrefix + "DataRequester DataRequester: " + thispath + " lastRequest:" + lastRequest); - } - - - @Override - protected Void doInBackground(Void... params) { - // Log.d(TAG, logPrefix + "DataRequester: doInBack: " + params); - - try { - - forceGoogleApiConnect(); - DataMap datamap; - - if (isCancelled()) { - Log.d(TAG, "doInBackground CANCELLED programmatically"); - return null; - } - - if (googleApiClient != null) { - if (!googleApiClient.isConnected()) - googleApiClient.blockingConnect(15, TimeUnit.SECONDS); - } - - // this code might not be needed in this way, but we need to see that later - if ((googleApiClient != null) && (googleApiClient.isConnected())) { - if ((System.currentTimeMillis() - lastRequest > 20 * 1000)) { - - // enforce 20-second debounce period - lastRequest = System.currentTimeMillis(); - - // NodeApi.GetConnectedNodesResult nodes = - // Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); - if (localnode == null || (localnode != null && localnode.isEmpty())) - setLocalNodeName(); - - CapabilityInfo capabilityInfo = getCapabilities(); - - int count = 0; - Node phoneNode = null; - - if (capabilityInfo != null) { - phoneNode = updatePhoneSyncBgsCapability(capabilityInfo); - count = capabilityInfo.getNodes().size(); - } - - Log.d(TAG, "doInBackground connected. CapabilityApi.GetCapabilityResult mPhoneNodeID=" - + (phoneNode != null ? phoneNode.getId() : "") + " count=" + count + " localnode=" - + localnode);// KS - - if (count > 0) { - - for (Node node : capabilityInfo.getNodes()) { - - // Log.d(TAG, "doInBackground path: " + path); - - switch (path) { - // simple send as is payloads - - case WEARABLE_RESEND_PATH: - Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), - WEARABLE_RESEND_PATH, null); - break; - case WEARABLE_DATA_PATH: - case WEARABLE_CANCELBOLUS_PATH: - case WEARABLE_CONFIRM_ACTIONSTRING_PATH: - case WEARABLE_INITIATE_ACTIONSTRING_PATH: - case OPEN_SETTINGS: - case NEW_STATUS_PATH: - case NEW_PREFERENCES_PATH: - case BASAL_DATA_PATH: - case BOLUS_PROGRESS_PATH: - case ACTION_CONFIRMATION_REQUEST_PATH: - case NEW_CHANGECONFIRMATIONREQUEST_PATH: - case ACTION_CANCELNOTIFICATION_REQUEST_PATH: { - Log.w(TAG, logPrefix + "Unhandled path"); - // sendMessagePayload(node, path, path, payload); - } - - default:// SYNC_ALL_DATA - // this fall through is messy and non-deterministic for new paths - - } - } - } else { - - Log.d(TAG, logPrefix + "doInBackground connected but getConnectedNodes returns 0."); - - } - } else { - // no resend - Log.d(TAG, logPrefix + "Inside the timeout, will not be executed"); - - } - } else { - Log.d(TAG, logPrefix + "Not connected for sending: api " - + ((googleApiClient == null) ? "is NULL!" : "not null")); - if (googleApiClient != null) { - googleApiClient.connect(); - } else { - googleApiConnect(); - } - } - - } catch (Exception ex) { - Log.e(TAG, logPrefix + "Error executing DataRequester in background. Exception: " + ex.getMessage()); - } - - return null; - } - } - - - public CapabilityInfo getCapabilities() { - - CapabilityApi.GetCapabilityResult capabilityResult = Wearable.CapabilityApi.getCapability(googleApiClient, - CAPABILITY_PHONE_APP, CapabilityApi.FILTER_REACHABLE).await(GET_CAPABILITIES_TIMEOUT_MS, - TimeUnit.MILLISECONDS); - - if (!capabilityResult.getStatus().isSuccess()) { - Log.e(TAG, logPrefix + "doInBackground Failed to get capabilities, status: " - + capabilityResult.getStatus().getStatusMessage()); - return null; - } - - return capabilityResult.getCapability(); - - } - public class BolusCancelTask extends AsyncTask { Context mContext; @@ -266,8 +112,11 @@ public class ListenerService extends WearableListenerService implements GoogleAp @Override protected Void doInBackground(Void... params) { - // Log.d(TAG, logPrefix + "BolusCancelTask: doInBack: " + params); - + Log.d(TAG, logPrefix + "BolusCancelTask.doInBackground: " + params); + if (!googleApiClient.isConnected()) { + Log.i(TAG, "BolusCancelTask.doInBackground: not connected"); + googleApiClient.blockingConnect(15, TimeUnit.SECONDS); + } if (googleApiClient.isConnected()) { NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); @@ -275,16 +124,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), WEARABLE_CANCELBOLUS_PATH, null); } - } else { - googleApiClient.blockingConnect(15, TimeUnit.SECONDS); - if (googleApiClient.isConnected()) { - NodeApi.GetConnectedNodesResult nodes = - Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); - for (Node node : nodes.getNodes()) { - Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), WEARABLE_CANCELBOLUS_PATH, null); - } - - } } return null; } @@ -303,9 +142,12 @@ public class ListenerService extends WearableListenerService implements GoogleAp @Override protected Void doInBackground(Void... params) { + Log.i(TAG, "MessageActionTask.doInBackground: "); - forceGoogleApiConnect(); - + if (!googleApiClient.isConnected()) { + Log.i(TAG, "MessageActionTask.doInBackground: not connected"); + googleApiClient.blockingConnect(15, TimeUnit.SECONDS); + } if (googleApiClient.isConnected()) { NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); @@ -313,22 +155,42 @@ public class ListenerService extends WearableListenerService implements GoogleAp Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), mMessagePath, mActionstring.getBytes()); } - } else { - googleApiClient.blockingConnect(15, TimeUnit.SECONDS); - if (googleApiClient.isConnected()) { - NodeApi.GetConnectedNodesResult nodes = - Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); - for (Node node : nodes.getNodes()) { - Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), mMessagePath, mActionstring.getBytes()); - } - } } return null; } } + public class ResendDataTask extends AsyncTask { + Context mContext; + + ResendDataTask(Context context) { + mContext = context; + } + + @Override + protected Void doInBackground(Void... params) { + Log.d(TAG, logPrefix + "ResendDataTask.doInBackground: " + params); + + if (!googleApiClient.isConnected()) { + Log.i(TAG, "ResendDataTask.doInBackground: not connected"); + googleApiClient.blockingConnect(15, TimeUnit.SECONDS); + } + if (googleApiClient.isConnected()) { + Log.i(TAG, "ResendDataTask.doInBackground: connected"); + NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(googleApiClient).await(); + for (Node node : nodes.getNodes()) { + Wearable.MessageApi.sendMessage(googleApiClient, node.getId(), WEARABLE_RESEND_PATH, null); + } + } else { + Log.i(TAG, "ResendDataTask.doInBackground: could not connect"); + } + return null; + + } + } + public void requestData() { - sendData(WEARABLE_RESEND_PATH, null); + new ResendDataTask(this).execute(); } public void cancelBolus() { @@ -343,59 +205,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp new MessageActionTask(this, WEARABLE_INITIATE_ACTIONSTRING_PATH, actionstring).execute(); } - - private Node updatePhoneSyncBgsCapability(CapabilityInfo capabilityInfo) { - // Log.d(TAG, "CapabilityInfo: " + capabilityInfo); - - Set connectedNodes = capabilityInfo.getNodes(); - return pickBestNode(connectedNodes); - // mPhoneNodeId = pickBestNodeId(connectedNodes); - } - - - private Node pickBestNode(Set nodes) { - Node bestNode = null; - // Find a nearby node or pick one arbitrarily - for (Node node : nodes) { - if (node.isNearby()) { - return node; - } - bestNode = node; - } - return bestNode; - } - - - private synchronized void sendData(String path, byte[] payload) { - // Log.d(TAG, "WR: sendData: path: " + path + ", payload=" + payload); - - if (path == null) - return; - if (mDataRequester != null) { - // Log.d(TAG, logPrefix + "sendData DataRequester != null lastRequest:" + - // WearUtil.dateTimeText(lastRequest)); - if (mDataRequester.getStatus() != AsyncTask.Status.FINISHED) { - // Log.d(TAG, logPrefix + "sendData Should be canceled? Let run 'til finished."); - // mDataRequester.cancel(true); - } - } - - Log.d(TAG, - logPrefix + "sendData: execute lastRequest:" + wearUtil.dateTimeText(lastRequest)); - mDataRequester = (DataRequester) new DataRequester(this, path, payload).execute(); - // executeTask(mDataRequester); - - // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - // Log.d(TAG, "sendData SDK < M call execute lastRequest:" + WearUtil.dateTimeText(lastRequest)); - // mDataRequester = (DataRequester) new DataRequester(this, path, payload).execute(); - // } else { - // Log.d(TAG, "sendData SDK >= M call executeOnExecutor lastRequest:" + WearUtil.dateTimeText(lastRequest)); - // // TODO xdrip executor - // mDataRequester = (DataRequester) new DataRequester(this, path, payload).executeOnExecutor(xdrip.executor); - // } - } - - private void googleApiConnect() { if (googleApiClient != null) { // Remove old listener(s) @@ -419,20 +228,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp Wearable.MessageApi.addListener(googleApiClient, this); } - - private void forceGoogleApiConnect() { - if (googleApiClient == null || (!googleApiClient.isConnected() && !googleApiClient.isConnecting())) { - try { - Log.d(TAG, "forceGoogleApiConnect: forcing google api reconnection"); - googleApiConnect(); - Thread.sleep(2000); - } catch (InterruptedException e) { - // - } - } - } - - @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -486,7 +281,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp return START_STICKY; } - @Override public void onDataChanged(DataEventBuffer dataEvents) { @@ -548,10 +342,13 @@ public class ListenerService extends WearableListenerService implements GoogleAp SharedPreferences.Editor editor = sharedPreferences.edit(); String keyControl = getString(R.string.key_wear_control); if (dataMap.containsKey(keyControl)) { + boolean previousWearControl = sharedPreferences.getBoolean(keyControl, false); boolean wearControl = dataMap.getBoolean(keyControl, false); editor.putBoolean(keyControl, wearControl); editor.apply(); - updateTiles(); + if (wearControl != previousWearControl) { + updateTiles(); + } } String keyPercentage = getString(R.string.key_boluswizard_percentage); if (dataMap.containsKey(keyPercentage)) { @@ -565,6 +362,26 @@ public class ListenerService extends WearableListenerService implements GoogleAp editor.putBoolean(keyUnits, mgdl); editor.apply(); } + } else if (path.equals(QUICK_WIZARD_PATH)) { + dataMap = DataMapItem.fromDataItem(event.getDataItem()).getDataMap(); + Log.i(TAG, "onDataChanged: QUICK_WIZARD_PATH" + dataMap); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); + dataMap.remove("timestamp"); + String key = getString(R.string.key_quick_wizard_data_map); + String dataString = Base64.encodeToString(dataMap.toByteArray(), Base64.DEFAULT); + if (!dataString.equals(sharedPreferences.getString(key, ""))) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(key, dataString); + editor.apply(); + // Todo maybe add debounce function, due to 20 seconds update limit? + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + TileService.getUpdater(this) + .requestUpdate(QuickWizardTileService.class); + } + Log.i(TAG, "onDataChanged: updated QUICK_WIZARD"); + } else { + Log.i(TAG, "onDataChanged: ignore update"); + } } else if (path.equals(NEW_CHANGECONFIRMATIONREQUEST_PATH)) { String title = DataMapItem.fromDataItem(event.getDataItem()).getDataMap().getString("title"); String message = DataMapItem.fromDataItem(event.getDataItem()).getDataMap().getString("message"); @@ -592,6 +409,9 @@ public class ListenerService extends WearableListenerService implements GoogleAp TileService.getUpdater(this) .requestUpdate(TempTargetTileService.class); + + TileService.getUpdater(this) + .requestUpdate(QuickWizardTileService.class); } } @@ -666,7 +486,7 @@ public class ListenerService extends WearableListenerService implements GoogleAp PendingIntent cancelPendingIntent = PendingIntent.getService(this, 0, cancelIntent, 0); NotificationCompat.Builder notificationBuilder = - new NotificationCompat.Builder(this, vibrate ? AAPS_NOTIFY_CHANNEL_ID_BOLUSPROGRESS: AAPS_NOTIFY_CHANNEL_ID_BOLUSPROGRESS_SILENT) + new NotificationCompat.Builder(this, vibrate ? AAPS_NOTIFY_CHANNEL_ID_BOLUSPROGRESS : AAPS_NOTIFY_CHANNEL_ID_BOLUSPROGRESS_SILENT) .setSmallIcon(R.drawable.ic_icon) .setContentTitle(getString(R.string.bolus_progress)) .setContentText(progresspercent + "% - " + progresstatus) @@ -723,7 +543,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp bolusprogressThread.start(); } - private class DismissThread extends Thread { private final int notificationID; private final int seconds; @@ -757,7 +576,7 @@ public class ListenerService extends WearableListenerService implements GoogleAp context.startService(intent); } - public static void initiateAction(Context context, String actionstring) { + public static void initiateAction(Context context, @NotNull String actionstring) { Intent intent = new Intent(context, ListenerService.class); intent.putExtra("actionstring", actionstring); intent.setAction(ACTION_INITIATE_ACTION); @@ -780,18 +599,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp public void onConnected(Bundle bundle) { // Log.d(TAG, logPrefix + "onConnected call requestData"); - CapabilityApi.CapabilityListener capabilityListener = new CapabilityApi.CapabilityListener() { - - @Override - public void onCapabilityChanged(CapabilityInfo capabilityInfo) { - updatePhoneSyncBgsCapability(capabilityInfo); - Log.d(TAG, logPrefix + "onConnected onCapabilityChanged mPhoneNodeID:" + mPhoneNodeId - + ", Capability: " + capabilityInfo); - } - }; - - Wearable.CapabilityApi.addCapabilityListener(googleApiClient, capabilityListener, CAPABILITY_PHONE_APP); - Wearable.ChannelApi.addListener(googleApiClient, this); requestData(); } @@ -806,28 +613,6 @@ public class ListenerService extends WearableListenerService implements GoogleAp } - - private void setLocalNodeName() { - forceGoogleApiConnect(); - PendingResult result = Wearable.NodeApi.getLocalNode(googleApiClient); - result.setResultCallback(new ResultCallback() { - - @Override - public void onResult(NodeApi.GetLocalNodeResult getLocalNodeResult) { - if (!getLocalNodeResult.getStatus().isSuccess()) { - Log.e(TAG, "ERROR: failed to getLocalNode Status=" - + getLocalNodeResult.getStatus().getStatusMessage()); - } else { - Log.d(TAG, "getLocalNode Status=: " + getLocalNodeResult.getStatus().getStatusMessage()); - Node getnode = getLocalNodeResult.getNode(); - localnode = getnode != null ? getnode.getDisplayName() + "|" + getnode.getId() : ""; - Log.d(TAG, "setLocalNodeName. localnode=" + localnode); - } - } - }); - } - - @Override public void onDestroy() { super.onDestroy(); diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/actions/BackgroundActionActivity.kt b/wear/src/main/java/info/nightscout/androidaps/interaction/actions/BackgroundActionActivity.kt new file mode 100644 index 0000000000..a7db92eea9 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/actions/BackgroundActionActivity.kt @@ -0,0 +1,29 @@ +package info.nightscout.androidaps.interaction.actions + +import android.app.Activity +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import info.nightscout.androidaps.data.ListenerService + +const val TAG = "QuickWizard" + +class BackgroundActionActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val actionString = intent.extras?.getString("actionString") + Log.i(TAG, "QuickWizardActivity.onCreate: actionString=$actionString") + if (actionString != null) { + ListenerService.initiateAction(this, actionString) + val message = intent.extras?.getString("message") + if (message != null) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + } else { + Log.e(TAG, "BackgroundActionActivity.onCreate extras 'actionString' required") + } + finishAffinity() + } + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/interaction/actions/TempTargetActivity.java b/wear/src/main/java/info/nightscout/androidaps/interaction/actions/TempTargetActivity.java index 8c761f898b..df2db8fafa 100644 --- a/wear/src/main/java/info/nightscout/androidaps/interaction/actions/TempTargetActivity.java +++ b/wear/src/main/java/info/nightscout/androidaps/interaction/actions/TempTargetActivity.java @@ -31,9 +31,7 @@ public class TempTargetActivity extends ViewSelectorActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (executeInBackground()){ - return; - } + setAdapter(new MyGridViewPagerAdapter()); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); @@ -41,21 +39,6 @@ public class TempTargetActivity extends ViewSelectorActivity { isSingleTarget = sp.getBoolean("singletarget", true); } - private boolean executeInBackground() { - Bundle extras = getIntent().getExtras(); - if (extras != null) { - String actionString = extras.getString("actionString", ""); - boolean inBackground = extras.getBoolean("inBackground", false); - if (inBackground) { - ListenerService.initiateAction(this, actionString); - confirmAction(this, R.string.action_tempt_confirmation); - finishAffinity(); - return true; - } - } - return false; - } - @Override protected void onPause() { super.onPause(); diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/ActionSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/ActionSource.kt index 9c2ccfdf56..a495d3ddf8 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/ActionSource.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/ActionSource.kt @@ -1,5 +1,6 @@ package info.nightscout.androidaps.tile +import android.content.res.Resources import info.nightscout.androidaps.R import info.nightscout.androidaps.interaction.actions.BolusActivity import info.nightscout.androidaps.interaction.actions.TreatmentActivity @@ -7,48 +8,49 @@ import info.nightscout.androidaps.interaction.actions.ECarbActivity import info.nightscout.androidaps.interaction.actions.TempTargetActivity import info.nightscout.androidaps.interaction.actions.WizardActivity -object ActionSource : TileSource { +object ActionSource : StaticTileSource(), TileSource { - override fun getActions(): List { + override val preferencePrefix = "tile_action_" + + override fun getActions(resources: Resources): List { return listOf( - Action( - id = 0, + StaticAction( settingName = "wizard", - nameRes = R.string.menu_wizard_short, + buttonText = resources.getString(R.string.menu_wizard_short), iconRes = R.drawable.ic_calculator_green, activityClass = WizardActivity::class.java.name, ), - Action( - id = 1, + StaticAction( settingName = "treatment", - nameRes = R.string.menu_treatment_short, + buttonText = resources.getString(R.string.menu_treatment_short), iconRes = R.drawable.ic_bolus_carbs, activityClass = TreatmentActivity::class.java.name, ), - Action( - id = 2, + StaticAction( settingName = "bolus", - nameRes = R.string.action_insulin, + buttonText = resources.getString(R.string.action_insulin), iconRes = R.drawable.ic_bolus, activityClass = BolusActivity::class.java.name, ), - Action( - id = 3, + StaticAction( settingName = "carbs", - nameRes = R.string.action_carbs, + buttonText = resources.getString(R.string.action_carbs), iconRes = R.drawable.ic_carbs_orange, activityClass = ECarbActivity::class.java.name, ), - Action( - id = 4, + StaticAction( settingName = "temp_target", - nameRes = R.string.menu_tempt, + buttonText = resources.getString(R.string.menu_tempt), iconRes = R.drawable.ic_temptarget_flat, activityClass = TempTargetActivity::class.java.name, ) ) } + override fun getResourceReferences(resources: Resources): List { + return getActions(resources).map { it.iconRes } + } + override fun getDefaultConfig(): Map { return mapOf( "tile_action_1" to "wizard", 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 da0dac33cb..b30ec2a87a 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/ActionsTileService.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/ActionsTileService.kt @@ -1,10 +1,6 @@ package info.nightscout.androidaps.tile class ActionsTileService : TileBase() { - - override val preferencePrefix = "tile_action_" - override val resourceVersion = "1" - override val idIconActionPrefix = "ic_action_" + override val resourceVersion = "ActionsTileService" override val source = ActionSource - } diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardSource.kt new file mode 100644 index 0000000000..d241ac0b9a --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardSource.kt @@ -0,0 +1,79 @@ +package info.nightscout.androidaps.tile + +import android.content.Context +import android.content.res.Resources +import android.util.Base64 +import android.util.Log +import androidx.preference.PreferenceManager +import com.google.android.gms.wearable.DataMap +import info.nightscout.androidaps.R +import info.nightscout.androidaps.interaction.actions.BackgroundActionActivity +import java.util.* + +object QuickWizardSource : TileSource { + + override fun getSelectedActions(context: Context): List { + val quickList = mutableListOf() + val quickMap = getDataMap(context) + val sfm = secondsFromMidnight() + + for (quick in quickMap) { + val validFrom = quick.getInt("from", 0) + val validTo = quick.getInt("to", 0) + val isActive = sfm in validFrom..validTo + // use from and to to schedule new update for timeline, for now just refresh every minute + val guid = quick.getString("guid", "") + if (isActive && guid != "") { + quickList.add( + Action( + buttonText = quick.getString("button_text", "?"), + buttonTextSub = "${quick.getInt("carbs", 0)} g", + iconRes = R.drawable.ic_quick_wizard, + activityClass = BackgroundActionActivity::class.java.name, + actionString = "quick_wizard $guid", + message = context.resources.getString(R.string.action_quick_wizard_confirmation), + ) + ) + Log.i(TAG, "getSelectedActions: active " + quick.getString("button_text", "?") + " guid=" + guid) + } else { + Log.i(TAG, "getSelectedActions: not active " + quick.getString("button_text", "?") + " guid=" + guid) + } + + } + + return quickList + } + + private fun getDataMap(context: Context): ArrayList { + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) + val key = context.resources.getString(R.string.key_quick_wizard_data_map) + if (sharedPrefs.contains(key)) { + val rawB64Data: String? = sharedPrefs.getString(key, null) + val rawData: ByteArray = Base64.decode(rawB64Data, Base64.DEFAULT) + try { + val map = DataMap.fromByteArray(rawData) + return map.getDataMapArrayList("quick_wizard") + + } catch (ex: IllegalArgumentException) { + Log.e(TAG, "getSelectedActions: IllegalArgumentException ", ex) + } + } + return arrayListOf() + } + + private fun secondsFromMidnight(): Int { + val c = Calendar.getInstance() + c.set(Calendar.HOUR_OF_DAY, 0) + c.set(Calendar.MINUTE, 0) + c.set(Calendar.SECOND, 0) + c.set(Calendar.MILLISECOND, 0) + val passed: Long = System.currentTimeMillis() - c.timeInMillis + + return (passed / 1000).toInt() + } + + override fun getResourceReferences(resources: Resources): List { + return listOf(R.drawable.ic_quick_wizard) + } + +} diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt b/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt new file mode 100644 index 0000000000..ef85e789a7 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/tile/QuickWizardTileService.kt @@ -0,0 +1,8 @@ +package info.nightscout.androidaps.tile + +const val TAG = "QuickWizard" + +class QuickWizardTileService : TileBase() { + override val resourceVersion = "QuickWizardTileService" + override val source = QuickWizardSource +} diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/StaticTileSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/StaticTileSource.kt new file mode 100644 index 0000000000..bbd7be1a91 --- /dev/null +++ b/wear/src/main/java/info/nightscout/androidaps/tile/StaticTileSource.kt @@ -0,0 +1,59 @@ +package info.nightscout.androidaps.tile + +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Resources +import androidx.annotation.DrawableRes +import androidx.preference.PreferenceManager + +class StaticAction( + val settingName: String, + buttonText: String, + buttonTextSub: String? = null, + activityClass: String, + @DrawableRes iconRes: Int, + actionString: String? = null, + message: String? = null, +) : Action(buttonText, buttonTextSub, activityClass, iconRes, actionString, message) + +abstract class StaticTileSource { + + abstract fun getActions(resources: Resources): List + + abstract val preferencePrefix: String + abstract fun getDefaultConfig(): Map + + open fun getSelectedActions(context: Context): List { + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) + setDefaultSettings(sharedPrefs) + + val actionList: MutableList = mutableListOf() + for (i in 1..4) { + val action = getActionFromPreference(context.resources, sharedPrefs, i) + if (action != null) { + actionList.add(action) + } + } + if (actionList.isEmpty()) { + return getActions(context.resources).take(4) + } + return actionList + } + + private fun getActionFromPreference(resources: Resources, sharedPrefs: SharedPreferences, index: Int): Action? { + val actionPref = sharedPrefs.getString(preferencePrefix + index, "none") + return getActions(resources).find { action -> action.settingName == actionPref } + } + + open fun setDefaultSettings(sharedPrefs: SharedPreferences) { + val defaults = getDefaultConfig() + val firstKey = defaults.firstNotNullOf { settings -> settings.key } + if (!sharedPrefs.contains(firstKey)) { + val editor = sharedPrefs.edit() + for ((key, value) in defaults) { + editor.putString(key, value) + } + editor.apply() + } + } +} diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetSource.kt b/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetSource.kt index 234d35bcdf..37d4f55e1c 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetSource.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetSource.kt @@ -1,61 +1,64 @@ package info.nightscout.androidaps.tile +import android.content.res.Resources import info.nightscout.androidaps.R +import info.nightscout.androidaps.interaction.actions.BackgroundActionActivity import info.nightscout.androidaps.interaction.actions.TempTargetActivity -object TempTargetSource : TileSource { +object TempTargetSource : StaticTileSource(), TileSource { + override val preferencePrefix= "tile_tempt_" - override fun getActions(): List { + override fun getActions(resources: Resources): List { + val message = resources.getString(R.string.action_tempt_confirmation) return listOf( - Action( - id = 0, + StaticAction( settingName = "activity", - nameRes = R.string.temp_target_activity, + buttonText = resources.getString(R.string.temp_target_activity), iconRes = R.drawable.ic_target_activity, - activityClass = TempTargetActivity::class.java.name, - background = true, + activityClass = BackgroundActionActivity::class.java.name, + message = message, // actionString = "temptarget false 90 8.0 8.0", actionString = "temptarget preset activity", ), - Action( - id = 1, + StaticAction( settingName = "eating_soon", - nameRes = R.string.temp_target_eating_soon, + buttonText = resources.getString(R.string.temp_target_eating_soon), iconRes = R.drawable.ic_target_eatingsoon, - activityClass = TempTargetActivity::class.java.name, - background = true, + activityClass = BackgroundActionActivity::class.java.name, + message = message, // actionString = "temptarget false 45 4.5 4.5", actionString = "temptarget preset eating", ), - Action( - id = 2, + StaticAction( settingName = "hypo", - nameRes = R.string.temp_target_hypo, + buttonText = resources.getString(R.string.temp_target_hypo), iconRes = R.drawable.ic_target_hypo, - activityClass = TempTargetActivity::class.java.name, - background = true, + activityClass = BackgroundActionActivity::class.java.name, + message = message, // actionString = "temptarget false 45 7.0 7.0", actionString = "temptarget preset hypo", ), - Action( - id = 3, + StaticAction( settingName = "manual", - nameRes = R.string.temp_target_manual, + buttonText = resources.getString(R.string.temp_target_manual), iconRes = R.drawable.ic_target_manual, activityClass = TempTargetActivity::class.java.name, ), - Action( - id = 4, + StaticAction( settingName = "cancel", - nameRes = R.string.generic_cancel, + buttonText = resources.getString(R.string.generic_cancel), iconRes = R.drawable.ic_target_cancel, - activityClass = TempTargetActivity::class.java.name, + activityClass = BackgroundActionActivity::class.java.name, + message = message, actionString = "temptarget cancel", - background = true, ) ) } + override fun getResourceReferences(resources: Resources): List { + return getActions(resources).map { it.iconRes } + } + override fun getDefaultConfig(): Map { return mapOf( "tile_tempt_1" to "activity", 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 1c7c6ebe39..6e0ff0e30f 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetTileService.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/TempTargetTileService.kt @@ -2,9 +2,7 @@ package info.nightscout.androidaps.tile class TempTargetTileService : TileBase() { - override val preferencePrefix = "tile_tempt_" - override val resourceVersion = "1" - override val idIconActionPrefix = "ic_tempt_" + override val resourceVersion = "TempTargetTileService" override val source = TempTargetSource; } diff --git a/wear/src/main/java/info/nightscout/androidaps/tile/TileBase.kt b/wear/src/main/java/info/nightscout/androidaps/tile/TileBase.kt index 415e727920..5c86a89f84 100644 --- a/wear/src/main/java/info/nightscout/androidaps/tile/TileBase.kt +++ b/wear/src/main/java/info/nightscout/androidaps/tile/TileBase.kt @@ -1,8 +1,9 @@ package info.nightscout.androidaps.tile -import android.content.SharedPreferences +import android.content.Context +import android.os.Build import androidx.annotation.DrawableRes -import androidx.annotation.StringRes +import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.preference.PreferenceManager import androidx.wear.tiles.ActionBuilders @@ -42,18 +43,17 @@ private const val LARGE_SCREEN_WIDTH_DP = 210 interface TileSource { - fun getActions(): List - fun getDefaultConfig(): Map + fun getResourceReferences(resources: android.content.res.Resources): List + fun getSelectedActions(context: Context): List } -data class Action( - val id: Int, - val settingName: String, - @StringRes val nameRes: Int, +open class Action( + val buttonText: String, + val buttonTextSub: String? = null, val activityClass: String, @DrawableRes val iconRes: Int, - val background: Boolean = false, val actionString: String? = null, + val message: String? = null, ) enum class WearControl { @@ -62,10 +62,7 @@ enum class WearControl { abstract class TileBase : TileService() { - open val resourceVersion = "1" - open val idIconActionPrefix = "ic_action_" - - abstract val preferencePrefix: String + abstract val resourceVersion: String abstract val source: TileSource private val serviceJob = Job() @@ -89,19 +86,25 @@ abstract class TileBase : TileService() { .build() } + private fun getSelectedActions(): List { + // TODO check why thi scan not be don in scope of the coroutine + return source.getSelectedActions(this) + } + + @RequiresApi(Build.VERSION_CODES.N) override fun onResourcesRequest( requestParams: ResourcesRequest ): ListenableFuture = serviceScope.future { Resources.Builder() .setVersion(resourceVersion) .apply { - source.getActions().mapNotNull { action -> + source.getResourceReferences(resources).forEach { resourceId -> addIdToImageMapping( - idIconActionPrefix + action.id, + resourceId.toString(), ImageResource.Builder() .setAndroidResourceByResId( AndroidImageResourceByResId.Builder() - .setResourceId(action.iconRes) + .setResourceId(resourceId) .build() ) .build() @@ -157,15 +160,17 @@ abstract class TileBase : TileService() { .build() private fun doAction(action: Action): ActionBuilders.Action { - val inBackground = ActionBuilders.AndroidBooleanExtra.Builder().setValue(action.background).build() val builder = ActionBuilders.AndroidActivity.Builder() .setClassName(action.activityClass) .setPackageName(this.packageName) - .addKeyToExtraMapping("inBackground", inBackground) if (action.actionString != null) { val actionString = ActionBuilders.AndroidStringExtra.Builder().setValue(action.actionString).build() builder.addKeyToExtraMapping("actionString", actionString) } + if (action.message != null) { + val message = ActionBuilders.AndroidStringExtra.Builder().setValue(action.message).build() + builder.addKeyToExtraMapping("message", message) + } return ActionBuilders.LaunchAction.Builder() .setAndroidActivity(builder.build()) @@ -174,8 +179,8 @@ abstract class TileBase : TileService() { private fun action(action: Action, deviceParameters: DeviceParameters): LayoutElement { val circleDiameter = circleDiameter(deviceParameters) - val iconSize = dp(circleDiameter * ICON_SIZE_FRACTION) - val text = resources.getString(action.nameRes) + val text = action.buttonText + val textSub = action.buttonTextSub return Box.Builder() .setWidth(dp(circleDiameter)) .setHeight(dp(circleDiameter)) @@ -193,7 +198,7 @@ abstract class TileBase : TileService() { ) .setSemantics( Semantics.Builder() - .setContentDescription(text) + .setContentDescription("$text $textSub") .build() ) .setClickable( @@ -203,32 +208,55 @@ abstract class TileBase : TileService() { ) .build() ) - .addContent( - Column.Builder() - .addContent( - Image.Builder() - .setWidth(iconSize) - .setHeight(iconSize) - .setResourceId(idIconActionPrefix + action.id) - .build() - ).addContent( - Text.Builder() - .setText(text) - .setFontStyle( - FontStyle.Builder() - .setWeight(FONT_WEIGHT_BOLD) - .setColor( - argb(ContextCompat.getColor(baseContext, R.color.white)) - ) - .setSize(buttonTextSize(deviceParameters, text)) - .build() - ) - .build() - ).build() - ) + .addContent(addTextContent(action, deviceParameters)) .build() } + private fun addTextContent(action: Action, deviceParameters: DeviceParameters): LayoutElement { + val circleDiameter = circleDiameter(deviceParameters) + val iconSize = dp(circleDiameter * ICON_SIZE_FRACTION) + val text = action.buttonText + val textSub = action.buttonTextSub + val col = Column.Builder() + .addContent( + Image.Builder() + .setWidth(iconSize) + .setHeight(iconSize) + .setResourceId(action.iconRes.toString()) + .build() + ).addContent( + Text.Builder() + .setText(text) + .setFontStyle( + FontStyle.Builder() + .setWeight(FONT_WEIGHT_BOLD) + .setColor( + argb(ContextCompat.getColor(baseContext, R.color.white)) + ) + .setSize(buttonTextSize(deviceParameters, text)) + .build() + ) + .build() + ) + if (textSub != null) { + col.addContent( + Text.Builder() + .setText(textSub) + .setFontStyle( + FontStyle.Builder() + .setColor( + argb(ContextCompat.getColor(baseContext, R.color.white)) + ) + .setSize(buttonTextSize(deviceParameters, textSub)) + .build() + ) + .build() + ) + } + + return col.build() + } + private fun circleDiameter(deviceParameters: DeviceParameters) = when (deviceParameters.screenShape) { SCREEN_SHAPE_ROUND -> ((sqrt(2f) - 1) * deviceParameters.screenHeightDp) - (2 * SPACING_ACTIONS) else -> 0.5f * deviceParameters.screenHeightDp - SPACING_ACTIONS @@ -257,37 +285,4 @@ abstract class TileBase : TileService() { return WearControl.DISABLED } - private fun getSelectedActions(): List { - val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) - setDefaultSettings(sharedPrefs) - - val actionList: MutableList = mutableListOf() - for (i in 1..4) { - val action = getActionFromPreference(sharedPrefs, i) - if (action != null) { - actionList.add(action) - } - } - if (actionList.isEmpty()) { - return source.getActions().take(4) - } - return actionList - } - - private fun getActionFromPreference(sharedPrefs: SharedPreferences, index: Int): Action? { - val actionPref = sharedPrefs.getString(preferencePrefix + index, "none") - return source.getActions().find { action -> action.settingName == actionPref } - } - - private fun setDefaultSettings(sharedPrefs: SharedPreferences) { - val defaults = source.getDefaultConfig() - val firstKey = defaults.firstNotNullOf { settings -> settings.key } - if (!sharedPrefs.contains(firstKey)) { - val editor = sharedPrefs.edit() - for ((key, value) in defaults) { - editor.putString(key, value) - } - editor.apply() - } - } } diff --git a/wear/src/main/res/drawable-notround-nodpi/quick_wizard_tile_preview.png b/wear/src/main/res/drawable-notround-nodpi/quick_wizard_tile_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..850ff52de72e6d53f05b71b5e352bb2b809025f0 GIT binary patch literal 13916 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 + + + + + + diff --git a/wear/src/main/res/values/strings.xml b/wear/src/main/res/values/strings.xml index 77481f8360..f6865795b1 100644 --- a/wear/src/main/res/values/strings.xml +++ b/wear/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ AAPS(Actions) AAPS(Temp Target) + AAPS(Quick Wizard) No data! Old data! @@ -86,9 +87,11 @@ None Default Menu + XL Duration Temp Target Requested + Quick Wizard Requested Treatment Requested Bolus Requested Calculation Requested @@ -165,7 +168,7 @@ AAPS Bolus Progress Silent Bolus progress and cancel Bolus progress and cancel with less vibrations - + QuickWizard wearcontrol units_mgdl boluswizard_percentage @@ -182,5 +185,6 @@ No config available Wear controls disabled No data available + quick_wizard_data_map