291 clean up oauth mess (bitfireAT/davx5#307)

* Move GoogleOAuth members into GoogleLoginFragment

* Require login flow capable browser and notify user if missing

* Receive AppAuth redirects only in standard and gplay flavor

* Set davx5 as user-agent for AppAuth connection builder

* Re-authentication in Account settings

* Catch unauthorized exceptions at collection refresh and notify user to re-authenticate

* Suggest email address on account creation

* Set contact groups default setting as per-contact categories for oauth logins

* Add authentication to debug info, minor other changes

* Better error handling; don't pre-set group type

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Sunik Kupfer 2023-06-30 12:42:37 +02:00 committed by Ricki Hirner
parent 6a2c366358
commit 0f92b0fb05
No known key found for this signature in database
GPG key ID: 79A019FCAAEDD3AA
18 changed files with 229 additions and 118 deletions

View file

@ -28,10 +28,6 @@ android {
buildConfigField "String", "userAgent", "\"DAVx5\""
manifestPlaceholders = [
'appAuthRedirectScheme': applicationId
]
testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner"
kapt {
@ -72,8 +68,9 @@ android {
}
sourceSets {
androidTest.java.srcDirs = [ "src/androidTest/java" ]
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
androidTest {
assets.srcDirs += files("$projectDir/schemas".toString())
}
}
signingConfigs {

View file

@ -64,6 +64,10 @@
<service android:name=".ForegroundService"/>
<!-- Remove the node added by AppAuth -->
<activity android:name="net.openid.appauth.RedirectUriReceiverActivity"
tools:node="remove"/>
<activity android:name=".ui.intro.IntroActivity" android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ui.AccountsActivity"

View file

@ -1,36 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.network
import android.net.Uri
import at.bitfire.davdroid.BuildConfig
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.ResponseTypeValues
object GoogleOAuth {
// davx5integration@gmail.com (for davx5-ose)
private const val CLIENT_ID = "1069050168830-eg09u4tk1cmboobevhm4k3bj1m4fav9i.apps.googleusercontent.com"
val SCOPES = arrayOf(
"https://www.googleapis.com/auth/calendar", // CalDAV
"https://www.googleapis.com/auth/carddav" // CardDAV
)
private val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://accounts.google.com/o/oauth2/v2/auth"),
Uri.parse("https://oauth2.googleapis.com/token")
)
fun authRequestBuilder(clientId: String?) =
AuthorizationRequest.Builder(
serviceConfig,
clientId ?: CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse(BuildConfig.APPLICATION_ID + ":/oauth/redirect")
)
}

View file

@ -293,11 +293,12 @@ class HttpClient private constructor(
}
private object UserAgentInterceptor: Interceptor {
object UserAgentInterceptor: Interceptor {
// use Locale.ROOT because numbers may be encoded as non-ASCII characters in other locales
private val userAgentDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.ROOT)
private val userAgentDate = userAgentDateFormat.format(Date(BuildConfig.buildTime))
private val userAgent = "${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4jvm; " +
val userAgent = "${BuildConfig.userAgent}/${BuildConfig.VERSION_NAME} ($userAgentDate; dav4jvm; " +
"okhttp/${OkHttp.VERSION}) Android/${Build.VERSION.RELEASE}"
init {

View file

@ -10,13 +10,24 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import net.openid.appauth.AppAuthConfiguration
import net.openid.appauth.AuthorizationService
import java.net.HttpURLConnection
import java.net.URL
@Module
@InstallIn(SingletonComponent::class)
object OAuthModule {
@Provides
fun authorizationService(@ApplicationContext context: Context): AuthorizationService = AuthorizationService(context)
fun authorizationService(@ApplicationContext context: Context): AuthorizationService =
AuthorizationService(context,
AppAuthConfiguration.Builder()
.setConnectionBuilder { uri ->
val url = URL(uri.toString())
(url.openConnection() as HttpURLConnection).apply {
setRequestProperty("User-Agent", HttpClient.UserAgentInterceptor.userAgent)
}
}.build()
)
}

View file

@ -7,6 +7,7 @@ package at.bitfire.davdroid.servicedetection
import android.accounts.Account
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -27,6 +28,7 @@ import at.bitfire.dav4jvm.Property
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.exception.UnauthorizedException
import at.bitfire.dav4jvm.property.AddressbookDescription
import at.bitfire.dav4jvm.property.AddressbookHomeSet
import at.bitfire.dav4jvm.property.CalendarColor
@ -59,6 +61,7 @@ import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import at.bitfire.davdroid.ui.account.SettingsActivity
import com.google.common.util.concurrent.ListenableFuture
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -204,6 +207,16 @@ class RefreshCollectionsWorker @AssistedInject constructor(
} catch(e: InvalidAccountException) {
Logger.log.log(Level.SEVERE, "Invalid account", e)
return Result.failure()
} catch (e: UnauthorizedException) {
Logger.log.log(Level.SEVERE, "Not authorized (anymore)", e)
// notify that we need to re-authenticate in the account settings
val settingsIntent = Intent(applicationContext, SettingsActivity::class.java)
.putExtra(SettingsActivity.EXTRA_ACCOUNT, account)
notifyRefreshError(
applicationContext.getString(R.string.sync_error_authentication_failed),
settingsIntent
)
return Result.failure()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't refresh collection list", e)
@ -211,19 +224,15 @@ class RefreshCollectionsWorker @AssistedInject constructor(
.withCause(e)
.withAccount(account)
.build()
val notify = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(applicationContext.getString(R.string.refresh_collections_worker_refresh_failed))
.setContentText(applicationContext.getString(R.string.refresh_collections_worker_refresh_couldnt_refresh))
.setContentIntent(PendingIntent.getActivity(applicationContext, 0, debugIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
NotificationManagerCompat.from(applicationContext)
.notifyIfPossible(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
notifyRefreshError(
applicationContext.getString(R.string.refresh_collections_worker_refresh_couldnt_refresh),
debugIntent
)
return Result.failure()
}
// Success
return Result.success()
}
@ -247,6 +256,18 @@ class RefreshCollectionsWorker @AssistedInject constructor(
completer.set(ForegroundInfo(NotificationUtils.NOTIFY_SYNC_EXPEDITED, notification))
}
private fun notifyRefreshError(contentText: String, contentIntent: Intent) {
val notify = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_GENERAL)
.setSmallIcon(R.drawable.ic_sync_problem_notify)
.setContentTitle(applicationContext.getString(R.string.refresh_collections_worker_refresh_failed))
.setContentText(contentText)
.setContentIntent(PendingIntent.getActivity(applicationContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))
.setSubText(account.name)
.setCategory(NotificationCompat.CATEGORY_ERROR)
.build()
NotificationManagerCompat.from(applicationContext)
.notifyIfPossible(serviceId.toString(), NotificationUtils.NOTIFY_REFRESH_COLLECTIONS, notify)
}
/**
* Contains the methods, which do the actual refreshing work. Collected here for testability

View file

@ -598,11 +598,27 @@ class DebugInfoActivity : AppCompatActivity() {
private fun dumpMainAccount(account: Account, writer: Writer) {
writer.append(" - Account: ${account.name}\n")
writer.append("\n\n - Account: ${account.name}\n")
writer.append(dumpAccount(account, AccountDumpInfo.mainAccount(context, account)))
writer.append(dumpSyncWorkersInfo(account))
try {
val accountSettings = AccountSettings(context, account)
val credentials = accountSettings.credentials()
val authStr = mutableListOf<String>()
if (credentials.userName != null)
authStr += "user name"
if (credentials.password != null)
authStr += "password"
if (credentials.certificateAlias != null)
authStr += "client certificate"
credentials.authState?.let { authState ->
authStr += "OAuth [${authState.authorizationServiceConfiguration?.authorizationEndpoint}]"
}
if (authStr.isNotEmpty())
writer .append(" Authentication: ")
.append(authStr.joinToString(", "))
.append("\n")
writer.append(" WiFi only: ${accountSettings.getSyncWifiOnly()}")
accountSettings.getSyncWifiOnlySSIDs()?.let { ssids ->
writer.append(", SSIDs: ${ssids.joinToString(", ")}")
@ -614,6 +630,10 @@ class DebugInfoActivity : AppCompatActivity() {
" Manage calendar colors: ${accountSettings.getManageCalendarColors()}\n" +
" Use event colors: ${accountSettings.getEventColors()}\n"
)
writer.append("\nSync workers:\n")
.append(dumpSyncWorkersInfo(account))
.append("\n")
} catch (e: InvalidAccountException) {
writer.append("$e\n")
}
@ -625,7 +645,7 @@ class DebugInfoActivity : AppCompatActivity() {
val table = dumpAccount(account, AccountDumpInfo.addressBookAccount(account))
writer.append(TextTable.indent(table, 4))
.append("URL: ${accountManager.getUserData(account, LocalAddressBook.USER_DATA_URL)}\n")
.append(" Read-only: ${accountManager.getUserData(account, LocalAddressBook.USER_DATA_READ_ONLY) ?: 0}\n\n")
.append(" Read-only: ${accountManager.getUserData(account, LocalAddressBook.USER_DATA_READ_ONLY) ?: 0}\n")
}
private fun dumpAccount(account: Account, infos: Iterable<AccountDumpInfo>): String {

View file

@ -14,6 +14,7 @@ import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsClient
import androidx.core.content.getSystemService
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
@ -36,6 +37,8 @@ object UiUtils {
const val SHORTCUT_SYNC_ALL = "syncAllAccounts"
const val SNACKBAR_LENGTH_VERY_LONG = 5000 // 5s
fun haveCustomTabs(context: Context) = CustomTabsClient.getPackageName(context, null, false) != null
/**
* Starts the [Intent.ACTION_VIEW] intent with the given URL, if possible.
* If the intent can't be resolved (for instance, because there is no browser

View file

@ -25,16 +25,17 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.preference.*
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.syncadapter.Syncer
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.syncadapter.Syncer
import at.bitfire.davdroid.ui.UiUtils
import at.bitfire.davdroid.ui.setup.GoogleLoginFragment
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
import com.google.android.material.snackbar.Snackbar
@ -217,32 +218,53 @@ class SettingsActivity: AppCompatActivity() {
}
// preference group: authentication
val prefUserName = findPreference<EditTextPreference>("username")!!
val prefPassword = findPreference<EditTextPreference>("password")!!
val prefCertAlias = findPreference<Preference>("certificate_alias")!!
model.credentials.observe(viewLifecycleOwner) { credentials ->
prefUserName.summary = credentials.userName
prefUserName.text = credentials.userName
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newUserName ->
model.updateCredentials(Credentials(newUserName as String, credentials.password, credentials.certificateAlias))
false
}
val authCategory: PreferenceCategory = findPreference("authentication")!!
val prefUserName = findPreference<EditTextPreference>(getString(R.string.settings_username_key))!!
val prefPassword = findPreference<EditTextPreference>(getString(R.string.settings_password_key))!!
val prefCertAlias = findPreference<Preference>(getString(R.string.settings_certificate_alias_key))!!
val prefOAuth = findPreference<Preference>(getString(R.string.settings_oauth_key))!!
if (credentials.userName != null) {
prefPassword.isVisible = true
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newPassword ->
model.updateCredentials(Credentials(credentials.userName, newPassword as String, credentials.certificateAlias))
model.credentials.observe(viewLifecycleOwner) { credentials ->
if (credentials.authState != null) {
// using OAuth, hide other settings
authCategory.removePreference(prefUserName)
authCategory.removePreference(prefPassword)
authCategory.removePreference(prefCertAlias)
prefOAuth.setOnPreferenceClickListener {
parentFragmentManager.beginTransaction()
.replace(android.R.id.content, GoogleLoginFragment(), null)
.addToBackStack(null)
.commit()
true
}
} else {
// not using OAuth, hide OAuth setting
authCategory.removePreference(prefOAuth)
prefUserName.summary = credentials.userName
prefUserName.text = credentials.userName
prefUserName.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newUserName ->
model.updateCredentials(Credentials(newUserName as String, credentials.password, credentials.certificateAlias))
false
}
} else
prefPassword.isVisible = false
prefCertAlias.summary = credentials.certificateAlias ?: getString(R.string.settings_certificate_alias_empty)
prefCertAlias.setOnPreferenceClickListener {
KeyChain.choosePrivateKeyAlias(requireActivity(), { newAlias ->
model.updateCredentials(Credentials(credentials.userName, credentials.password, newAlias))
}, null, null, null, -1, credentials.certificateAlias)
true
if (credentials.userName != null) {
prefPassword.isVisible = true
prefPassword.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newPassword ->
model.updateCredentials(Credentials(credentials.userName, newPassword as String, credentials.certificateAlias))
false
}
} else
prefPassword.isVisible = false
prefCertAlias.summary = credentials.certificateAlias ?: getString(R.string.settings_certificate_alias_empty)
prefCertAlias.setOnPreferenceClickListener {
KeyChain.choosePrivateKeyAlias(requireActivity(), { newAlias ->
model.updateCredentials(Credentials(credentials.userName, credentials.password, newAlias))
}, null, null, null, -1, credentials.certificateAlias)
true
}
}
}

View file

@ -66,6 +66,7 @@ class AccountDetailsFragment : Fragment() {
// default account name
model.name.value =
config.calDAV?.emails?.firstOrNull()
?: loginModel.suggestedAccountName
?: loginModel.credentials?.userName
?: loginModel.credentials?.certificateAlias
?: loginModel.baseURI?.host

View file

@ -6,6 +6,7 @@ package at.bitfire.davdroid.ui.setup
import android.content.Intent
import android.net.MailTo
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@ -19,6 +20,7 @@ import androidx.fragment.app.viewModels
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.LoginCredentialsFragmentBinding
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.ui.UiUtils
import com.google.android.material.snackbar.Snackbar
import dagger.Binds
import dagger.Module
@ -66,7 +68,7 @@ class DefaultLoginCredentialsFragment : Fragment() {
}, null, null, null, -1, model.certificateAlias.value)
}
v.login.setOnClickListener {
v.login.setOnClickListener { _ ->
if (validate()) {
val nextFragment =
if (model.loginGoogle.value == true)

View file

@ -5,6 +5,7 @@
package at.bitfire.davdroid.ui.setup
import android.app.Application
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
@ -47,12 +48,13 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.GoogleOAuth
import at.bitfire.davdroid.ui.UiUtils
import com.google.accompanist.themeadapter.material.MdcTheme
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@ -62,9 +64,12 @@ import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationRequest
import net.openid.appauth.AuthorizationResponse
import net.openid.appauth.AuthorizationService
import net.openid.appauth.AuthorizationServiceConfiguration
import net.openid.appauth.ResponseTypeValues
import net.openid.appauth.TokenResponse
import org.apache.commons.lang3.StringUtils
import java.net.URI
import java.util.logging.Level
import javax.inject.Inject
@AndroidEntryPoint
@ -72,8 +77,30 @@ class GoogleLoginFragment: Fragment() {
companion object {
// Support site
val URI_TESTED_WITH_GOOGLE: Uri = Uri.parse("https://www.davx5.com/tested-with/google")
// davx5integration@gmail.com (for davx5-ose)
private const val CLIENT_ID = "1069050168830-eg09u4tk1cmboobevhm4k3bj1m4fav9i.apps.googleusercontent.com"
val SCOPES = arrayOf(
"https://www.googleapis.com/auth/calendar", // CalDAV
"https://www.googleapis.com/auth/carddav" // CardDAV
)
private val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://accounts.google.com/o/oauth2/v2/auth"),
Uri.parse("https://oauth2.googleapis.com/token")
)
fun authRequestBuilder(clientId: String?) =
AuthorizationRequest.Builder(
serviceConfig,
clientId ?: CLIENT_ID,
ResponseTypeValues.CODE,
Uri.parse(BuildConfig.APPLICATION_ID + ":/oauth2/redirect")
)
fun googleBaseUri(googleAccount: String): URI =
URI("https", "www.google.com", "/calendar/dav/$googleAccount/events/", null)
@ -92,21 +119,28 @@ class GoogleLoginFragment: Fragment() {
if (authResponse != null)
model.authenticate(authResponse)
else
Logger.log.warning("Couldn't obtain authorization code")
Snackbar.make(requireView(), R.string.login_oauth_couldnt_obtain_auth_code, Snackbar.LENGTH_LONG).show()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val view = ComposeView(requireActivity()).apply {
setContent {
GoogleLogin(onLogin = { account, clientId ->
loginModel.baseURI = googleBaseUri(account)
GoogleLogin(onLogin = { accountEmail, clientId ->
loginModel.baseURI = googleBaseUri(accountEmail)
loginModel.suggestedAccountName = accountEmail
val authRequest = GoogleOAuth.authRequestBuilder(clientId)
.setScopes(*GoogleOAuth.SCOPES)
.setLoginHint(account)
val authRequest = authRequestBuilder(clientId)
.setScopes(*SCOPES)
.setLoginHint(accountEmail)
.build()
authRequestContract.launch(authRequest)
try {
authRequestContract.launch(authRequest)
} catch (e: ActivityNotFoundException) {
Logger.log.log(Level.WARNING, "Couldn't start OAuth intent", e)
Snackbar.make(requireView(), getString(R.string.install_browser), Snackbar.LENGTH_LONG).show()
}
})
}
}
@ -164,7 +198,7 @@ class GoogleLoginFragment: Fragment() {
@Composable
fun GoogleLogin(
onLogin: (account: String, clientId: String?) -> Unit
onLogin: (accountEmail: String, clientId: String?) -> Unit
) {
val context = LocalContext.current
MdcTheme {
@ -255,4 +289,4 @@ fun GoogleLogin(
@Preview
fun PreviewGoogleLogin() {
GoogleLogin(onLogin = { _, _ -> })
}
}

View file

@ -16,4 +16,9 @@ class LoginModel: ViewModel() {
var configuration: DavResourceFinder.Configuration? = null
/**
* Account name that should be used as default account name when no email addresses have been found.
*/
var suggestedAccountName: String? = null
}

View file

@ -29,6 +29,7 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.DebugInfoActivity
import at.bitfire.davdroid.ui.UiUtils.haveCustomTabs
import com.google.android.material.snackbar.Snackbar
import dagger.Binds
import dagger.Module
@ -87,11 +88,11 @@ class NextcloudLoginFlowFragment: Fragment() {
// reset URL so that the browser isn't shown another time
loginFlowModel.loginUrl.value = null
if (haveCustomTabs(loginUri)) {
if (haveCustomTabs(requireActivity())) {
// Custom Tabs are available
val browser = CustomTabsIntent.Builder()
.setToolbarColor(resources.getColor(R.color.primaryColor))
.build()
.setToolbarColor(resources.getColor(R.color.primaryColor))
.build()
browser.intent.data = loginUri
startActivityForResult(browser.intent, REQUEST_BROWSER, browser.startAnimationBundle)
@ -133,24 +134,6 @@ class NextcloudLoginFlowFragment: Fragment() {
return view
}
private fun haveCustomTabs(uri: Uri): Boolean {
val browserIntent = Intent()
.setAction(Intent.ACTION_VIEW)
.addCategory(Intent.CATEGORY_BROWSABLE)
.setData(uri)
val pm = requireActivity().packageManager
val appsSupportingCustomTabs = pm.queryIntentActivities(browserIntent, 0)
for (pkg in appsSupportingCustomTabs) {
// check whether app resolves Custom Tabs service, too
val serviceIntent = Intent(ACTION_CUSTOM_TABS_CONNECTION).apply {
setPackage(pkg.activityInfo.packageName)
}
if (pm.resolveService(serviceIntent, 0) != null)
return true
}
return false
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode != REQUEST_BROWSER)
return

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M11,7L9.6,8.4l2.6,2.6H2v2h10.2l-2.6,2.6L11,17l5,-5L11,7zM20,19h-8v2h8c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2h-8v2h8V19z"/>
</vector>

View file

@ -299,6 +299,7 @@
<string name="login_google_client_id_description">You may use your own client ID, in case our does not work.</string>
<string name="login_google_client_id_description_link">Show me how!</string>
<string name="login_google_disclaimer">%s is not affiliated to, nor has it been authorized, sponsored or otherwise approved by Google LLC.</string>
<string name="login_oauth_couldnt_obtain_auth_code">Couldn\'t obtain authorization code</string>
<string name="login_configuration_detection">Configuration detection</string>
<string name="login_querying_server">Please wait, querying server…</string>
@ -349,7 +350,10 @@
<string name="settings_sync_wifi_only_ssids_permissions_action">Manage</string>
<string name="settings_more_info_faq">More information (FAQ)</string>
<string name="settings_authentication">Authentication</string>
<string name="settings_username_key" translatable="false">username</string>
<string name="settings_oauth_key" translatable="false">oauth</string>
<string name="settings_oauth">Re-authenticate</string>
<string name="settings_oauth_summary">Perform OAuth login again</string>
<string name="settings_username_key">username</string>
<string name="settings_username">User name</string>
<string name="settings_enter_username">Enter user name:</string>
<string name="settings_password_key" translatable="false">password</string>

View file

@ -45,13 +45,16 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_authentication">
<PreferenceCategory
android:key="authentication"
android:title="@string/settings_authentication">
<EditTextPreference
android:key="@string/settings_username_key"
android:title="@string/settings_username"
android:icon="@drawable/ic_login"
android:persistent="false"
android:dialogTitle="@string/settings_enter_username"/>
android:dialogTitle="@string/settings_enter_username" />
<EditTextPreference
android:key="@string/settings_password_key"
@ -65,6 +68,12 @@
android:title="@string/settings_certificate_alias"
android:persistent="false"/>
<Preference
android:key="@string/settings_oauth_key"
android:title="@string/settings_oauth"
android:summary="@string/settings_oauth_summary"
android:icon="@drawable/ic_login" />
</PreferenceCategory>
<PreferenceCategory

View file

@ -0,0 +1,25 @@
<!--
~ Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<!-- AppAuth login flow redirect -->
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity"
android:exported="true"
tools:node="replace">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="at.bitfire.davdroid" android:path="/oauth2/redirect"/>
</intent-filter>
</activity>
</application>
</manifest>