Replace Google Sign In with App Auth

pull/1226/head
Alex Baker 5 years ago
parent 47189e0466
commit 26a4b16235

@ -93,6 +93,10 @@ android {
resValue("string", "mapbox_key", tasks_mapbox_key_debug ?: "")
resValue("string", "google_key", tasks_google_key_debug ?: "")
isTestCoverageEnabled = project.hasProperty("coverage")
setManifestPlaceholders(mapOf(
"appAuthRedirectScheme" to "com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf"
))
}
getByName("release") {
val tasks_mapbox_key: String? by project
@ -102,6 +106,10 @@ android {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard.pro")
signingConfig = signingConfigs.getByName("release")
setManifestPlaceholders(mapOf(
"appAuthRedirectScheme" to "com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif"
))
}
}
@ -202,6 +210,8 @@ dependencies {
implementation("com.mapbox.mapboxsdk:mapbox-sdk-services:5.3.0")
implementation("com.etesync:journalmanager:1.1.1")
implementation("com.github.QuadFlask:colorpicker:0.0.15")
implementation("androidx.security:security-crypto:1.1.0-alpha02")
implementation("net.openid:appauth:0.7.1")
// https://github.com/mapbox/mapbox-gl-native-android/issues/316
genericImplementation("com.mapbox.mapboxsdk:mapbox-android-sdk:7.4.1")
@ -216,7 +226,6 @@ dependencies {
googleplayImplementation("com.google.android.gms:play-services-maps:17.0.0")
googleplayImplementation("com.google.android.libraries.places:places:2.4.0")
googleplayImplementation("com.android.billingclient:billing:1.2.2")
googleplayImplementation("com.google.android.gms:play-services-auth:19.0.0")
androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.hilt}")
kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}")

@ -823,3 +823,69 @@
name: commonmark
copyrightHolder: Atlassian and others
license: BSD 2-Clause
- artifact: net.openid:appauth:+
name: appauth
copyrightHolder: The AppAuth for Android Authors
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://github.com/openid/AppAuth-Android
- artifact: androidx.browser:browser:+
name: browser
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.legacy:legacy-support-core-ui:+
name: legacy-support-core-ui
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.legacy:legacy-support-core-utils:+
name: legacy-support-core-utils
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.slidingpanelayout:slidingpanelayout:+
name: slidingpanelayout
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.asynclayoutinflater:asynclayoutinflater:+
name: asynclayoutinflater
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.documentfile:documentfile:+
name: documentfile
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.localbroadcastmanager:localbroadcastmanager:+
name: localbroadcastmanager
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.print:print:+
name: print
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.security:security-crypto:+
name: security-crypto
copyrightHolder: Android Open Source Project
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/security#1.1.0-alpha02
- artifact: com.google.crypto.tink:tink-android:+
name: tink-android
copyrightHolder: Google Inc.
license: Apache License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://github.com/google/tink

@ -0,0 +1,11 @@
{
"client_id": "1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf.apps.googleusercontent.com",
"redirect_uri": "com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf:/oauth2redirect",
"authorization_scope": "openid email profile",
"discovery_uri": "https://accounts.google.com/.well-known/openid-configuration",
"authorization_endpoint_uri": "",
"token_endpoint_uri": "",
"registration_endpoint_uri": "",
"user_info_endpoint_uri": "",
"https_required": true
}

@ -2,7 +2,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" tools:ignore="PrivateResource">Tasks Debug</string>
<string name="tasks_caldav_url">https://192.168.1.120:8443</string>
<string name="google_sign_in" tools:ignore="TypographyDashes">1006257750459-3jt0e32kbqgug7hkluqe26d5mbno92no.apps.googleusercontent.com</string>
<string name="debug_strict_mode_thread">Strict mode - Thread</string>
<string name="debug_strict_mode_vm">Strict mode - VM</string>
<string name="debug_leakcanary">LeakCanary</string>

@ -1,17 +1,13 @@
package org.tasks.gtasks
import android.app.Activity
import android.content.Intent
import com.todoroo.astrid.activity.MainActivity
import io.reactivex.disposables.Disposable
import io.reactivex.disposables.Disposables
import org.tasks.auth.OauthSignIn
import javax.inject.Inject
@Suppress("UNUSED_PARAMETER")
class PlayServices @Inject constructor() {
val signInIntent: Intent? = null
val isPlayServicesAvailable: Boolean
get() = false
@ -27,8 +23,4 @@ class PlayServices @Inject constructor() {
fun check(mainActivity: MainActivity?): Disposable {
return Disposables.empty()
}
fun getSignedInAccount(): OauthSignIn? = null
fun signInFromIntent(data: Intent?): OauthSignIn? = null
}

@ -15,10 +15,6 @@
<service android:name=".location.GeofenceTransitionsIntentService"/>
<activity
android:name=".auth.SignInActivity"
android:theme="@style/TranslucentDialog" />
</application>
</manifest>

@ -1,16 +0,0 @@
package org.tasks.auth
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
class GoogleSignInAccount(
private val account: GoogleSignInAccount
) : OauthSignIn {
override val id: String?
get() = account.id
override val idToken: String?
get() = account.idToken
override val email: String?
get() = account.email
}

@ -1,77 +0,0 @@
package org.tasks.auth
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.exception.HttpException
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG
import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog
import org.tasks.caldav.CaldavClientProvider
import org.tasks.data.CaldavAccount
import org.tasks.gtasks.PlayServices
import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.ui.Toaster
import javax.inject.Inject
@AndroidEntryPoint
class SignInActivity : InjectingAppCompatActivity() {
@Inject lateinit var toaster: Toaster
@Inject lateinit var provider: CaldavClientProvider
@Inject lateinit var playServices: PlayServices
@Inject lateinit var firebase: Firebase
val viewModel: SignInViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.observe(this, this::onSignIn, this::onError)
lifecycleScope.launch {
playServices
.getSignedInAccount()
?.let { validate(it) }
?: startActivityForResult(playServices.signInIntent, RC_SIGN_IN)
}
}
private fun onSignIn(account: CaldavAccount?) {
account?.let { toaster.longToast(getString(R.string.logged_in, it.name)) }
finish()
}
private fun onError(t: Throwable) {
if (t is HttpException && t.code == 402) {
newPurchaseDialog(true).show(supportFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
} else {
firebase.reportException(t)
toaster.longToast(t.message)
finish()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == RC_SIGN_IN) {
playServices
.signInFromIntent(data)
?.let { validate(it) }
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun validate(account: OauthSignIn) = lifecycleScope.launch(Dispatchers.IO) {
viewModel.validate(account.id!!, account.email!!, account.idToken!!)
}
companion object {
private const val RC_SIGN_IN = 10000
}
}

@ -1,45 +0,0 @@
package org.tasks.auth
import android.content.Context
import androidx.hilt.lifecycle.ViewModelInject
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.caldav.CaldavClientProvider
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.data.CaldavDao
import org.tasks.ui.CompletableViewModel
class SignInViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context,
private val provider: CaldavClientProvider,
private val caldavDao: CaldavDao
) : CompletableViewModel<CaldavAccount?>() {
suspend fun validate(id: String, email: String, idToken: String) {
run {
val homeSet = provider
.forUrl(
"${context.getString(R.string.tasks_caldav_url)}/google_login",
token = idToken
)
.setForeground()
.homeSet(token = idToken)
val username = "google_$id"
caldavDao.getAccount(TYPE_TASKS, username)
?.apply {
error = null
caldavDao.update(this)
}
?: CaldavAccount().apply {
accountType = TYPE_TASKS
uuid = UUIDHelper.newUUID()
url = homeSet
this.username = username
name = email
caldavDao.insert(this)
}
}
}
}

@ -2,21 +2,11 @@ package org.tasks.gtasks
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.widget.Toast
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.Tasks
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.R
import org.tasks.auth.GoogleSignInAccount
import org.tasks.auth.OauthSignIn
import org.tasks.data.LocationDao
import org.tasks.preferences.Preferences
import timber.log.Timber
@ -63,40 +53,6 @@ class PlayServices @Inject constructor(
}
}
suspend fun getSignedInAccount(): OauthSignIn? {
return withContext(Dispatchers.IO) {
try {
Tasks
.await(client.silentSignIn())
?.let { GoogleSignInAccount(it) }
} catch (e: Exception) {
Timber.e(e)
null
}
}
}
fun signInFromIntent(data: Intent?): OauthSignIn? = try {
GoogleSignIn
.getSignedInAccountFromIntent(data)
.getResult(ApiException::class.java)
?.let { GoogleSignInAccount(it) }
} catch (e: ApiException) {
Timber.e(e)
null
}
val signInIntent: Intent
get() = client.signInIntent
private val client: GoogleSignInClient
get() = GoogleSignIn.getClient(
context,
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.requestIdToken(context.getString(R.string.google_sign_in))
.build())
private val status: String
get() = GoogleApiAvailability.getInstance().getErrorString(result)

@ -134,6 +134,19 @@
android:name="android.hardware.touchscreen"
android:required="false"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.APP_BROWSER" />
<data android:scheme="https" />
</intent>
</queries>
<application
android:allowBackup="true"
android:backupAgent="org.tasks.backup.TasksBackupAgent"
@ -151,6 +164,12 @@
android:preserveLegacyExternalStorage="true"
android:hasFragileUserData="true">
<activity
android:name=".auth.SignInActivity"
android:theme="@style/TranslucentDialog"
android:windowSoftInputMode="stateHidden" >
</activity>
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="false"/>

@ -1962,6 +1962,160 @@
"license": "BSD 2-Clause",
"normalizedLicense": "bsd_2_clauses",
"libraryName": "commonmark"
},
{
"artifactId": {
"name": "appauth",
"group": "net.openid",
"version": "+"
},
"copyrightHolder": "The AppAuth for Android Authors",
"copyrightStatement": "Copyright &copy; The AppAuth for Android Authors. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://github.com/openid/AppAuth-Android",
"libraryName": "appauth"
},
{
"artifactId": {
"name": "browser",
"group": "androidx.browser",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "browser"
},
{
"artifactId": {
"name": "legacy-support-core-ui",
"group": "androidx.legacy",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "legacy-support-core-ui"
},
{
"artifactId": {
"name": "legacy-support-core-utils",
"group": "androidx.legacy",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "legacy-support-core-utils"
},
{
"artifactId": {
"name": "slidingpanelayout",
"group": "androidx.slidingpanelayout",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "slidingpanelayout"
},
{
"artifactId": {
"name": "asynclayoutinflater",
"group": "androidx.asynclayoutinflater",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "asynclayoutinflater"
},
{
"artifactId": {
"name": "documentfile",
"group": "androidx.documentfile",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "documentfile"
},
{
"artifactId": {
"name": "localbroadcastmanager",
"group": "androidx.localbroadcastmanager",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "localbroadcastmanager"
},
{
"artifactId": {
"name": "print",
"group": "androidx.print",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://developer.android.com/tools/extras/support-library.html",
"libraryName": "print"
},
{
"artifactId": {
"name": "security-crypto",
"group": "androidx.security",
"version": "+"
},
"copyrightHolder": "Android Open Source Project",
"copyrightStatement": "Copyright &copy; Android Open Source Project. All rights reserved.",
"license": "The Apache Software License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "https://developer.android.com/jetpack/androidx/releases/security#1.1.0-alpha02",
"libraryName": "security-crypto"
},
{
"artifactId": {
"name": "tink-android",
"group": "com.google.crypto.tink",
"version": "+"
},
"copyrightHolder": "Google Inc.",
"copyrightStatement": "Copyright &copy; Google Inc. All rights reserved.",
"license": "Apache License, Version 2.0",
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt",
"normalizedLicense": "apache2",
"url": "http://github.com/google/tink",
"libraryName": "tink-android"
}
]
}

@ -0,0 +1,125 @@
/*
* Copyright 2017 The AppAuth for Android Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tasks.auth
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import dagger.hilt.android.qualifiers.ApplicationContext
import net.openid.appauth.*
import org.json.JSONException
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.ReentrantLock
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AuthStateManager @Inject constructor(@ApplicationContext private val context: Context) {
private val prefs = EncryptedSharedPreferences.create(
context,
STORE_NAME,
MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
private val prefsLock = ReentrantLock()
private val currentAuthState = AtomicReference<AuthState>()
val current: AuthState
get() {
if (currentAuthState.get() != null) {
return currentAuthState.get()
}
val state = readState()
return if (currentAuthState.compareAndSet(null, state)) {
state
} else {
currentAuthState.get()
}
}
fun replace(state: AuthState): AuthState {
writeState(state)
currentAuthState.set(state)
return state
}
fun updateAfterAuthorization(
response: AuthorizationResponse?,
ex: AuthorizationException?
): AuthState {
val current = current
current.update(response, ex)
return replace(current)
}
fun updateAfterTokenResponse(
response: TokenResponse?,
ex: AuthorizationException?
): AuthState {
val current = current
current.update(response, ex)
return replace(current)
}
fun updateAfterRegistration(
response: RegistrationResponse?,
ex: AuthorizationException?
): AuthState {
val current = current
if (ex != null) {
return current
}
current.update(response)
return replace(current)
}
private fun readState(): AuthState {
prefsLock.lock()
return try {
val currentState = prefs.getString(KEY_STATE, null)
?: return AuthState()
try {
AuthState.jsonDeserialize(currentState)
} catch (ex: JSONException) {
Timber.w("Failed to deserialize stored auth state - discarding")
AuthState()
}
} finally {
prefsLock.unlock()
}
}
private fun writeState(state: AuthState?) {
prefsLock.lock()
try {
val editor = prefs.edit()
if (state == null) {
editor.remove(KEY_STATE)
} else {
editor.putString(KEY_STATE, state.jsonSerializeString())
}
check(editor.commit()) { "Failed to write state to shared prefs" }
} finally {
prefsLock.unlock()
}
}
companion object {
private const val STORE_NAME = "AuthState"
private const val KEY_STATE = "state"
}
}

@ -0,0 +1,91 @@
package org.tasks.auth
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.openid.appauth.*
import net.openid.appauth.AuthorizationService
import net.openid.appauth.browser.AnyBrowserMatcher
import javax.inject.Inject
import kotlin.coroutines.suspendCoroutine
class AuthorizationService @Inject constructor(
@ApplicationContext context: Context,
private val authStateManager: AuthStateManager,
configuration: Configuration
) {
private val authorizationService = AuthorizationService(
context,
AppAuthConfiguration.Builder()
.setBrowserMatcher(AnyBrowserMatcher.INSTANCE)
.setConnectionBuilder(configuration.connectionBuilder)
.build())
fun dispose() {
authorizationService.dispose()
}
fun getAuthorizationRequestIntent(
request: AuthorizationRequest,
customTabsIntent: CustomTabsIntent
): Intent {
return authorizationService.getAuthorizationRequestIntent(request, customTabsIntent)
}
fun createCustomTabsIntent(uri: Uri, color: Int): CustomTabsIntent {
return authorizationService
.createCustomTabsIntentBuilder(uri)
.setToolbarColor(color)
.build()
}
fun performRegistrationRequest(
request: RegistrationRequest,
callback: (RegistrationResponse?, AuthorizationException?) -> Unit
) {
authorizationService.performRegistrationRequest(request, callback)
}
suspend fun performTokenRequest(request: TokenRequest, clientAuthentication: ClientAuthentication): TokenResponse? {
return withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
authorizationService.performTokenRequest(request, clientAuthentication) { response, exception ->
if (exception != null) {
cont.resumeWith(Result.failure(exception))
} else {
cont.resumeWith(Result.success(response))
}
}
}
}
}
suspend fun getFreshToken(): String? = withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
authStateManager
.current
.performActionWithFreshTokens(authorizationService) { _, idToken, exception ->
if (exception == null) {
cont.resumeWith(Result.success(idToken))
} else {
cont.resumeWith(Result.failure(exception))
}
}
}
}
fun signOut() {
// discard the authorization and token state, but retain the configuration and
// dynamic client registration (if applicable), to save from retrieving them again.
val currentState = authStateManager.current
val clearedState = AuthState(currentState.authorizationServiceConfiguration!!)
if (currentState.lastRegistrationResponse != null) {
clearedState.update(currentState.lastRegistrationResponse)
}
authStateManager.replace(clearedState)
}
}

@ -0,0 +1,221 @@
/*
* Copyright 2016 The AppAuth for Android Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tasks.auth
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.text.TextUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import net.openid.appauth.connectivity.ConnectionBuilder
import net.openid.appauth.connectivity.DefaultConnectionBuilder
import okio.Buffer
import okio.buffer
import okio.source
import org.json.JSONException
import org.json.JSONObject
import org.tasks.R
import java.io.IOException
import java.nio.charset.StandardCharsets
import javax.inject.Inject
import javax.inject.Singleton
/**
* Reads and validates the demo app configuration from `res/raw/auth_config.json`. Configuration
* changes are detected by comparing the hash of the last known configuration to the read
* configuration. When a configuration change is detected, the app state is reset.
*/
@Singleton
class Configuration @Inject constructor(
@ApplicationContext private val context: Context
) {
private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private var configJson: JSONObject? = null
private var configHash: String? = null
/**
* Returns a description of the configuration error, if the configuration is invalid.
*/
var configurationError: String? = null
var clientId: String? = null
private set
private var mScope: String? = null
private var mRedirectUri: Uri? = null
var discoveryUri: Uri? = null
private set
var authEndpointUri: Uri? = null
private set
var tokenEndpointUri: Uri? = null
private set
var registrationEndpointUri: Uri? = null
private set
var userInfoEndpointUri: Uri? = null
private set
private var isHttpsRequired = false
private set
/**
* Indicates whether the configuration has changed from the last known valid state.
*/
fun hasConfigurationChanged(): Boolean {
val lastHash = lastKnownConfigHash
return configHash != lastHash
}
/**
* Indicates whether the current configuration is valid.
*/
val isValid: Boolean
get() = configurationError == null
/**
* Indicates that the current configuration should be accepted as the "last known valid"
* configuration.
*/
fun acceptConfiguration() {
prefs.edit().putString(KEY_LAST_HASH, configHash).apply()
}
val scope: String
get() = mScope!!
val redirectUri: Uri
get() = mRedirectUri!!
val connectionBuilder: ConnectionBuilder = DefaultConnectionBuilder.INSTANCE
private val lastKnownConfigHash: String?
get() = prefs.getString(KEY_LAST_HASH, null)
@Throws(InvalidConfigurationException::class)
private fun readConfiguration() {
val configSource = context.resources.openRawResource(R.raw.auth_config).source().buffer()
val configData = Buffer()
configJson = try {
configSource.readAll(configData)
JSONObject(configData.readString(StandardCharsets.UTF_8))
} catch (ex: IOException) {
throw InvalidConfigurationException(
"Failed to read configuration: " + ex.message)
} catch (ex: JSONException) {
throw InvalidConfigurationException(
"Unable to parse configuration: " + ex.message)
}
configHash = configData.sha256().base64()
clientId = getConfigString("client_id")
mScope = getRequiredConfigString("authorization_scope")
mRedirectUri = getRequiredConfigUri("redirect_uri")
if (!isRedirectUriRegistered) {
throw InvalidConfigurationException(
"redirect_uri is not handled by any activity in this app! "
+ "Ensure that the appAuthRedirectScheme in your build.gradle file "
+ "is correctly configured, or that an appropriate intent filter "
+ "exists in your app manifest.")
}
if (getConfigString("discovery_uri") == null) {
authEndpointUri = getRequiredConfigWebUri("authorization_endpoint_uri")
tokenEndpointUri = getRequiredConfigWebUri("token_endpoint_uri")
userInfoEndpointUri = getRequiredConfigWebUri("user_info_endpoint_uri")
if (clientId == null) {
registrationEndpointUri = getRequiredConfigWebUri("registration_endpoint_uri")
}
} else {
discoveryUri = getRequiredConfigWebUri("discovery_uri")
}
isHttpsRequired = configJson!!.optBoolean("https_required", true)
}
private fun getConfigString(propName: String?): String? {
var value = configJson!!.optString(propName) ?: return null
value = value.trim { it <= ' ' }
return if (TextUtils.isEmpty(value)) {
null
} else value
}
@Throws(InvalidConfigurationException::class)
private fun getRequiredConfigString(propName: String): String {
return getConfigString(propName)
?: throw InvalidConfigurationException(
"$propName is required but not specified in the configuration")
}
@Throws(InvalidConfigurationException::class)
fun getRequiredConfigUri(propName: String): Uri {
val uriStr = getRequiredConfigString(propName)
val uri: Uri
uri = try {
Uri.parse(uriStr)
} catch (ex: Throwable) {
throw InvalidConfigurationException("$propName could not be parsed", ex)
}
if (!uri.isHierarchical || !uri.isAbsolute) {
throw InvalidConfigurationException(
"$propName must be hierarchical and absolute")
}
if (!TextUtils.isEmpty(uri.encodedUserInfo)) {
throw InvalidConfigurationException("$propName must not have user info")
}
if (!TextUtils.isEmpty(uri.encodedQuery)) {
throw InvalidConfigurationException("$propName must not have query parameters")
}
if (!TextUtils.isEmpty(uri.encodedFragment)) {
throw InvalidConfigurationException("$propName must not have a fragment")
}
return uri
}
@Throws(InvalidConfigurationException::class)
fun getRequiredConfigWebUri(propName: String): Uri {
val uri = getRequiredConfigUri(propName)
val scheme = uri.scheme
if (TextUtils.isEmpty(scheme) || !("http" == scheme || "https" == scheme)) {
throw InvalidConfigurationException(
"$propName must have an http or https scheme")
}
return uri
}
// ensure that the redirect URI declared in the configuration is handled by some activity
// in the app, by querying the package manager speculatively
private val isRedirectUriRegistered: Boolean
get() {
// ensure that the redirect URI declared in the configuration is handled by some activity
// in the app, by querying the package manager speculatively
val redirectIntent = Intent()
redirectIntent.setPackage(context.packageName)
redirectIntent.action = Intent.ACTION_VIEW
redirectIntent.addCategory(Intent.CATEGORY_BROWSABLE)
redirectIntent.data = mRedirectUri
return !context.packageManager.queryIntentActivities(redirectIntent, 0).isEmpty()
}
class InvalidConfigurationException : Exception {
internal constructor(reason: String?) : super(reason) {}
internal constructor(reason: String?, cause: Throwable?) : super(reason, cause) {}
}
companion object {
private const val PREFS_NAME = "config"
private const val KEY_LAST_HASH = "lastHash"
}
init {
try {
readConfiguration()
} catch (ex: InvalidConfigurationException) {
configurationError = ex.message
}
}
}

@ -1,7 +0,0 @@
package org.tasks.auth
interface OauthSignIn {
val idToken: String?
val email: String?
val id: String?
}

@ -0,0 +1,286 @@
/*
* Copyright 2015 The AppAuth for Android Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.tasks.auth
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.annotation.AnyThread
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import net.openid.appauth.*
import org.tasks.R
import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.themes.ThemeColor
import timber.log.Timber
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors.newSingleThreadExecutor
import java.util.concurrent.atomic.AtomicReference
import javax.inject.Inject
/**
* Demonstrates the usage of the AppAuth to authorize a user with an OAuth2 / OpenID Connect
* provider. Based on the configuration provided in `res/raw/auth_config.json`, the code
* contained here will:
*
* - Retrieve an OpenID Connect discovery document for the provider, or use a local static
* configuration.
* - Utilize dynamic client registration, if no static client id is specified.
* - Initiate the authorization request using the built-in heuristics or a user-selected browser.
*
* _NOTE_: From a clean checkout of this project, the authorization service is not configured.
* Edit `res/values/auth_config.xml` to provide the required configuration properties. See the
* README.md in the app/ directory for configuration instructions, and the adjacent IDP-specific
* instructions.
*/
@AndroidEntryPoint
class SignInActivity : InjectingAppCompatActivity() {
@Inject lateinit var authService: AuthorizationService
@Inject lateinit var authStateManager: AuthStateManager
@Inject lateinit var configuration: Configuration
@Inject lateinit var themeColor: ThemeColor
private val viewModel: SignInViewModel by viewModels()
private val mClientId = AtomicReference<String?>()
private val mAuthRequest = AtomicReference<AuthorizationRequest>()
private val mAuthIntent = AtomicReference<CustomTabsIntent>()
private var mAuthIntentLatch = CountDownLatch(1)
private val mExecutor: ExecutorService = newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (authStateManager.current.isAuthorized &&
!configuration.hasConfigurationChanged()) {
Timber.i("User is already authenticated, signing out")
authService.signOut()
}
if (!configuration.isValid) {
displayError(configuration.configurationError)
return
}
if (configuration.hasConfigurationChanged()) {
// discard any existing authorization state due to the change of configuration
Timber.i("Configuration change detected, discarding old state")
authStateManager.replace(AuthState())
configuration.acceptConfiguration()
}
mExecutor.submit { initializeAppAuth() }
}
override fun onStop() {
super.onStop()
mExecutor.shutdownNow()
}
override fun onDestroy() {
super.onDestroy()
authService.dispose()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == RC_AUTH) {
if (resultCode == RESULT_OK) {
lifecycleScope.launch {
viewModel.handleResult(data!!)
authStateManager.current.authorizationException?.let { e ->
displayError(e.message)
}
finish()
}
} else {
displayError(getString(R.string.authorization_cancelled))
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
@MainThread
fun startAuth() {
// WrongThread inference is incorrect for lambdas
// noinspection WrongThread
mExecutor.submit { doAuth() }
}
/**
* Initializes the authorization service configuration if necessary, either from the local
* static values or by retrieving an OpenID discovery document.
*/
@WorkerThread
private fun initializeAppAuth() {
Timber.i("Initializing AppAuth")
if (authStateManager.current.authorizationServiceConfiguration != null) {
// configuration is already created, skip to client initialization
Timber.i("auth config already established")
initializeClient()
return
}
// if we are not using discovery, build the authorization service configuration directly
// from the static configuration values.
if (configuration.discoveryUri == null) {
Timber.i("Creating auth config from res/raw/auth_config.json")
val config = AuthorizationServiceConfiguration(
configuration.authEndpointUri!!,
configuration.tokenEndpointUri!!,
configuration.registrationEndpointUri)
authStateManager.replace(AuthState(config))
initializeClient()
return
}
// WrongThread inference is incorrect for lambdas
// noinspection WrongThread
Timber.i("Retrieving OpenID discovery doc")
AuthorizationServiceConfiguration.fetchFromUrl(
configuration.discoveryUri!!, { config: AuthorizationServiceConfiguration?, ex: AuthorizationException? -> handleConfigurationRetrievalResult(config, ex) },
configuration.connectionBuilder)
}
@MainThread
private fun handleConfigurationRetrievalResult(
config: AuthorizationServiceConfiguration?,
ex: AuthorizationException?) {
if (config == null) {
Timber.i(ex, "Failed to retrieve discovery document")
displayError("Failed to retrieve discovery document: " + ex!!.message)
return
}
Timber.i("Discovery document retrieved")
authStateManager.replace(AuthState(config))
mExecutor.submit { initializeClient() }
}
/**
* Initiates a dynamic registration request if a client ID is not provided by the static
* configuration.
*/
@WorkerThread
private fun initializeClient() {
if (configuration.clientId != null) {
Timber.i("Using static client ID: %s", configuration.clientId)
// use a statically configured client ID
mClientId.set(configuration.clientId)
runOnUiThread { initializeAuthRequest() }
return
}
val lastResponse = authStateManager.current.lastRegistrationResponse
if (lastResponse != null) {
Timber.i("Using dynamic client ID: %s", lastResponse.clientId)
// already dynamically registered a client ID
mClientId.set(lastResponse.clientId)
runOnUiThread { initializeAuthRequest() }
return
}
// WrongThread inference is incorrect for lambdas
// noinspection WrongThread
Timber.i("Dynamically registering client")
val registrationRequest = RegistrationRequest.Builder(
authStateManager.current.authorizationServiceConfiguration!!, listOf(configuration.redirectUri))
.setTokenEndpointAuthenticationMethod(ClientSecretBasic.NAME)
.build()
authService.performRegistrationRequest(
registrationRequest) { response: RegistrationResponse?, ex: AuthorizationException? -> handleRegistrationResponse(response, ex) }
}
@MainThread
private fun handleRegistrationResponse(
response: RegistrationResponse?,
ex: AuthorizationException?) {
authStateManager.updateAfterRegistration(response, ex)
if (response == null) {
Timber.i(ex, "Failed to dynamically register client")
displayErrorLater("Failed to register client: " + ex!!.message)
return
}
Timber.i("Dynamically registered client: %s", response.clientId)
mClientId.set(response.clientId)
initializeAuthRequest()
return
}
/**
* Performs the authorization request, using the browser selected in the spinner,
* and a user-provided `login_hint` if available.
*/
@WorkerThread
private fun doAuth() {
try {
mAuthIntentLatch.await()
} catch (ex: InterruptedException) {
Timber.w("Interrupted while waiting for auth intent")
}
val intent = authService.getAuthorizationRequestIntent(
mAuthRequest.get(),
mAuthIntent.get())
startActivityForResult(intent, RC_AUTH)
}
@MainThread
private fun displayError(error: String?) {
Timber.e(error)
setResult(RESULT_CANCELED, Intent().putExtra(EXTRA_ERROR, error))
finish()
}
// WrongThread inference is incorrect in this case
@AnyThread
private fun displayErrorLater(error: String) {
runOnUiThread { displayError(error) }
}
@MainThread
private fun initializeAuthRequest() {
createAuthRequest()
warmUpBrowser()
startAuth()
}
private fun warmUpBrowser() {
mAuthIntentLatch = CountDownLatch(1)
mExecutor.execute {
Timber.i("Warming up browser instance for auth request")
mAuthIntent.set(authService.createCustomTabsIntent(
mAuthRequest.get().toUri(),
themeColor.primaryColor
))
mAuthIntentLatch.countDown()
}
}
private fun createAuthRequest() {
Timber.i("Creating auth request")
val authRequestBuilder = AuthorizationRequest.Builder(
authStateManager.current.authorizationServiceConfiguration!!,
mClientId.get()!!,
ResponseTypeValues.CODE,
configuration.redirectUri)
.setScope(configuration.scope)
mAuthRequest.set(authRequestBuilder.build())
}
companion object {
const val EXTRA_ERROR = "extra_error"
private const val RC_AUTH = 100
}
}

@ -0,0 +1,89 @@
package org.tasks.auth
import android.content.Context
import android.content.Intent
import android.util.Base64
import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.ViewModel
import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.ClientAuthentication.UnsupportedAuthenticationMethod
import org.json.JSONObject
import org.tasks.R
import org.tasks.caldav.CaldavClientProvider
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavDao
import timber.log.Timber
class SignInViewModel @ViewModelInject constructor(
@ApplicationContext private val context: Context,
private val authStateManager: AuthStateManager,
private val authorizationService: AuthorizationService,
private val provider: CaldavClientProvider,
private val caldavDao: CaldavDao
) : ViewModel() {
suspend fun handleResult(intent: Intent): CaldavAccount? {
val response = AuthorizationResponse.fromIntent(intent)
val ex = AuthorizationException.fromIntent(intent)
if (response != null || ex != null) {
authStateManager.updateAfterAuthorization(response, ex)
}
if (response?.authorizationCode != null) {
authStateManager.updateAfterAuthorization(response, ex)
exchangeAuthorizationCode(response)
}
val auth = authStateManager.current
if (!auth.isAuthorized) {
return null
}
val idToken = auth.idToken
val parts: List<String> = idToken!!.split(".")
val payloadJson = JSONObject(String(Base64.decode(parts[1], Base64.DEFAULT)))
val sub = payloadJson.getString("sub")
val username = "google_$sub"
val email = payloadJson.getString("email")
val homeSet = provider
.forUrl(
"${context.getString(R.string.tasks_caldav_url)}/google_login",
token = idToken
)
.setForeground()
.homeSet(token = idToken)
return caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username)
?.apply {
error = null
caldavDao.update(this)
}
?: CaldavAccount().apply {
accountType = CaldavAccount.TYPE_TASKS
uuid = UUIDHelper.newUUID()
url = homeSet
this.username = username
name = email
caldavDao.insert(this)
}
}
private suspend fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) {
val request = authorizationResponse.createTokenExchangeRequest()
val clientAuthentication = try {
authStateManager.current.clientAuthentication
} catch (ex: UnsupportedAuthenticationMethod) {
throw ex
}
try {
authorizationService.performTokenRequest(request, clientAuthentication)?.let {
authStateManager.updateAfterTokenResponse(it, null)
if (authStateManager.current.isAuthorized) {
Timber.d("Authorization successful")
}
}
} catch (e: AuthorizationException) {
authStateManager.updateAfterTokenResponse(null, e)
}
}
}

@ -9,10 +9,13 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import javax.inject.Inject
@AndroidEntryPoint
class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var authorizationService: AuthorizationService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -61,6 +64,17 @@ class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolba
override suspend fun updateAccount() = updateAccount(caldavAccount!!.url)
override suspend fun removeAccount() {
authorizationService.signOut()
super.removeAccount()
}
override fun onStop() {
super.onStop()
authorizationService.dispose()
}
override val helpUrl: String
get() = getString(R.string.help_url_sync)
}

@ -13,4 +13,8 @@ class AddCaldavAccountViewModel @ViewModelInject constructor(
.setForeground()
.homeSet(username, password) }
}
override fun onCleared() {
provider.dispose()
}
}

@ -12,9 +12,9 @@ import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.internal.tls.OkHostnameVerifier
import org.tasks.DebugNetworkInterceptor
import org.tasks.auth.AuthorizationService
import org.tasks.billing.Inventory
import org.tasks.data.CaldavAccount
import org.tasks.gtasks.PlayServices
import org.tasks.preferences.Preferences
import org.tasks.security.KeyStoreEncryption
import java.util.concurrent.TimeUnit
@ -26,8 +26,8 @@ class CaldavClientProvider @Inject constructor(
private val encryption: KeyStoreEncryption,
private val preferences: Preferences,
private val interceptor: DebugNetworkInterceptor,
private val playServices: PlayServices,
private val inventory: Inventory
private val inventory: Inventory,
private val authorizationService: AuthorizationService
) {
suspend fun forUrl(
url: String?,
@ -66,8 +66,8 @@ class CaldavClientProvider @Inject constructor(
token: String? = null
): Interceptor? {
return when {
account?.isTasksOrg == true -> playServices.getSignedInAccount()?.let {
TokenInterceptor(it.idToken!!, inventory)
account?.isTasksOrg == true -> authorizationService.getFreshToken()?.let {
TokenInterceptor(it, inventory)
}
username?.isNotBlank() == true && password?.isNotBlank() == true ->
BasicDigestAuthHandler(null, username, password)
@ -104,4 +104,8 @@ class CaldavClientProvider @Inject constructor(
return builder.build()
}
fun dispose() {
authorizationService.dispose()
}
}

@ -116,6 +116,8 @@ class CaldavSynchronizer @Inject constructor(
} catch (e: DavException) {
setError(account, e.message)
firebase.reportException(e)
} finally {
provider.dispose()
}
}

@ -10,4 +10,8 @@ class CreateCalendarViewModel @ViewModelInject constructor(
suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) {
run { provider.forAccount(account).makeCollection(name, color) }
}
override fun onCleared() {
provider.dispose()
}
}

@ -13,4 +13,8 @@ class DeleteCalendarViewModel @ViewModelInject constructor(
calendar.url?.let { provider.forAccount(account, it).deleteCollection() }
}
}
override fun onCleared() {
provider.dispose()
}
}

@ -9,4 +9,8 @@ class UpdateCaldavAccountViewModel @ViewModelInject constructor(
suspend fun updateCaldavAccount(url: String, username: String, password: String) {
run { provider.forUrl(url, username, password).homeSet(username, password) }
}
override fun onCleared() {
provider.dispose()
}
}

@ -13,4 +13,8 @@ class UpdateCalendarViewModel @ViewModelInject constructor(
calendar.url?.let { provider.forAccount(account, it).updateCollection(name, color) }
}
}
override fun onCleared() {
provider.dispose()
}
}

@ -9,10 +9,12 @@ import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.auth.SignInActivity
import org.tasks.jobs.WorkManager
import org.tasks.preferences.fragments.MainSettingsFragment
import org.tasks.preferences.fragments.Synchronization.Companion.REQUEST_CALDAV_SETTINGS
import org.tasks.preferences.fragments.Synchronization.Companion.REQUEST_GOOGLE_TASKS
import org.tasks.preferences.fragments.Synchronization.Companion.REQUEST_TASKS_ORG
import org.tasks.sync.SyncAdapters
import org.tasks.ui.Toaster
import javax.inject.Inject
@ -58,6 +60,13 @@ class MainPreferences : BasePreferences() {
} else {
data?.getStringExtra(GtasksLoginActivity.EXTRA_ERROR)?.let { toaster.longToast(it) }
}
} else if (requestCode == REQUEST_TASKS_ORG) {
if (resultCode == Activity.RESULT_OK) {
syncAdapters.sync(true)
workManager.updateBackgroundSync()
} else {
data?.getStringExtra(SignInActivity.EXTRA_ERROR)?.let { toaster.longToast(it) }
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}

@ -665,4 +665,5 @@ File %1$s contained %2$s.\n\n
<string name="current_subscription">Current subscription: %s</string>
<string name="purchases_updated">Purchases updated</string>
<string name="follow_reddit">Follow r/tasks</string>
<string name="authorization_cancelled">Authorization cancelled</string>
</resources>

@ -0,0 +1,11 @@
{
"client_id": "363426363175-jdrijf7hql9030klgjcjlpi6k5spviif.apps.googleusercontent.com",
"redirect_uri": "com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif:/oauth2redirect",
"authorization_scope": "openid email profile",
"discovery_uri": "https://accounts.google.com/.well-known/openid-configuration",
"authorization_endpoint_uri": "",
"token_endpoint_uri": "",
"registration_endpoint_uri": "",
"user_info_endpoint_uri": "",
"https_required": true
}

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<string name="app_name">Tasks</string>
<string name="tasks_caldav_url">https://caldav.tasks.org</string>
<string name="google_sign_in" tools:ignore="TypographyDashes">363426363175-op3tqa3qir2qkm7dtj6jr6mp4hudmsgs.apps.googleusercontent.com</string>
</resources>

@ -385,5 +385,33 @@
+| +--- org.apache.commons:commons-collections4:4.1
+| +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9
+| \--- commons-codec:commons-codec:1.7 -> 1.11
+\--- com.github.QuadFlask:colorpicker:0.0.15
+ \--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*)
++--- com.github.QuadFlask:colorpicker:0.0.15
+| \--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*)
++--- androidx.security:security-crypto:1.1.0-alpha02
+| +--- androidx.annotation:annotation:1.1.0
+| +--- com.google.crypto.tink:tink-android:1.4.0
+| \--- androidx.collection:collection:1.1.0 (*)
+\--- net.openid:appauth:0.7.1
+ \--- androidx.browser:browser:1.0.0
+ +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ +--- androidx.interpolator:interpolator:1.0.0 (*)
+ +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
+ \--- androidx.legacy:legacy-support-core-ui:1.0.0
+ +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
+ +--- androidx.customview:customview:1.0.0 (*)
+ +--- androidx.viewpager:viewpager:1.0.0 (*)
+ +--- androidx.coordinatorlayout:coordinatorlayout:1.0.0 -> 1.1.0 (*)
+ +--- androidx.drawerlayout:drawerlayout:1.0.0 (*)
+ +--- androidx.slidingpanelayout:slidingpanelayout:1.0.0
+ | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ | \--- androidx.customview:customview:1.0.0 (*)
+ +--- androidx.interpolator:interpolator:1.0.0 (*)
+ +--- androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 -> 1.1.0 (*)
+ +--- androidx.asynclayoutinflater:asynclayoutinflater:1.0.0
+ | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ | \--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ \--- androidx.cursoradapter:cursoradapter:1.0.0 (*)

@ -246,21 +246,6 @@
+| +--- com.google.auto.value:auto-value-annotations:1.6.2 -> 1.7.4
+| \--- com.google.code.gson:gson:2.8.5 -> 2.8.6
++--- com.android.billingclient:billing:1.2.2
++--- com.google.android.gms:play-services-auth:19.0.0
+| +--- androidx.fragment:fragment:1.0.0 -> 1.2.5 (*)
+| +--- androidx.loader:loader:1.0.0 (*)
+| +--- com.google.android.gms:play-services-auth-api-phone:17.0.0
+| | +--- com.google.android.gms:play-services-base:17.0.0 -> 17.3.0 (*)
+| | +--- com.google.android.gms:play-services-basement:17.0.0 -> 17.3.0 (*)
+| | \--- com.google.android.gms:play-services-tasks:17.0.0 -> 17.1.0 (*)
+| +--- com.google.android.gms:play-services-auth-base:17.0.0
+| | +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
+| | +--- com.google.android.gms:play-services-base:17.0.0 -> 17.3.0 (*)
+| | +--- com.google.android.gms:play-services-basement:17.0.0 -> 17.3.0 (*)
+| | \--- com.google.android.gms:play-services-tasks:17.0.0 -> 17.1.0 (*)
+| +--- com.google.android.gms:play-services-base:17.1.0 -> 17.3.0 (*)
+| +--- com.google.android.gms:play-services-basement:17.1.1 -> 17.3.0 (*)
+| \--- com.google.android.gms:play-services-tasks:17.0.0 -> 17.1.0 (*)
++--- com.gitlab.bitfireAT:dav4jvm:2.1.1
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.10 (*)
+| \--- org.apache.commons:commons-lang3:3.9
@ -512,5 +497,33 @@
+| +--- org.apache.commons:commons-collections4:4.1
+| +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9
+| \--- commons-codec:commons-codec:1.7 -> 1.11
+\--- com.github.QuadFlask:colorpicker:0.0.15
+ \--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*)
++--- com.github.QuadFlask:colorpicker:0.0.15
+| \--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*)
++--- androidx.security:security-crypto:1.1.0-alpha02
+| +--- androidx.annotation:annotation:1.1.0
+| +--- com.google.crypto.tink:tink-android:1.4.0
+| \--- androidx.collection:collection:1.1.0 (*)
+\--- net.openid:appauth:0.7.1
+ \--- androidx.browser:browser:1.0.0
+ +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ +--- androidx.interpolator:interpolator:1.0.0 (*)
+ +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
+ \--- androidx.legacy:legacy-support-core-ui:1.0.0
+ +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
+ +--- androidx.customview:customview:1.0.0 (*)
+ +--- androidx.viewpager:viewpager:1.0.0 (*)
+ +--- androidx.coordinatorlayout:coordinatorlayout:1.0.0 -> 1.1.0 (*)
+ +--- androidx.drawerlayout:drawerlayout:1.0.0 (*)
+ +--- androidx.slidingpanelayout:slidingpanelayout:1.0.0
+ | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ | +--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ | \--- androidx.customview:customview:1.0.0 (*)
+ +--- androidx.interpolator:interpolator:1.0.0 (*)
+ +--- androidx.swiperefreshlayout:swiperefreshlayout:1.0.0 -> 1.1.0 (*)
+ +--- androidx.asynclayoutinflater:asynclayoutinflater:1.0.0
+ | +--- androidx.annotation:annotation:1.0.0 -> 1.1.0
+ | \--- androidx.core:core:1.0.0 -> 1.3.2 (*)
+ \--- androidx.cursoradapter:cursoradapter:1.0.0 (*)

Loading…
Cancel
Save