Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions phone-app/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ indent_size = 4
max_line_length = 160
ij_kotlin_allow_trailing_comma = false
ij_kotlin_allow_trailing_comma_on_call_site = false
ktlint_function_naming_ignore_when_annotated_with = Composable
12 changes: 12 additions & 0 deletions phone-app/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.spotless)
}

Expand Down Expand Up @@ -97,6 +98,7 @@ android {
buildFeatures {
viewBinding = true
buildConfig = true
compose = true
}
}

Expand Down Expand Up @@ -124,6 +126,16 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(libs.okhttp)

// Jetpack Compose
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
debugImplementation(libs.androidx.compose.ui.tooling)
// Google Play Services & Maps Platform
implementation(libs.play.services.location)
implementation(libs.places)
Expand Down
8 changes: 8 additions & 0 deletions phone-app/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />


<!-- Package visibility queries for Huawei Wear Engine and Huawei Health on Android 11+ (API 30+) -->
<queries>
<package android:name="com.huawei.health" />
<intent>
<action android:name="com.huawei.health.action.WEAR_ENGINE_SERVICE" />
</intent>
</queries>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
Expand Down
10 changes: 8 additions & 2 deletions phone-app/app/src/main/java/com/watchnavigator/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ class MainActivity : AppCompatActivity() {
}

binding.btnConnectWatch.setOnClickListener {
viewModel.checkWatchConnection()
viewModel.connectOrRequestWatchPermission()
}

binding.btnNavigate.setOnClickListener {
Expand Down Expand Up @@ -346,7 +346,13 @@ class MainActivity : AppCompatActivity() {
binding.btnConnectWatch.text = getString(R.string.btn_connect_watch)
}
is WatchConnectionState.Unauthorized -> {
binding.tvWatchStatus.text = getString(R.string.watch_status_unauthorized)
val message = state.message
binding.tvWatchStatus.text =
if (message.isBlank()) {
getString(R.string.watch_status_unauthorized)
} else {
"${getString(R.string.watch_status_unauthorized)} ($message)"
}
binding.btnConnectWatch.isEnabled = true
binding.btnConnectWatch.text = getString(R.string.retry)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.watchnavigator.data
import android.content.Context
import com.huawei.hmf.tasks.Task
import com.huawei.wearengine.HiWear
import com.huawei.wearengine.auth.AuthCallback
import com.huawei.wearengine.auth.AuthClient
import com.huawei.wearengine.auth.Permission
import com.huawei.wearengine.common.WearEngineErrorCode
Expand Down Expand Up @@ -49,6 +50,8 @@ interface WearEngineService {

suspend fun checkPermissions(): Boolean

suspend fun requestPermission(): Boolean

suspend fun checkConnection(): WatchConnectionState

suspend fun sendNavMessage(message: WatchNavMessage): Result<Unit>
Expand Down Expand Up @@ -139,6 +142,76 @@ class HuaweiWearEngineService(
}
}

override suspend fun requestPermission(): Boolean =
withContext(ioDispatcher) {
try {
val granted =
suspendCancellableCoroutine<Boolean> { continuation ->
val authCallback =
object : AuthCallback {
override fun onOk(permissions: Array<out Permission>?) {
val hasDeviceManager =
permissions?.any {
it == Permission.DEVICE_MANAGER || it.name == Permission.DEVICE_MANAGER.name
} ?: false
if (continuation.isActive) {
continuation.resume(hasDeviceManager)
}
}

override fun onCancel() {
if (continuation.isActive) {
continuation.resume(false)
}
}
}

try {
val task = authClient.requestPermission(authCallback, Permission.DEVICE_MANAGER)
task.addOnFailureListener { exception ->
if (continuation.isActive) {
continuation.resumeWithException(exception)
}
}
task.addOnCanceledListener {
if (continuation.isActive) {
continuation.cancel()
}
}
} catch (e: Exception) {
if (continuation.isActive) {
continuation.resumeWithException(e)
}
}
}

if (granted) {
val state = checkConnection()
state is WatchConnectionState.Connected || state is WatchConnectionState.Disconnected
} else {
_connectionState.value =
WatchConnectionState.Unauthorized(
"Huawei Wear Engine permission request was cancelled. Please grant Device Manager permission to connect to your watch."
)
false
}
} catch (e: TimeoutCancellationException) {
_connectionState.value =
WatchConnectionState.Unauthorized(
"Permission request timed out: ${e.message}"
)
false
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_connectionState.value =
WatchConnectionState.Unauthorized(
"Failed to request Wear Engine permissions: ${e.message}"
)
false
}
}

override suspend fun checkConnection(): WatchConnectionState =
withContext(ioDispatcher) {
_connectionState.value = WatchConnectionState.Connecting
Expand Down
22 changes: 22 additions & 0 deletions phone-app/app/src/main/java/com/watchnavigator/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,28 @@ class MainViewModel(
}
}

fun requestWatchPermission() {
if (watchConnectionJob?.isActive == true) return
sessionManager.clearWatchSendError()
watchConnectionJob =
viewModelScope.launch {
wearEngineService?.requestPermission()
}
}

fun connectOrRequestWatchPermission() {
if (watchConnectionJob?.isActive == true) return
sessionManager.clearWatchSendError()
watchConnectionJob =
viewModelScope.launch {
if (watchConnectionState.value is WatchConnectionState.Unauthorized) {
wearEngineService?.requestPermission()
} else {
wearEngineService?.checkConnection()
}
}
}

fun onQueryChanged(query: String) {
_queryFlow.value = query
}
Expand Down
55 changes: 55 additions & 0 deletions phone-app/app/src/main/java/com/watchnavigator/ui/theme/Color.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.watchnavigator.ui.theme

import androidx.compose.ui.graphics.Color

// Material 3 Light Palette
val md_theme_light_primary = Color(0xFF006494)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFCBE6FF)
val md_theme_light_onPrimaryContainer = Color(0xFF001E30)
val md_theme_light_secondary = Color(0xFF50606E)
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
val md_theme_light_secondaryContainer = Color(0xFFD3E5F5)
val md_theme_light_onSecondaryContainer = Color(0xFF0C1D29)
val md_theme_light_tertiary = Color(0xFF00A86B)
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
val md_theme_light_tertiaryContainer = Color(0xFFA5F2C8)
val md_theme_light_onTertiaryContainer = Color(0xFF002111)
val md_theme_light_error = Color(0xFFBA1A1A)
val md_theme_light_onError = Color(0xFFFFFFFF)
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
val md_theme_light_onErrorContainer = Color(0xFF410002)
val md_theme_light_background = Color(0xFFFCFCFF)
val md_theme_light_onBackground = Color(0xFF191C1E)
val md_theme_light_surface = Color(0xFFFCFCFF)
val md_theme_light_onSurface = Color(0xFF191C1E)
val md_theme_light_surfaceVariant = Color(0xFFDEE3EA)
val md_theme_light_onSurfaceVariant = Color(0xFF41474D)
val md_theme_light_outline = Color(0xFF72787E)
val md_theme_light_outlineVariant = Color(0xFFC1C7CE)

// Material 3 Dark Palette
val md_theme_dark_primary = Color(0xFF8FCDFF)
val md_theme_dark_onPrimary = Color(0xFF003450)
val md_theme_dark_primaryContainer = Color(0xFF004B71)
val md_theme_dark_onPrimaryContainer = Color(0xFFCBE6FF)
val md_theme_dark_secondary = Color(0xFFB7C9D9)
val md_theme_dark_onSecondary = Color(0xFF22323F)
val md_theme_dark_secondaryContainer = Color(0xFF394956)
val md_theme_dark_onSecondaryContainer = Color(0xFFD3E5F5)
val md_theme_dark_tertiary = Color(0xFF00E676)
val md_theme_dark_onTertiary = Color(0xFF00381F)
val md_theme_dark_tertiaryContainer = Color(0xFF005230)
val md_theme_dark_onTertiaryContainer = Color(0xFFA5F2C8)
val md_theme_dark_error = Color(0xFFFFB4AB)
val md_theme_dark_onError = Color(0xFF690005)
val md_theme_dark_errorContainer = Color(0xFF93000A)
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
val md_theme_dark_background = Color(0xFF101417)
val md_theme_dark_onBackground = Color(0xFFE1E2E5)
val md_theme_dark_surface = Color(0xFF101417)
val md_theme_dark_onSurface = Color(0xFFE1E2E5)
val md_theme_dark_surfaceVariant = Color(0xFF41474D)
val md_theme_dark_onSurfaceVariant = Color(0xFFC1C7CE)
val md_theme_dark_outline = Color(0xFF8B9198)
val md_theme_dark_outlineVariant = Color(0xFF41474D)
112 changes: 112 additions & 0 deletions phone-app/app/src/main/java/com/watchnavigator/ui/theme/Theme.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.watchnavigator.ui.theme

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat

val LightColorScheme =
lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
onError = md_theme_light_onError,
errorContainer = md_theme_light_errorContainer,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
outlineVariant = md_theme_light_outlineVariant
)

val DarkColorScheme =
darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
onError = md_theme_dark_onError,
errorContainer = md_theme_dark_errorContainer,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
outlineVariant = md_theme_dark_outlineVariant
)

@Composable
@Suppress("ktlint:standard:function-naming")
fun WatchNavigatorTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme =
when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}

val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as? Activity)?.window
if (window != null) {
@Suppress("DEPRECATION")
window.statusBarColor = colorScheme.surface.toArgb()
@Suppress("DEPRECATION")
window.navigationBarColor = colorScheme.surface.toArgb()
val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = !darkTheme
insetsController.isAppearanceLightNavigationBars = !darkTheme
}
}
}

MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
Loading
Loading