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