diff --git a/WearOAuth/build.gradle b/WearOAuth/build.gradle index 62898d491..ee73fe247 100644 --- a/WearOAuth/build.gradle +++ b/WearOAuth/build.gradle @@ -16,9 +16,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext{ - compose_version = "1.5.14" - } dependencies { classpath libs.kotlin.gradle.plugin } @@ -27,6 +24,7 @@ buildscript { plugins { alias(libs.plugins.com.diffplug.spotless) apply(false) alias(libs.plugins.com.android.application) apply(false) + alias(libs.plugins.roborazzi) apply false } subprojects { diff --git a/WearOAuth/oauth-device-grant/build.gradle b/WearOAuth/oauth-device-grant/build.gradle index 531f0610a..d63c83fd0 100644 --- a/WearOAuth/oauth-device-grant/build.gradle +++ b/WearOAuth/oauth-device-grant/build.gradle @@ -50,7 +50,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion libs.versions.compose.compiler.get() } testOptions { unitTests { diff --git a/WearOAuth/oauth-pkce/build.gradle b/WearOAuth/oauth-pkce/build.gradle index f14a21517..58439252c 100644 --- a/WearOAuth/oauth-pkce/build.gradle +++ b/WearOAuth/oauth-pkce/build.gradle @@ -16,6 +16,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + alias libs.plugins.roborazzi } android { @@ -50,34 +51,61 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion libs.versions.compose.compiler.get() + } + + testOptions { + unitTests { + includeAndroidResources true + } } } dependencies { + def composeBom = platform(libs.androidx.compose.bom) + + // General compose dependencies + implementation composeBom implementation(libs.wear.compose.foundation) // For Wear Material Design UX guidelines and specifications implementation(libs.wear.compose.material) - implementation(libs.androidx.compose.material3) // For integration between Wear Compose and Androidx Navigation libraries implementation(libs.wear.compose.navigation) // For Wear preview annotations + implementation libs.compose.ui.tooling.preview implementation(libs.androidx.compose.ui.tooling) // Horologist dependencies implementation(libs.horologist.compose.layout) - // Standard android deps + // Standard android dependencies implementation projects.util - implementation libs.kotlin.stdlib - implementation libs.androidx.core.ktx - implementation libs.androidx.appcompat + implementation(libs.kotlin.stdlib) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.wear) implementation(libs.wear.phone.interactions) - implementation libs.playservices.wearable + implementation(libs.playservices.wearable) + + implementation libs.androidx.ui.test.manifest + + // Testing + testImplementation libs.androidx.ui.test.junit4 + testImplementation libs.junit + testImplementation libs.robolectric + testImplementation libs.roborazzi + testImplementation libs.roborazzi.compose + testImplementation libs.roborazzi.rule + testImplementation(libs.horologist.roboscreenshots) { + exclude(group: "com.github.QuickBirdEng.kotlin-snapshot-testing") + } + + debugImplementation libs.compose.ui.tooling + debugImplementation libs.androidx.ui.test.manifest + debugImplementation composeBom } diff --git a/WearOAuth/oauth-pkce/src/main/AndroidManifest.xml b/WearOAuth/oauth-pkce/src/main/AndroidManifest.xml index c048714f8..0a1c2502b 100644 --- a/WearOAuth/oauth-pkce/src/main/AndroidManifest.xml +++ b/WearOAuth/oauth-pkce/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ diff --git a/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEActivity.kt b/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEActivity.kt index e85431b97..2de3aaf99 100644 --- a/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEActivity.kt +++ b/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEActivity.kt @@ -18,18 +18,18 @@ package com.example.android.wearable.oauth.pkce import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.wear.compose.material3.Button -import androidx.wear.compose.material3.ListHeader -import androidx.wear.compose.material3.Text +import androidx.wear.compose.material.Chip +import androidx.wear.compose.material.ListHeader +import androidx.wear.compose.material.Text +import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices +import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.AppScaffold import com.google.android.horologist.compose.layout.ScalingLazyColumn @@ -55,42 +55,76 @@ class AuthPKCEActivity : ComponentActivity() { } } -@OptIn(ExperimentalHorologistApi::class) @Composable fun PKCEApp(pkceViewModel: AuthPKCEViewModel) { AppScaffold { val uiState = pkceViewModel.uiState.collectAsState() - val localContext = LocalContext.current - val columnState = rememberResponsiveColumnState( - contentPadding = ScalingLazyColumnDefaults.padding( - first = ScalingLazyColumnDefaults.ItemType.Text, - last = ScalingLazyColumnDefaults.ItemType.Text - ) + AuthenticateScreen( + uiState.value.statusCode, + uiState.value.resultMessage, + pkceViewModel::startAuthFlow ) - ScreenScaffold(scrollState = columnState) { - ScalingLazyColumn(columnState = columnState) { - item { - ListHeader { - Text( - stringResource(R.string.oauth_pkce), - textAlign = TextAlign.Center - ) - } + } +} + +@OptIn(ExperimentalHorologistApi::class) +@Composable +fun AuthenticateScreen( + statusCode: Int, + resultMessage: String, + startAuthFlow: () -> Unit +) { + val columnState = rememberResponsiveColumnState( + contentPadding = ScalingLazyColumnDefaults.padding( + first = ScalingLazyColumnDefaults.ItemType.Text, + last = ScalingLazyColumnDefaults.ItemType.Text + ) + ) + ScreenScaffold(scrollState = columnState) { + ScalingLazyColumn(columnState = columnState) { + item { + ListHeader { + Text( + stringResource(R.string.oauth_pkce), + textAlign = TextAlign.Center + ) } - item { - Button( - onClick = { pkceViewModel.startAuthFlow(localContext) }, - modifier = Modifier.fillMaxSize() - ) { + } + item { + Chip( + onClick = { startAuthFlow() }, + label = { Text( text = stringResource(R.string.authenticate), modifier = Modifier.align(Alignment.CenterVertically) ) } - } - item { Text(stringResource(id = uiState.value.statusCode)) } - item { Text(uiState.value.resultMessage) } + ) } + item { Text(stringResource(id = statusCode)) } + item { Text(resultMessage) } } } } + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun AuthenticateScreenPreview() { + AuthenticateScreen( + statusCode = R.string.status_retrieved, + resultMessage = "Bobby Bonson", + startAuthFlow = {} + ) +} + +@WearPreviewDevices +@WearPreviewFontScales +@Composable +fun AuthenticateScreenFailedPreview() { + AuthenticateScreen( + statusCode = R.string.status_failed, + resultMessage = "", + startAuthFlow = {} + ) +} diff --git a/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEViewModel.kt b/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEViewModel.kt index ee3862900..9a239eae5 100644 --- a/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEViewModel.kt +++ b/WearOAuth/oauth-pkce/src/main/java/com/example/android/wearable/oauth/pkce/AuthPKCEViewModel.kt @@ -15,10 +15,11 @@ */ package com.example.android.wearable.oauth.pkce +import android.app.Application import android.content.Context import android.net.Uri import android.util.Log -import androidx.lifecycle.ViewModel +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import androidx.wear.phone.interactions.authentication.CodeChallenge import androidx.wear.phone.interactions.authentication.CodeVerifier @@ -55,7 +56,8 @@ data class ProofKeyCodeExchangeState( * different steps of the flow. It first retrieves the OAuth code, uses it to exchange it for an * access token, and uses the token to retrieve the user's name. */ -class AuthPKCEViewModel : ViewModel() { +class AuthPKCEViewModel(application: Application) : AndroidViewModel(application) { + private val context = getApplication().applicationContext private val _uiState = MutableStateFlow(ProofKeyCodeExchangeState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -74,7 +76,7 @@ class AuthPKCEViewModel : ViewModel() { * the phone. After the user consents on their phone, the wearable app is notified and can * continue the authorization process. */ - fun startAuthFlow(context: Context) { + fun startAuthFlow() { viewModelScope.launch { val codeVerifier = CodeVerifier() diff --git a/WearOAuth/oauth-pkce/src/main/res/values/strings.xml b/WearOAuth/oauth-pkce/src/main/res/values/strings.xml index 886f0fad2..1cb1c2e96 100644 --- a/WearOAuth/oauth-pkce/src/main/res/values/strings.xml +++ b/WearOAuth/oauth-pkce/src/main/res/values/strings.xml @@ -17,12 +17,12 @@ Authenticate OAuth PKCE - Starting authorization… Switch to your phone to authenticate. - Authorization failed + Starting authentication… Switch to your phone to authenticate. + Authentication failed Retrieving token… Could not retrieve token Retrieving user profile… Could not retrieve user profile - User profile retrieved. Name: + User profile retrieved.\nName: Authenticate diff --git a/WearOAuth/oauth-pkce/src/test/java/com/example/android/wearable/oauth/pkce/AuthenticateScreenTest.kt b/WearOAuth/oauth-pkce/src/test/java/com/example/android/wearable/oauth/pkce/AuthenticateScreenTest.kt new file mode 100644 index 000000000..39c8a30ed --- /dev/null +++ b/WearOAuth/oauth-pkce/src/test/java/com/example/android/wearable/oauth/pkce/AuthenticateScreenTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2024 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. + */ +package com.example.android.wearable.oauth.pkce + +import com.google.android.horologist.compose.layout.AppScaffold +import com.google.android.horologist.compose.layout.ResponsiveTimeText +import com.google.android.horologist.screenshots.FixedTimeSource +import com.google.android.horologist.screenshots.rng.WearDevice +import com.google.android.horologist.screenshots.rng.WearScreenshotTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner + +@RunWith(ParameterizedRobolectricTestRunner::class) +class AuthenticateScreenTest(override val device: WearDevice) : WearScreenshotTest() { + override val tolerance = 0.02f + + // code to make sure multiple tests are run + override fun testName(suffix: String): String = + "src/test/snapshots/images/" + + "${this.javaClass.`package`?.name}_${this.javaClass.simpleName}_" + + "${testInfo.methodName}$suffix.png" + + @Test + fun authenticateScreenTest() = runTest { + AppScaffold( + timeText = { ResponsiveTimeText(timeSource = FixedTimeSource) } + ) { + AuthenticateScreenPreview() + } + } + + @Test + fun authenticateFailedScreenTest() = runTest { + AppScaffold( + timeText = { ResponsiveTimeText(timeSource = FixedTimeSource) } + ) { + AuthenticateScreenFailedPreview() + } + } + + companion object { + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters + fun devices() = WearDevice.entries + } +} diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[0].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[0].png new file mode 100644 index 000000000..18e2b1b70 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[0].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[1].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[1].png new file mode 100644 index 000000000..2d9639704 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[1].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[2].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[2].png new file mode 100644 index 000000000..2b88dbcbb Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[2].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[3].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[3].png new file mode 100644 index 000000000..3c0eaeb47 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[3].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[4].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[4].png new file mode 100644 index 000000000..3c0eaeb47 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[4].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[5].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[5].png new file mode 100644 index 000000000..93d012b80 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[5].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[6].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[6].png new file mode 100644 index 000000000..8f77f022f Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[6].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[7].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[7].png new file mode 100644 index 000000000..eadfe3007 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateFailedScreenTest[7].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[0].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[0].png new file mode 100644 index 000000000..60fefad56 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[0].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[1].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[1].png new file mode 100644 index 000000000..e99c5b259 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[1].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[2].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[2].png new file mode 100644 index 000000000..b086016fb Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[2].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[3].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[3].png new file mode 100644 index 000000000..82d849911 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[3].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[4].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[4].png new file mode 100644 index 000000000..82d849911 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[4].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[5].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[5].png new file mode 100644 index 000000000..fff8d917c Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[5].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[6].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[6].png new file mode 100644 index 000000000..e3664945f Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[6].png differ diff --git a/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[7].png b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[7].png new file mode 100644 index 000000000..cbf707b11 Binary files /dev/null and b/WearOAuth/oauth-pkce/src/test/snapshots/images/com.example.android.wearable.oauth.pkce_AuthenticateScreenTest_authenticateScreenTest[7].png differ diff --git a/WearOAuth/util/src/main/AndroidManifest.xml b/WearOAuth/util/src/main/AndroidManifest.xml deleted file mode 100644 index 5b907f170..000000000 --- a/WearOAuth/util/src/main/AndroidManifest.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - \ No newline at end of file