Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 458b942a authored by Charlotte Lu's avatar Charlotte Lu
Browse files

Add CertificateDetailsPageProvider.

Test: Unit Test
Fix: 326191189
Change-Id: I542903b26bac589ba67c297d8758ea0a69ebdf23
parent c8d5701a
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -112,6 +112,10 @@
        android:title="@string/wifi_auto_connect_title"
        android:summary="@string/wifi_auto_connect_summary"/>

    <com.android.settings.spa.preference.ComposePreference
        android:key="certificate_details"
        settings:controller="com.android.settings.wifi.details2.CertificateDetailsPreferenceController"/>

    <!-- Add device Preference -->
    <Preference
        android:key="add_device_to_network"
+1 −1
Original line number Diff line number Diff line
@@ -22,11 +22,11 @@ import com.android.settings.network.apn.ApnEditPageProvider
import com.android.settings.spa.about.AboutPhonePageProvider
import com.android.settings.spa.app.AllAppListPageProvider
import com.android.settings.spa.app.AppsMainPageProvider
import com.android.settings.spa.app.battery.BatteryOptimizationModeAppListPageProvider
import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider
import com.android.settings.spa.app.backgroundinstall.BackgroundInstalledAppsPageProvider
import com.android.settings.spa.app.battery.BatteryOptimizationModeAppListPageProvider
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
import com.android.settings.spa.app.specialaccess.BackupTasksAppsListProvider
+6 −1
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import com.android.settings.wifi.WifiConfigUiBase2;
import com.android.settings.wifi.WifiDialog2;
import com.android.settings.wifi.WifiUtils;
import com.android.settings.wifi.details2.AddDevicePreferenceController2;
import com.android.settings.wifi.details2.CertificateDetailsPreferenceController;
import com.android.settings.wifi.details2.WifiAutoConnectPreferenceController2;
import com.android.settings.wifi.details2.WifiDetailPreferenceController2;
import com.android.settings.wifi.details2.WifiMeteredPreferenceController2;
@@ -122,8 +123,12 @@ public class WifiNetworkDetailsFragment extends RestrictedDashboardFragment impl
    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        String wifiEntryKey = getArguments().getString(KEY_CHOSEN_WIFIENTRY_KEY);
        setupNetworksDetailTracker();
        use(WifiPrivacyPreferenceController.class)
                .setWifiEntryKey(getArguments().getString(KEY_CHOSEN_WIFIENTRY_KEY));
                .setWifiEntryKey(wifiEntryKey);
        use(CertificateDetailsPreferenceController.class)
                .setWifiEntry(mNetworkDetailsTracker.getWifiEntry());
    }

    @Override
+131 −0
Original line number Diff line number Diff line
/*
 * 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 android.content.DialogInterface
import android.net.http.SslCertificate
import android.security.KeyChain
import android.security.keystore.KeyProperties
import android.security.keystore2.AndroidKeyStoreLoadStoreParameter
import android.util.Log
import android.view.View
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import android.widget.Spinner
import androidx.appcompat.app.AlertDialog
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.android.settings.R
import com.android.settings.spa.preference.ComposePreferenceController
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.wifi.flags.Flags
import com.android.wifitrackerlib.WifiEntry
import java.security.KeyStore
import java.security.cert.X509Certificate

class CertificateDetailsPreferenceController(context: Context, preferenceKey: String) :
    ComposePreferenceController(context, preferenceKey) {

    private lateinit var wifiEntry: WifiEntry
    lateinit var certificateAliases: String
    lateinit var certX509: X509Certificate

    fun setWifiEntry(entry: WifiEntry) {
        wifiEntry = entry
    }

    override fun getAvailabilityStatus(): Int {
        return if (Flags.androidVWifiApi() && getCertX509(wifiEntry)) AVAILABLE
        else CONDITIONALLY_UNAVAILABLE
    }

    @Composable
    override fun Content() {
        CertificateDetails()
    }

    @Composable
    fun CertificateDetails() {
        val context = LocalContext.current
        Preference(object : PreferenceModel {
            override val title = stringResource(com.android.internal.R.string.ssl_certificate)
            override val summary = { certificateAliases }
            override val onClick: () -> 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<String>()
        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
+73 −0
Original line number Diff line number Diff line
/*
 * 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<X509Certificate> {}

    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