More work on Wear OS

This commit is contained in:
2021-09-07 10:27:21 -04:00
parent b98a2afd66
commit 1f5e5bca3a
11 changed files with 258 additions and 265 deletions

View File

@@ -43,14 +43,11 @@ android {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.6.0'
// General Wear functionality
implementation 'androidx.wear:wear:1.1.0'
// Tiles functionality
implementation "androidx.wear.tiles:tiles:1.0.0-alpha11"
// Preview Tiles in an Activity for testing purposes
debugImplementation "androidx.wear.tiles:tiles-renderer:1.0.0-alpha11"
// Helper library for transforming coroutines to ListenableFutures
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.5.1'
implementation 'com.beust:klaxon:5.5'
implementation 'androidx.appcompat:appcompat:1.3.1'
debugImplementation "androidx.wear.tiles:tiles-renderer:1.0.0-alpha11"
}

View File

@@ -16,7 +16,7 @@ class TilePreviewActivity : ComponentActivity() {
tileUiClient = TileUiClient(
context = this,
component = ComponentName(this, GoalsTileService::class.java),
component = ComponentName(this, PowerTileService::class.java),
parentView = rootLayout
)
tileUiClient.connect()

View File

@@ -18,10 +18,10 @@
android:value="true" />
<service
android:name="com.chriskaczor.homemonitor.wear.GoalsTileService"
android:name="com.chriskaczor.homemonitor.wear.PowerTileService"
android:description="@string/tile_description"
android:icon="@drawable/ic_run"
android:label="@string/fitness_tile_label"
android:icon="@drawable/ic_refresh"
android:label="@string/power_tile_label"
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
<intent-filter>
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />

View File

@@ -1,211 +0,0 @@
package com.chriskaczor.homemonitor.wear
import androidx.core.content.ContextCompat
import androidx.wear.tiles.ActionBuilders
import androidx.wear.tiles.ColorBuilders.argb
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters
import androidx.wear.tiles.DimensionBuilders.*
import androidx.wear.tiles.LayoutElementBuilders.*
import androidx.wear.tiles.ModifiersBuilders.*
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
import androidx.wear.tiles.RequestBuilders.TileRequest
import androidx.wear.tiles.ResourceBuilders.*
import androidx.wear.tiles.TileBuilders.Tile
import androidx.wear.tiles.TileService
import androidx.wear.tiles.TimelineBuilders.Timeline
import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.guava.future
// TODO: Review Constants.
// Updating this version triggers a new call to onResourcesRequest(). This is useful for dynamic
// resources, the contents of which change even though their id stays the same (e.g. a graph).
// In this sample, our resources are all fixed, so we use a constant value.
private const val RESOURCES_VERSION = "1"
// dimensions
private val PROGRESS_BAR_THICKNESS = dp(6f)
private val BUTTON_SIZE = dp(48f)
private val BUTTON_RADIUS = dp(24f)
private val BUTTON_PADDING = dp(12f)
private val VERTICAL_SPACING_HEIGHT = dp(8f)
// Complete degrees for a circle (relates to [Arc] component)
private const val ARC_TOTAL_DEGREES = 360f
// identifiers
private const val ID_IMAGE_START_RUN = "image_start_run"
private const val ID_CLICK_START_RUN = "click_start_run"
/**
* Creates a Fitness Tile, showing your progress towards a daily goal. The progress is defined
* randomly, for demo purposes only. A new random progress is shown when the user taps the button.
*/
class GoalsTileService : TileService() {
// For coroutines, use a custom scope we can cancel when the service is destroyed
private val serviceScope = CoroutineScope(Dispatchers.IO)
// TODO: Build a Tile.
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {
// Retrieves progress value to populate the Tile.
val goalProgress = PowerRepository.getPowerStatus()
// Retrieves device parameters to later retrieve font styles for any text in the Tile.
val deviceParams = requestParams.deviceParameters!!
// Creates Tile.
Tile.Builder()
// If there are any graphics/images defined in the Tile's layout, the system will
// retrieve them via onResourcesRequest() and match them with this version number.
.setResourcesVersion(RESOURCES_VERSION)
// Creates a timeline to hold one or more tile entries for a specific time periods.
.setTimeline(
Timeline.Builder()
.addTimelineEntry(
TimelineEntry.Builder()
.setLayout(
Layout.Builder()
.setRoot(
// Creates the root [Box] [LayoutElement]
layout(goalProgress, deviceParams)
)
.build()
)
.build()
)
.build()
).build()
}
// TODO: Supply resources (graphics) for the Tile.
override fun onResourcesRequest(requestParams: ResourcesRequest) = serviceScope.future {
Resources.Builder()
.setVersion(RESOURCES_VERSION)
.addIdToImageMapping(
ID_IMAGE_START_RUN,
ImageResource.Builder()
.setAndroidResourceByResId(
AndroidImageResourceByResId.Builder()
.setResourceId(R.drawable.ic_run)
.build()
)
.build()
)
.build()
}
// TODO: Review onDestroy() - cancellation of the serviceScope
override fun onDestroy() {
super.onDestroy()
// Cleans up the coroutine
serviceScope.cancel()
}
// TODO: Create root Box layout and content.
// Creates a simple [Box] container that lays out its children one over the other. In our
// case, an [Arc] that shows progress on top of a [Column] that includes the current steps
// [Text], the total steps [Text], a [Spacer], and a running icon [Image].
private fun layout(goalProgress: PowerStatus, deviceParameters: DeviceParameters) =
Box.Builder()
// Sets width and height to expand and take up entire Tile space.
.setWidth(expand())
.setHeight(expand())
// Adds an [Arc] via local function.
//.addContent(progressArc(goalProgress.percentage))
// TODO: Add Column containing the rest of the data.
// Adds a [Column] containing the two [Text] objects, a [Spacer], and a [Image].
.addContent(
Column.Builder()
// Adds a [Text] via local function.
.addContent(
currentStepsText(goalProgress.generation.toString(), deviceParameters)
)
// Adds a [Text] via local function.
.addContent(
totalStepsText(
goalProgress.consumption.toString(),
deviceParameters
)
)
// TODO: Add Spacer and Image representations of our step graphic.
// Adds a [Spacer].
.addContent(Spacer.Builder().setHeight(VERTICAL_SPACING_HEIGHT).build())
// Adds an [Image] via local function.
.addContent(startRunButton())
.build()
)
.build()
// TODO: Create a function that constructs an Arc representation of the current step progress.
// Creates an [Arc] representing current progress towards steps goal.
private fun progressArc(percentage: Float) = Arc.Builder()
.addContent(
ArcLine.Builder()
// Uses degrees() helper to build an [AngularDimension] which represents progress.
.setLength(degrees(percentage * ARC_TOTAL_DEGREES))
.setColor(argb(ContextCompat.getColor(this, R.color.primary)))
.setThickness(PROGRESS_BAR_THICKNESS)
.build()
)
// Element will start at 12 o'clock or 0 degree position in the circle.
.setAnchorAngle(degrees(0.0f))
// Aligns the contents of this container relative to anchor angle above.
// ARC_ANCHOR_START - Anchors at the start of the elements. This will cause elements
// added to an arc to begin at the given anchor_angle, and sweep around to the right.
.setAnchorType(ARC_ANCHOR_START)
.build()
// TODO: Create functions that construct/stylize Text representations of the step count & goal.
// Creates a [Text] with current step count and stylizes it.
private fun currentStepsText(current: String, deviceParameters: DeviceParameters) =
Text.Builder()
.setText(current)
.setFontStyle(FontStyles.display2(deviceParameters).build())
.build()
// Creates a [Text] with total step count goal and stylizes it.
private fun totalStepsText(goal: String, deviceParameters: DeviceParameters) = Text.Builder()
.setText(goal)
.setFontStyle(FontStyles.title3(deviceParameters).build())
.build()
// TODO: Create a function that constructs/stylizes a clickable Image of a running icon.
// Creates a running icon [Image] that's also a button to refresh the tile.
private fun startRunButton() =
Image.Builder()
.setWidth(BUTTON_SIZE)
.setHeight(BUTTON_SIZE)
.setResourceId(ID_IMAGE_START_RUN)
.setModifiers(
Modifiers.Builder()
.setPadding(
Padding.Builder()
.setStart(BUTTON_PADDING)
.setEnd(BUTTON_PADDING)
.setTop(BUTTON_PADDING)
.setBottom(BUTTON_PADDING)
.build()
)
.setBackground(
Background.Builder()
.setCorner(Corner.Builder().setRadius(BUTTON_RADIUS).build())
.setColor(argb(ContextCompat.getColor(this, R.color.primaryDark)))
.build()
)
// TODO: Add click (START)
.setClickable(
Clickable.Builder()
.setId(ID_CLICK_START_RUN)
.setOnClick(ActionBuilders.LoadAction.Builder().build())
.build()
)
// TODO: Add click (END)
.build()
)
.build()
}

View File

@@ -0,0 +1,199 @@
package com.chriskaczor.homemonitor.wear
import androidx.core.content.ContextCompat
import androidx.wear.tiles.ActionBuilders
import androidx.wear.tiles.ColorBuilders.argb
import androidx.wear.tiles.DeviceParametersBuilders.DeviceParameters
import androidx.wear.tiles.DimensionBuilders.dp
import androidx.wear.tiles.DimensionBuilders.expand
import androidx.wear.tiles.LayoutElementBuilders.*
import androidx.wear.tiles.ModifiersBuilders.*
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
import androidx.wear.tiles.RequestBuilders.TileRequest
import androidx.wear.tiles.ResourceBuilders.*
import androidx.wear.tiles.TileBuilders.Tile
import androidx.wear.tiles.TileService
import androidx.wear.tiles.TimelineBuilders.Timeline
import androidx.wear.tiles.TimelineBuilders.TimelineEntry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.guava.future
private const val RESOURCES_VERSION = "1"
// dimensions
private val BUTTON_SIZE = dp(48f)
private val BUTTON_RADIUS = dp(24f)
private val BUTTON_PADDING = dp(12f)
private val VERTICAL_SPACING_HEIGHT = dp(8f)
// identifiers
private const val ID_IMAGE_REFRESH = "image_refresh"
private const val ID_IMAGE_GENERATION = "image_generation"
private const val ID_IMAGE_CONSUMPTION = "image_consumption"
private const val ID_CLICK_REFRESH = "click_refresh"
class PowerTileService : TileService() {
private val serviceScope = CoroutineScope(Dispatchers.IO)
override fun onTileRequest(requestParams: TileRequest) = serviceScope.future {
val powerStatus = PowerRepository.getPowerStatus()
val deviceParams = requestParams.deviceParameters!!
Tile.Builder()
.setResourcesVersion(RESOURCES_VERSION)
.setTimeline(
Timeline.Builder()
.addTimelineEntry(
TimelineEntry.Builder()
.setLayout(
Layout.Builder()
.setRoot(
layout(powerStatus, deviceParams)
).build()
).build()
).build()
).build()
}
override fun onResourcesRequest(requestParams: ResourcesRequest) = serviceScope.future {
Resources.Builder()
.setVersion(RESOURCES_VERSION)
.addIdToImageMapping(
ID_IMAGE_REFRESH,
ImageResource.Builder()
.setAndroidResourceByResId(
AndroidImageResourceByResId.Builder()
.setResourceId(R.drawable.ic_refresh)
.build()
).build()
)
.addIdToImageMapping(
ID_IMAGE_GENERATION,
ImageResource.Builder()
.setAndroidResourceByResId(
AndroidImageResourceByResId.Builder()
.setResourceId(R.drawable.ic_sun)
.build()
).build()
)
.addIdToImageMapping(
ID_IMAGE_CONSUMPTION,
ImageResource.Builder()
.setAndroidResourceByResId(
AndroidImageResourceByResId.Builder()
.setResourceId(R.drawable.ic_plug)
.build()
).build()
).build()
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
private fun layout(goalProgress: PowerStatus, deviceParameters: DeviceParameters) =
Box.Builder()
.setWidth(expand())
.setHeight(expand())
.addContent(
Column.Builder()
.addContent(
generationLayout(goalProgress.generation, deviceParameters)
)
.addContent(
consumptionLayout(goalProgress.consumption, deviceParameters)
)
.addContent(Spacer.Builder().setHeight(VERTICAL_SPACING_HEIGHT).build())
.addContent(refreshButton())
.build()
).build()
private fun generationLayout(generation: Int, deviceParameters: DeviceParameters) =
Row.Builder()
.addContent(
Image.Builder()
.setHeight(dp(36f))
.setWidth(dp(36f))
.setModifiers(
Modifiers.Builder()
.setPadding(
Padding.Builder()
.setStart(dp(0f))
.setEnd(dp(10f))
.setTop(dp(1f))
.setBottom(dp(0f))
.build()
)
.build()
)
.setResourceId(ID_IMAGE_GENERATION)
.build()
)
.addContent(
Text.Builder()
.setText(generation.toString())
.setFontStyle(FontStyles.display3(deviceParameters).build())
.build()
).build()
private fun consumptionLayout(consumption: Int, deviceParameters: DeviceParameters) =
Row.Builder()
.addContent(
Image.Builder()
.setHeight(dp(36f))
.setWidth(dp(36f))
.setModifiers(
Modifiers.Builder()
.setPadding(
Padding.Builder()
.setStart(dp(0f))
.setEnd(dp(10f))
.setTop(dp(1f))
.setBottom(dp(0f))
.build()
)
.build()
)
.setResourceId(ID_IMAGE_CONSUMPTION)
.build()
)
.addContent(
Text.Builder()
.setText(consumption.toString())
.setFontStyle(FontStyles.display3(deviceParameters).build())
.build()
).build()
private fun refreshButton() =
Image.Builder()
.setWidth(BUTTON_SIZE)
.setHeight(BUTTON_SIZE)
.setResourceId(ID_IMAGE_REFRESH)
.setModifiers(
Modifiers.Builder()
.setPadding(
Padding.Builder()
.setStart(BUTTON_PADDING)
.setEnd(BUTTON_PADDING)
.setTop(BUTTON_PADDING)
.setBottom(BUTTON_PADDING)
.build()
)
.setBackground(
Background.Builder()
.setCorner(Corner.Builder().setRadius(BUTTON_RADIUS).build())
.setColor(argb(ContextCompat.getColor(this, R.color.primaryDark)))
.build()
)
.setClickable(
Clickable.Builder()
.setId(ID_CLICK_REFRESH)
.setOnClick(ActionBuilders.LoadAction.Builder().build())
.build()
).build()
).build()
}

View File

@@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M21,14c0,-0.55 -0.45,-1 -1,-1h-2v2h2C20.55,15 21,14.55 21,14z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M20,17h-2v2h2c0.55,0 1,-0.45 1,-1C21,17.45 20.55,17 20,17z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M12,14h-2v4h2c0,1.1 0.9,2 2,2h3v-8h-3C12.9,12 12,12.9 12,14z"/>
<path
android:fillColor="@android:color/white"
android:pathData="M5,13c0,-1.1 0.9,-2 2,-2h1.5c1.93,0 3.5,-1.57 3.5,-3.5S10.43,4 8.5,4H5C4.45,4 4,4.45 4,5c0,0.55 0.45,1 1,1h3.5C9.33,6 10,6.67 10,7.5S9.33,9 8.5,9H7c-2.21,0 -4,1.79 -4,4c0,2.21 1.79,4 4,4h2v-2H7C5.9,15 5,14.1 5,13z"/>
</vector>

View File

@@ -0,0 +1,21 @@
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.0dp"
android:height="24.0dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#C58AF9"
android:pathData="M17.65,6.35c-1.63,-1.63 -3.94,-2.57 -6.48,-2.31c-3.67,0.37 -6.69,3.35 -7.1,7.02C3.52,15.91 7.27,20 12,20c3.19,0 5.93,-1.87 7.21,-4.57c0.31,-0.66 -0.16,-1.43 -0.89,-1.43h-0.01c-0.37,0 -0.72,0.2 -0.88,0.53c-1.13,2.43 -3.84,3.97 -6.81,3.32c-2.22,-0.49 -4.01,-2.3 -4.49,-4.52C5.31,9.44 8.26,6 12,6c1.66,0 3.14,0.69 4.22,1.78l-2.37,2.37C13.54,10.46 13.76,11 14.21,11H19c0.55,0 1,-0.45 1,-1V5.21c0,-0.45 -0.54,-0.67 -0.85,-0.35L17.65,6.35z"/>
</vector>

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="@color/primary"
android:pathData="M13.49,5.48c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM9.89,19.38l1,-4.4 2.1,2v6h2v-7.5l-2.1,-2 0.6,-3c1.3,1.5 3.3,2.5 5.5,2.5v-2c-1.9,0 -3.5,-1 -4.3,-2.4l-1,-1.6c-0.4,-0.6 -1,-1 -1.7,-1 -0.3,0 -0.5,0.1 -0.8,0.1l-5.2,2.2v4.7h2v-3.4l1.8,-0.7 -1.6,8.1 -4.9,-1 -0.4,2 7,1.4z" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5s5,-2.24 5,-5S14.76,7 12,7L12,7zM2,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S1.45,13 2,13zM20,13l2,0c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1l-2,0c-0.55,0 -1,0.45 -1,1S19.45,13 20,13zM11,2v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1V2c0,-0.55 -0.45,-1 -1,-1S11,1.45 11,2zM11,20v2c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2c0,-0.55 -0.45,-1 -1,-1C11.45,19 11,19.45 11,20zM5.99,4.58c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0s0.39,-1.03 0,-1.41L5.99,4.58zM18.36,16.95c-0.39,-0.39 -1.03,-0.39 -1.41,0c-0.39,0.39 -0.39,1.03 0,1.41l1.06,1.06c0.39,0.39 1.03,0.39 1.41,0c0.39,-0.39 0.39,-1.03 0,-1.41L18.36,16.95zM19.42,5.99c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L19.42,5.99zM7.05,18.36c0.39,-0.39 0.39,-1.03 0,-1.41c-0.39,-0.39 -1.03,-0.39 -1.41,0l-1.06,1.06c-0.39,0.39 -0.39,1.03 0,1.41s1.03,0.39 1.41,0L7.05,18.36z"/>
</vector>

View File

@@ -1,18 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2021 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#C58AF9</color>
<color name="primaryDark">#3B294B</color>

View File

@@ -3,9 +3,5 @@
<string name="app_name">Home Monitor</string>
<string name="tile_description">Home Monitor</string>
<string name="fitness_tile_label">Step progress</string>
<string name="goal">/ %d steps</string>
<string name="placeholder_text">Time to create a tile!</string>
<string name="power_tile_label">Power</string>
</resources>