Support specifying a client certificate for mTLS auth (#940)
This commit is contained in:
parent
ec05bddba8
commit
39dc3bceee
@ -5,11 +5,13 @@ class FeverSecurityKey private constructor() : SecurityKey() {
|
||||
var serverUrl: String? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
|
||||
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
|
||||
this.serverUrl = serverUrl
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.clientCertificateAlias = clientCertificateAlias
|
||||
}
|
||||
|
||||
constructor(value: String? = DESUtils.empty) : this() {
|
||||
@ -17,6 +19,7 @@ class FeverSecurityKey private constructor() : SecurityKey() {
|
||||
serverUrl = it.serverUrl
|
||||
username = it.username
|
||||
password = it.password
|
||||
clientCertificateAlias = it.clientCertificateAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
|
||||
var serverUrl: String? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
|
||||
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
|
||||
this.serverUrl = serverUrl
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.clientCertificateAlias = clientCertificateAlias
|
||||
}
|
||||
|
||||
constructor(value: String? = DESUtils.empty) : this() {
|
||||
@ -17,6 +19,7 @@ class FreshRSSSecurityKey private constructor() : SecurityKey() {
|
||||
serverUrl = it.serverUrl
|
||||
username = it.username
|
||||
password = it.password
|
||||
clientCertificateAlias = it.clientCertificateAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,11 +5,13 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
|
||||
var serverUrl: String? = null
|
||||
var username: String? = null
|
||||
var password: String? = null
|
||||
var clientCertificateAlias: String? = null
|
||||
|
||||
constructor(serverUrl: String?, username: String?, password: String?) : this() {
|
||||
constructor(serverUrl: String?, username: String?, password: String?, clientCertificateAlias: String?) : this() {
|
||||
this.serverUrl = serverUrl
|
||||
this.username = username
|
||||
this.password = password
|
||||
this.clientCertificateAlias = clientCertificateAlias
|
||||
}
|
||||
|
||||
constructor(value: String? = DESUtils.empty) : this() {
|
||||
@ -17,6 +19,7 @@ class GoogleReaderSecurityKey private constructor() : SecurityKey() {
|
||||
serverUrl = it.serverUrl
|
||||
username = it.username
|
||||
password = it.password
|
||||
clientCertificateAlias = it.clientCertificateAlias
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,11 +70,13 @@ class FeverRssService @Inject constructor(
|
||||
private suspend fun getFeverAPI() =
|
||||
FeverSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
||||
FeverAPI.getInstance(
|
||||
context = context,
|
||||
serverUrl = serverUrl!!,
|
||||
username = username!!,
|
||||
password = password!!,
|
||||
httpUsername = null,
|
||||
httpPassword = null,
|
||||
clientCertificateAlias = clientCertificateAlias,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -72,11 +72,13 @@ class GoogleReaderRssService @Inject constructor(
|
||||
private suspend fun getGoogleReaderAPI() =
|
||||
GoogleReaderSecurityKey(accountDao.queryById(context.currentAccountId)!!.securityKey).run {
|
||||
GoogleReaderAPI.getInstance(
|
||||
context = context,
|
||||
serverUrl = serverUrl!!,
|
||||
username = username!!,
|
||||
password = password!!,
|
||||
httpUsername = null,
|
||||
httpPassword = null,
|
||||
clientCertificateAlias = clientCertificateAlias,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,9 @@
|
||||
|
||||
package me.ash.reader.infrastructure.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.security.KeyChain
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@ -31,15 +33,18 @@ import okhttp3.Cache
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.platform.Platform
|
||||
import java.io.File
|
||||
import java.net.Socket
|
||||
import java.security.KeyManagementException
|
||||
import java.security.NoSuchAlgorithmException
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.X509KeyManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
@ -54,6 +59,7 @@ object OkHttpClientModule {
|
||||
fun provideOkHttpClient(
|
||||
@ApplicationContext context: Context,
|
||||
): OkHttpClient = cachingHttpClient(
|
||||
context = context,
|
||||
cacheDirectory = context.cacheDir.resolve("http")
|
||||
).newBuilder()
|
||||
.addNetworkInterceptor(UserAgentInterceptor)
|
||||
@ -61,11 +67,13 @@ object OkHttpClientModule {
|
||||
}
|
||||
|
||||
fun cachingHttpClient(
|
||||
context: Context,
|
||||
cacheDirectory: File? = null,
|
||||
cacheSize: Long = 10L * 1024L * 1024L,
|
||||
trustAllCerts: Boolean = true,
|
||||
connectTimeoutSecs: Long = 30L,
|
||||
readTimeoutSecs: Long = 30L,
|
||||
clientCertificateAlias: String? = null,
|
||||
): OkHttpClient {
|
||||
val builder: OkHttpClient.Builder = OkHttpClient.Builder()
|
||||
|
||||
@ -78,31 +86,75 @@ fun cachingHttpClient(
|
||||
.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
|
||||
if (trustAllCerts) {
|
||||
builder.trustAllCerts()
|
||||
if (!clientCertificateAlias.isNullOrBlank() || trustAllCerts) {
|
||||
builder.setupSsl(context, clientCertificateAlias, trustAllCerts)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun OkHttpClient.Builder.trustAllCerts() {
|
||||
fun OkHttpClient.Builder.setupSsl(
|
||||
context: Context,
|
||||
clientCertificateAlias: String?,
|
||||
trustAllCerts: Boolean
|
||||
) {
|
||||
try {
|
||||
val trustManager = object : X509TrustManager {
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
val clientKeyManager = clientCertificateAlias?.let { clientAlias ->
|
||||
object : X509KeyManager {
|
||||
override fun getClientAliases(keyType: String?, issuers: Array<Principal>?) =
|
||||
throw UnsupportedOperationException("getClientAliases")
|
||||
|
||||
override fun chooseClientAlias(
|
||||
keyType: Array<String>?,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
) = clientCertificateAlias
|
||||
|
||||
override fun getServerAliases(keyType: String?, issuers: Array<Principal>?) =
|
||||
throw UnsupportedOperationException("getServerAliases")
|
||||
|
||||
override fun chooseServerAlias(
|
||||
keyType: String?,
|
||||
issuers: Array<Principal>?,
|
||||
socket: Socket?
|
||||
) = throw UnsupportedOperationException("chooseServerAlias")
|
||||
|
||||
override fun getCertificateChain(alias: String?): Array<X509Certificate>? {
|
||||
return if (alias == clientAlias) KeyChain.getCertificateChain(context, clientAlias) else null
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
override fun getPrivateKey(alias: String?): PrivateKey? {
|
||||
return if (alias == clientAlias) KeyChain.getPrivateKey(context, clientAlias) else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val trustManager = if (trustAllCerts) {
|
||||
hostnameVerifier { _, _ -> true }
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
object : X509TrustManager {
|
||||
override fun checkClientTrusted(
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?
|
||||
) = Unit
|
||||
|
||||
override fun checkServerTrusted(
|
||||
chain: Array<out X509Certificate>?,
|
||||
authType: String?
|
||||
) = Unit
|
||||
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = emptyArray()
|
||||
}
|
||||
} else {
|
||||
Platform.get().platformTrustManager()
|
||||
}
|
||||
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, arrayOf<TrustManager>(trustManager), null)
|
||||
sslContext.init(arrayOf(clientKeyManager), arrayOf(trustManager), null)
|
||||
val sslSocketFactory = sslContext.socketFactory
|
||||
|
||||
sslSocketFactory(sslSocketFactory, trustManager)
|
||||
.hostnameVerifier(HostnameVerifier { _, _ -> true })
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
// ignore
|
||||
} catch (e: KeyManagementException) {
|
||||
|
@ -1,14 +1,18 @@
|
||||
package me.ash.reader.infrastructure.rss.provider
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import me.ash.reader.infrastructure.di.UserAgentInterceptor
|
||||
import me.ash.reader.infrastructure.di.cachingHttpClient
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
abstract class ProviderAPI {
|
||||
abstract class ProviderAPI(context: Context, clientCertificateAlias: String?) {
|
||||
|
||||
protected val client: OkHttpClient = cachingHttpClient()
|
||||
protected val client: OkHttpClient = cachingHttpClient(
|
||||
context = context,
|
||||
clientCertificateAlias = clientCertificateAlias,
|
||||
)
|
||||
.newBuilder()
|
||||
.addNetworkInterceptor(UserAgentInterceptor)
|
||||
.build()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package me.ash.reader.infrastructure.rss.provider.fever
|
||||
|
||||
import android.content.Context
|
||||
import me.ash.reader.infrastructure.exception.FeverAPIException
|
||||
import me.ash.reader.infrastructure.rss.provider.ProviderAPI
|
||||
import me.ash.reader.ui.ext.encodeBase64
|
||||
@ -10,11 +11,13 @@ import okhttp3.executeAsync
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class FeverAPI private constructor(
|
||||
context: Context,
|
||||
private val serverUrl: String,
|
||||
private val apiKey: String,
|
||||
private val httpUsername: String? = null,
|
||||
private val httpPassword: String? = null,
|
||||
) : ProviderAPI() {
|
||||
clientCertificateAlias: String? = null,
|
||||
) : ProviderAPI(context, clientCertificateAlias) {
|
||||
|
||||
private suspend inline fun <reified T> postRequest(query: String?): T {
|
||||
val response = client.newCall(
|
||||
@ -104,14 +107,16 @@ class FeverAPI private constructor(
|
||||
private val instances: ConcurrentHashMap<String, FeverAPI> = ConcurrentHashMap()
|
||||
|
||||
fun getInstance(
|
||||
context: Context,
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
password: String,
|
||||
httpUsername: String? = null,
|
||||
httpPassword: String? = null,
|
||||
clientCertificateAlias: String? = null,
|
||||
): FeverAPI = "$username:$password".md5().run {
|
||||
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword") {
|
||||
FeverAPI(serverUrl, this, httpUsername, httpPassword)
|
||||
instances.getOrPut("$serverUrl$this$httpUsername$httpPassword$clientCertificateAlias") {
|
||||
FeverAPI(context, serverUrl, this, httpUsername, httpPassword, clientCertificateAlias)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
package me.ash.reader.infrastructure.rss.provider.greader
|
||||
|
||||
import android.content.Context
|
||||
import me.ash.reader.infrastructure.di.USER_AGENT_STRING
|
||||
import me.ash.reader.infrastructure.exception.GoogleReaderAPIException
|
||||
import me.ash.reader.infrastructure.exception.RetryException
|
||||
@ -10,12 +11,14 @@ import okhttp3.executeAsync
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class GoogleReaderAPI private constructor(
|
||||
context: Context,
|
||||
private val serverUrl: String,
|
||||
private val username: String,
|
||||
private val password: String,
|
||||
private val httpUsername: String? = null,
|
||||
private val httpPassword: String? = null,
|
||||
) : ProviderAPI() {
|
||||
clientCertificateAlias: String? = null,
|
||||
) : ProviderAPI(context, clientCertificateAlias) {
|
||||
|
||||
enum class Stream(val tag: String) {
|
||||
ALL_ITEMS("user/-/state/com.google/reading-list"),
|
||||
@ -350,13 +353,15 @@ class GoogleReaderAPI private constructor(
|
||||
private val instances: ConcurrentHashMap<String, GoogleReaderAPI> = ConcurrentHashMap()
|
||||
|
||||
fun getInstance(
|
||||
context: Context,
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
password: String,
|
||||
httpUsername: String? = null,
|
||||
httpPassword: String? = null,
|
||||
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword") {
|
||||
GoogleReaderAPI(serverUrl, username, password, httpUsername, httpPassword)
|
||||
clientCertificateAlias: String? = null
|
||||
): GoogleReaderAPI = instances.getOrPut("$serverUrl$username$password$httpUsername$httpPassword$clientCertificateAlias") {
|
||||
GoogleReaderAPI(context, serverUrl, username, password, httpUsername, httpPassword, clientCertificateAlias)
|
||||
}
|
||||
|
||||
fun clearInstance() {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.component.base
|
||||
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
@ -22,6 +24,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
@ -46,6 +49,7 @@ fun RYOutlineTextField(
|
||||
errorMessage: String = "",
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@ -59,7 +63,11 @@ fun RYOutlineTextField(
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.focusRequester(focusRequester),
|
||||
modifier = if (onClick != null) {
|
||||
Modifier.focusProperties { canFocus = false }
|
||||
} else {
|
||||
Modifier.focusRequester(focusRequester)
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
focusedContainerColor = Color.Transparent
|
||||
@ -115,5 +123,18 @@ fun RYOutlineTextField(
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
readOnly = onClick != null,
|
||||
interactionSource = onClick?.let {
|
||||
remember { MutableInteractionSource() }
|
||||
.also { interactionSource ->
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect {
|
||||
if (it is PressInteraction.Release) {
|
||||
onClick.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.addition
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -55,6 +57,7 @@ fun AddFeverAccountDialog(
|
||||
var feverServerUrl by rememberSaveable { mutableStateOf("") }
|
||||
var feverUsername by rememberSaveable { mutableStateOf("") }
|
||||
var feverPassword by rememberSaveable { mutableStateOf("") }
|
||||
var feverClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
RYDialog(
|
||||
modifier = Modifier.padding(horizontal = 44.dp),
|
||||
@ -121,6 +124,19 @@ fun AddFeverAccountDialog(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
RYOutlineTextField(
|
||||
requestFocus = false,
|
||||
readOnly = accountUiState.isLoading,
|
||||
value = feverClientCertificateAlias,
|
||||
onValueChange = { feverClientCertificateAlias = it },
|
||||
label = stringResource(R.string.client_certificate),
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
feverClientCertificateAlias = alias ?: ""
|
||||
}, null, null, null, null)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -138,6 +154,7 @@ fun AddFeverAccountDialog(
|
||||
serverUrl = feverServerUrl,
|
||||
username = feverUsername,
|
||||
password = feverPassword,
|
||||
clientCertificateAlias = feverClientCertificateAlias,
|
||||
).toString(),
|
||||
)) { account, exception ->
|
||||
if (account == null) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.addition
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -55,6 +57,7 @@ fun AddFreshRSSAccountDialog(
|
||||
var freshRSSServerUrl by rememberSaveable { mutableStateOf("") }
|
||||
var freshRSSUsername by rememberSaveable { mutableStateOf("") }
|
||||
var freshRSSPassword by rememberSaveable { mutableStateOf("") }
|
||||
var freshRSSClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
RYDialog(
|
||||
modifier = Modifier.padding(horizontal = 44.dp),
|
||||
@ -122,6 +125,19 @@ fun AddFreshRSSAccountDialog(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
RYOutlineTextField(
|
||||
requestFocus = false,
|
||||
readOnly = accountUiState.isLoading,
|
||||
value = freshRSSClientCertificateAlias,
|
||||
onValueChange = { freshRSSClientCertificateAlias = it },
|
||||
label = stringResource(R.string.client_certificate),
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
freshRSSClientCertificateAlias = alias ?: ""
|
||||
}, null, null, null, null)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -142,6 +158,7 @@ fun AddFreshRSSAccountDialog(
|
||||
serverUrl = freshRSSServerUrl,
|
||||
username = freshRSSUsername,
|
||||
password = freshRSSPassword,
|
||||
clientCertificateAlias = freshRSSClientCertificateAlias,
|
||||
).toString(),
|
||||
)) { account, exception ->
|
||||
if (account == null) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.addition
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
@ -56,6 +58,7 @@ fun AddGoogleReaderAccountDialog(
|
||||
var googleReaderServerUrl by rememberSaveable { mutableStateOf("") }
|
||||
var googleReaderUsername by rememberSaveable { mutableStateOf("") }
|
||||
var googleReaderPassword by rememberSaveable { mutableStateOf("") }
|
||||
var googleReaderClientCertificateAlias by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
RYDialog(
|
||||
modifier = Modifier.padding(horizontal = 44.dp),
|
||||
@ -123,6 +126,19 @@ fun AddGoogleReaderAccountDialog(
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
RYOutlineTextField(
|
||||
requestFocus = false,
|
||||
readOnly = accountUiState.isLoading,
|
||||
value = googleReaderClientCertificateAlias,
|
||||
onValueChange = { googleReaderClientCertificateAlias = it },
|
||||
label = stringResource(R.string.client_certificate),
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
googleReaderClientCertificateAlias = alias ?: ""
|
||||
}, null, null, null, null)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
@ -143,6 +159,7 @@ fun AddGoogleReaderAccountDialog(
|
||||
serverUrl = googleReaderServerUrl,
|
||||
username = googleReaderUsername,
|
||||
password = googleReaderPassword,
|
||||
clientCertificateAlias = googleReaderClientCertificateAlias.takeIf { it.isNotEmpty() },
|
||||
).toString(),
|
||||
)) { account, exception ->
|
||||
if (account == null) {
|
||||
|
@ -1,7 +1,16 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.connection
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.R
|
||||
@ -17,6 +26,8 @@ fun LazyItemScope.FeverConnection(
|
||||
account: Account,
|
||||
viewModel: AccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val securityKey by remember {
|
||||
derivedStateOf { FeverSecurityKey(account.securityKey) }
|
||||
}
|
||||
@ -56,6 +67,16 @@ fun LazyItemScope.FeverConnection(
|
||||
passwordDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.client_certificate),
|
||||
desc = securityKey.clientCertificateAlias,
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
securityKey.clientCertificateAlias = alias
|
||||
save(account, viewModel, securityKey)
|
||||
}, null, null, null, null)
|
||||
},
|
||||
) {}
|
||||
|
||||
TextFieldDialog(
|
||||
visible = serverUrlDialogVisible,
|
||||
|
@ -1,7 +1,16 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.connection
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.R
|
||||
@ -17,6 +26,8 @@ fun LazyItemScope.FreshRSSConnection(
|
||||
account: Account,
|
||||
viewModel: AccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val securityKey by remember {
|
||||
derivedStateOf { FreshRSSSecurityKey(account.securityKey) }
|
||||
}
|
||||
@ -56,6 +67,16 @@ fun LazyItemScope.FreshRSSConnection(
|
||||
passwordDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.client_certificate),
|
||||
desc = securityKey.clientCertificateAlias,
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
securityKey.clientCertificateAlias = alias
|
||||
save(account, viewModel, securityKey)
|
||||
}, null, null, null, null)
|
||||
},
|
||||
) {}
|
||||
|
||||
TextFieldDialog(
|
||||
visible = serverUrlDialogVisible,
|
||||
|
@ -1,7 +1,16 @@
|
||||
package me.ash.reader.ui.page.settings.accounts.connection
|
||||
|
||||
import android.app.Activity
|
||||
import android.security.KeyChain
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.ash.reader.R
|
||||
@ -17,6 +26,8 @@ fun LazyItemScope.GoogleReaderConnection(
|
||||
account: Account,
|
||||
viewModel: AccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val securityKey by remember {
|
||||
derivedStateOf { GoogleReaderSecurityKey(account.securityKey) }
|
||||
}
|
||||
@ -56,6 +67,16 @@ fun LazyItemScope.GoogleReaderConnection(
|
||||
passwordDialogVisible = true
|
||||
},
|
||||
) {}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.client_certificate),
|
||||
desc = securityKey.clientCertificateAlias,
|
||||
onClick = {
|
||||
KeyChain.choosePrivateKeyAlias(context as Activity, { alias ->
|
||||
securityKey.clientCertificateAlias = alias
|
||||
save(account, viewModel, securityKey)
|
||||
}, null, null, null, null)
|
||||
},
|
||||
) {}
|
||||
|
||||
TextFieldDialog(
|
||||
visible = serverUrlDialogVisible,
|
||||
|
@ -392,6 +392,7 @@
|
||||
<string name="server_url">Server URL</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="password">Password</string>
|
||||
<string name="client_certificate">Client certificate (optional)</string>
|
||||
<string name="connection">Connection</string>
|
||||
<string name="system_default">System</string>
|
||||
<string name="initial_open_app">App when link is clicked</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user