Looks great now

This commit is contained in:
Philipp Heckel 2026-01-04 23:54:13 -05:00
parent eb3b01da48
commit adeb8ebfbd
6 changed files with 91 additions and 41 deletions

View file

@ -169,7 +169,7 @@ class CertificateSettingsFragment : BasePreferenceFragment(),
it.readBytes()
}
if (data != null && data.isNotEmpty()) {
ClientCertificateFragment.newInstance(data)
ClientCertificateFragment.newInstanceAdd(data)
.show(childFragmentManager, ClientCertificateFragment.TAG)
} else {
Toast.makeText(context, R.string.settings_advanced_certificates_error_invalid_p12, Toast.LENGTH_SHORT).show()

View file

@ -14,7 +14,6 @@ import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.R
@ -64,6 +63,7 @@ class ClientCertificateFragment : DialogFragment() {
// Page 2 views
private lateinit var page2Layout: LinearLayout
private lateinit var descriptionPage2Text: TextView
private lateinit var baseUrlValueText: TextView
private lateinit var subjectText: TextView
private lateinit var issuerText: TextView
@ -143,7 +143,7 @@ class ClientCertificateFragment : DialogFragment() {
true
}
R.id.client_certificate_action_delete -> {
confirmDeleteCertificate()
deleteCertificate()
true
}
else -> false
@ -163,6 +163,7 @@ class ClientCertificateFragment : DialogFragment() {
// Page 2 views
page2Layout = view.findViewById(R.id.client_certificate_page2)
descriptionPage2Text = view.findViewById(R.id.client_certificate_description_page2)
baseUrlValueText = view.findViewById(R.id.client_certificate_base_url_value)
subjectText = view.findViewById(R.id.client_certificate_subject)
issuerText = view.findViewById(R.id.client_certificate_issuer)
@ -197,7 +198,7 @@ class ClientCertificateFragment : DialogFragment() {
deleteMenuItem.isVisible = true
// Hide description for view mode
view?.findViewById<TextView>(R.id.client_certificate_description_page2)?.isVisible = false
descriptionPage2Text.isVisible = false
// Load certificate from repository
lifecycleScope.launch(Dispatchers.IO) {
@ -301,22 +302,16 @@ class ClientCertificateFragment : DialogFragment() {
}
}
private fun confirmDeleteCertificate() {
private fun deleteCertificate() {
val url = baseUrl ?: return
MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.client_certificate_dialog_delete_confirm)
.setPositiveButton(R.string.client_certificate_dialog_button_delete) { _, _ ->
lifecycleScope.launch(Dispatchers.IO) {
repository.removeClientCertificate(url)
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.client_certificate_dialog_deleted_toast, Toast.LENGTH_SHORT).show()
listener?.onCertificateDeleted()
dismiss()
}
}
lifecycleScope.launch(Dispatchers.IO) {
repository.removeClientCertificate(url)
withContext(Dispatchers.Main) {
Toast.makeText(context, R.string.client_certificate_dialog_deleted_toast, Toast.LENGTH_SHORT).show()
listener?.onCertificateDeleted()
dismiss()
}
.setNegativeButton(R.string.client_certificate_dialog_button_cancel, null)
.show()
}
}
private fun handleBack() {
@ -357,7 +352,7 @@ class ClientCertificateFragment : DialogFragment() {
/**
* Create fragment for ADD mode - two-page flow to add a client certificate
*/
fun newInstance(pkcs12Data: ByteArray): ClientCertificateFragment {
fun newInstanceAdd(pkcs12Data: ByteArray): ClientCertificateFragment {
return ClientCertificateFragment().apply {
arguments = Bundle().apply {
putString(ARG_MODE, Mode.ADD.name)

View file

@ -49,6 +49,8 @@ class TrustedCertificateFragment : DialogFragment() {
private lateinit var fingerprintText: TextView
private lateinit var validFromText: TextView
private lateinit var validUntilText: TextView
private lateinit var caText: TextView
private lateinit var caInfoText: TextView
interface TrustedCertificateListener {
fun onCertificateTrusted(certificate: X509Certificate)
@ -145,6 +147,8 @@ class TrustedCertificateFragment : DialogFragment() {
fingerprintText = view.findViewById(R.id.trusted_certificate_fingerprint)
validFromText = view.findViewById(R.id.trusted_certificate_valid_from)
validUntilText = view.findViewById(R.id.trusted_certificate_valid_until)
caText = view.findViewById(R.id.trusted_certificate_ca)
caInfoText = view.findViewById(R.id.trusted_certificate_ca_info)
when (mode) {
Mode.UNKNOWN -> setupUnknownMode()
@ -211,6 +215,11 @@ class TrustedCertificateFragment : DialogFragment() {
validFromText.text = dateFormat.format(certificate.notBefore)
validUntilText.text = dateFormat.format(certificate.notAfter)
// Determine if this is a CA certificate (self-signed)
val isCa = certificate.subjectX500Principal == certificate.issuerX500Principal
caText.text = if (isCa) getString(R.string.common_yes) else getString(R.string.common_no)
caInfoText.isVisible = isCa
// Show warning if certificate is expired or not yet valid
val now = Date()
when {

View file

@ -63,10 +63,26 @@
android:paddingTop="16dp"
android:paddingBottom="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/client_certificate_base_url_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/client_certificate_dialog_base_url_hint"
app:placeholderText="@string/client_certificate_dialog_base_url_placeholder">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/client_certificate_base_url_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/client_certificate_password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/client_certificate_dialog_password_hint"
app:passwordToggleEnabled="true">
@ -78,21 +94,6 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/client_certificate_base_url_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/client_certificate_dialog_base_url_hint">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/client_certificate_base_url_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/client_certificate_error_text"
android:layout_width="match_parent"

View file

@ -208,6 +208,48 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintWidth_percent="0.5" />
<!-- Certificate Authority -->
<TextView
android:id="@+id/trusted_certificate_ca_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trusted_certificate_dialog_ca"
android:textSize="12sp"
android:textColor="?android:textColorSecondary"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_valid_from"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/trusted_certificate_ca"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_ca_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- CA Info (only shown for CA certificates) -->
<TextView
android:id="@+id/trusted_certificate_ca_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/trusted_certificate_dialog_ca_info"
android:textSize="14sp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_ca"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -2,6 +2,8 @@
tools:ignore="MissingTranslation">
<!-- Common strings -->
<string name="common_yes">Yes</string>
<string name="common_no">No</string>
<string name="common_copied_to_clipboard">Copied to clipboard</string>
<!-- Notification channels -->
@ -424,8 +426,8 @@
<string name="settings_advanced_exact_alarms_true">ntfy can schedule exact alarms. Exact alarms are required to reconnect WebSockets in the background. Click to revoke the permission.</string>
<string name="settings_advanced_exact_alarms_false">ntfy cannot schedule exact alarms. Exact alarms are required to reconnect WebSockets in the background. Click to grant the permission.</string>
<string name="settings_advanced_certificates_title">Manage certificates</string>
<string name="settings_advanced_certificates_summary">Trust self-signed server certificates and manage client certificates for mTLS</string>
<string name="settings_advanced_certificates_trusted_header">Trusted server certificates</string>
<string name="settings_advanced_certificates_summary">Add certificates to the trust store and manage client certificates for mTLS</string>
<string name="settings_advanced_certificates_trusted_header">Trusted certificates</string>
<string name="settings_advanced_certificates_trusted_item_summary_leaf">Leaf certificate, issued by %1$s\nExpires %2$s</string>
<string name="settings_advanced_certificates_trusted_item_summary_leaf_expired">Leaf certificate, issued by %1$s\nExpired</string>
<string name="settings_advanced_certificates_trusted_item_summary_ca">CA certificate, self-signed\nExpires %1$s</string>
@ -434,7 +436,7 @@
<string name="settings_advanced_certificates_trusted_add_summary">Import a certificate into the trust store (PEM). HTTPS and WebSocket connections will trust this certificate.</string>
<string name="settings_advanced_certificates_client_header">Client certificates (mTLS)</string>
<string name="settings_advanced_certificates_client_item_summary">Client certificate, issued by %1$s\nExpires %2$s, used by %3$s</string>
<string name="settings_advanced_certificates_client_item_summary_expired">EClient certificate, issued by %1$s\nExpired, used by %2$s</string>
<string name="settings_advanced_certificates_client_item_summary_expired">Client certificate, issued by %1$s\nExpired, used by %2$s</string>
<string name="settings_advanced_certificates_client_add_title">Add a client certificate</string>
<string name="settings_advanced_certificates_client_add_summary">Import certificate for mutual TLS authentication (PKCS#12). This certificate will be used when connecting to the server.</string>
<string name="settings_advanced_certificates_error_invalid_cert">Invalid certificate file</string>
@ -511,6 +513,8 @@
<string name="trusted_certificate_dialog_fingerprint">SHA-256 fingerprint</string>
<string name="trusted_certificate_dialog_valid_from">Valid from</string>
<string name="trusted_certificate_dialog_valid_until">Valid until</string>
<string name="trusted_certificate_dialog_ca">Certificate Authority (CA)</string>
<string name="trusted_certificate_dialog_ca_info">All certificates signed by this Certificate Authority will be trusted.</string>
<string name="trusted_certificate_dialog_expired_warning">Warning: This certificate has expired.</string>
<string name="trusted_certificate_dialog_not_yet_valid_warning">Warning: This certificate is not yet valid.</string>
<string name="trusted_certificate_dialog_button_trust">Trust</string>
@ -521,10 +525,11 @@
<!-- Client certificate dialog (ClientCertificateFragment) -->
<string name="client_certificate_dialog_title">Client certificate</string>
<string name="client_certificate_dialog_title_add">Add client certificate</string>
<string name="client_certificate_dialog_description_page1">Enter the password for the PKCS#12 file and the server URL this certificate should be used for.</string>
<string name="client_certificate_dialog_description_page1">Enter the service URL this certificate should be used for, and the password for the PKCS#12 file.</string>
<string name="client_certificate_dialog_description_page2">Review the certificate details and save to add this client certificate.</string>
<string name="client_certificate_dialog_password_hint">Password</string>
<string name="client_certificate_dialog_base_url_hint">Server URL (e.g. https://example.com)</string>
<string name="client_certificate_dialog_base_url_hint">Service URL</string>
<string name="client_certificate_dialog_base_url_placeholder">e.g. https://ntfy.example.com</string>
<string name="client_certificate_dialog_subject">Subject</string>
<string name="client_certificate_dialog_issuer">Issuer</string>
<string name="client_certificate_dialog_fingerprint">SHA-256 Fingerprint</string>
@ -533,8 +538,6 @@
<string name="client_certificate_dialog_button_next">Next</string>
<string name="client_certificate_dialog_button_save">Save</string>
<string name="client_certificate_dialog_button_delete">Delete</string>
<string name="client_certificate_dialog_button_cancel">Cancel</string>
<string name="client_certificate_dialog_delete_confirm">Delete this certificate?</string>
<string name="client_certificate_dialog_added_toast">Certificate added</string>
<string name="client_certificate_dialog_deleted_toast">Certificate deleted</string>
<string name="client_certificate_dialog_error_wrong_password">Wrong password or invalid PKCS#12 file</string>