diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be9978293c225f7313c9efe74254fb448d6f955e..be2a2ce0f616c4c85b7f509ded73485d6f9b6a27 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -819,6 +819,8 @@ + + diff --git a/app/src/main/kotlin/at/bitfire/davdroid/App.kt b/app/src/main/kotlin/at/bitfire/davdroid/App.kt index 1f9f8ac9b48fb091e43e81dd722361f26d1f1767..833c6d5fe05e417e973318701e26edc747ac39a4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/App.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/App.kt @@ -14,6 +14,7 @@ import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import at.bitfire.davdroid.log.Logger import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener +import at.bitfire.davdroid.syncadapter.SyncAdapterComponentManager import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationUtils @@ -100,6 +101,8 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide // don't block UI for some background checks thread { + SyncAdapterComponentManager.updateComponents(this) + // watch for account changes/deletions accountsUpdatedListener.listen() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt index 40f23a5a35317e72cdf0e83223c04ce280a489ea..0ddf36488576b4323c6ed889ef0a5204f5c837c5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/Constants.kt @@ -35,5 +35,4 @@ object Constants { const val MURENA_DAV_URL = "https://murena.io/remote.php/dav" - const val E_BROWSER_PACKAGE_NAME = "foundation.e.browser" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt index 90c3aa7eceabf8915e67d2b0147e2639ef93a991..6b4210bac101f81b358731876c58def3755289b0 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt @@ -5,7 +5,7 @@ package at.bitfire.davdroid.network import android.content.Context -import at.bitfire.davdroid.Constants +import android.content.pm.PackageManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,9 +20,13 @@ import java.net.URL @InstallIn(SingletonComponent::class) object OAuthModule { + private const val E_BROWSER_PACKAGE_NAME = "foundation.e.browser" + @Provides - fun authorizationService(@ApplicationContext context: Context): AuthorizationService = - AuthorizationService(context, + fun authorizationService(@ApplicationContext context: Context): AuthorizationService { + val isEBrowserInstalled = isInstalled(context, E_BROWSER_PACKAGE_NAME) + return AuthorizationService( + context, AppAuthConfiguration.Builder() .setConnectionBuilder { uri -> val url = URL(uri.toString()) @@ -30,7 +34,22 @@ object OAuthModule { setRequestProperty("User-Agent", HttpClient.UserAgentInterceptor.userAgent) } } - .setBrowserMatcher { it.packageName == Constants.E_BROWSER_PACKAGE_NAME } + .setBrowserMatcher { browser -> + if (isEBrowserInstalled) { + browser.packageName == E_BROWSER_PACKAGE_NAME + } else { + true + } + } .build() ) + } + + private fun isInstalled(context: Context, packageName: String): Boolean = + try { + context.packageManager.getPackageInfo(packageName, 0) + true + } catch (_: PackageManager.NameNotFoundException) { + false + } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt index d8578412fac6be19c706825fcc1dbd23d9bd2e6f..33a781c01909a942c842e1a1438af0e0fb195d92 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/receiver/BootCompletedReceiver.kt @@ -20,6 +20,7 @@ import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService import at.bitfire.davdroid.murenasso.MurenaSsoMigrationService.Companion.NOTIFICATION_TAG_MURENA_SSO import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.syncadapter.AccountUtils +import at.bitfire.davdroid.syncadapter.SyncAdapterComponentManager import at.bitfire.davdroid.ui.NotificationUtils import at.bitfire.davdroid.ui.NotificationUtils.CHANNEL_GENERAL import at.bitfire.davdroid.ui.NotificationUtils.NOTIFY_MIGRATE_TO_MURENA_SSO @@ -55,6 +56,8 @@ class BootCompletedReceiver: BroadcastReceiver() { } private fun initializeSync(context: Context) { + SyncAdapterComponentManager.updateComponents(context) + AccountUtils.getMainAccounts(context) .forEach { // sync intervals are checked in App.onCreate() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncAdapterComponentManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncAdapterComponentManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..2efc345630bb50bec16b80eb82af2489ef276199 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/syncadapter/SyncAdapterComponentManager.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2026 e Foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package at.bitfire.davdroid.syncadapter + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import at.bitfire.davdroid.R +import at.bitfire.davdroid.log.Logger +import java.util.logging.Level + +object SyncAdapterComponentManager { + + private data class SyncServiceSpec( + val serviceClass: Class<*>, + val authorityResId: Int + ) + + private val optionalSyncServices = listOf( + SyncServiceSpec(MurenaTasksSyncAdapterService::class.java, R.string.task_authority), + SyncServiceSpec(MurenaNotesSyncAdapterService::class.java, R.string.notes_authority), + SyncServiceSpec(MurenaEmailSyncAdapterService::class.java, R.string.email_authority), + SyncServiceSpec(MurenaMediaSyncAdapterService::class.java, R.string.media_authority), + SyncServiceSpec(MurenaAppDataSyncAdapterService::class.java, R.string.app_data_authority), + SyncServiceSpec(MurenaMeteredEdriveSyncAdapterService::class.java, R.string.metered_edrive_authority), + SyncServiceSpec(MurenaPasswordSyncAdapterService::class.java, R.string.password_authority), + SyncServiceSpec(GoogleTasksSyncAdapterService::class.java, R.string.task_authority), + SyncServiceSpec(GoogleEmailSyncAdapterService::class.java, R.string.email_authority), + SyncServiceSpec(YahooTasksSyncAdapterService::class.java, R.string.task_authority), + SyncServiceSpec(YahooEmailSyncAdapterService::class.java, R.string.email_authority) + ) + + fun updateComponents(context: Context) { + val packageManager = context.packageManager + + for (syncService in optionalSyncServices) { + val authority = context.getString(syncService.authorityResId) + val isProviderAvailable = hasProvider(packageManager, authority) + setComponentEnabled(packageManager, context, syncService.serviceClass, isProviderAvailable, authority) + } + } + + private fun hasProvider(packageManager: PackageManager, authority: String): Boolean = + packageManager.resolveContentProvider(authority, 0) != null + + private fun setComponentEnabled( + packageManager: PackageManager, + context: Context, + serviceClass: Class<*>, + enabled: Boolean, + authority: String + ) { + val component = ComponentName(context, serviceClass) + val desiredState = if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED + + if (packageManager.getComponentEnabledSetting(component) == desiredState) { + return + } + + packageManager.setComponentEnabledSetting(component, desiredState, PackageManager.DONT_KILL_APP) + Logger.log.log(Level.INFO, "Set ${serviceClass.simpleName} to ${if (enabled) "ENABLED" else "DISABLED"} (authority=$authority)") + } +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt index a3d6e1b7391685cd3017200bd27ab193027fbceb..c79d58288fb4b74289b67a1cda4a36d6e8f42571 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt @@ -21,6 +21,7 @@ import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.syncadapter.SyncWorker import at.bitfire.davdroid.ui.setup.LoginActivity +import at.bitfire.davdroid.util.PermissionUtils import com.google.android.material.navigation.NavigationView import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint @@ -35,6 +36,7 @@ class AccountsActivity : AppCompatActivity(), NavigationView.OnNavigationItemSel private lateinit var binding: ActivityAccountsBinding val model by viewModels() + private var hasAskedRequiredPermissions = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -79,6 +81,11 @@ class AccountsActivity : AppCompatActivity(), NavigationView.OnNavigationItemSel override fun onResume() { super.onResume() accountsDrawerHandler.initMenu(this, binding.navView.menu) + + if (!hasAskedRequiredPermissions && PermissionUtils.collectMissingRequiredPermissions(this).isNotEmpty()) { + hasAskedRequiredPermissions = true + startActivity(Intent(this, PermissionsActivity::class.java)) + } } override fun onNavigationItemSelected(item: MenuItem): Boolean { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt index 659538a083ad172d4a61db508fb35c26adac301e..f616c5a67982987d4710a6ba3139993198917a5b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsActivity.kt @@ -12,10 +12,11 @@ class PermissionsActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (savedInstanceState == null) + if (savedInstanceState == null) { supportFragmentManager.beginTransaction() - .add(android.R.id.content, PermissionsFragment()) - .commit() + .add(android.R.id.content, PermissionsFragment()) + .commit() + } } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsFragment.kt index 6a75b1e91d773e381dec11d7771d5ca9fa1ac687..dafa126c788787c1495f39f9a631e61c74f7530f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsFragment.kt @@ -16,6 +16,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.MainThread import androidx.core.content.ContextCompat @@ -25,31 +26,43 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.PackageChangedReceiver +import at.bitfire.davdroid.R +import at.bitfire.davdroid.databinding.ActivityPermissionsBinding import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS import at.bitfire.davdroid.util.PermissionUtils.havePermissions -import at.bitfire.davdroid.R -import at.bitfire.davdroid.databinding.ActivityPermissionsBinding import at.bitfire.ical4android.TaskProvider import at.bitfire.ical4android.TaskProvider.ProviderName class PermissionsFragment: Fragment() { val model by viewModels() - + private lateinit var requestPermission: ActivityResultLauncher> + private var hasAskedRequiredPermissions = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val binding = ActivityPermissionsBinding.inflate(inflater, container, false) binding.lifecycleOwner = viewLifecycleOwner binding.model = model - binding.text.text = getString(R.string.permissions_text, getString(R.string.app_name)) - val requestPermission = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { + initPermissionLauncher() + observePermissionToggles() + binding.appSettings.setOnClickListener { + PermissionUtils.showAppSettings(requireActivity()) + } + + return binding.root + } + + private fun initPermissionLauncher() { + requestPermission = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { model.checkPermissions() } + } + private fun observePermissionToggles() { model.needAutoResetPermission.observe(viewLifecycleOwner) { keepPermissions -> if (keepPermissions == true && model.haveAutoResetPermission.value == false) { Toast.makeText(requireActivity(), R.string.permissions_autoreset_instruction, Toast.LENGTH_LONG).show() @@ -95,17 +108,25 @@ class PermissionsFragment: Fragment() { requestPermission.launch(all.toTypedArray()) } } - - binding.appSettings.setOnClickListener { - PermissionUtils.showAppSettings(requireActivity()) - } - - return binding.root } override fun onResume() { super.onResume() model.checkPermissions() + requestMissingPermissions() + } + + private fun requestMissingPermissions() { + if (hasAskedRequiredPermissions) { + return + } + + val missing = PermissionUtils.collectMissingRequiredPermissions(requireContext()) + + if (missing.isNotEmpty()) { + hasAskedRequiredPermissions = true + requestPermission.launch(missing.toTypedArray()) + } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt index 626f9f4b0f8688cc980fd0886bbf4d0c0e1543c0..920d696549b4605f6fd327e2931fec52679ee231 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt @@ -13,6 +13,11 @@ import androidx.activity.addCallback import androidx.annotation.WorkerThread import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import at.bitfire.davdroid.R import at.bitfire.davdroid.log.Logger import com.github.appintro.AppIntro2 @@ -50,23 +55,6 @@ class IntroActivity: AppIntro2() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val factories = EntryPointAccessors.fromActivity(this, IntroActivityEntryPoint::class.java).introFragmentFactories() - for (factory in factories) - Logger.log.fine("Found intro fragment factory ${factory::class.java} with order ${factory.getOrder(this)}") - - val factoriesWithOrder = factories - .associateWith { it.getOrder(this) } - .filterValues { it != IntroFragmentFactory.DONT_SHOW } - - val anyPositiveOrder = factoriesWithOrder.values.any { it > 0 } - if (anyPositiveOrder) { - val factoriesSortedByOrder = factoriesWithOrder - .toList() - .sortedBy { (_, v) -> v } // sort by value (= getOrder()) - for ((factory, _) in factoriesSortedByOrder) - addSlide(factory.create()) - } - setBarColor(ResourcesCompat.getColor(resources, R.color.accentColor, null)) isSkipButtonEnabled = false @@ -78,6 +66,31 @@ class IntroActivity: AppIntro2() { goToPreviousSlide() } } + + val factories = EntryPointAccessors.fromActivity(this, IntroActivityEntryPoint::class.java).introFragmentFactories() + + lifecycleScope.launch { + val factoriesWithOrder = withContext(Dispatchers.IO) { + factories.associateWith { it.getOrder(this@IntroActivity) } + .filterValues { it != IntroFragmentFactory.DONT_SHOW } + } + + for (factory in factories) { + Logger.log.fine("Found intro fragment factory ${factory::class.java} with order ${factoriesWithOrder[factory]}") + } + + if (!lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + return@launch + } + + val anyPositiveOrder = factoriesWithOrder.values.any { it > 0 } + if (anyPositiveOrder) { + factoriesWithOrder + .toList() + .sortedBy { (_, v) -> v } + .forEach { (factory, _) -> addSlide(factory.create()) } + } + } } override fun onPageSelected(position: Int) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt index 3488f84deba7988f463e12614c8716f8797d1433..11def19678f9566843d0990e3a164f4c03bfb563 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroFragmentFactory.kt @@ -5,6 +5,7 @@ package at.bitfire.davdroid.ui.intro import android.content.Context +import androidx.annotation.WorkerThread import androidx.fragment.app.Fragment interface IntroFragmentFactory { @@ -25,6 +26,7 @@ interface IntroFragmentFactory { * * [DONT_SHOW] (0): don't show the fragment * * ≥0: show the fragment (lower numbers are shown first) */ + @WorkerThread fun getOrder(context: Context): Int /** diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt index 98df793fd3383a5ed2047ab80f706f82e217aefc..dbf9ebefead5f231bbd5f482aff7b1cf986472df 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/PermissionsIntroFragment.kt @@ -10,11 +10,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import at.bitfire.davdroid.R import at.bitfire.davdroid.util.PermissionUtils import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS -import at.bitfire.davdroid.R -import at.bitfire.ical4android.TaskProvider import javax.inject.Inject class PermissionsIntroFragment : Fragment() { @@ -26,19 +25,17 @@ class PermissionsIntroFragment : Fragment() { class Factory @Inject constructor(): IntroFragmentFactory { override fun getOrder(context: Context): Int { - // show PermissionsFragment as intro fragment when no permissions are granted - val permissions = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS + - TaskProvider.PERMISSIONS_JTX + - TaskProvider.PERMISSIONS_OPENTASKS + - TaskProvider.PERMISSIONS_TASKS_ORG - return if (PermissionUtils.haveAnyPermission(context, permissions)) + val permissions = requiredPermissions() + return if (PermissionUtils.havePermissions(context, permissions)) IntroFragmentFactory.DONT_SHOW else 50 } + private fun requiredPermissions(): Array = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS + override fun create() = PermissionsIntroFragment() } -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt index a7dfc99762c90d78b7f6ee2ae85a78dda1dd6e70..1552c762465917b49aac8bd47095f27b40083d74 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/LoginActivity.kt @@ -5,9 +5,11 @@ package at.bitfire.davdroid.ui.setup import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import at.bitfire.davdroid.log.Logger +import at.bitfire.davdroid.util.PermissionUtils import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -54,6 +56,10 @@ class LoginActivity : AppCompatActivity() { @Inject lateinit var loginFragmentFactories: Map + private var hasAskedRequiredPermissions = false + private val requestRequiredPermissions = registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions() + ) { } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -79,4 +85,22 @@ class LoginActivity : AppCompatActivity() { } } } + + override fun onStart() { + super.onStart() + requestMissingPermissions() + } + + private fun requestMissingPermissions() { + if (hasAskedRequiredPermissions) { + return + } + + val missing = PermissionUtils.collectMissingRequiredPermissions(this) + + if (missing.isNotEmpty()) { + hasAskedRequiredPermissions = true + requestRequiredPermissions.launch(missing.toTypedArray()) + } + } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt index 5a417b26431a08cbddcd49d449a7610803897ee8..11854ef583255b381beb1fdaf06a4f975c660c70 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationBaseFragment.kt @@ -123,7 +123,11 @@ abstract class OpenIdAuthenticationBaseFragment(private val identityProvider: Id return } - viewModel.requestAuthCode(serviceConfiguration, requireActivity().intent) + val isAuthorizationLaunched = viewModel.requestAuthCode(serviceConfiguration, requireActivity().intent) + if (!isAuthorizationLaunched) { + showAuthorizationToast() + return + } requireActivity().setResult(Activity.RESULT_OK) requireActivity().finish() } @@ -204,6 +208,11 @@ abstract class OpenIdAuthenticationBaseFragment(private val identityProvider: Id finishActivity() } + private fun showAuthorizationToast() { + Toast.makeText(requireContext(), R.string.login_oauth_couldnt_open_authorization, Toast.LENGTH_LONG).show() + finishActivity() + } + protected fun getAuthState(): AuthState = viewModel.getAuthState() protected fun proceedNext(userName: String, baseUrl: String, cardDavUrl: String? = null) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt index a0d2d1e20fe4b3b5d0d0b625670fa1d376e63c53..9783966e3b533cb33e05580d49a82cceb354ddce 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/OpenIdAuthenticationViewModel.kt @@ -18,6 +18,7 @@ package at.bitfire.davdroid.ui.setup import android.app.Application import android.app.PendingIntent +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Build import androidx.annotation.WorkerThread @@ -93,7 +94,7 @@ class OpenIdAuthenticationViewModel @Inject constructor( fun requestAuthCode( serviceConfiguration: AuthorizationServiceConfiguration, intent: Intent - ) { + ): Boolean { authState = AuthState(serviceConfiguration) val provider = requireNotNull(identityProvider) { "identityProvider must be set before requestAuthCode()" } @@ -117,11 +118,17 @@ class OpenIdAuthenticationViewModel @Inject constructor( .setLoginHint(sanitizeHint(loginHint)) .build() - authorizationService.performAuthorizationRequest( - authRequest, - createPostAuthorizationIntent(authRequest, intent), - authorizationService.createCustomTabsIntentBuilder().build() - ) + return try { + authorizationService.performAuthorizationRequest( + authRequest, + createPostAuthorizationIntent(authRequest, intent), + authorizationService.createCustomTabsIntentBuilder().build() + ) + true + } catch (e: ActivityNotFoundException) { + Logger.log.log(Level.WARNING, "Couldn't start OpenID authorization intent", e) + false + } } private fun sanitizeHint(hint: String?): String? { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index d23070485f84e28a917428b417fdc241a2ff0023..ac793f4766d0fa76161d64f91c427d1ef58333a1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -104,6 +104,17 @@ object PermissionUtils { fun havePermissions(context: Context, permissions: Array) = permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } + fun collectMissingRequiredPermissions(context: Context): List { + val required = CONTACT_PERMISSIONS + CALENDAR_PERMISSIONS + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + arrayOf(Manifest.permission.POST_NOTIFICATIONS) + else + emptyArray() + return required.filter { + ContextCompat.checkSelfPermission(context, it) != PackageManager.PERMISSION_GRANTED + } + } + /** * Shows a notification about missing permissions. * diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 057d8f613dbdf5184734139a7290ea11bf6574fc..12910ff57a13608659d4ad87e13106d95d5be246 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -498,6 +498,7 @@ Datenschutzrichtliniefür mehr Informationen.]]> Google API Services Nutzerdaten-Richtlinie, inklusive der eingeschränkten Nutzungsbedingungen.]]> Authentifizierungscode konnte nicht abgerufen werden + Autorisierungsseite konnte nicht geöffnet werden Wieder anmelden Erneut mit OAuth anmelden Benutzername @@ -517,4 +518,4 @@ VPN ohne zugrundeliegende überprüfte Internetverbindung reicht für Synchronisierung nicht aus (empfohlen) Anmeldung erforderlich Melden Sie sich an, um auf Murena Workspace online in /e/OS zuzugreifen. - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e8aaf30fdbde9aca2ca29b59c0c2d7ac6de30eb8..c7100a28b963c43265346dc1fcf14537eaeb92e2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -492,6 +492,7 @@ Cuenta Google OK No se pudo obtener el código de autorización + No se pudo abrir la página de autorización Error al iniciar sesión, inténtalo más tarde Puede que recibas advertencias inesperadas y/o tengas que crear tu propio ID de cliente. Esto iniciará el flujo de inicio de sesión de Nextcloud en un navegador web. @@ -516,4 +517,4 @@ Error de autenticación. Por favor, usar credenciales válidas Inicio de sesión requerido Inicia sesión para acceder a Murena Workspace en línea en /e/OS. - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2bf55040ef788556f87b1336f756c363f2accaa4..da24185d67c2b4acf1fd1a3a9bdd802dd2759c20 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -494,6 +494,7 @@ Compte Google Se connecter avec Google N\'a pas pu obtenir le code d\'autorisation + Impossible d\'ouvrir la page d\'autorisation nom d\'utilisateur Dernièrement synchronisé : Jamais synchronisé @@ -524,4 +525,4 @@ Plus tard Connexion requise Connectez-vous pour accéder à Murena Workspace en ligne dans /e/OS. - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 25d39a8bf3d4a779e965ecd1bc957135980b0ba5..73d8e7432c021f9d8bf0de5ec43a1350cbc332f3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -448,6 +448,7 @@ Per impostare un account protetto con l\'autenticazione a due fattori all\'interno di /e/OS, è necessario creare una application password dedicata nelle impostazioni dell\'account su murena.io. Autenticazione a Due Fattori Nome utente (indirizzo e-mail) / password errati\? + Impossibile aprire la pagina di autorizzazione Installa un certificato Non è stato trovato alcun certificato Usa il certificato client diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 61dafd445ba4359418a016e245b049f204a08fb5..79e157dfc2643cc572ba1cb04d0339b1c8bc4ecb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -326,6 +326,7 @@ Privacy policy for details.]]> Google API Services User Data Policy, including the Limited Use requirements.]]> Couldn\'t obtain authorization code + Couldn\'t open authorization page Nextcloud Login with Nextcloud Login Flow