Top level Tasks.org sign in preference

pull/1228/head
Alex Baker 5 years ago
parent 337fa2646f
commit 9d37a20c95

@ -47,6 +47,19 @@ class Purchase(private val purchase: Purchase) {
return null return null
} }
val isTasksSubscription: Boolean
get() {
return subscriptionPrice
?.let {
if (isMonthly) {
it >= 3
} else {
it >= 30
}
}
?: false
}
companion object { companion object {
private val PATTERN = Pattern.compile("^(annual|monthly)_([0-3][0-9]|499)$") private val PATTERN = Pattern.compile("^(annual|monthly)_([0-3][0-9]|499)$")
} }

@ -38,6 +38,19 @@ class AuthStateManager @Inject constructor(@ApplicationContext private val conte
private val prefsLock = ReentrantLock() private val prefsLock = ReentrantLock()
private val currentAuthState = AtomicReference<AuthState>() private val currentAuthState = AtomicReference<AuthState>()
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 = current
val clearedState = currentState.authorizationServiceConfiguration
?.let { AuthState(it) }
?: return
if (currentState.lastRegistrationResponse != null) {
clearedState.update(currentState.lastRegistrationResponse)
}
replace(clearedState)
}
val current: AuthState val current: AuthState
get() { get() {
if (currentAuthState.get() != null) { if (currentAuthState.get() != null) {

@ -79,13 +79,6 @@ class AuthorizationService @Inject constructor(
} }
fun signOut() { fun signOut() {
// discard the authorization and token state, but retain the configuration and authStateManager.signOut()
// 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)
} }
} }

@ -21,10 +21,15 @@ import androidx.annotation.MainThread
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.exception.HttpException
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.openid.appauth.* import net.openid.appauth.*
import org.tasks.R import org.tasks.R
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.injection.InjectingAppCompatActivity import org.tasks.injection.InjectingAppCompatActivity
import org.tasks.themes.ThemeColor import org.tasks.themes.ThemeColor
import timber.log.Timber import timber.log.Timber
@ -43,18 +48,14 @@ import javax.inject.Inject
* configuration. * configuration.
* - Utilize dynamic client registration, if no static client id is specified. * - 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. * - 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 @AndroidEntryPoint
class SignInActivity : InjectingAppCompatActivity() { class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHandler {
@Inject lateinit var authService: AuthorizationService @Inject lateinit var authService: AuthorizationService
@Inject lateinit var authStateManager: AuthStateManager @Inject lateinit var authStateManager: AuthStateManager
@Inject lateinit var configuration: Configuration @Inject lateinit var configuration: Configuration
@Inject lateinit var themeColor: ThemeColor @Inject lateinit var themeColor: ThemeColor
@Inject lateinit var inventory: Inventory
private val viewModel: SignInViewModel by viewModels() private val viewModel: SignInViewModel by viewModels()
@ -66,13 +67,16 @@ class SignInActivity : InjectingAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel.error.observe(this, this::handleError)
if (authStateManager.current.isAuthorized && if (authStateManager.current.isAuthorized &&
!configuration.hasConfigurationChanged()) { !configuration.hasConfigurationChanged()) {
Timber.i("User is already authenticated, signing out") Timber.i("User is already authenticated, signing out")
authService.signOut() authStateManager.signOut()
} }
if (!configuration.isValid) { if (!configuration.isValid) {
displayError(configuration.configurationError) returnError(configuration.configurationError)
return return
} }
if (configuration.hasConfigurationChanged()) { if (configuration.hasConfigurationChanged()) {
@ -84,6 +88,14 @@ class SignInActivity : InjectingAppCompatActivity() {
mExecutor.submit { initializeAppAuth() } mExecutor.submit { initializeAppAuth() }
} }
private fun handleError(e: Throwable) {
if (e is HttpException && e.code == 402) {
newPurchaseDialog().show(supportFragmentManager, FRAG_TAG_PURCHASE_DIALOG)
} else {
returnError(e.message)
}
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
@ -100,14 +112,18 @@ class SignInActivity : InjectingAppCompatActivity() {
if (requestCode == RC_AUTH) { if (requestCode == RC_AUTH) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
lifecycleScope.launch { lifecycleScope.launch {
viewModel.handleResult(data!!) val account = try {
authStateManager.current.authorizationException?.let { e -> viewModel.handleResult(data!!)
displayError(e.message) } catch (e: Exception) {
returnError(e.message)
}
if (account != null) {
setResult(RESULT_OK)
finish()
} }
finish()
} }
} else { } else {
displayError(getString(R.string.authorization_cancelled)) returnError(getString(R.string.authorization_cancelled))
} }
} else { } else {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
@ -162,7 +178,7 @@ class SignInActivity : InjectingAppCompatActivity() {
ex: AuthorizationException?) { ex: AuthorizationException?) {
if (config == null) { if (config == null) {
Timber.i(ex, "Failed to retrieve discovery document") Timber.i(ex, "Failed to retrieve discovery document")
displayError("Failed to retrieve discovery document: " + ex!!.message) returnError("Failed to retrieve discovery document: " + ex!!.message)
return return
} }
Timber.i("Discovery document retrieved") Timber.i("Discovery document retrieved")
@ -237,7 +253,7 @@ class SignInActivity : InjectingAppCompatActivity() {
} }
@MainThread @MainThread
private fun displayError(error: String?) { private fun returnError(error: String?) {
Timber.e(error) Timber.e(error)
setResult(RESULT_CANCELED, Intent().putExtra(EXTRA_ERROR, error)) setResult(RESULT_CANCELED, Intent().putExtra(EXTRA_ERROR, error))
finish() finish()
@ -246,7 +262,7 @@ class SignInActivity : InjectingAppCompatActivity() {
// WrongThread inference is incorrect in this case // WrongThread inference is incorrect in this case
@AnyThread @AnyThread
private fun displayErrorLater(error: String) { private fun displayErrorLater(error: String) {
runOnUiThread { displayError(error) } runOnUiThread { returnError(error) }
} }
@MainThread @MainThread
@ -283,4 +299,18 @@ class SignInActivity : InjectingAppCompatActivity() {
const val EXTRA_ERROR = "extra_error" const val EXTRA_ERROR = "extra_error"
private const val RC_AUTH = 100 private const val RC_AUTH = 100
} }
override fun onPurchaseDialogDismissed() {
if (inventory.hasTasksSubscription) {
lifecycleScope.launch {
val account = viewModel.setupAccount(authStateManager.current)
if (account != null) {
setResult(RESULT_OK)
finish()
}
}
} else {
finish()
}
}
} }

@ -3,9 +3,11 @@ package org.tasks.auth
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.hilt.lifecycle.ViewModelInject import androidx.hilt.lifecycle.ViewModelInject
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.todoroo.astrid.helper.UUIDHelper import com.todoroo.astrid.helper.UUIDHelper
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import net.openid.appauth.AuthState
import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationResponse import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.ClientAuthentication.UnsupportedAuthenticationMethod import net.openid.appauth.ClientAuthentication.UnsupportedAuthenticationMethod
@ -22,6 +24,8 @@ class SignInViewModel @ViewModelInject constructor(
private val provider: CaldavClientProvider, private val provider: CaldavClientProvider,
private val caldavDao: CaldavDao private val caldavDao: CaldavDao
) : ViewModel() { ) : ViewModel() {
val error = MutableLiveData<Throwable>()
suspend fun handleResult(intent: Intent): CaldavAccount? { suspend fun handleResult(intent: Intent): CaldavAccount? {
val response = AuthorizationResponse.fromIntent(intent) val response = AuthorizationResponse.fromIntent(intent)
val ex = AuthorizationException.fromIntent(intent) val ex = AuthorizationException.fromIntent(intent)
@ -34,33 +38,46 @@ class SignInViewModel @ViewModelInject constructor(
authStateManager.updateAfterAuthorization(response, ex) authStateManager.updateAfterAuthorization(response, ex)
exchangeAuthorizationCode(response) exchangeAuthorizationCode(response)
} }
val auth = authStateManager.current
if (!auth.isAuthorized) { ex?.let {
error.value = ex
return null return null
} }
return authStateManager.current
.takeIf { it.isAuthorized }
?.let { setupAccount(it) }
}
suspend fun setupAccount(auth: AuthState): CaldavAccount? {
val tokenString = auth.idToken ?: return null val tokenString = auth.idToken ?: return null
val idToken = IdToken(tokenString) val idToken = IdToken(tokenString)
val username = "google_${idToken.sub}" val username = "google_${idToken.sub}"
val homeSet = provider try {
.forUrl( val homeSet = provider
"${context.getString(R.string.tasks_caldav_url)}/google_login", .forUrl(
token = tokenString "${context.getString(R.string.tasks_caldav_url)}/google_login",
) token = tokenString
.setForeground() )
.homeSet(token = tokenString) .setForeground()
return caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username) .homeSet(token = tokenString)
?.apply { return caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username)
error = null ?.apply {
caldavDao.update(this) error = null
} caldavDao.update(this)
?: CaldavAccount().apply { }
accountType = CaldavAccount.TYPE_TASKS ?: CaldavAccount().apply {
uuid = UUIDHelper.newUUID() accountType = CaldavAccount.TYPE_TASKS
url = homeSet uuid = UUIDHelper.newUUID()
this.username = username url = homeSet
name = idToken.email this.username = username
caldavDao.insert(this) name = idToken.email
} caldavDao.insert(this)
}
} catch (e: Exception) {
error.postValue(e)
}
return null
} }
private suspend fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) { private suspend fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) {

@ -14,7 +14,7 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener { class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var authorizationService: AuthorizationService @Inject lateinit var authStateManager: AuthStateManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -46,6 +46,9 @@ class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolba
finish() finish()
} }
override val needsPurchase: Boolean
get() = !inventory.hasTasksSubscription
override fun hasChanges() = override fun hasChanges() =
newName != caldavAccount!!.name newName != caldavAccount!!.name
|| binding.repeat.isChecked != caldavAccount!!.isSuppressRepeatingTasks || binding.repeat.isChecked != caldavAccount!!.isSuppressRepeatingTasks
@ -65,16 +68,10 @@ class TasksAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolba
override suspend fun updateAccount() = updateAccount(caldavAccount!!.url) override suspend fun updateAccount() = updateAccount(caldavAccount!!.url)
override suspend fun removeAccount() { override suspend fun removeAccount() {
authorizationService.signOut() authStateManager.signOut()
super.removeAccount() 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)
} }

@ -67,6 +67,9 @@ class Inventory @Inject constructor(
} }
.firstOrNull() .firstOrNull()
val hasTasksSubscription: Boolean
get() = subscription?.isTasksSubscription ?: false
companion object { companion object {
private const val SKU_VIP = "vip" private const val SKU_VIP = "vip"
const val SKU_TASKER = "tasker" const val SKU_TASKER = "tasker"

@ -29,6 +29,10 @@ import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class PurchaseDialog : DialogFragment(), OnPurchasesUpdated { class PurchaseDialog : DialogFragment(), OnPurchasesUpdated {
interface PurchaseHandler {
fun onPurchaseDialogDismissed()
}
private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() { private val purchaseReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
setup() setup()
@ -233,6 +237,9 @@ class PurchaseDialog : DialogFragment(), OnPurchasesUpdated {
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog) super.onDismiss(dialog)
activity.takeIf { it is PurchaseHandler }?.let {
(it as PurchaseHandler).onPurchaseDialogDismissed()
}
if (arguments?.getBoolean(EXTRA_FINISH_ACTIVITY, false) == true) { if (arguments?.getBoolean(EXTRA_FINISH_ACTIVITY, false) == true) {
activity?.finish() activity?.finish()
} }

@ -94,7 +94,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.name, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(binding.name, InputMethodManager.SHOW_IMPLICIT)
} }
if (!inventory.hasPro) { if (needsPurchase) {
newSnackbar(getString(R.string.this_feature_requires_a_subscription)) newSnackbar(getString(R.string.this_feature_requires_a_subscription))
.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE) .setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(R.string.button_subscribe) { .setAction(R.string.button_subscribe) {
@ -104,6 +104,9 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
} }
} }
protected open val needsPurchase: Boolean
get() = !inventory.hasPro
@get:StringRes @get:StringRes
protected abstract val description: Int protected abstract val description: Int

@ -1,10 +1,21 @@
package org.tasks.preferences.fragments package org.tasks.preferences.fragments
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.auth.AuthStateManager
import org.tasks.auth.IdToken
import org.tasks.auth.SignInActivity
import org.tasks.auth.TasksAccountSettingsActivity
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavDao
import org.tasks.injection.InjectingPreferenceFragment import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.IconPreference import org.tasks.preferences.IconPreference
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
@ -17,6 +28,8 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
@Inject lateinit var appWidgetManager: AppWidgetManager @Inject lateinit var appWidgetManager: AppWidgetManager
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var authStateManager: AuthStateManager
@Inject lateinit var caldavDao: CaldavDao
private val viewModel: PreferencesViewModel by activityViewModels() private val viewModel: PreferencesViewModel by activityViewModels()
@ -25,6 +38,10 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val pref = findPreference(R.string.tasks_org) as IconPreference
pref.setIcon(R.drawable.ic_round_icon_36dp)
pref.iconVisible = true
findPreference(R.string.synchronization).summary = findPreference(R.string.synchronization).summary =
resources resources
.getStringArray(R.array.synchronization_services) .getStringArray(R.array.synchronization_services)
@ -38,10 +55,59 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
lifecycleScope.launch {
updateAccount()
}
updateBackupWarning() updateBackupWarning()
updateWidgetVisibility() updateWidgetVisibility()
} }
private suspend fun updateAccount() {
val pref = findPreference(R.string.tasks_org) as IconPreference
pref.drawable = ContextCompat
.getDrawable(requireContext(), R.drawable.ic_keyboard_arrow_right_24px)
?.mutate()
pref.tint = context?.getColor(R.color.icon_tint_with_alpha)
val accounts = caldavDao.getAccounts(CaldavAccount.TYPE_TASKS)
if (accounts.isEmpty()) {
pref.setOnPreferenceClickListener { signIn() }
pref.summary = getString(R.string.sign_in_with_google)
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
if (!account.error.isNullOrBlank()) {
pref.drawable = ContextCompat
.getDrawable(requireContext(), R.drawable.ic_outline_error_outline_24px)
?.mutate()
pref.tint = context?.getColor(R.color.overdue)
}
pref.setOnPreferenceClickListener {
startActivity(
Intent(requireContext(), TasksAccountSettingsActivity::class.java)
.putExtra(BaseCaldavAccountSettingsActivity.EXTRA_CALDAV_DATA, account)
)
false
}
}
private fun signIn(): Boolean {
activity?.startActivityForResult(
Intent(activity, SignInActivity::class.java),
Synchronization.REQUEST_TASKS_ORG)
return false
}
private fun updateWidgetVisibility() { private fun updateWidgetVisibility() {
findPreference(R.string.widget_settings).isVisible = appWidgetManager.widgetIds.isNotEmpty() findPreference(R.string.widget_settings).isVisible = appWidgetManager.widgetIds.isNotEmpty()
} }

@ -18,6 +18,7 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.BaseCaldavAccountSettingsActivity import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.caldav.CaldavAccountSettingsActivity import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.data.CaldavAccount.Companion.TYPE_LOCAL import org.tasks.data.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.data.CaldavDao import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskAccount import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskListDao import org.tasks.data.GoogleTaskListDao
@ -27,7 +28,6 @@ import org.tasks.etesync.EteSyncAccountSettingsActivity
import org.tasks.injection.InjectingPreferenceFragment import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.jobs.WorkManager import org.tasks.jobs.WorkManager
import org.tasks.opentasks.OpenTaskAccountSettingsActivity import org.tasks.opentasks.OpenTaskAccountSettingsActivity
import org.tasks.auth.TasksAccountSettingsActivity
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.sync.AddAccountDialog.Companion.newAccountDialog import org.tasks.sync.AddAccountDialog.Companion.newAccountDialog
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
@ -140,7 +140,7 @@ class Synchronization : InjectingPreferenceFragment() {
private suspend fun addCaldavAccounts(category: PreferenceCategory): Boolean { private suspend fun addCaldavAccounts(category: PreferenceCategory): Boolean {
val accounts = caldavDao.getAccounts().filter { val accounts = caldavDao.getAccounts().filter {
it.accountType != TYPE_LOCAL it.accountType != TYPE_LOCAL && it.accountType != TYPE_TASKS
} }
for (account in accounts) { for (account in accounts) {
val preference = Preference(context) val preference = Preference(context)
@ -149,7 +149,6 @@ class Synchronization : InjectingPreferenceFragment() {
if (isNullOrEmpty(error)) { if (isNullOrEmpty(error)) {
preference.setSummary(when { preference.setSummary(when {
account.isCaldavAccount -> R.string.caldav account.isCaldavAccount -> R.string.caldav
account.isTasksOrg -> R.string.tasks_org
account.isEteSyncAccount account.isEteSyncAccount
|| (account.isOpenTasks || (account.isOpenTasks
&& account.uuid?.startsWith(ACCOUNT_TYPE_ETESYNC) == true) -> && account.uuid?.startsWith(ACCOUNT_TYPE_ETESYNC) == true) ->
@ -164,7 +163,6 @@ class Synchronization : InjectingPreferenceFragment() {
} }
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener { preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(context, when { val intent = Intent(context, when {
account.isTasksOrg -> TasksAccountSettingsActivity::class.java
account.isCaldavAccount -> CaldavAccountSettingsActivity::class.java account.isCaldavAccount -> CaldavAccountSettingsActivity::class.java
account.isEteSyncAccount -> EteSyncAccountSettingsActivity::class.java account.isEteSyncAccount -> EteSyncAccountSettingsActivity::class.java
account.isOpenTasks -> OpenTaskAccountSettingsActivity::class.java account.isOpenTasks -> OpenTaskAccountSettingsActivity::class.java

@ -15,13 +15,11 @@ import androidx.fragment.app.Fragment
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.auth.SignInActivity
import org.tasks.caldav.CaldavAccountSettingsActivity import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.etesync.EteSyncAccountSettingsActivity import org.tasks.etesync.EteSyncAccountSettingsActivity
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.themes.DrawableUtil import org.tasks.themes.DrawableUtil
import javax.inject.Inject import javax.inject.Inject
@ -47,7 +45,7 @@ class AddAccountDialog : DialogFragment() {
view.findViewById<TextView>(R.id.text2).text = descriptions[position] view.findViewById<TextView>(R.id.text2).text = descriptions[position]
val icon = view.findViewById<ImageView>(R.id.image_view) val icon = view.findViewById<ImageView>(R.id.image_view)
icon.setImageDrawable(DrawableUtil.getWrapped(context, icons[position])) icon.setImageDrawable(DrawableUtil.getWrapped(context, icons[position]))
if (position == 3) { if (position == 2) {
icon.drawable.setTint(context.getColor(R.color.icon_tint)) icon.drawable.setTint(context.getColor(R.color.icon_tint))
} }
return view return view
@ -59,17 +57,14 @@ class AddAccountDialog : DialogFragment() {
.setSingleChoiceItems(adapter, -1) { dialog, which -> .setSingleChoiceItems(adapter, -1) { dialog, which ->
when (which) { when (which) {
0 -> activity?.startActivityForResult( 0 -> activity?.startActivityForResult(
Intent(activity, SignInActivity::class.java),
REQUEST_TASKS_ORG)
1 -> activity?.startActivityForResult(
Intent(activity, GtasksLoginActivity::class.java), Intent(activity, GtasksLoginActivity::class.java),
REQUEST_GOOGLE_TASKS) REQUEST_GOOGLE_TASKS)
2 -> activity?.startActivity( 1 -> activity?.startActivity(
Intent(ACTION_VIEW, Uri.parse(getString(R.string.url_davx5)))) Intent(ACTION_VIEW, Uri.parse(getString(R.string.url_davx5))))
3 -> activity?.startActivityForResult( 2 -> activity?.startActivityForResult(
Intent(activity, CaldavAccountSettingsActivity::class.java), Intent(activity, CaldavAccountSettingsActivity::class.java),
REQUEST_CALDAV_SETTINGS) REQUEST_CALDAV_SETTINGS)
4 -> activity?.startActivityForResult( 3 -> activity?.startActivityForResult(
Intent(activity, EteSyncAccountSettingsActivity::class.java), Intent(activity, EteSyncAccountSettingsActivity::class.java),
REQUEST_CALDAV_SETTINGS) REQUEST_CALDAV_SETTINGS)
} }

@ -1,26 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="1146dp"
android:height="1146dp"
android:viewportWidth="1146"
android:viewportHeight="1146">
<group>
<clip-path
android:pathData="M0,0l1145.72,0l0,1145.72l-1145.72,0z"/>
<path
android:pathData="M572.65,571.88m-572.51,0a572.51,572.51 0,1 1,1145.02 0a572.51,572.51 45,1 1,-1145.02 0"
android:fillColor="#2196F3"/>
<group>
<clip-path
android:pathData="M572.65,571.88m-572.51,0a572.51,572.51 0,1 1,1145.02 0a572.51,572.51 45,1 1,-1145.02 0"/>
<path
android:fillColor="#FF000000"
android:pathData="M429.5,771.89L230.23,572.61L162.61,640.23L1146.31,1623.94L1719.76,1050.49L935.33,266.06L429.5,771.89Z"
android:fillType="nonZero"
android:fillAlpha="0.15"/>
</group>
</group>
<path
android:pathData="M429.5,771.88L230.22,572.61L162.61,640.23L429.5,907.12L1002.95,333.67L935.33,266.05L429.5,771.88Z"
android:fillColor="#ffffff"
android:fillType="nonZero"/>
</vector>

@ -0,0 +1,14 @@
<vector android:autoMirrored="true" android:height="36dp"
android:viewportHeight="1146" android:viewportWidth="1146"
android:width="36dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0,0l1145.72,0l0,1145.72l-1145.72,0z"/>
<path android:fillColor="#2196F3" android:pathData="M572.65,571.88m-572.51,0a572.51,572.51 0,1 1,1145.02 0a572.51,572.51 45,1 1,-1145.02 0"/>
<group>
<clip-path android:pathData="M572.65,571.88m-572.51,0a572.51,572.51 0,1 1,1145.02 0a572.51,572.51 45,1 1,-1145.02 0"/>
<path android:fillAlpha="0.15" android:fillColor="#FF000000"
android:fillType="nonZero" android:pathData="M429.5,771.89L230.23,572.61L162.61,640.23L1146.31,1623.94L1719.76,1050.49L935.33,266.06L429.5,771.89Z"/>
</group>
</group>
<path android:fillColor="#ffffff" android:fillType="nonZero" android:pathData="M429.5,771.88L230.22,572.61L162.61,640.23L429.5,907.12L1002.95,333.67L935.33,266.05L429.5,771.88Z"/>
</vector>

@ -657,7 +657,6 @@
<string name="insufficient_subscription">Unzureichende Abonnementstufe. Bitte aktualisieren Sie Ihr Abonnement, um den Dienst wieder aufzunehmen.</string> <string name="insufficient_subscription">Unzureichende Abonnementstufe. Bitte aktualisieren Sie Ihr Abonnement, um den Dienst wieder aufzunehmen.</string>
<string name="your_subscription_expired">Ihr Abonnement ist abgelaufen. Abonnieren Sie jetzt, um den Dienst fortzusetzen.</string> <string name="your_subscription_expired">Ihr Abonnement ist abgelaufen. Abonnieren Sie jetzt, um den Dienst fortzusetzen.</string>
<string name="logged_in">Angemeldet als %s</string> <string name="logged_in">Angemeldet als %s</string>
<string name="tasks_org_sync_description">Ihre Daten mit Tasks.org synchronisieren</string>
<string name="more_options">Weitere Optionen</string> <string name="more_options">Weitere Optionen</string>
<string name="background_location_permission_required">Diese Anwendung sammelt Standortdaten, um standortbezogene Erinnerungen zu ermöglichen, auch wenn die Anwendung geschlossen ist oder nicht verwendet wird.</string> <string name="background_location_permission_required">Diese Anwendung sammelt Standortdaten, um standortbezogene Erinnerungen zu ermöglichen, auch wenn die Anwendung geschlossen ist oder nicht verwendet wird.</string>
<string name="repeat_monthly_fifth_week">fünften</string> <string name="repeat_monthly_fifth_week">fünften</string>

@ -660,7 +660,6 @@
<string name="insufficient_subscription">Nivel de suscripción insuficiente. Actualice su suscripción para reanudar el servicio.</string> <string name="insufficient_subscription">Nivel de suscripción insuficiente. Actualice su suscripción para reanudar el servicio.</string>
<string name="your_subscription_expired">Vuestra suscripción ha expirado. Suscribe ahora a resume servicio.</string> <string name="your_subscription_expired">Vuestra suscripción ha expirado. Suscribe ahora a resume servicio.</string>
<string name="logged_in">Autenticado como %s</string> <string name="logged_in">Autenticado como %s</string>
<string name="tasks_org_sync_description">Sincronice sus datos con Tasks.org</string>
<string name="more_options">Más opciones</string> <string name="more_options">Más opciones</string>
<string name="background_location_permission_required">Esta aplicación recopila datos de ubicación para habilitar recordatorios basados en la ubicación incluso cuando la aplicación está cerrada o no está en uso.</string> <string name="background_location_permission_required">Esta aplicación recopila datos de ubicación para habilitar recordatorios basados en la ubicación incluso cuando la aplicación está cerrada o no está en uso.</string>
<string name="repeat_monthly_fifth_week">quinto</string> <string name="repeat_monthly_fifth_week">quinto</string>

@ -655,7 +655,6 @@
<string name="insufficient_subscription">Niveau d\'abonnement insuffisant. Veuillez mettre votre abonnement à niveau pour reprendre le service.</string> <string name="insufficient_subscription">Niveau d\'abonnement insuffisant. Veuillez mettre votre abonnement à niveau pour reprendre le service.</string>
<string name="your_subscription_expired">Votre abonnement a expiré. Abonnez-vous dès maintenant pour reprendre le service.</string> <string name="your_subscription_expired">Votre abonnement a expiré. Abonnez-vous dès maintenant pour reprendre le service.</string>
<string name="logged_in">Connecté en tant que %s</string> <string name="logged_in">Connecté en tant que %s</string>
<string name="tasks_org_sync_description">Synchroniser vos données avec Tasks.org</string>
<string name="more_options">Plus doptions</string> <string name="more_options">Plus doptions</string>
<string name="background_location_permission_required">Cette application collecte des données de localisation pour activer les rappels basés sur la localisation, même lorsque lapplication est fermée ou non en cours dutilisation.</string> <string name="background_location_permission_required">Cette application collecte des données de localisation pour activer les rappels basés sur la localisation, même lorsque lapplication est fermée ou non en cours dutilisation.</string>
<string name="repeat_monthly_fifth_week">cinquième</string> <string name="repeat_monthly_fifth_week">cinquième</string>

@ -656,7 +656,6 @@
<string name="logged_in">Logolva itt: %s</string> <string name="logged_in">Logolva itt: %s</string>
<string name="custom_filter_is_subtask">Alfeladat</string> <string name="custom_filter_is_subtask">Alfeladat</string>
<string name="custom_filter_has_subtask">Van alfeladata</string> <string name="custom_filter_has_subtask">Van alfeladata</string>
<string name="tasks_org_sync_description">Adatok szinkronizálása a Tasks.org-al</string>
<string name="more_options">Több lehetőség</string> <string name="more_options">Több lehetőség</string>
<string name="cancel">Mégse</string> <string name="cancel">Mégse</string>
<string name="ok">OK</string> <string name="ok">OK</string>

@ -445,7 +445,6 @@
<string name="upgrade_blurb_4">Dukungan Anda sangat berarti bagi saya, terima kasih!</string> <string name="upgrade_blurb_4">Dukungan Anda sangat berarti bagi saya, terima kasih!</string>
<string name="invalid_username_or_password">username dan password salah</string> <string name="invalid_username_or_password">username dan password salah</string>
<string name="davx5_selection_description">Sinkronkan tugas Anda dengan aplikasi DAVx⁵</string> <string name="davx5_selection_description">Sinkronkan tugas Anda dengan aplikasi DAVx⁵</string>
<string name="tasks_org_sync_description">Sinkronkan data Anda dengan Tasks.org</string>
<string name="disable_sort_groups">Nonaktifkan grup sortir</string> <string name="disable_sort_groups">Nonaktifkan grup sortir</string>
<string name="improve_performance_summary">Nonaktifkan grup sortir dan subtugas yang bisa diciutkan untuk meningkatkan kinerja aplikasi</string> <string name="improve_performance_summary">Nonaktifkan grup sortir dan subtugas yang bisa diciutkan untuk meningkatkan kinerja aplikasi</string>
<string name="improve_performance">Meningkatkan kinerja</string> <string name="improve_performance">Meningkatkan kinerja</string>

@ -660,7 +660,6 @@
<string name="insufficient_subscription">Livello di abbonamento insufficiente. Per favore aggiornalo per riattivare il servizio.</string> <string name="insufficient_subscription">Livello di abbonamento insufficiente. Per favore aggiornalo per riattivare il servizio.</string>
<string name="your_subscription_expired">Il tuo abbonamento è scaduto. Abbonati ora per riattivare il servizio.</string> <string name="your_subscription_expired">Il tuo abbonamento è scaduto. Abbonati ora per riattivare il servizio.</string>
<string name="logged_in">Collegato come %s</string> <string name="logged_in">Collegato come %s</string>
<string name="tasks_org_sync_description">Sincronizzare i tuoi dati con Tasks.org</string>
<string name="more_options">Altre opzioni</string> <string name="more_options">Altre opzioni</string>
<string name="background_location_permission_required">Questa app raccoglie i dati di localizzazione per abilitare i promemoria basati sulla posizione, anche quando è chiusa o non è in uso.</string> <string name="background_location_permission_required">Questa app raccoglie i dati di localizzazione per abilitare i promemoria basati sulla posizione, anche quando è chiusa o non è in uso.</string>
<string name="repeat_monthly_fifth_week">quinto</string> <string name="repeat_monthly_fifth_week">quinto</string>

@ -693,7 +693,6 @@
<string name="logged_in">נכנסת %s</string> <string name="logged_in">נכנסת %s</string>
<string name="custom_filter_is_subtask">הוא תת־משימה</string> <string name="custom_filter_is_subtask">הוא תת־משימה</string>
<string name="custom_filter_has_subtask">יש תת־משימות</string> <string name="custom_filter_has_subtask">יש תת־משימות</string>
<string name="tasks_org_sync_description">סנכרון הנתונים שלך מול Tasks.org</string>
<string name="more_options">אפשרויות נוספות</string> <string name="more_options">אפשרויות נוספות</string>
<string name="repeat_monthly_fifth_week">החמישי</string> <string name="repeat_monthly_fifth_week">החמישי</string>
<string name="always_display_full_date">הצגת התאריך המלא</string> <string name="always_display_full_date">הצגת התאריך המלא</string>

@ -660,7 +660,6 @@
<string name="price_per_year">$%s/år</string> <string name="price_per_year">$%s/år</string>
<string name="your_subscription_expired">Ditt abonnement har utløpt. Abonner nå for å fortsette tjenesten.</string> <string name="your_subscription_expired">Ditt abonnement har utløpt. Abonner nå for å fortsette tjenesten.</string>
<string name="logged_in">Innlogget som %s</string> <string name="logged_in">Innlogget som %s</string>
<string name="tasks_org_sync_description">Synkroniser din data med Tasks.org</string>
<string name="more_options">Flere innstillinger</string> <string name="more_options">Flere innstillinger</string>
<string name="follow_reddit">Følg r/tasks</string> <string name="follow_reddit">Følg r/tasks</string>
<string name="background_location_permission_required">Dette programmet posisjonsdata for å kunne utløse posisjonsbaserte påminnelser, selv når programmet er lukket eller ikke i bruk.</string> <string name="background_location_permission_required">Dette programmet posisjonsdata for å kunne utløse posisjonsbaserte påminnelser, selv når programmet er lukket eller ikke i bruk.</string>

@ -655,7 +655,6 @@
<string name="insufficient_subscription">Abonnementsniveau onvoldoende. Verhoog a.u.b. je abonnement om deze dienst te hervatten.</string> <string name="insufficient_subscription">Abonnementsniveau onvoldoende. Verhoog a.u.b. je abonnement om deze dienst te hervatten.</string>
<string name="your_subscription_expired">Je abonnement is verlopen. Abonneer je nu om deze dienst te hervatten.</string> <string name="your_subscription_expired">Je abonnement is verlopen. Abonneer je nu om deze dienst te hervatten.</string>
<string name="logged_in">Ingelogd als %s</string> <string name="logged_in">Ingelogd als %s</string>
<string name="tasks_org_sync_description">Synchroniseer je data met Tasks.org</string>
<string name="more_options">Meer opties</string> <string name="more_options">Meer opties</string>
<string name="follow_reddit">Volg r/tasks</string> <string name="follow_reddit">Volg r/tasks</string>
<string name="background_location_permission_required">Deze applicatie verzamelt locatiegegevens om locatie-gebaseerde herinneringen mogelijk te maken, zelfs als de applicatie gesloten is of niet gebruikt wordt.</string> <string name="background_location_permission_required">Deze applicatie verzamelt locatiegegevens om locatie-gebaseerde herinneringen mogelijk te maken, zelfs als de applicatie gesloten is of niet gebruikt wordt.</string>

@ -677,7 +677,6 @@
<string name="insufficient_subscription">Недостаточный уровень подписки. Обновите подписку, чтобы возобновить обслуживание.</string> <string name="insufficient_subscription">Недостаточный уровень подписки. Обновите подписку, чтобы возобновить обслуживание.</string>
<string name="your_subscription_expired">Срок действия вашей подписки истек. Подпишитесь сейчас, чтобы возобновить обслуживание.</string> <string name="your_subscription_expired">Срок действия вашей подписки истек. Подпишитесь сейчас, чтобы возобновить обслуживание.</string>
<string name="logged_in">Вы вошли как %s</string> <string name="logged_in">Вы вошли как %s</string>
<string name="tasks_org_sync_description">Синхронизируйте свои данные с Tasks.org</string>
<string name="more_options">Дополнительные параметры</string> <string name="more_options">Дополнительные параметры</string>
<string name="background_location_permission_required">Это приложение собирает данные о местоположении, чтобы активировать напоминания на основе местоположения, даже когда приложение закрыто или не используется.</string> <string name="background_location_permission_required">Это приложение собирает данные о местоположении, чтобы активировать напоминания на основе местоположения, даже когда приложение закрыто или не используется.</string>
</resources> </resources>

@ -662,7 +662,6 @@
<string name="insufficient_subscription">Yetersiz abonelik düzeyi. Hizmeti devam ettirmek için lütfen aboneliğinizi yükseltin.</string> <string name="insufficient_subscription">Yetersiz abonelik düzeyi. Hizmeti devam ettirmek için lütfen aboneliğinizi yükseltin.</string>
<string name="your_subscription_expired">Aboneliğinizin süresi doldu. Hizmeti devam ettirmek için şimdi abone olun.</string> <string name="your_subscription_expired">Aboneliğinizin süresi doldu. Hizmeti devam ettirmek için şimdi abone olun.</string>
<string name="logged_in">Oturum açıldı: %s</string> <string name="logged_in">Oturum açıldı: %s</string>
<string name="tasks_org_sync_description">Verilerinizi Tasks.org ile eşzamanlayın</string>
<string name="more_options">Daha fazla seçenek</string> <string name="more_options">Daha fazla seçenek</string>
<string name="repeat_monthly_fifth_week">beşinci</string> <string name="repeat_monthly_fifth_week">beşinci</string>
<string name="authorization_cancelled">Yetkilendirme iptal edildi</string> <string name="authorization_cancelled">Yetkilendirme iptal edildi</string>

@ -642,7 +642,6 @@
<string name="ok"></string> <string name="ok"></string>
<string name="custom_filter_is_subtask">是子任务</string> <string name="custom_filter_is_subtask">是子任务</string>
<string name="custom_filter_has_subtask">有子任务</string> <string name="custom_filter_has_subtask">有子任务</string>
<string name="tasks_org_sync_description">用Tasks.org同步你的数据</string>
<string name="purchases_updated">购买已更新</string> <string name="purchases_updated">购买已更新</string>
<string name="current_subscription">当前订阅:%s</string> <string name="current_subscription">当前订阅:%s</string>
<string name="price_per_month_abbreviated">$%s/月</string> <string name="price_per_month_abbreviated">$%s/月</string>

@ -186,7 +186,6 @@
</string-array> </string-array>
<string-array name="synchronization_services"> <string-array name="synchronization_services">
<item>@string/tasks_org</item>
<item>@string/gtasks_GPr_header</item> <item>@string/gtasks_GPr_header</item>
<item>@string/davx5</item> <item>@string/davx5</item>
<item>@string/caldav</item> <item>@string/caldav</item>
@ -194,7 +193,6 @@
</string-array> </string-array>
<string-array name="synchronization_services_description"> <string-array name="synchronization_services_description">
<item>@string/tasks_org_sync_description</item>
<item>@string/google_tasks_selection_description</item> <item>@string/google_tasks_selection_description</item>
<item>@string/davx5_selection_description</item> <item>@string/davx5_selection_description</item>
<item>@string/caldav_selection_description</item> <item>@string/caldav_selection_description</item>
@ -202,7 +200,6 @@
</string-array> </string-array>
<array name="synchronization_services_icons"> <array name="synchronization_services_icons">
<item>@drawable/ic_round_icon</item>
<item>@drawable/ic_google</item> <item>@drawable/ic_google</item>
<item>@drawable/ic_davx5_icon_green_bg</item> <item>@drawable/ic_davx5_icon_green_bg</item>
<item>@drawable/ic_webdav_logo</item> <item>@drawable/ic_webdav_logo</item>

@ -322,6 +322,7 @@ File %1$s contained %2$s.\n\n
<string name="google_drive_backup">Google Drive backup</string> <string name="google_drive_backup">Google Drive backup</string>
<string name="miscellaneous">Miscellaneous</string> <string name="miscellaneous">Miscellaneous</string>
<string name="synchronization">Synchronization</string> <string name="synchronization">Synchronization</string>
<string name="synchronization_third_party">Third-party synchronization</string>
<string name="subtasks">Subtasks</string> <string name="subtasks">Subtasks</string>
<string name="enabled">Enabled</string> <string name="enabled">Enabled</string>
<string name="font_size">Font size</string> <string name="font_size">Font size</string>
@ -548,7 +549,6 @@ File %1$s contained %2$s.\n\n
<string name="enter_tag_name">Enter tag name</string> <string name="enter_tag_name">Enter tag name</string>
<string name="create_new_tag">Create \"%s\"</string> <string name="create_new_tag">Create \"%s\"</string>
<string name="choose_synchronization_service">Select a platform</string> <string name="choose_synchronization_service">Select a platform</string>
<string name="tasks_org_sync_description">Synchronize your data with Tasks.org</string>
<string name="google_tasks_selection_description">Basic service that synchronizes with your Google account</string> <string name="google_tasks_selection_description">Basic service that synchronizes with your Google account</string>
<string name="caldav_selection_description">Synchronization based on open internet standards</string> <string name="caldav_selection_description">Synchronization based on open internet standards</string>
<string name="etesync_selection_description">Open source, end-to-end encrypted synchronization</string> <string name="etesync_selection_description">Open source, end-to-end encrypted synchronization</string>
@ -666,4 +666,5 @@ File %1$s contained %2$s.\n\n
<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> <string name="authorization_cancelled">Authorization cancelled</string>
<string name="sign_in_with_google">Sign in with Google</string>
</resources> </resources>

@ -1,11 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<org.tasks.preferences.IconPreference
android:key="@string/tasks_org"
android:layout="@layout/preference_icon"
android:title="@string/tasks_org"
android:summary=" "
tools:summary="@string/sign_in_with_google" />
<Preference <Preference
app:fragment="org.tasks.preferences.fragments.LookAndFeel" app:fragment="org.tasks.preferences.fragments.LookAndFeel"
app:icon="@drawable/ic_outline_palette_24px" app:icon="@drawable/ic_outline_palette_24px"
app:title="@string/preferences_look_and_feel" /> app:title="@string/preferences_look_and_feel"
app:allowDividerAbove="true" />
<Preference <Preference
app:fragment="org.tasks.preferences.fragments.Notifications" app:fragment="org.tasks.preferences.fragments.Notifications"
@ -16,7 +25,7 @@
android:key="@string/synchronization" android:key="@string/synchronization"
app:fragment="org.tasks.preferences.fragments.Synchronization" app:fragment="org.tasks.preferences.fragments.Synchronization"
app:icon="@drawable/ic_outline_cloud_24px" app:icon="@drawable/ic_outline_cloud_24px"
app:title="@string/synchronization" /> app:title="@string/synchronization_third_party" />
<Preference <Preference
app:fragment="org.tasks.preferences.fragments.TaskDefaults" app:fragment="org.tasks.preferences.fragments.TaskDefaults"

Loading…
Cancel
Save