From 9f1c1567b2ca41b640976bbf86c91eb0b4a9d499 Mon Sep 17 00:00:00 2001 From: John Zoeller Date: Thu, 30 May 2024 15:38:12 -0400 Subject: [PATCH 1/2] Migrate OAuth PKCE sample to compose" --- WearOAuth/oauth-pkce/build.gradle | 89 +++++++++++-------- .../wearable/oauth/pkce/AuthPKCEActivity.kt | 76 ++++++++++++---- .../wearable/oauth/pkce/AuthPKCEViewModel.kt | 46 ++++++---- .../src/main/res/layout/activity_auth.xml | 52 ----------- .../src/main/res/values/strings.xml | 3 +- 5 files changed, 140 insertions(+), 126 deletions(-) delete mode 100644 WearOAuth/oauth-pkce/src/main/res/layout/activity_auth.xml diff --git a/WearOAuth/oauth-pkce/build.gradle b/WearOAuth/oauth-pkce/build.gradle index 98f909cfb..86fe5c84a 100644 --- a/WearOAuth/oauth-pkce/build.gradle +++ b/WearOAuth/oauth-pkce/build.gradle @@ -14,51 +14,68 @@ * limitations under the License. */ plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' + id 'com.android.application' + id 'org.jetbrains.kotlin.android' } android { - compileSdk 34 + compileSdk 34 + namespace "com.example.android.wearable.oauth.pkce" - namespace "com.example.android.wearable.oauth.pkce" + defaultConfig { + applicationId "com.example.android.wearable.oauth.pkce" + minSdk 26 + targetSdk 33 + versionCode 1 + versionName "1.0" + } - defaultConfig { - applicationId "com.example.android.wearable.oauth.pkce" - minSdk 26 - targetSdk 33 - versionCode 1 - versionName "1.0" - } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.majorVersion + } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.majorVersion - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" - } - } - buildFeatures { - viewBinding true + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" } + } + // + buildFeatures { + compose true + } + composeOptions { kotlinCompilerExtensionVersion compose_version } } dependencies { - implementation projects.util - 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.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.androidx.compose.ui.tooling) + + // Horologist dependencies + implementation(libs.horologist.compose.layout) + + // Standard android deps + implementation projects.util + 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 } 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 53ed2678d..207f1152a 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 @@ -16,10 +16,26 @@ package com.example.android.wearable.oauth.pkce import android.os.Bundle -import android.view.View -import android.widget.TextView import androidx.activity.ComponentActivity -import androidx.activity.viewModels +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 com.google.android.horologist.annotations.ExperimentalHorologistApi +import com.google.android.horologist.compose.layout.AppScaffold +import com.google.android.horologist.compose.layout.ScalingLazyColumn +import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults +import com.google.android.horologist.compose.layout.ScreenScaffold +import com.google.android.horologist.compose.layout.rememberResponsiveColumnState /** * Demonstrates the OAuth flow on Wear OS. This sample currently handles the callback from the @@ -35,22 +51,46 @@ class AuthPKCEActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_auth) - val viewModel by viewModels() - - // Start the OAuth flow when the user presses the button - findViewById(R.id.authenticateButton).setOnClickListener { - viewModel.startAuthFlow() - } - - // Show current status on the screen - viewModel.status.observe(this) { statusText -> - findViewById(R.id.status_text_view).text = resources.getText(statusText) - } + setContent { PKCEApp(pkceViewModel = viewModel()) } + } +} - // Show dynamic content on the screen - viewModel.result.observe(this) { resultText -> - findViewById(R.id.result_text_view).text = resultText +@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.Unspecified, + last = ScalingLazyColumnDefaults.ItemType.Unspecified + ) + ) + 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() + ) { + Text( + text = stringResource(R.string.authenticate), + modifier = Modifier.align(Alignment.CenterVertically) + ) + } + } + item { Text(stringResource(id = uiState.value.statusCode)) } + item { Text(uiState.value.resultMessage) } + } } } } 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 86de2aad5..ee3862900 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,11 +15,10 @@ */ 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.AndroidViewModel -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.wear.phone.interactions.authentication.CodeChallenge import androidx.wear.phone.interactions.authentication.CodeVerifier @@ -32,29 +31,37 @@ import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch private const val TAG = "WearOAuthViewModel" // TODO Add your client id & secret here (for dev purposes only). +// DO NOT MERGE private const val CLIENT_ID = "" private const val CLIENT_SECRET = "" +data class ProofKeyCodeExchangeState( + // Status to show on the Wear OS display + val statusCode: Int = R.string.start_auth_flow, + // Dynamic content to show on the Wear OS display + val resultMessage: String = "" +) + /** * The viewModel that implements the OAuth flow. The method [startAuthFlow] implements the * 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(application: Application) : AndroidViewModel(application) { - // Status to show on the Wear OS display - val status: MutableLiveData by lazy { MutableLiveData() } +class AuthPKCEViewModel : ViewModel() { + private val _uiState = MutableStateFlow(ProofKeyCodeExchangeState()) + val uiState: StateFlow = _uiState.asStateFlow() - // Dynamic content to show on the Wear OS display - val result: MutableLiveData by lazy { MutableLiveData() } - - private fun showStatus(statusString: Int, resultString: String = "") { - status.postValue(statusString) - result.postValue(resultString) + private fun showStatus(statusCode: Int = R.string.start_auth_flow, resultString: String = "") { + _uiState.value = + _uiState.value.copy(statusCode = statusCode, resultMessage = resultString) } /** @@ -67,7 +74,7 @@ class AuthPKCEViewModel(application: Application) : AndroidViewModel(application * the phone. After the user consents on their phone, the wearable app is notified and can * continue the authorization process. */ - fun startAuthFlow() { + fun startAuthFlow(context: Context) { viewModelScope.launch { val codeVerifier = CodeVerifier() @@ -78,16 +85,16 @@ class AuthPKCEViewModel(application: Application) : AndroidViewModel(application .encodedPath("https://accounts.google.com/o/oauth2/v2/auth") .appendQueryParameter("scope", "https://www.googleapis.com/auth/userinfo.profile") .build() - val oauthRequest = OAuthRequest.Builder(getApplication()) + val oauthRequest = OAuthRequest.Builder(context) .setAuthProviderUrl(uri) .setCodeChallenge(CodeChallenge(codeVerifier)) .setClientId(CLIENT_ID) .build() // Step 1: Retrieve the OAuth code - showStatus(R.string.status_switch_to_phone) - val code = retrieveOAuthCode(oauthRequest).getOrElse { - showStatus(R.string.status_failed) + showStatus(statusCode = R.string.status_switch_to_phone) + val code = retrieveOAuthCode(oauthRequest, context).getOrElse { + showStatus(statusCode = R.string.status_failed) return@launch } @@ -114,13 +121,14 @@ class AuthPKCEViewModel(application: Application) : AndroidViewModel(application * communication with the paired device, where the user can log in. */ private suspend fun retrieveOAuthCode( - oauthRequest: OAuthRequest + oauthRequest: OAuthRequest, + context: Context ): Result { Log.d(TAG, "Authorization requested. Request URL: ${oauthRequest.requestUrl}") // Wrap the callback-based request inside a coroutine wrapper return suspendCoroutine { c -> - RemoteAuthClient.create(getApplication()).sendAuthorizationRequest( + RemoteAuthClient.create(context).sendAuthorizationRequest( request = oauthRequest, executor = { command -> command?.run() }, clientCallback = object : RemoteAuthClient.Callback() { diff --git a/WearOAuth/oauth-pkce/src/main/res/layout/activity_auth.xml b/WearOAuth/oauth-pkce/src/main/res/layout/activity_auth.xml deleted file mode 100644 index 7ad398170..000000000 --- a/WearOAuth/oauth-pkce/src/main/res/layout/activity_auth.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - -