diff --git a/res/xml/wifi_network_details_fragment2.xml b/res/xml/wifi_network_details_fragment2.xml
index 598f9d86173..eacff88bdd9 100644
--- a/res/xml/wifi_network_details_fragment2.xml
+++ b/res/xml/wifi_network_details_fragment2.xml
@@ -112,6 +112,10 @@
android:title="@string/wifi_auto_connect_title"
android:summary="@string/wifi_auto_connect_summary"/>
+
+
Unit = { createCertificateDetailsDialog(context, certX509) }
+ })
+ }
+
+ private fun getCertX509(wifiEntry: WifiEntry): Boolean {
+ if (certX509 != null ) return true
+ certificateAliases =
+ wifiEntry.wifiConfiguration?.enterpriseConfig?.caCertificateAliases?.get(0)
+ ?: return false
+ return try {
+ val keyStore = KeyStore.getInstance("AndroidKeyStore")
+ keyStore.load(AndroidKeyStoreLoadStoreParameter(KeyProperties.NAMESPACE_WIFI))
+ val cert = keyStore.getCertificate(certificateAliases)
+ certX509 = KeyChain.toCertificate(cert.encoded)
+ true
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to open Android Keystore.", e)
+ false
+ }
+ }
+
+ private fun createCertificateDetailsDialog(context: Context, certX509: X509Certificate) {
+ val listener =
+ DialogInterface.OnClickListener { dialog, id ->
+ dialog.dismiss()
+ }
+ val titles = ArrayList()
+ val sslCert = SslCertificate(certX509)
+ titles.add(sslCert.issuedTo.cName)
+ val arrayAdapter = ArrayAdapter(
+ context,
+ android.R.layout.simple_spinner_item,
+ titles
+ )
+ arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+ val spinner = Spinner(context)
+ spinner.setAdapter(arrayAdapter)
+
+ val certLayout = LinearLayout(context)
+ certLayout.orientation = LinearLayout.VERTICAL
+ // Prevent content overlapping with spinner
+ certLayout.setClipChildren(true)
+ certLayout.addView(spinner)
+
+ val view = sslCert.inflateCertificateView(context)
+ view.visibility = View.VISIBLE
+ certLayout.addView(view)
+ certLayout.visibility = View.VISIBLE
+
+ val dialog = AlertDialog.Builder(context)
+ .setView(certLayout)
+ .setTitle(com.android.internal.R.string.ssl_certificate)
+ .setPositiveButton(R.string.wifi_settings_ssid_block_button_close, null)
+ .setNegativeButton(R.string.trusted_credentials_remove_label, listener).create()
+ dialog.show()
+ dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false)
+ }
+
+ companion object {
+ const val TAG = "CertificateDetailsPreferenceController"
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/wifi/details2/CertificateDetailsPreferenceControllerTest.kt b/tests/spa_unit/src/com/android/settings/wifi/details2/CertificateDetailsPreferenceControllerTest.kt
new file mode 100644
index 00000000000..149937496ff
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/wifi/details2/CertificateDetailsPreferenceControllerTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.wifi.details2
+
+import android.content.Context
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import java.security.cert.X509Certificate
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class CertificateDetailsPreferenceControllerTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val mockCertX509 = mock {}
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+ doNothing().whenever(mock).startActivity(any())
+ }
+
+ private val controller = CertificateDetailsPreferenceController(context, TEST_KEY)
+
+ @Before
+ fun setUp() {
+ controller.certificateAliases = MOCK_CA
+ controller.certX509 = mockCertX509
+ }
+
+ @Test
+ fun title_isDisplayed() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ controller.Content()
+ }
+ }
+
+ composeTestRule.onNodeWithText(context.getString(com.android.internal.R.string.ssl_certificate))
+ .assertIsDisplayed()
+ }
+
+ private companion object {
+ const val TEST_KEY = "test_key"
+ const val MOCK_CA = "mock_ca"
+ }
+}
\ No newline at end of file