Compare commits
No commits in common. "11bea0e5859e17b56758d0dab39bac4ab7a40667" and "e077a3f4c576296edc0bad4d93ece308d781de17" have entirely different histories.
11bea0e585
...
e077a3f4c5
|
@ -5,7 +5,6 @@ import android.security.keystore.KeyProperties
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import java.security.KeyPairGenerator
|
import java.security.KeyPairGenerator
|
||||||
import java.security.KeyStore
|
|
||||||
|
|
||||||
object Identities {
|
object Identities {
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -44,9 +43,6 @@ object Identities {
|
||||||
|
|
||||||
@Query("SELECT * FROM IdentityUsage WHERE :identityId = identityId")
|
@Query("SELECT * FROM IdentityUsage WHERE :identityId = identityId")
|
||||||
suspend fun getUsagesFor(identityId: Int): List<IdentityUsage>
|
suspend fun getUsagesFor(identityId: Int): List<IdentityUsage>
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun delete(vararg identities: Identity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun insert(key: String, name: String? = null): Long =
|
suspend fun insert(key: String, name: String? = null): Long =
|
||||||
|
@ -61,14 +57,6 @@ object Identities {
|
||||||
suspend fun update(vararg identities: Identity) =
|
suspend fun update(vararg identities: Identity) =
|
||||||
Database.INSTANCE.identityDao().update(*identities)
|
Database.INSTANCE.identityDao().update(*identities)
|
||||||
|
|
||||||
suspend fun delete(vararg identities: Identity) {
|
|
||||||
for (identity in identities) {
|
|
||||||
if (identity.key.isNotEmpty())
|
|
||||||
deleteClientCert(identity.key)
|
|
||||||
}
|
|
||||||
Database.INSTANCE.identityDao().delete(*identities)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateClientCert(alias: String) {
|
fun generateClientCert(alias: 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")
|
||||||
|
@ -81,16 +69,5 @@ object Identities {
|
||||||
Log.i(TAG, "generateClientCert: key pair with alias \"$alias\" has been generated")
|
Log.i(TAG, "generateClientCert: key pair with alias \"$alias\" has been generated")
|
||||||
}
|
}
|
||||||
|
|
||||||
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\"")
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "deleteClientCert: no such alias \"$alias\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val TAG = "Identities"
|
private const val TAG = "Identities"
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dev.lowrespalmtree.comet.Identities.Identity
|
import dev.lowrespalmtree.comet.Identities.Identity
|
||||||
|
@ -15,7 +14,6 @@ class IdentitiesAdapter(private val listener: Listener) :
|
||||||
|
|
||||||
interface Listener {
|
interface Listener {
|
||||||
fun onIdentityClick(identity: Identity)
|
fun onIdentityClick(identity: Identity)
|
||||||
fun onIdentityLongClick(identity: Identity, view: View)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||||
|
@ -31,13 +29,7 @@ class IdentitiesAdapter(private val listener: Listener) :
|
||||||
val item = identities[position]
|
val item = identities[position]
|
||||||
holder.binding.labelText.text = item.name.orEmpty()
|
holder.binding.labelText.text = item.name.orEmpty()
|
||||||
holder.binding.keyText.text = item.key
|
holder.binding.keyText.text = item.key
|
||||||
holder.binding.container.setOnClickListener {
|
holder.binding.container.setOnClickListener { listener.onIdentityClick(item) }
|
||||||
listener.onIdentityClick(item)
|
|
||||||
}
|
|
||||||
holder.binding.container.setOnLongClickListener {
|
|
||||||
listener.onIdentityLongClick(item, holder.itemView);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = identities.size
|
override fun getItemCount(): Int = identities.size
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.os.Bundle
|
||||||
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.widget.PopupMenu
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
@ -14,7 +13,6 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import dev.lowrespalmtree.comet.Identities.Identity
|
import dev.lowrespalmtree.comet.Identities.Identity
|
||||||
import dev.lowrespalmtree.comet.databinding.FragmentIdentitiesBinding
|
import dev.lowrespalmtree.comet.databinding.FragmentIdentitiesBinding
|
||||||
import dev.lowrespalmtree.comet.utils.confirm
|
|
||||||
import dev.lowrespalmtree.comet.utils.toast
|
import dev.lowrespalmtree.comet.utils.toast
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -52,32 +50,6 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
||||||
IdentityDialog(requireContext(), identity, this).show()
|
IdentityDialog(requireContext(), identity, this).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onIdentityLongClick(identity: Identity, view: View) {
|
|
||||||
PopupMenu(requireContext(), view)
|
|
||||||
.apply {
|
|
||||||
menuInflater.inflate(R.menu.identity, this.menu)
|
|
||||||
setOnMenuItemClickListener { item ->
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.item_edit -> {
|
|
||||||
IdentityDialog(
|
|
||||||
requireContext(),
|
|
||||||
identity,
|
|
||||||
this@IdentitiesFragment
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
R.id.item_delete -> {
|
|
||||||
confirm(requireContext(), R.string.confirm_identity_delete) {
|
|
||||||
vm.deleteIdentity(identity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveIdentity(identity: Identity) {
|
override fun onSaveIdentity(identity: Identity) {
|
||||||
vm.saveIdentity(identity)
|
vm.saveIdentity(identity)
|
||||||
}
|
}
|
||||||
|
@ -85,10 +57,7 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
||||||
private fun openNewIdentityEditor() {
|
private fun openNewIdentityEditor() {
|
||||||
toast(requireContext(), R.string.generating_keypair)
|
toast(requireContext(), R.string.generating_keypair)
|
||||||
vm.newIdentity.observe(viewLifecycleOwner) { identity ->
|
vm.newIdentity.observe(viewLifecycleOwner) { identity ->
|
||||||
if (identity == null)
|
|
||||||
return@observe
|
|
||||||
vm.newIdentity.removeObservers(viewLifecycleOwner)
|
vm.newIdentity.removeObservers(viewLifecycleOwner)
|
||||||
vm.newIdentity.value = null
|
|
||||||
IdentityDialog(requireContext(), identity, this).show()
|
IdentityDialog(requireContext(), identity, this).show()
|
||||||
}
|
}
|
||||||
vm.createNewIdentity()
|
vm.createNewIdentity()
|
||||||
|
@ -105,25 +74,16 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
||||||
val newIdentityId = Identities.insert(alias)
|
val newIdentityId = Identities.insert(alias)
|
||||||
newIdentity.postValue(Identities.get(newIdentityId))
|
newIdentity.postValue(Identities.get(newIdentityId))
|
||||||
}
|
}
|
||||||
.invokeOnCompletion { refreshIdentities() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshIdentities() {
|
fun refreshIdentities() {
|
||||||
viewModelScope.launch(Dispatchers.IO) { identities.postValue(Identities.getAll()) }
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
identities.postValue(Identities.getAll())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveIdentity(identity: Identity) {
|
fun saveIdentity(identity: Identity) {
|
||||||
viewModelScope.launch(Dispatchers.IO) { Identities.update(identity) }
|
viewModelScope.launch(Dispatchers.IO) { Identities.update(identity) }
|
||||||
.invokeOnCompletion { refreshIdentities() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteIdentity(identity: Identity) {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) { Identities.delete(identity) }
|
|
||||||
.invokeOnCompletion { refreshIdentities() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "IdentitiesFragment"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,7 +18,7 @@ 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)
|
||||||
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)
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package dev.lowrespalmtree.comet
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.preference.EditTextPreference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import dev.lowrespalmtree.comet.Identities.Identity
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
|
class IdentityEditorFragment : PreferenceFragmentCompat() {
|
||||||
|
private val vm: IdentityEditorViewModel by viewModels()
|
||||||
|
private lateinit var namePref: EditTextPreference
|
||||||
|
|
||||||
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
setPreferencesFromResource(R.xml.identity_preferences, rootKey)
|
||||||
|
namePref = findPreference<EditTextPreference>("name")!!
|
||||||
|
|
||||||
|
// vm.identity.observe(viewLifecycleOwner) {
|
||||||
|
// namePref.apply {
|
||||||
|
// // TODO
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// arguments?.getLong("id")?.also { vm.loadIdentity(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class IdentityEditorViewModel : ViewModel() {
|
||||||
|
val identity: MutableLiveData<Identity> by lazy { MutableLiveData<Identity>() }
|
||||||
|
|
||||||
|
fun loadIdentity(id: Long) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
identity.postValue(Identities.get(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,10 @@
|
||||||
package dev.lowrespalmtree.comet.utils
|
package dev.lowrespalmtree.comet.utils
|
||||||
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import dev.lowrespalmtree.comet.R
|
import dev.lowrespalmtree.comet.R
|
||||||
import dev.lowrespalmtree.comet.databinding.DialogConfirmBinding
|
|
||||||
|
|
||||||
fun toast(context: Context, stringId: Int, length: Int = Toast.LENGTH_SHORT) =
|
fun toast(context: Context, stringId: Int, length: Int = Toast.LENGTH_SHORT) =
|
||||||
Toast.makeText(context, stringId, length).show()
|
Toast.makeText(context, stringId, length).show()
|
||||||
|
@ -20,14 +16,3 @@ fun getDrawableFromAttr(context: Context, @AttrRes attr: Int) =
|
||||||
|
|
||||||
fun getFancySelectBgRes(context: Context) =
|
fun getFancySelectBgRes(context: Context) =
|
||||||
getDrawableFromAttr(context, R.attr.selectableItemBackground)
|
getDrawableFromAttr(context, R.attr.selectableItemBackground)
|
||||||
|
|
||||||
fun confirm(context: Context, @StringRes prompt: Int, onOk: () -> Unit) {
|
|
||||||
val binding = DialogConfirmBinding.inflate(LayoutInflater.from(context))
|
|
||||||
binding.textView.setText(prompt)
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.confirm)
|
|
||||||
.setView(binding.root)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> onOk() }
|
|
||||||
.create()
|
|
||||||
.show()
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
<?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">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="16sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -3,4 +3,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"
|
||||||
style="@style/CometLink" />
|
style="@style/CometText"
|
||||||
|
android:textColor="@color/link"
|
||||||
|
android:textIsSelectable="false" />
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item android:id="@+id/item_edit" android:title="@string/edit" />
|
|
||||||
<item android:id="@+id/item_delete" android:title="@string/delete" />
|
|
||||||
</menu>
|
|
|
@ -31,7 +31,16 @@
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/identitiesFragment"
|
android:id="@+id/identitiesFragment"
|
||||||
android:name="dev.lowrespalmtree.comet.IdentitiesFragment"
|
android:name="dev.lowrespalmtree.comet.IdentitiesFragment"
|
||||||
android:label="IdentitiesFragment" />
|
android:label="IdentitiesFragment" >
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_identitiesFragment_to_identityEditorFragment"
|
||||||
|
app:destination="@id/identityEditorFragment" />
|
||||||
|
</fragment>
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/identityEditorFragment"
|
||||||
|
android:name="dev.lowrespalmtree.comet.IdentityEditorFragment"
|
||||||
|
android:label="IdentityEditorFragment" />
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/settingsFragment"
|
android:id="@+id/settingsFragment"
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<color name="url_bar">#002b36</color>
|
<color name="url_bar">#002b36</color>
|
||||||
<color name="url_bar_loading">#fdf6e3</color>
|
<color name="url_bar_loading">#fdf6e3</color>
|
||||||
<!--
|
<!--
|
||||||
Based on Solarized - https://ethanschoonover.com/solarized/
|
Based on Solarized
|
||||||
SOLARIZED HEX 16/8 TERMCOL XTERM/HEX L*A*B RGB HSB
|
SOLARIZED HEX 16/8 TERMCOL XTERM/HEX L*A*B RGB HSB
|
||||||
base03 #002b36 8/4 brblack 234 #1c1c1c 15 -12 -12 0 43 54 193 100 21
|
base03 #002b36 8/4 brblack 234 #1c1c1c 15 -12 -12 0 43 54 193 100 21
|
||||||
base02 #073642 0/4 black 235 #262626 20 -12 -12 7 54 66 192 90 26
|
base02 #073642 0/4 black 235 #262626 20 -12 -12 7 54 66 192 90 26
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name" translatable="false">Comet</string>
|
<string name="app_name">Comet</string>
|
||||||
<string name="todo" translatable="false">TODO</string>
|
|
||||||
|
|
||||||
<string name="error_alert_title">Error</string>
|
<string name="error_alert_title">Error</string>
|
||||||
<string name="url">URL</string>
|
<string name="url">URL</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
|
@ -13,6 +11,7 @@
|
||||||
<string name="pref_home_set">Set last visited page as home page</string>
|
<string name="pref_home_set">Set last visited page as home page</string>
|
||||||
<string name="no_current_url">No current URL.</string>
|
<string name="no_current_url">No current URL.</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- Preference Protocol -->
|
<!-- Preference Protocol -->
|
||||||
<string name="pref_protocol_header">Protocol</string>
|
<string name="pref_protocol_header">Protocol</string>
|
||||||
<string name="pref_connection_timeout_title">Connection timeout (seconds)</string>
|
<string name="pref_connection_timeout_title">Connection timeout (seconds)</string>
|
||||||
|
@ -40,9 +39,6 @@
|
||||||
<string name="generating_keypair">Generating client certificate…</string>
|
<string name="generating_keypair">Generating client certificate…</string>
|
||||||
<string name="identity_alias_title">Keystore reference</string>
|
<string name="identity_alias_title">Keystore reference</string>
|
||||||
<string name="edit_identity">Edit identity</string>
|
<string name="edit_identity">Edit identity</string>
|
||||||
<string name="delete">Delete</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="edit">Edit</string>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -9,9 +9,4 @@
|
||||||
<item name="android:paddingBottom">4dp</item>
|
<item name="android:paddingBottom">4dp</item>
|
||||||
<item name="android:textIsSelectable">true</item>
|
<item name="android:textIsSelectable">true</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="CometLink" parent="CometText">
|
|
||||||
<item name="android:textColor">@color/link</item>
|
|
||||||
<item name="android:textIsSelectable">false</item>
|
|
||||||
<item name="android:background">?attr/selectableItemBackground</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
Reference in a new issue