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