Implement two-way communication between phone and wear apps
This commit is contained in:
parent
c1cf8d724f
commit
b1d33435cd
16 changed files with 507 additions and 42 deletions
123
.idea/codestyles/Project.xml
Normal file
123
.idea/codestyles/Project.xml
Normal 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>
|
5
.idea/codestyles/codeStyleConfig.xml
Normal file
5
.idea/codestyles/codeStyleConfig.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
53
.idea/runConfigurations/app.xml
Normal file
53
.idea/runConfigurations/app.xml
Normal 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>
|
53
.idea/runConfigurations/wear.xml
Normal file
53
.idea/runConfigurations/wear.xml
Normal 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>
|
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
wear/src/main/res/drawable/ic_pause_inset.xml
Normal file
5
wear/src/main/res/drawable/ic_pause_inset.xml
Normal 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" />
|
5
wear/src/main/res/drawable/ic_play_inset.xml
Normal file
5
wear/src/main/res/drawable/ic_play_inset.xml
Normal 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" />
|
|
@ -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>
|
4
wear/src/main/res/values/strings.xml
Normal file
4
wear/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="toggle_timer">Toggle timer</string>
|
||||
</resources>
|
Loading…
Reference in a new issue