diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt index 6f715c133..55de0d664 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/FeverSecurityKey.kt @@ -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 } } } diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt index 8ab1f3b7e..611d22741 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/FreshRSSSecurityKey.kt @@ -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 } } } diff --git a/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt b/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt index b0104e490..f824191df 100644 --- a/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt +++ b/app/src/main/java/me/ash/reader/domain/model/account/security/GoogleReaderSecurityKey.kt @@ -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 } } } diff --git a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt index d4462d738..385b52d82 100644 --- a/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt +++ b/app/src/main/java/me/ash/reader/domain/service/FeverRssService.kt @@ -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, ) } diff --git a/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt b/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt index a53affabd..85c68b689 100644 --- a/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt +++ b/app/src/main/java/me/ash/reader/domain/service/GoogleReaderRssService.kt @@ -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, ) } diff --git a/app/src/main/java/me/ash/reader/infrastructure/di/OkHttpClientModule.kt b/app/src/main/java/me/ash/reader/infrastructure/di/OkHttpClientModule.kt index 2973f4ff9..45edfa5f0 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/di/OkHttpClientModule.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/di/OkHttpClientModule.kt @@ -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?, authType: String?) { + val clientKeyManager = clientCertificateAlias?.let { clientAlias -> + object : X509KeyManager { + override fun getClientAliases(keyType: String?, issuers: Array?) = + throw UnsupportedOperationException("getClientAliases") + + override fun chooseClientAlias( + keyType: Array?, + issuers: Array?, + socket: Socket? + ) = clientCertificateAlias + + override fun getServerAliases(keyType: String?, issuers: Array?) = + throw UnsupportedOperationException("getServerAliases") + + override fun chooseServerAlias( + keyType: String?, + issuers: Array?, + socket: Socket? + ) = throw UnsupportedOperationException("chooseServerAlias") + + override fun getCertificateChain(alias: String?): Array? { + return if (alias == clientAlias) KeyChain.getCertificateChain(context, clientAlias) else null + } + + override fun getPrivateKey(alias: String?): PrivateKey? { + return if (alias == clientAlias) KeyChain.getPrivateKey(context, clientAlias) else null + } } + } - override fun checkServerTrusted(chain: Array?, authType: String?) { - } + val trustManager = if (trustAllCerts) { + hostnameVerifier { _, _ -> true } + + @SuppressLint("CustomX509TrustManager") + object : X509TrustManager { + override fun checkClientTrusted( + chain: Array?, + authType: String? + ) = Unit - override fun getAcceptedIssuers(): Array = emptyArray() + override fun checkServerTrusted( + chain: Array?, + authType: String? + ) = Unit + + override fun getAcceptedIssuers(): Array = emptyArray() + } + } else { + Platform.get().platformTrustManager() } val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, arrayOf(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) { diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt index d393b099d..8745c2c5c 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/ProviderAPI.kt @@ -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() diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/fever/FeverAPI.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/fever/FeverAPI.kt index 32388e7b6..c2d21b35e 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/fever/FeverAPI.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/fever/FeverAPI.kt @@ -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 postRequest(query: String?): T { val response = client.newCall( @@ -104,14 +107,16 @@ class FeverAPI private constructor( private val instances: ConcurrentHashMap = 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) } } diff --git a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt index 20cf413aa..29e2544c4 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/rss/provider/greader/GoogleReaderAPI.kt @@ -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 = 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() { diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt index 1a4698eb9..f37f9ce85 100644 --- a/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt @@ -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() + } + } + } + } + } ) } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt index 0324a0085..fd3a9a164 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt @@ -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) { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt index f830ae7f6..232404f20 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt @@ -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) { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt index 35fdea9c6..7a0e48c82 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt @@ -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) { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FeverConnection.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FeverConnection.kt index d92a409d9..fd56eaf47 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FeverConnection.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FeverConnection.kt @@ -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, diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FreshRSSConnection.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FreshRSSConnection.kt index 6e54749d6..b9dec52df 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FreshRSSConnection.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/FreshRSSConnection.kt @@ -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, diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/GoogleReaderConnection.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/GoogleReaderConnection.kt index 1980964af..19cd35608 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/GoogleReaderConnection.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/connection/GoogleReaderConnection.kt @@ -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, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e03a0bee1..10f3faf1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -392,6 +392,7 @@ Server URL Username Password + Client certificate (optional) Connection System App when link is clicked