From 3a34721b127e60c2b6c06737c43cacc99532223e Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 9 Dec 2020 11:39:01 -0600 Subject: [PATCH] Update Tasks.org sign in * Replace JWT with session authentication * Add support for GitHub sign in --- app/build.gradle.kts | 1 - app/licenses.yml | 12 -- app/src/debug/res/raw/github_config.json | 11 ++ app/src/debug/res/raw/google_config.json | 2 +- .../genericRelease/res/raw/github_config.json | 11 ++ .../genericRelease/res/raw/google_config.json | 2 +- .../res/raw/github_config.json | 11 ++ .../res/raw/google_config.json | 2 +- app/src/main/AndroidManifest.xml | 6 + app/src/main/assets/licenses.json | 28 ---- .../java/org/tasks/auth/AuthStateManager.kt | 59 +-------- .../org/tasks/auth/AuthorizationService.kt | 34 +++-- .../auth/AuthorizationServiceProvider.kt | 21 --- .../main/java/org/tasks/auth/Configuration.kt | 10 +- .../org/tasks/auth/DebugConnectionBuilder.kt | 69 ++++++++++ app/src/main/java/org/tasks/auth/IdToken.kt | 9 +- .../java/org/tasks/auth/SignInActivity.kt | 84 +++++++++--- .../java/org/tasks/auth/SignInViewModel.kt | 87 +++++++++---- .../java/org/tasks/billing/PurchaseDialog.kt | 41 +++++- .../tasks/caldav/AddCaldavAccountViewModel.kt | 4 - .../java/org/tasks/caldav/CaldavClient.kt | 8 -- .../org/tasks/caldav/CaldavClientProvider.kt | 15 +-- .../org/tasks/caldav/CaldavSynchronizer.kt | 23 ++-- .../tasks/caldav/CreateCalendarViewModel.kt | 4 - .../tasks/caldav/DeleteCalendarViewModel.kt | 4 - .../caldav/UpdateCaldavAccountViewModel.kt | 4 - .../tasks/caldav/UpdateCalendarViewModel.kt | 4 - .../main/java/org/tasks/data/CaldavAccount.kt | 2 +- .../org/tasks/dialogs/AlertDialogBuilder.java | 6 + .../java/org/tasks/jobs/MigrateLocalWork.kt | 4 - .../fragments/MainSettingsFragment.kt | 18 +-- .../preferences/fragments/TasksAccount.kt | 123 +++++++++++++----- app/src/main/res/values/arrays.xml | 15 +++ app/src/main/res/values/strings.xml | 6 + deps_fdroid.txt | 4 - deps_googleplay.txt | 4 - 36 files changed, 444 insertions(+), 304 deletions(-) create mode 100644 app/src/debug/res/raw/github_config.json create mode 100644 app/src/genericRelease/res/raw/github_config.json create mode 100644 app/src/googleplayRelease/res/raw/github_config.json delete mode 100644 app/src/main/java/org/tasks/auth/AuthorizationServiceProvider.kt create mode 100644 app/src/main/java/org/tasks/auth/DebugConnectionBuilder.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7f59a4921..9164f986f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,7 +203,6 @@ dependencies { implementation("com.etesync:journalmanager:1.1.1") implementation("com.etebase:client:2.3.2") implementation("com.github.QuadFlask:colorpicker:0.0.15") - implementation("androidx.security:security-crypto:1.1.0-alpha02") implementation("com.github.openid:AppAuth-Android:27b62d5") // https://github.com/mapbox/mapbox-gl-native-android/issues/316 diff --git a/app/licenses.yml b/app/licenses.yml index 0da97a238..efe9a7fea 100644 --- a/app/licenses.yml +++ b/app/licenses.yml @@ -829,18 +829,6 @@ 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: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: http://github.com/google/tink - artifact: com.etebase:client:+ name: client copyrightHolder: Tom Hacohen diff --git a/app/src/debug/res/raw/github_config.json b/app/src/debug/res/raw/github_config.json new file mode 100644 index 000000000..9726938c7 --- /dev/null +++ b/app/src/debug/res/raw/github_config.json @@ -0,0 +1,11 @@ +{ + "client_id": "a50fdbf3e289a7fb2fc6", + "redirect_uri": "org.tasks.github.a50fdbf3e289a7fb2fc6://oauth2redirect", + "authorization_scope": "none", + "discovery_uri": "https://caldav.tasks.org/oauth/github-configuration", + "authorization_endpoint_uri": "", + "token_endpoint_uri": "", + "registration_endpoint_uri": "", + "user_info_endpoint_uri": "", + "https_required": true +} diff --git a/app/src/debug/res/raw/google_config.json b/app/src/debug/res/raw/google_config.json index 1f8499225..01b422e81 100644 --- a/app/src/debug/res/raw/google_config.json +++ b/app/src/debug/res/raw/google_config.json @@ -2,7 +2,7 @@ "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", + "discovery_uri": "https://caldav.tasks.org/oauth/google-configuration", "authorization_endpoint_uri": "", "token_endpoint_uri": "", "registration_endpoint_uri": "", diff --git a/app/src/genericRelease/res/raw/github_config.json b/app/src/genericRelease/res/raw/github_config.json new file mode 100644 index 000000000..9726938c7 --- /dev/null +++ b/app/src/genericRelease/res/raw/github_config.json @@ -0,0 +1,11 @@ +{ + "client_id": "a50fdbf3e289a7fb2fc6", + "redirect_uri": "org.tasks.github.a50fdbf3e289a7fb2fc6://oauth2redirect", + "authorization_scope": "none", + "discovery_uri": "https://caldav.tasks.org/oauth/github-configuration", + "authorization_endpoint_uri": "", + "token_endpoint_uri": "", + "registration_endpoint_uri": "", + "user_info_endpoint_uri": "", + "https_required": true +} diff --git a/app/src/genericRelease/res/raw/google_config.json b/app/src/genericRelease/res/raw/google_config.json index 71dca8be8..53dda06eb 100644 --- a/app/src/genericRelease/res/raw/google_config.json +++ b/app/src/genericRelease/res/raw/google_config.json @@ -2,7 +2,7 @@ "client_id": "363426363175-rg39b1q2302l6mlkup40l4d6ids4osiv.apps.googleusercontent.com", "redirect_uri": "com.googleusercontent.apps.363426363175-rg39b1q2302l6mlkup40l4d6ids4osiv:/oauth2redirect", "authorization_scope": "openid email profile", - "discovery_uri": "https://accounts.google.com/.well-known/openid-configuration", + "discovery_uri": "https://caldav.tasks.org/oauth/google-configuration", "authorization_endpoint_uri": "", "token_endpoint_uri": "", "registration_endpoint_uri": "", diff --git a/app/src/googleplayRelease/res/raw/github_config.json b/app/src/googleplayRelease/res/raw/github_config.json new file mode 100644 index 000000000..9726938c7 --- /dev/null +++ b/app/src/googleplayRelease/res/raw/github_config.json @@ -0,0 +1,11 @@ +{ + "client_id": "a50fdbf3e289a7fb2fc6", + "redirect_uri": "org.tasks.github.a50fdbf3e289a7fb2fc6://oauth2redirect", + "authorization_scope": "none", + "discovery_uri": "https://caldav.tasks.org/oauth/github-configuration", + "authorization_endpoint_uri": "", + "token_endpoint_uri": "", + "registration_endpoint_uri": "", + "user_info_endpoint_uri": "", + "https_required": true +} diff --git a/app/src/googleplayRelease/res/raw/google_config.json b/app/src/googleplayRelease/res/raw/google_config.json index e74d5f210..4f13dd62c 100644 --- a/app/src/googleplayRelease/res/raw/google_config.json +++ b/app/src/googleplayRelease/res/raw/google_config.json @@ -2,7 +2,7 @@ "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", + "discovery_uri": "https://caldav.tasks.org/oauth/google-configuration", "authorization_endpoint_uri": "", "token_endpoint_uri": "", "registration_endpoint_uri": "", diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8392ae140..22880f249 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -173,6 +173,12 @@ + + + + + + () fun signOut() { @@ -56,7 +38,7 @@ class AuthStateManager @Inject constructor(@ApplicationContext private val conte if (currentAuthState.get() != null) { return currentAuthState.get() } - val state = readState() + val state = AuthState() return if (currentAuthState.compareAndSet(null, state)) { state } else { @@ -65,7 +47,6 @@ class AuthStateManager @Inject constructor(@ApplicationContext private val conte } fun replace(state: AuthState): AuthState { - writeState(state) currentAuthState.set(state) return state } @@ -99,40 +80,4 @@ class AuthStateManager @Inject constructor(@ApplicationContext private val conte 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" - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/auth/AuthorizationService.kt b/app/src/main/java/org/tasks/auth/AuthorizationService.kt index 79d2e4ec5..a4e98c75c 100644 --- a/app/src/main/java/org/tasks/auth/AuthorizationService.kt +++ b/app/src/main/java/org/tasks/auth/AuthorizationService.kt @@ -12,10 +12,21 @@ import net.openid.appauth.browser.AnyBrowserMatcher import kotlin.coroutines.suspendCoroutine class AuthorizationService constructor( + val iss: String, context: Context, - private val authStateManager: AuthStateManager, - val configuration: Configuration + debugConnectionBuilder: DebugConnectionBuilder ) { + val isGitHub = iss == ISS_GITHUB + val authStateManager = AuthStateManager() + val configuration = Configuration( + context, + when (iss) { + ISS_GOOGLE -> Configuration.GOOGLE_CONFIG + ISS_GITHUB -> Configuration.GITHUB_CONFIG + else -> throw IllegalArgumentException() + }, + debugConnectionBuilder + ) private val authorizationService = AuthorizationService( context, AppAuthConfiguration.Builder() @@ -62,21 +73,8 @@ class AuthorizationService constructor( } } - suspend fun getFreshToken(): String? { - val authState = authStateManager.current - if (!authState.isAuthorized) { - return null - } - return withContext(Dispatchers.IO) { - suspendCoroutine { cont -> - authState.performActionWithFreshTokens(authorizationService) { _, idToken, exception -> - if (exception == null) { - cont.resumeWith(Result.success(idToken)) - } else { - cont.resumeWith(Result.failure(exception)) - } - } - } - } + companion object { + const val ISS_GOOGLE = "google" + const val ISS_GITHUB = "github" } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/auth/AuthorizationServiceProvider.kt b/app/src/main/java/org/tasks/auth/AuthorizationServiceProvider.kt deleted file mode 100644 index cebfcef9b..000000000 --- a/app/src/main/java/org/tasks/auth/AuthorizationServiceProvider.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.tasks.auth - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import org.tasks.auth.Configuration.Companion.GOOGLE_CONFIG -import javax.inject.Inject - -class AuthorizationServiceProvider @Inject constructor( - @ApplicationContext context: Context, - authStateManager: AuthStateManager -){ - val google = AuthorizationService( - context, - authStateManager, - Configuration(context, GOOGLE_CONFIG) - ) - - fun dispose() { - google.dispose() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/auth/Configuration.kt b/app/src/main/java/org/tasks/auth/Configuration.kt index 5dee932a2..42b5ae007 100644 --- a/app/src/main/java/org/tasks/auth/Configuration.kt +++ b/app/src/main/java/org/tasks/auth/Configuration.kt @@ -24,6 +24,7 @@ import okio.buffer import okio.source import org.json.JSONException import org.json.JSONObject +import org.tasks.BuildConfig import org.tasks.R import java.io.IOException import java.nio.charset.StandardCharsets @@ -35,7 +36,8 @@ import java.nio.charset.StandardCharsets */ class Configuration constructor( private val context: Context, - private val authConfig: Int + private val authConfig: Int, + debugConnectionBuilder: DebugConnectionBuilder ) { private val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) private var configJson: JSONObject? = null @@ -90,7 +92,10 @@ class Configuration constructor( val redirectUri: Uri get() = mRedirectUri!! - val connectionBuilder: ConnectionBuilder = DefaultConnectionBuilder.INSTANCE + val connectionBuilder: ConnectionBuilder = when { + BuildConfig.DEBUG -> debugConnectionBuilder + else -> DefaultConnectionBuilder.INSTANCE + } private val lastKnownConfigHash: String? get() = prefs.getString(KEY_LAST_HASH, null) @@ -207,6 +212,7 @@ class Configuration constructor( private const val PREFS_NAME = "config" private const val KEY_LAST_HASH = "lastHash" const val GOOGLE_CONFIG = R.raw.google_config + const val GITHUB_CONFIG = R.raw.github_config } init { diff --git a/app/src/main/java/org/tasks/auth/DebugConnectionBuilder.kt b/app/src/main/java/org/tasks/auth/DebugConnectionBuilder.kt new file mode 100644 index 000000000..a684c4e07 --- /dev/null +++ b/app/src/main/java/org/tasks/auth/DebugConnectionBuilder.kt @@ -0,0 +1,69 @@ +/* + * 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.net.Uri +import at.bitfire.cert4android.CustomCertManager +import dagger.hilt.android.qualifiers.ApplicationContext +import net.openid.appauth.Preconditions +import net.openid.appauth.connectivity.ConnectionBuilder +import okhttp3.internal.tls.OkHostnameVerifier +import org.tasks.DebugNetworkInterceptor +import org.tasks.preferences.Preferences +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext + +/** + * Creates [HttpURLConnection] instances using the default, platform-provided + * mechanism, with sensible production defaults. + */ +class DebugConnectionBuilder @Inject constructor( + @ApplicationContext private val context: Context, + private val interceptor: DebugNetworkInterceptor, + private val preferences: Preferences, +) : ConnectionBuilder { + + var appInForeground: Boolean = true + + @Throws(IOException::class) + override fun openConnection(uri: Uri): HttpURLConnection { + Preconditions.checkNotNull(uri, "url must not be null") + Preconditions.checkArgument(HTTPS_SCHEME == uri.scheme, + "only https connections are permitted") + val customCertManager = CustomCertManager(context) + customCertManager.appInForeground = appInForeground + val hostnameVerifier = customCertManager.hostnameVerifier(OkHostnameVerifier) + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, arrayOf(customCertManager), null) + val conn = URL(uri.toString()).openConnection() as HttpsURLConnection + conn.connectTimeout = CONNECTION_TIMEOUT_MS + conn.readTimeout = READ_TIMEOUT_MS + conn.instanceFollowRedirects = false + conn.hostnameVerifier = hostnameVerifier + conn.sslSocketFactory = sslContext.socketFactory + return conn + } + + companion object { + private val CONNECTION_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(15).toInt() + private val READ_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10).toInt() + private const val HTTPS_SCHEME = "https" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/auth/IdToken.kt b/app/src/main/java/org/tasks/auth/IdToken.kt index 26f7b85c9..2ab0ad179 100644 --- a/app/src/main/java/org/tasks/auth/IdToken.kt +++ b/app/src/main/java/org/tasks/auth/IdToken.kt @@ -5,11 +5,14 @@ import org.json.JSONObject class IdToken(idToken: String) { private val parts: List = idToken.split(".") - val json = JSONObject(String(Base64.decode(parts[1], Base64.DEFAULT))) + private val json = JSONObject(String(Base64.decode(parts[1], Base64.DEFAULT))) - val email: String - get() = json.getString("email") + val email: String? + get() = json.optString("email").takeIf { it.isNotBlank() } val sub: String get() = json.getString("sub") + + val login: String? + get() = json.optString("login").takeIf { it.isNotBlank() } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/auth/SignInActivity.kt b/app/src/main/java/org/tasks/auth/SignInActivity.kt index 06405a588..9642545ce 100644 --- a/app/src/main/java/org/tasks/auth/SignInActivity.kt +++ b/app/src/main/java/org/tasks/auth/SignInActivity.kt @@ -15,6 +15,11 @@ package org.tasks.auth import android.content.Intent import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ImageView +import android.widget.TextView import androidx.activity.viewModels import androidx.annotation.AnyThread import androidx.annotation.MainThread @@ -30,6 +35,7 @@ import org.tasks.billing.Inventory import org.tasks.billing.PurchaseDialog import org.tasks.billing.PurchaseDialog.Companion.FRAG_TAG_PURCHASE_DIALOG import org.tasks.billing.PurchaseDialog.Companion.newPurchaseDialog +import org.tasks.dialogs.DialogBuilder import org.tasks.injection.InjectingAppCompatActivity import org.tasks.themes.ThemeColor import timber.log.Timber @@ -50,10 +56,9 @@ import javax.inject.Inject */ @AndroidEntryPoint class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHandler { - @Inject lateinit var authorizationServiceProvider: AuthorizationServiceProvider - @Inject lateinit var authStateManager: AuthStateManager @Inject lateinit var themeColor: ThemeColor @Inject lateinit var inventory: Inventory + @Inject lateinit var dialogBuilder: DialogBuilder private val viewModel: SignInViewModel by viewModels() @@ -63,16 +68,68 @@ class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHand private var mAuthIntentLatch = CountDownLatch(1) private val mExecutor: ExecutorService = newSingleThreadExecutor() - lateinit var authService: AuthorizationService - lateinit var configuration: Configuration + private lateinit var authService: AuthorizationService + + private val configuration: Configuration + get() = authService.configuration + + private val authStateManager: AuthStateManager + get() = authService.authStateManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.error.observe(this, this::handleError) - authService = authorizationServiceProvider.google - configuration = authService.configuration + val titles = resources.getStringArray(R.array.sign_in_titles) + val summaries = resources.getStringArray(R.array.sign_in_summaries) + val typedArray = resources.obtainTypedArray(R.array.sign_in_icons) + val icons = IntArray(typedArray.length()) + for (i in icons.indices) { + icons[i] = typedArray.getResourceId(i, 0) + } + typedArray.recycle() + val adapter = object : BaseAdapter() { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = layoutInflater.inflate(R.layout.simple_list_item_2_themed, null) + val icon = view.findViewById(R.id.image_view) + icon.setImageResource(icons[position]) + view.findViewById(R.id.text2).text = titles[position] + view.findViewById(R.id.text1).text = summaries[position] + if (position == 1) { + icon.drawable.setTint(getColor(R.color.icon_tint)) + } + return view + } + + override fun getCount() = titles.size + + override fun getItem(position: Int) = titles[position] + + override fun getItemId(position: Int): Long = position.toLong() + } + val autoSelect = intent.getIntExtra(EXTRA_SELECT_SERVICE, -1) + if (autoSelect >= 0 && autoSelect < titles.size) { + selectService(autoSelect) + } else { + dialogBuilder.newDialog() + .setAdapter(adapter) { _, which -> selectService(which) } + .setOnCancelListener { finish() } + .show() + } + } + + private fun selectService(which: Int) { + viewModel.initializeAuthService(when (which) { + 0 -> AuthorizationService.ISS_GOOGLE + 1 -> AuthorizationService.ISS_GITHUB + else -> throw IllegalArgumentException() + }) + viewModel.authService?.let { startAuthorization(it) } + } + + private fun startAuthorization(authService: AuthorizationService) { + this.authService = authService if (authStateManager.current.isAuthorized && !configuration.hasConfigurationChanged()) { @@ -94,23 +151,17 @@ class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHand private fun handleError(e: Throwable) { if (e is HttpException && e.code == 402) { - newPurchaseDialog(tasksPayment = true) + newPurchaseDialog(tasksPayment = true, github = authService.isGitHub) .show(supportFragmentManager, FRAG_TAG_PURCHASE_DIALOG) } else { returnError(e.message) } } - override fun onStop() { - super.onStop() - - mExecutor.shutdownNow() - } - override fun onDestroy() { super.onDestroy() - authService.dispose() + mExecutor.shutdownNow() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -118,7 +169,7 @@ class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHand if (resultCode == RESULT_OK) { lifecycleScope.launch { val account = try { - viewModel.handleResult(data!!) + viewModel.handleResult(authService, data!!) } catch (e: Exception) { returnError(e.message) } @@ -302,13 +353,14 @@ class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHand companion object { const val EXTRA_ERROR = "extra_error" + const val EXTRA_SELECT_SERVICE = "extra_select_service" private const val RC_AUTH = 100 } override fun onPurchaseDialogDismissed() { if (inventory.subscription?.isTasksSubscription == true) { lifecycleScope.launch { - val account = viewModel.setupAccount(authStateManager.current) + val account = viewModel.setupAccount(authService) if (account != null) { setResult(RESULT_OK) finish() diff --git a/app/src/main/java/org/tasks/auth/SignInViewModel.kt b/app/src/main/java/org/tasks/auth/SignInViewModel.kt index 04ee559be..4d28abc42 100644 --- a/app/src/main/java/org/tasks/auth/SignInViewModel.kt +++ b/app/src/main/java/org/tasks/auth/SignInViewModel.kt @@ -7,28 +7,38 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.todoroo.astrid.helper.UUIDHelper import dagger.hilt.android.qualifiers.ApplicationContext -import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationResponse import net.openid.appauth.ClientAuthentication.UnsupportedAuthenticationMethod +import net.openid.appauth.GrantTypeValues +import net.openid.appauth.TokenRequest import org.tasks.R import org.tasks.caldav.CaldavClientProvider import org.tasks.data.CaldavAccount import org.tasks.data.CaldavDao +import org.tasks.security.KeyStoreEncryption import timber.log.Timber class SignInViewModel @ViewModelInject constructor( @ApplicationContext private val context: Context, - private val authStateManager: AuthStateManager, - private val authorizationServiceProvider: AuthorizationServiceProvider, private val provider: CaldavClientProvider, - private val caldavDao: CaldavDao + private val caldavDao: CaldavDao, + private val encryption: KeyStoreEncryption, + private val debugConnectionBuilder: DebugConnectionBuilder ) : ViewModel() { val error = MutableLiveData() - suspend fun handleResult(intent: Intent): CaldavAccount? { + var authService: AuthorizationService? = null + + fun initializeAuthService(iss: String) { + authService?.dispose() + authService = AuthorizationService(iss, context, debugConnectionBuilder) + } + + suspend fun handleResult(authService: AuthorizationService, intent: Intent): CaldavAccount? { val response = AuthorizationResponse.fromIntent(intent) val ex = AuthorizationException.fromIntent(intent) + val authStateManager = authService.authStateManager if (response != null || ex != null) { authStateManager.updateAfterAuthorization(response, ex) @@ -36,7 +46,7 @@ class SignInViewModel @ViewModelInject constructor( if (response?.authorizationCode != null) { authStateManager.updateAfterAuthorization(response, ex) - exchangeAuthorizationCode(response) + exchangeAuthorizationCode(authService, response) } ex?.let { @@ -46,32 +56,36 @@ class SignInViewModel @ViewModelInject constructor( return authStateManager.current .takeIf { it.isAuthorized } - ?.let { setupAccount(it) } + ?.let { setupAccount(authService) } } - suspend fun setupAccount(auth: AuthState): CaldavAccount? { - val tokenString = auth.idToken ?: return null - val idToken = IdToken(tokenString) - val username = "google_${idToken.sub}" + suspend fun setupAccount(authService: AuthorizationService): CaldavAccount? { + val auth = authService.authStateManager.current + val tokenString = auth.accessToken ?: return null + val idToken = auth.idToken?.let { IdToken(it) } ?: return null try { val homeSet = provider .forUrl( - "${context.getString(R.string.tasks_caldav_url)}/google_login", + context.getString(R.string.tasks_caldav_url), token = tokenString ) .setForeground() .homeSet(token = tokenString) + val username = "${authService.iss}_${idToken.sub}" + val password = encryption.encrypt(tokenString) return caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username) ?.apply { error = null + this.password = password caldavDao.update(this) } ?: CaldavAccount().apply { accountType = CaldavAccount.TYPE_TASKS uuid = UUIDHelper.newUUID() - url = homeSet this.username = username - name = idToken.email + this.password = password + url = homeSet + name = idToken.email ?: idToken.login caldavDao.insert(this) } } catch (e: Exception) { @@ -80,24 +94,49 @@ class SignInViewModel @ViewModelInject constructor( return null } - private suspend fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { - val request = authorizationResponse.createTokenExchangeRequest() + private suspend fun exchangeAuthorizationCode( + authService: AuthorizationService, + authorizationResponse: AuthorizationResponse + ) { + val authStateManager = authService.authStateManager + val request = if (authService.isGitHub) { + authorizationResponse.createGithubTokenRequest() + } else { + authorizationResponse.createTokenExchangeRequest() + } val clientAuthentication = try { authStateManager.current.clientAuthentication } catch (ex: UnsupportedAuthenticationMethod) { throw ex } try { - authorizationServiceProvider - .google - .performTokenRequest(request, clientAuthentication)?.let { - authStateManager.updateAfterTokenResponse(it, null) - if (authStateManager.current.isAuthorized) { - Timber.d("Authorization successful") - } - } + authService.performTokenRequest(request, clientAuthentication)?.let { + authStateManager.updateAfterTokenResponse(it, null) + if (authStateManager.current.isAuthorized) { + Timber.d("Authorization successful") + } + } } catch (e: AuthorizationException) { + Timber.e(e) authStateManager.updateAfterTokenResponse(null, e) } } + + override fun onCleared() { + authService?.dispose() + } + + companion object { + fun AuthorizationResponse.createGithubTokenRequest(): TokenRequest { + checkNotNull(authorizationCode) { "authorizationCode not available for exchange request" } + return TokenRequest + .Builder(request.configuration, request.clientId) + .setGrantType(GrantTypeValues.AUTHORIZATION_CODE) + .setRedirectUri(request.redirectUri) + .setCodeVerifier(request.codeVerifier) + .setAuthorizationCode(authorizationCode) + .setAdditionalParameters(emptyMap()) + .build() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/billing/PurchaseDialog.kt b/app/src/main/java/org/tasks/billing/PurchaseDialog.kt index 124ff8534..840c374eb 100644 --- a/app/src/main/java/org/tasks/billing/PurchaseDialog.kt +++ b/app/src/main/java/org/tasks/billing/PurchaseDialog.kt @@ -9,6 +9,7 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.text.method.LinkMovementMethod +import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment @@ -77,11 +78,34 @@ class PurchaseDialog : DialogFragment(), OnPurchasesUpdated { setWaitScreen(BuildConfig.FLAVOR != "generic") - return dialogBuilder.newDialog() - .setView(binding.root) - .show() + return if (BuildConfig.FLAVOR == "generic") { + if (isGitHub) { + getPurchaseDialog() + } else { + getMessageDialog(R.string.no_google_play_subscription) + } + } else { + if (isGitHub) { + getMessageDialog(R.string.insufficient_sponsorship) + } else { + getPurchaseDialog() + } + } } + private fun getPurchaseDialog(): AlertDialog = + dialogBuilder.newDialog().setView(binding.root).show() + + private fun getMessageDialog(res: Int): AlertDialog = + dialogBuilder.newDialog() + .setMessage(res) + .setPositiveButton(R.string.ok, null) + .setNeutralButton(R.string.help) { _, _ -> + val url = Uri.parse(getString(R.string.subscription_help_url)) + startActivity(Intent(Intent.ACTION_VIEW, url)) + } + .show() + private fun updateText() { var benefits = "### ${getString(R.string.upgrade_header)}" benefits += if (nameYourPrice) { @@ -268,20 +292,29 @@ class PurchaseDialog : DialogFragment(), OnPurchasesUpdated { private val isTasksPayment: Boolean get() = arguments?.getBoolean(EXTRA_TASKS_PAYMENT, false) ?: false + private val isGitHub: Boolean + get() = arguments?.getBoolean(EXTRA_GITHUB, false) ?: false + companion object { private const val EXTRA_PRICE = "extra_price" private const val EXTRA_PRICE_CHANGED = "extra_price_changed" private const val EXTRA_NAME_YOUR_PRICE = "extra_name_your_price" private const val EXTRA_TASKS_PAYMENT = "extra_tasks_payment" + private const val EXTRA_GITHUB = "extra_github" + @JvmStatic val FRAG_TAG_PURCHASE_DIALOG = "frag_tag_purchase_dialog" @JvmStatic @JvmOverloads - fun newPurchaseDialog(tasksPayment: Boolean = false): PurchaseDialog { + fun newPurchaseDialog( + tasksPayment: Boolean = false, + github: Boolean = BuildConfig.FLAVOR == "generic" + ): PurchaseDialog { val dialog = PurchaseDialog() val args = Bundle() args.putBoolean(EXTRA_TASKS_PAYMENT, tasksPayment) + args.putBoolean(EXTRA_GITHUB, github) dialog.arguments = args return dialog } diff --git a/app/src/main/java/org/tasks/caldav/AddCaldavAccountViewModel.kt b/app/src/main/java/org/tasks/caldav/AddCaldavAccountViewModel.kt index 40047d7cc..dc2e3123a 100644 --- a/app/src/main/java/org/tasks/caldav/AddCaldavAccountViewModel.kt +++ b/app/src/main/java/org/tasks/caldav/AddCaldavAccountViewModel.kt @@ -13,8 +13,4 @@ class AddCaldavAccountViewModel @ViewModelInject constructor( .setForeground() .homeSet(username, password) } } - - override fun onCleared() { - provider.dispose() - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/CaldavClient.kt b/app/src/main/java/org/tasks/caldav/CaldavClient.kt index 989753336..64375aa9b 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavClient.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavClient.kt @@ -41,14 +41,6 @@ class CaldavClient( suspend fun forAccount(account: CaldavAccount) = provider.forAccount(account) - @Throws(KeyManagementException::class, NoSuchAlgorithmException::class) - suspend fun forUrl( - url: String?, - username: String, - password: String, - token: String? = null - ): CaldavClient = provider.forUrl(url, username, password, token) - @WorkerThread @Throws(DavException::class, IOException::class) private fun tryFindPrincipal(link: String): String? { diff --git a/app/src/main/java/org/tasks/caldav/CaldavClientProvider.kt b/app/src/main/java/org/tasks/caldav/CaldavClientProvider.kt index 77c0cc4fa..85e42316e 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavClientProvider.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavClientProvider.kt @@ -12,7 +12,6 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.internal.tls.OkHostnameVerifier import org.tasks.DebugNetworkInterceptor -import org.tasks.auth.AuthorizationServiceProvider import org.tasks.billing.Inventory import org.tasks.data.CaldavAccount import org.tasks.preferences.Preferences @@ -26,8 +25,7 @@ class CaldavClientProvider @Inject constructor( private val encryption: KeyStoreEncryption, private val preferences: Preferences, private val interceptor: DebugNetworkInterceptor, - private val inventory: Inventory, - private val authorizationServiceProvider: AuthorizationServiceProvider + private val inventory: Inventory ) { suspend fun forUrl( url: String?, @@ -59,7 +57,7 @@ class CaldavClientProvider @Inject constructor( CustomCertManager(context) } - private suspend fun getAuthInterceptor( + private fun getAuthInterceptor( account: CaldavAccount? = null, username: String? = account?.username, password: String? = account?.getPassword(encryption), @@ -67,9 +65,8 @@ class CaldavClientProvider @Inject constructor( ): Interceptor? { return when { account?.isTasksOrg == true -> - authorizationServiceProvider - .google - .getFreshToken() + account.password + ?.let { encryption.decrypt(it) } ?.let { TokenInterceptor(it, inventory) } username?.isNotBlank() == true && password?.isNotBlank() == true -> BasicDigestAuthHandler(null, username, password) @@ -106,8 +103,4 @@ class CaldavClientProvider @Inject constructor( return builder.build() } - - fun dispose() { - authorizationServiceProvider.dispose() - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt index 8e522a57b..c3ef3783e 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavSynchronizer.kt @@ -30,7 +30,6 @@ import org.tasks.analytics.Firebase import org.tasks.billing.Inventory import org.tasks.caldav.iCalendar.Companion.fromVtodo import org.tasks.data.CaldavAccount -import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavDao import org.tasks.data.CaldavTask @@ -65,15 +64,17 @@ class CaldavSynchronizer @Inject constructor( suspend fun sync(account: CaldavAccount) { Thread.currentThread().contextClassLoader = context.classLoader - if (account.accountType != TYPE_TASKS) { - if (!inventory.hasPro) { - setError(account, context.getString(R.string.requires_pro_subscription)) - return - } - if (isNullOrEmpty(account.password)) { - setError(account, context.getString(R.string.password_required)) - return - } + if (!inventory.hasPro && !account.isTasksOrg) { + setError(account, context.getString(R.string.requires_pro_subscription)) + return + } + if (isNullOrEmpty(account.password)) { + setError(account, context.getString(if (account.isTasksOrg) { + R.string.authentication_required + } else { + R.string.password_required + })) + return } try { synchronize(account) @@ -107,8 +108,6 @@ class CaldavSynchronizer @Inject constructor( } catch (e: DavException) { setError(account, e.message) firebase.reportException(e) - } finally { - provider.dispose() } } diff --git a/app/src/main/java/org/tasks/caldav/CreateCalendarViewModel.kt b/app/src/main/java/org/tasks/caldav/CreateCalendarViewModel.kt index c89a7ac78..51f50d28f 100644 --- a/app/src/main/java/org/tasks/caldav/CreateCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/caldav/CreateCalendarViewModel.kt @@ -10,8 +10,4 @@ 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() - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/DeleteCalendarViewModel.kt b/app/src/main/java/org/tasks/caldav/DeleteCalendarViewModel.kt index f10548ba3..346e5074d 100644 --- a/app/src/main/java/org/tasks/caldav/DeleteCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/caldav/DeleteCalendarViewModel.kt @@ -13,8 +13,4 @@ class DeleteCalendarViewModel @ViewModelInject constructor( calendar.url?.let { provider.forAccount(account, it).deleteCollection() } } } - - override fun onCleared() { - provider.dispose() - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/UpdateCaldavAccountViewModel.kt b/app/src/main/java/org/tasks/caldav/UpdateCaldavAccountViewModel.kt index 69544fe0b..347ec6e48 100644 --- a/app/src/main/java/org/tasks/caldav/UpdateCaldavAccountViewModel.kt +++ b/app/src/main/java/org/tasks/caldav/UpdateCaldavAccountViewModel.kt @@ -9,8 +9,4 @@ 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() - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/UpdateCalendarViewModel.kt b/app/src/main/java/org/tasks/caldav/UpdateCalendarViewModel.kt index 73a0c7923..6d81c9b7f 100644 --- a/app/src/main/java/org/tasks/caldav/UpdateCalendarViewModel.kt +++ b/app/src/main/java/org/tasks/caldav/UpdateCalendarViewModel.kt @@ -13,8 +13,4 @@ class UpdateCalendarViewModel @ViewModelInject constructor( calendar.url?.let { provider.forAccount(account, it).updateCollection(name, color) } } } - - override fun onCleared() { - provider.dispose() - } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/CaldavAccount.kt b/app/src/main/java/org/tasks/data/CaldavAccount.kt index 39932d60e..9a1b8769a 100644 --- a/app/src/main/java/org/tasks/data/CaldavAccount.kt +++ b/app/src/main/java/org/tasks/data/CaldavAccount.kt @@ -170,7 +170,7 @@ class CaldavAccount : Parcelable { fun isTasksSubscription(context: Context): Boolean { val caldavUrl = context.getString(R.string.tasks_caldav_url) - return url?.startsWith("https://${caldavUrl}/calendars/") == true && + return url?.startsWith("${caldavUrl}/calendars/") == true && !isPaymentRequired() && !isLoggedOut() } diff --git a/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java b/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java index f9f7a8503..b1134204b 100644 --- a/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java +++ b/app/src/main/java/org/tasks/dialogs/AlertDialogBuilder.java @@ -64,6 +64,12 @@ public class AlertDialogBuilder { return this; } + public AlertDialogBuilder setAdapter( + ListAdapter adapter, DialogInterface.OnClickListener onClickListener) { + builder.setAdapter(adapter, onClickListener); + return this; + } + public AlertDialogBuilder setView(View dialogView) { builder.setView(dialogView); return this; diff --git a/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt b/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt index 13a1a39dc..7fd54fa72 100644 --- a/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt +++ b/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt @@ -35,10 +35,6 @@ class MigrateLocalWork @WorkerInject constructor( return Result.success() } - override fun destroy() { - clientProvider.dispose() - } - companion object { const val EXTRA_ACCOUNT = "extra_account" } diff --git a/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt b/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt index 274fb7a60..6b62e26d3 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt @@ -9,8 +9,6 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import org.tasks.BuildConfig import org.tasks.R -import org.tasks.auth.AuthStateManager -import org.tasks.auth.IdToken import org.tasks.auth.SignInActivity import org.tasks.data.CaldavAccount import org.tasks.data.CaldavDao @@ -28,7 +26,6 @@ class MainSettingsFragment : InjectingPreferenceFragment() { @Inject lateinit var appWidgetManager: AppWidgetManager @Inject lateinit var preferences: Preferences - @Inject lateinit var authStateManager: AuthStateManager @Inject lateinit var caldavDao: CaldavDao private val viewModel: PreferencesViewModel by activityViewModels() @@ -71,20 +68,11 @@ class MainSettingsFragment : InjectingPreferenceFragment() { val accounts = caldavDao.getAccounts(CaldavAccount.TYPE_TASKS) if (accounts.isEmpty()) { pref.setOnPreferenceClickListener { signIn() } - pref.summary = getString(R.string.sign_in_with_google) + pref.summary = getString(R.string.not_signed_in) return } - val idToken = authStateManager.current - .takeIf { it.isAuthorized } - ?.idToken - ?.let { IdToken(it) } - val account = idToken - ?.let { token -> accounts.firstOrNull { it.username == "google_${token.sub}" } } - ?: accounts.first().apply { - // auth state doesn't match any accounts - authStateManager.signOut() - } - pref.summary = idToken?.email ?: account.name + val account = accounts.first() + pref.summary = account.name if (!account.error.isNullOrBlank()) { pref.drawable = ContextCompat diff --git a/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt b/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt index 85ba602f2..d24a141b2 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt @@ -4,6 +4,7 @@ import android.app.Activity.RESULT_OK import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope @@ -13,7 +14,6 @@ import kotlinx.coroutines.launch import org.tasks.BuildConfig import org.tasks.LocalBroadcastManager import org.tasks.R -import org.tasks.auth.AuthStateManager import org.tasks.auth.SignInActivity import org.tasks.billing.BillingClient import org.tasks.billing.Inventory @@ -29,7 +29,6 @@ import javax.inject.Inject @AndroidEntryPoint class TasksAccount : InjectingPreferenceFragment() { - @Inject lateinit var authStateManager: AuthStateManager @Inject lateinit var taskDeleter: TaskDeleter @Inject lateinit var billingClient: BillingClient @Inject lateinit var inventory: Inventory @@ -69,10 +68,7 @@ class TasksAccount : InjectingPreferenceFragment() { } findPreference(R.string.upgrade_to_pro).setOnPreferenceClickListener { - PurchaseDialog - .newPurchaseDialog(this, REQUEST_PURCHASE) - .show(parentFragmentManager, PurchaseDialog.FRAG_TAG_PURCHASE_DIALOG) - false + showPurchaseDialog() } findPreference(R.string.button_unsubscribe).setOnPreferenceClickListener { @@ -90,19 +86,26 @@ class TasksAccount : InjectingPreferenceFragment() { false } - findPreference(R.string.sign_in_with_google).setOnPreferenceClickListener { - activity?.startActivityForResult( - Intent(activity, SignInActivity::class.java), - Synchronization.REQUEST_TASKS_ORG) - false + if (isGitHubAccount) { + findPreference(R.string.upgrade_to_pro).isVisible = false + findPreference(R.string.button_unsubscribe).isVisible = false + findPreference(R.string.refresh_purchases).isVisible = false } refreshUi() } + private fun showPurchaseDialog(): Boolean { + PurchaseDialog + .newPurchaseDialog(this, REQUEST_PURCHASE) + .show(parentFragmentManager, PurchaseDialog.FRAG_TAG_PURCHASE_DIALOG) + return false + } + private fun removeAccount() = lifecycleScope.launch { + // try to delete session from caldav.tasks.org taskDeleter.delete(caldavAccount) - authStateManager.signOut() + inventory.updateTasksSubscription() activity?.onBackPressed() } @@ -121,9 +124,67 @@ class TasksAccount : InjectingPreferenceFragment() { localBroadcastManager.unregisterReceiver(purchaseReceiver) } + private val isGitHubAccount: Boolean + get() = caldavAccount.username?.startsWith("github") == true + private fun refreshUi() { (findPreference(R.string.sign_in_with_google) as IconPreference).apply { - isVisible = caldavAccount.isLoggedOut() + if (caldavAccount.error.isNullOrBlank()) { + isVisible = false + return + } + isVisible = true + when { + caldavAccount.isPaymentRequired() -> { + val subscription = inventory.subscription + if (isGitHubAccount) { + title = null + setSummary(R.string.insufficient_sponsorship) + if (BuildConfig.FLAVOR == "googleplay") { + onPreferenceClickListener = null + } else { + setOnPreferenceClickListener { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.url_sponsor)))) + false + } + } + } else { + setOnPreferenceClickListener { + showPurchaseDialog() + } + if (subscription == null) { + setTitle(R.string.upgrade_to_pro) + setSummary(R.string.your_subscription_expired) + } else { + setTitle(R.string.manage_subscription) + setSummary(R.string.insufficient_subscription) + } + } + } + caldavAccount.isLoggedOut() -> { + setTitle(if (isGitHubAccount) { + R.string.sign_in_with_github + } else { + R.string.sign_in_with_google + }) + setSummary(R.string.authentication_required) + setOnPreferenceClickListener { + activity?.startActivityForResult( + Intent(activity, SignInActivity::class.java) + .putExtra( + SignInActivity.EXTRA_SELECT_SERVICE, + if (isGitHubAccount) 1 else 0 + ), + Synchronization.REQUEST_TASKS_ORG) + false + } + } + else -> { + this.title = null + this.summary = caldavAccount.error + this.onPreferenceClickListener = null + } + } iconVisible = true } @@ -140,30 +201,20 @@ class TasksAccount : InjectingPreferenceFragment() { } val subscription = inventory.subscription findPreference(R.string.upgrade_to_pro).apply { - if (caldavAccount.isPaymentRequired()) { - if (subscription == null) { - setTitle(R.string.upgrade_to_pro) - setSummary(R.string.your_subscription_expired) - } else { - setTitle(R.string.manage_subscription) - setSummary(R.string.insufficient_subscription) - } + title = getString( + if (subscription == null) { + R.string.upgrade_to_pro + } else { + R.string.manage_subscription + }) + summary = if (subscription == null) { + null } else { - title = getString( - if (subscription == null) { - R.string.upgrade_to_pro - } else { - R.string.manage_subscription - }) - summary = if (subscription == null) { - null - } else { - val price = getString( - if (subscription.isMonthly) R.string.price_per_month else R.string.price_per_year, - (subscription.subscriptionPrice!! - .01).toString() - ) - getString(R.string.current_subscription, price) - } + val price = getString( + if (subscription.isMonthly) R.string.price_per_month else R.string.price_per_year, + (subscription.subscriptionPrice!! - .01).toString() + ) + getString(R.string.current_subscription, price) } } findPreference(R.string.button_unsubscribe).isEnabled = inventory.subscription != null diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index c8c96cdcc..cff87513f 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -238,4 +238,19 @@ 1 2 + + + @string/google_play_subscribers + @string/github_sponsors + + + + @string/sign_in_with_google + @string/sign_in_with_github + + + + @drawable/ic_google + @drawable/ic_octocat + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e1ed557c6..a01883443 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -662,6 +662,8 @@ File %1$s contained %2$s.\n\n Logged in %s Your subscription has expired. Subscribe now to resume service. Insufficient subscription level. Please upgrade your subscription to resume service. + No eligible GitHub sponsorship found + No eligible Google Play subscription found $%s/year $%s/yr $%s/month @@ -670,7 +672,11 @@ File %1$s contained %2$s.\n\n Purchases updated Follow r/tasks Authorization cancelled + Not signed in + Google Play subscribers + GitHub Sponsors Sign in with Google + Sign in with GitHub Authentication required Sponsor Migrate diff --git a/deps_fdroid.txt b/deps_fdroid.txt index 7f2d01fed..0015b075d 100644 --- a/deps_fdroid.txt +++ b/deps_fdroid.txt @@ -394,10 +394,6 @@ +| \--- com.squareup.okhttp3:logging-interceptor:3.12.1 -> 3.12.7 (*) ++--- 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 (*) +\--- com.github.openid:AppAuth-Android:27b62d5 + +--- androidx.browser:browser:1.2.0 + | +--- androidx.core:core:1.1.0 -> 1.3.2 (*) diff --git a/deps_googleplay.txt b/deps_googleplay.txt index 2f1d91717..27823b55d 100644 --- a/deps_googleplay.txt +++ b/deps_googleplay.txt @@ -506,10 +506,6 @@ +| \--- com.squareup.okhttp3:logging-interceptor:3.12.1 -> 3.12.7 (*) ++--- 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 (*) +\--- com.github.openid:AppAuth-Android:27b62d5 + +--- androidx.browser:browser:1.2.0 + | +--- androidx.core:core:1.1.0 -> 1.3.2 (*)