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 androidx.room.*
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
|
||||
object Identities {
|
||||
@Entity
|
||||
|
@ -43,6 +44,9 @@ object Identities {
|
|||
|
||||
@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 =
|
||||
|
@ -57,6 +61,14 @@ object Identities {
|
|||
suspend fun update(vararg identities: Identity) =
|
||||
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) {
|
||||
val algo = KeyProperties.KEY_ALGORITHM_RSA
|
||||
val kpg = KeyPairGenerator.getInstance(algo, "AndroidKeyStore")
|
||||
|
@ -69,5 +81,16 @@ object Identities {
|
|||
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"
|
||||
}
|
|
@ -2,6 +2,7 @@ package dev.lowrespalmtree.comet
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dev.lowrespalmtree.comet.Identities.Identity
|
||||
|
@ -14,6 +15,7 @@ class IdentitiesAdapter(private val listener: Listener) :
|
|||
|
||||
interface Listener {
|
||||
fun onIdentityClick(identity: Identity)
|
||||
fun onIdentityLongClick(identity: Identity, view: View)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||
|
@ -29,7 +31,13 @@ class IdentitiesAdapter(private val listener: Listener) :
|
|||
val item = identities[position]
|
||||
holder.binding.labelText.text = item.name.orEmpty()
|
||||
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
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupMenu
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
@ -13,6 +14,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dev.lowrespalmtree.comet.Identities.Identity
|
||||
import dev.lowrespalmtree.comet.databinding.FragmentIdentitiesBinding
|
||||
import dev.lowrespalmtree.comet.utils.confirm
|
||||
import dev.lowrespalmtree.comet.utils.toast
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -50,6 +52,32 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
|||
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) {
|
||||
vm.saveIdentity(identity)
|
||||
}
|
||||
|
@ -57,7 +85,10 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
|||
private fun openNewIdentityEditor() {
|
||||
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()
|
||||
}
|
||||
vm.createNewIdentity()
|
||||
|
@ -74,16 +105,25 @@ class IdentitiesFragment : Fragment(), IdentitiesAdapter.Listener, IdentityDialo
|
|||
val newIdentityId = Identities.insert(alias)
|
||||
newIdentity.postValue(Identities.get(newIdentityId))
|
||||
}
|
||||
.invokeOnCompletion { refreshIdentities() }
|
||||
}
|
||||
|
||||
fun refreshIdentities() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
identities.postValue(Identities.getAll())
|
||||
}
|
||||
viewModelScope.launch(Dispatchers.IO) { identities.postValue(Identities.getAll()) }
|
||||
}
|
||||
|
||||
fun saveIdentity(identity: 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() {
|
||||
binding = DialogIdentityBinding.inflate(LayoutInflater.from(context))
|
||||
binding.labelInput.setText(identity.name)
|
||||
binding.labelInput.setText(identity.name.orEmpty())
|
||||
binding.aliasText.text = identity.key
|
||||
AlertDialog.Builder(context)
|
||||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,4 +8,4 @@ fun isConnectedToNetwork(context: Context): Boolean {
|
|||
val connManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val caps = connManager.getNetworkCapabilities(connManager.activeNetwork) ?: return false
|
||||
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
package dev.lowrespalmtree.comet.utils
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.StringRes
|
||||
import dev.lowrespalmtree.comet.R
|
||||
import dev.lowrespalmtree.comet.databinding.DialogConfirmBinding
|
||||
|
||||
fun toast(context: Context, stringId: Int, length: Int = Toast.LENGTH_SHORT) =
|
||||
Toast.makeText(context, stringId, length).show()
|
||||
|
@ -15,4 +19,15 @@ fun getDrawableFromAttr(context: Context, @AttrRes attr: Int) =
|
|||
.resourceId
|
||||
|
||||
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
|
||||
android:id="@+id/identitiesFragment"
|
||||
android:name="dev.lowrespalmtree.comet.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" />
|
||||
android:label="IdentitiesFragment" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/settingsFragment"
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<color name="url_bar">#002b36</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
|
||||
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
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<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="url">URL</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="no_current_url">No current URL.</string>
|
||||
|
||||
|
||||
<!-- Preference Protocol -->
|
||||
<string name="pref_protocol_header">Protocol</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="identity_alias_title">Keystore reference</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>
|
Reference in a new issue