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
}
val isTasksSubscription: Boolean
get() {
return subscriptionPrice
?.let {
if (isMonthly) {
it >= 3
} else {
it >= 30
}
}
?: false
}
companion object {
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 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
get() {
if (currentAuthState.get() != null) {

@ -79,13 +79,6 @@ class AuthorizationService @Inject constructor(
}
fun signOut() {
// discard the authorization and token state, but retain the configuration and
// dynamic client registration (if applicable), to save from retrieving them again.
val currentState = authStateManager.current
val clearedState = AuthState(currentState.authorizationServiceConfiguration!!)
if (currentState.lastRegistrationResponse != null) {
clearedState.update(currentState.lastRegistrationResponse)
}
authStateManager.replace(clearedState)
authStateManager.signOut()
}
}

@ -21,10 +21,15 @@ import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.browser.customtabs.CustomTabsIntent
import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.exception.HttpException
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import net.openid.appauth.*
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.themes.ThemeColor
import timber.log.Timber
@ -43,18 +48,14 @@ import javax.inject.Inject
* configuration.
* - Utilize dynamic client registration, if no static client id is specified.
* - Initiate the authorization request using the built-in heuristics or a user-selected browser.
*
* _NOTE_: From a clean checkout of this project, the authorization service is not configured.
* Edit `res/values/auth_config.xml` to provide the required configuration properties. See the
* README.md in the app/ directory for configuration instructions, and the adjacent IDP-specific
* instructions.
*/
@AndroidEntryPoint
class SignInActivity : InjectingAppCompatActivity() {
class SignInActivity : InjectingAppCompatActivity(), PurchaseDialog.PurchaseHandler {
@Inject lateinit var authService: AuthorizationService
@Inject lateinit var authStateManager: AuthStateManager
@Inject lateinit var configuration: Configuration
@Inject lateinit var themeColor: ThemeColor
@Inject lateinit var inventory: Inventory
private val viewModel: SignInViewModel by viewModels()
@ -66,13 +67,16 @@ class SignInActivity : InjectingAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.error.observe(this, this::handleError)
if (authStateManager.current.isAuthorized &&
!configuration.hasConfigurationChanged()) {
Timber.i("User is already authenticated, signing out")
authService.signOut()
authStateManager.signOut()
}
if (!configuration.isValid) {
displayError(configuration.configurationError)
returnError(configuration.configurationError)
return
}
if (configuration.hasConfigurationChanged()) {
@ -84,6 +88,14 @@ class SignInActivity : InjectingAppCompatActivity() {
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() {
super.onStop()
@ -100,14 +112,18 @@ class SignInActivity : InjectingAppCompatActivity() {
if (requestCode == RC_AUTH) {
if (resultCode == RESULT_OK) {
lifecycleScope.launch {
viewModel.handleResult(data!!)
authStateManager.current.authorizationException?.let { e ->
displayError(e.message)
val account = try {
viewModel.handleResult(data!!)
} catch (e: Exception) {
returnError(e.message)
}
if (account != null) {
setResult(RESULT_OK)
finish()
}
finish()
}
} else {
displayError(getString(R.string.authorization_cancelled))
returnError(getString(R.string.authorization_cancelled))
}
} else {
super.onActivityResult(requestCode, resultCode, data)
@ -162,7 +178,7 @@ class SignInActivity : InjectingAppCompatActivity() {
ex: AuthorizationException?) {
if (config == null) {
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
}
Timber.i("Discovery document retrieved")
@ -237,7 +253,7 @@ class SignInActivity : InjectingAppCompatActivity() {
}
@MainThread
private fun displayError(error: String?) {
private fun returnError(error: String?) {
Timber.e(error)
setResult(RESULT_CANCELED, Intent().putExtra(EXTRA_ERROR, error))
finish()
@ -246,7 +262,7 @@ class SignInActivity : InjectingAppCompatActivity() {
// WrongThread inference is incorrect in this case
@AnyThread
private fun displayErrorLater(error: String) {
runOnUiThread { displayError(error) }
runOnUiThread { returnError(error) }
}
@MainThread
@ -283,4 +299,18 @@ class SignInActivity : InjectingAppCompatActivity() {
const val EXTRA_ERROR = "extra_error"
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.Intent
import androidx.hilt.lifecycle.ViewModelInject
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
@ -22,6 +24,8 @@ class SignInViewModel @ViewModelInject constructor(
private val provider: CaldavClientProvider,
private val caldavDao: CaldavDao
) : ViewModel() {
val error = MutableLiveData<Throwable>()
suspend fun handleResult(intent: Intent): CaldavAccount? {
val response = AuthorizationResponse.fromIntent(intent)
val ex = AuthorizationException.fromIntent(intent)
@ -34,33 +38,46 @@ class SignInViewModel @ViewModelInject constructor(
authStateManager.updateAfterAuthorization(response, ex)
exchangeAuthorizationCode(response)
}
val auth = authStateManager.current
if (!auth.isAuthorized) {
ex?.let {
error.value = ex
return null
}
return authStateManager.current
.takeIf { it.isAuthorized }
?.let { setupAccount(it) }
}
suspend fun setupAccount(auth: AuthState): CaldavAccount? {
val tokenString = auth.idToken ?: return null
val idToken = IdToken(tokenString)
val username = "google_${idToken.sub}"
val homeSet = provider
.forUrl(
"${context.getString(R.string.tasks_caldav_url)}/google_login",
token = tokenString
)
.setForeground()
.homeSet(token = tokenString)
return caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username)
?.apply {
error = null
caldavDao.update(this)
}
?: CaldavAccount().apply {
accountType = CaldavAccount.TYPE_TASKS
uuid = UUIDHelper.newUUID()
url = homeSet
this.username = username
name = idToken.email
caldavDao.insert(this)
}
try {
val homeSet = provider
.forUrl(
"${context.getString(R.string.tasks_caldav_url)}/google_login",
token = tokenString
)
.setForeground()
.homeSet(token = tokenString)
return caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username)
?.apply {
error = null
caldavDao.update(this)
}
?: CaldavAccount().apply {
accountType = CaldavAccount.TYPE_TASKS
uuid = UUIDHelper.newUUID()
url = homeSet
this.username = username
name = idToken.email
caldavDao.insert(this)
}
} catch (e: Exception) {
error.postValue(e)
}
return null
}
private suspend fun exchangeAuthorizationCode(authorizationResponse: AuthorizationResponse) {

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

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

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

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

@ -1,10 +1,21 @@
package org.tasks.preferences.fragments
import android.content.Intent
import android.os.Bundle
import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
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.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.preferences.IconPreference
import org.tasks.preferences.Preferences
@ -17,6 +28,8 @@ 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()
@ -25,6 +38,10 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
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 =
resources
.getStringArray(R.array.synchronization_services)
@ -38,10 +55,59 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
override fun onResume() {
super.onResume()
lifecycleScope.launch {
updateAccount()
}
updateBackupWarning()
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() {
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.CaldavAccountSettingsActivity
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.GoogleTaskAccount
import org.tasks.data.GoogleTaskListDao
@ -27,7 +28,6 @@ import org.tasks.etesync.EteSyncAccountSettingsActivity
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.jobs.WorkManager
import org.tasks.opentasks.OpenTaskAccountSettingsActivity
import org.tasks.auth.TasksAccountSettingsActivity
import org.tasks.preferences.Preferences
import org.tasks.sync.AddAccountDialog.Companion.newAccountDialog
import org.tasks.sync.SyncAdapters
@ -140,7 +140,7 @@ class Synchronization : InjectingPreferenceFragment() {
private suspend fun addCaldavAccounts(category: PreferenceCategory): Boolean {
val accounts = caldavDao.getAccounts().filter {
it.accountType != TYPE_LOCAL
it.accountType != TYPE_LOCAL && it.accountType != TYPE_TASKS
}
for (account in accounts) {
val preference = Preference(context)
@ -149,7 +149,6 @@ class Synchronization : InjectingPreferenceFragment() {
if (isNullOrEmpty(error)) {
preference.setSummary(when {
account.isCaldavAccount -> R.string.caldav
account.isTasksOrg -> R.string.tasks_org
account.isEteSyncAccount
|| (account.isOpenTasks
&& account.uuid?.startsWith(ACCOUNT_TYPE_ETESYNC) == true) ->
@ -164,7 +163,6 @@ class Synchronization : InjectingPreferenceFragment() {
}
preference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
val intent = Intent(context, when {
account.isTasksOrg -> TasksAccountSettingsActivity::class.java
account.isCaldavAccount -> CaldavAccountSettingsActivity::class.java
account.isEteSyncAccount -> EteSyncAccountSettingsActivity::class.java
account.isOpenTasks -> OpenTaskAccountSettingsActivity::class.java

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

@ -655,7 +655,6 @@
<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="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="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>

@ -656,7 +656,6 @@
<string name="logged_in">Logolva itt: %s</string>
<string name="custom_filter_is_subtask">Alfeladat</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="cancel">Mégse</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="invalid_username_or_password">username dan password salah</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="improve_performance_summary">Nonaktifkan grup sortir dan subtugas yang bisa diciutkan untuk meningkatkan kinerja aplikasi</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="your_subscription_expired">Il tuo abbonamento è scaduto. Abbonati ora per riattivare il servizio.</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="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>

@ -693,7 +693,6 @@
<string name="logged_in">נכנסת %s</string>
<string name="custom_filter_is_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="repeat_monthly_fifth_week">החמישי</string>
<string name="always_display_full_date">הצגת התאריך המלא</string>

@ -660,7 +660,6 @@
<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="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="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>

@ -655,7 +655,6 @@
<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="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="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>

@ -677,7 +677,6 @@
<string name="insufficient_subscription">Недостаточный уровень подписки. Обновите подписку, чтобы возобновить обслуживание.</string>
<string name="your_subscription_expired">Срок действия вашей подписки истек. Подпишитесь сейчас, чтобы возобновить обслуживание.</string>
<string name="logged_in">Вы вошли как %s</string>
<string name="tasks_org_sync_description">Синхронизируйте свои данные с Tasks.org</string>
<string name="more_options">Дополнительные параметры</string>
<string name="background_location_permission_required">Это приложение собирает данные о местоположении, чтобы активировать напоминания на основе местоположения, даже когда приложение закрыто или не используется.</string>
</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="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="tasks_org_sync_description">Verilerinizi Tasks.org ile eşzamanlayın</string>
<string name="more_options">Daha fazla seçenek</string>
<string name="repeat_monthly_fifth_week">beşinci</string>
<string name="authorization_cancelled">Yetkilendirme iptal edildi</string>

@ -642,7 +642,6 @@
<string name="ok"></string>
<string name="custom_filter_is_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="current_subscription">当前订阅:%s</string>
<string name="price_per_month_abbreviated">$%s/月</string>

@ -186,7 +186,6 @@
</string-array>
<string-array name="synchronization_services">
<item>@string/tasks_org</item>
<item>@string/gtasks_GPr_header</item>
<item>@string/davx5</item>
<item>@string/caldav</item>
@ -194,7 +193,6 @@
</string-array>
<string-array name="synchronization_services_description">
<item>@string/tasks_org_sync_description</item>
<item>@string/google_tasks_selection_description</item>
<item>@string/davx5_selection_description</item>
<item>@string/caldav_selection_description</item>
@ -202,7 +200,6 @@
</string-array>
<array name="synchronization_services_icons">
<item>@drawable/ic_round_icon</item>
<item>@drawable/ic_google</item>
<item>@drawable/ic_davx5_icon_green_bg</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="miscellaneous">Miscellaneous</string>
<string name="synchronization">Synchronization</string>
<string name="synchronization_third_party">Third-party synchronization</string>
<string name="subtasks">Subtasks</string>
<string name="enabled">Enabled</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="create_new_tag">Create \"%s\"</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="caldav_selection_description">Synchronization based on open internet standards</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="follow_reddit">Follow r/tasks</string>
<string name="authorization_cancelled">Authorization cancelled</string>
<string name="sign_in_with_google">Sign in with Google</string>
</resources>

@ -1,11 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<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
app:fragment="org.tasks.preferences.fragments.LookAndFeel"
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
app:fragment="org.tasks.preferences.fragments.Notifications"
@ -16,7 +25,7 @@
android:key="@string/synchronization"
app:fragment="org.tasks.preferences.fragments.Synchronization"
app:icon="@drawable/ic_outline_cloud_24px"
app:title="@string/synchronization" />
app:title="@string/synchronization_third_party" />
<Preference
app:fragment="org.tasks.preferences.fragments.TaskDefaults"

Loading…
Cancel
Save