Implement input chooser activity

This commit is contained in:
Phillip Thelen 2022-07-11 12:48:01 +02:00
parent a90925f81a
commit 46c8b9efa1
16 changed files with 229 additions and 46 deletions

View file

@ -26,17 +26,18 @@ class DeviceCommunicationService : WearableListenerService() {
super.onMessageReceived(event)
when (event.path) {
DeviceCommunication.REQUEST_AUTH -> processAuthRequest(event)
DeviceCommunication.SHOW_REGISTER -> openActivity(LoginActivity::class.java)
DeviceCommunication.SHOW_LOGIN -> openActivity(LoginActivity::class.java)
DeviceCommunication.SHOW_RYA -> openActivity(MainActivity::class.java)
DeviceCommunication.SHOW_REGISTER -> openActivity(event, LoginActivity::class.java)
DeviceCommunication.SHOW_LOGIN -> openActivity(event, LoginActivity::class.java)
DeviceCommunication.SHOW_RYA -> openActivity(event, MainActivity::class.java)
DeviceCommunication.SHOW_TASK_EDIT -> openTaskForm(event)
}
}
private fun openActivity(activityClass: Class<*>) {
private fun openActivity(event: MessageEvent, activityClass: Class<*>) {
val intent = Intent(this, activityClass)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
messageClient.sendMessage(event.sourceNodeId, "/action_completed", null)
}
private fun openTaskForm(event: MessageEvent) {
@ -44,7 +45,8 @@ class DeviceCommunicationService : WearableListenerService() {
val startIntent = Intent(this, TaskFormActivity::class.java).apply {
putExtra(TaskFormActivity.TASK_ID_KEY, taskID)
}
startActivity(startIntent)
startActivity(startIntent)
messageClient.sendMessage(event.sourceNodeId, "/action_completed", null)
}
private fun processAuthRequest(event: MessageEvent) {

View file

@ -1,2 +1,2 @@
NAME=4.0
CODE=4190
CODE=4200

View file

@ -52,6 +52,7 @@
<activity android:name="com.habitrpg.wearos.habitica.ui.activities.LevelupActivity" />
<activity android:name="com.habitrpg.wearos.habitica.ui.activities.ConfirmationActivity" />
<activity android:name="com.habitrpg.wearos.habitica.ui.activities.InputActivity" />
</application>
</manifest>

View file

@ -58,7 +58,9 @@ class MainApplication : Application() {
}
private fun logLaunch() {
Firebase.analytics.logEvent("wear_launched", null)
if (!BuildConfig.DEBUG) {
Firebase.analytics.logEvent("wear_launched", null)
}
}
private fun setupFirebase() {

View file

@ -123,7 +123,7 @@ class ApiClient @Inject constructor(
val responseBuilder = response.newBuilder()
responseBuilder.header("was-cached", (response.networkResponse == null).toString())
if (request.method == "GET") {
if (response.code == 504) {
if (response.code == 504 || response.request.header("x-api-user") != hostConfig.userID) {
// Cache miss. Network might be down, but retry call without cache to be sure.
chain.proceed(request.newBuilder()
.header("Cache-Control", "no-cache")

View file

@ -87,9 +87,12 @@ abstract class BaseActivity<B: ViewBinding, VM: BaseViewModel> : ComponentActivi
}
}
internal fun openRemoteActivity(url: String) {
internal fun openRemoteActivity(url: String, keepActive: Boolean = false) {
sendMessage("open_activity", url, null)
startActivity(Intent(this, ContinuePhoneActivity::class.java))
startActivity(Intent(this, ContinuePhoneActivity::class.java)
.apply {
putExtra("keep_active", keepActive)
})
}
internal fun sendMessage(

View file

@ -20,7 +20,7 @@ import kotlin.time.toDuration
class ContinuePhoneActivity : BaseActivity<ActivityContinuePhoneBinding, ContinuePhoneViewModel>() {
override val viewModel: ContinuePhoneViewModel by viewModels()
private var secondsToShow = 2
private var secondsToShow = 5
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityContinuePhoneBinding.inflate(layoutInflater)
@ -29,16 +29,23 @@ class ContinuePhoneActivity : BaseActivity<ActivityContinuePhoneBinding, Continu
override fun onStart() {
super.onStart()
messageClient.addListener(viewModel)
binding.root.setOnClickListener {
finish()
}
lifecycleScope.launch {
delay(secondsToShow.toDuration(DurationUnit.SECONDS))
viewModel.onActionCompleted = {
finish()
}
if (!viewModel.keepActive) {
lifecycleScope.launch {
delay(secondsToShow.toDuration(DurationUnit.SECONDS))
finish()
}
}
val alphaAnimation = AlphaAnimation(0f, 1f)
val translateAnimation = TranslateAnimation((-20f).dpToPx(this), 0f, 0f, 0f)
val set = AnimationSet(true)
@ -51,4 +58,9 @@ class ContinuePhoneActivity : BaseActivity<ActivityContinuePhoneBinding, Continu
set.addAnimation(translateAnimation)
binding.iconView.startAnimation(set)
}
override fun onDestroy() {
messageClient.removeListener(viewModel)
super.onDestroy()
}
}

View file

@ -0,0 +1,74 @@
package com.habitrpg.wearos.habitica.ui.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.speech.RecognizerIntent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.view.postDelayed
import com.habitrpg.android.habitica.databinding.ActivityInputBinding
import com.habitrpg.wearos.habitica.ui.viewmodels.InputViewModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class InputActivity: BaseActivity<ActivityInputBinding, InputViewModel>() {
override val viewModel: InputViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
binding = ActivityInputBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
binding.titleView.text = viewModel.title
binding.speechInput.setOnClickListener {
showSpeechInput()
}
binding.keyboardInput.setOnClickListener {
showKeyboard()
}
binding.editText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_DONE) {
if (binding.editText.text?.isNotEmpty() == true) {
returnInput(binding.editText.text.toString())
}
}
false
}
}
private fun returnInput(inputString: String?) {
val data = Intent()
data.putExtra("input", inputString)
setResult(Activity.RESULT_OK, data)
finish()
}
private fun showKeyboard() {
binding.editText.hint = binding.titleView.text
binding.editText.requestFocus()
binding.editText.postDelayed(100) {
val imm: InputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.editText, InputMethodManager.SHOW_FORCED)
}
}
private val speechInputResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val spokenText: String? = it.data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.firstOrNull()
returnInput(spokenText)
}
}
private fun showSpeechInput() {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_PROMPT, binding.titleView.text)
}
speechInputResult.launch(intent)
}
}

View file

@ -87,7 +87,7 @@ class LoginActivity: BaseActivity<ActivityLoginBinding, LoginViewModel>() {
}
private fun openRegisterOnPhone() {
openRemoteActivity(DeviceCommunication.SHOW_REGISTER)
openRemoteActivity(DeviceCommunication.SHOW_REGISTER, true)
}
private fun openLoginOnPhone() {

View file

@ -30,8 +30,13 @@ class SplashActivity: BaseActivity<ActivitySplashBinding, SplashViewModel>() {
startLoginActivity()
}
}
}
override fun onStart() {
super.onStart()
messageClient.addListener(viewModel)
if (!viewModel.hasAuthentication) {
sendMessage("provide_auth", "/request/auth", null) {
if (it) {
showAccountLoader(true)
@ -40,6 +45,13 @@ class SplashActivity: BaseActivity<ActivitySplashBinding, SplashViewModel>() {
startLoginActivity()
}
}
}
}
override fun onStop() {
messageClient.removeListener(viewModel)
super.onStop()
}
private fun startMainActivity() {
@ -64,9 +76,4 @@ class SplashActivity: BaseActivity<ActivitySplashBinding, SplashViewModel>() {
binding.textView.isVisible = show
}
}
override fun onPause() {
messageClient.removeListener(viewModel)
super.onPause()
}
}

View file

@ -1,17 +1,14 @@
package com.habitrpg.wearos.habitica.ui.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.lifecycle.lifecycleScope
import com.habitrpg.android.habitica.R
import com.habitrpg.android.habitica.databinding.ActivityTaskFormBinding
@ -45,23 +42,16 @@ class TaskFormActivity : BaseActivity<ActivityTaskFormBinding, TaskFormViewModel
binding = ActivityTaskFormBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
binding.editText.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_DONE) {
if (binding.editText.text?.isNotEmpty() == true) {
binding.editTaskWrapper.isVisible = false
binding.taskConfirmationWrapper.isVisible = true
binding.confirmationText.text = binding.editText.text
binding.editText.clearFocus()
}
}
false
binding.editText.setOnClickListener {
it.clearFocus()
requestInput()
}
binding.editButton.setOnClickListener {
binding.editTaskWrapper.isVisible = true
binding.taskConfirmationWrapper.isVisible = false
if (intent.extras?.containsKey("task_type") == true) {
binding.editText.requestFocus()
showKeyboard()
requestInput()
}
}
binding.todoButton.setOnClickListener { taskType = TaskType.TODO }
@ -91,26 +81,28 @@ class TaskFormActivity : BaseActivity<ActivityTaskFormBinding, TaskFormViewModel
binding.taskTypeHeader.isVisible = false
binding.taskTypeWrapper.isVisible = false
binding.header.textView.text = getString(R.string.create_task, taskType?.value)
binding.editText.requestFocus()
requestInput()
} else {
taskType = TaskType.TODO
binding.header.textView.text = getString(R.string.new_task)
}
}
override fun onResume() {
super.onResume()
if (binding.editText.hasFocus()) {
showKeyboard()
private val inputResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
val input = it.data?.getStringExtra("input")
if (input?.isNotBlank() == true) {
binding.editTaskWrapper.isVisible = false
binding.taskConfirmationWrapper.isVisible = true
binding.confirmationText.text = input
binding.editText.setText(input)
}
}
private fun showKeyboard() {
binding.editText.postDelayed(100) {
val imm: InputMethodManager =
getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.editText, InputMethodManager.SHOW_FORCED)
private fun requestInput() {
val intent = Intent(this, InputActivity::class.java).apply {
putExtra("title", getString(R.string.task_title_hint))
}
inputResult.launch(intent)
}
private fun updateTaskTypeButton(button: TextView, thisType: TaskType) {

View file

@ -1,5 +1,8 @@
package com.habitrpg.wearos.habitica.ui.viewmodels
import androidx.lifecycle.SavedStateHandle
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.MessageEvent
import com.habitrpg.wearos.habitica.data.repositories.TaskRepository
import com.habitrpg.wearos.habitica.data.repositories.UserRepository
import com.habitrpg.wearos.habitica.managers.LoadingManager
@ -9,10 +12,18 @@ import javax.inject.Inject
@HiltViewModel
class ContinuePhoneViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
userRepository: UserRepository,
taskRepository: TaskRepository,
exceptionBuilder: ExceptionHandlerBuilder,
loadingManager: LoadingManager
) : BaseViewModel(userRepository, taskRepository, exceptionBuilder, loadingManager) {
) : BaseViewModel(userRepository, taskRepository, exceptionBuilder, loadingManager), MessageClient.OnMessageReceivedListener {
val keepActive = savedStateHandle.get<Boolean>("keep_active") ?: false
var onActionCompleted: (() -> Unit)? = null
override fun onMessageReceived(event: MessageEvent) {
when (event.path) {
"/action_completed" -> onActionCompleted?.invoke()
}
}
}

View file

@ -0,0 +1,20 @@
package com.habitrpg.wearos.habitica.ui.viewmodels
import androidx.lifecycle.SavedStateHandle
import com.habitrpg.wearos.habitica.data.repositories.TaskRepository
import com.habitrpg.wearos.habitica.data.repositories.UserRepository
import com.habitrpg.wearos.habitica.managers.LoadingManager
import com.habitrpg.wearos.habitica.util.ExceptionHandlerBuilder
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class InputViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
userRepository: UserRepository,
taskRepository: TaskRepository,
exceptionBuilder: ExceptionHandlerBuilder,
loadingManager: LoadingManager
) : BaseViewModel(userRepository, taskRepository, exceptionBuilder, loadingManager) {
val title = savedStateHandle.get<String>("title") ?: ""
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:gravity="center">
<TextView
android:id="@+id/title_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginBottom="@dimen/spacing_large"/>
<LinearLayout
android:id="@+id/picker_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/speech_input"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_microphone"
android:tint="@color/watch_black"
android:scaleType="center"
android:background="@drawable/circle"
android:backgroundTint="@color/watch_purple_100"
android:layout_marginEnd="16dp"/>
<ImageView
android:id="@+id/keyboard_input"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/ic_keyboard"
android:tint="@color/watch_black"
android:scaleType="center"
android:background="@drawable/circle"
android:backgroundTint="@color/watch_purple_100"/>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone">
<EditText
android:id="@+id/edit_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>

View file

@ -65,6 +65,7 @@
android:paddingHorizontal="18dp"
android:hint="@string/task_title_hint"
android:background="@drawable/row_background_outline"
android:focusable="false"
android:inputType="textCapSentences" />
<TextView
android:id="@+id/task_type_header"