Compare commits
6 commits
11bea0e585
...
4282567f5f
Author | SHA1 | Date | |
---|---|---|---|
dece | 4282567f5f | ||
dece | 1d69c075a1 | ||
dece | b6d855fdeb | ||
dece | 02ba3a1401 | ||
dece | ea6f54cd73 | ||
dece | 9a91c72d1e |
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "70da3095877de4a82021855471523b90",
|
||||
"identityHash": "ffa6a7f7cce2d67541e0fbe28441e780",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "HistoryEntry",
|
||||
|
@ -38,7 +38,7 @@
|
|||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
|
@ -57,38 +57,12 @@
|
|||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"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",
|
||||
"columnName": "uri",
|
||||
"fieldPath": "urls",
|
||||
"columnName": "urls",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "identityId",
|
||||
"columnName": "identityId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
|
@ -104,7 +78,7 @@
|
|||
"views": [],
|
||||
"setupQueries": [
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
package dev.lowrespalmtree.comet
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import androidx.room.*
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
History.HistoryEntry::class,
|
||||
Identities.Identity::class,
|
||||
Identities.IdentityUsage::class,
|
||||
],
|
||||
version = 1
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun historyEntryDao(): History.HistoryEntryDao
|
||||
abstract fun identityDao(): Identities.IdentityDao
|
||||
|
@ -27,3 +27,17 @@ object Database {
|
|||
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?
|
||||
}
|
|
@ -4,8 +4,10 @@ import android.security.keystore.KeyGenParameterSpec
|
|||
import android.security.keystore.KeyProperties
|
||||
import android.util.Log
|
||||
import androidx.room.*
|
||||
import java.lang.IllegalArgumentException
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import javax.security.auth.x500.X500Principal
|
||||
|
||||
object Identities {
|
||||
@Entity
|
||||
|
@ -16,16 +18,8 @@ object Identities {
|
|||
val key: String,
|
||||
/** Label for this identity. */
|
||||
var name: String?,
|
||||
)
|
||||
|
||||
@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
|
||||
/** URL paths configured to use this identity. */
|
||||
var urls: UrlList
|
||||
)
|
||||
|
||||
@Dao
|
||||
|
@ -42,15 +36,12 @@ object Identities {
|
|||
@Update
|
||||
suspend fun update(vararg identities: Identity)
|
||||
|
||||
@Query("SELECT * FROM IdentityUsage WHERE :identityId = identityId")
|
||||
suspend fun getUsagesFor(identityId: Int): List<IdentityUsage>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg identities: Identity)
|
||||
}
|
||||
|
||||
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? =
|
||||
Database.INSTANCE.identityDao().get(id)
|
||||
|
@ -69,11 +60,20 @@ object Identities {
|
|||
Database.INSTANCE.identityDao().delete(*identities)
|
||||
}
|
||||
|
||||
fun generateClientCert(alias: String) {
|
||||
fun generateClientCert(alias: String, commonName: String) {
|
||||
val algo = KeyProperties.KEY_ALGORITHM_RSA
|
||||
val kpg = KeyPairGenerator.getInstance(algo, "AndroidKeyStore")
|
||||
val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
||||
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)
|
||||
.build()
|
||||
kpg.initialize(spec)
|
||||
|
@ -83,7 +83,6 @@ object Identities {
|
|||
|
||||
private fun deleteClientCert(alias: String) {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
||||
Log.i(TAG, keyStore.aliases().toList().joinToString { it })
|
||||
if (keyStore.containsAlias(alias)) {
|
||||
keyStore.deleteEntry(alias)
|
||||
Log.i(TAG, "deleteClientCert: deleted entry with alias \"$alias\"")
|
||||
|
|
|
@ -35,7 +35,7 @@ class IdentitiesAdapter(private val listener: Listener) :
|
|||
listener.onIdentityClick(item)
|
||||
}
|
||||
holder.binding.container.setOnLongClickListener {
|
||||
listener.onIdentityLongClick(item, holder.itemView);
|
||||
listener.onIdentityLongClick(item, holder.itemView)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
||||
class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialog.Listener {
|
||||
class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityEditDialog.Listener {
|
||||
private val vm: IdentitiesViewModel by viewModels()
|
||||
private lateinit var binding: FragmentIdentitiesBinding
|
||||
private lateinit var adapter: IdentitiesAdapter
|
||||
|
@ -41,7 +41,7 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
|||
adapter = IdentitiesAdapter(this)
|
||||
binding.list.adapter = adapter
|
||||
|
||||
binding.floatingActionButton.setOnClickListener { openNewIdentityEditor() }
|
||||
binding.floatingActionButton.setOnClickListener { openIdentityWizard() }
|
||||
|
||||
vm.identities.observe(viewLifecycleOwner) { adapter.setIdentities(it) }
|
||||
|
||||
|
@ -49,7 +49,7 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
|||
}
|
||||
|
||||
override fun onIdentityClick(identity: Identity) {
|
||||
IdentityDialog(requireContext(), identity, this).show()
|
||||
IdentityEditDialog(requireContext(), identity, this).show()
|
||||
}
|
||||
|
||||
override fun onIdentityLongClick(identity: Identity, view: View) {
|
||||
|
@ -59,7 +59,7 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
|||
setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.item_edit -> {
|
||||
IdentityDialog(
|
||||
IdentityEditDialog(
|
||||
requireContext(),
|
||||
identity,
|
||||
this@IdentitiesFragment
|
||||
|
@ -82,27 +82,39 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
|||
vm.saveIdentity(identity)
|
||||
}
|
||||
|
||||
private fun openNewIdentityEditor() {
|
||||
/**
|
||||
* Open the new identity wizard.
|
||||
*
|
||||
* There is a first dialog to ask the user about the desired subject common name,
|
||||
* then the certificate is generated and the edition dialog is opened.
|
||||
*/
|
||||
private fun openIdentityWizard() {
|
||||
InputDialog(requireContext(), getString(R.string.input_common_name))
|
||||
.show(
|
||||
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
|
||||
IdentityDialog(requireContext(), identity, this).show()
|
||||
IdentityEditDialog(requireContext(), identity, this).show()
|
||||
}
|
||||
vm.createNewIdentity()
|
||||
vm.createNewIdentity(text)
|
||||
},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
|
||||
class IdentitiesViewModel : ViewModel() {
|
||||
val identities: MutableLiveData<List<Identity>> by lazy { MutableLiveData<List<Identity>>() }
|
||||
val newIdentity: MutableLiveData<Identity> by lazy { MutableLiveData<Identity>() }
|
||||
|
||||
fun createNewIdentity() {
|
||||
fun createNewIdentity(commonName: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val alias = "identity-${UUID.randomUUID()}"
|
||||
Identities.generateClientCert(alias)
|
||||
val newIdentityId = Identities.insert(alias)
|
||||
Identities.generateClientCert(alias, commonName)
|
||||
val newIdentityId = Identities.insert(alias, commonName)
|
||||
newIdentity.postValue(Identities.get(newIdentityId))
|
||||
}
|
||||
.invokeOnCompletion { refreshIdentities() }
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Context
|
|||
import android.view.LayoutInflater
|
||||
import dev.lowrespalmtree.comet.databinding.DialogIdentityBinding
|
||||
|
||||
class IdentityDialog(
|
||||
class IdentityEditDialog(
|
||||
private val context: Context,
|
||||
private val identity: Identities.Identity,
|
||||
private val listener: Listener
|
||||
|
@ -19,12 +19,14 @@ class IdentityDialog(
|
|||
fun show() {
|
||||
binding = DialogIdentityBinding.inflate(LayoutInflater.from(context))
|
||||
binding.labelInput.setText(identity.name.orEmpty())
|
||||
binding.urlInput.setText(identity.urls.getOrNull(0).orEmpty())
|
||||
binding.aliasText.text = identity.key
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.edit_identity)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
identity.name = binding.labelInput.text.toString()
|
||||
identity.urls = arrayListOf(binding.urlInput.text.toString())
|
||||
listener.onSaveIdentity(identity)
|
||||
}
|
||||
.create()
|
23
app/src/main/java/dev/lowrespalmtree/comet/InputDialog.kt
Normal file
23
app/src/main/java/dev/lowrespalmtree/comet/InputDialog.kt
Normal 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()
|
||||
}
|
||||
}
|
|
@ -5,19 +5,15 @@ import android.content.ActivityNotFoundException
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.addCallback
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.setMargins
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.PreferenceManager
|
||||
|
@ -122,11 +118,13 @@ class PageFragment : Fragment(), PageAdapter.Listener {
|
|||
when (uri.scheme) {
|
||||
"gemini" -> {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val protocol =
|
||||
prefs.getString("tls_version", Request.DEFAULT_TLS_VERSION)!!
|
||||
val connectionTimeout =
|
||||
prefs.getInt("connection_timeout", Request.DEFAULT_CONNECTION_TIMEOUT_SEC)
|
||||
val readTimeout =
|
||||
prefs.getInt("read_timeout", Request.DEFAULT_READ_TIMEOUT_SEC)
|
||||
vm.sendGeminiRequest(uri, connectionTimeout, readTimeout)
|
||||
vm.sendGeminiRequest(uri, protocol, connectionTimeout, readTimeout)
|
||||
}
|
||||
else -> openUnknownScheme(uri)
|
||||
}
|
||||
|
@ -196,29 +194,14 @@ class PageFragment : Fragment(), PageAdapter.Listener {
|
|||
}
|
||||
|
||||
private fun askForInput(prompt: String, uri: Uri) {
|
||||
val editText = EditText(requireContext())
|
||||
editText.inputType = InputType.TYPE_CLASS_TEXT
|
||||
val inputView = FrameLayout(requireContext()).apply {
|
||||
addView(FrameLayout(requireContext()).apply {
|
||||
addView(editText)
|
||||
val params = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
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()
|
||||
InputDialog(requireContext(), prompt.ifEmpty { "Input required" })
|
||||
.show(
|
||||
onOk = { text ->
|
||||
val newUri = uri.buildUpon().query(text).build()
|
||||
openUrl(newUri.toString(), base = vm.currentUrl)
|
||||
}
|
||||
.setOnDismissListener { updateState(PageViewModel.State.IDLE) }
|
||||
.create()
|
||||
.show()
|
||||
},
|
||||
onDismiss = { updateState(PageViewModel.State.IDLE) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun openUnknownScheme(uri: Uri) {
|
||||
|
|
|
@ -52,7 +52,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
|
|||
* The URI must be valid, absolute and with a gemini scheme.
|
||||
*/
|
||||
@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\"")
|
||||
loadingUrl = uri
|
||||
state.postValue(State.CONNECTING)
|
||||
|
@ -60,7 +60,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
|
|||
requestJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
val response = try {
|
||||
val request = Request(uri)
|
||||
val socket = request.connect(connectionTimeout, readTimeout)
|
||||
val socket = request.connect(protocol, connectionTimeout, readTimeout)
|
||||
val channel = request.proceed(socket, this)
|
||||
Response.from(channel, viewModelScope)
|
||||
} catch (e: Exception) {
|
||||
|
@ -71,7 +71,7 @@ class PageViewModel(@Suppress("unused") private val savedStateHandle: SavedState
|
|||
signalError(
|
||||
when (e) {
|
||||
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 CancellationException -> "Connection cancelled: ${e.message}."
|
||||
else -> "Oops, something failed!"
|
||||
|
|
|
@ -17,9 +17,9 @@ import javax.net.ssl.X509TrustManager
|
|||
class Request(private val uri: Uri) {
|
||||
private val port get() = if (uri.port > 0) uri.port else 1965
|
||||
|
||||
fun connect(connectionTimeout: Int, readTimeout: Int): SSLSocket {
|
||||
Log.d(TAG, "connect")
|
||||
val context = SSLContext.getInstance("TLSv1.2")
|
||||
fun connect(protocol: String, connectionTimeout: Int, readTimeout: Int): SSLSocket {
|
||||
Log.d(TAG, "connect: $protocol, c.to. $connectionTimeout, r.to. $readTimeout")
|
||||
val context = SSLContext.getInstance(protocol)
|
||||
context.init(null, arrayOf(TrustManager()), null)
|
||||
val socket = context.socketFactory.createSocket() as SSLSocket
|
||||
socket.soTimeout = readTimeout * 1000
|
||||
|
@ -70,6 +70,7 @@ class Request(private val uri: Uri) {
|
|||
|
||||
companion object {
|
||||
private const val TAG = "Request"
|
||||
const val DEFAULT_TLS_VERSION = "TLSv1.3"
|
||||
const val DEFAULT_CONNECTION_TIMEOUT_SEC = 10
|
||||
const val DEFAULT_READ_TIMEOUT_SEC = 10
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ fun confirm(context: Context, @StringRes prompt: Int, onOk: () -> Unit) {
|
|||
.setTitle(R.string.confirm)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> onOk() }
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||
.create()
|
||||
.show()
|
||||
}
|
|
@ -8,6 +8,6 @@
|
|||
android:id="@+id/text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16sp" />
|
||||
style="@style/CometText" />
|
||||
|
||||
</LinearLayout>
|
|
@ -3,12 +3,14 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/identity_name" />
|
||||
android:text="@string/identity_name"
|
||||
style="@style/CometLabel" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/labelInput"
|
||||
|
@ -18,10 +20,33 @@
|
|||
android:importantForAutofill="no"
|
||||
android:inputType="text" />
|
||||
|
||||
<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" />
|
||||
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
|
||||
android:id="@+id/aliasText"
|
||||
|
|
14
app/src/main/res/layout/dialog_input.xml
Normal file
14
app/src/main/res/layout/dialog_input.xml
Normal 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>
|
|
@ -1,12 +1,11 @@
|
|||
<resources>
|
||||
<!-- Reply Preference -->
|
||||
<string-array name="reply_entries">
|
||||
<item>Reply</item>
|
||||
<item>Reply to all</item>
|
||||
<string-array name="tls_version_entries">
|
||||
<item>TLS v1.3</item>
|
||||
<item>TLS v1.2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="reply_values">
|
||||
<item>reply</item>
|
||||
<item>reply_all</item>
|
||||
<string-array name="tls_version_values">
|
||||
<item>TLSv1.3</item>
|
||||
<item>TLSv1.2</item>
|
||||
</string-array>
|
||||
</resources>
|
|
@ -44,5 +44,8 @@
|
|||
<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="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>
|
|
@ -14,4 +14,9 @@
|
|||
<item name="android:textIsSelectable">false</item>
|
||||
<item name="android:background">?attr/selectableItemBackground</item>
|
||||
</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>
|
|
@ -13,18 +13,18 @@
|
|||
app:key="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 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
|
||||
android:defaultValue="10"
|
||||
android:max="60"
|
||||
|
|
Reference in a new issue