diff --git a/app/src/googleplay/java/org/tasks/billing/Purchase.kt b/app/src/googleplay/java/org/tasks/billing/Purchase.kt index 8bd7e73e9..a5aa13f15 100644 --- a/app/src/googleplay/java/org/tasks/billing/Purchase.kt +++ b/app/src/googleplay/java/org/tasks/billing/Purchase.kt @@ -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)$") } diff --git a/app/src/main/java/org/tasks/auth/AuthStateManager.kt b/app/src/main/java/org/tasks/auth/AuthStateManager.kt index 632598911..be1e33bc4 100644 --- a/app/src/main/java/org/tasks/auth/AuthStateManager.kt +++ b/app/src/main/java/org/tasks/auth/AuthStateManager.kt @@ -38,6 +38,19 @@ class AuthStateManager @Inject constructor(@ApplicationContext private val conte private val prefsLock = ReentrantLock() private val currentAuthState = AtomicReference() + 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) { diff --git a/app/src/main/java/org/tasks/auth/AuthorizationService.kt b/app/src/main/java/org/tasks/auth/AuthorizationService.kt index f5bbde12c..1669496b8 100644 --- a/app/src/main/java/org/tasks/auth/AuthorizationService.kt +++ b/app/src/main/java/org/tasks/auth/AuthorizationService.kt @@ -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() } } \ 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 36b884ccb..1f31f4a36 100644 --- a/app/src/main/java/org/tasks/auth/SignInActivity.kt +++ b/app/src/main/java/org/tasks/auth/SignInActivity.kt @@ -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() + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/auth/SignInViewModel.kt b/app/src/main/java/org/tasks/auth/SignInViewModel.kt index 80fc73644..20c466e09 100644 --- a/app/src/main/java/org/tasks/auth/SignInViewModel.kt +++ b/app/src/main/java/org/tasks/auth/SignInViewModel.kt @@ -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() + 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) { diff --git a/app/src/main/java/org/tasks/auth/TasksAccountSettingsActivity.kt b/app/src/main/java/org/tasks/auth/TasksAccountSettingsActivity.kt index 8e1b24cf8..56c5e6bae 100644 --- a/app/src/main/java/org/tasks/auth/TasksAccountSettingsActivity.kt +++ b/app/src/main/java/org/tasks/auth/TasksAccountSettingsActivity.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/billing/Inventory.kt b/app/src/main/java/org/tasks/billing/Inventory.kt index a9b8743d9..2636c2dfb 100644 --- a/app/src/main/java/org/tasks/billing/Inventory.kt +++ b/app/src/main/java/org/tasks/billing/Inventory.kt @@ -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" diff --git a/app/src/main/java/org/tasks/billing/PurchaseDialog.kt b/app/src/main/java/org/tasks/billing/PurchaseDialog.kt index e28b80e9a..ab3adaabb 100644 --- a/app/src/main/java/org/tasks/billing/PurchaseDialog.kt +++ b/app/src/main/java/org/tasks/billing/PurchaseDialog.kt @@ -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() } diff --git a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt index d844d005e..ad891851e 100644 --- a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt @@ -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 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 fcc5d6f2b..4d9ec323e 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt @@ -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() } diff --git a/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt b/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt index bc152a623..31a88041b 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/Synchronization.kt @@ -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 diff --git a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt index 50ec83d4c..d5175881a 100644 --- a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt +++ b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt @@ -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(R.id.text2).text = descriptions[position] val icon = view.findViewById(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) } diff --git a/app/src/main/res/drawable/ic_round_icon.xml b/app/src/main/res/drawable/ic_round_icon.xml deleted file mode 100644 index 21e9a9807..000000000 --- a/app/src/main/res/drawable/ic_round_icon.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_round_icon_36dp.xml b/app/src/main/res/drawable/ic_round_icon_36dp.xml new file mode 100644 index 000000000..751ad0bcb --- /dev/null +++ b/app/src/main/res/drawable/ic_round_icon_36dp.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4823de5e6..2c72cb22f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -657,7 +657,6 @@ Unzureichende Abonnementstufe. Bitte aktualisieren Sie Ihr Abonnement, um den Dienst wieder aufzunehmen. Ihr Abonnement ist abgelaufen. Abonnieren Sie jetzt, um den Dienst fortzusetzen. Angemeldet als %s - Ihre Daten mit Tasks.org synchronisieren Weitere Optionen Diese Anwendung sammelt Standortdaten, um standortbezogene Erinnerungen zu ermöglichen, auch wenn die Anwendung geschlossen ist oder nicht verwendet wird. fünften diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 637f5a4e2..e4c70b9b7 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -660,7 +660,6 @@ Nivel de suscripción insuficiente. Actualice su suscripción para reanudar el servicio. Vuestra suscripción ha expirado. Suscribe ahora a resume servicio. Autenticado como %s - Sincronice sus datos con Tasks.org Más opciones 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. quinto diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d717b2101..9cba07e1d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -655,7 +655,6 @@ Niveau d\'abonnement insuffisant. Veuillez mettre votre abonnement à niveau pour reprendre le service. Votre abonnement a expiré. Abonnez-vous dès maintenant pour reprendre le service. Connecté en tant que %s - Synchroniser vos données avec Tasks.org Plus d’options Cette application collecte des données de localisation pour activer les rappels basés sur la localisation, même lorsque l’application est fermée ou non en cours d’utilisation. cinquième diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 0353908c2..3afcb3340 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -656,7 +656,6 @@ Logolva itt: %s Alfeladat Van alfeladata - Adatok szinkronizálása a Tasks.org-al Több lehetőség Mégse OK diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 66fcf5371..820bfb4ab 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -445,7 +445,6 @@ Dukungan Anda sangat berarti bagi saya, terima kasih! username dan password salah Sinkronkan tugas Anda dengan aplikasi DAVx⁵ - Sinkronkan data Anda dengan Tasks.org Nonaktifkan grup sortir Nonaktifkan grup sortir dan subtugas yang bisa diciutkan untuk meningkatkan kinerja aplikasi Meningkatkan kinerja diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f5bd0d8f7..451bb0d3c 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -660,7 +660,6 @@ Livello di abbonamento insufficiente. Per favore aggiornalo per riattivare il servizio. Il tuo abbonamento è scaduto. Abbonati ora per riattivare il servizio. Collegato come %s - Sincronizzare i tuoi dati con Tasks.org Altre opzioni Questa app raccoglie i dati di localizzazione per abilitare i promemoria basati sulla posizione, anche quando è chiusa o non è in uso. quinto diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 52b68671d..576680cbc 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -693,7 +693,6 @@ נכנסת %s הוא תת־משימה יש תת־משימות - סנכרון הנתונים שלך מול Tasks.org אפשרויות נוספות החמישי הצגת התאריך המלא diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 283b7c1fe..ed5e6ff30 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -660,7 +660,6 @@ $%s/år Ditt abonnement har utløpt. Abonner nå for å fortsette tjenesten. Innlogget som %s - Synkroniser din data med Tasks.org Flere innstillinger Følg r/tasks Dette programmet posisjonsdata for å kunne utløse posisjonsbaserte påminnelser, selv når programmet er lukket eller ikke i bruk. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index a5d341f71..96c20575d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -655,7 +655,6 @@ Abonnementsniveau onvoldoende. Verhoog a.u.b. je abonnement om deze dienst te hervatten. Je abonnement is verlopen. Abonneer je nu om deze dienst te hervatten. Ingelogd als %s - Synchroniseer je data met Tasks.org Meer opties Volg r/tasks Deze applicatie verzamelt locatiegegevens om locatie-gebaseerde herinneringen mogelijk te maken, zelfs als de applicatie gesloten is of niet gebruikt wordt. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2d7a09be4..ad4b9c760 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -677,7 +677,6 @@ Недостаточный уровень подписки. Обновите подписку, чтобы возобновить обслуживание. Срок действия вашей подписки истек. Подпишитесь сейчас, чтобы возобновить обслуживание. Вы вошли как %s - Синхронизируйте свои данные с Tasks.org Дополнительные параметры Это приложение собирает данные о местоположении, чтобы активировать напоминания на основе местоположения, даже когда приложение закрыто или не используется. \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index cdb2c1e31..7b2095b58 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -662,7 +662,6 @@ Yetersiz abonelik düzeyi. Hizmeti devam ettirmek için lütfen aboneliğinizi yükseltin. Aboneliğinizin süresi doldu. Hizmeti devam ettirmek için şimdi abone olun. Oturum açıldı: %s - Verilerinizi Tasks.org ile eşzamanlayın Daha fazla seçenek beşinci Yetkilendirme iptal edildi diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2a31545c0..bf947785b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -642,7 +642,6 @@ 是子任务 有子任务 - 用Tasks.org同步你的数据 购买已更新 当前订阅:%s $%s/月 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 2cb88b1c9..527c7a126 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -186,7 +186,6 @@ - @string/tasks_org @string/gtasks_GPr_header @string/davx5 @string/caldav @@ -194,7 +193,6 @@ - @string/tasks_org_sync_description @string/google_tasks_selection_description @string/davx5_selection_description @string/caldav_selection_description @@ -202,7 +200,6 @@ - @drawable/ic_round_icon @drawable/ic_google @drawable/ic_davx5_icon_green_bg @drawable/ic_webdav_logo diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e5da2ec75..8ea98466e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -322,6 +322,7 @@ File %1$s contained %2$s.\n\n Google Drive backup Miscellaneous Synchronization + Third-party synchronization Subtasks Enabled Font size @@ -548,7 +549,6 @@ File %1$s contained %2$s.\n\n Enter tag name Create \"%s\" Select a platform - Synchronize your data with Tasks.org Basic service that synchronizes with your Google account Synchronization based on open internet standards Open source, end-to-end encrypted synchronization @@ -666,4 +666,5 @@ File %1$s contained %2$s.\n\n Purchases updated Follow r/tasks Authorization cancelled + Sign in with Google diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 914bb3e0b..f0eb6f010 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,11 +1,20 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + + app:title="@string/preferences_look_and_feel" + app:allowDividerAbove="true" /> + app:title="@string/synchronization_third_party" />