Identities: add deletion and fix sync issues
- Deletion works and clear the corresponding keystore entries. - No more sync issues with the listing when creating/updating/deleting.
This commit is contained in:
parent
f9c341c35a
commit
11bea0e585
|
@ -5,6 +5,7 @@ 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
|
||||||
|
@ -43,6 +44,9 @@ 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 =
|
||||||
|
@ -57,6 +61,14 @@ 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")
|
||||||
|
@ -69,5 +81,16 @@ 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,6 +2,7 @@ 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
|
||||||
|
@ -14,6 +15,7 @@ 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 =
|
||||||
|
@ -29,7 +31,13 @@ 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 { listener.onIdentityClick(item) }
|
holder.binding.container.setOnClickListener {
|
||||||
|
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,6 +4,7 @@ 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
|
||||||
|
@ -13,6 +14,7 @@ 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
|
||||||
|
@ -50,6 +52,32 @@ 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)
|
||||||
}
|
}
|
||||||
|
@ -57,7 +85,10 @@ 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()
|
||||||
|
@ -74,16 +105,25 @@ 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) {
|
viewModelScope.launch(Dispatchers.IO) { identities.postValue(Identities.getAll()) }
|
||||||
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)
|
binding.labelInput.setText(identity.name.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)
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
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,10 +1,14 @@
|
||||||
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()
|
||||||
|
@ -16,3 +20,14 @@ 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()
|
||||||
|
}
|
13
app/src/main/res/layout/dialog_confirm.xml
Normal file
13
app/src/main/res/layout/dialog_confirm.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?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>
|
5
app/src/main/res/menu/identity.xml
Normal file
5
app/src/main/res/menu/identity.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?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,16 +31,7 @@
|
||||||
<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
|
Based on Solarized - https://ethanschoonover.com/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,5 +1,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Comet</string>
|
<string name="app_name" translatable="false">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>
|
||||||
|
@ -11,7 +13,6 @@
|
||||||
<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>
|
||||||
|
@ -39,6 +40,9 @@
|
||||||
<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>
|
Reference in a new issue