diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0822592c0..9913ed490 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -229,11 +229,6 @@ dependencies { implementation(libs.etebase) implementation(libs.colorpicker) 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.androidx.recyclerview) @@ -273,6 +268,9 @@ dependencies { googleplayImplementation(libs.horologist.datalayer.grpc) googleplayImplementation(libs.horologist.datalayer.core) googleplayImplementation(libs.play.services.wearable) + googleplayImplementation(libs.microsoft.authentication) { + exclude("com.microsoft.device.display", "display-mask") + } googleplayImplementation(projects.wearDatalayer) androidTestImplementation(libs.dagger.hilt.testing) diff --git a/app/src/generic/AndroidManifest.xml b/app/src/generic/AndroidManifest.xml index 2b327f7a7..e5499af2d 100644 --- a/app/src/generic/AndroidManifest.xml +++ b/app/src/generic/AndroidManifest.xml @@ -1,6 +1,24 @@ - + - + + + + + + + + + + + diff --git a/app/src/generic/java/org/tasks/auth/IdentityProvider.kt b/app/src/generic/java/org/tasks/auth/IdentityProvider.kt new file mode 100644 index 000000000..a85663843 --- /dev/null +++ b/app/src/generic/java/org/tasks/auth/IdentityProvider.kt @@ -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" + ) + } +} \ No newline at end of file diff --git a/app/src/generic/java/org/tasks/auth/MicrosoftAuthenticationActivity.kt b/app/src/generic/java/org/tasks/auth/MicrosoftAuthenticationActivity.kt new file mode 100644 index 000000000..365570e35 --- /dev/null +++ b/app/src/generic/java/org/tasks/auth/MicrosoftAuthenticationActivity.kt @@ -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" + } +} diff --git a/app/src/generic/java/org/tasks/sync/microsoft/AppAuthExtensions.kt b/app/src/generic/java/org/tasks/sync/microsoft/AppAuthExtensions.kt new file mode 100644 index 000000000..7b2eae97e --- /dev/null +++ b/app/src/generic/java/org/tasks/sync/microsoft/AppAuthExtensions.kt @@ -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 { + val authService = AuthorizationService(this) + return try { + suspendCoroutine { cont -> + authService.performTokenRequest(tokenRequest) { response, ex -> + cont.resumeWith(Result.success(Pair(response, ex))) + } + } + } finally { + authService.dispose() + } +} diff --git a/app/src/generic/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt b/app/src/generic/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt new file mode 100644 index 000000000..8994ead4a --- /dev/null +++ b/app/src/generic/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt @@ -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() + ) + } + } +} \ No newline at end of file diff --git a/app/src/generic/java/org/tasks/sync/microsoft/MicrosoftTokenProvider.kt b/app/src/generic/java/org/tasks/sync/microsoft/MicrosoftTokenProvider.kt new file mode 100644 index 000000000..662c0882c --- /dev/null +++ b/app/src/generic/java/org/tasks/sync/microsoft/MicrosoftTokenProvider.kt @@ -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!! + } +} \ No newline at end of file diff --git a/app/src/generic/res/values/keys.xml b/app/src/generic/res/values/keys.xml index 9f5dcc50f..9a0a47489 100644 --- a/app/src/generic/res/values/keys.xml +++ b/app/src/generic/res/values/keys.xml @@ -5,5 +5,4 @@ %s support@tasks.org - /xVwQTvk42gGm0o6zNvelaYloFcs= \ No newline at end of file diff --git a/app/src/genericRelease/res/raw/microsoft_config.json b/app/src/genericRelease/res/raw/microsoft_config.json deleted file mode 100644 index 51215e776..000000000 --- a/app/src/genericRelease/res/raw/microsoft_config.json +++ /dev/null @@ -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 - } -} diff --git a/app/src/googleplay/AndroidManifest.xml b/app/src/googleplay/AndroidManifest.xml index 582361fe3..deb06d4a4 100644 --- a/app/src/googleplay/AndroidManifest.xml +++ b/app/src/googleplay/AndroidManifest.xml @@ -53,6 +53,20 @@ + + + + + + + + + diff --git a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt b/app/src/googleplay/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt similarity index 100% rename from app/src/main/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt rename to app/src/googleplay/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt diff --git a/app/src/googleplay/java/org/tasks/sync/microsoft/MicrosoftTokenProvider.kt b/app/src/googleplay/java/org/tasks/sync/microsoft/MicrosoftTokenProvider.kt new file mode 100644 index 000000000..13f617c8a --- /dev/null +++ b/app/src/googleplay/java/org/tasks/sync/microsoft/MicrosoftTokenProvider.kt @@ -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 + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 954258193..b6e4de5cf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1013,20 +1013,6 @@ - - - - - - - - - diff --git a/app/src/main/java/org/tasks/http/HttpClientFactory.kt b/app/src/main/java/org/tasks/http/HttpClientFactory.kt index 46acf1aa8..a42fd6a34 100644 --- a/app/src/main/java/org/tasks/http/HttpClientFactory.kt +++ b/app/src/main/java/org/tasks/http/HttpClientFactory.kt @@ -3,8 +3,6 @@ package org.tasks.http import android.content.Context import at.bitfire.cert4android.CustomCertManager import at.bitfire.dav4jvm.BasicDigestAuthHandler -import com.microsoft.identity.client.AcquireTokenSilentParameters -import com.microsoft.identity.client.PublicClientApplication import dagger.hilt.android.qualifiers.ApplicationContext import io.ktor.client.HttpClient import io.ktor.client.engine.android.Android @@ -24,13 +22,12 @@ import kotlinx.serialization.json.Json import okhttp3.OkHttpClient import okhttp3.internal.tls.OkHostnameVerifier import org.tasks.BuildConfig -import org.tasks.R import org.tasks.caldav.TasksCookieJar import org.tasks.data.entity.CaldavAccount import org.tasks.extensions.Context.cookiePersistor import org.tasks.security.KeyStoreEncryption import org.tasks.sync.microsoft.MicrosoftService -import org.tasks.sync.microsoft.MicrosoftSignInViewModel +import org.tasks.sync.microsoft.MicrosoftTokenProvider import timber.log.Timber import javax.inject.Inject import javax.net.ssl.SSLContext @@ -38,6 +35,7 @@ import javax.net.ssl.SSLContext class HttpClientFactory @Inject constructor( @ApplicationContext private val context: Context, private val encryption: KeyStoreEncryption, + private val microsoftTokenProvider: MicrosoftTokenProvider, ) { suspend fun newClient(foreground: Boolean) = newClient( foreground = foreground, @@ -87,28 +85,7 @@ class HttpClientFactory @Inject constructor( } suspend fun getMicrosoftService(account: CaldavAccount): MicrosoftService = withContext(Dispatchers.IO) { - 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}") - } - + val token = microsoftTokenProvider.getToken(account) val client = HttpClient(Android) { expectSuccess = true @@ -121,7 +98,7 @@ class HttpClientFactory @Inject constructor( } defaultRequest { - header("Authorization", "Bearer ${result.accessToken}") + header("Authorization", "Bearer $token") } install(HttpTimeout) { diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 8c4309ed3..6fa4dfbe3 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -1460,51 +1460,6 @@ +| +--- 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 ++--- androidx.recyclerview:recyclerview:1.4.0 (*) ++--- androidx.compose:compose-bom:2025.04.00 diff --git a/deps_googleplay.txt b/deps_googleplay.txt index ea17955c0..1fb6b8ac6 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -806,6 +806,87 @@ +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.9.24 -> 2.1.20 (*) ++--- com.google.android.horologist:horologist-datalayer:0.6.23 (*) ++--- 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 +| +--- io.grpc:grpc-kotlin-stub:1.4.3 (*) +| +--- io.grpc:grpc-protobuf-lite:1.72.0 (*) @@ -1574,12 +1655,7 @@ +| | | +--- androidx.core:core:1.1.0 -> 1.13.1 (*) +| | | +--- androidx.customview:customview:1.0.0 -> 1.1.0 (*) +| | | \--- androidx.collection:collection:1.0.0 -> 1.4.5 (*) -+| | +--- 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.constraintlayout:constraintlayout:2.0.1 -> 2.2.1 (*) +| | +--- androidx.core:core:1.6.0 -> 1.13.1 (*) +| | +--- androidx.drawerlayout:drawerlayout:1.1.1 (*) +| | +--- androidx.dynamicanimation:dynamicanimation:1.0.0 @@ -1806,58 +1882,7 @@ ++--- net.openid:appauth:0.11.1 +| +--- androidx.annotation:annotation:1.2.0 -> 1.9.1 (*) +| +--- androidx.appcompat:appcompat:1.3.0 -> 1.7.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 (*) ++| \--- androidx.browser:browser:1.3.0 (*) ++--- org.osmdroid:osmdroid-android:6.1.20 ++--- androidx.recyclerview:recyclerview:1.4.0 (*) ++--- androidx.compose:compose-bom:2025.04.00 (*)