-
Notifications
You must be signed in to change notification settings - Fork 226
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #801 from Automattic/update/authenticate-watch-fro…
…m-phone Sending refresh token from phone to watch
- Loading branch information
Showing
33 changed files
with
779 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<!-- Copyright (C) 2021 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 | ||
http://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. | ||
--> | ||
|
||
<resources xmlns:tools="http://schemas.android.com/tools"> | ||
<string-array | ||
name="android_wear_capabilities" | ||
translatable="false" | ||
tools:ignore="UnusedResources"> | ||
<!-- declaring the provided capabilities --> | ||
<item>horologist_phone</item> | ||
</string-array> | ||
</resources> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
...es/features/account/src/main/java/au/com/shiftyjelly/pocketcasts/account/di/AuthModule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package au.com.shiftyjelly.pocketcasts.account.di | ||
|
||
import android.content.Context | ||
import com.google.android.horologist.annotations.ExperimentalHorologistApi | ||
import com.google.android.horologist.data.WearDataLayerRegistry | ||
import dagger.Module | ||
import dagger.Provides | ||
import dagger.hilt.InstallIn | ||
import dagger.hilt.android.qualifiers.ApplicationContext | ||
import dagger.hilt.components.SingletonComponent | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.SupervisorJob | ||
import javax.inject.Singleton | ||
|
||
@Module | ||
@InstallIn(SingletonComponent::class) | ||
object AuthModule { | ||
|
||
@Singleton | ||
@Provides | ||
@ForApplicationScope | ||
fun coroutineScope(): CoroutineScope = | ||
CoroutineScope(SupervisorJob() + Dispatchers.Default) | ||
|
||
@OptIn(ExperimentalHorologistApi::class) | ||
@Singleton | ||
@Provides | ||
fun providesWearDataLayerRegistry( | ||
@ApplicationContext context: Context, | ||
@ForApplicationScope coroutineScope: CoroutineScope | ||
): WearDataLayerRegistry { | ||
return WearDataLayerRegistry.fromContext( | ||
application = context, | ||
coroutineScope = coroutineScope | ||
) | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
...atures/account/src/main/java/au/com/shiftyjelly/pocketcasts/account/di/AuthPhoneModule.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package au.com.shiftyjelly.pocketcasts.account.di | ||
|
||
import au.com.shiftyjelly.pocketcasts.account.watchsync.WatchSyncAuthData | ||
import au.com.shiftyjelly.pocketcasts.account.watchsync.WatchSyncAuthDataSerializer | ||
import com.google.android.horologist.annotations.ExperimentalHorologistApi | ||
import com.google.android.horologist.auth.data.phone.tokenshare.TokenBundleRepository | ||
import com.google.android.horologist.auth.data.phone.tokenshare.impl.TokenBundleRepositoryImpl | ||
import com.google.android.horologist.data.WearDataLayerRegistry | ||
import dagger.Module | ||
import dagger.Provides | ||
import dagger.hilt.InstallIn | ||
import dagger.hilt.components.SingletonComponent | ||
import kotlinx.coroutines.CoroutineScope | ||
import javax.inject.Qualifier | ||
|
||
@Module | ||
@InstallIn(SingletonComponent::class) | ||
object AuthPhoneModule { | ||
|
||
@ExperimentalHorologistApi | ||
@Provides | ||
fun providesTokenBundleRepository( | ||
wearDataLayerRegistry: WearDataLayerRegistry, | ||
@ForApplicationScope coroutineScope: CoroutineScope, | ||
): TokenBundleRepository<WatchSyncAuthData?> { | ||
return TokenBundleRepositoryImpl( | ||
registry = wearDataLayerRegistry, | ||
coroutineScope = coroutineScope, | ||
serializer = WatchSyncAuthDataSerializer | ||
) | ||
} | ||
} | ||
|
||
@Qualifier | ||
@Retention(AnnotationRetention.BINARY) | ||
annotation class ForApplicationScope |
81 changes: 81 additions & 0 deletions
81
...tures/account/src/main/java/au/com/shiftyjelly/pocketcasts/account/watchsync/WatchSync.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
package au.com.shiftyjelly.pocketcasts.account.watchsync | ||
|
||
import android.annotation.SuppressLint | ||
import au.com.shiftyjelly.pocketcasts.repositories.sync.LoginResult | ||
import au.com.shiftyjelly.pocketcasts.repositories.sync.SignInSource | ||
import au.com.shiftyjelly.pocketcasts.repositories.sync.SyncManager | ||
import au.com.shiftyjelly.pocketcasts.utils.log.LogBuffer | ||
import com.google.android.horologist.annotations.ExperimentalHorologistApi | ||
import com.google.android.horologist.auth.data.phone.tokenshare.TokenBundleRepository | ||
import kotlinx.coroutines.CancellationException | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import timber.log.Timber | ||
import javax.inject.Inject | ||
import javax.inject.Singleton | ||
|
||
@Singleton | ||
@SuppressLint("VisibleForTests") // https://issuetracker.google.com/issues/239451111 | ||
class WatchSync @OptIn(ExperimentalHorologistApi::class) | ||
@Inject constructor( | ||
private val syncManager: SyncManager, | ||
private val tokenBundleRepository: TokenBundleRepository<WatchSyncAuthData?>, | ||
) { | ||
/** | ||
* This should be called by the phone app to update the refresh token available to | ||
* the watch app in the data layer. | ||
*/ | ||
@OptIn(ExperimentalHorologistApi::class) | ||
suspend fun sendAuthToDataLayer() { | ||
withContext(Dispatchers.Default) { | ||
try { | ||
Timber.i("Updating WatchSyncAuthData in data layer") | ||
|
||
val watchSyncAuthData = syncManager.getRefreshToken()?.let { refreshToken -> | ||
syncManager.getLoginIdentity()?.let { loginIdentity -> | ||
WatchSyncAuthData( | ||
refreshToken = refreshToken, | ||
loginIdentity = loginIdentity | ||
) | ||
} | ||
} | ||
|
||
if (watchSyncAuthData == null) { | ||
Timber.i("Removing WatchSyncAuthData from data layer") | ||
} | ||
|
||
tokenBundleRepository.update(watchSyncAuthData) | ||
} catch (cancellationException: CancellationException) { | ||
// Don't catch CancellationException since this represents the normal cancellation of a coroutine | ||
throw cancellationException | ||
} catch (exception: Exception) { | ||
LogBuffer.e( | ||
LogBuffer.TAG_BACKGROUND_TASKS, | ||
"Saving refresh token to data layer failed: $exception" | ||
) | ||
} | ||
} | ||
} | ||
|
||
suspend fun processAuthDataChange(data: WatchSyncAuthData?, onResult: (LoginResult) -> Unit) { | ||
if (data != null) { | ||
|
||
Timber.i("Received WatchSyncAuthData change from phone") | ||
|
||
if (!syncManager.isLoggedIn()) { | ||
val result = syncManager.loginWithToken( | ||
token = data.refreshToken, | ||
loginIdentity = data.loginIdentity, | ||
signInSource = SignInSource.WatchPhoneSync | ||
) | ||
onResult(result) | ||
} else { | ||
Timber.i("Received WatchSyncAuthData from phone, but user is already logged in") | ||
} | ||
} else { | ||
// The user either was never logged in on their phone or just logged out. | ||
// Either way, leave the user's login state on the watch unchanged. | ||
Timber.i("Received null WatchSyncAuthData change") | ||
} | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
...count/src/main/java/au/com/shiftyjelly/pocketcasts/account/watchsync/WatchSyncAuthData.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package au.com.shiftyjelly.pocketcasts.account.watchsync | ||
|
||
import androidx.datastore.core.Serializer | ||
import au.com.shiftyjelly.pocketcasts.preferences.RefreshToken | ||
import au.com.shiftyjelly.pocketcasts.repositories.sync.LoginIdentity | ||
import com.squareup.moshi.Json | ||
import com.squareup.moshi.JsonClass | ||
import com.squareup.moshi.Moshi | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.withContext | ||
import java.io.InputStream | ||
import java.io.InputStreamReader | ||
import java.io.OutputStream | ||
|
||
@JsonClass(generateAdapter = true) | ||
data class WatchSyncAuthData( | ||
@field:Json(name = "refreshToken") val refreshToken: RefreshToken, | ||
@field:Json(name = "loginIdentity") val loginIdentity: LoginIdentity, | ||
) | ||
|
||
object WatchSyncAuthDataSerializer : Serializer<WatchSyncAuthData?> { | ||
|
||
private val adapter = WatchSyncAuthDataJsonAdapter( | ||
Moshi.Builder() | ||
.add(RefreshToken::class.java, RefreshToken.Adapter) | ||
.add(LoginIdentity.Adapter) | ||
.build() | ||
) | ||
|
||
override val defaultValue: WatchSyncAuthData? = null | ||
|
||
override suspend fun readFrom(input: InputStream): WatchSyncAuthData? { | ||
val string = InputStreamReader(input).readText() | ||
return adapter.fromJson(string) | ||
} | ||
|
||
override suspend fun writeTo(t: WatchSyncAuthData?, output: OutputStream) { | ||
withContext(Dispatchers.IO) { | ||
if (t != null) { | ||
val jsonString = adapter.toJson(t) | ||
output.write(jsonString.toByteArray()) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
53 changes: 53 additions & 0 deletions
53
...mpose/src/main/java/au/com/shiftyjelly/pocketcasts/compose/images/GravatarProfileImage.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package au.com.shiftyjelly.pocketcasts.compose.images | ||
|
||
import androidx.compose.animation.Crossfade | ||
import androidx.compose.animation.core.tween | ||
import androidx.compose.foundation.Image | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.alpha | ||
import androidx.compose.ui.platform.LocalContext | ||
import au.com.shiftyjelly.pocketcasts.utils.Gravatar | ||
import coil.compose.rememberAsyncImagePainter | ||
import coil.request.ImageRequest | ||
|
||
@Composable | ||
fun GravatarProfileImage( | ||
email: String, | ||
modifier: Modifier = Modifier, | ||
contentDescription: String?, | ||
placeholder: @Composable (() -> Unit) = {}, | ||
) { | ||
|
||
val gravatarUrl = remember(email) { | ||
Gravatar.getUrl(email) | ||
} | ||
|
||
val gravatarPainter = rememberAsyncImagePainter( | ||
model = ImageRequest.Builder(LocalContext.current) | ||
.data(gravatarUrl) | ||
.crossfade(true) | ||
.build(), | ||
) | ||
|
||
Crossfade( | ||
gravatarPainter.state.painter == null, | ||
animationSpec = tween(500), | ||
) { showPlaceholder -> | ||
Image( | ||
painter = gravatarPainter, | ||
contentDescription = contentDescription, | ||
modifier = modifier | ||
.alpha(if (showPlaceholder) 0f else 1f) | ||
) | ||
|
||
// If the gravatar image has not loaded or fails to load (because there is no gravatar image associated | ||
// with this account), show the placeholder. We are settings the placeholder this way instead of | ||
// setting a placeholder on an AsyncImagePainter because this approach continues showing the placeholder | ||
// when there is not a gravatar image associated with the account. | ||
if (showPlaceholder) { | ||
placeholder() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.