diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c79a2fd3..9ed26cc07 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -229,6 +229,9 @@ dependencies { implementation(libs.etebase) implementation(libs.colorpicker) implementation(libs.appauth) + implementation(libs.microsoft.authentication) { + exclude("com.microsoft.device.display", "display-mask") + } implementation(libs.osmdroid) implementation(libs.androidx.recyclerview) diff --git a/app/src/debug/res/raw/microsoft_config.json b/app/src/debug/res/raw/microsoft_config.json new file mode 100644 index 000000000..3eea43cf9 --- /dev/null +++ b/app/src/debug/res/raw/microsoft_config.json @@ -0,0 +1,20 @@ +{ + "client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901", + "authorization_user_agent" : "DEFAULT", + "redirect_uri" : "msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D", + "account_mode" : "MULTIPLE", + "authorities" : [ + { + "type": "AAD", + "audience": { + "type": "AzureADandPersonalMicrosoftAccount", + "tenant_id": "common" + } + } + ], + "logging": { + "level": "verbose", + "logcat_enabled": true, + "pii_enabled": true + } +} \ No newline at end of file diff --git a/app/src/debug/res/values/keys.xml b/app/src/debug/res/values/keys.xml index 2cd776335..1e5efd11f 100644 --- a/app/src/debug/res/values/keys.xml +++ b/app/src/debug/res/values/keys.xml @@ -15,4 +15,5 @@ Restart app Clear hints com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf + /8wnYBRqh5nnQgFzbIXfxXSs41xE= \ No newline at end of file diff --git a/app/src/googleplayRelease/res/raw/microsoft_config.json b/app/src/googleplayRelease/res/raw/microsoft_config.json new file mode 100644 index 000000000..500845d11 --- /dev/null +++ b/app/src/googleplayRelease/res/raw/microsoft_config.json @@ -0,0 +1,20 @@ +{ + "client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901", + "authorization_user_agent" : "DEFAULT", + "redirect_uri" : "msauth://org.tasks/sEe08kX5nGJi4miFX3VkNXICC%2FY%3D", + "account_mode" : "MULTIPLE", + "authorities" : [ + { + "type": "AAD", + "audience": { + "type": "AzureADandPersonalMicrosoftAccount", + "tenant_id": "common" + } + } + ], + "logging": { + "level": "info", + "logcat_enabled": true, + "pii_enabled": false + } +} \ No newline at end of file diff --git a/app/src/googleplayRelease/res/values/keys.xml b/app/src/googleplayRelease/res/values/keys.xml index ee090a016..0868853d4 100644 --- a/app/src/googleplayRelease/res/values/keys.xml +++ b/app/src/googleplayRelease/res/values/keys.xml @@ -1,4 +1,5 @@ com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif + /sEe08kX5nGJi4miFX3VkNXICC/Y= \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c96c67637..96871d96c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -178,14 +178,6 @@ - - - - - - - - = 3.0. DeX Dual Mode support --> + + + + + + + + + diff --git a/app/src/main/java/org/tasks/auth/IdentityProvider.kt b/app/src/main/java/org/tasks/auth/IdentityProvider.kt deleted file mode 100644 index 2c2e05e6b..000000000 --- a/app/src/main/java/org/tasks/auth/IdentityProvider.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.tasks.auth - -import android.net.Uri -import androidx.core.net.toUri - -data class IdentityProvider( - val name: String, - val discoveryEndpoint: Uri, - val clientId: String, - val redirectUri: Uri, - val scope: String -) { - 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/main/java/org/tasks/auth/MicrosoftAuthenticationActivity.kt b/app/src/main/java/org/tasks/auth/MicrosoftAuthenticationActivity.kt deleted file mode 100644 index 5e93a9e2e..000000000 --- a/app/src/main/java/org/tasks/auth/MicrosoftAuthenticationActivity.kt +++ /dev/null @@ -1,152 +0,0 @@ -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.preferences.fragments.TasksAccountViewModel.Companion.getStringOrNull -import org.tasks.security.KeyStoreEncryption -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 - - 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 - ) - } - 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/main/java/org/tasks/http/HttpClientFactory.kt b/app/src/main/java/org/tasks/http/HttpClientFactory.kt index 7c60c391a..0f696738c 100644 --- a/app/src/main/java/org/tasks/http/HttpClientFactory.kt +++ b/app/src/main/java/org/tasks/http/HttpClientFactory.kt @@ -3,6 +3,8 @@ 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 @@ -19,16 +21,16 @@ import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import net.openid.appauth.AuthState 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.requestTokenRefresh +import org.tasks.sync.microsoft.MicrosoftSignInViewModel import timber.log.Timber import javax.inject.Inject import javax.net.ssl.SSLContext @@ -85,18 +87,28 @@ class HttpClientFactory @Inject constructor( } suspend fun getMicrosoftService(account: CaldavAccount): MicrosoftService { - 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") + 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 client = HttpClient(Android) { expectSuccess = true @@ -109,7 +121,7 @@ class HttpClientFactory @Inject constructor( } defaultRequest { - header("Authorization", "Bearer ${authState.accessToken}") + header("Authorization", "Bearer ${result.accessToken}") } install(HttpTimeout) { diff --git a/app/src/main/java/org/tasks/http/HttpErrorHandler.kt b/app/src/main/java/org/tasks/http/HttpErrorHandler.kt index 3c19b0a1a..01b566e6c 100644 --- a/app/src/main/java/org/tasks/http/HttpErrorHandler.kt +++ b/app/src/main/java/org/tasks/http/HttpErrorHandler.kt @@ -1,18 +1,47 @@ package org.tasks.http import io.ktor.client.HttpClient +import io.ktor.client.call.body import io.ktor.client.plugins.HttpClientPlugin import io.ktor.client.plugins.HttpSend import io.ktor.client.plugins.plugin import io.ktor.client.statement.HttpResponse import io.ktor.http.isSuccess import io.ktor.util.AttributeKey +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import timber.log.Timber -open class NetworkException(cause: Throwable? = null) : Exception(cause) -class UnauthorizedException(cause: Throwable? = null) : NetworkException(cause) -class NotFoundException(cause: Throwable? = null) : NetworkException(cause) -class ServiceUnavailableException(cause: Throwable? = null) : NetworkException(cause) -class HttpException(val code: Int, override val message: String? = null) : NetworkException() +@Serializable +data class GraphErrorResponse( + val error: GraphError +) + +@Serializable +data class GraphError( + val code: String, + val message: String, + val innerError: GraphInnerError? = null +) { + fun isTokenError() = code in listOf( + "InvalidAuthenticationToken", + "AuthenticationError", + "UnknownError" + ) +} + +@Serializable +data class GraphInnerError( + val date: String, + @SerialName("request-id") val requestId: String, + @SerialName("client-request-id")val clientRequestId: String +) + +open class NetworkException(message: String? = null, cause: Throwable? = null) : Exception(message, cause) +class UnauthorizedException(message: String? = null, cause: Throwable? = null) : NetworkException(message, cause) +class NotFoundException(message: String? = null, cause: Throwable? = null) : NetworkException(message, cause) +class ServiceUnavailableException(message: String? = null, cause: Throwable? = null) : NetworkException(message, cause) +class HttpException(val code: Int, override val message: String? = null) : NetworkException(message) class HttpErrorHandler { class Config { @@ -32,11 +61,33 @@ class HttpErrorHandler { val originalCall = execute(request) if (!originalCall.response.status.isSuccess()) { - when (originalCall.response.status.value) { - 401 -> throw UnauthorizedException() - 404 -> throw NotFoundException() - in 500..599 -> throw ServiceUnavailableException() - else -> throw HttpException(originalCall.response.status.value) + val errorResponse = try { + originalCall.response.body() + } catch (e: Exception) { + Timber.e(e) + null + } + + val errorMessage = buildString { + append("HTTP ${originalCall.response.status.value}") + errorResponse?.error?.let { error -> + append(" - ${error.code}") + if (error.message.isNotBlank()) { + append(": ${error.message}") + } + error.innerError?.let { inner -> + append(" (Request ID: ${inner.requestId})") + } + } + } + + Timber.e(errorMessage) + + when { + errorResponse?.error?.isTokenError() == true -> throw UnauthorizedException(errorMessage) + originalCall.response.status.value == 404 -> throw NotFoundException(errorMessage) + originalCall.response.status.value in 500..599 -> throw ServiceUnavailableException(errorMessage) + else -> throw HttpException(originalCall.response.status.value, errorMessage) } } diff --git a/app/src/main/java/org/tasks/sync/microsoft/AppAuthExtensions.kt b/app/src/main/java/org/tasks/sync/microsoft/AppAuthExtensions.kt deleted file mode 100644 index c3d8a6ebc..000000000 --- a/app/src/main/java/org/tasks/sync/microsoft/AppAuthExtensions.kt +++ /dev/null @@ -1,39 +0,0 @@ -package org.tasks.sync.microsoft - -import android.content.Context -import net.openid.appauth.* -import org.tasks.auth.IdentityProvider -import kotlin.coroutines.suspendCoroutine - -suspend fun IdentityProvider.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()) - } - ) - } - } -} - -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/main/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt index 8994ead4a..24ddfc058 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSignInViewModel.kt @@ -1,74 +1,94 @@ 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 com.microsoft.identity.client.AcquireTokenParameters +import com.microsoft.identity.client.AuthenticationCallback +import com.microsoft.identity.client.IAuthenticationResult +import com.microsoft.identity.client.Prompt +import com.microsoft.identity.client.PublicClientApplication +import com.microsoft.identity.client.exception.MsalException import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers 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 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.extensions.Context.toast +import timber.log.Timber import javax.inject.Inject @HiltViewModel class MicrosoftSignInViewModel @Inject constructor( - private val debugConnectionBuilder: DebugConnectionBuilder, + private val caldavDao: CaldavDao, + private val firebase: Firebase, ) : 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() + viewModelScope.launch(Dispatchers.IO) { + val app = PublicClientApplication.createMultipleAccountPublicClientApplication( + activity, + R.raw.microsoft_config ) - val authorizationService = AuthorizationService( - activity, - AppAuthConfiguration.Builder() - .setBrowserMatcher(AnyBrowserMatcher.INSTANCE) - .setConnectionBuilder( - if (BuildConfig.DEBUG) { - debugConnectionBuilder - } else { - DefaultConnectionBuilder.INSTANCE + val parameters = AcquireTokenParameters.Builder() + .startAuthorizationFromActivity(activity) + .withScopes(scopes) + .withPrompt(Prompt.SELECT_ACCOUNT) + .withCallback(object : AuthenticationCallback { + override fun onSuccess(authenticationResult: IAuthenticationResult) { + val email = authenticationResult.account.claims?.get("preferred_username") as? String + if (email == null) { + Timber.e("No email found") + return } - ) - .build() - ) - authorizationService.performAuthorizationRequest( - authRequest, - PendingIntent.getActivity( - activity, - authRequest.hashCode(), - intent, - PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT - ), - authorizationService.createCustomTabsIntentBuilder() - .build() - ) + Timber.d("Successfully signed in") + viewModelScope.launch { + caldavDao + .getAccount(TYPE_MICROSOFT, email) + ?.let { + caldavDao.update( + it.copy(error = null) + ) + } + ?: caldavDao + .insert( + CaldavAccount( + uuid = UUIDHelper.newUUID(), + name = email, + username = email, + accountType = TYPE_MICROSOFT, + ) + ) + .also { + firebase.logEvent( + R.string.event_sync_add_account, + R.string.param_type to Constants.SYNC_TYPE_MICROSOFT + ) + } + } + } + + override fun onError(exception: MsalException?) { + Timber.e(exception) + activity.toast(exception?.message ?: exception?.javaClass?.simpleName ?: "Sign in failed") + } + + override fun onCancel() { + Timber.d("onCancel") + } + }) + .build() + + app.acquireToken(parameters) } } -} \ No newline at end of file + + companion object { + val scopes = listOf("https://graph.microsoft.com/.default") + } +} diff --git a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSynchronizer.kt b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSynchronizer.kt index 501c1ee73..d7cfa33ba 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSynchronizer.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftSynchronizer.kt @@ -64,10 +64,6 @@ class MicrosoftSynchronizer @Inject constructor( Timber.d("Synchronizing $account") Thread.currentThread().contextClassLoader = context.classLoader - if (isNullOrEmpty(account.password)) { - setError(account, ERROR_UNAUTHORIZED) - return - } try { synchronize(account) } catch (e: SocketTimeoutException) { @@ -290,7 +286,7 @@ class MicrosoftSynchronizer @Inject constructor( microsoft.paginateLists(nextPageToken) } } catch (e: Exception) { - val error = e.message ?: "Sync failed" + val error = e.message ?: e.javaClass.simpleName Timber.e(e) setError(account, error) return null diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 6a397e01a..c7a653666 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -1413,6 +1413,85 @@ +| +--- 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.0 ++| +--- com.microsoft.identity:common:21.0.0 ++| | +--- com.microsoft.identity:common4j:21.0.0 ++| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*) ++| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 -> 1.10.1 (*) ++| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 -> 2.8.7 (*) ++| | +--- 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.1 (*) ++| | | \--- 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 ++| | | | | | +--- androidx.collection:collection:1.0.0 -> 1.4.4 (*) ++| | | | | | +--- androidx.core:core:1.2.0 -> 1.13.1 (*) ++| | | | | | +--- androidx.fragment:fragment:1.0.0 -> 1.8.6 (*) ++| | | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.2.0 ++| | | | | | | +--- androidx.collection:collection:1.0.0 -> 1.4.4 (*) ++| | | | | | | +--- androidx.core:core:1.2.0 -> 1.13.1 (*) ++| | | | | | | \--- androidx.fragment:fragment:1.0.0 -> 1.8.6 (*) ++| | | | | | \--- com.google.android.gms:play-services-tasks:18.0.1 ++| | | | | | \--- com.google.android.gms:play-services-basement:18.0.0 -> 18.2.0 (*) ++| | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.2.0 (*) ++| | | | | \--- com.google.android.gms:play-services-tasks:18.0.1 (*) ++| | | | +--- com.google.android.gms:play-services-auth-base:18.0.4 ++| | | | | +--- androidx.collection:collection:1.0.0 -> 1.4.4 (*) ++| | | | | +--- com.google.android.gms:play-services-base:18.0.1 (*) ++| | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.2.0 (*) ++| | | | | \--- com.google.android.gms:play-services-tasks:18.0.1 (*) ++| | | | +--- com.google.android.gms:play-services-base:18.0.1 (*) ++| | | | +--- com.google.android.gms:play-services-basement:18.2.0 (*) ++| | | | +--- com.google.android.gms:play-services-fido:20.0.1 -> 20.1.0 ++| | | | | +--- com.google.android.gms:play-services-base:18.0.1 (*) ++| | | | | +--- com.google.android.gms:play-services-basement:18.0.0 -> 18.2.0 (*) ++| | | | | \--- com.google.android.gms:play-services-tasks:18.0.1 (*) ++| | | | \--- com.google.android.gms:play-services-tasks:18.0.1 (*) ++| | | +--- 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.1 (*) ++| \--- 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 628e5d913..880262d2b 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -1766,6 +1766,76 @@ +| +--- 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.0 ++| +--- com.microsoft.identity:common:21.0.0 ++| | +--- com.microsoft.identity:common4j:21.0.0 ++| | +--- org.jetbrains.kotlin:kotlin-stdlib:1.7.21 -> 2.1.20 (*) ++| | +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4 -> 1.10.1 (*) ++| | +--- androidx.lifecycle:lifecycle-runtime-ktx:2.5.1 -> 2.8.7 (*) ++| | +--- 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.1 (*) ++| | | \--- 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.4 (*) ++| | | | | +--- 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.1 (*) ++| \--- 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44be88acb..719941448 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -159,6 +159,7 @@ markwon-strikethrough = { module = "io.noties.markwon:ext-strikethrough", versio markwon-tables = { module = "io.noties.markwon:ext-tables", version.ref = "markwon" } markwon-tasklist = { module = "io.noties.markwon:ext-tasklist", version.ref = "markwon" } material = { module = "com.google.android.material:material", version.ref = "material" } +microsoft-authentication = { module = "com.microsoft.identity.client:msal", version = "6.0.0" } mockito-android = { module = "org.mockito:mockito-android", version.ref = "mockito" } mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }