Implement two-way communication between phone and wear apps

This commit is contained in:
William Brawner 2020-07-23 20:15:00 -07:00
parent c1cf8d724f
commit b1d33435cd
16 changed files with 507 additions and 42 deletions

View file

@ -0,0 +1,123 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="WRAP_ON_TYPING" value="1" />
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View file

@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="app" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="app" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View file

@ -0,0 +1,53 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="wear" type="AndroidRunConfigurationType" factoryName="Android App" activateToolWindowBeforeRun="false">
<module name="wear" />
<option name="DEPLOY" value="true" />
<option name="DEPLOY_APK_FROM_BUNDLE" value="false" />
<option name="DEPLOY_AS_INSTANT" value="false" />
<option name="ARTIFACT_NAME" value="" />
<option name="PM_INSTALL_OPTIONS" value="" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="MODE" value="default_activity" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Auto>
<Hybrid>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Hybrid>
<Java />
<Native>
<option name="USE_JAVA_AWARE_DEBUGGER" value="false" />
<option name="SHOW_STATIC_VARS" value="true" />
<option name="WORKING_DIR" value="" />
<option name="TARGET_LOGGING_CHANNELS" value="lldb process:gdb-remote packets" />
<option name="SHOW_OPTIMIZED_WARNING" value="true" />
</Native>
<Profilers>
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
</Profilers>
<option name="DEEP_LINK" value="" />
<option name="ACTIVITY_CLASS" value="" />
<method v="2">
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>

View file

@ -3,6 +3,18 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
def keystoreProperties = new Properties()
try {
def keystorePropertiesFile = rootProject.file("keystore.properties")
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} catch (FileNotFoundException ignored) {
logger.warn("Unable to load keystore properties. Using debug signing configuration instead")
keystoreProperties['keyAlias'] = "androiddebugkey"
keystoreProperties['keyPassword'] = "android"
keystoreProperties['storeFile'] = new File(System.getProperty("user.home"), ".android/debug.keystore").absolutePath
keystoreProperties['storePassword'] = "android"
}
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
@ -24,6 +36,21 @@ android {
"room.expandProjection": "true"]
}
}
signingConfig signingConfigs.debug
}
signingConfigs {
debug {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
@ -45,7 +72,7 @@ android {
dependencies {
implementation project(':shared')
wearApp project(':wear')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'

View file

@ -1,8 +1,10 @@
package com.wbrawner.trainterval.activetimer
import android.content.Context
import android.media.AudioManager
import android.media.SoundPool
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
@ -14,18 +16,21 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import com.google.android.gms.wearable.*
import com.wbrawner.trainterval.Logger
import com.wbrawner.trainterval.R
import com.wbrawner.trainterval.shared.IntervalTimerDao
import com.wbrawner.trainterval.shared.IntervalTimerState
import com.wbrawner.trainterval.shared.IntervalTimerState.Companion.TIMER_ACTIONS_TOGGLE
import com.wbrawner.trainterval.shared.IntervalTimerState.Companion.TIMER_STATE
import com.wbrawner.trainterval.shared.Phase
import com.wbrawner.trainterval.shared.toDataMap
import kotlinx.android.synthetic.main.fragment_active_timer.*
import kotlinx.coroutines.*
import org.koin.android.ext.android.inject
import org.koin.core.parameter.parametersOf
class ActiveTimerFragment : Fragment() {
class ActiveTimerFragment : Fragment(), MessageClient.OnMessageReceivedListener {
private var coroutineScope: CoroutineScope? = null
private val activeTimerViewModel: ActiveTimerViewModel by activityViewModels()
@ -34,6 +39,8 @@ class ActiveTimerFragment : Fragment() {
private var timerId: Long = 0
private lateinit var soundPool: SoundPool
private val soundIds = mutableListOf<Int>()
private lateinit var dataClient: DataClient
private lateinit var messageClient: MessageClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -64,6 +71,13 @@ class ActiveTimerFragment : Fragment() {
super.onOptionsItemSelected(item)
}
override fun onAttach(context: Context) {
super.onAttach(context)
dataClient = Wearable.getDataClient(context)
messageClient = Wearable.getMessageClient(context)
messageClient.addListener(this)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
@ -83,6 +97,12 @@ class ActiveTimerFragment : Fragment() {
is IntervalTimerState.TimerRunningState -> renderTimer(state)
is IntervalTimerState.ExitState -> findNavController().navigateUp()
}
val req = PutDataMapRequest.create(TIMER_STATE).run {
setUrgent()
dataMap.putAll(state.toDataMap())
asPutDataRequest()
}
dataClient.putDataItem(req).addOnSuccessListener { /* No op*/ }
})
activeTimerViewModel.init(logger, timerDao, timerId)
}
@ -110,10 +130,15 @@ class ActiveTimerFragment : Fragment() {
view?.findViewById<View>(it)?.visibility = View.VISIBLE
}
(activity as? AppCompatActivity)?.supportActionBar?.title = state.timerName
val backgroundColor = resources.getColor(state.timerBackground, context?.theme)
val backgroundColor = resources.getColor(state.phase.colorRes, context?.theme)
timerBackground.setBackgroundColor(backgroundColor)
playPauseButton.setImageDrawable(requireContext().getDrawable(state.playPauseIcon))
timerPhase.text = getString(state.phase)
playPauseButton.setImageDrawable(
requireContext().getDrawable(
if (state.isRunning) R.drawable.ic_pause
else R.drawable.ic_play_arrow
)
)
timerPhase.text = getString(state.phase.stringRes)
timeRemaining.text = state.timeRemaining
timerSets.text = state.currentSet.toString()
timerRounds.text = state.currentRound.toString()
@ -130,12 +155,23 @@ class ActiveTimerFragment : Fragment() {
soundPool.play(soundId, volume, volume, 1, 0, 1f)
}
override fun onMessageReceived(event: MessageEvent) {
Log.d("WearMessage", "Received event: ${event.path}")
if (event.path?.compareTo(TIMER_ACTIONS_TOGGLE) != 0) return
activeTimerViewModel.toggleTimer()
}
override fun onDestroyView() {
coroutineScope?.cancel()
coroutineScope = null
super.onDestroyView()
}
override fun onDestroy() {
messageClient.removeListener(this)
super.onDestroy()
}
companion object {
private const val EXTRA_TIMER_ID = "timerId"
}

View file

@ -3,11 +3,13 @@
buildscript {
ext.kotlin_version = '1.3.72'
ext.room_version = '2.2.5'
ext.wearable_play_services = '17.0.0'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

View file

@ -37,6 +37,7 @@ dependencies {
implementation "androidx.room:room-ktx:$room_version"
testImplementation "androidx.room:room-testing:$room_version"
api "androidx.room:room-runtime:$room_version"
api "com.google.android.gms:play-services-wearable:$wearable_play_services"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View file

@ -1,13 +1,12 @@
package com.wbrawner.trainterval.shared
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.google.android.gms.wearable.DataMap
import java.io.Serializable
/**
* Used to represent the state while a user has a specific timer open.
*/
sealed class IntervalTimerState {
sealed class IntervalTimerState : Serializable {
object LoadingState : IntervalTimerState()
class TimerRunningState(
val timerName: String,
@ -15,9 +14,8 @@ sealed class IntervalTimerState {
val currentSet: Int,
val currentRound: Int,
val soundId: Int?,
@StringRes val phase: Int,
@ColorRes val timerBackground: Int,
@DrawableRes val playPauseIcon: Int
val phase: Phase,
val isRunning: Boolean
) : IntervalTimerState() {
constructor(
timer: IntervalTimer,
@ -28,17 +26,76 @@ sealed class IntervalTimerState {
timerRunning: Boolean
) : this(
timerName = timer.name,
phase = phase.stringRes,
phase = phase,
timeRemaining = timeRemaining.toIntervalDuration().toString(),
currentSet = currentSet,
currentRound = currentRound,
timerBackground = phase.colorRes,
soundId = if (timerRunning && timeRemaining == timer.durationForPhase(phase))
phase.ordinal
else null,
playPauseIcon = if (timerRunning) R.drawable.ic_pause else R.drawable.ic_play_arrow
isRunning = timerRunning
)
}
object ExitState : IntervalTimerState()
companion object {
const val TIMER_STATE = "/timer/state"
const val TIMER_ACTIONS_TOGGLE = "/timer/actions/toggle"
}
}
const val KEY_STATE = "com.wbrawner.trainterval.timerState"
const val STATE_LOADING = "com.wbrawner.trainterval.timerLoading"
const val STATE_ACTIVE = "com.wbrawner.trainterval.timerActive"
const val STATE_EXIT = "com.wbrawner.trainterval.timerExit"
const val KEY_TIMER_NAME = "com.wbrawner.trainterval.timerName"
const val KEY_TIME_REMAINING = "com.wbrawner.trainterval.timeRemaining"
const val KEY_CURRENT_SET = "com.wbrawner.trainterval.currentSet"
const val KEY_CURRENT_ROUND = "com.wbrawner.trainterval.currentRound"
const val KEY_SOUND_ID = "com.wbrawner.trainterval.soundId"
const val KEY_PHASE = "com.wbrawner.trainterval.phase"
const val KEY_RUNNING = "com.wbrawner.trainterval.timerRunning"
fun IntervalTimerState.toDataMap(): DataMap {
val dataMap = DataMap()
when (this) {
is IntervalTimerState.LoadingState -> dataMap.putString(KEY_STATE, STATE_LOADING)
is IntervalTimerState.TimerRunningState -> {
dataMap.putAll(this.toDataMap())
dataMap.putString(KEY_STATE, STATE_ACTIVE)
}
is IntervalTimerState.ExitState -> dataMap.putString(KEY_STATE, STATE_EXIT)
}
return dataMap
}
fun DataMap.toIntervalTimerState(): IntervalTimerState? = when (getString(KEY_STATE)) {
STATE_LOADING -> IntervalTimerState.LoadingState
STATE_EXIT -> IntervalTimerState.ExitState
STATE_ACTIVE -> IntervalTimerState.TimerRunningState(
getString(KEY_TIMER_NAME),
getString(KEY_TIME_REMAINING),
getInt(KEY_CURRENT_SET),
getInt(KEY_CURRENT_ROUND),
getInt(KEY_SOUND_ID),
Phase.valueOf(getString(KEY_PHASE)),
getBoolean(KEY_RUNNING)
)
else -> null
}
fun IntervalTimerState.TimerRunningState.toDataMap(): DataMap {
val state = this
return DataMap().apply {
putString(KEY_TIMER_NAME, state.timerName)
putString(KEY_TIME_REMAINING, state.timeRemaining)
putInt(KEY_CURRENT_SET, state.currentSet)
putInt(KEY_CURRENT_ROUND, state.currentRound)
state.soundId?.let {
putInt(KEY_SOUND_ID, it)
}
putString(KEY_PHASE, state.phase.name)
putBoolean(KEY_RUNNING, state.isRunning)
}
}

View file

@ -2,18 +2,45 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def keystoreProperties = new Properties()
try {
def keystorePropertiesFile = rootProject.file("keystore.properties")
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
} catch (FileNotFoundException ignored) {
logger.warn("Unable to load keystore properties. Using debug signing configuration instead")
keystoreProperties['keyAlias'] = "androiddebugkey"
keystoreProperties['keyPassword'] = "android"
keystoreProperties['storeFile'] = new File(System.getProperty("user.home"), ".android/debug.keystore").absolutePath
keystoreProperties['storePassword'] = "android"
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.1"
defaultConfig {
applicationId "com.wbrawner.trainterval.wear"
applicationId "com.wbrawner.trainterval"
minSdkVersion 28
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
signingConfig signingConfigs.debug
}
signingConfigs {
debug {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
@ -29,7 +56,6 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.wear:wear:1.0.0'
implementation 'com.google.android.support:wearable:2.7.0'
implementation 'com.google.android.gms:play-services-wearable:17.0.0'
compileOnly 'com.google.android.wearable:wearable:2.7.0'
testImplementation 'junit:junit:4.12'
}

View file

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wbrawner.trainterval.wear">
package="com.wbrawner.trainterval">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-feature android:name="android.hardware.type.watch" />
<application
@ -17,20 +16,15 @@
android:name="com.google.android.wearable"
android:required="true" />
<!--
Set to true if your app is Standalone, that is, it does not require the handheld
app to run.
-->
<meta-data
android:name="com.google.android.wearable.standalone"
android:value="true" />
android:value="false" />
<activity
android:name=".MainActivity"
android:name=".wear.MainActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

View file

@ -2,26 +2,78 @@ package com.wbrawner.trainterval.wear
import android.os.Bundle
import android.support.wearable.activity.WearableActivity
import com.google.android.gms.wearable.DataClient
import com.google.android.gms.wearable.DataEventBuffer
import com.google.android.gms.wearable.Wearable
import android.util.Log
import android.view.View
import com.google.android.gms.wearable.*
import com.wbrawner.trainterval.R
import com.wbrawner.trainterval.shared.IntervalTimerState
import com.wbrawner.trainterval.shared.IntervalTimerState.Companion.TIMER_ACTIONS_TOGGLE
import com.wbrawner.trainterval.shared.IntervalTimerState.Companion.TIMER_STATE
import com.wbrawner.trainterval.shared.toIntervalTimerState
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : WearableActivity(), DataClient.OnDataChangedListener {
private lateinit var dataClient: DataClient
private lateinit var messageClient: MessageClient
private lateinit var nodeClient: NodeClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView.text = "Hello, Wear OS!"
dataClient = Wearable.getDataClient(this)
messageClient = Wearable.getMessageClient(this)
nodeClient = Wearable.getNodeClient(this)
}
override fun onResume() {
super.onResume()
Wearable.getDataClient(this).addListener(this)
dataClient.addListener(this)
}
override fun onPause() {
super.onPause()
dataClient.removeListener(this)
}
override fun onDataChanged(dataEvents: DataEventBuffer) {
dataEvents.forEach { event ->
event.type
dataEvents.forEach {
if (it.dataItem.uri.path?.compareTo(TIMER_STATE) != 0) return@forEach
val intervalTimerState = DataMapItem.fromDataItem(it.dataItem)
.dataMap
.toIntervalTimerState()
?: return@forEach
when (intervalTimerState) {
is IntervalTimerState.LoadingState -> timeRemaining.text = "Loading"
is IntervalTimerState.TimerRunningState -> {
val backgroundColor =
resources.getColor(intervalTimerState.phase.colorRes, theme)
timerRoot.setBackgroundColor(backgroundColor)
timeRemaining.text = intervalTimerState.timeRemaining
toggleButton.setImageDrawable(
getDrawable(
if (intervalTimerState.isRunning) R.drawable.ic_pause_inset
else R.drawable.ic_play_inset
)
)
}
is IntervalTimerState.ExitState -> timeRemaining.text = "Exit"
}
}
}
fun toggleTimer(@Suppress("UNUSED_PARAMETER") view: View) {
nodeClient.connectedNodes.addOnSuccessListener { nodes ->
nodes.map { it.id }
.forEach { nodeId ->
messageClient.sendMessage(nodeId, TIMER_ACTIONS_TOGGLE, null)
.addOnSuccessListener {
Log.d("Wearable", "Sent message to $nodeId")
}
.addOnFailureListener {
Log.d("Wearable", "Failed to send message to $nodeId")
}
}
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_pause"
android:insetTop="5dp"
android:insetBottom="5dp" />

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/ic_play_arrow"
android:insetTop="5dp"
android:insetBottom="5dp" />

View file

@ -1,12 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.wearable.view.WearableFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/timerRoot"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@color/colorSurface">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
android:id="@+id/timeRemaining"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="center"
android:autoSizeTextType="uniform"
android:fontFamily="monospace"
android:gravity="center"
android:padding="16dp"
android:textAlignment="center"
android:textColor="@color/colorOnSurface"
tools:text="00:00:00" />
</android.support.wearable.view.WearableFrameLayout>
<ImageButton
android:id="@+id/toggleButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:backgroundTint="#40000000"
android:contentDescription="@string/toggle_timer"
android:fitsSystemWindows="true"
android:foregroundGravity="center"
android:onClick="toggleTimer"
android:src="@drawable/ic_play_arrow" />
</FrameLayout>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="toggle_timer">Toggle timer</string>
</resources>