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", "mapbox_key", tasks_mapbox_key_debug ?: "")
resValue("string", "google_key", tasks_google_key_debug ?: "") resValue("string", "google_key", tasks_google_key_debug ?: "")
isTestCoverageEnabled = project.hasProperty("coverage") isTestCoverageEnabled = project.hasProperty("coverage")
setManifestPlaceholders(mapOf(
"appAuthRedirectScheme" to "com.googleusercontent.apps.1006257750459-vf4mvft1b3rfda8b4c4bl4k4418abqlf"
))
} }
getByName("release") { getByName("release") {
val tasks_mapbox_key: String? by project val tasks_mapbox_key: String? by project
@ -102,6 +106,10 @@ android {
isMinifyEnabled = true isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard.pro") proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard.pro")
signingConfig = signingConfigs.getByName("release") 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.mapbox.mapboxsdk:mapbox-sdk-services:5.3.0")
implementation("com.etesync:journalmanager:1.1.1") implementation("com.etesync:journalmanager:1.1.1")
implementation("com.github.QuadFlask:colorpicker:0.0.15") 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 // https://github.com/mapbox/mapbox-gl-native-android/issues/316
genericImplementation("com.mapbox.mapboxsdk:mapbox-android-sdk:7.4.1") 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.gms:play-services-maps:17.0.0")
googleplayImplementation("com.google.android.libraries.places:places:2.4.0") googleplayImplementation("com.google.android.libraries.places:places:2.4.0")
googleplayImplementation("com.android.billingclient:billing:1.2.2") 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}") androidTestImplementation("com.google.dagger:hilt-android-testing:${Versions.hilt}")
kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}") kaptAndroidTest("com.google.dagger:hilt-compiler:${Versions.hilt}")

@ -823,3 +823,69 @@
name: commonmark name: commonmark
copyrightHolder: Atlassian and others copyrightHolder: Atlassian and others
license: BSD 2-Clause 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"> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" tools:ignore="PrivateResource">Tasks Debug</string> <string name="app_name" tools:ignore="PrivateResource">Tasks Debug</string>
<string name="tasks_caldav_url">https://192.168.1.120:8443</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_thread">Strict mode - Thread</string>
<string name="debug_strict_mode_vm">Strict mode - VM</string> <string name="debug_strict_mode_vm">Strict mode - VM</string>
<string name="debug_leakcanary">LeakCanary</string> <string name="debug_leakcanary">LeakCanary</string>

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

@ -15,10 +15,6 @@
<service android:name=".location.GeofenceTransitionsIntentService"/> <service android:name=".location.GeofenceTransitionsIntentService"/>
<activity
android:name=".auth.SignInActivity"
android:theme="@style/TranslucentDialog" />
</application> </application>
</manifest> </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.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.widget.Toast 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.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability 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 dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.tasks.R import org.tasks.R
import org.tasks.auth.GoogleSignInAccount
import org.tasks.auth.OauthSignIn
import org.tasks.data.LocationDao import org.tasks.data.LocationDao
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import timber.log.Timber 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 private val status: String
get() = GoogleApiAvailability.getInstance().getErrorString(result) get() = GoogleApiAvailability.getInstance().getErrorString(result)

@ -134,6 +134,19 @@
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
android:required="false"/> 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 <application
android:allowBackup="true" android:allowBackup="true"
android:backupAgent="org.tasks.backup.TasksBackupAgent" android:backupAgent="org.tasks.backup.TasksBackupAgent"
@ -151,6 +164,12 @@
android:preserveLegacyExternalStorage="true" android:preserveLegacyExternalStorage="true"
android:hasFragileUserData="true"> android:hasFragileUserData="true">
<activity
android:name=".auth.SignInActivity"
android:theme="@style/TranslucentDialog"
android:windowSoftInputMode="stateHidden" >
</activity>
<meta-data <meta-data
android:name="firebase_crashlytics_collection_enabled" android:name="firebase_crashlytics_collection_enabled"
android:value="false"/> android:value="false"/>

@ -1962,6 +1962,160 @@
"license": "BSD 2-Clause", "license": "BSD 2-Clause",
"normalizedLicense": "bsd_2_clauses", "normalizedLicense": "bsd_2_clauses",
"libraryName": "commonmark" "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 kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.caldav.BaseCaldavAccountSettingsActivity import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener { class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var authorizationService: AuthorizationService
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -61,6 +64,17 @@ class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolba
override suspend fun updateAccount() = updateAccount(caldavAccount!!.url) 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 override val helpUrl: String
get() = getString(R.string.help_url_sync) get() = getString(R.string.help_url_sync)
} }

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

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

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

@ -10,4 +10,8 @@ class CreateCalendarViewModel @ViewModelInject constructor(
suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) { suspend fun createCalendar(account: CaldavAccount, name: String, color: Int) {
run { provider.forAccount(account).makeCollection(name, color) } 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() } 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) { suspend fun updateCaldavAccount(url: String, username: String, password: String) {
run { provider.forUrl(url, username, password).homeSet(username, password) } 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) } 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 dagger.hilt.android.AndroidEntryPoint
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R import org.tasks.R
import org.tasks.auth.SignInActivity
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.preferences.fragments.MainSettingsFragment import org.tasks.preferences.fragments.MainSettingsFragment
import org.tasks.preferences.fragments.Synchronization.Companion.REQUEST_CALDAV_SETTINGS 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_GOOGLE_TASKS
import org.tasks.preferences.fragments.Synchronization.Companion.REQUEST_TASKS_ORG
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
import org.tasks.ui.Toaster import org.tasks.ui.Toaster
import javax.inject.Inject import javax.inject.Inject
@ -58,6 +60,13 @@ class MainPreferences : BasePreferences() {
} else { } else {
data?.getStringExtra(GtasksLoginActivity.EXTRA_ERROR)?.let { toaster.longToast(it) } 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 { } else {
super.onActivityResult(requestCode, resultCode, data) 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="current_subscription">Current subscription: %s</string>
<string name="purchases_updated">Purchases updated</string> <string name="purchases_updated">Purchases updated</string>
<string name="follow_reddit">Follow r/tasks</string> <string name="follow_reddit">Follow r/tasks</string>
<string name="authorization_cancelled">Authorization cancelled</string>
</resources> </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"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<string name="app_name">Tasks</string> <string name="app_name">Tasks</string>
<string name="tasks_caldav_url">https://caldav.tasks.org</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> </resources>

@ -385,5 +385,33 @@
+| +--- org.apache.commons:commons-collections4:4.1 +| +--- org.apache.commons:commons-collections4:4.1
+| +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9 +| +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9
+| \--- commons-codec:commons-codec:1.7 -> 1.11 +| \--- commons-codec:commons-codec:1.7 -> 1.11
+\--- com.github.QuadFlask:colorpicker:0.0.15 ++--- com.github.QuadFlask:colorpicker:0.0.15
+ \--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*) +| \--- 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.auto.value:auto-value-annotations:1.6.2 -> 1.7.4
+| \--- com.google.code.gson:gson:2.8.5 -> 2.8.6 +| \--- com.google.code.gson:gson:2.8.5 -> 2.8.6
++--- com.android.billingclient:billing:1.2.2 ++--- 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 ++--- com.gitlab.bitfireAT:dav4jvm:2.1.1
+| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.10 (*) +| +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.10 (*)
+| \--- org.apache.commons:commons-lang3:3.9 +| \--- org.apache.commons:commons-lang3:3.9
@ -512,5 +497,33 @@
+| +--- org.apache.commons:commons-collections4:4.1 +| +--- org.apache.commons:commons-collections4:4.1
+| +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9 +| +--- org.apache.commons:commons-lang3:3.8.1 -> 3.9
+| \--- commons-codec:commons-codec:1.7 -> 1.11 +| \--- commons-codec:commons-codec:1.7 -> 1.11
+\--- com.github.QuadFlask:colorpicker:0.0.15 ++--- com.github.QuadFlask:colorpicker:0.0.15
+ \--- androidx.appcompat:appcompat:1.1.0 -> 1.2.0 (*) +| \--- 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