mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 19:50:18 +00:00
Support OAuth for Google (bitfireAT/davx5#289)
* Proof of concept (without auth state storage) * Implement Bearer authentication, create network package * Properly create/dispose AuthService * Use proper ActivityResultContract * Integrate into default login activity * Change client ID to davx5integration@gmail.com * Google Login: adapt login view * Fix tests * Don't allow empty Google account * Move strings to resources
This commit is contained in:
parent
1b3fde0854
commit
5b38943205
|
@ -28,6 +28,10 @@ android {
|
|||
|
||||
buildConfigField "String", "userAgent", "\"DAVx5\""
|
||||
|
||||
manifestPlaceholders = [
|
||||
'appAuthRedirectScheme': applicationId
|
||||
]
|
||||
|
||||
testInstrumentationRunner "at.bitfire.davdroid.CustomTestRunner"
|
||||
|
||||
kapt {
|
||||
|
@ -123,7 +127,7 @@ dependencies {
|
|||
implementation 'androidx.concurrent:concurrent-futures-ktx:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.5.7'
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.0'
|
||||
implementation 'androidx.hilt:hilt-work:1.0.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
|
@ -135,6 +139,7 @@ dependencies {
|
|||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation 'net.openid:appauth:0.11.1'
|
||||
|
||||
// Jetpack Compose
|
||||
def composeBom = platform("androidx.compose:compose-bom:${versions.composeBom}")
|
||||
|
@ -142,6 +147,8 @@ dependencies {
|
|||
androidTestImplementation composeBom
|
||||
implementation 'androidx.compose.material:material'
|
||||
implementation 'androidx.compose.runtime:runtime-livedata'
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview'
|
||||
implementation 'com.google.accompanist:accompanist-themeadapter-material:0.30.1'
|
||||
|
||||
// Jetpack Room
|
||||
|
|
|
@ -6,9 +6,10 @@ package at.bitfire.davdroid.db
|
|||
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.property.ResourceType
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
|
@ -43,7 +44,7 @@ class CollectionTest {
|
|||
|
||||
@Before
|
||||
fun setUp() {
|
||||
httpClient = HttpClient.Builder().build()
|
||||
httpClient = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
|
||||
Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.os.Build
|
||||
import androidx.test.filters.SdkSuppress
|
|
@ -2,9 +2,12 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.Request
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
|
@ -13,16 +16,23 @@ import org.junit.Assert.assertEquals
|
|||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assume
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class HttpClientTest {
|
||||
|
||||
lateinit var server: MockWebServer
|
||||
lateinit var httpClient: HttpClient
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
httpClient = HttpClient.Builder().build()
|
||||
hiltRule.inject()
|
||||
|
||||
httpClient = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build()
|
||||
|
||||
server = MockWebServer()
|
||||
server.start(30000)
|
|
@ -10,7 +10,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.property.AddressbookHomeSet
|
||||
import at.bitfire.dav4jvm.property.ResourceType
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.servicedetection.DavResourceFinder.Configuration.ServiceInfo
|
||||
|
@ -68,7 +68,7 @@ class DavResourceFinderTest {
|
|||
loginModel.credentials = Credentials("mock", "12345")
|
||||
|
||||
finder = DavResourceFinder(InstrumentationRegistry.getInstrumentation().targetContext, loginModel)
|
||||
client = HttpClient.Builder()
|
||||
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
.addAuthentication(null, loginModel.credentials!!)
|
||||
.build()
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import androidx.test.platform.app.InstrumentationRegistry
|
|||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
|
@ -109,7 +109,7 @@ class RefreshCollectionsWorkerTest {
|
|||
loginModel.baseURI = URI.create("/")
|
||||
loginModel.credentials = Credentials("mock", "12345")
|
||||
|
||||
client = HttpClient.Builder()
|
||||
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
.addAuthentication(null, loginModel.credentials!!)
|
||||
.build()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ package at.bitfire.davdroid.webdav
|
|||
import android.content.Context
|
||||
import android.security.NetworkSecurityPolicy
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
|
@ -65,7 +65,7 @@ class DavDocumentsProviderTest {
|
|||
loginModel.baseURI = URI.create("/")
|
||||
loginModel.credentials = Credentials("mock", "12345")
|
||||
|
||||
client = HttpClient.Builder()
|
||||
client = HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||
.addAuthentication(null, loginModel.credentials!!)
|
||||
.build()
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package at.bitfire.davdroid
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
|
|
|
@ -18,7 +18,7 @@ import at.bitfire.dav4jvm.PropStat
|
|||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.Response.HrefRelation
|
||||
import at.bitfire.dav4jvm.property.GetETag
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
|
@ -98,7 +98,7 @@ class SyncManagerTest {
|
|||
account,
|
||||
arrayOf(),
|
||||
"TestAuthority",
|
||||
HttpClient.Builder().build(),
|
||||
HttpClient.Builder(InstrumentationRegistry.getInstrumentation().targetContext).build(),
|
||||
SyncResult(),
|
||||
collection,
|
||||
server
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.content.ContentProviderClient
|
|||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
|
|
|
@ -11,7 +11,7 @@ import at.bitfire.dav4jvm.DavCollection
|
|||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.property.GetCTag
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
|
@ -23,14 +23,14 @@ import okhttp3.mockwebserver.MockWebServer
|
|||
import org.junit.Assert.assertEquals
|
||||
|
||||
class TestSyncManager(
|
||||
context: Context,
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: HttpClient,
|
||||
syncResult: SyncResult,
|
||||
localCollection: LocalTestCollection,
|
||||
val mockWebServer: MockWebServer
|
||||
context: Context,
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: HttpClient,
|
||||
syncResult: SyncResult,
|
||||
localCollection: LocalTestCollection,
|
||||
val mockWebServer: MockWebServer
|
||||
): SyncManager<LocalTestResource, LocalTestCollection, DavCollection>(context, account, AccountSettings(context, account), httpClient, extras, authority, syncResult, localCollection) {
|
||||
|
||||
override fun prepare(): Boolean {
|
||||
|
|
|
@ -4,15 +4,32 @@
|
|||
|
||||
package at.bitfire.davdroid.db
|
||||
|
||||
import net.openid.appauth.AuthState
|
||||
|
||||
data class Credentials(
|
||||
val userName: String? = null,
|
||||
val password: String? = null,
|
||||
val certificateAlias: String? = null
|
||||
val userName: String? = null,
|
||||
val password: String? = null,
|
||||
|
||||
val certificateAlias: String? = null,
|
||||
|
||||
val authState: AuthState? = null
|
||||
) {
|
||||
|
||||
override fun toString(): String {
|
||||
val maskedPassword = "*****".takeIf { password != null }
|
||||
return "Credentials(userName=$userName, password=$maskedPassword, certificateAlias=$certificateAlias)"
|
||||
val s = mutableListOf<String>()
|
||||
|
||||
if (userName != null)
|
||||
s += "userName=$userName"
|
||||
if (password != null)
|
||||
s += "password=*****"
|
||||
|
||||
if (certificateAlias != null)
|
||||
s += "certificateAlias=$certificateAlias"
|
||||
|
||||
if (authState != null)
|
||||
s += "authState=${authState.jsonSerializeString()}"
|
||||
|
||||
return "Credentials(" + s.joinToString(", ") + ")"
|
||||
}
|
||||
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
/***************************************************************************************************
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.net.DnsResolver
|
||||
import android.os.Build
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Sends an OAuth Bearer token authorization as described in RFC 6750.
|
||||
*/
|
||||
class BearerAuthInterceptor(
|
||||
private val accessToken: String
|
||||
): Interceptor {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromAuthState(authService: AuthorizationService, authState: AuthState, callback: AuthStateUpdateCallback? = null): BearerAuthInterceptor? {
|
||||
return runBlocking {
|
||||
val accessTokenFuture = CompletableDeferred<String>()
|
||||
|
||||
authState.performActionWithFreshTokens(authService) { accessToken: String?, _: String?, ex: AuthorizationException? ->
|
||||
if (accessToken != null) {
|
||||
// persist updated AuthState
|
||||
callback?.onUpdate(authState)
|
||||
|
||||
// emit access token
|
||||
accessTokenFuture.complete(accessToken)
|
||||
}
|
||||
else {
|
||||
Logger.log.log(Level.WARNING, "Couldn't obtain access token", ex)
|
||||
accessTokenFuture.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// return value
|
||||
try {
|
||||
BearerAuthInterceptor(accessTokenFuture.await())
|
||||
} catch (ignored: CancellationException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
Logger.log.finer("Authenticating request with access token")
|
||||
val rq = chain.request().newBuilder()
|
||||
.header("Authorization", "Bearer $accessToken")
|
||||
.build()
|
||||
return chain.proceed(rq)
|
||||
}
|
||||
|
||||
|
||||
fun interface AuthStateUpdateCallback {
|
||||
fun onUpdate(authState: AuthState)
|
||||
}
|
||||
|
||||
}
|
40
app/src/main/java/at/bitfire/davdroid/network/GoogleOAuth.kt
Normal file
40
app/src/main/java/at/bitfire/davdroid/network/GoogleOAuth.kt
Normal file
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import net.openid.appauth.ResponseTypeValues
|
||||
|
||||
object GoogleOAuth {
|
||||
|
||||
// davx5integration@gmail.com
|
||||
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() =
|
||||
AuthorizationRequest.Builder(
|
||||
serviceConfig,
|
||||
CLIENT_ID,
|
||||
ResponseTypeValues.CODE,
|
||||
Uri.parse(BuildConfig.APPLICATION_ID + ":/oauth/redirect")
|
||||
)
|
||||
|
||||
fun createAuthService(context: Context) = AuthorizationService(context)
|
||||
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
/***************************************************************************************************
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
|
@ -10,6 +10,7 @@ import android.security.KeyChain
|
|||
import at.bitfire.cert4android.CustomCertManager
|
||||
import at.bitfire.dav4jvm.BasicDigestAuthHandler
|
||||
import at.bitfire.dav4jvm.UrlUtils
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
|
@ -19,6 +20,8 @@ import dagger.hilt.EntryPoint
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationService
|
||||
import okhttp3.*
|
||||
import okhttp3.brotli.BrotliInterceptor
|
||||
import okhttp3.internal.tls.OkHostnameVerifier
|
||||
|
@ -37,7 +40,8 @@ import javax.net.ssl.*
|
|||
|
||||
class HttpClient private constructor(
|
||||
val okHttpClient: OkHttpClient,
|
||||
private val certManager: CustomCertManager?
|
||||
private val certManager: CustomCertManager? = null,
|
||||
private var authService: AuthorizationService? = null
|
||||
): AutoCloseable {
|
||||
|
||||
@EntryPoint
|
||||
|
@ -79,14 +83,15 @@ class HttpClient private constructor(
|
|||
.addInterceptor(UserAgentInterceptor)
|
||||
}
|
||||
|
||||
|
||||
override fun close() {
|
||||
authService?.dispose()
|
||||
okHttpClient.cache?.close()
|
||||
certManager?.close()
|
||||
}
|
||||
|
||||
|
||||
class Builder(
|
||||
val context: Context? = null,
|
||||
val context: Context,
|
||||
accountSettings: AccountSettings? = null,
|
||||
val logger: java.util.logging.Logger? = Logger.log,
|
||||
val loggerLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY
|
||||
|
@ -97,6 +102,7 @@ class HttpClient private constructor(
|
|||
}
|
||||
|
||||
private var appInForeground = false
|
||||
private var authService: AuthorizationService? = null
|
||||
private var certManagerProducer: CertManagerProducer? = null
|
||||
private var certificateAlias: String? = null
|
||||
private var offerCompression: Boolean = false
|
||||
|
@ -114,60 +120,68 @@ class HttpClient private constructor(
|
|||
orig.addNetworkInterceptor(loggingInterceptor)
|
||||
}
|
||||
|
||||
if (context != null) {
|
||||
val settings = EntryPointAccessors.fromApplication(context, HttpClientEntryPoint::class.java).settingsManager()
|
||||
val settings = EntryPointAccessors.fromApplication(context, HttpClientEntryPoint::class.java).settingsManager()
|
||||
|
||||
// custom proxy support
|
||||
try {
|
||||
val proxyTypeValue = settings.getInt(Settings.PROXY_TYPE)
|
||||
if (proxyTypeValue != Settings.PROXY_TYPE_SYSTEM) {
|
||||
// we set our own proxy
|
||||
val address by lazy { // lazy because not required for PROXY_TYPE_NONE
|
||||
InetSocketAddress(
|
||||
settings.getString(Settings.PROXY_HOST),
|
||||
settings.getInt(Settings.PROXY_PORT)
|
||||
)
|
||||
}
|
||||
val proxy =
|
||||
when (proxyTypeValue) {
|
||||
Settings.PROXY_TYPE_NONE -> Proxy.NO_PROXY
|
||||
Settings.PROXY_TYPE_HTTP -> Proxy(Proxy.Type.HTTP, address)
|
||||
Settings.PROXY_TYPE_SOCKS -> Proxy(Proxy.Type.SOCKS, address)
|
||||
else -> throw IllegalArgumentException("Invalid proxy type")
|
||||
}
|
||||
orig.proxy(proxy)
|
||||
Logger.log.log(Level.INFO, "Using proxy setting", proxy)
|
||||
// custom proxy support
|
||||
try {
|
||||
val proxyTypeValue = settings.getInt(Settings.PROXY_TYPE)
|
||||
if (proxyTypeValue != Settings.PROXY_TYPE_SYSTEM) {
|
||||
// we set our own proxy
|
||||
val address by lazy { // lazy because not required for PROXY_TYPE_NONE
|
||||
InetSocketAddress(
|
||||
settings.getString(Settings.PROXY_HOST),
|
||||
settings.getInt(Settings.PROXY_PORT)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
|
||||
val proxy =
|
||||
when (proxyTypeValue) {
|
||||
Settings.PROXY_TYPE_NONE -> Proxy.NO_PROXY
|
||||
Settings.PROXY_TYPE_HTTP -> Proxy(Proxy.Type.HTTP, address)
|
||||
Settings.PROXY_TYPE_SOCKS -> Proxy(Proxy.Type.SOCKS, address)
|
||||
else -> throw IllegalArgumentException("Invalid proxy type")
|
||||
}
|
||||
orig.proxy(proxy)
|
||||
Logger.log.log(Level.INFO, "Using proxy setting", proxy)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Can't set proxy, ignoring", e)
|
||||
}
|
||||
|
||||
customCertManager() {
|
||||
// by default, use a CustomCertManager that respects the "distrust system certificates" setting
|
||||
val trustSystemCerts = !settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
|
||||
CustomCertManager(context, true /*BuildConfig.customCertsUI*/, trustSystemCerts)
|
||||
}
|
||||
customCertManager {
|
||||
// by default, use a CustomCertManager that respects the "distrust system certificates" setting
|
||||
val trustSystemCerts = !settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES)
|
||||
CustomCertManager(context, true /*BuildConfig.customCertsUI*/, trustSystemCerts)
|
||||
}
|
||||
|
||||
// use account settings for authentication and cookies
|
||||
accountSettings?.let {
|
||||
addAuthentication(null, it.credentials())
|
||||
}
|
||||
if (accountSettings != null)
|
||||
addAuthentication(null, accountSettings.credentials(), authStateCallback = { authState: AuthState ->
|
||||
accountSettings.credentials(Credentials(authState = authState))
|
||||
})
|
||||
}
|
||||
|
||||
constructor(context: Context, host: String?, credentials: Credentials?): this(context) {
|
||||
constructor(context: Context, host: String?, credentials: Credentials?) : this(context) {
|
||||
if (credentials != null)
|
||||
addAuthentication(host, credentials)
|
||||
}
|
||||
|
||||
fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false): Builder {
|
||||
fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder {
|
||||
if (credentials.userName != null && credentials.password != null) {
|
||||
val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.userName, credentials.password, insecurePreemptive)
|
||||
orig.addNetworkInterceptor(authHandler)
|
||||
.authenticator(authHandler)
|
||||
}
|
||||
|
||||
if (credentials.certificateAlias != null)
|
||||
certificateAlias = credentials.certificateAlias
|
||||
|
||||
credentials.authState?.let { authState ->
|
||||
val newAuthService = GoogleOAuth.createAuthService(context)
|
||||
authService = newAuthService
|
||||
BearerAuthInterceptor.fromAuthState(newAuthService, authState, authStateCallback)?.let { bearerAuthInterceptor ->
|
||||
orig.addNetworkInterceptor(bearerAuthInterceptor)
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -218,8 +232,6 @@ class HttpClient private constructor(
|
|||
|
||||
var keyManager: KeyManager? = null
|
||||
certificateAlias?.let { alias ->
|
||||
val context = requireNotNull(context)
|
||||
|
||||
// get provider certificate and private key
|
||||
val certs = KeyChain.getCertificateChain(context, alias) ?: return@let
|
||||
val key = KeyChain.getPrivateKey(context, alias) ?: return@let
|
||||
|
@ -248,30 +260,33 @@ class HttpClient private constructor(
|
|||
orig.protocols(listOf(Protocol.HTTP_1_1))
|
||||
}
|
||||
|
||||
if (certManagerProducer != null || keyManager != null) {
|
||||
val certManager = certManagerProducer?.certManager()
|
||||
certManager?.appInForeground = appInForeground
|
||||
val certManager =
|
||||
if (certManagerProducer != null || keyManager != null) {
|
||||
val manager = certManagerProducer?.certManager()
|
||||
manager?.appInForeground = appInForeground
|
||||
|
||||
val trustManager = certManager ?: { // fall back to system default trust manager
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(null as KeyStore?)
|
||||
factory.trustManagers.first() as X509TrustManager
|
||||
}()
|
||||
val trustManager = manager ?: { // fall back to system default trust manager
|
||||
val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(null as KeyStore?)
|
||||
factory.trustManagers.first() as X509TrustManager
|
||||
}()
|
||||
|
||||
val hostnameVerifier = certManager?.hostnameVerifier(OkHostnameVerifier)
|
||||
?: OkHostnameVerifier
|
||||
val hostnameVerifier = manager?.hostnameVerifier(OkHostnameVerifier)
|
||||
?: OkHostnameVerifier
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(
|
||||
if (keyManager != null) arrayOf(keyManager) else null,
|
||||
arrayOf(trustManager),
|
||||
null)
|
||||
orig.sslSocketFactory(sslContext.socketFactory, trustManager)
|
||||
orig.hostnameVerifier(hostnameVerifier)
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(
|
||||
if (keyManager != null) arrayOf(keyManager) else null,
|
||||
arrayOf(trustManager),
|
||||
null)
|
||||
orig.sslSocketFactory(sslContext.socketFactory, trustManager)
|
||||
orig.hostnameVerifier(hostnameVerifier)
|
||||
|
||||
return HttpClient(orig.build(), certManager)
|
||||
} else
|
||||
return HttpClient(orig.build(), null)
|
||||
manager
|
||||
} else
|
||||
null
|
||||
|
||||
return HttpClient(orig.build(), certManager = certManager, authService = authService)
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.network
|
||||
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
|
@ -12,9 +12,10 @@ import at.bitfire.dav4jvm.exception.DavException
|
|||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.dav4jvm.exception.UnauthorizedException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.StringHandler
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.ui.setup.LoginModel
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import okhttp3.HttpUrl
|
||||
|
@ -451,11 +452,11 @@ class DavResourceFinder(
|
|||
) {
|
||||
|
||||
data class ServiceInfo(
|
||||
var principal: HttpUrl? = null,
|
||||
val homeSets: MutableSet<HttpUrl> = HashSet(),
|
||||
val collections: MutableMap<HttpUrl, Collection> = HashMap(),
|
||||
var principal: HttpUrl? = null,
|
||||
val homeSets: MutableSet<HttpUrl> = HashSet(),
|
||||
val collections: MutableMap<HttpUrl, Collection> = HashMap(),
|
||||
|
||||
val emails: MutableList<String> = LinkedList()
|
||||
val emails: MutableList<String> = LinkedList()
|
||||
)
|
||||
|
||||
override fun toString(): String {
|
||||
|
|
|
@ -43,7 +43,6 @@ import at.bitfire.dav4jvm.property.ResourceType
|
|||
import at.bitfire.dav4jvm.property.Source
|
||||
import at.bitfire.dav4jvm.property.SupportedAddressData
|
||||
import at.bitfire.dav4jvm.property.SupportedCalendarComponentSet
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
|
@ -52,6 +51,7 @@ import at.bitfire.davdroid.db.HomeSet
|
|||
import at.bitfire.davdroid.db.Principal
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker.Companion.ARG_SERVICE_ID
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
|
|
|
@ -23,6 +23,7 @@ import dagger.hilt.EntryPoint
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import net.openid.appauth.AuthState
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.util.logging.Level
|
||||
|
||||
|
@ -63,6 +64,9 @@ class AccountSettings(
|
|||
const val KEY_USERNAME = "user_name"
|
||||
const val KEY_CERTIFICATE_ALIAS = "certificate_alias"
|
||||
|
||||
/** OAuth [AuthState] (serialized as JSON) */
|
||||
const val KEY_AUTH_STATE = "auth_state"
|
||||
|
||||
const val KEY_WIFI_ONLY = "wifi_only" // sync on WiFi only (default: false)
|
||||
const val KEY_WIFI_ONLY_SSIDS = "wifi_only_ssids" // restrict sync to specific WiFi SSIDs
|
||||
|
||||
|
@ -111,14 +115,18 @@ class AccountSettings(
|
|||
var currentlyUpdating = false
|
||||
|
||||
fun initialUserData(credentials: Credentials?): Bundle {
|
||||
val bundle = Bundle(2)
|
||||
val bundle = Bundle()
|
||||
bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())
|
||||
|
||||
if (credentials != null) {
|
||||
if (credentials.userName != null)
|
||||
bundle.putString(KEY_USERNAME, credentials.userName)
|
||||
|
||||
if (credentials.certificateAlias != null)
|
||||
bundle.putString(KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
|
||||
|
||||
if (credentials.authState != null)
|
||||
bundle.putString(KEY_AUTH_STATE, credentials.authState.jsonSerializeString())
|
||||
}
|
||||
|
||||
return bundle
|
||||
|
@ -176,13 +184,24 @@ class AccountSettings(
|
|||
fun credentials() = Credentials(
|
||||
accountManager.getUserData(account, KEY_USERNAME),
|
||||
accountManager.getPassword(account),
|
||||
accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS)
|
||||
|
||||
accountManager.getUserData(account, KEY_CERTIFICATE_ALIAS),
|
||||
|
||||
accountManager.getUserData(account, KEY_AUTH_STATE)?.let { json ->
|
||||
AuthState.jsonDeserialize(json)
|
||||
}
|
||||
)
|
||||
|
||||
fun credentials(credentials: Credentials) {
|
||||
// Basic/Digest auth
|
||||
accountManager.setUserData(account, KEY_USERNAME, credentials.userName)
|
||||
accountManager.setPassword(account, credentials.password)
|
||||
|
||||
// client certificate
|
||||
accountManager.setUserData(account, KEY_CERTIFICATE_ALIAS, credentials.certificateAlias)
|
||||
|
||||
// OAuth
|
||||
accountManager.setUserData(account, KEY_AUTH_STATE, credentials.authState?.jsonSerializeString())
|
||||
}
|
||||
|
||||
|
||||
|
@ -475,4 +494,4 @@ class AccountSettings(
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ import android.content.SyncResult
|
|||
import android.content.pm.PackageManager
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.Response
|
|||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.content.ContentProviderClient
|
|||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.provider.CalendarContract
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -9,7 +9,7 @@ import android.content.ContentProviderClient
|
|||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.provider.ContactsContract
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
|
|
|
@ -17,7 +17,7 @@ import at.bitfire.dav4jvm.exception.DavException
|
|||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils.sameTypeAs
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.Response
|
|||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.content.ContentProviderClient
|
|||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Build
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -27,6 +27,7 @@ import at.bitfire.davdroid.db.AppDatabase
|
|||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.db.SyncStats
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.resource.*
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
|
|
|
@ -8,7 +8,7 @@ import android.accounts.Account
|
|||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.content.ContentProviderClient
|
|||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Build
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.Response
|
|||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -16,7 +16,7 @@ import androidx.lifecycle.*
|
|||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
|
|
|
@ -14,7 +14,7 @@ import androidx.fragment.app.DialogFragment
|
|||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.*
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.databinding.DeleteCollectionBinding
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
|
|
|
@ -67,11 +67,18 @@ class DefaultLoginCredentialsFragment : Fragment() {
|
|||
}
|
||||
|
||||
v.login.setOnClickListener {
|
||||
if (validate())
|
||||
if (validate()) {
|
||||
val nextFragment =
|
||||
if (model.loginGoogle.value == true)
|
||||
GoogleLoginFragment()
|
||||
else
|
||||
DetectConfigurationFragment()
|
||||
|
||||
parentFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, DetectConfigurationFragment(), null)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
.replace(android.R.id.content, nextFragment, null)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
return v.root
|
||||
|
@ -194,6 +201,10 @@ class DefaultLoginCredentialsFragment : Fragment() {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
model.loginGoogle.value == true -> {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
|
||||
return valid
|
||||
|
|
|
@ -25,9 +25,10 @@ class DefaultLoginCredentialsModel(app: Application): AndroidViewModel(app) {
|
|||
|
||||
private var initialized = false
|
||||
|
||||
val loginWithEmailAddress = MutableLiveData<Boolean>()
|
||||
val loginWithUrlAndUsername = MutableLiveData<Boolean>()
|
||||
val loginAdvanced = MutableLiveData<Boolean>()
|
||||
val loginWithEmailAddress = MutableLiveData(true)
|
||||
val loginWithUrlAndUsername = MutableLiveData(false)
|
||||
val loginAdvanced = MutableLiveData(false)
|
||||
val loginGoogle = MutableLiveData(false)
|
||||
|
||||
val baseUrl = MutableLiveData<String>()
|
||||
val baseUrlError = MutableLiveData<String>()
|
||||
|
@ -42,14 +43,9 @@ class DefaultLoginCredentialsModel(app: Application): AndroidViewModel(app) {
|
|||
val certificateAlias = MutableLiveData<String>()
|
||||
val certificateAliasError = MutableLiveData<String>()
|
||||
|
||||
val loginUseUsernamePassword = MutableLiveData<Boolean>()
|
||||
val loginUseClientCertificate = MutableLiveData<Boolean>()
|
||||
val loginUseUsernamePassword = MutableLiveData(false)
|
||||
val loginUseClientCertificate = MutableLiveData(false)
|
||||
|
||||
init {
|
||||
loginWithEmailAddress.value = true
|
||||
loginUseClientCertificate.value = false
|
||||
loginUseUsernamePassword.value = false
|
||||
}
|
||||
|
||||
fun clearUrlError(s: Editable) {
|
||||
if (s.toString() != "https://") {
|
||||
|
|
|
@ -28,13 +28,13 @@ import kotlin.concurrent.thread
|
|||
|
||||
class DetectConfigurationFragment: Fragment() {
|
||||
|
||||
val loginModel by activityViewModels<LoginModel>()
|
||||
val model by viewModels<DetectConfigurationModel>()
|
||||
private val loginModel by activityViewModels<LoginModel>()
|
||||
private val model by viewModels<DetectConfigurationModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
model.detectConfiguration(loginModel).observe(this, { result ->
|
||||
model.detectConfiguration(loginModel).observe(this) { result ->
|
||||
// save result for next step
|
||||
loginModel.configuration = result
|
||||
|
||||
|
@ -50,7 +50,7 @@ class DetectConfigurationFragment: Fragment() {
|
|||
parentFragmentManager.beginTransaction()
|
||||
.add(NothingDetectedFragment(), null)
|
||||
.commit()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
|
@ -127,4 +127,4 @@ class DetectConfigurationFragment: Fragment() {
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui.setup
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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 kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import net.openid.appauth.AuthState
|
||||
import net.openid.appauth.AuthorizationException
|
||||
import net.openid.appauth.AuthorizationRequest
|
||||
import net.openid.appauth.AuthorizationResponse
|
||||
import net.openid.appauth.TokenResponse
|
||||
import java.net.URI
|
||||
|
||||
class GoogleLoginFragment: Fragment() {
|
||||
|
||||
companion object {
|
||||
fun googleBaseUri(googleAccount: String): URI =
|
||||
URI("https", "www.google.com", "/calendar/dav/$googleAccount/events/", null)
|
||||
}
|
||||
|
||||
private val loginModel by activityViewModels<LoginModel>()
|
||||
private val model by viewModels<Model>()
|
||||
|
||||
private val authRequestContract = registerForActivityResult(object: ActivityResultContract<AuthorizationRequest, AuthorizationResponse?>() {
|
||||
override fun createIntent(context: Context, input: AuthorizationRequest) =
|
||||
model.authService.getAuthorizationRequestIntent(input)
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): AuthorizationResponse? =
|
||||
intent?.let { AuthorizationResponse.fromIntent(it) }
|
||||
}) { authResponse ->
|
||||
if (authResponse != null)
|
||||
model.authenticate(authResponse)
|
||||
else
|
||||
Logger.log.warning("Couldn't obtain authorization code")
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val v = ComposeView(requireActivity())
|
||||
v.setContent {
|
||||
GoogleLogin()
|
||||
}
|
||||
|
||||
model.credentials.observe(viewLifecycleOwner) { credentials ->
|
||||
loginModel.credentials = credentials
|
||||
|
||||
// continue with service detection
|
||||
parentFragmentManager.beginTransaction()
|
||||
.replace(android.R.id.content, DetectConfigurationFragment(), null)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun GoogleLogin() {
|
||||
MdcTheme {
|
||||
Column(Modifier.padding(8.dp).verticalScroll(rememberScrollState())) {
|
||||
Text(
|
||||
stringResource(R.string.login_type_google),
|
||||
style = MaterialTheme.typography.h5,
|
||||
modifier = Modifier.padding(vertical = 16.dp))
|
||||
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Text(
|
||||
stringResource(R.string.login_google_guide),
|
||||
modifier = Modifier.padding(vertical = 8.dp))
|
||||
Button(
|
||||
onClick = {
|
||||
UiUtils.launchUri(requireActivity(), Uri.parse("https://www.davx5.com/tested-with/google"))
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
modifier = Modifier.wrapContentSize()
|
||||
) {
|
||||
Text(stringResource(R.string.intro_more_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val email = remember { mutableStateOf("") }
|
||||
val emailError = remember { mutableStateOf<Boolean>(false) }
|
||||
OutlinedTextField(
|
||||
email.value,
|
||||
singleLine = true,
|
||||
onValueChange = { account ->
|
||||
email.value = account
|
||||
loginModel.baseURI = googleBaseUri(account)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
|
||||
label = { Text(stringResource(R.string.login_google_account)) },
|
||||
isError = emailError.value,
|
||||
placeholder = { Text("example@gmail.com") },
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)
|
||||
)
|
||||
|
||||
Button({
|
||||
val valid = email.value.orEmpty().contains('@')
|
||||
emailError.value = !valid
|
||||
|
||||
if (valid) {
|
||||
val authRequest = GoogleOAuth.authRequestBuilder()
|
||||
.setScopes(*GoogleOAuth.SCOPES)
|
||||
.setLoginHint(email.value)
|
||||
.build()
|
||||
authRequestContract.launch(authRequest)
|
||||
}
|
||||
}, modifier = Modifier.wrapContentSize()) {
|
||||
Text(stringResource(R.string.login_login))
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.login_google_disclaimer, getString(R.string.app_name)),
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(top = 24.dp))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Model(application: Application): AndroidViewModel(application) {
|
||||
|
||||
val authService = GoogleOAuth.createAuthService(getApplication())
|
||||
val credentials = MutableLiveData<Credentials>()
|
||||
|
||||
fun authenticate(resp: AuthorizationResponse) = viewModelScope.launch(Dispatchers.IO) {
|
||||
val authState = AuthState(resp, null) // authorization code must not be stored; exchange it to refresh token
|
||||
|
||||
authService.performTokenRequest(resp.createTokenExchangeRequest()) { tokenResponse: TokenResponse?, refreshTokenException: AuthorizationException? ->
|
||||
Logger.log.info("Refresh token response: ${tokenResponse?.jsonSerializeString()}")
|
||||
if (tokenResponse != null) {
|
||||
// success
|
||||
authState.update(tokenResponse, refreshTokenException)
|
||||
// save authState (= refresh token)
|
||||
|
||||
credentials.postValue(Credentials(authState = authState))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
authService.dispose()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -24,7 +24,7 @@ import androidx.lifecycle.AndroidViewModel
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
@ -271,10 +271,10 @@ class NextcloudLoginFlowFragment: Fragment() {
|
|||
class Factory @Inject constructor(): LoginCredentialsFragmentFactory {
|
||||
|
||||
override fun getFragment(intent: Intent) =
|
||||
if (intent.hasExtra(EXTRA_LOGIN_FLOW))
|
||||
NextcloudLoginFlowFragment()
|
||||
else
|
||||
null
|
||||
if (intent.hasExtra(EXTRA_LOGIN_FLOW))
|
||||
NextcloudLoginFlowFragment()
|
||||
else
|
||||
null
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import androidx.lifecycle.lifecycleScope
|
|||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.UrlUtils
|
||||
import at.bitfire.davdroid.App
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.ActivityAddWebdavMountBinding
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
|
|
|
@ -8,7 +8,7 @@ import android.content.Context
|
|||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import at.bitfire.davdroid.Android10Resolver
|
||||
import at.bitfire.davdroid.network.Android10Resolver
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
|
|
|
@ -30,8 +30,8 @@ import at.bitfire.dav4jvm.DavResource
|
|||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.MemoryCookieStore
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.network.MemoryCookieStore
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.WebDavDocument
|
||||
|
|
|
@ -7,7 +7,7 @@ package at.bitfire.davdroid.webdav
|
|||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.HttpUtils
|
||||
import at.bitfire.dav4jvm.property.GetETag
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
|
|
|
@ -22,6 +22,7 @@ import at.bitfire.dav4jvm.exception.DavException
|
|||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.davdroid.*
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
|
|
|
@ -12,7 +12,7 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.network.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
|
|
|
@ -312,9 +312,18 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RadioButton
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:checked="@={model.loginGoogle}"
|
||||
android:paddingStart="14dp"
|
||||
android:text="@string/login_type_google"
|
||||
android:textAlignment="viewStart" />
|
||||
|
||||
</RadioGroup>
|
||||
</ScrollView>
|
||||
|
||||
|
|
|
@ -292,6 +292,10 @@
|
|||
<string name="login_use_client_certificate">Use client certificate</string>
|
||||
<string name="login_no_certificate_found">No certificate found</string>
|
||||
<string name="login_install_certificate">Install certificate</string>
|
||||
<string name="login_type_google">Google Contacts / Calendar</string>
|
||||
<string name="login_google_guide">Please read our guide about the login to Google. There may be unexpected warnings which are explained there.</string>
|
||||
<string name="login_google_account">Google account</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_configuration_detection">Configuration detection</string>
|
||||
<string name="login_querying_server">Please wait, querying server…</string>
|
||||
|
|
Loading…
Reference in a new issue