AndroidAPS/wear/src/main/java/info/nightscout/androidaps/tile/TileBase.kt

289 lines
12 KiB
Kotlin
Raw Normal View History

package info.nightscout.androidaps.tile
2022-01-18 20:54:39 +01:00
import android.content.Context
import android.os.Build
import androidx.annotation.DrawableRes
2022-01-18 20:54:39 +01:00
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import androidx.wear.tiles.ActionBuilders
import androidx.wear.tiles.ColorBuilders.argb
import androidx.wear.tiles.DeviceParametersBuilders.SCREEN_SHAPE_ROUND
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters
import androidx.wear.tiles.DimensionBuilders.SpProp
import androidx.wear.tiles.DimensionBuilders.dp
import androidx.wear.tiles.DimensionBuilders.sp
import androidx.wear.tiles.LayoutElementBuilders.*
import androidx.wear.tiles.ModifiersBuilders.Background
import androidx.wear.tiles.ModifiersBuilders.Clickable
import androidx.wear.tiles.ModifiersBuilders.Corner
import androidx.wear.tiles.ModifiersBuilders.Modifiers
import androidx.wear.tiles.ModifiersBuilders.Semantics
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
import androidx.wear.tiles.ResourceBuilders.AndroidImageResourceByResId
import androidx.wear.tiles.ResourceBuilders.ImageResource
import androidx.wear.tiles.ResourceBuilders.Resources
import androidx.wear.tiles.TileBuilders.Tile
import androidx.wear.tiles.TileService
import androidx.wear.tiles.TimelineBuilders.Timeline
import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import com.google.common.util.concurrent.ListenableFuture
import info.nightscout.androidaps.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.guava.future
import kotlin.math.sqrt
private const val SPACING_ACTIONS = 3f
private const val ICON_SIZE_FRACTION = 0.4f // Percentage of button diameter
private const val BUTTON_COLOR = R.color.gray_850
private const val LARGE_SCREEN_WIDTH_DP = 210
interface TileSource {
2022-01-18 20:54:39 +01:00
fun getResourceReferences(resources: android.content.res.Resources): List<Int>
fun getSelectedActions(context: Context): List<Action>
}
2022-01-18 20:54:39 +01:00
open class Action(
val buttonText: String,
val buttonTextSub: String? = null,
val activityClass: String,
@DrawableRes val iconRes: Int,
2022-01-12 12:22:53 +01:00
val actionString: String? = null,
2022-01-18 20:54:39 +01:00
val message: String? = null,
)
2022-01-13 10:29:31 +01:00
enum class WearControl {
NO_DATA, ENABLED, DISABLED
}
2022-01-12 12:22:53 +01:00
abstract class TileBase : TileService() {
2022-01-18 20:54:39 +01:00
abstract val resourceVersion: String
2022-01-12 12:22:53 +01:00
abstract val source: TileSource
private val serviceJob = Job()
private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob)
override fun onTileRequest(
requestParams: RequestBuilders.TileRequest
): ListenableFuture<Tile> = serviceScope.future {
2022-01-11 10:38:20 +01:00
val actionsSelected = getSelectedActions()
2022-01-13 10:29:31 +01:00
val wearControl = getWearControl()
Tile.Builder()
.setResourcesVersion(resourceVersion)
.setTimeline(
Timeline.Builder().addTimelineEntry(
TimelineEntry.Builder().setLayout(
2022-01-13 10:29:31 +01:00
Layout.Builder().setRoot(layout(wearControl, actionsSelected, requestParams.deviceParameters!!)).build()
).build()
).build()
)
.build()
}
2022-01-18 20:54:39 +01:00
private fun getSelectedActions(): List<Action> {
// 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<Resources> = serviceScope.future {
Resources.Builder()
.setVersion(resourceVersion)
.apply {
2022-01-18 20:54:39 +01:00
source.getResourceReferences(resources).forEach { resourceId ->
addIdToImageMapping(
2022-01-18 20:54:39 +01:00
resourceId.toString(),
ImageResource.Builder()
.setAndroidResourceByResId(
AndroidImageResourceByResId.Builder()
2022-01-18 20:54:39 +01:00
.setResourceId(resourceId)
.build()
)
.build()
)
}
}
.build()
}
2022-01-13 10:29:31 +01:00
private fun layout(wearControl: WearControl, actions: List<Action>, deviceParameters: DeviceParameters): LayoutElement {
if (wearControl == WearControl.DISABLED) {
return Text.Builder()
.setText(resources.getString(R.string.wear_control_not_enabled))
.build()
2022-01-13 10:29:31 +01:00
} else if (wearControl == WearControl.NO_DATA) {
return Text.Builder()
.setText(resources.getString(R.string.wear_control_no_data))
.build()
}
if (actions.isNotEmpty()) {
with(Column.Builder()) {
if (actions.size == 1 || actions.size == 3) {
addContent(addRowSingle(actions[0], deviceParameters))
}
if (actions.size == 4 || actions.size == 2) {
addContent(addRowDouble(actions[0], actions[1], deviceParameters))
}
if (actions.size == 3) {
addContent(addRowDouble(actions[1], actions[2], deviceParameters))
}
if (actions.size == 4) {
addContent(Spacer.Builder().setHeight(dp(SPACING_ACTIONS)).build())
addContent(addRowDouble(actions[2], actions[3], deviceParameters))
}
return build()
}
}
return Text.Builder()
.setText(resources.getString(R.string.tile_no_config))
.build()
}
private fun addRowSingle(action: Action, deviceParameters: DeviceParameters): LayoutElement =
2022-01-12 12:22:53 +01:00
Row.Builder()
.addContent(action(action, deviceParameters))
2022-01-12 12:22:53 +01:00
.build()
private fun addRowDouble(action1: Action, action2: Action, deviceParameters: DeviceParameters): LayoutElement =
Row.Builder()
.addContent(action(action1, deviceParameters))
.addContent(Spacer.Builder().setWidth(dp(SPACING_ACTIONS)).build())
2022-01-12 12:22:53 +01:00
.addContent(action(action2, deviceParameters))
.build()
private fun doAction(action: Action): ActionBuilders.Action {
2022-01-12 12:22:53 +01:00
val builder = ActionBuilders.AndroidActivity.Builder()
.setClassName(action.activityClass)
.setPackageName(this.packageName)
if (action.actionString != null) {
2022-01-12 12:22:53 +01:00
val actionString = ActionBuilders.AndroidStringExtra.Builder().setValue(action.actionString).build()
builder.addKeyToExtraMapping("actionString", actionString)
}
2022-01-18 20:54:39 +01:00
if (action.message != null) {
val message = ActionBuilders.AndroidStringExtra.Builder().setValue(action.message).build()
builder.addKeyToExtraMapping("message", message)
}
2022-01-12 12:22:53 +01:00
return ActionBuilders.LaunchAction.Builder()
2022-01-12 12:22:53 +01:00
.setAndroidActivity(builder.build())
.build()
}
private fun action(action: Action, deviceParameters: DeviceParameters): LayoutElement {
val circleDiameter = circleDiameter(deviceParameters)
2022-01-18 20:54:39 +01:00
val text = action.buttonText
val textSub = action.buttonTextSub
return Box.Builder()
.setWidth(dp(circleDiameter))
.setHeight(dp(circleDiameter))
.setModifiers(
Modifiers.Builder()
.setBackground(
Background.Builder()
.setColor(
argb(ContextCompat.getColor(baseContext, BUTTON_COLOR))
)
.setCorner(
Corner.Builder().setRadius(dp(circleDiameter / 2)).build()
)
.build()
)
.setSemantics(
Semantics.Builder()
2022-01-18 20:54:39 +01:00
.setContentDescription("$text $textSub")
.build()
)
.setClickable(
Clickable.Builder()
.setOnClick(doAction(action))
.build()
)
.build()
)
2022-01-18 20:54:39 +01:00
.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(
2022-01-18 20:54:39 +01:00
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()
2022-01-18 20:54:39 +01:00
)
.build()
)
if (textSub != null) {
col.addContent(
Text.Builder()
.setText(textSub)
.setFontStyle(
FontStyle.Builder()
.setColor(
argb(ContextCompat.getColor(baseContext, R.color.white))
)
2022-01-18 20:54:39 +01:00
.setSize(buttonTextSize(deviceParameters, textSub))
.build()
2022-01-18 20:54:39 +01:00
)
.build()
)
2022-01-18 20:54:39 +01:00
}
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
}
private fun buttonTextSize(deviceParameters: DeviceParameters, text: String): SpProp {
if (text.length > 6) {
return sp(if (isLargeScreen(deviceParameters)) 14f else 12f)
}
return sp(if (isLargeScreen(deviceParameters)) 16f else 14f)
}
private fun isLargeScreen(deviceParameters: DeviceParameters): Boolean {
return deviceParameters.screenWidthDp >= LARGE_SCREEN_WIDTH_DP
}
2022-01-13 10:29:31 +01:00
private fun getWearControl(): WearControl {
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
2022-01-13 11:16:26 +01:00
if (!sharedPrefs.contains("wearcontrol")) {
return WearControl.NO_DATA
}
2022-01-13 10:29:31 +01:00
val wearControlPref = sharedPrefs.getBoolean("wearcontrol", false)
if (wearControlPref) {
return WearControl.ENABLED
}
return WearControl.DISABLED
}
}