msal for Google Play, AppAuth for F-Droid

pull/3591/head
Alex Baker 7 months ago
parent a519a06c3b
commit a4ca8b28aa

@ -229,11 +229,6 @@ dependencies {
implementation(libs.etebase) implementation(libs.etebase)
implementation(libs.colorpicker) implementation(libs.colorpicker)
implementation(libs.appauth) implementation(libs.appauth)
implementation(libs.microsoft.authentication) {
exclude("com.microsoft.device.display", "display-mask")
exclude("com.google.android.gms")
exclude("com.google.android.libraries.identity.googleid")
}
implementation(libs.osmdroid) implementation(libs.osmdroid)
implementation(libs.androidx.recyclerview) implementation(libs.androidx.recyclerview)
@ -273,6 +268,9 @@ dependencies {
googleplayImplementation(libs.horologist.datalayer.grpc) googleplayImplementation(libs.horologist.datalayer.grpc)
googleplayImplementation(libs.horologist.datalayer.core) googleplayImplementation(libs.horologist.datalayer.core)
googleplayImplementation(libs.play.services.wearable) googleplayImplementation(libs.play.services.wearable)
googleplayImplementation(libs.microsoft.authentication) {
exclude("com.microsoft.device.display", "display-mask")
}
googleplayImplementation(projects.wearDatalayer) googleplayImplementation(projects.wearDatalayer)
androidTestImplementation(libs.dagger.hilt.testing) androidTestImplementation(libs.dagger.hilt.testing)

@ -1,6 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest> <manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">
<application/> <application tools:ignore="MissingApplicationIcon">
<activity
android:name=".auth.MicrosoftAuthenticationActivity"
android:theme="@style/TranslucentDialog"/>
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true"
tools:node="merge">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="${applicationId}"
android:scheme="msauth" />
</intent-filter>
</activity>
</application>
</manifest> </manifest>

@ -0,0 +1,38 @@
package org.tasks.auth
import android.net.Uri
import androidx.core.net.toUri
import net.openid.appauth.AuthorizationServiceConfiguration
import kotlin.coroutines.suspendCoroutine
data class IdentityProvider(
val name: String,
val discoveryEndpoint: Uri,
val clientId: String,
val redirectUri: Uri,
val scope: String
) {
suspend fun retrieveConfig(): AuthorizationServiceConfiguration {
return suspendCoroutine { cont ->
AuthorizationServiceConfiguration.fetchFromUrl(discoveryEndpoint) { serviceConfiguration, ex ->
cont.resumeWith(
when {
ex != null -> Result.failure(ex)
serviceConfiguration != null -> Result.success(serviceConfiguration)
else -> Result.failure(IllegalStateException())
}
)
}
}
}
companion object {
val MICROSOFT = IdentityProvider(
"Microsoft",
"https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration".toUri(),
"9d4babd5-e7ba-4286-ba4b-17274495a901",
"msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D".toUri(),
"user.read Tasks.ReadWrite openid offline_access email"
)
}
}

@ -0,0 +1,157 @@
package org.tasks.auth
import android.os.Bundle
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color.Companion.White
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationServiceDiscovery
import okhttp3.Request
import org.json.JSONObject
import org.tasks.R
import org.tasks.analytics.Constants
import org.tasks.analytics.Firebase
import org.tasks.data.UUIDHelper
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT
import org.tasks.http.HttpClientFactory
import org.tasks.jobs.WorkManager
import org.tasks.preferences.fragments.TasksAccountViewModel.Companion.getStringOrNull
import org.tasks.security.KeyStoreEncryption
import org.tasks.sync.SyncAdapters
import org.tasks.sync.microsoft.requestTokenExchange
import javax.inject.Inject
@AndroidEntryPoint
class MicrosoftAuthenticationActivity : ComponentActivity() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var encryption: KeyStoreEncryption
@Inject lateinit var httpClientFactory: HttpClientFactory
@Inject lateinit var firebase: Firebase
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val authState = AuthState(
AuthorizationResponse.fromIntent(intent),
AuthorizationException.fromIntent(intent)
)
authState.authorizationException?.let {
error(it.message ?: "Authentication failed")
return
}
lifecycleScope.launch {
val (resp, ex) = requestTokenExchange(authState.lastAuthorizationResponse!!)
authState.update(resp, ex)
if (authState.isAuthorized) {
val email = getEmail(authState.accessToken) ?: run {
error("Failed to fetch profile")
return@launch
}
caldavDao
.getAccount(TYPE_MICROSOFT, email)
?.let {
caldavDao.update(
it.copy(password = encryption.encrypt(authState.jsonSerializeString()))
)
}
?: caldavDao
.insert(
CaldavAccount(
uuid = UUIDHelper.newUUID(),
name = email,
username = email,
password = encryption.encrypt(authState.jsonSerializeString()),
accountType = TYPE_MICROSOFT,
)
)
.also {
firebase.logEvent(
R.string.event_sync_add_account,
R.string.param_type to Constants.SYNC_TYPE_MICROSOFT
)
}
syncAdapters.sync(true)
workManager.updateBackgroundSync()
finish()
} else {
error(ex?.message ?: "Token exchange failed")
}
}
setContent {
var showDialog by remember { mutableStateOf(true) }
if (showDialog) {
Dialog(
onDismissRequest = { showDialog = false },
DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(100.dp)
.background(White, shape = RoundedCornerShape(8.dp))
) {
CircularProgressIndicator()
}
}
}
}
}
private suspend fun getEmail(accessToken: String?): String? = withContext(Dispatchers.IO) {
if (accessToken == null) {
return@withContext null
}
val discovery = AuthorizationServiceDiscovery(
JSONObject(
intent.getStringExtra(EXTRA_SERVICE_DISCOVERY)!!
)
)
val userInfo = httpClientFactory
.newClient(foreground = false)
.newCall(
Request.Builder()
.url(discovery.userinfoEndpoint!!.toString())
.addHeader("Authorization", "Bearer $accessToken")
.build()
)
.execute()
val response = userInfo.body?.string() ?: return@withContext null
JSONObject(response).getStringOrNull("email")
}
private fun error(message: String) {
Toast.makeText(this@MicrosoftAuthenticationActivity, message, LENGTH_LONG).show()
finish()
}
companion object {
const val EXTRA_SERVICE_DISCOVERY = "extra_service_discovery"
}
}

@ -0,0 +1,29 @@
package org.tasks.sync.microsoft
import android.content.Context
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.TokenRequest
import net.openid.appauth.TokenResponse
import kotlin.coroutines.suspendCoroutine
suspend fun Context.requestTokenRefresh(state: AuthState) =
requestToken(state.createTokenRefreshRequest())
suspend fun Context.requestTokenExchange(response: AuthorizationResponse) =
requestToken(response.createTokenExchangeRequest())
private suspend fun Context.requestToken(tokenRequest: TokenRequest): Pair<TokenResponse?, AuthorizationException?> {
val authService = AuthorizationService(this)
return try {
suspendCoroutine { cont ->
authService.performTokenRequest(tokenRequest) { response, ex ->
cont.resumeWith(Result.success(Pair(response, ex)))
}
}
} finally {
authService.dispose()
}
}

@ -0,0 +1,74 @@
package org.tasks.sync.microsoft
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import net.openid.appauth.AppAuthConfiguration
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationService
import net.openid.appauth.ResponseTypeValues
import net.openid.appauth.browser.AnyBrowserMatcher
import net.openid.appauth.connectivity.DefaultConnectionBuilder
import org.tasks.BuildConfig
import org.tasks.auth.DebugConnectionBuilder
import org.tasks.auth.IdentityProvider
import org.tasks.auth.MicrosoftAuthenticationActivity
import org.tasks.auth.MicrosoftAuthenticationActivity.Companion.EXTRA_SERVICE_DISCOVERY
import javax.inject.Inject
@HiltViewModel
class MicrosoftSignInViewModel @Inject constructor(
private val debugConnectionBuilder: DebugConnectionBuilder,
) : ViewModel() {
fun signIn(activity: Activity) {
viewModelScope.launch {
val idp = IdentityProvider.MICROSOFT
val serviceConfig = idp.retrieveConfig()
val authRequest = AuthorizationRequest
.Builder(
serviceConfig,
idp.clientId,
ResponseTypeValues.CODE,
idp.redirectUri
)
.setScope(idp.scope)
.setPrompt(AuthorizationRequest.Prompt.SELECT_ACCOUNT)
.build()
val intent = Intent(activity, MicrosoftAuthenticationActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra(
EXTRA_SERVICE_DISCOVERY,
serviceConfig.discoveryDoc!!.docJson.toString()
)
val authorizationService = AuthorizationService(
activity,
AppAuthConfiguration.Builder()
.setBrowserMatcher(AnyBrowserMatcher.INSTANCE)
.setConnectionBuilder(
if (BuildConfig.DEBUG) {
debugConnectionBuilder
} else {
DefaultConnectionBuilder.INSTANCE
}
)
.build()
)
authorizationService.performAuthorizationRequest(
authRequest,
PendingIntent.getActivity(
activity,
authRequest.hashCode(),
intent,
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
),
authorizationService.createCustomTabsIntentBuilder()
.build()
)
}
}
}

@ -0,0 +1,29 @@
package org.tasks.sync.microsoft
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import net.openid.appauth.AuthState
import org.tasks.data.entity.CaldavAccount
import org.tasks.security.KeyStoreEncryption
import javax.inject.Inject
class MicrosoftTokenProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val encryption: KeyStoreEncryption,
) {
suspend fun getToken(account: CaldavAccount): String {
val authState = encryption.decrypt(account.password)?.let { AuthState.jsonDeserialize(it) }
?: throw RuntimeException("Missing credentials")
if (authState.needsTokenRefresh) {
val (token, ex) = context.requestTokenRefresh(authState)
authState.update(token, ex)
if (authState.isAuthorized) {
account.password = encryption.encrypt(authState.jsonSerializeString())
}
}
if (!authState.isAuthorized) {
throw RuntimeException("Needs authentication")
}
return authState.accessToken!!
}
}

@ -5,5 +5,4 @@
<!--suppress CheckTagEmptyBody --> <!--suppress CheckTagEmptyBody -->
<string name="manage_subscription_url">%s</string> <string name="manage_subscription_url">%s</string>
<string name="support_email">support@tasks.org</string> <string name="support_email">support@tasks.org</string>
<string name="microsoft_oauth_path">/xVwQTvk42gGm0o6zNvelaYloFcs=</string>
</resources> </resources>

@ -1,20 +0,0 @@
{
"client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901",
"authorization_user_agent" : "DEFAULT",
"redirect_uri" : "msauth://org.tasks/xVwQTvk42gGm0o6zNvelaYloFcs%3D",
"account_mode" : "MULTIPLE",
"authorities" : [
{
"type": "AAD",
"audience": {
"type": "AzureADandPersonalMicrosoftAccount",
"tenant_id": "common"
}
}
],
"logging": {
"level": "info",
"logcat_enabled": true,
"pii_enabled": false
}
}

@ -53,6 +53,20 @@
</intent-filter> </intent-filter>
</service> </service>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${applicationId}"
android:path="@string/microsoft_oauth_path"
android:scheme="msauth" />
</intent-filter>
</activity>
</application> </application>
</manifest> </manifest>

@ -0,0 +1,39 @@
package org.tasks.sync.microsoft
import android.content.Context
import com.microsoft.identity.client.AcquireTokenSilentParameters
import com.microsoft.identity.client.PublicClientApplication
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.data.entity.CaldavAccount
import timber.log.Timber
import javax.inject.Inject
class MicrosoftTokenProvider @Inject constructor(
@ApplicationContext private val context: Context,
) {
fun getToken(account: CaldavAccount): String {
val app = PublicClientApplication.createMultipleAccountPublicClientApplication(
context,
R.raw.microsoft_config
)
val result = try {
val msalAccount = app.accounts.firstOrNull { it.username == account.username }
?: throw RuntimeException("No matching account found")
val parameters = AcquireTokenSilentParameters.Builder()
.withScopes(MicrosoftSignInViewModel.scopes)
.forAccount(msalAccount)
.fromAuthority(msalAccount.authority)
.forceRefresh(true)
.build()
app.acquireTokenSilent(parameters)
} catch (e: Exception) {
Timber.e(e)
throw RuntimeException("Authentication failed: ${e.message}")
}
return result.accessToken
}
}

@ -1013,20 +1013,6 @@
<!-- Version >= 3.0. DeX Dual Mode support --> <!-- Version >= 3.0. DeX Dual Mode support -->
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/> <meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
<activity
android:name="com.microsoft.identity.client.BrowserTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${applicationId}"
android:path="@string/microsoft_oauth_path"
android:scheme="msauth" />
</intent-filter>
</activity>
</application> </application>
</manifest> </manifest>

@ -3,8 +3,6 @@ package org.tasks.http
import android.content.Context import android.content.Context
import at.bitfire.cert4android.CustomCertManager import at.bitfire.cert4android.CustomCertManager
import at.bitfire.dav4jvm.BasicDigestAuthHandler import at.bitfire.dav4jvm.BasicDigestAuthHandler
import com.microsoft.identity.client.AcquireTokenSilentParameters
import com.microsoft.identity.client.PublicClientApplication
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android import io.ktor.client.engine.android.Android
@ -24,13 +22,12 @@ import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.caldav.TasksCookieJar import org.tasks.caldav.TasksCookieJar
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.extensions.Context.cookiePersistor import org.tasks.extensions.Context.cookiePersistor
import org.tasks.security.KeyStoreEncryption import org.tasks.security.KeyStoreEncryption
import org.tasks.sync.microsoft.MicrosoftService import org.tasks.sync.microsoft.MicrosoftService
import org.tasks.sync.microsoft.MicrosoftSignInViewModel import org.tasks.sync.microsoft.MicrosoftTokenProvider
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
@ -38,6 +35,7 @@ import javax.net.ssl.SSLContext
class HttpClientFactory @Inject constructor( class HttpClientFactory @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val encryption: KeyStoreEncryption, private val encryption: KeyStoreEncryption,
private val microsoftTokenProvider: MicrosoftTokenProvider,
) { ) {
suspend fun newClient(foreground: Boolean) = newClient( suspend fun newClient(foreground: Boolean) = newClient(
foreground = foreground, foreground = foreground,
@ -87,28 +85,7 @@ class HttpClientFactory @Inject constructor(
} }
suspend fun getMicrosoftService(account: CaldavAccount): MicrosoftService = withContext(Dispatchers.IO) { suspend fun getMicrosoftService(account: CaldavAccount): MicrosoftService = withContext(Dispatchers.IO) {
val app = PublicClientApplication.createMultipleAccountPublicClientApplication( val token = microsoftTokenProvider.getToken(account)
context,
R.raw.microsoft_config
)
val result = try {
val msalAccount = app.accounts.firstOrNull { it.username == account.username }
?: throw RuntimeException("No matching account found")
val parameters = AcquireTokenSilentParameters.Builder()
.withScopes(MicrosoftSignInViewModel.scopes)
.forAccount(msalAccount)
.fromAuthority(msalAccount.authority)
.forceRefresh(true)
.build()
app.acquireTokenSilent(parameters)
} catch (e: Exception) {
Timber.e(e)
throw RuntimeException("Authentication failed: ${e.message}")
}
val client = HttpClient(Android) { val client = HttpClient(Android) {
expectSuccess = true expectSuccess = true
@ -121,7 +98,7 @@ class HttpClientFactory @Inject constructor(
} }
defaultRequest { defaultRequest {
header("Authorization", "Bearer ${result.accessToken}") header("Authorization", "Bearer $token")
} }
install(HttpTimeout) { install(HttpTimeout) {

@ -1460,51 +1460,6 @@
+| +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| +--- androidx.core:core:1.1.0 -> 1.13.1 (*)
+| +--- androidx.annotation:annotation:1.1.0 -> 1.9.1 (*) +| +--- androidx.annotation:annotation:1.1.0 -> 1.9.1 (*)
+| \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava +| \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava
++--- com.microsoft.identity.client:msal:6.0.1
+| +--- com.microsoft.identity:common:21.1.0
+| | +--- com.microsoft.identity:common4j:21.1.0
+| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*)
+| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 -> 1.10.2 (*)
+| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 -> 2.9.0 (*)
+| | +--- androidx.datastore:datastore-preferences:1.0.0 -> 1.1.4 (*)
+| | +--- org.apache.httpcomponents.core5:httpcore5:5.3
+| | +--- com.nimbusds:nimbus-jose-jwt:9.37.3
+| | | \--- com.github.stephenc.jcip:jcip-annotations:1.0-1
+| | +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.0 (*)
+| | +--- com.google.code.gson:gson:2.8.9 -> 2.12.1
+| | +--- com.squareup.moshi:moshi:1.14.0
+| | | +--- com.squareup.okio:okio:2.10.0 -> 3.9.0 (*)
+| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0 -> 2.1.20 (*)
+| | +--- androidx.browser:browser:1.0.0 -> 1.3.0 (*)
+| | +--- androidx.constraintlayout:constraintlayout:1.1.3 -> 2.2.1 (*)
+| | +--- com.yubico.yubikit:android:2.5.0
+| | | +--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | | \--- com.yubico.yubikit:core:2.5.0
+| | | \--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | +--- com.yubico.yubikit:piv:2.5.0
+| | | +--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | | \--- com.yubico.yubikit:core:2.5.0 (*)
+| | +--- androidx.credentials:credentials:1.2.2
+| | | +--- androidx.annotation:annotation:1.5.0 -> 1.9.1 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.20 (*)
+| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.10.2 (*)
+| | | \--- androidx.credentials:credentials-play-services-auth:1.2.2 (c)
+| | +--- androidx.credentials:credentials-play-services-auth:1.2.2
+| | | +--- androidx.credentials:credentials:1.2.2 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.20 (*)
+| | | \--- androidx.credentials:credentials:1.2.2 (c)
+| | +--- io.opentelemetry:opentelemetry-api:1.18.0
+| | | \--- io.opentelemetry:opentelemetry-context:1.18.0
+| | \--- androidx.fragment:fragment:1.3.2 -> 1.8.6 (*)
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*)
+| +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.0 (*)
+| +--- androidx.browser:browser:1.0.0 -> 1.3.0 (*)
+| +--- com.google.code.gson:gson:2.8.9 -> 2.12.1
+| +--- com.nimbusds:nimbus-jose-jwt:9.37.3 (*)
+| +--- org.apache.httpcomponents.core5:httpcore5:5.3
+| +--- androidx.constraintlayout:constraintlayout:1.1.3 -> 2.2.1 (*)
+| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.10.2 (*)
+| \--- io.opentelemetry:opentelemetry-api:1.18.0 (*)
++--- org.osmdroid:osmdroid-android:6.1.20 ++--- org.osmdroid:osmdroid-android:6.1.20
++--- androidx.recyclerview:recyclerview:1.4.0 (*) ++--- androidx.recyclerview:recyclerview:1.4.0 (*)
++--- androidx.compose:compose-bom:2025.04.00 ++--- androidx.compose:compose-bom:2025.04.00

@ -806,6 +806,87 @@
+| \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.20 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.20 (*)
++--- com.google.android.horologist:horologist-datalayer:0.6.23 (*) ++--- com.google.android.horologist:horologist-datalayer:0.6.23 (*)
++--- com.google.android.gms:play-services-wearable:19.0.0 (*) ++--- com.google.android.gms:play-services-wearable:19.0.0 (*)
++--- com.microsoft.identity.client:msal:6.0.1
+| +--- com.microsoft.identity:common:21.1.0
+| | +--- com.microsoft.identity:common4j:21.1.0
+| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*)
+| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 -> 1.10.2 (*)
+| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 -> 2.9.0 (*)
+| | +--- androidx.datastore:datastore-preferences:1.0.0 -> 1.1.4 (*)
+| | +--- org.apache.httpcomponents.core5:httpcore5:5.3
+| | +--- com.nimbusds:nimbus-jose-jwt:9.37.3
+| | | \--- com.github.stephenc.jcip:jcip-annotations:1.0-1
+| | +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.0 (*)
+| | +--- com.google.code.gson:gson:2.8.9 -> 2.12.1
+| | +--- com.squareup.moshi:moshi:1.14.0
+| | | +--- com.squareup.okio:okio:2.10.0 -> 3.9.0 (*)
+| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0 -> 2.1.20 (*)
+| | +--- androidx.browser:browser:1.0.0 -> 1.3.0
+| | | +--- androidx.collection:collection:1.1.0 -> 1.4.5 (*)
+| | | +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*)
+| | | +--- androidx.interpolator:interpolator:1.0.0 (*)
+| | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*)
+| | | +--- androidx.annotation:annotation:1.1.0 -> 1.9.1 (*)
+| | | \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava
+| | +--- androidx.constraintlayout:constraintlayout:1.1.3 -> 2.2.1
+| | | +--- androidx.appcompat:appcompat:1.2.0 -> 1.7.0 (*)
+| | | +--- androidx.constraintlayout:constraintlayout-core:1.1.1
+| | | | \--- androidx.annotation:annotation:1.8.1 -> 1.9.1 (*)
+| | | +--- androidx.core:core:1.3.2 -> 1.13.1 (*)
+| | | \--- androidx.profileinstaller:profileinstaller:1.4.0 (*)
+| | +--- com.yubico.yubikit:android:2.5.0
+| | | +--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | | \--- com.yubico.yubikit:core:2.5.0
+| | | \--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | +--- com.yubico.yubikit:piv:2.5.0
+| | | +--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | | \--- com.yubico.yubikit:core:2.5.0 (*)
+| | +--- androidx.credentials:credentials:1.2.2
+| | | +--- androidx.annotation:annotation:1.5.0 -> 1.9.1 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.20 (*)
+| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.10.2 (*)
+| | | \--- androidx.credentials:credentials-play-services-auth:1.2.2 (c)
+| | +--- androidx.credentials:credentials-play-services-auth:1.2.2
+| | | +--- androidx.credentials:credentials:1.2.2 (*)
+| | | +--- com.google.android.gms:play-services-auth:20.7.0
+| | | | +--- androidx.fragment:fragment:1.0.0 -> 1.8.6 (*)
+| | | | +--- com.google.android.gms:play-services-auth-api-phone:18.0.1
+| | | | | +--- com.google.android.gms:play-services-base:18.0.1 -> 18.5.0 (*)
+| | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.5.0 (*)
+| | | | | \--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*)
+| | | | +--- com.google.android.gms:play-services-auth-base:18.0.4
+| | | | | +--- androidx.collection:collection:1.0.0 -> 1.4.5 (*)
+| | | | | +--- com.google.android.gms:play-services-base:18.0.1 -> 18.5.0 (*)
+| | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.5.0 (*)
+| | | | | \--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*)
+| | | | +--- com.google.android.gms:play-services-base:18.0.1 -> 18.5.0 (*)
+| | | | +--- com.google.android.gms:play-services-basement:18.2.0 -> 18.5.0 (*)
+| | | | +--- com.google.android.gms:play-services-fido:20.0.1 -> 20.1.0
+| | | | | +--- com.google.android.gms:play-services-base:18.0.1 -> 18.5.0 (*)
+| | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.5.0 (*)
+| | | | | \--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*)
+| | | | \--- com.google.android.gms:play-services-tasks:18.0.1 -> 18.2.0 (*)
+| | | +--- com.google.android.gms:play-services-fido:20.1.0 (*)
+| | | +--- com.google.android.libraries.identity.googleid:googleid:1.1.0
+| | | | +--- androidx.credentials:credentials:1.0.0-alpha04 -> 1.2.2 (*)
+| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.0 -> 2.1.20 (*)
+| | | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0 -> 2.1.20 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.20 (*)
+| | | \--- androidx.credentials:credentials:1.2.2 (c)
+| | +--- com.google.android.gms:play-services-fido:20.1.0 (*)
+| | +--- com.google.android.libraries.identity.googleid:googleid:1.1.0 (*)
+| | +--- io.opentelemetry:opentelemetry-api:1.18.0
+| | | \--- io.opentelemetry:opentelemetry-context:1.18.0
+| | \--- androidx.fragment:fragment:1.3.2 -> 1.8.6 (*)
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*)
+| +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.0 (*)
+| +--- androidx.browser:browser:1.0.0 -> 1.3.0 (*)
+| +--- com.google.code.gson:gson:2.8.9 -> 2.12.1
+| +--- com.nimbusds:nimbus-jose-jwt:9.37.3 (*)
+| +--- org.apache.httpcomponents.core5:httpcore5:5.3
+| +--- androidx.constraintlayout:constraintlayout:1.1.3 -> 2.2.1 (*)
+| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.10.2 (*)
+| \--- io.opentelemetry:opentelemetry-api:1.18.0 (*)
++--- project :wear-datalayer ++--- project :wear-datalayer
+| +--- io.grpc:grpc-kotlin-stub:1.4.3 (*) +| +--- io.grpc:grpc-kotlin-stub:1.4.3 (*)
+| +--- io.grpc:grpc-protobuf-lite:1.72.0 (*) +| +--- io.grpc:grpc-protobuf-lite:1.72.0 (*)
@ -1574,12 +1655,7 @@
+| | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*)
+| | | +--- androidx.customview:customview:1.0.0 -> 1.1.0 (*) +| | | +--- androidx.customview:customview:1.0.0 -> 1.1.0 (*)
+| | | \--- androidx.collection:collection:1.0.0 -> 1.4.5 (*) +| | | \--- androidx.collection:collection:1.0.0 -> 1.4.5 (*)
+| | +--- androidx.constraintlayout:constraintlayout:2.0.1 -> 2.2.1 +| | +--- androidx.constraintlayout:constraintlayout:2.0.1 -> 2.2.1 (*)
+| | | +--- androidx.appcompat:appcompat:1.2.0 -> 1.7.0 (*)
+| | | +--- androidx.constraintlayout:constraintlayout-core:1.1.1
+| | | | \--- androidx.annotation:annotation:1.8.1 -> 1.9.1 (*)
+| | | +--- androidx.core:core:1.3.2 -> 1.13.1 (*)
+| | | \--- androidx.profileinstaller:profileinstaller:1.4.0 (*)
+| | +--- androidx.core:core:1.6.0 -> 1.13.1 (*) +| | +--- androidx.core:core:1.6.0 -> 1.13.1 (*)
+| | +--- androidx.drawerlayout:drawerlayout:1.1.1 (*) +| | +--- androidx.drawerlayout:drawerlayout:1.1.1 (*)
+| | +--- androidx.dynamicanimation:dynamicanimation:1.0.0 +| | +--- androidx.dynamicanimation:dynamicanimation:1.0.0
@ -1806,58 +1882,7 @@
++--- net.openid:appauth:0.11.1 ++--- net.openid:appauth:0.11.1
+| +--- androidx.annotation:annotation:1.2.0 -> 1.9.1 (*) +| +--- androidx.annotation:annotation:1.2.0 -> 1.9.1 (*)
+| +--- androidx.appcompat:appcompat:1.3.0 -> 1.7.0 (*) +| +--- androidx.appcompat:appcompat:1.3.0 -> 1.7.0 (*)
+| \--- androidx.browser:browser:1.3.0 +| \--- androidx.browser:browser:1.3.0 (*)
+| +--- androidx.collection:collection:1.1.0 -> 1.4.5 (*)
+| +--- androidx.concurrent:concurrent-futures:1.0.0 -> 1.1.0 (*)
+| +--- androidx.interpolator:interpolator:1.0.0 (*)
+| +--- androidx.core:core:1.1.0 -> 1.13.1 (*)
+| +--- androidx.annotation:annotation:1.1.0 -> 1.9.1 (*)
+| \--- com.google.guava:listenablefuture:1.0 -> 9999.0-empty-to-avoid-conflict-with-guava
++--- com.microsoft.identity.client:msal:6.0.1
+| +--- com.microsoft.identity:common:21.1.0
+| | +--- com.microsoft.identity:common4j:21.1.0
+| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*)
+| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 -> 1.10.2 (*)
+| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 -> 2.9.0 (*)
+| | +--- androidx.datastore:datastore-preferences:1.0.0 -> 1.1.4 (*)
+| | +--- org.apache.httpcomponents.core5:httpcore5:5.3
+| | +--- com.nimbusds:nimbus-jose-jwt:9.37.3
+| | | \--- com.github.stephenc.jcip:jcip-annotations:1.0-1
+| | +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.0 (*)
+| | +--- com.google.code.gson:gson:2.8.9 -> 2.12.1
+| | +--- com.squareup.moshi:moshi:1.14.0
+| | | +--- com.squareup.okio:okio:2.10.0 -> 3.9.0 (*)
+| | | \--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0 -> 2.1.20 (*)
+| | +--- androidx.browser:browser:1.0.0 -> 1.3.0 (*)
+| | +--- androidx.constraintlayout:constraintlayout:1.1.3 -> 2.2.1 (*)
+| | +--- com.yubico.yubikit:android:2.5.0
+| | | +--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | | \--- com.yubico.yubikit:core:2.5.0
+| | | \--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | +--- com.yubico.yubikit:piv:2.5.0
+| | | +--- org.slf4j:slf4j-api:2.0.9 -> 2.0.16
+| | | \--- com.yubico.yubikit:core:2.5.0 (*)
+| | +--- androidx.credentials:credentials:1.2.2
+| | | +--- androidx.annotation:annotation:1.5.0 -> 1.9.1 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.20 (*)
+| | | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1 -> 1.10.2 (*)
+| | | \--- androidx.credentials:credentials-play-services-auth:1.2.2 (c)
+| | +--- androidx.credentials:credentials-play-services-auth:1.2.2
+| | | +--- androidx.credentials:credentials:1.2.2 (*)
+| | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.8.22 -> 2.1.20 (*)
+| | | \--- androidx.credentials:credentials:1.2.2 (c)
+| | +--- io.opentelemetry:opentelemetry-api:1.18.0
+| | | \--- io.opentelemetry:opentelemetry-context:1.18.0
+| | \--- androidx.fragment:fragment:1.3.2 -> 1.8.6 (*)
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*)
+| +--- androidx.appcompat:appcompat:1.1.0 -> 1.7.0 (*)
+| +--- androidx.browser:browser:1.0.0 -> 1.3.0 (*)
+| +--- com.google.code.gson:gson:2.8.9 -> 2.12.1
+| +--- com.nimbusds:nimbus-jose-jwt:9.37.3 (*)
+| +--- org.apache.httpcomponents.core5:httpcore5:5.3
+| +--- androidx.constraintlayout:constraintlayout:1.1.3 -> 2.2.1 (*)
+| +--- org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4 -> 1.10.2 (*)
+| \--- io.opentelemetry:opentelemetry-api:1.18.0 (*)
++--- org.osmdroid:osmdroid-android:6.1.20 ++--- org.osmdroid:osmdroid-android:6.1.20
++--- androidx.recyclerview:recyclerview:1.4.0 (*) ++--- androidx.recyclerview:recyclerview:1.4.0 (*)
++--- androidx.compose:compose-bom:2025.04.00 (*) ++--- androidx.compose:compose-bom:2025.04.00 (*)

Loading…
Cancel
Save