Skip to content

Commit

Permalink
Merge pull request #1106 from JohnZoellerG/main
Browse files Browse the repository at this point in the history
Migrate Wear OS OAuth Device Auth Grant sample from Views to Compose.
  • Loading branch information
JohnZoellerG authored May 24, 2024
2 parents 9d77d60 + 3e77854 commit 004bb18
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 127 deletions.
3 changes: 1 addition & 2 deletions WearOAuth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ https://developer.android.com/training/wearables/get-started/creating
2.0 client.

- For the Device Authorization Grant sample you must follow the instructions
for ["TV and limited input"](https://developers.google.com/identity/protocols/oauth2/limited-input-device#creatingcred)
.
for ["TV and limited input"](https://developers.google.com/identity/protocols/oauth2/limited-input-device#creatingcred).
- For the PKCE sample you follow the instructions
for ["web application"](https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred)
. Make sure to register https://wear.googleapis.com/3p_auth/<package_name>
Expand Down
3 changes: 3 additions & 0 deletions WearOAuth/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

// 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
}
Expand Down
6 changes: 6 additions & 0 deletions WearOAuth/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ compose-compiler = "1.5.14"
org-jetbrains-kotlin = "1.9.24"
org-jetbrains-kotlinx = "1.8.1"
ktlint = "0.50.0"
compose-material3 = "1.0.0-alpha22"
horologist-compose = "0.6.12"


[libraries]
android-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
Expand All @@ -21,6 +24,7 @@ android-tools-lint-gradle = "com.android.tools.lint:lint-gradle:31.4.1"
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }
androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
androidx-compose-ui-tooling = { module = "androidx.wear.compose:compose-ui-tooling", version.ref = "androidx-wear-compose" }
androidx-constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
androidx-constraintlayout-compose = "androidx.constraintlayout:constraintlayout-compose:1.0.1"
androidx-core-ktx = "androidx.core:core-ktx:1.13.1"
Expand All @@ -47,6 +51,7 @@ compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", versio
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" }
horologist-compose-layout = { group = "com.google.android.horologist", name = "horologist-compose-layout", version.ref = "horologist-compose" }
jacoco-ant = "org.jacoco:org.jacoco.ant:0.8.12"
junit = "junit:junit:4.13.2"
kotlin-annotation-processing-gradle = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-gradle", version.ref = "org-jetbrains-kotlin" }
Expand Down Expand Up @@ -88,6 +93,7 @@ wear-watchface-complications-rendering = { module = "androidx.wear.watchface:wat
wear-watchface-data = { module = "androidx.wear.watchface:watchface-data", version.ref = "androidx-wear-watchface" }
wear-watchface-editor = { module = "androidx.wear.watchface:watchface-editor", version.ref = "androidx-wear-watchface" }
wear-watchface-style = { module = "androidx.wear.watchface:watchface-style", version.ref = "androidx-wear-watchface" }
androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "compose-material3" }

[plugins]
com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
Expand Down
88 changes: 53 additions & 35 deletions WearOAuth/oauth-device-grant/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,67 @@
* 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.devicegrant"

namespace "com.example.android.wearable.oauth.devicegrant"
defaultConfig {
applicationId "com.example.android.wearable.oauth.devicegrant"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
}

defaultConfig {
applicationId "com.example.android.wearable.oauth.devicegrant"
minSdk 26
targetSdk 33
versionCode 1
versionName "1.0"
// Used to make the direct HTTP call for the token exchange. This will eventually be removed
// when the sample is updated to use server-side redirects.
useLibrary "org.apache.http.legacy"

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}

// Used to make the direct HTTP call for the token exchange. This will eventually be removed
// when the sample is updated to use server-side redirects.
useLibrary "org.apache.http.legacy"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.majorVersion
}
kotlinOptions { jvmTarget = JavaVersion.VERSION_17.majorVersion }
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.remote.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 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.remote.interactions)
implementation (libs.playservices.wearable)
}
3 changes: 2 additions & 1 deletion WearOAuth/oauth-device-grant/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<application
android:allowBackup="false"
android:label="OAuth Device Authorization Grant"
android:icon="@mipmap/ic_launcher"
android:theme="@android:style/Theme.DeviceDefault">

<meta-data
Expand All @@ -35,7 +36,7 @@

<activity
android:name=".AuthDeviceGrantActivity"
android:label="wearoauth"
android:label="wearoauth Grant"
android:taskAffinity=".main"
android:exported="true"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,27 @@
package com.example.android.wearable.oauth.devicegrant

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.ScalingLazyColumnDefaults.ItemType
import com.google.android.horologist.compose.layout.ScreenScaffold
import com.google.android.horologist.compose.layout.rememberResponsiveColumnState

/**
* Demonstrates the OAuth 2.0 flow on Wear OS using Device Authorization Grant, as described in
Expand All @@ -38,22 +55,46 @@ class AuthDeviceGrantActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_auth)
val viewModel by viewModels<AuthDeviceGrantViewModel>()

// Start the OAuth flow when the user presses the button
findViewById<View>(R.id.authenticateButton).setOnClickListener {
viewModel.startAuthFlow()
}

// Show current status on the screen
viewModel.status.observe(this) { statusText ->
findViewById<TextView>(R.id.status_text_view).text = resources.getText(statusText)
}
setContent { AuthenticateApp(deviceGrantViewModel = viewModel()) }
}
}

// Show dynamic content on the screen
viewModel.result.observe(this) { resultText ->
findViewById<TextView>(R.id.result_text_view).text = resultText
@OptIn(ExperimentalHorologistApi::class)
@Composable
fun AuthenticateApp(deviceGrantViewModel: AuthDeviceGrantViewModel) {
AppScaffold {
val uiState = deviceGrantViewModel.uiState.collectAsState()
val localContext = LocalContext.current
val columnState = rememberResponsiveColumnState(
contentPadding = ScalingLazyColumnDefaults.padding(
first = ItemType.Unspecified,
last = ItemType.Unspecified
)
)
ScreenScaffold(scrollState = columnState) {
ScalingLazyColumn(columnState = columnState) {
item {
ListHeader {
Text(
stringResource(R.string.oauth_device_auth_grant),
textAlign = TextAlign.Center
)
}
}
item {
Button(
onClick = { deviceGrantViewModel.startAuthFlow(localContext) },
modifier = Modifier.fillMaxSize()
) {
Text(
text = stringResource(R.string.get_grant_from_phone),
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
item { Text(uiState.value.statusCode.toString()) }
item { Text(uiState.value.resultMessage) }
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@
*/
package com.example.android.wearable.oauth.devicegrant

import android.app.Application
import android.content.Context
import android.content.Intent
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.remote.interactions.RemoteActivityHelper
import com.example.android.wearable.oauth.util.doGetRequest
import com.example.android.wearable.oauth.util.doPostRequest
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

private const val TAG = "AuthDeviceGrantViewModel"
Expand All @@ -35,35 +37,38 @@ private const val TAG = "AuthDeviceGrantViewModel"
private const val CLIENT_ID = ""
private const val CLIENT_SECRET = ""

data class DeviceGrantState(
val statusCode: Int = 0,
val resultMessage: String = ""
)

/**
*
* The viewModel that implements the OAuth flow. The method [startAuthFlow] implements the
* different steps of the flow. It first retrieves the URL that should be opened on the paired
* device, then polls for the access token, and uses it to retrieve the user's name.
*/
class AuthDeviceGrantViewModel(application: Application) : AndroidViewModel(application) {
// Status to show on the Wear OS display
val status: MutableLiveData<Int> by lazy { MutableLiveData<Int>() }
class AuthDeviceGrantViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DeviceGrantState())
val uiState: StateFlow<DeviceGrantState> = _uiState.asStateFlow()

// Dynamic content to show on the Wear OS display
val result: MutableLiveData<String> by lazy { MutableLiveData<String>() }

private fun showStatus(statusString: Int, resultString: String = "") {
status.postValue(statusString)
result.postValue(resultString)
private fun showStatus(statusString: Int = 0, resultString: String = "") {
_uiState.value =
_uiState.value.copy(statusCode = statusString, resultMessage = resultString)
}

fun startAuthFlow() {
fun startAuthFlow(context: Context) {
viewModelScope.launch {
// Step 1: Retrieve the verification URI
showStatus(R.string.status_switch_to_phone)
showStatus(statusString = R.string.status_switch_to_phone)
val verificationInfo = retrieveVerificationInfo().getOrElse {
showStatus(R.string.status_failed)
showStatus(statusString = R.string.status_failed)
return@launch
}

// Step 2: Show the pairing code & open the verification URI on the paired device
showStatus(R.string.status_code, verificationInfo.userCode)
fireRemoteIntent(verificationInfo.verificationUri)
fireRemoteIntent(context, verificationInfo.verificationUri)

// Step 3: Poll the Auth server for the token
val token = retrieveToken(verificationInfo.deviceCode, verificationInfo.interval)
Expand Down Expand Up @@ -127,8 +132,8 @@ class AuthDeviceGrantViewModel(application: Application) : AndroidViewModel(appl
* use [Universal Links](https://developer.apple.com/ios/universal-links/) to intercept the
* intent.
*/
private fun fireRemoteIntent(verificationUri: String) {
RemoteActivityHelper(getApplication()).startRemoteActivity(
private fun fireRemoteIntent(context: Context, verificationUri: String) {
RemoteActivityHelper(context).startRemoteActivity(
Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(verificationUri)
Expand Down
Loading

0 comments on commit 004bb18

Please sign in to comment.