Compare commits

...

6 commits

18 changed files with 176 additions and 121 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "70da3095877de4a82021855471523b90", "identityHash": "ffa6a7f7cce2d67541e0fbe28441e780",
"entities": [ "entities": [
{ {
"tableName": "HistoryEntry", "tableName": "HistoryEntry",
@ -38,7 +38,7 @@
}, },
{ {
"tableName": "Identity", "tableName": "Identity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `key` TEXT NOT NULL, `name` TEXT)", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `key` TEXT NOT NULL, `name` TEXT, `urls` TEXT NOT NULL)",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -57,38 +57,12 @@
"columnName": "name", "columnName": "name",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "IdentityUsage",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uri` TEXT NOT NULL, `identityId` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
}, },
{ {
"fieldPath": "uri", "fieldPath": "urls",
"columnName": "uri", "columnName": "urls",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
},
{
"fieldPath": "identityId",
"columnName": "identityId",
"affinity": "INTEGER",
"notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
@ -104,7 +78,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '70da3095877de4a82021855471523b90')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ffa6a7f7cce2d67541e0fbe28441e780')"
] ]
} }
} }

View file

@ -1,18 +1,18 @@
package dev.lowrespalmtree.comet package dev.lowrespalmtree.comet
import android.content.Context import android.content.Context
import android.util.Base64
import androidx.room.*
import androidx.room.Database import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database( @Database(
entities = [ entities = [
History.HistoryEntry::class, History.HistoryEntry::class,
Identities.Identity::class, Identities.Identity::class,
Identities.IdentityUsage::class,
], ],
version = 1 version = 1
) )
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun historyEntryDao(): History.HistoryEntryDao abstract fun historyEntryDao(): History.HistoryEntryDao
abstract fun identityDao(): Identities.IdentityDao abstract fun identityDao(): Identities.IdentityDao
@ -26,4 +26,18 @@ object Database {
return return
INSTANCE = Room.databaseBuilder(context, AppDatabase::class.java, "comet.db").build() INSTANCE = Room.databaseBuilder(context, AppDatabase::class.java, "comet.db").build()
} }
}
typealias UrlList = ArrayList<String>
class Converters {
@TypeConverter
fun fromUrlList(value: UrlList?): String? =
value?.joinToString("-") {
Base64.encodeToString(it.encodeToByteArray(), Base64.DEFAULT)
}
@TypeConverter
fun stringToUrlList(value: String?): UrlList? =
value?.split("-")?.map { Base64.decode(it, Base64.DEFAULT).decodeToString() } as UrlList?
} }

View file

@ -4,8 +4,10 @@ import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Log import android.util.Log
import androidx.room.* import androidx.room.*
import java.lang.IllegalArgumentException
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.KeyStore import java.security.KeyStore
import javax.security.auth.x500.X500Principal
object Identities { object Identities {
@Entity @Entity
@ -16,16 +18,8 @@ object Identities {
val key: String, val key: String,
/** Label for this identity. */ /** Label for this identity. */
var name: String?, var name: String?,
) /** URL paths configured to use this identity. */
var urls: UrlList
@Entity
data class IdentityUsage(
/** ID. */
@PrimaryKey(autoGenerate = true) val id: Int,
/** URL path where an identity can be used. */
val uri: String,
/** ID of the Identity to use. */
val identityId: Int
) )
@Dao @Dao
@ -42,15 +36,12 @@ object Identities {
@Update @Update
suspend fun update(vararg identities: Identity) suspend fun update(vararg identities: Identity)
@Query("SELECT * FROM IdentityUsage WHERE :identityId = identityId")
suspend fun getUsagesFor(identityId: Int): List<IdentityUsage>
@Delete @Delete
suspend fun delete(vararg identities: Identity) suspend fun delete(vararg identities: Identity)
} }
suspend fun insert(key: String, name: String? = null): Long = suspend fun insert(key: String, name: String? = null): Long =
Database.INSTANCE.identityDao().insert(Identity(0, key, name)) Database.INSTANCE.identityDao().insert(Identity(0, key, name, arrayListOf()))
suspend fun get(id: Long): Identity? = suspend fun get(id: Long): Identity? =
Database.INSTANCE.identityDao().get(id) Database.INSTANCE.identityDao().get(id)
@ -69,11 +60,20 @@ object Identities {
Database.INSTANCE.identityDao().delete(*identities) Database.INSTANCE.identityDao().delete(*identities)
} }
fun generateClientCert(alias: String) { fun generateClientCert(alias: String, commonName: String) {
val algo = KeyProperties.KEY_ALGORITHM_RSA val algo = KeyProperties.KEY_ALGORITHM_RSA
val kpg = KeyPairGenerator.getInstance(algo, "AndroidKeyStore") val kpg = KeyPairGenerator.getInstance(algo, "AndroidKeyStore")
val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val spec = KeyGenParameterSpec.Builder(alias, purposes) val spec = KeyGenParameterSpec.Builder(alias, purposes)
.apply {
if (commonName.isNotEmpty()) {
try {
setCertificateSubject(X500Principal("CN=$commonName"))
} catch (e: IllegalArgumentException) {
Log.e(TAG, "generateClientCert: bad common name: ${e.message}")
}
}
}
.setDigests(KeyProperties.DIGEST_SHA256) .setDigests(KeyProperties.DIGEST_SHA256)
.build() .build()
kpg.initialize(spec) kpg.initialize(spec)
@ -83,7 +83,6 @@ object Identities {
private fun deleteClientCert(alias: String) { private fun deleteClientCert(alias: String) {
val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) } val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
Log.i(TAG, keyStore.aliases().toList().joinToString { it })
if (keyStore.containsAlias(alias)) { if (keyStore.containsAlias(alias)) {
keyStore.deleteEntry(alias) keyStore.deleteEntry(alias)
Log.i(TAG, "deleteClientCert: deleted entry with alias \"$alias\"") Log.i(TAG, "deleteClientCert: deleted entry with alias \"$alias\"")

View file

@ -35,7 +35,7 @@ class IdentitiesAdapter(private val listener: Listener) :
listener.onIdentityClick(item) listener.onIdentityClick(item)
} }
holder.binding.container.setOnLongClickListener { holder.binding.container.setOnLongClickListener {
listener.onIdentityLongClick(item, holder.itemView); listener.onIdentityLongClick(item, holder.itemView)
true true
} }
} }

View file

@ -20,7 +20,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialog.Listener { class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityEditDialog.Listener {
private val vm: IdentitiesViewModel by viewModels() private val vm: IdentitiesViewModel by viewModels()
private lateinit var binding: FragmentIdentitiesBinding private lateinit var binding: FragmentIdentitiesBinding
private lateinit var adapter: IdentitiesAdapter private lateinit var adapter: IdentitiesAdapter
@ -41,7 +41,7 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
adapter = IdentitiesAdapter(this) adapter = IdentitiesAdapter(this)
binding.list.adapter = adapter binding.list.adapter = adapter
binding.floatingActionButton.setOnClickListener { openNewIdentityEditor() } binding.floatingActionButton.setOnClickListener { openIdentityWizard() }
vm.identities.observe(viewLifecycleOwner) { adapter.setIdentities(it) } vm.identities.observe(viewLifecycleOwner) { adapter.setIdentities(it) }
@ -49,7 +49,7 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
} }
override fun onIdentityClick(identity: Identity) { override fun onIdentityClick(identity: Identity) {
IdentityDialog(requireContext(), identity, this).show() IdentityEditDialog(requireContext(), identity, this).show()
} }
override fun onIdentityLongClick(identity: Identity, view: View) { override fun onIdentityLongClick(identity: Identity, view: View) {
@ -59,7 +59,7 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
setOnMenuItemClickListener { item -> setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.item_edit -> { R.id.item_edit -> {
IdentityDialog( IdentityEditDialog(
requireContext(), requireContext(),
identity, identity,
this@IdentitiesFragment this@IdentitiesFragment
@ -82,27 +82,39 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
vm.saveIdentity(identity) vm.saveIdentity(identity)
} }
private fun openNewIdentityEditor() { /**
toast(requireContext(), R.string.generating_keypair) * Open the new identity wizard.
vm.newIdentity.observe(viewLifecycleOwner) { identity -> *
if (identity == null) * There is a first dialog to ask the user about the desired subject common name,
return@observe * then the certificate is generated and the edition dialog is opened.
vm.newIdentity.removeObservers(viewLifecycleOwner) */
vm.newIdentity.value = null private fun openIdentityWizard() {
IdentityDialog(requireContext(), identity, this).show() InputDialog(requireContext(), getString(R.string.input_common_name))
} .show(
vm.createNewIdentity() onOk = { text ->
toast(requireContext(), R.string.generating_keypair)
vm.newIdentity.observe(viewLifecycleOwner) { identity ->
if (identity == null)
return@observe
vm.newIdentity.removeObservers(viewLifecycleOwner)
vm.newIdentity.value = null
IdentityEditDialog(requireContext(), identity, this).show()
}
vm.createNewIdentity(text)
},
onDismiss = {}
)
} }
class IdentitiesViewModel : ViewModel() { class IdentitiesViewModel : ViewModel() {
val identities: MutableLiveData<List<Identity>> by lazy { MutableLiveData<List<Identity>>() } val identities: MutableLiveData<List<Identity>> by lazy { MutableLiveData<List<Identity>>() }
val newIdentity: MutableLiveData<Identity> by lazy { MutableLiveData<Identity>() } val newIdentity: MutableLiveData<Identity> by lazy { MutableLiveData<Identity>() }
fun createNewIdentity() { fun createNewIdentity(commonName: String) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val alias = "identity-${UUID.randomUUID()}" val alias = "identity-${UUID.randomUUID()}"
Identities.generateClientCert(alias) Identities.generateClientCert(alias, commonName)
val newIdentityId = Identities.insert(alias) val newIdentityId = Identities.insert(alias, commonName)
newIdentity.postValue(Identities.get(newIdentityId)) newIdentity.postValue(Identities.get(newIdentityId))
} }
.invokeOnCompletion { refreshIdentities() } .invokeOnCompletion { refreshIdentities() }

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import dev.lowrespalmtree.comet.databinding.DialogIdentityBinding import dev.lowrespalmtree.comet.databinding.DialogIdentityBinding
class IdentityDialog( class IdentityEditDialog(
private val context: Context, private val context: Context,
private val identity: Identities.Identity, private val identity: Identities.Identity,
private val listener: Listener private val listener: Listener
@ -19,12 +19,14 @@ class IdentityDialog(
fun show() { fun show() {
binding = DialogIdentityBinding.inflate(LayoutInflater.from(context)) binding = DialogIdentityBinding.inflate(LayoutInflater.from(context))
binding.labelInput.setText(identity.name.orEmpty()) binding.labelInput.setText(identity.name.orEmpty())
binding.urlInput.setText(identity.urls.getOrNull(0).orEmpty())
binding.aliasText.text = identity.key binding.aliasText.text = identity.key
AlertDialog.Builder(context) AlertDialog.Builder(context)
.setTitle(R.string.edit_identity) .setTitle(R.string.edit_identity)
.setView(binding.root) .setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
identity.name = binding.labelInput.text.toString() identity.name = binding.labelInput.text.toString()
identity.urls = arrayListOf(binding.urlInput.text.toString())
listener.onSaveIdentity(identity) listener.onSaveIdentity(identity)
} }
.create() .create()

View file

@ -0,0 +1,23 @@
package dev.lowrespalmtree.comet
import android.app.AlertDialog
import android.content.Context
import android.view.LayoutInflater
import dev.lowrespalmtree.comet.databinding.DialogInputBinding
/** Generic text input dialog. Used for code 10 and a few other simple text input. */
class InputDialog(
private val context: Context,
private val prompt: String
) {
fun show(onOk: (text: String) -> Unit, onDismiss: () -> Unit) {
val binding = DialogInputBinding.inflate(LayoutInflater.from(context))
AlertDialog.Builder(context)
.setMessage(prompt)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> onOk(binding.textInput.text.toString()) }
.setOnDismissListener { onDismiss() }
.create()
.show()
}
}

View file

@ -5,19 +5,15 @@ import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.InputType
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.TextView import android.widget.TextView
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.setMargins
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -122,11 +118,13 @@ class PageFragment : Fragment(), PageAdapter.Listener {
when (uri.scheme) { when (uri.scheme) {
"gemini" -> { "gemini" -> {
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
val protocol =
prefs.getString("tls_version", Request.DEFAULT_TLS_VERSION)!!
val connectionTimeout = val connectionTimeout =
prefs.getInt("connection_timeout", Request.DEFAULT_CONNECTION_TIMEOUT_SEC) prefs.getInt("connection_timeout", Request.DEFAULT_CONNECTION_TIMEOUT_SEC)
val readTimeout = val readTimeout =
prefs.getInt("read_timeout", Request.DEFAULT_READ_TIMEOUT_SEC) prefs.getInt("read_timeout", Request.DEFAULT_READ_TIMEOUT_SEC)
vm.sendGeminiRequest(uri, connectionTimeout, readTimeout) vm.sendGeminiRequest(uri, protocol, connectionTimeout, readTimeout)
} }
else -> openUnknownScheme(uri) else -> openUnknownScheme(uri)
} }
@ -196,29 +194,14 @@ class PageFragment : Fragment(), PageAdapter.Listener {
} }
private fun askForInput(prompt: String, uri: Uri) { private fun askForInput(prompt: String, uri: Uri) {
val editText = EditText(requireContext()) InputDialog(requireContext(), prompt.ifEmpty { "Input required" })
editText.inputType = InputType.TYPE_CLASS_TEXT .show(
val inputView = FrameLayout(requireContext()).apply { onOk = { text ->
addView(FrameLayout(requireContext()).apply { val newUri = uri.buildUpon().query(text).build()
addView(editText) openUrl(newUri.toString(), base = vm.currentUrl)
val params = FrameLayout.LayoutParams( },
FrameLayout.LayoutParams.MATCH_PARENT, onDismiss = { updateState(PageViewModel.State.IDLE) }
FrameLayout.LayoutParams.WRAP_CONTENT )
)
params.setMargins(resources.getDimensionPixelSize(R.dimen.text_margin))
layoutParams = params
})
}
AlertDialog.Builder(requireContext())
.setMessage(prompt.ifEmpty { "Input required" })
.setView(inputView)
.setPositiveButton(android.R.string.ok) { _, _ ->
val newUri = uri.buildUpon().query(editText.text.toString()).build()
openUrl(newUri.toString(), base = vm.currentUrl)
}
.setOnDismissListener { updateState(PageViewModel.State.IDLE) }
.create()
.show()
} }
private fun openUnknownScheme(uri: Uri) { private fun openUnknownScheme(uri: Uri) {

View file

@ -52,7 +52,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
* The URI must be valid, absolute and with a gemini scheme. * The URI must be valid, absolute and with a gemini scheme.
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
fun sendGeminiRequest(uri: Uri, connectionTimeout: Int, readTimeout: Int, redirects: Int = 0) { fun sendGeminiRequest(uri: Uri, protocol: String, connectionTimeout: Int, readTimeout: Int, redirects: Int = 0) {
Log.d(TAG, "sendRequest: URI \"$uri\"") Log.d(TAG, "sendRequest: URI \"$uri\"")
loadingUrl = uri loadingUrl = uri
state.postValue(State.CONNECTING) state.postValue(State.CONNECTING)
@ -60,7 +60,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
requestJob = viewModelScope.launch(Dispatchers.IO) { requestJob = viewModelScope.launch(Dispatchers.IO) {
val response = try { val response = try {
val request = Request(uri) val request = Request(uri)
val socket = request.connect(connectionTimeout, readTimeout) val socket = request.connect(protocol, connectionTimeout, readTimeout)
val channel = request.proceed(socket, this) val channel = request.proceed(socket, this)
Response.from(channel, viewModelScope) Response.from(channel, viewModelScope)
} catch (e: Exception) { } catch (e: Exception) {
@ -71,7 +71,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
signalError( signalError(
when (e) { when (e) {
is UnknownHostException -> "Unknown host \"${uri.authority}\"." is UnknownHostException -> "Unknown host \"${uri.authority}\"."
is ConnectException -> "Can't connect to this server: ${e.localizedMessage}." is ConnectException -> "Can't connect to this server: ${e.message}."
is SocketTimeoutException -> "Connection timed out." is SocketTimeoutException -> "Connection timed out."
is CancellationException -> "Connection cancelled: ${e.message}." is CancellationException -> "Connection cancelled: ${e.message}."
else -> "Oops, something failed!" else -> "Oops, something failed!"

View file

@ -17,9 +17,9 @@ import javax.net.ssl.X509TrustManager
class Request(private val uri: Uri) { class Request(private val uri: Uri) {
private val port get() = if (uri.port > 0) uri.port else 1965 private val port get() = if (uri.port > 0) uri.port else 1965
fun connect(connectionTimeout: Int, readTimeout: Int): SSLSocket { fun connect(protocol: String, connectionTimeout: Int, readTimeout: Int): SSLSocket {
Log.d(TAG, "connect") Log.d(TAG, "connect: $protocol, c.to. $connectionTimeout, r.to. $readTimeout")
val context = SSLContext.getInstance("TLSv1.2") val context = SSLContext.getInstance(protocol)
context.init(null, arrayOf(TrustManager()), null) context.init(null, arrayOf(TrustManager()), null)
val socket = context.socketFactory.createSocket() as SSLSocket val socket = context.socketFactory.createSocket() as SSLSocket
socket.soTimeout = readTimeout * 1000 socket.soTimeout = readTimeout * 1000
@ -70,6 +70,7 @@ class Request(private val uri: Uri) {
companion object { companion object {
private const val TAG = "Request" private const val TAG = "Request"
const val DEFAULT_TLS_VERSION = "TLSv1.3"
const val DEFAULT_CONNECTION_TIMEOUT_SEC = 10 const val DEFAULT_CONNECTION_TIMEOUT_SEC = 10
const val DEFAULT_READ_TIMEOUT_SEC = 10 const val DEFAULT_READ_TIMEOUT_SEC = 10
} }

View file

@ -28,6 +28,7 @@ fun confirm(context: Context, @StringRes prompt: Int, onOk: () -> Unit) {
.setTitle(R.string.confirm) .setTitle(R.string.confirm)
.setView(binding.root) .setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> onOk() } .setPositiveButton(android.R.string.ok) { _, _ -> onOk() }
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
.create() .create()
.show() .show()
} }

View file

@ -8,6 +8,6 @@
android:id="@+id/text_view" android:id="@+id/text_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16sp" /> style="@style/CometText" />
</LinearLayout> </LinearLayout>

View file

@ -3,12 +3,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="16dp" android:padding="16dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/identity_name" /> android:text="@string/identity_name"
style="@style/CometLabel" />
<EditText <EditText
android:id="@+id/labelInput" android:id="@+id/labelInput"
@ -18,10 +20,33 @@
android:importantForAutofill="no" android:importantForAutofill="no"
android:inputType="text" /> android:inputType="text" />
<Space
android:layout_width="wrap_content"
android:layout_height="8dp" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/identity_alias_title" /> android:text="@string/identity_usages"
style="@style/CometLabel" />
<EditText
android:id="@+id/urlInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/url"
android:inputType="textUri"
android:importantForAutofill="no" />
<Space
android:layout_width="wrap_content"
android:layout_height="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/identity_alias_title"
style="@style/CometLabel" />
<TextView <TextView
android:id="@+id/aliasText" android:id="@+id/aliasText"

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/text_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:inputType="text" />
</LinearLayout>

View file

@ -1,12 +1,11 @@
<resources> <resources>
<!-- Reply Preference --> <string-array name="tls_version_entries">
<string-array name="reply_entries"> <item>TLS v1.3</item>
<item>Reply</item> <item>TLS v1.2</item>
<item>Reply to all</item>
</string-array> </string-array>
<string-array name="reply_values"> <string-array name="tls_version_values">
<item>reply</item> <item>TLSv1.3</item>
<item>reply_all</item> <item>TLSv1.2</item>
</string-array> </string-array>
</resources> </resources>

View file

@ -44,5 +44,8 @@
<string name="confirm">Confirm</string> <string name="confirm">Confirm</string>
<string name="confirm_identity_delete">Are you sure you want to delete this identity? The client certificate cannot be retrieved afterwards.</string> <string name="confirm_identity_delete">Are you sure you want to delete this identity? The client certificate cannot be retrieved afterwards.</string>
<string name="edit">Edit</string> <string name="edit">Edit</string>
<string name="identity_usages">Active URL path</string>
<string name="input_common_name">Enter a name to use as the certificate\'s subject common name. This can be left empty.</string>
<string name="tls_version">TLS version</string>
</resources> </resources>

View file

@ -14,4 +14,9 @@
<item name="android:textIsSelectable">false</item> <item name="android:textIsSelectable">false</item>
<item name="android:background">?attr/selectableItemBackground</item> <item name="android:background">?attr/selectableItemBackground</item>
</style> </style>
<style name="CometLabel">
<item name="android:textSize">16sp</item>
<item name="android:textColor">@color/main_accent</item>
<item name="android:paddingTop">2dp</item>
</style>
</resources> </resources>

View file

@ -13,18 +13,18 @@
app:key="home_set" app:key="home_set"
app:title="@string/pref_home_set" /> app:title="@string/pref_home_set" />
<ListPreference
app:defaultValue="reply"
app:entries="@array/reply_entries"
app:entryValues="@array/reply_values"
app:key="reply"
app:title="@string/reply_title"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/pref_protocol_header"> <PreferenceCategory app:title="@string/pref_protocol_header">
<DropDownPreference
app:key="tls_version"
app:entries="@array/tls_version_entries"
app:entryValues="@array/tls_version_values"
app:defaultValue="TLSv1.3"
app:useSimpleSummaryProvider="true"
app:title="@string/tls_version" />
<SeekBarPreference <SeekBarPreference
android:defaultValue="10" android:defaultValue="10"
android:max="60" android:max="60"