Merge "Rework Add Wifi page for cleaner code" into pi-car-dev
diff --git a/Android.mk b/Android.mk
index b27b4d8..1fd2fc6 100644
--- a/Android.mk
+++ b/Android.mk
@@ -40,6 +40,7 @@
car-apps-common \
car-settings-lib \
car-setup-wizard-lib \
+ car-setup-wizard-lib-utils \
setup-wizard-lib-gingerbread-compat \
SettingsLib
@@ -99,6 +100,7 @@
car-apps-common \
car-settings-lib \
car-setup-wizard-lib \
+ car-setup-wizard-lib-utils \
setup-wizard-lib-gingerbread-compat \
SettingsLib
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index f4d2a8e..ee56b77 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -61,6 +61,9 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+ <permission android:name="com.android.car.settings.SET_INITIAL_LOCK"
+ android:protectionLevel="signature|setup"/>
+
<application
android:icon="@drawable/ic_launcher_settings"
android:theme="@style/CarSettingTheme"
@@ -307,6 +310,10 @@
<service android:name=".bluetooth.BluetoothPairingService" />
+ <service android:name=".setupservice.InitialLockSetupService"
+ android:exported="true"
+ android:permission="com.android.car.settings.SET_INITIAL_LOCK"/>
+
<receiver android:name=".bluetooth.BluetoothPairingRequest">
<intent-filter>
<action android:name="android.bluetooth.device.action.PAIRING_REQUEST" />
diff --git a/res/values/preference_keys.xml b/res/values/preference_keys.xml
index a97df25..7a34f12 100644
--- a/res/values/preference_keys.xml
+++ b/res/values/preference_keys.xml
@@ -64,8 +64,9 @@
<string name="pk_data_limit" translatable="false">data_limit</string>
<string name="pk_app_data_usage" translatable="false">app_data_usage</string>
<string name="pk_app_data_usage_detail" translatable="false">app_data_usage_detail</string>
- <string name="pk_wifi_tether_settings_entry" translatable="false">wifi_tether_settings_entry
- </string>
+ <string name="pk_wifi_tether_settings_entry" translatable="false">wifi_tether_settings_entry</string>
+ <string name="pk_wifi_tether_security" translatable="false">wifi_tether_security</string>
+ <string name="pk_wifi_tether_ap_band" translatable="false">wifi_tether_ap_band</string>
<string name="pk_wifi_tether_auto_off" translatable="false">wifi_tether_auto_off</string>
<string name="pk_wifi_tether_password" translatable="false">wifi_tether_password</string>
<string name="pk_wifi_tether_name" translatable="false">wifi_tether_name</string>
@@ -169,9 +170,24 @@
</string>
<!-- Special App Access Settings -->
+ <string name="pk_modify_system_settings_entry" translatable="false">
+ modify_system_settings_entry
+ </string>
+ <string name="pk_modify_system_settings" translatable="false">modify_system_settings</string>
+ <string name="pk_modify_system_settings_description" translatable="false">
+ modify_system_settings_description
+ </string>
<string name="pk_notification_access_entry" translatable="false">notification_access_entry
</string>
<string name="pk_notification_access" translatable="false">notification_access</string>
+ <string name="pk_usage_access_entry" translatable="false">usage_access_entry</string>
+ <string name="pk_usage_access" translatable="false">usage_access</string>
+ <string name="pk_usage_access_description" translatable="false">usage_access_description
+ </string>
+ <string name="pk_wifi_control_entry" translatable="false">wifi_control_entry</string>
+ <string name="pk_wifi_control" translatable="false">wifi_control</string>
+ <string name="pk_wifi_control_description" translatable="false">wifi_control_description
+ </string>
<!-- DateTime Settings -->
<string name="pk_auto_datetime_switch" translatable="false">auto_datetime_switch</string>
diff --git a/res/values/string_arrays.xml b/res/values/string_arrays.xml
index 8626d42..50028ad 100644
--- a/res/values/string_arrays.xml
+++ b/res/values/string_arrays.xml
@@ -21,4 +21,37 @@
<item>@string/wifi_security_wep</item>
<item>@string/wifi_security_wpa_wpa2</item>
</string-array>
-</resources>
+
+ <!-- Security types for wireless tether -->
+ <string-array translatable="false" name="wifi_tether_security">
+ <item>@string/wifi_security_wpa2</item>
+ <item>@string/wifi_security_none</item>
+ </string-array>
+
+ <!-- Wi-Fi AP band settings. Either Auto, 2.4GHz or 5GHz. -->
+ <!-- Note that adding/removing/moving the items will need wifi settings code change. -->
+ <string-array translatable="false" name="wifi_ap_band_config_full">
+ <item>0</item>
+ <item>1</item>
+ </string-array>
+
+ <string-array translatable="false" name="wifi_ap_band_summary_full">
+ <item>@string/wifi_ap_choose_2G</item>
+ <item>@string/wifi_ap_choose_5G</item>
+ </string-array>
+
+ <string-array translatable="false" name="wifi_ap_band_dual_mode">
+ <item>0</item>
+ <item>-1</item>
+ </string-array>
+
+ <string-array translatable="false" name="wifi_ap_band_dual_mode_summary">
+ <item>@string/wifi_ap_choose_2G</item>
+ <item>@string/wifi_ap_prefer_5G</item>
+ </string-array>
+
+ <string-array translatable="false" name="wifi_ap_band_config_2G_only">
+ <item>@string/wifi_ap_choose_auto</item>
+ <item>@string/wifi_ap_choose_2G</item>
+ </string-array>
+</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2d349bc..c5e510d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -223,8 +223,30 @@
<string name="wifi_hotspot_name_summary_connected">Other devices can connect to <xliff:g id="wifi_hotspot_name">%1$s</xliff:g></string>
<!-- Label for Wifi hotspot password. [CHAR LIMIT=30]-->
<string name="wifi_hotspot_password_title">Hotspot password</string>
+ <!-- Label for Wifi security type title [CHAR LIMIT=15]-->
+ <string name="wifi_hotspot_security_title">Security</string>
+ <!-- Label for Wifi security type WPA2-Personal [CHAR LIMIT=25]-->
+ <string name="wifi_hotspot_wpa2_personal">WPA2-Personal</string>
+ <!-- Label for Wifi security type WPA2-Personal [CHAR LIMIT=25]-->
+ <string name="wifi_hotspot_security_none">None</string>
<!-- Label for Wifi hotspot AP Band. [CHAR LIMIT=15]-->
<string name="wifi_hotspot_ap_band_title">AP Band</string>
+ <!-- Label for the RadioGroup to choose wifi ap band [CHAR LIMIT=25]-->
+ <string name="wifi_ap_band_config">Select AP Band</string>
+ <!-- Label for the radio button to choose wifi ap channel automatically [CHAR LIMIT=10]-->
+ <string name="wifi_ap_choose_auto">Auto</string>
+ <!-- Label for the radio button to choose wifi ap 2.4 GHz band [CHAR LIMIT=25]-->
+ <string name="wifi_ap_choose_2G">2.4 GHz Band</string>
+ <!-- Label for the radio button to only choose wifi ap 5GHz band [CHAR LIMIT=25]-->
+ <string name="wifi_ap_choose_5G">5.0 GHz Band</string>
+ <!-- Label for the radio button to prefer 5GHz wifi ap band [CHAR LIMIT=80]-->
+ <string name="wifi_ap_prefer_5G">5.0 GHz Band preferred</string>
+ <!-- Label for adding to the list of selected bands when 2.4 GHz is selected [CHAR LIMIT=15]-->
+ <string name="wifi_ap_2G">2.4 GHz</string>
+ <!-- Label for adding to the list of selected bands when 5.0 GHz is selected [CHAR LIMIT=15]-->
+ <string name="wifi_ap_5G">5.0 GHz</string>
+ <!-- Label that is shown as a dialog summary informing users they must pick at LEAST one band for their hotspot [CHAR LIMIT=25]-->
+ <string name="wifi_ap_band_select_one">Choose at least one band for Wi\u2011Fi hotspot:</string>
<!-- Tethering controls, item title to go into the tethering settings when USB, Bluetooth and Wifi tethering are available [CHAR LIMIT=60]-->
<string name="tether_settings_title_all">Hotspot & tethering</string>
<!-- Title for the toggle to turn off hotspot automatically [CHAR LIMIT=50]-->
@@ -368,6 +390,8 @@
<string name="app_disable_dialog_text">If you disable this app, Android and other apps may no longer function as intended.</string>
<!-- Manage applications, label for option to disable app. [CHAR LIMIT=30] -->
<string name="app_disable_dialog_positive">Disable app</string>
+ <!-- Manage applications, text informing that an application is not installed for the current user. [CHAR_LIMIT=NONE] -->
+ <string name="not_installed">Not installed for this user</string>
<!-- Manage applications, individual application info screen, heading for settings related to the app's permissions. for example, it may list all the permissions the app has. -->
<string name="permissions_label">Permissions</string>
<!-- Label for the toggle that enables/disables an app's notifications. [CHAR LIMIT=20] -->
@@ -455,6 +479,14 @@
<!-- Label for screen where user can grant applications special access to various systems. [CHAR_LIMIT=60] -->
<string name="special_access">Special app access</string>
+ <!-- Text for toggle button to control whether system processes are shown in app lists. [CHAR_LIMIT=30] -->
+ <string name="show_system">Show system</string>
+ <!-- Text for toggle button to control whether system processes are hidden in app lists. [CHAR_LIMIT=30] -->
+ <string name="hide_system">Hide system</string>
+ <!-- Title for managing apps which can modify system settings. [CHAR_LIMIT=30] -->
+ <string name="modify_system_settings_title">Modify system settings</string>
+ <!-- Description of modifying system settings. [CHAR_LIMIT=NONE] -->
+ <string name="modify_system_settings_description">This permission allows an app to modify system settings.</string>
<!-- Title for managing components which can listen to notifications. [CHAR_LIMIT=30] -->
<string name="notification_access_title">Notification access</string>
<!-- Title for a warning message about security implications of enabling a notification listener, displayed as a dialog message. [CHAR_LIMIT=NONE] -->
@@ -467,6 +499,14 @@
<string name="notification_listener_revoke_warning_confirm">Turn off</string>
<!-- Negative button for a dialog warning message about revoking notification listener access. [CHAR_LIMIT=30] -->
<string name="notification_listener_revoke_warning_cancel">Cancel</string>
+ <!-- Title for managing apps which can query usage data. [CHAR_LIMIT=30] -->
+ <string name="usage_access_title">Usage access</string>
+ <!-- Description of the usage access permission. [CHAR_LIMIT=NONE] -->
+ <string name="usage_access_description">Usage access allows an app to track what other apps you\u2019re using and how often, as well as your carrier, language settings, and other details.</string>
+ <!-- Title for managing apps which can change Wi-Fi state. [CHAR_LIMIT=30] -->
+ <string name="wifi_control_title">Wi-Fi control</string>
+ <!-- Description of the change wifi state permission. [CHAR_LIMIT=NONE] -->
+ <string name="wifi_control_description">Wi-Fi control allow an app to turn Wi-Fi on or off, scan and connect to Wi-Fi networks, add or remove networks, or start a local-only hotspot.</string>
<!-- Location --><skip/>
<!-- Main setting menu item to go into location settings [CHAR LIMIT=40] -->
diff --git a/res/xml/homepage_fragment.xml b/res/xml/homepage_fragment.xml
index 3eebf50..10e3e2d 100644
--- a/res/xml/homepage_fragment.xml
+++ b/res/xml/homepage_fragment.xml
@@ -19,9 +19,11 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/settings_label">
+ <!-- TODO: Re-enable once more suggestion use cases are supported.
<com.android.car.settings.common.LogicalPreferenceGroup
android:key="@string/pk_suggestions"
settings:controller="com.android.car.settings.suggestions.SuggestionsPreferenceController"/>
+ -->
<Preference
android:fragment="com.android.car.settings.display.DisplaySettingsFragment"
android:icon="@drawable/ic_settings_display"
diff --git a/res/xml/modify_system_settings_fragment.xml b/res/xml/modify_system_settings_fragment.xml
new file mode 100644
index 0000000..7aa22b4
--- /dev/null
+++ b/res/xml/modify_system_settings_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+-->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/modify_system_settings_title">
+ <Preference
+ android:key="@string/pk_modify_system_settings_description"
+ android:selectable="false"
+ android:summary="@string/modify_system_settings_description"/>
+ <com.android.car.settings.common.LogicalPreferenceGroup
+ android:key="@string/pk_modify_system_settings"
+ settings:controller="com.android.car.settings.applications.specialaccess.AppOpsPreferenceController"/>
+</PreferenceScreen>
diff --git a/res/xml/special_access_fragment.xml b/res/xml/special_access_fragment.xml
index 2cde396..33c1706 100644
--- a/res/xml/special_access_fragment.xml
+++ b/res/xml/special_access_fragment.xml
@@ -20,8 +20,23 @@
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:title="@string/special_access">
<Preference
+ android:fragment="com.android.car.settings.applications.specialaccess.ModifySystemSettingsFragment"
+ android:key="@string/pk_modify_system_settings_entry"
+ android:title="@string/modify_system_settings_title"
+ settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
+ <Preference
android:fragment="com.android.car.settings.applications.specialaccess.NotificationAccessFragment"
android:key="@string/pk_notification_access_entry"
android:title="@string/notification_access_title"
settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
+ <Preference
+ android:fragment="com.android.car.settings.applications.specialaccess.UsageAccessFragment"
+ android:key="@string/pk_usage_access_entry"
+ android:title="@string/usage_access_title"
+ settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
+ <Preference
+ android:fragment="com.android.car.settings.applications.specialaccess.WifiControlFragment"
+ android:key="@string/pk_wifi_control_entry"
+ android:title="@string/wifi_control_title"
+ settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
</PreferenceScreen>
diff --git a/res/xml/usage_access_fragment.xml b/res/xml/usage_access_fragment.xml
new file mode 100644
index 0000000..a1ac3e2
--- /dev/null
+++ b/res/xml/usage_access_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+-->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/usage_access_title">
+ <Preference
+ android:key="@string/pk_usage_access_description"
+ android:selectable="false"
+ android:summary="@string/usage_access_description"/>
+ <com.android.car.settings.common.LogicalPreferenceGroup
+ android:key="@string/pk_usage_access"
+ settings:controller="com.android.car.settings.applications.specialaccess.AppOpsPreferenceController"/>
+</PreferenceScreen>
diff --git a/res/xml/wifi_control_fragment.xml b/res/xml/wifi_control_fragment.xml
new file mode 100644
index 0000000..58d4b0e
--- /dev/null
+++ b/res/xml/wifi_control_fragment.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+-->
+
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:title="@string/wifi_control_title">
+ <Preference
+ android:key="@string/pk_wifi_control_description"
+ android:selectable="false"
+ android:summary="@string/wifi_control_description"/>
+ <com.android.car.settings.common.LogicalPreferenceGroup
+ android:key="@string/pk_wifi_control"
+ settings:controller="com.android.car.settings.applications.specialaccess.WifiControlPreferenceController"/>
+</PreferenceScreen>
diff --git a/res/xml/wifi_tether_fragment.xml b/res/xml/wifi_tether_fragment.xml
index df9bb83..a0d1bcf 100644
--- a/res/xml/wifi_tether_fragment.xml
+++ b/res/xml/wifi_tether_fragment.xml
@@ -21,13 +21,27 @@
android:key="@string/pk_wifi_tether_name"
android:title="@string/wifi_hotspot_name_title"
settings:controller="com.android.car.settings.wifi.WifiTetherNamePreferenceController"/>
+ <ListPreference
+ android:key="@string/pk_wifi_tether_security"
+ android:title="@string/wifi_hotspot_security_title"
+ android:dialogTitle="@string/wifi_hotspot_security_title"
+ android:persistent="false"
+ settings:controller="com.android.car.settings.wifi.WifiTetherSecurityPreferenceController"/>
<com.android.car.settings.common.ValidatedEditTextPreference
android:key="@string/pk_wifi_tether_password"
android:title="@string/wifi_hotspot_password_title"
+ android:persistent="false"
settings:controller="com.android.car.settings.wifi.WifiTetherPasswordPreferenceController"/>
<SwitchPreference
android:key="@string/pk_wifi_tether_auto_off"
android:title="@string/wifi_hotspot_auto_off_title"
android:summary="@string/wifi_hotspot_auto_off_summary"
+ android:persistent="false"
settings:controller="com.android.car.settings.wifi.WifiTetherAutoOffPreferenceController"/>
+ <ListPreference
+ android:key="@string/pk_wifi_tether_ap_band"
+ android:title="@string/wifi_hotspot_ap_band_title"
+ android:dialogTitle="@string/wifi_hotspot_ap_band_title"
+ android:persistent="false"
+ settings:controller="com.android.car.settings.wifi.WifiTetherApBandPreferenceController"/>
</PreferenceScreen>
diff --git a/src/com/android/car/settings/applications/specialaccess/AppOpsFragment.java b/src/com/android/car/settings/applications/specialaccess/AppOpsFragment.java
new file mode 100644
index 0000000..165dbb8
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/AppOpsFragment.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.os.Bundle;
+import android.widget.Button;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+
+/**
+ * Fragment which hosts an {@link AppOpsPreferenceController} to display a list of controls to
+ * allow/disallow app operations. There is a toggle in the app bar for showing/hiding system
+ * applications. The semantics of what constitues a system app is left up to the controller.
+ */
+public abstract class AppOpsFragment extends SettingsFragment {
+
+ private static final String KEY_SHOW_SYSTEM = "showSystem";
+
+ private boolean mShowSystem;
+
+ @Override
+ @XmlRes
+ protected int getActionBarLayoutId() {
+ return R.layout.action_bar_with_button;
+ }
+
+ /** Returns the {@link AppOpsPreferenceController} via {@link #use(Class, int)} lookup. */
+ protected abstract AppOpsPreferenceController lookupAppOpsPreferenceController();
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mShowSystem = savedInstanceState.getBoolean(KEY_SHOW_SYSTEM, false);
+ lookupAppOpsPreferenceController().setShowSystem(mShowSystem);
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Button toggleShowSystem = requireActivity().findViewById(R.id.action_button1);
+ setButtonText(toggleShowSystem);
+ toggleShowSystem.setOnClickListener(v -> {
+ mShowSystem = !mShowSystem;
+ lookupAppOpsPreferenceController().setShowSystem(mShowSystem);
+ setButtonText(toggleShowSystem);
+ });
+ }
+
+ private void setButtonText(Button button) {
+ // Show text to reverse the current state.
+ button.setText(mShowSystem ? R.string.hide_system : R.string.show_system);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_SHOW_SYSTEM, mShowSystem);
+ }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java b/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
new file mode 100644
index 0000000..a3fea69
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceController.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.app.AppOpsManager;
+import android.app.Application;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.CallSuper;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.SwitchPreference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+import com.android.settingslib.applications.ApplicationsState.CompoundFilter;
+
+import java.util.ArrayList;
+
+/**
+ * Displays a list of toggles for applications requesting permission to perform the operation with
+ * which this controller was initialized. {@link #init(int, String, int)} should be called when
+ * this controller is instantiated to specify the {@link AppOpsManager} operation code to control
+ * access for.
+ */
+public class AppOpsPreferenceController extends PreferenceController<PreferenceGroup> {
+
+ private static final AppFilter FILTER_HAS_INFO = new AppFilter() {
+ @Override
+ public void init() {
+ // No op.
+ }
+
+ @Override
+ public boolean filterApp(AppEntry info) {
+ return info.extraInfo != null;
+ }
+ };
+
+ private final AppOpsManager mAppOpsManager;
+ private final ApplicationsState mApplicationsState;
+
+ private final ApplicationsState.Callbacks mCallbacks =
+ new ApplicationsState.Callbacks() {
+ @Override
+ public void onRunningStateChanged(boolean running) {
+ // TODO: show loading UI.
+ }
+
+ @Override
+ public void onPackageListChanged() {
+ rebuild();
+ }
+
+ @Override
+ public void onRebuildComplete(ArrayList<AppEntry> entries) {
+ mEntries = entries;
+ refreshUi();
+ }
+
+ @Override
+ public void onPackageIconChanged() {
+ // No op.
+ }
+
+ @Override
+ public void onPackageSizeChanged(String packageName) {
+ // No op.
+ }
+
+ @Override
+ public void onAllSizesComputed() {
+ // No op.
+ }
+
+ @Override
+ public void onLauncherInfoChanged() {
+ rebuild();
+ }
+
+ @Override
+ public void onLoadEntriesCompleted() {
+ mHasReceivedLoadEntries = true;
+ rebuild();
+ }
+ };
+
+ private final AppStateBaseBridge.Callback mBridgeCallback = new AppStateBaseBridge.Callback() {
+ @Override
+ public void onExtraInfoUpdated() {
+ mHasReceivedBridgeCallback = true;
+ rebuild();
+ }
+ };
+
+ private final Preference.OnPreferenceChangeListener mOnPreferenceChangeListener =
+ new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ AppOpPreference appOpPreference = (AppOpPreference) preference;
+ AppEntry entry = appOpPreference.mEntry;
+ PermissionState extraInfo = (PermissionState) entry.extraInfo;
+ boolean allowOp = (Boolean) newValue;
+ if (allowOp != extraInfo.isPermissible()) {
+ mAppOpsManager.setMode(mAppOpsOpCode, entry.info.uid,
+ entry.info.packageName,
+ allowOp ? AppOpsManager.MODE_ALLOWED : mNegativeOpMode);
+ // Update the extra info of this entry so that it reflects the new mode.
+ mExtraInfoBridge.forceUpdate(entry);
+ return true;
+ }
+ return false;
+ }
+ };
+
+ private int mAppOpsOpCode = AppOpsManager.OP_NONE;
+ private String mPermission;
+ private int mNegativeOpMode = -1;
+
+ private ApplicationsState.Session mSession;
+ private AppStateAppOpsBridge mExtraInfoBridge;
+
+ private ArrayList<AppEntry> mEntries;
+
+ private boolean mHasReceivedLoadEntries;
+ private boolean mHasReceivedBridgeCallback;
+
+ private boolean mShowSystem;
+
+ public AppOpsPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ mApplicationsState = ApplicationsState.getInstance(
+ (Application) context.getApplicationContext());
+ }
+
+ @Override
+ protected Class<PreferenceGroup> getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ /**
+ * Initializes this controller with the {@code appOpsOpCode} (selected from the operations in
+ * {@link AppOpsManager}) to control access for.
+ *
+ * @param permission the {@link android.Manifest.permission} apps must hold to perform the
+ * operation.
+ * @param negativeOpMode the operation mode that will be passed to {@link
+ * AppOpsManager#setMode(int, int, String, int)} when access for a app is
+ * revoked.
+ */
+ public void init(int appOpsOpCode, String permission, int negativeOpMode) {
+ mAppOpsOpCode = appOpsOpCode;
+ mPermission = permission;
+ mNegativeOpMode = negativeOpMode;
+ }
+
+ /**
+ * Rebuilds the preference list to show system applications if {@code showSystem} is true.
+ * System applications will be hidden otherwise.
+ */
+ public void setShowSystem(boolean showSystem) {
+ if (mShowSystem != showSystem) {
+ mShowSystem = showSystem;
+ rebuild();
+ }
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mAppOpsOpCode == AppOpsManager.OP_NONE) {
+ throw new IllegalStateException("App operation code must be initialized");
+ }
+ if (mPermission == null) {
+ throw new IllegalStateException("Manifest permission must be initialized");
+ }
+ if (mNegativeOpMode == -1) {
+ throw new IllegalStateException("Negative case app operation mode must be initialized");
+ }
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ mSession = mApplicationsState.newSession(mCallbacks);
+ mExtraInfoBridge = new AppStateAppOpsBridge(getContext(), mApplicationsState, mAppOpsOpCode,
+ mPermission, mBridgeCallback);
+ }
+
+ @Override
+ protected void onStartInternal() {
+ mSession.onResume();
+ mExtraInfoBridge.start();
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mSession.onPause();
+ mExtraInfoBridge.stop();
+ }
+
+ @Override
+ protected void onDestroyInternal() {
+ mSession.onDestroy();
+ mExtraInfoBridge.destroy();
+ }
+
+ @Override
+ protected void updateState(PreferenceGroup preference) {
+ if (mEntries == null) {
+ // Still loading.
+ return;
+ }
+ preference.removeAll();
+ for (AppEntry entry : mEntries) {
+ mApplicationsState.ensureIcon(entry);
+ Preference appOpPreference = new AppOpPreference(getContext(), entry);
+ appOpPreference.setOnPreferenceChangeListener(mOnPreferenceChangeListener);
+ preference.addPreference(appOpPreference);
+ }
+ }
+
+ @CallSuper
+ protected AppFilter getAppFilter() {
+ AppFilter filterObj = new CompoundFilter(FILTER_HAS_INFO,
+ ApplicationsState.FILTER_NOT_HIDE);
+ if (!mShowSystem) {
+ filterObj = new CompoundFilter(filterObj,
+ ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER);
+ }
+ return filterObj;
+ }
+
+ private void rebuild() {
+ if (!mHasReceivedLoadEntries || !mHasReceivedBridgeCallback) {
+ // Don't rebuild the list until all the app entries are loaded.
+ return;
+ }
+ mSession.rebuild(getAppFilter(), ApplicationsState.ALPHA_COMPARATOR, /* foreground= */
+ false);
+ }
+
+ private static class AppOpPreference extends SwitchPreference {
+
+ private final AppEntry mEntry;
+
+ AppOpPreference(Context context, AppEntry entry) {
+ super(context);
+ String key = entry.info.packageName + "|" + entry.info.uid;
+ setKey(key);
+ setTitle(entry.label);
+ setIcon(entry.icon);
+ setSummary(getAppStateText(entry.info));
+ setPersistent(false);
+ PermissionState extraInfo = (PermissionState) entry.extraInfo;
+ setChecked(extraInfo.isPermissible());
+ mEntry = entry;
+ }
+
+ private String getAppStateText(ApplicationInfo info) {
+ if ((info.flags & ApplicationInfo.FLAG_INSTALLED) == 0) {
+ return getContext().getString(R.string.not_installed);
+ } else if (!info.enabled || info.enabledSetting
+ == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED) {
+ return getContext().getString(R.string.disabled);
+ }
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/ModifySystemSettingsFragment.java b/src/com/android/car/settings/applications/specialaccess/ModifySystemSettingsFragment.java
new file mode 100644
index 0000000..bd3955d
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/ModifySystemSettingsFragment.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+
+/**
+ * Displays apps which have requested to modify system settings and their current allowed status.
+ */
+public class ModifySystemSettingsFragment extends AppOpsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.modify_system_settings_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ lookupAppOpsPreferenceController().init(AppOpsManager.OP_WRITE_SETTINGS,
+ Manifest.permission.WRITE_SETTINGS,
+ AppOpsManager.MODE_ERRORED);
+ }
+
+ @Override
+ protected AppOpsPreferenceController lookupAppOpsPreferenceController() {
+ return use(AppOpsPreferenceController.class, R.string.pk_modify_system_settings);
+ }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/UsageAccessFragment.java b/src/com/android/car/settings/applications/specialaccess/UsageAccessFragment.java
new file mode 100644
index 0000000..c44e870
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/UsageAccessFragment.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+
+/**
+ * Displays apps which have requested to access usage data and their current allowed status.
+ */
+public class UsageAccessFragment extends AppOpsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.usage_access_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ lookupAppOpsPreferenceController().init(AppOpsManager.OP_GET_USAGE_STATS,
+ Manifest.permission.PACKAGE_USAGE_STATS,
+ AppOpsManager.MODE_IGNORED);
+ }
+
+ @Override
+ protected AppOpsPreferenceController lookupAppOpsPreferenceController() {
+ return use(AppOpsPreferenceController.class, R.string.pk_usage_access);
+ }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/WifiControlFragment.java b/src/com/android/car/settings/applications/specialaccess/WifiControlFragment.java
new file mode 100644
index 0000000..eff706c
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/WifiControlFragment.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+
+/**
+ * Displays apps which have requested to control Wi-Fi settings and their current allowed status.
+ */
+public class WifiControlFragment extends AppOpsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.wifi_control_fragment;
+ }
+
+ @Override
+ protected AppOpsPreferenceController lookupAppOpsPreferenceController() {
+ return use(WifiControlPreferenceController.class, R.string.pk_wifi_control);
+ }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/WifiControlPreferenceController.java b/src/com/android/car/settings/applications/specialaccess/WifiControlPreferenceController.java
new file mode 100644
index 0000000..8780f1f
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/WifiControlPreferenceController.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+
+import com.android.car.settings.applications.specialaccess.AppStateAppOpsBridge.PermissionState;
+import com.android.car.settings.common.FragmentController;
+import com.android.internal.util.ArrayUtils;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+import com.android.settingslib.applications.ApplicationsState.CompoundFilter;
+
+/**
+ * Manages the list of apps requesting to control Wi-Fi settings. Apps that also request {@link
+ * Manifest.permission#NETWORK_SETTINGS} are excluded from the list as this permission overrules
+ * {@link Manifest.permission#CHANGE_WIFI_STATE}.
+ */
+public class WifiControlPreferenceController extends AppOpsPreferenceController {
+
+ private static final AppFilter FILTER_CHANGE_WIFI_STATE = new AppFilter() {
+ @Override
+ public void init() {
+ // No op.
+ }
+
+ @Override
+ public boolean filterApp(ApplicationsState.AppEntry info) {
+ return !ArrayUtils.contains(
+ ((PermissionState) info.extraInfo).getRequestedPermissions(),
+ Manifest.permission.NETWORK_SETTINGS);
+ }
+ };
+
+ public WifiControlPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ init(AppOpsManager.OP_CHANGE_WIFI_STATE, Manifest.permission.CHANGE_WIFI_STATE,
+ AppOpsManager.MODE_IGNORED);
+ }
+
+ @Override
+ protected AppFilter getAppFilter() {
+ AppFilter filter = super.getAppFilter();
+ return new CompoundFilter(filter, FILTER_CHANGE_WIFI_STATE);
+ }
+}
diff --git a/src/com/android/car/settings/home/HomepageFragment.java b/src/com/android/car/settings/home/HomepageFragment.java
index 2e11e05..9ca03ae 100644
--- a/src/com/android/car/settings/home/HomepageFragment.java
+++ b/src/com/android/car/settings/home/HomepageFragment.java
@@ -19,11 +19,9 @@
import android.content.Context;
import androidx.annotation.XmlRes;
-import androidx.loader.app.LoaderManager;
import com.android.car.settings.R;
import com.android.car.settings.common.SettingsFragment;
-import com.android.car.settings.suggestions.SuggestionsPreferenceController;
/**
* Homepage for settings for car.
@@ -39,7 +37,8 @@
@Override
public void onAttach(Context context) {
super.onAttach(context);
- use(SuggestionsPreferenceController.class, R.string.pk_suggestions).setLoaderManager(
- LoaderManager.getInstance(/* owner= */ this));
+ // TODO: Re-enable suggestions once more use cases are supported.
+ // use(SuggestionsPreferenceController.class, R.string.pk_suggestions).setLoaderManager(
+ // LoaderManager.getInstance(/* owner= */ this));
}
}
diff --git a/src/com/android/car/settings/security/PasswordHelper.java b/src/com/android/car/settings/security/PasswordHelper.java
index 856d42f..daefc84 100644
--- a/src/com/android/car/settings/security/PasswordHelper.java
+++ b/src/com/android/car/settings/security/PasswordHelper.java
@@ -21,6 +21,8 @@
import android.content.Context;
import com.android.car.settings.R;
+import com.android.car.settings.common.Logger;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.ValidateLockFlags;
import java.util.LinkedList;
import java.util.List;
@@ -30,22 +32,20 @@
*/
public class PasswordHelper {
public static final String EXTRA_CURRENT_SCREEN_LOCK = "extra_current_screen_lock";
-
/**
* Required minimum length of PIN or password.
*/
- static final int MIN_LENGTH = 4;
-
+ public static final int MIN_LENGTH = 4;
// Error code returned from validate(String).
static final int NO_ERROR = 0;
static final int CONTAINS_INVALID_CHARACTERS = 1;
static final int TOO_SHORT = 1 << 1;
static final int CONTAINS_NON_DIGITS = 1 << 2;
static final int CONTAINS_SEQUENTIAL_DIGITS = 1 << 3;
-
+ private static final Logger LOG = new Logger(PasswordHelper.class);
private final boolean mIsPin;
- PasswordHelper(boolean isPin) {
+ public PasswordHelper(boolean isPin) {
mIsPin = isPin;
}
@@ -70,9 +70,21 @@
}
/**
+ * Validates PIN/Password using the setup wizard {@link ValidateLockFlags}.
+ *
+ * @param password The password to validate.
+ * @return The error code where 0 is no error.
+ */
+ public int validateSetupWizard(byte[] password) {
+ return mIsPin ? translateSettingsToSuwError(validatePin(password))
+ : translateSettingsToSuwError(validatePassword(password));
+ }
+
+ /**
* Converts error code from validatePassword to an array of messages describing the errors with
* important message comes first. The messages are concatenated with a space in between.
* Please make sure each message ends with a period.
+ *
* @param errorCode the code returned by {@link #validatePassword(byte[]) validatePassword}
*/
public List<String> convertErrorCodeToMessages(Context context, int errorCode) {
@@ -99,6 +111,27 @@
return errorCode;
}
+ private int translateSettingsToSuwError(int error) {
+ int output = 0;
+ if ((error & CONTAINS_NON_DIGITS) > 0) {
+ LOG.v("CONTAINS_NON_DIGITS");
+ output |= ValidateLockFlags.INVALID_BAD_SYMBOLS;
+ }
+ if ((error & CONTAINS_INVALID_CHARACTERS) > 0) {
+ LOG.v("INVALID_CHAR");
+ output |= ValidateLockFlags.INVALID_BAD_SYMBOLS;
+ }
+ if ((error & TOO_SHORT) > 0) {
+ LOG.v("TOO_SHORT");
+ output |= ValidateLockFlags.INVALID_LENGTH;
+ }
+ if ((error & CONTAINS_SEQUENTIAL_DIGITS) > 0) {
+ LOG.v("SEQUENTIAL_DIGITS");
+ output |= ValidateLockFlags.INVALID_LACKS_COMPLEXITY;
+ }
+ return output;
+ }
+
private int validatePin(byte[] pin) {
int errorCode = NO_ERROR;
PasswordMetrics metrics = PasswordMetrics.computeForPassword(pin);
diff --git a/src/com/android/car/settings/setupservice/InitialLockSetupService.java b/src/com/android/car/settings/setupservice/InitialLockSetupService.java
new file mode 100644
index 0000000..7ea8bc8
--- /dev/null
+++ b/src/com/android/car/settings/setupservice/InitialLockSetupService.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2019 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.car.settings.setupservice;
+
+
+import android.app.Service;
+import android.app.admin.DevicePolicyManager;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.security.PasswordHelper;
+import com.android.car.setupwizardlib.IInitialLockSetupService;
+import com.android.car.setupwizardlib.InitialLockSetupConstants;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.LockTypes;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.SetLockCodes;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.ValidateLockFlags;
+import com.android.car.setupwizardlib.InitialLockSetupHelper;
+import com.android.car.setupwizardlib.LockConfig;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockPatternView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Service that is used by Setup Wizard (exclusively) to set the initial lock screen.
+ *
+ * <p>This service provides functionality to get the lock config state, check if a password is
+ * valid based on the Settings defined password criteria, and save a lock if there is not one
+ * already saved. The interface for these operations is found in the {@link
+ * IInitialLockSetupService}.
+ */
+public class InitialLockSetupService extends Service {
+
+ private static final Logger LOG = new Logger(InitialLockSetupService.class);
+ private static final String SET_LOCK_PERMISSION = "com.android.car.settings.SET_INITIAL_LOCK";
+
+ private final InitialLockSetupServiceImpl mIInitialLockSetupService =
+ new InitialLockSetupServiceImpl();
+
+ /**
+ * Will return an {@link IBinder} for the service unless either the caller does not have the
+ * appropriate permissions or a lock has already been set on the device. In this case, the
+ * service will return {@code null}.
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ LOG.v("onBind");
+ if (checkCallingOrSelfPermission(SET_LOCK_PERMISSION)
+ != PackageManager.PERMISSION_GRANTED) {
+ // Check permission as a failsafe.
+ return null;
+ }
+ int userId = new CarUserManagerHelper(getApplicationContext()).getCurrentProcessUserId();
+ LockPatternUtils lockPatternUtils = new LockPatternUtils(getApplicationContext());
+ // Deny binding if there is an existing lock.
+ if (lockPatternUtils.getKeyguardStoredPasswordQuality(userId)
+ != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+ LOG.v("Rejecting binding, lock exists");
+ return null;
+ }
+ return mIInitialLockSetupService;
+ }
+
+ // Translates the byte[] pattern received into the List<LockPatternView.Cell> that is
+ // recognized by LockPatternUtils.
+ private List<LockPatternView.Cell> toSettingsPattern(byte[] pattern) {
+ List<LockPatternView.Cell> outputList = new ArrayList<>();
+ for (int i = 0; i < pattern.length; i++) {
+ outputList.add(LockPatternView.Cell.of(
+ InitialLockSetupHelper.getPatternCellRowFromByte(pattern[i]),
+ InitialLockSetupHelper.getPatternCellColumnFromByte(pattern[i])));
+ }
+ return outputList;
+ }
+
+ // Implementation of the service binder interface.
+ private class InitialLockSetupServiceImpl extends IInitialLockSetupService.Stub {
+
+ @Override
+ public int getServiceVersion() {
+ return InitialLockSetupConstants.LIBRARY_VERSION;
+ }
+
+ @Override
+ public LockConfig getLockConfig(@LockTypes int lockType) {
+ // All lock types currently are configured the same.
+ switch (lockType) {
+ case LockTypes.PASSWORD:
+ // fall through
+ case LockTypes.PIN:
+ // fall through
+ case LockTypes.PATTERN:
+ return new LockConfig(/* enabled= */true, PasswordHelper.MIN_LENGTH);
+ }
+ return null;
+ }
+
+ @Override
+ @ValidateLockFlags
+ public int checkValidLock(@LockTypes int lockType, byte[] password) {
+ PasswordHelper passwordHelper;
+ switch (lockType) {
+ case LockTypes.PASSWORD:
+ passwordHelper = new PasswordHelper(/* isPin= */ false);
+ return passwordHelper.validateSetupWizard(password);
+ case LockTypes.PIN:
+ passwordHelper = new PasswordHelper(/* isPin= */ true);
+ return passwordHelper.validateSetupWizard(password);
+ case LockTypes.PATTERN:
+ return password.length >= LockPatternUtils.MIN_LOCK_PATTERN_SIZE
+ ? 0 : ValidateLockFlags.INVALID_LENGTH;
+ default:
+ LOG.e("other lock type, returning generic error");
+ return ValidateLockFlags.INVALID_GENERIC;
+ }
+ }
+
+ @Override
+ @SetLockCodes
+ public int setLock(@LockTypes int lockType, byte[] password) {
+ int userId = new CarUserManagerHelper(
+ InitialLockSetupService.this.getApplicationContext())
+ .getCurrentProcessUserId();
+ LockPatternUtils lockPatternUtils = new LockPatternUtils(
+ InitialLockSetupService.this.getApplicationContext());
+ int currentPassword = lockPatternUtils.getKeyguardStoredPasswordQuality(userId);
+ if (currentPassword != DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED) {
+ LOG.v("Password already set, rejecting call to setLock");
+ return SetLockCodes.FAIL_LOCK_EXISTS;
+ }
+ if (!InitialLockSetupHelper.isValidLockResultCode(checkValidLock(lockType, password))) {
+ LOG.v("Password is not valid, rejecting call to setLock");
+ return SetLockCodes.FAIL_LOCK_INVALID;
+ }
+
+ boolean success = false;
+ try {
+ switch (lockType) {
+ case LockTypes.PASSWORD:
+ // Need to remove setup wizard lib byte array encoding and use the
+ // LockPatternUtils encoding.
+ byte[] encodedPassword = LockPatternUtils.charSequenceToByteArray(
+ InitialLockSetupHelper.byteArrayToCharSequence(password));
+ lockPatternUtils.saveLockPassword(encodedPassword,
+ /* savedPassword= */ null,
+ DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC, userId);
+ success = true;
+ break;
+ case LockTypes.PIN:
+ // Need to remove setup wizard lib byte array encoding and use the
+ // LockPatternUtils encoding.
+ byte[] encodedPin = LockPatternUtils.charSequenceToByteArray(
+ InitialLockSetupHelper.byteArrayToCharSequence(password));
+ lockPatternUtils.saveLockPassword(encodedPin, /* savedPassword= */ null,
+ DevicePolicyManager.PASSWORD_QUALITY_NUMERIC, userId);
+ success = true;
+ break;
+ case LockTypes.PATTERN:
+ // Need to remove the setup wizard lib pattern encoding and use the
+ // LockPatternUtils pattern format.
+ List<LockPatternView.Cell> pattern = toSettingsPattern(password);
+ lockPatternUtils.saveLockPattern(pattern, userId);
+ pattern.clear();
+ success = true;
+ break;
+ default:
+ LOG.e("Unknown lock type, returning a failure");
+ }
+ } catch (Exception e) {
+ LOG.e("Save lock exception", e);
+ success = false;
+ }
+ Arrays.fill(password, (byte) 0);
+ return success ? SetLockCodes.SUCCESS : SetLockCodes.FAIL_LOCK_GENERIC;
+ }
+ }
+}
diff --git a/src/com/android/car/settings/wifi/CarWifiManager.java b/src/com/android/car/settings/wifi/CarWifiManager.java
index 096d409..6f4a9f8 100644
--- a/src/com/android/car/settings/wifi/CarWifiManager.java
+++ b/src/com/android/car/settings/wifi/CarWifiManager.java
@@ -185,6 +185,28 @@
mWifiManager.setWifiApConfiguration(config);
}
+ /**
+ * Gets the country code in ISO 3166 format.
+ */
+ public String getCountryCode() {
+ return mWifiManager.getCountryCode();
+ }
+
+ /**
+ * Checks if the chipset supports dual frequency band (2.4 GHz and 5 GHz).
+ */
+ public boolean isDualBandSupported() {
+ return mWifiManager.isDualBandSupported();
+ }
+
+ /**
+ * Check if the chipset requires conversion of 5GHz Only apBand to ANY.
+ * @return {@code true} if required, {@code false} otherwise.
+ */
+ public boolean isDualModeSupported() {
+ return mWifiManager.isDualModeSupported();
+ }
+
/** Gets the wifi state from {@link WifiManager}. */
public int getWifiState() {
return mWifiManager.getWifiState();
diff --git a/src/com/android/car/settings/wifi/WifiTetherApBandPreferenceController.java b/src/com/android/car/settings/wifi/WifiTetherApBandPreferenceController.java
new file mode 100644
index 0000000..1410168
--- /dev/null
+++ b/src/com/android/car/settings/wifi/WifiTetherApBandPreferenceController.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2019 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.car.settings.wifi;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.wifi.WifiConfiguration;
+
+import androidx.preference.ListPreference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+
+/**
+ * Controls WiFi Hotspot AP Band configuration.
+ */
+public class WifiTetherApBandPreferenceController extends
+ WifiTetherBasePreferenceController<ListPreference> {
+
+ private String[] mBandEntries;
+ private String[] mBandSummaries;
+ private int mBandIndex;
+ private boolean mIsDualMode;
+
+ public WifiTetherApBandPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class<ListPreference> getPreferenceType() {
+ return ListPreference.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ super.onCreateInternal();
+ mIsDualMode = getCarWifiManager().isDualModeSupported();
+ updatePreferenceEntries();
+ getPreference().setEntries(mBandSummaries);
+ getPreference().setEntryValues(mBandEntries);
+ }
+
+ @Override
+ public void updateState(ListPreference preference) {
+ super.updateState(preference);
+
+ WifiConfiguration config = getCarWifiApConfig();
+ if (config == null) {
+ mBandIndex = 0;
+ } else if (is5GhzBandSupported()) {
+ mBandIndex = validateSelection(config.apBand);
+ } else {
+ config.apBand = 0;
+ setCarWifiApConfig(config);
+ mBandIndex = config.apBand;
+ }
+
+ if (!is5GhzBandSupported()) {
+ preference.setEnabled(false);
+ preference.setSummary(R.string.wifi_ap_choose_2G);
+ } else {
+ preference.setValue(Integer.toString(config.apBand));
+ preference.setSummary(getSummary());
+ }
+
+ }
+
+ @Override
+ protected String getSummary() {
+ if (is5GhzBandSupported()) {
+ if (mBandIndex != WifiConfiguration.AP_BAND_ANY) {
+ return mBandSummaries[mBandIndex];
+ } else {
+ return getContext().getString(R.string.wifi_ap_prefer_5G);
+ }
+ } else {
+ return getContext().getString(R.string.wifi_ap_choose_2G);
+ }
+ }
+
+ @Override
+ protected String getDefaultSummary() {
+ return null;
+ }
+
+ @Override
+ public boolean handlePreferenceChanged(ListPreference preference, Object newValue) {
+ mBandIndex = validateSelection(Integer.parseInt((String) newValue));
+ updateApBand(); // updating AP band because mBandIndex may have been assigned a new value.
+ refreshUi();
+ return true;
+ }
+
+ private int validateSelection(int band) {
+ // Reset the band to 2.4 GHz if we get a weird config back to avoid a crash.
+ boolean isDualMode = getCarWifiManager().isDualModeSupported();
+
+ // unsupported states:
+ // 1: no dual mode means we can't have AP_BAND_ANY - default to 5GHZ
+ // 2: no 5 GHZ support means we can't have AP_BAND_5GHZ - default to 2GHZ
+ // 3: With Dual mode support we can't have AP_BAND_5GHZ - default to ANY
+ if (!isDualMode && WifiConfiguration.AP_BAND_ANY == band) {
+ return WifiConfiguration.AP_BAND_5GHZ;
+ } else if (!is5GhzBandSupported() && WifiConfiguration.AP_BAND_5GHZ == band) {
+ return WifiConfiguration.AP_BAND_2GHZ;
+ } else if (isDualMode && WifiConfiguration.AP_BAND_5GHZ == band) {
+ return WifiConfiguration.AP_BAND_ANY;
+ }
+
+ return band;
+ }
+
+ private void updatePreferenceEntries() {
+ Resources res = getContext().getResources();
+ int entriesRes = R.array.wifi_ap_band_config_full;
+ int summariesRes = R.array.wifi_ap_band_summary_full;
+ // change the list options if this is a dual mode device
+ if (mIsDualMode) {
+ entriesRes = R.array.wifi_ap_band_dual_mode;
+ summariesRes = R.array.wifi_ap_band_dual_mode_summary;
+ }
+ mBandEntries = res.getStringArray(entriesRes);
+ mBandSummaries = res.getStringArray(summariesRes);
+ }
+
+ private void updateApBand() {
+ WifiConfiguration config = getCarWifiApConfig();
+ config.apBand = mBandIndex;
+ setCarWifiApConfig(config);
+ if (mBandIndex == WifiConfiguration.AP_BAND_ANY) {
+ getPreference().setValue(mBandEntries[WifiConfiguration.AP_BAND_2GHZ]);
+ } else {
+ getPreference().setValue(mBandEntries[mBandIndex]);
+ }
+ }
+
+ private boolean is5GhzBandSupported() {
+ String countryCode = getCarWifiManager().getCountryCode();
+ return getCarWifiManager().isDualBandSupported() && countryCode != null;
+ }
+}
diff --git a/src/com/android/car/settings/wifi/WifiTetherBasePreferenceController.java b/src/com/android/car/settings/wifi/WifiTetherBasePreferenceController.java
index 8c6dd97..dd74f4b 100644
--- a/src/com/android/car/settings/wifi/WifiTetherBasePreferenceController.java
+++ b/src/com/android/car/settings/wifi/WifiTetherBasePreferenceController.java
@@ -89,6 +89,10 @@
mCarWifiManager.setWifiApConfig(configuration);
}
+ protected CarWifiManager getCarWifiManager() {
+ return mCarWifiManager;
+ }
+
protected abstract String getSummary();
protected abstract String getDefaultSummary();
diff --git a/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceController.java b/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceController.java
index b634483..14f83e0 100644
--- a/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceController.java
+++ b/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceController.java
@@ -17,15 +17,23 @@
package com.android.car.settings.wifi;
import android.car.drivingstate.CarUxRestrictions;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
import android.net.wifi.WifiConfiguration;
import android.text.InputType;
import android.text.TextUtils;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.ValidatedEditTextPreference;
+import java.util.UUID;
+
/**
* Controls Wifi Hotspot password configuration.
*
@@ -36,13 +44,31 @@
public class WifiTetherPasswordPreferenceController extends
WifiTetherBasePreferenceController<ValidatedEditTextPreference> {
+ protected static final String SHARED_PREFERENCE_PATH =
+ "com.android.car.settings.wifi.WifiTetherPreferenceController";
+ protected static final String KEY_SAVED_PASSWORD =
+ "com.android.car.settings.wifi.SAVED_PASSWORD";
+
private static final int HOTSPOT_PASSWORD_MIN_LENGTH = 8;
private static final int HOTSPOT_PASSWORD_MAX_LENGTH = 63;
private static final ValidatedEditTextPreference.Validator PASSWORD_VALIDATOR =
value -> value.length() >= HOTSPOT_PASSWORD_MIN_LENGTH
&& value.length() <= HOTSPOT_PASSWORD_MAX_LENGTH;
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mSecurityType = intent.getIntExtra(
+ WifiTetherSecurityPreferenceController.KEY_SECURITY_TYPE,
+ /* defaultValue= */ WifiConfiguration.KeyMgmt.NONE);
+ syncPassword();
+ }
+ };
+ private final SharedPreferences mSharedPreferences =
+ getContext().getSharedPreferences(SHARED_PREFERENCE_PATH, Context.MODE_PRIVATE);
+
private String mPassword;
+ private int mSecurityType;
public WifiTetherPasswordPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
@@ -57,8 +83,23 @@
@Override
protected void onCreateInternal() {
super.onCreateInternal();
+
getPreference().setValidator(PASSWORD_VALIDATOR);
- mPassword = getCarWifiApConfig().preSharedKey;
+ mSecurityType = getCarWifiApConfig().getAuthType();
+ syncPassword();
+ }
+
+ @Override
+ protected void onStartInternal() {
+ LocalBroadcastManager.getInstance(getContext()).registerReceiver(mReceiver,
+ new IntentFilter(
+ WifiTetherSecurityPreferenceController.ACTION_SECURITY_TYPE_CHANGED));
+ }
+
+ @Override
+ protected void onStopInternal() {
+ super.onStopInternal();
+ LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(mReceiver);
}
@Override
@@ -73,7 +114,7 @@
@Override
protected void updateState(ValidatedEditTextPreference preference) {
super.updateState(preference);
- preference.setText(mPassword);
+ updatePasswordDisplay();
if (TextUtils.isEmpty(mPassword)) {
preference.setSummaryInputType(InputType.TYPE_CLASS_TEXT);
} else {
@@ -92,9 +133,49 @@
return getContext().getString(R.string.default_password_summary);
}
+ private void syncPassword() {
+ mPassword = getSyncedPassword();
+ updatePassword(mPassword);
+ refreshUi();
+ }
+
+ private String getSyncedPassword() {
+ if (getCarWifiApConfig().getAuthType() == WifiConfiguration.KeyMgmt.NONE) {
+ return null;
+ }
+
+ if (!TextUtils.isEmpty(getCarWifiApConfig().preSharedKey)) {
+ return getCarWifiApConfig().preSharedKey;
+ }
+
+ if (!TextUtils.isEmpty(
+ mSharedPreferences.getString(KEY_SAVED_PASSWORD, /* defaultValue= */ null))) {
+ return mSharedPreferences.getString(KEY_SAVED_PASSWORD, /* defaultValue= */ null);
+ }
+
+ return generateRandomPassword();
+ }
+
+ private static String generateRandomPassword() {
+ String randomUUID = UUID.randomUUID().toString();
+ // First 12 chars from xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
+ return randomUUID.substring(0, 8) + randomUUID.substring(9, 13);
+ }
+
private void updatePassword(String password) {
WifiConfiguration config = getCarWifiApConfig();
config.preSharedKey = password;
setCarWifiApConfig(config);
+
+ if (!TextUtils.isEmpty(password)) {
+ mSharedPreferences.edit().putString(KEY_SAVED_PASSWORD, password).commit();
+ }
}
+
+ private void updatePasswordDisplay() {
+ getPreference().setText(mPassword);
+ getPreference().setVisible(mSecurityType != WifiConfiguration.KeyMgmt.NONE);
+ getPreference().setSummary(getSummary());
+ }
+
}
diff --git a/src/com/android/car/settings/wifi/WifiTetherSecurityPreferenceController.java b/src/com/android/car/settings/wifi/WifiTetherSecurityPreferenceController.java
new file mode 100644
index 0000000..618bc25
--- /dev/null
+++ b/src/com/android/car/settings/wifi/WifiTetherSecurityPreferenceController.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2019 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.car.settings.wifi;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.wifi.WifiConfiguration;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.preference.ListPreference;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+
+/**
+ * Controls WiFi Hotspot Security Type configuration.
+ */
+public class WifiTetherSecurityPreferenceController extends
+ WifiTetherBasePreferenceController<ListPreference> {
+
+ public static final String KEY_SECURITY_TYPE = "KEY_SECURITY_TYPE";
+ public static final String ACTION_SECURITY_TYPE_CHANGED =
+ "com.android.car.settings.wifi.ACTION_WIFI_TETHER_SECURITY_TYPE_CHANGED";
+
+ private int mSecurityType;
+
+ public WifiTetherSecurityPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ @Override
+ protected Class<ListPreference> getPreferenceType() {
+ return ListPreference.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ super.onCreateInternal();
+ mSecurityType = getCarWifiApConfig().getAuthType();
+ getPreference().setEntries(
+ getContext().getResources().getStringArray(R.array.wifi_tether_security));
+ String[] entryValues = {Integer.toString(WifiConfiguration.KeyMgmt.WPA2_PSK),
+ Integer.toString(WifiConfiguration.KeyMgmt.NONE)};
+ getPreference().setEntryValues(entryValues);
+ getPreference().setValue(String.valueOf(mSecurityType));
+ }
+
+ @Override
+ protected boolean handlePreferenceChanged(ListPreference preference,
+ Object newValue) {
+ mSecurityType = Integer.parseInt(newValue.toString());
+ updateSecurityType();
+ refreshUi();
+ return true;
+ }
+
+ @Override
+ protected void updateState(ListPreference preference) {
+ super.updateState(preference);
+ preference.setValue(Integer.toString(mSecurityType));
+ }
+
+ @Override
+ protected String getSummary() {
+ int stringResId = mSecurityType == WifiConfiguration.KeyMgmt.WPA2_PSK
+ ? R.string.wifi_hotspot_wpa2_personal : R.string.wifi_hotspot_security_none;
+ return getContext().getString(stringResId);
+ }
+
+ @Override
+ protected String getDefaultSummary() {
+ return null;
+ }
+
+ private void updateSecurityType() {
+ WifiConfiguration config = getCarWifiApConfig();
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(mSecurityType);
+
+ if (mSecurityType == WifiConfiguration.KeyMgmt.NONE) {
+ config.preSharedKey = "";
+ } else {
+ config.preSharedKey = getSavedPassword();
+ }
+
+ setCarWifiApConfig(config);
+ broadcastSecurityTypeChanged();
+ }
+
+ private void broadcastSecurityTypeChanged() {
+ Intent intent = new Intent(ACTION_SECURITY_TYPE_CHANGED);
+ intent.putExtra(KEY_SECURITY_TYPE, mSecurityType);
+ LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
+ }
+
+ private String getSavedPassword() {
+ SharedPreferences sp = getContext().getSharedPreferences(
+ WifiTetherPasswordPreferenceController.SHARED_PREFERENCE_PATH,
+ Context.MODE_PRIVATE);
+ String savedPassword =
+ sp.getString(WifiTetherPasswordPreferenceController.KEY_SAVED_PASSWORD,
+ /* defaultValue= */ null);
+ return savedPassword;
+ }
+}
diff --git a/tests/robotests/AndroidManifest.xml b/tests/robotests/AndroidManifest.xml
index 77fd39c..efe1f69 100644
--- a/tests/robotests/AndroidManifest.xml
+++ b/tests/robotests/AndroidManifest.xml
@@ -19,5 +19,4 @@
coreApp="true"
package="com.android.car.settings.robotests">
<application/>
-
</manifest>
diff --git a/tests/robotests/src/com/android/car/settings/CarSettingsRobolectricTestRunner.java b/tests/robotests/src/com/android/car/settings/CarSettingsRobolectricTestRunner.java
index a5ac933..ba9b84d 100644
--- a/tests/robotests/src/com/android/car/settings/CarSettingsRobolectricTestRunner.java
+++ b/tests/robotests/src/com/android/car/settings/CarSettingsRobolectricTestRunner.java
@@ -101,7 +101,8 @@
// By adding any resources from libraries we need to the AndroidManifest, we can access
// them from within the parallel universe's resource loader.
- return new AndroidManifest(Fs.fromURL(manifestPath), Fs.fromURL(resDir),
+ AndroidManifest androidManifest = new AndroidManifest(Fs.fromURL(manifestPath),
+ Fs.fromURL(resDir),
Fs.fromURL(assetsDir)) {
@Override
public List<ResourcePath> getIncludedResourcePaths() {
@@ -123,6 +124,10 @@
return paths;
}
};
+ // Need to remove this permission since this version of robolectric does not support
+ // multiple protection levels.
+ androidManifest.getPermissions().remove("com.android.car.settings.SET_INITIAL_LOCK");
+ return androidManifest;
} catch (MalformedURLException e) {
throw new RuntimeException("CarSettingsobolectricTestRunner failure", e);
}
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceControllerTest.java
new file mode 100644
index 0000000..497bd95
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/AppOpsPreferenceControllerTest.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2019 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.car.settings.applications.specialaccess;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ParceledListSlice;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.common.LogicalPreferenceGroup;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
+import com.android.car.settings.testutils.ShadowActivityThread;
+import com.android.car.settings.testutils.ShadowAppOpsManager;
+import com.android.car.settings.testutils.ShadowApplicationsState;
+import com.android.settingslib.applications.ApplicationsState;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.AdditionalMatchers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/** Unit test for {@link AppOpsPreferenceController}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowAppOpsManager.class, ShadowApplicationsState.class,
+ ShadowActivityThread.class})
+public class AppOpsPreferenceControllerTest {
+
+ private static final int APP_OP_CODE = AppOpsManager.OP_WRITE_SETTINGS;
+ private static final String PERMISSION = Manifest.permission.WRITE_SETTINGS;
+ private static final int NEGATIVE_MODE = AppOpsManager.MODE_ERRORED;
+
+ @Mock
+ private IPackageManager mIPackageManager;
+ @Mock
+ private ParceledListSlice<PackageInfo> mParceledPackages;
+ @Mock
+ private ApplicationsState mApplicationsState;
+ @Mock
+ private ApplicationsState.Session mSession;
+ @Mock
+ private ApplicationsState.Session mBridgeSession;
+ @Captor
+ private ArgumentCaptor<ApplicationsState.Callbacks> mCallbackCaptor;
+
+ private Context mContext;
+ private PreferenceGroup mPreferenceGroup;
+ private PreferenceControllerTestHelper<AppOpsPreferenceController> mControllerHelper;
+ private AppOpsPreferenceController mController;
+
+ @Before
+ public void setUp() throws RemoteException {
+ MockitoAnnotations.initMocks(this);
+ ShadowActivityThread.setPackageManager(mIPackageManager);
+ when(mIPackageManager.getPackagesHoldingPermissions(
+ AdditionalMatchers.aryEq(new String[]{PERMISSION}),
+ eq(PackageManager.GET_PERMISSIONS),
+ eq(UserHandle.myUserId())))
+ .thenReturn(mParceledPackages);
+ when(mParceledPackages.getList()).thenReturn(Collections.emptyList());
+ ShadowApplicationsState.setInstance(mApplicationsState);
+ when(mApplicationsState.newSession(mCallbackCaptor.capture()))
+ .thenReturn(mSession)
+ .thenReturn(mBridgeSession);
+ when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
+
+ mContext = RuntimeEnvironment.application;
+ mPreferenceGroup = new LogicalPreferenceGroup(mContext);
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ AppOpsPreferenceController.class);
+ mController = mControllerHelper.getController();
+ mController.init(APP_OP_CODE, PERMISSION, NEGATIVE_MODE);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.CREATED);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowApplicationsState.reset();
+ ShadowActivityThread.reset();
+ }
+
+ @Test
+ public void checkInitialized_noOpCode_throwsIllegalStateException() {
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ AppOpsPreferenceController.class);
+ mController = mControllerHelper.getController();
+
+ mController.init(AppOpsManager.OP_NONE, PERMISSION, NEGATIVE_MODE);
+
+ assertThrows(IllegalStateException.class,
+ () -> mControllerHelper.setPreference(mPreferenceGroup));
+ }
+
+ @Test
+ public void checkInitialized_noPermission_throwsIllegalStateException() {
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ AppOpsPreferenceController.class);
+ mController = mControllerHelper.getController();
+
+ mController.init(APP_OP_CODE, /* permission= */ null, NEGATIVE_MODE);
+
+ assertThrows(IllegalStateException.class,
+ () -> mControllerHelper.setPreference(mPreferenceGroup));
+ }
+
+ @Test
+ public void checkInitialized_noNegativeOpMode_throwsIllegalStateException() {
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ AppOpsPreferenceController.class);
+ mController = mControllerHelper.getController();
+
+ mController.init(APP_OP_CODE, PERMISSION, AppOpsManager.MODE_DEFAULT);
+
+ assertThrows(IllegalStateException.class,
+ () -> mControllerHelper.setPreference(mPreferenceGroup));
+ }
+
+ @Test
+ public void onStart_resumesSessions() {
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ verify(mSession).onResume();
+ verify(mBridgeSession).onResume();
+ }
+
+ @Test
+ public void onStop_pausesSessions() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+ verify(mSession).onPause();
+ verify(mBridgeSession).onPause();
+ }
+
+ @Test
+ public void onDestroy_destroysSessions() {
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+ verify(mSession).onDestroy();
+ verify(mBridgeSession).onDestroy();
+ }
+
+ @Test
+ public void onLoadEntriesCompleted_extraInfoUpdated_rebuildsEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+
+ // Extra info updated callback happens synchronously onStart since we are using the main
+ // looper for testing.
+ callbacks.onLoadEntriesCompleted();
+
+ verify(mSession).rebuild(any(), eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
+ eq(false));
+ }
+
+ @Test
+ public void onRebuildComplete_addsPreferencesForEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ ArrayList<AppEntry> entries = new ArrayList<>();
+ entries.add(createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */ true));
+ entries.add(
+ createAppEntry("another.test.package", /* uid= */ 2, /* isOpPermissible= */ false));
+ callbacks.onLoadEntriesCompleted();
+
+ callbacks.onRebuildComplete(entries);
+
+ assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(2);
+ assertThat(((TwoStatePreference) mPreferenceGroup.getPreference(0)).isChecked()).isTrue();
+ assertThat(((TwoStatePreference) mPreferenceGroup.getPreference(1)).isChecked()).isFalse();
+ }
+
+ @Test
+ public void onPreferenceChange_checkedState_setsAppOpModeAllowed() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ ArrayList<AppEntry> entries = new ArrayList<>();
+ String packageName = "test.package";
+ int uid = 1;
+ entries.add(createAppEntry("test.package", uid, /* isOpPermissible= */ false));
+ callbacks.onLoadEntriesCompleted();
+ callbacks.onRebuildComplete(entries);
+ TwoStatePreference appPref = (TwoStatePreference) mPreferenceGroup.getPreference(0);
+
+ appPref.performClick();
+
+ assertThat(getShadowAppOpsManager().getMode(APP_OP_CODE, uid, packageName)).isEqualTo(
+ AppOpsManager.MODE_ALLOWED);
+ }
+
+ @Test
+ public void onPreferenceChange_uncheckedState_setsNegativeAppOpMode() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ ArrayList<AppEntry> entries = new ArrayList<>();
+ String packageName = "test.package";
+ int uid = 1;
+ entries.add(createAppEntry("test.package", uid, /* isOpPermissible= */ true));
+ callbacks.onLoadEntriesCompleted();
+ callbacks.onRebuildComplete(entries);
+ TwoStatePreference appPref = (TwoStatePreference) mPreferenceGroup.getPreference(0);
+
+ appPref.performClick();
+
+ assertThat(getShadowAppOpsManager().getMode(APP_OP_CODE, uid, packageName)).isEqualTo(
+ NEGATIVE_MODE);
+ }
+
+ @Test
+ public void onPreferenceChange_rebuildsEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ ArrayList<AppEntry> entries = new ArrayList<>();
+ String packageName = "test.package";
+ int uid = 1;
+ entries.add(createAppEntry("test.package", uid, /* isOpPermissible= */ false));
+ callbacks.onLoadEntriesCompleted();
+ callbacks.onRebuildComplete(entries);
+ TwoStatePreference appPref = (TwoStatePreference) mPreferenceGroup.getPreference(0);
+
+ appPref.performClick();
+
+ // 2 times: onLoadEntriesCompleted, onPreferenceChange
+ verify(mSession, times(2)).rebuild(any(),
+ eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
+ eq(false));
+ }
+
+ @Test
+ public void showSystem_rebuildsEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ callbacks.onLoadEntriesCompleted();
+
+ mController.setShowSystem(true);
+
+ // 2 times: onLoadEntriesCompleted, setShowSystem
+ verify(mSession, times(2)).rebuild(any(),
+ eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
+ eq(false));
+ }
+
+ @Test
+ public void rebuildFilter_showingSystemApps_keepsSystemEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ callbacks.onLoadEntriesCompleted();
+ mController.setShowSystem(true);
+ ArgumentCaptor<ApplicationsState.AppFilter> filterCaptor = ArgumentCaptor.forClass(
+ ApplicationsState.AppFilter.class);
+ // 2 times: onLoadEntriesCompleted, setShowSystem
+ verify(mSession, times(2)).rebuild(filterCaptor.capture(),
+ eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
+ eq(false));
+
+ // Get the filter from setShowSystem.
+ ApplicationsState.AppFilter filter = filterCaptor.getAllValues().get(1);
+
+ AppEntry systemApp = createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */
+ false);
+ systemApp.info.flags |= ApplicationInfo.FLAG_SYSTEM;
+
+ assertThat(filter.filterApp(systemApp)).isTrue();
+ }
+
+ @Test
+ public void rebuildFilter_notShowingSystemApps_removesSystemEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ callbacks.onLoadEntriesCompleted();
+ ArgumentCaptor<ApplicationsState.AppFilter> filterCaptor = ArgumentCaptor.forClass(
+ ApplicationsState.AppFilter.class);
+ verify(mSession).rebuild(filterCaptor.capture(),
+ eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
+ eq(false));
+
+ // Not showing system by default
+ ApplicationsState.AppFilter filter = filterCaptor.getValue();
+
+ AppEntry systemApp = createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */
+ false);
+ systemApp.info.flags |= ApplicationInfo.FLAG_SYSTEM;
+
+ assertThat(filter.filterApp(systemApp)).isFalse();
+ }
+
+ @Test
+ public void rebuildFilter_removesNullExtraInfoEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ ApplicationsState.Callbacks callbacks = mCallbackCaptor.getAllValues().get(0);
+ callbacks.onLoadEntriesCompleted();
+ ArgumentCaptor<ApplicationsState.AppFilter> filterCaptor = ArgumentCaptor.forClass(
+ ApplicationsState.AppFilter.class);
+ verify(mSession).rebuild(filterCaptor.capture(),
+ eq(ApplicationsState.ALPHA_COMPARATOR), /* foreground= */
+ eq(false));
+
+ ApplicationsState.AppFilter filter = filterCaptor.getValue();
+
+ AppEntry appEntry = createAppEntry("test.package", /* uid= */ 1, /* isOpPermissible= */
+ false);
+ appEntry.extraInfo = null;
+
+ assertThat(filter.filterApp(appEntry)).isFalse();
+ }
+
+ private AppEntry createAppEntry(String packageName, int uid, boolean isOpPermissible) {
+ ApplicationInfo info = new ApplicationInfo();
+ info.packageName = packageName;
+ info.uid = uid;
+
+ AppStateAppOpsBridge.PermissionState extraInfo = mock(
+ AppStateAppOpsBridge.PermissionState.class);
+ when(extraInfo.isPermissible()).thenReturn(isOpPermissible);
+
+ AppEntry appEntry = mock(AppEntry.class);
+ appEntry.info = info;
+ appEntry.label = packageName;
+ appEntry.extraInfo = extraInfo;
+
+ return appEntry;
+ }
+
+ private ShadowAppOpsManager getShadowAppOpsManager() {
+ return Shadow.extract(mContext.getSystemService(Context.APP_OPS_SERVICE));
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/common/SettingsFragmentTest.java b/tests/robotests/src/com/android/car/settings/common/SettingsFragmentTest.java
index cf11631..a19e3dd 100644
--- a/tests/robotests/src/com/android/car/settings/common/SettingsFragmentTest.java
+++ b/tests/robotests/src/com/android/car/settings/common/SettingsFragmentTest.java
@@ -37,6 +37,7 @@
import com.android.car.settings.CarSettingsRobolectricTestRunner;
import com.android.car.settings.R;
+import com.android.car.settings.testutils.DummyFragment;
import com.android.car.settings.testutils.FragmentController;
import org.junit.Before;
@@ -272,7 +273,7 @@
@Test
public void onActivityCreated_hasAppIconIfRoot() {
mFragmentController.setup();
- TestSettingsFragment otherFragment = new TestSettingsFragment();
+ DummyFragment otherFragment = new DummyFragment();
mFragment.launchFragment(otherFragment);
FrameLayout actionBarContainer =
diff --git a/tests/robotests/src/com/android/car/settings/security/InitialLockSetupServiceTest.java b/tests/robotests/src/com/android/car/settings/security/InitialLockSetupServiceTest.java
new file mode 100644
index 0000000..d05a33d
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/security/InitialLockSetupServiceTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2019 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.car.settings.security;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.RemoteException;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.setupservice.InitialLockSetupService;
+import com.android.car.settings.testutils.ShadowLockPatternUtils;
+import com.android.car.setupwizardlib.IInitialLockSetupService;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.LockTypes;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.SetLockCodes;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.ValidateLockFlags;
+import com.android.car.setupwizardlib.InitialLockSetupHelper;
+import com.android.car.setupwizardlib.LockConfig;
+import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockPatternView;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.Robolectric;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowContextWrapper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests that the {@link InitialLockSetupService} properly handles connections and lock requests.
+ */
+@Config(shadows = ShadowLockPatternUtils.class)
+@RunWith(CarSettingsRobolectricTestRunner.class)
+public class InitialLockSetupServiceTest {
+
+ private static final String LOCK_PERMISSION = "com.android.car.settings.SET_INITIAL_LOCK";
+
+ private InitialLockSetupService mInitialLockSetupService;
+ private Context mContext;
+
+ @Before
+ public void setupService() {
+ ShadowLockPatternUtils.reset();
+ mInitialLockSetupService = Robolectric.buildService(InitialLockSetupService.class)
+ .create()
+ .get();
+ mContext = RuntimeEnvironment.application;
+ ShadowContextWrapper shadowContextWrapper = Shadows.shadowOf((ContextWrapper) mContext);
+ shadowContextWrapper.grantPermissions(LOCK_PERMISSION);
+ }
+
+ @Test
+ public void testBindReturnsNull_ifLockSet() {
+ ShadowLockPatternUtils.setPasswordQuality(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+ assertThat(mInitialLockSetupService.onBind(new Intent())).isNull();
+ }
+
+ @Test
+ public void testBindReturnsInstanceOfServiceInterface_ifLockNotSet() throws RemoteException {
+ assertThat(mInitialLockSetupService.onBind(
+ new Intent()) instanceof IInitialLockSetupService.Stub).isTrue();
+ }
+
+ @Test
+ public void testGetLockConfig_returnsCorrectConfig() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ LockConfig pinConfig = service.getLockConfig(LockTypes.PIN);
+ assertThat(pinConfig.enabled).isTrue();
+ assertThat(pinConfig.minLockLength).isEqualTo(LockPatternUtils.MIN_LOCK_PASSWORD_SIZE);
+ LockConfig patternConfig = service.getLockConfig(LockTypes.PATTERN);
+ assertThat(patternConfig.enabled).isTrue();
+ assertThat(patternConfig.minLockLength).isEqualTo(LockPatternUtils.MIN_LOCK_PATTERN_SIZE);
+ LockConfig passwordConfig = service.getLockConfig(LockTypes.PASSWORD);
+ assertThat(passwordConfig.enabled).isTrue();
+ assertThat(passwordConfig.minLockLength).isEqualTo(LockPatternUtils.MIN_LOCK_PASSWORD_SIZE);
+ }
+
+ @Test
+ public void testCheckValidLock_tooShort() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ int result = service.checkValidLock(LockTypes.PASSWORD, "hi".getBytes());
+ assertThat(result & ValidateLockFlags.INVALID_LENGTH)
+ .isEqualTo(ValidateLockFlags.INVALID_LENGTH);
+ }
+
+ @Test
+ public void testCheckValidLock_longEnough() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ int result = service.checkValidLock(LockTypes.PASSWORD, "password".getBytes());
+ assertThat(result & ValidateLockFlags.INVALID_LENGTH)
+ .isNotEqualTo(ValidateLockFlags.INVALID_LENGTH);
+ }
+
+ @Test
+ public void testCheckValidLockPin_withLetters() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ int result = service.checkValidLock(LockTypes.PIN, "12a3".getBytes());
+ assertThat(result & ValidateLockFlags.INVALID_BAD_SYMBOLS)
+ .isEqualTo(ValidateLockFlags.INVALID_BAD_SYMBOLS);
+ }
+
+ @Test
+ public void testCheckValidLockPattern_tooShort() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ byte[] pattern = new byte[LockPatternUtils.MIN_LOCK_PATTERN_SIZE - 1];
+ for (int i = 0; i < pattern.length; i++) {
+ pattern[i] = (byte) i;
+ }
+ int result = service.checkValidLock(LockTypes.PATTERN, pattern);
+ assertThat(result & ValidateLockFlags.INVALID_LENGTH)
+ .isEqualTo(ValidateLockFlags.INVALID_LENGTH);
+ }
+
+ @Test
+ public void testCheckValidLockPattern_longEnough() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ byte[] pattern = new byte[LockPatternUtils.MIN_LOCK_PATTERN_SIZE + 1];
+ for (int i = 0; i < pattern.length; i++) {
+ pattern[i] = (byte) i;
+ }
+ int result = service.checkValidLock(LockTypes.PATTERN, pattern);
+ assertThat(result & ValidateLockFlags.INVALID_LENGTH)
+ .isNotEqualTo(ValidateLockFlags.INVALID_LENGTH);
+ }
+
+ @Test
+ public void testSetLockPassword_doesNotWorkWithExistingPassword() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ ShadowLockPatternUtils.setPasswordQuality(DevicePolicyManager.PASSWORD_QUALITY_NUMERIC);
+ int result = service.setLock(LockTypes.PASSWORD, "password".getBytes());
+ assertThat(result).isEqualTo(SetLockCodes.FAIL_LOCK_EXISTS);
+ }
+
+ @Test
+ public void testSetLockPassword_doesNotWorkWithInvalidPassword() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ int result = service.setLock(LockTypes.PASSWORD, "hi".getBytes());
+ assertThat(result).isEqualTo(SetLockCodes.FAIL_LOCK_INVALID);
+ }
+
+ @Test
+ public void testSetLockPassword_setsDevicePassword() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ byte[] password = "password".getBytes();
+ // Need copy since password is cleared.
+ byte[] expectedPassword = Arrays.copyOf(password, password.length);
+ int result = service.setLock(LockTypes.PASSWORD, password);
+ assertThat(result).isEqualTo(SetLockCodes.SUCCESS);
+ assertThat(Arrays.equals(ShadowLockPatternUtils.getSavedPassword(),
+ expectedPassword)).isTrue();
+ }
+
+ @Test
+ public void testSetLockPin_setsDevicePin() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ byte[] password = "1580".getBytes();
+ byte[] expectedPassword = Arrays.copyOf(password, password.length);
+ int result = service.setLock(LockTypes.PIN, password);
+ assertThat(result).isEqualTo(SetLockCodes.SUCCESS);
+ assertThat(Arrays.equals(ShadowLockPatternUtils.getSavedPassword(),
+ expectedPassword)).isTrue();
+ }
+
+ @Test
+ public void testSetLockPattern_setsDevicePattern() throws RemoteException {
+ IInitialLockSetupService service = IInitialLockSetupService.Stub.asInterface(
+ mInitialLockSetupService.onBind(new Intent()));
+ List<LockPatternView.Cell> pattern = new ArrayList<>();
+ pattern.add(LockPatternView.Cell.of(0, 0));
+ pattern.add(LockPatternView.Cell.of(1, 0));
+ pattern.add(LockPatternView.Cell.of(2, 0));
+ pattern.add(LockPatternView.Cell.of(0, 1));
+ byte[] patternBytes = new byte[pattern.size()];
+ for (int i = 0; i < patternBytes.length; i++) {
+ LockPatternView.Cell cell = pattern.get(i);
+ patternBytes[i] = InitialLockSetupHelper.getByteFromPatternCell(cell.getRow(),
+ cell.getColumn());
+ }
+ int result = service.setLock(LockTypes.PATTERN, patternBytes);
+ assertThat(result).isEqualTo(SetLockCodes.SUCCESS);
+ List<LockPatternView.Cell> savedPattern = ShadowLockPatternUtils.getSavedPattern();
+ assertThat(savedPattern).containsExactlyElementsIn(pattern);
+
+ }
+
+ @Test
+ public void testBindFails_ifNoPermissionGranted() {
+ ShadowContextWrapper shadowContextWrapper = Shadows.shadowOf((ContextWrapper) mContext);
+ shadowContextWrapper.denyPermissions(LOCK_PERMISSION);
+ assertThat(mInitialLockSetupService.onBind(new Intent())).isNull();
+ }
+
+}
diff --git a/tests/robotests/src/com/android/car/settings/security/PasswordHelperTest.java b/tests/robotests/src/com/android/car/settings/security/PasswordHelperTest.java
index bbc468a..567a009 100644
--- a/tests/robotests/src/com/android/car/settings/security/PasswordHelperTest.java
+++ b/tests/robotests/src/com/android/car/settings/security/PasswordHelperTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.setupwizardlib.InitialLockSetupConstants.ValidateLockFlags;
import org.junit.Before;
import org.junit.Test;
@@ -31,12 +32,12 @@
public class PasswordHelperTest {
private PasswordHelper mPasswordHelper;
- private PasswordHelper mPinPresenter;
+ private PasswordHelper mPinHelper;
@Before
public void initObjects() {
- mPasswordHelper = new PasswordHelper(false);
- mPinPresenter = new PasswordHelper(true);
+ mPasswordHelper = new PasswordHelper(/* isPin= */ false);
+ mPinHelper = new PasswordHelper(/* isPin= */ true);
}
/**
@@ -51,7 +52,7 @@
}
/**
- * A test to check validate works when alphanumeric passwor contains white space.
+ * A test to check validate works when alphanumeric password contains white space.
*/
@Test
public void testValidatePasswordWhiteSpace() {
@@ -77,7 +78,7 @@
@Test
public void testValidatePinContainingNonDigits() {
byte[] password = "1a34".getBytes();
- assertThat(mPinPresenter.validate(password))
+ assertThat(mPinHelper.validate(password))
.isEqualTo(PasswordHelper.CONTAINS_NON_DIGITS);
}
@@ -87,7 +88,71 @@
@Test
public void testValidatePinWithTooFewDigits() {
byte[] password = "12".getBytes();
- assertThat(mPinPresenter.validate(password))
+ assertThat(mPinHelper.validate(password))
.isEqualTo(PasswordHelper.TOO_SHORT);
}
+
+ /**
+ * A test to check that validate will work as expected for setup wizard passwords that are
+ * too short.
+ */
+ @Test
+ public void testValidatePasswordTooShort_setupWizard() {
+ byte[] password = "lov".getBytes();
+ assertThat(mPasswordHelper.validateSetupWizard(password) & ValidateLockFlags.INVALID_LENGTH)
+ .isEqualTo(ValidateLockFlags.INVALID_LENGTH);
+ }
+
+ /**
+ * A test to check that validate works when setup wizard alphanumeric passwords contain white
+ * space.
+ */
+ @Test
+ public void testValidatePasswordWhiteSpace_setupWizard() {
+ byte[] password = "pass wd".getBytes();
+ assertThat(mPasswordHelper.validateSetupWizard(password)).isEqualTo(0);
+ }
+
+ /**
+ * A test to check validate works as expected for setup wizard alphanumeric password
+ * that contains an invalid character.
+ */
+ @Test
+ public void testValidatePasswordNonAscii_setupWizard() {
+ byte[] password = "1passwýd".getBytes();
+ assertThat(mPasswordHelper.validateSetupWizard(password)
+ & ValidateLockFlags.INVALID_BAD_SYMBOLS)
+ .isEqualTo(ValidateLockFlags.INVALID_BAD_SYMBOLS);
+ }
+
+ /**
+ * A test to check validate works as expected for setup wizard pin that contains non digits.
+ */
+ @Test
+ public void testValidatePinContainingNonDigits_setupWizard() {
+ byte[] password = "1a34".getBytes();
+ assertThat(mPinHelper.validateSetupWizard(password)
+ & ValidateLockFlags.INVALID_BAD_SYMBOLS)
+ .isEqualTo(ValidateLockFlags.INVALID_BAD_SYMBOLS);
+ }
+
+ /**
+ * A test to check validate works as expected for setup wizard pin with too few digits.
+ */
+ @Test
+ public void testValidatePinWithTooFewDigits_setupWizard() {
+ byte[] password = "12".getBytes();
+ assertThat(mPinHelper.validateSetupWizard(password) & ValidateLockFlags.INVALID_LENGTH)
+ .isEqualTo(ValidateLockFlags.INVALID_LENGTH);
+ }
+
+ /**
+ * A test to check that validate works as expected for a valid setup wizard pin.
+ */
+ @Test
+ public void testValidatePinWithValidPin_setupWizard() {
+ byte[] password = "1234".getBytes();
+ assertThat(mPinHelper.validateSetupWizard(password)).isEqualTo(0);
+ }
+
}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/BaseTestActivity.java b/tests/robotests/src/com/android/car/settings/testutils/BaseTestActivity.java
index ac770b9..5fff6f9 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/BaseTestActivity.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/BaseTestActivity.java
@@ -58,9 +58,10 @@
throw new IllegalArgumentException(
"cannot launch dialogs with launchFragment() - use showDialog() instead");
}
+ String tag = Integer.toString(getSupportFragmentManager().getBackStackEntryCount());
getSupportFragmentManager()
.beginTransaction()
- .replace(R.id.fragment_container, fragment)
+ .replace(R.id.fragment_container, fragment, tag)
.addToBackStack(null)
.commit();
}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/DummyFragment.java b/tests/robotests/src/com/android/car/settings/testutils/DummyFragment.java
index ec93bee..dc6cfba 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/DummyFragment.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/DummyFragment.java
@@ -16,10 +16,15 @@
package com.android.car.settings.testutils;
-import androidx.fragment.app.Fragment;
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
/**
* Empty Fragment.
*/
-public class DummyFragment extends Fragment {
+public class DummyFragment extends SettingsFragment {
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.settings_fragment;
+ }
}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java
index 5cd1ac6..2d51a86 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowActivityThread.java
@@ -25,14 +25,29 @@
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
import java.lang.reflect.Proxy;
@Implements(ActivityThread.class)
public class ShadowActivityThread {
+ private static IPackageManager sIPackageManager;
+
+ public static void setPackageManager(IPackageManager packageManager) {
+ sIPackageManager = packageManager;
+ }
+
+ @Resetter
+ public static void reset() {
+ sIPackageManager = null;
+ }
+
@Implementation
protected static IPackageManager getPackageManager() {
+ if (sIPackageManager != null) {
+ return sIPackageManager;
+ }
ClassLoader classLoader = ShadowActivityThread.class.getClassLoader();
Class<?> iPackageManagerClass;
try {
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowAppOpsManager.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowAppOpsManager.java
index b18c9ad..841af36 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowAppOpsManager.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowAppOpsManager.java
@@ -49,6 +49,12 @@
mOpToKeyToMode.put(code, key, mode);
}
+ /** Convenience method to get the mode directly instead of wrapped in an op list. */
+ public int getMode(int code, int uid, String packageName) {
+ Integer mode = mOpToKeyToMode.get(code, new InternalKey(uid, packageName));
+ return mode == null ? AppOpsManager.opToDefaultMode(code) : mode;
+ }
+
@Implementation
protected List<PackageOps> getPackagesForOps(int[] ops) {
if (ops == null) {
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java
index b5893cb..8215a8b 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowCarWifiManager.java
@@ -41,6 +41,8 @@
private static CarWifiManager sInstance;
private static int sCurrentState = STATE_UNKNOWN;
private static WifiConfiguration sWifiConfiguration = new WifiConfiguration();
+ private static boolean sIsDualModeSupported = true;
+ private static boolean sIsDualBandSupported = true;
public static void setInstance(CarWifiManager wifiManager) {
sInstance = wifiManager;
@@ -51,6 +53,8 @@
sInstance = null;
sWifiConfiguration = new WifiConfiguration();
sCurrentState = STATE_UNKNOWN;
+ sIsDualModeSupported = true;
+ sIsDualBandSupported = true;
}
@Implementation
@@ -122,8 +126,26 @@
}
@Implementation
- public void connectToSavedWifi(AccessPoint accessPoint, WifiManager.ActionListener listener) {
- sInstance.connectToSavedWifi(accessPoint, listener);
+ protected boolean isDualModeSupported() {
+ return sIsDualModeSupported;
+ }
+
+ @Implementation
+ protected String getCountryCode() {
+ return "1";
+ }
+
+ @Implementation
+ protected boolean isDualBandSupported() {
+ return sIsDualBandSupported;
+ }
+
+ public static void setIsDualModeSupported(boolean supported) {
+ sIsDualModeSupported = supported;
+ }
+
+ public static void setIsDualBandSupported(boolean supported) {
+ sIsDualBandSupported = supported;
}
public static int getCurrentState() {
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowLockPatternUtils.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowLockPatternUtils.java
index 37fe981..8f819c3 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowLockPatternUtils.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowLockPatternUtils.java
@@ -16,16 +16,28 @@
package com.android.car.settings.testutils;
+import android.app.admin.DevicePolicyManager;
+
import com.android.internal.widget.LockPatternUtils;
+import com.android.internal.widget.LockPatternView;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shadow for LockPatternUtils.
+ */
@Implements(LockPatternUtils.class)
public class ShadowLockPatternUtils {
private static LockPatternUtils sInstance;
+ private static int sPasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+ private static byte[] sSavedPassword;
+ private static List<LockPatternView.Cell> sSavedPattern;
public static void setInstance(LockPatternUtils lockPatternUtils) {
sInstance = lockPatternUtils;
@@ -34,10 +46,51 @@
@Resetter
public static void reset() {
sInstance = null;
+ sPasswordQuality = DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+ sSavedPassword = null;
+ sSavedPattern = null;
+ }
+
+ /**
+ * Sets the current password quality that is returned by
+ * {@link LockPatternUtils#getKeyguardStoredPasswordQuality}.
+ */
+ public static void setPasswordQuality(int passwordQuality) {
+ sPasswordQuality = passwordQuality;
+ }
+
+ /**
+ * Returns the password saved by a call to {@link LockPatternUtils#saveLockPassword}.
+ */
+ public static byte[] getSavedPassword() {
+ return sSavedPassword;
+ }
+
+ /**
+ * Returns the pattern saved by a call to {@link LockPatternUtils#saveLockPattern}.
+ */
+ public static List<LockPatternView.Cell> getSavedPattern() {
+ return sSavedPattern;
}
@Implementation
protected void clearLock(byte[] savedCredential, int userHandle) {
sInstance.clearLock(savedCredential, userHandle);
}
+
+ @Implementation
+ public int getKeyguardStoredPasswordQuality(int userHandle) {
+ return sPasswordQuality;
+ }
+
+ @Implementation
+ public void saveLockPassword(byte[] password, byte[] savedPassword, int requestedQuality,
+ int userHandler) {
+ sSavedPassword = password;
+ }
+
+ @Implementation
+ public void saveLockPattern(List<LockPatternView.Cell> pattern, int userId) {
+ sSavedPattern = new ArrayList<>(pattern);
+ }
}
diff --git a/tests/robotests/src/com/android/car/settings/wifi/WifiTetherApBandPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/wifi/WifiTetherApBandPreferenceControllerTest.java
new file mode 100644
index 0000000..43fa83e
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/wifi/WifiTetherApBandPreferenceControllerTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2019 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.car.settings.wifi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.net.wifi.WifiConfiguration;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.ListPreference;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.R;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
+import com.android.car.settings.testutils.ShadowCarWifiManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowCarWifiManager.class})
+public class WifiTetherApBandPreferenceControllerTest {
+
+ private Context mContext;
+ private ListPreference mPreference;
+ private PreferenceControllerTestHelper<WifiTetherApBandPreferenceController>
+ mControllerHelper;
+ private CarWifiManager mCarWifiManager;
+ private WifiTetherApBandPreferenceController mController;
+
+ @Before
+ public void setup() {
+ mContext = RuntimeEnvironment.application;
+ mCarWifiManager = new CarWifiManager(mContext);
+ mPreference = new ListPreference(mContext);
+ mControllerHelper =
+ new PreferenceControllerTestHelper<WifiTetherApBandPreferenceController>(mContext,
+ WifiTetherApBandPreferenceController.class, mPreference);
+ mController = mControllerHelper.getController();
+ }
+
+ @After
+ public void tearDown() {
+ ShadowCarWifiManager.reset();
+ }
+
+ @Test
+ public void onStart_5GhzBandNotSupported_preferenceIsNotEnabled() {
+ ShadowCarWifiManager.setIsDualBandSupported(false);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(!mPreference.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void onStart_5GhzBandNotSupported_summarySetToChoose2Ghz() {
+ ShadowCarWifiManager.setIsDualBandSupported(false);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(mPreference.getSummary())
+ .isEqualTo(mContext.getString(R.string.wifi_ap_choose_2G));
+ }
+
+ @Test
+ public void onStart_5GhzBandIsSupported_preferenceIsEnabled() {
+ ShadowCarWifiManager.setIsDualBandSupported(true);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void onStart_wifiConfigApBandSetTo2Ghz_valueIsSetTo2Ghz() {
+ ShadowCarWifiManager.setIsDualBandSupported(true);
+ WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_2GHZ;
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(mPreference.getValue())
+ .isEqualTo(Integer.toString(WifiConfiguration.AP_BAND_2GHZ));
+ }
+
+ @Test
+ public void onStart_wifiConfigApBandSetTo5Ghz_valueIsSetTo5Ghz() {
+ ShadowCarWifiManager.setIsDualBandSupported(true);
+ WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_5GHZ;
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(mPreference.getValue())
+ .isEqualTo(Integer.toString(WifiConfiguration.AP_BAND_5GHZ));
+ }
+
+ @Test
+ public void onPreferenceChangedTo5Ghz_updatesApBandConfigTo5Ghz() {
+ ShadowCarWifiManager.setIsDualBandSupported(true);
+ ShadowCarWifiManager.setIsDualModeSupported(false);
+ WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_2GHZ;
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ mController.handlePreferenceChanged(mPreference,
+ Integer.toString(WifiConfiguration.AP_BAND_5GHZ));
+
+ assertThat(mCarWifiManager.getWifiApConfig().apBand)
+ .isEqualTo(WifiConfiguration.AP_BAND_5GHZ);
+ }
+
+ @Test
+ public void onPreferenceChangedTo2Ghz_updatesApBandConfigTo2Ghz() {
+ ShadowCarWifiManager.setIsDualBandSupported(true);
+ ShadowCarWifiManager.setIsDualModeSupported(false);
+ WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_5GHZ;
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ mController.handlePreferenceChanged(mPreference,
+ Integer.toString(WifiConfiguration.AP_BAND_2GHZ));
+
+ assertThat(mCarWifiManager.getWifiApConfig().apBand)
+ .isEqualTo(WifiConfiguration.AP_BAND_2GHZ);
+ }
+
+ @Test
+ public void onStart_dualModeIsSupported_summarySetToPrefer5Ghz() {
+ ShadowCarWifiManager.setIsDualBandSupported(true);
+ ShadowCarWifiManager.setIsDualModeSupported(true);
+ WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_5GHZ;
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ assertThat(mPreference.getSummary()).isEqualTo(
+ mContext.getString(R.string.wifi_ap_prefer_5G));
+ }
+
+ @Test
+ public void onPreferenceChangedTo5Ghz_dualModeIsSupported_defaultToApBandAny() {
+ ShadowCarWifiManager.setIsDualBandSupported(true);
+ ShadowCarWifiManager.setIsDualModeSupported(true);
+ WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_2GHZ;
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ mController.handlePreferenceChanged(mPreference,
+ Integer.toString(WifiConfiguration.AP_BAND_5GHZ));
+
+ assertThat(mCarWifiManager.getWifiApConfig().apBand)
+ .isEqualTo(WifiConfiguration.AP_BAND_ANY);
+ }
+
+}
diff --git a/tests/robotests/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceControllerTest.java
index de99482..613525b 100644
--- a/tests/robotests/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/car/settings/wifi/WifiTetherPasswordPreferenceControllerTest.java
@@ -19,15 +19,20 @@
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
import android.net.wifi.WifiConfiguration;
import android.text.InputType;
+import android.text.TextUtils;
import androidx.lifecycle.Lifecycle;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.android.car.settings.CarSettingsRobolectricTestRunner;
import com.android.car.settings.common.PreferenceControllerTestHelper;
import com.android.car.settings.common.ValidatedEditTextPreference;
import com.android.car.settings.testutils.ShadowCarWifiManager;
+import com.android.car.settings.testutils.ShadowLocalBroadcastManager;
import org.junit.After;
import org.junit.Before;
@@ -35,10 +40,9 @@
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
-import org.robolectric.shadow.api.Shadow;
@RunWith(CarSettingsRobolectricTestRunner.class)
-@Config(shadows = {ShadowCarWifiManager.class})
+@Config(shadows = {ShadowCarWifiManager.class, ShadowLocalBroadcastManager.class})
public class WifiTetherPasswordPreferenceControllerTest {
private static final String TEST_PASSWORD = "TEST_PASSWORD";
@@ -48,37 +52,62 @@
private PreferenceControllerTestHelper<WifiTetherPasswordPreferenceController>
mControllerHelper;
private CarWifiManager mCarWifiManager;
+ private LocalBroadcastManager mLocalBroadcastManager;
+ private WifiTetherPasswordPreferenceController mController;
@Before
public void setup() {
mContext = RuntimeEnvironment.application;
mCarWifiManager = new CarWifiManager(mContext);
+ mLocalBroadcastManager = LocalBroadcastManager.getInstance(mContext);
mPreference = new ValidatedEditTextPreference(mContext);
mControllerHelper =
new PreferenceControllerTestHelper<WifiTetherPasswordPreferenceController>(mContext,
WifiTetherPasswordPreferenceController.class, mPreference);
+ mController = mControllerHelper.getController();
}
@After
public void tearDown() {
ShadowCarWifiManager.reset();
+ ShadowLocalBroadcastManager.reset();
+ SharedPreferences sp = mContext.getSharedPreferences(
+ WifiTetherPasswordPreferenceController.SHARED_PREFERENCE_PATH,
+ Context.MODE_PRIVATE);
+ sp.edit().remove(WifiTetherPasswordPreferenceController.KEY_SAVED_PASSWORD).commit();
}
@Test
- public void onStart_wifiConfigHasPassword_setsSummary() {
+ public void onStart_securityTypeIsNotNone_visibilityIsSetToTrue() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = null;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(mPreference.isVisible()).isTrue();
+ }
+
+ @Test
+ public void onStart_securityTypeIsNotNone_wifiConfigHasPassword_setsPasswordAsSummary() {
WifiConfiguration config = new WifiConfiguration();
config.preSharedKey = TEST_PASSWORD;
- getShadowCarWifiManager().setWifiApConfig(config);
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
assertThat(mPreference.getSummary()).isEqualTo(TEST_PASSWORD);
}
@Test
- public void onStart_wifiConfigHasPassword_obscuresSummary() {
+ public void onStart_securityTypeIsNotNone_wifiConfigHasPassword_obscuresSummary() {
WifiConfiguration config = new WifiConfiguration();
config.preSharedKey = TEST_PASSWORD;
- getShadowCarWifiManager().setWifiApConfig(config);
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
assertThat(mPreference.getSummaryInputType())
@@ -86,16 +115,137 @@
}
@Test
- public void onStart_wifiConfigHasNoPassword_doesNotObscureSummary() {
+ public void onStart_securityTypeIsNotNone_wifiConfigHasNoPassword_passwordIsNotEmpty() {
WifiConfiguration config = new WifiConfiguration();
- config.preSharedKey = null;
- getShadowCarWifiManager().setWifiApConfig(config);
+ config.preSharedKey = "";
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
- assertThat(mPreference.getSummaryInputType()).isEqualTo(InputType.TYPE_CLASS_TEXT);
+ assertThat(!TextUtils.isEmpty(mPreference.getSummary())).isTrue();
}
- private ShadowCarWifiManager getShadowCarWifiManager() {
- return Shadow.extract(mCarWifiManager);
+ @Test
+ public void onStart_securityTypeIsNotNone_wifiConfigHasNoPassword_obscuresSummary() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = "";
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(mPreference.getSummaryInputType())
+ .isEqualTo((InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD));
}
+
+ @Test
+ public void onStart_securityTypeIsNone_visibilityIsSetToFalse() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = null;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(!mPreference.isVisible()).isTrue();
+ }
+
+ @Test
+ public void onStart_receiverIsRegisteredOnLocalBroadcastManager() {
+ WifiConfiguration config = new WifiConfiguration();
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(
+ ShadowLocalBroadcastManager.getRegisteredBroadcastReceivers().size())
+ .isEqualTo(1);
+ }
+
+ @Test
+ public void onStop_receiverIsUnregisteredFromLocalBroadcastManager() {
+ WifiConfiguration config = new WifiConfiguration();
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+ assertThat(
+ ShadowLocalBroadcastManager.getRegisteredBroadcastReceivers().size())
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void onSecurityChangedToNone_visibilityIsFalse() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ Intent intent = new Intent(
+ WifiTetherSecurityPreferenceController.ACTION_SECURITY_TYPE_CHANGED);
+ intent.putExtra(WifiTetherSecurityPreferenceController.KEY_SECURITY_TYPE,
+ WifiConfiguration.KeyMgmt.NONE);
+ mLocalBroadcastManager.sendBroadcast(intent);
+
+ assertThat(mPreference.isVisible()).isFalse();
+ }
+
+ @Test
+ public void onSecurityChangedToWPA2PSK_visibilityIsTrue() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ Intent intent = new Intent(
+ WifiTetherSecurityPreferenceController.ACTION_SECURITY_TYPE_CHANGED);
+ intent.putExtra(WifiTetherSecurityPreferenceController.KEY_SECURITY_TYPE,
+ WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mLocalBroadcastManager.sendBroadcast(intent);
+
+ assertThat(mPreference.isVisible()).isTrue();
+ }
+
+ @Test
+ public void onChangePassword_updatesPassword() {
+ String oldPassword = "OLD_PASSWORD";
+ String newPassword = "NEW_PASSWORD";
+
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = oldPassword;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ mController.handlePreferenceChanged(mPreference, newPassword);
+ String passwordReturned = mCarWifiManager.getWifiApConfig().preSharedKey;
+
+ assertThat(passwordReturned).isEqualTo(newPassword);
+ }
+
+ @Test
+ public void onChangePassword_savesNewPassword() {
+ String oldPassword = "OLD_PASSWORD";
+ String newPassword = "NEW_PASSWORD";
+
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = oldPassword;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+ mController.handlePreferenceChanged(mPreference, newPassword);
+
+ SharedPreferences sp = mContext.getSharedPreferences(
+ WifiTetherPasswordPreferenceController.SHARED_PREFERENCE_PATH,
+ Context.MODE_PRIVATE);
+
+ String savedPassword = sp.getString(
+ WifiTetherPasswordPreferenceController.KEY_SAVED_PASSWORD, "");
+
+ assertThat(savedPassword).isEqualTo(newPassword);
+ }
+
}
diff --git a/tests/robotests/src/com/android/car/settings/wifi/WifiTetherSecurityPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/wifi/WifiTetherSecurityPreferenceControllerTest.java
new file mode 100644
index 0000000..67ac93e
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/wifi/WifiTetherSecurityPreferenceControllerTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2019 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.car.settings.wifi;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.wifi.WifiConfiguration;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.preference.ListPreference;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
+import com.android.car.settings.testutils.ShadowCarWifiManager;
+import com.android.car.settings.testutils.ShadowLocalBroadcastManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowCarWifiManager.class, ShadowLocalBroadcastManager.class})
+public class WifiTetherSecurityPreferenceControllerTest {
+
+ private Context mContext;
+ private ListPreference mPreference;
+ private PreferenceControllerTestHelper<WifiTetherSecurityPreferenceController>
+ mControllerHelper;
+ private CarWifiManager mCarWifiManager;
+ private LocalBroadcastManager mLocalBroadcastManager;
+ private WifiTetherSecurityPreferenceController mController;
+
+ @Before
+ public void setup() {
+ mContext = RuntimeEnvironment.application;
+ mCarWifiManager = new CarWifiManager(mContext);
+ mLocalBroadcastManager = LocalBroadcastManager.getInstance(mContext);
+ mPreference = new ListPreference(mContext);
+ mControllerHelper =
+ new PreferenceControllerTestHelper<WifiTetherSecurityPreferenceController>(mContext,
+ WifiTetherSecurityPreferenceController.class, mPreference);
+ mController = mControllerHelper.getController();
+ }
+
+ @After
+ public void tearDown() {
+ ShadowCarWifiManager.reset();
+ ShadowLocalBroadcastManager.reset();
+ SharedPreferences sp = mContext.getSharedPreferences(
+ WifiTetherPasswordPreferenceController.SHARED_PREFERENCE_PATH,
+ Context.MODE_PRIVATE);
+ sp.edit().remove(WifiTetherPasswordPreferenceController.KEY_SAVED_PASSWORD).commit();
+ }
+
+ @Test
+ public void onStart_securityTypeSetToNone_setsValueToNone() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = null;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(Integer.parseInt(mPreference.getValue()))
+ .isEqualTo(WifiConfiguration.KeyMgmt.NONE);
+ }
+
+ @Test
+ public void onStart_securityTypeSetToWPA2PSK_setsValueToWPA2PSK() {
+ String testPassword = "TEST_PASSWORD";
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = testPassword;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(Integer.parseInt(mPreference.getValue()))
+ .isEqualTo(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ }
+
+ @Test
+ public void onPreferenceChangedToNone_updatesSecurityTypeToNone() {
+ String testPassword = "TEST_PASSWORD";
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = testPassword;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ mController.handlePreferenceChanged(mPreference,
+ Integer.toString(WifiConfiguration.KeyMgmt.NONE));
+
+ assertThat(mCarWifiManager.getWifiApConfig().getAuthType())
+ .isEqualTo(WifiConfiguration.KeyMgmt.NONE);
+
+ }
+
+ @Test
+ public void onPreferenceChangedToWPA2PSK_updatesSecurityTypeToWPA2PSK() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = null;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ mController.handlePreferenceChanged(mPreference,
+ Integer.toString(WifiConfiguration.KeyMgmt.WPA2_PSK));
+
+ assertThat(mCarWifiManager.getWifiApConfig().getAuthType())
+ .isEqualTo(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ }
+
+ @Test
+ public void onPreferenceSwitchFromNoneToWPA2PSK_retrievesSavedPassword() {
+ String savedPassword = "SAVED_PASSWORD";
+ SharedPreferences sp = mContext.getSharedPreferences(
+ WifiTetherPasswordPreferenceController.SHARED_PREFERENCE_PATH,
+ Context.MODE_PRIVATE);
+ sp.edit().putString(WifiTetherPasswordPreferenceController.KEY_SAVED_PASSWORD,
+ savedPassword).commit();
+
+ WifiConfiguration config = new WifiConfiguration();
+ config.preSharedKey = null;
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ mController.handlePreferenceChanged(mPreference,
+ Integer.toString(WifiConfiguration.KeyMgmt.WPA2_PSK));
+
+ assertThat(mCarWifiManager.getWifiApConfig().preSharedKey).isEqualTo(savedPassword);
+ }
+
+ @Test
+ public void onPreferenceChanged_broadcastsExactlyOneIntent() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ int newSecurityType = WifiConfiguration.KeyMgmt.WPA2_PSK;
+ mController.handlePreferenceChanged(mPreference, newSecurityType);
+
+ assertThat(ShadowLocalBroadcastManager.getSentBroadcastIntents().size()).isEqualTo(1);
+ }
+
+ @Test
+ public void onPreferenceChangedToWPA2PSK_broadcastsSecurityTypeWPA2PSK() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ int newSecurityType = WifiConfiguration.KeyMgmt.WPA2_PSK;
+
+ mController.handlePreferenceChanged(mPreference, newSecurityType);
+
+ Intent expectedIntent = new Intent(
+ WifiTetherSecurityPreferenceController.ACTION_SECURITY_TYPE_CHANGED);
+ expectedIntent.putExtra(WifiTetherSecurityPreferenceController.KEY_SECURITY_TYPE,
+ newSecurityType);
+
+ assertThat(
+ ShadowLocalBroadcastManager.getSentBroadcastIntents().get(0).toString())
+ .isEqualTo(expectedIntent.toString());
+ }
+
+ @Test
+ public void onPreferenceChangedToNone_broadcastsSecurityTypeNone() {
+ WifiConfiguration config = new WifiConfiguration();
+ config.allowedKeyManagement.clear();
+ config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA2_PSK);
+ mCarWifiManager.setWifiApConfig(config);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ int newSecurityType = WifiConfiguration.KeyMgmt.NONE;
+
+ mController.handlePreferenceChanged(mPreference, newSecurityType);
+
+ Intent expectedIntent = new Intent(
+ WifiTetherSecurityPreferenceController.ACTION_SECURITY_TYPE_CHANGED);
+ expectedIntent.putExtra(WifiTetherSecurityPreferenceController.KEY_SECURITY_TYPE,
+ newSecurityType);
+
+ assertThat(
+ ShadowLocalBroadcastManager.getSentBroadcastIntents().get(0).toString())
+ .isEqualTo(expectedIntent.toString());
+ }
+}