mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 11:39:15 +00:00
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:
parent
6a2c366358
commit
0f92b0fb05
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = { _, _ -> })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
5
app/src/main/res/drawable/ic_login.xml
Normal file
5
app/src/main/res/drawable/ic_login.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
25
app/src/standard/AndroidManifest.xml
Normal file
25
app/src/standard/AndroidManifest.xml
Normal 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>
|
Loading…
Reference in a new issue