Initial WIP on Wear OS tile app

This commit is contained in:
2021-09-07 07:23:54 -04:00
parent 017c972d90
commit b98a2afd66
24 changed files with 1060 additions and 0 deletions

1
WearOS/src/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

56
WearOS/src/build.gradle Normal file
View File

@@ -0,0 +1,56 @@
/*
* 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.
*/
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 31
defaultConfig {
applicationId "com.chriskaczor.homemonitor.wear"
minSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
kotlinOptions {
jvmTarget = "1.8"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
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'
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chriskaczor.homemonitor.wear">
<uses-feature android:name="android.hardware.type.watch" />
<application>
<activity
android:name=".TilePreviewActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,29 @@
package com.chriskaczor.homemonitor.wear
import android.content.ComponentName
import android.os.Bundle
import android.widget.FrameLayout
import androidx.activity.ComponentActivity
import androidx.wear.tiles.manager.TileUiClient
class TilePreviewActivity : ComponentActivity() {
lateinit var tileUiClient: TileUiClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rootLayout = findViewById<FrameLayout>(R.id.tile_container)
tileUiClient = TileUiClient(
context = this,
component = ComponentName(this, GoalsTileService::class.java),
parentView = rootLayout
)
tileUiClient.connect()
}
override fun onDestroy() {
super.onDestroy()
tileUiClient.close()
}
}

View File

@@ -0,0 +1,19 @@
<?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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/tile_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chriskaczor.homemonitor.wear">
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="false"
android:label="@string/app_name"
android:supportsRtl="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@android:style/Theme.DeviceDefault">
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />
<service
android:name="com.chriskaczor.homemonitor.wear.GoalsTileService"
android:description="@string/tile_description"
android:icon="@drawable/ic_run"
android:label="@string/fitness_tile_label"
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
<intent-filter>
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
</intent-filter>
<meta-data
android:name="androidx.wear.tiles.PREVIEW"
android:resource="@drawable/tile_goals" />
</service>
</application>
</manifest>

View File

@@ -0,0 +1,211 @@
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,32 @@
package com.chriskaczor.homemonitor.wear
import com.beust.klaxon.Json
import com.beust.klaxon.Klaxon
import java.net.URL
import java.sql.Timestamp
data class PowerStatus(
@Json(name = "generation")
val generation: Int,
@Json(name = "consumption")
val consumption: Int,
@Json(name = "timestamp")
val timestamp: String
)
object PowerRepository {
suspend fun getPowerStatus(): PowerStatus {
val json = URL("http://home.kaczorzoo.net/api/power/status/recent").readText();
val data = Klaxon().parse<PowerStatus>(json) ?: return powerStatus;
powerStatus = powerStatus.copy(generation = data.generation, consumption = data.consumption, timestamp = data.timestamp);
return powerStatus;
}
}
var powerStatus =
PowerStatus(generation = 0, consumption = 0, timestamp = Timestamp(System.currentTimeMillis()).toString())

View File

@@ -0,0 +1,24 @@
<?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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,19 @@
<?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.
-->
<resources>
<color name="primary">#C58AF9</color>
<color name="primaryDark">#3B294B</color>
</resources>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<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>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">home.kaczorzoo.net</domain>
</domain-config>
</network-security-config>