Merge "Override phone settings from CarSettings." into pi-car-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 569d7ad..60838ed 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -160,10 +160,25 @@
</intent-filter>
<intent-filter android:priority="100">
+ <action android:name="android.settings.action.MANAGE_WRITE_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <intent-filter android:priority="100">
<action android:name="android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
+ <intent-filter android:priority="100">
+ <action android:name="android.settings.USAGE_ACCESS_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
+ <intent-filter android:priority="100">
+ <action android:name="android.settings.STORAGE_VOLUME_ACCESS_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+
<intent-filter android:priority="1">
<action android:name="android.settings.DATE_SETTINGS" />
<action android:name="android.intent.action.QUICK_CLOCK" />
@@ -238,6 +253,16 @@
<meta-data android:name="distractionOptimized" android:value="true"/>
</activity>
+ <activity android:name=".bluetooth.BluetoothDevicePickerActivity"
+ android:label="@string/bluetooth_device_picker"
+ android:configChanges="orientation|keyboardHidden|screenSize"
+ android:clearTaskOnLaunch="true">
+ <intent-filter>
+ <action android:name="android.bluetooth.devicepicker.action.LAUNCH" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
<activity android:name=".accounts.AddAccountActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:configChanges="orientation|keyboardHidden|screenSize"/>
diff --git a/res/values/config.xml b/res/values/config.xml
index 21b7908..a2e0ae3 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -26,6 +26,14 @@
<bool name="config_show_regulatory_info">true</bool>
<!-- Whether premium SMS should be shown or not. -->
<bool name="config_show_premium_sms">true</bool>
+ <!-- Whether all preferences should always ignore UX Restrictions -->
+ <bool name="config_always_ignore_ux_restrictions">false</bool>
+ <!-- Array of Preference Keys that ignore UX Restrictions -->
+ <string-array name="config_ignore_ux_restrictions">
+ <item>@string/pk_display_settings_entry</item>
+ <item>@string/pk_sound_settings_entry</item>
+ <item>@string/pk_storage_settings_entry</item>
+ </string-array>
<!-- The component which listens for the enabling of developer options. -->
<string name="config_dev_options_module" translatable="false">com.android.car.developeroptions/.Settings$DevelopmentSettingsDashboardActivity</string>
diff --git a/res/values/preference_keys.xml b/res/values/preference_keys.xml
index 822a49c..cf33c99 100644
--- a/res/values/preference_keys.xml
+++ b/res/values/preference_keys.xml
@@ -108,6 +108,7 @@
</string>
<string name="pk_bluetooth_device_address" translatable="false">bluetooth_device_address
</string>
+ <string name="pk_bluetooth_device_picker" translatable="false">bluetooth_device_picker</string>
<!-- Applications and Notifications Settings -->
<string name="pk_applications_settings_screen_entry" translatable="false">
@@ -191,6 +192,8 @@
<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_directory_access_entry" translatable="false">directory_access_entry</string>
+ <string name="pk_directory_access" translatable="false">directory_access</string>
<string name="pk_directory_access_details" translatable="false">directory_access_details
</string>
<string name="pk_directory_access_details_app" translatable="false">
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3b36795..3b38e1e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -315,6 +315,8 @@
<string name="bluetooth_notif_title">Pairing request</string>
<!-- Notification message when a Bluetooth device wants to pair with us -->
<string name="bluetooth_notif_message">Tap to pair with <xliff:g id="device_name">%1$s</xliff:g>.</string>
+ <!-- Title for page which supports selecting a Bluetooth device from other applications. [CHAR_LIMIT=40]-->
+ <string name="bluetooth_device_picker">Choose Bluetooth device</string>
<!-- Language settings screen heading. [CHAR LIMIT=30] -->
<string name="language_settings">Languages</string>
diff --git a/res/xml/add_wifi_fragment.xml b/res/xml/add_wifi_fragment.xml
index c164f77..d0eafa3 100644
--- a/res/xml/add_wifi_fragment.xml
+++ b/res/xml/add_wifi_fragment.xml
@@ -30,7 +30,7 @@
android:persistent="false"
android:title="@string/wifi_security"
settings:controller="com.android.car.settings.wifi.NetworkSecurityPreferenceController"/>
- <com.android.car.settings.common.PasswordEditTextPreference
+ <com.android.car.settings.wifi.NetworkNameRestrictedPasswordEditTextPreference
android:dialogTitle="@string/wifi_password"
android:key="@string/pk_add_wifi_password"
android:persistent="false"
diff --git a/res/xml/bluetooth_device_picker_fragment.xml b/res/xml/bluetooth_device_picker_fragment.xml
new file mode 100644
index 0000000..95dcacd
--- /dev/null
+++ b/res/xml/bluetooth_device_picker_fragment.xml
@@ -0,0 +1,25 @@
+<?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/bluetooth_device_picker">
+ <com.android.car.settings.common.LogicalPreferenceGroup
+ android:key="@string/pk_bluetooth_device_picker"
+ settings:controller="com.android.car.settings.bluetooth.BluetoothDevicePickerPreferenceController"/>
+</PreferenceScreen>
diff --git a/res/xml/directory_access_fragment.xml b/res/xml/directory_access_fragment.xml
new file mode 100644
index 0000000..ff5d2cb
--- /dev/null
+++ b/res/xml/directory_access_fragment.xml
@@ -0,0 +1,25 @@
+<?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/directory_access_title">
+ <com.android.car.settings.common.LogicalPreferenceGroup
+ android:key="@string/pk_directory_access"
+ settings:controller="com.android.car.settings.applications.specialaccess.DirectoryAccessPreferenceController"/>
+</PreferenceScreen>
diff --git a/res/xml/homepage_fragment.xml b/res/xml/homepage_fragment.xml
index 10e3e2d..75944e5 100644
--- a/res/xml/homepage_fragment.xml
+++ b/res/xml/homepage_fragment.xml
@@ -28,12 +28,14 @@
android:fragment="com.android.car.settings.display.DisplaySettingsFragment"
android:icon="@drawable/ic_settings_display"
android:key="@string/pk_display_settings_entry"
- android:title="@string/display_settings"/>
+ android:title="@string/display_settings"
+ settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
<Preference
android:fragment="com.android.car.settings.sound.SoundSettingsFragment"
android:icon="@drawable/ic_settings_sound"
android:key="@string/pk_sound_settings_entry"
- android:title="@string/sound_settings"/>
+ android:title="@string/sound_settings"
+ settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
<Preference
android:fragment="com.android.car.settings.network.NetworkAndInternetFragment"
android:icon="@drawable/ic_settings_wifi"
@@ -87,7 +89,8 @@
android:fragment="com.android.car.settings.storage.StorageSettingsFragment"
android:icon="@drawable/ic_storage"
android:key="@string/pk_storage_settings_entry"
- android:title="@string/storage_settings_title"/>
+ android:title="@string/storage_settings_title"
+ settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
<Preference
android:icon="@drawable/ic_lock"
android:key="@string/pk_security_settings_entry"
diff --git a/res/xml/special_access_fragment.xml b/res/xml/special_access_fragment.xml
index a9abce1..ecae227 100644
--- a/res/xml/special_access_fragment.xml
+++ b/res/xml/special_access_fragment.xml
@@ -40,6 +40,11 @@
android:title="@string/usage_access_title"
settings:controller="com.android.car.settings.common.DefaultRestrictionsPreferenceController"/>
<Preference
+ android:fragment="com.android.car.settings.applications.specialaccess.DirectoryAccessFragment"
+ android:key="@string/pk_directory_access_entry"
+ android:title="@string/directory_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"
diff --git a/src/com/android/car/settings/applications/specialaccess/DirectoryAccessFragment.java b/src/com/android/car/settings/applications/specialaccess/DirectoryAccessFragment.java
new file mode 100644
index 0000000..cb6394b
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/DirectoryAccessFragment.java
@@ -0,0 +1,32 @@
+/*
+ * 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;
+import com.android.car.settings.common.SettingsFragment;
+
+/** Displays the list of applications which have been denied external storage directory access. */
+public class DirectoryAccessFragment extends SettingsFragment {
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.directory_access_fragment;
+ }
+}
diff --git a/src/com/android/car/settings/applications/specialaccess/DirectoryAccessPreferenceController.java b/src/com/android/car/settings/applications/specialaccess/DirectoryAccessPreferenceController.java
new file mode 100644
index 0000000..60c9a22
--- /dev/null
+++ b/src/com/android/car/settings/applications/specialaccess/DirectoryAccessPreferenceController.java
@@ -0,0 +1,162 @@
+/*
+ * 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 android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY;
+import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES;
+import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS;
+import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.ArraySet;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.car.settings.common.PreferenceController;
+import com.android.settingslib.applications.ApplicationsState.AppEntry;
+import com.android.settingslib.applications.ApplicationsState.AppFilter;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Displays a list of preferences for apps that have directory access permissions set. Selecting an
+ * app launches a detailed view for controlling permissions at the directory level.
+ */
+public class DirectoryAccessPreferenceController extends PreferenceController<PreferenceGroup> {
+
+ private static final Logger LOG = new Logger(DirectoryAccessPreferenceController.class);
+
+ private static final AppFilter FILTER_APP_HAS_DIRECTORY_ACCESS = new AppFilter() {
+
+ private Set<String> mPackages;
+
+ @Override
+ public void init() {
+ throw new UnsupportedOperationException("Need to call constructor that takes context");
+ }
+
+ @Override
+ public void init(Context context) {
+ mPackages = null;
+ Uri providerUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(TABLE_PACKAGES)
+ .appendPath("*")
+ .build();
+ try (Cursor cursor = context.getContentResolver().query(providerUri,
+ TABLE_PACKAGES_COLUMNS, /* queryArgs= */ null, /* cancellationSignal= */
+ null)) {
+ if (cursor == null) {
+ LOG.w("Didn't get cursor for " + providerUri);
+ return;
+ }
+ int count = cursor.getCount();
+ if (count == 0) {
+ LOG.d("No packages anymore (was " + mPackages + ")");
+ return;
+ }
+ mPackages = new ArraySet<>(count);
+ while (cursor.moveToNext()) {
+ mPackages.add(cursor.getString(TABLE_PACKAGES_COL_PACKAGE));
+ }
+ LOG.d("init(): " + mPackages);
+ }
+ }
+
+
+ @Override
+ public boolean filterApp(AppEntry info) {
+ return mPackages != null && mPackages.contains(info.info.packageName);
+ }
+ };
+
+ private final AppEntryListManager.Callback mCallback = new AppEntryListManager.Callback() {
+ @Override
+ public void onAppEntryListChanged(List<AppEntry> entries) {
+ mEntries = entries;
+ refreshUi();
+ }
+ };
+
+ @VisibleForTesting
+ AppEntryListManager mAppEntryListManager;
+ private List<AppEntry> mEntries;
+
+ public DirectoryAccessPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ mAppEntryListManager = new AppEntryListManager(context);
+ }
+
+ @Override
+ protected Class<PreferenceGroup> getPreferenceType() {
+ return PreferenceGroup.class;
+ }
+
+ @Override
+ protected void onCreateInternal() {
+ mAppEntryListManager.init(/* extraInfoBridge= */ null,
+ () -> FILTER_APP_HAS_DIRECTORY_ACCESS, mCallback);
+ }
+
+ @Override
+ protected void onStartInternal() {
+ mAppEntryListManager.start();
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mAppEntryListManager.stop();
+ }
+
+ @Override
+ protected void onDestroyInternal() {
+ mAppEntryListManager.destroy();
+ }
+
+ @Override
+ protected void updateState(PreferenceGroup preference) {
+ if (mEntries == null) {
+ // Still loading.
+ return;
+ }
+ preference.removeAll();
+ for (AppEntry entry : mEntries) {
+ Preference appPreference = new Preference(getContext());
+ String key = entry.info.packageName + "|" + entry.info.uid;
+ appPreference.setKey(key);
+ appPreference.setTitle(entry.label);
+ appPreference.setIcon(entry.icon);
+ appPreference.setOnPreferenceClickListener(clickedPref -> {
+ getFragmentController().launchFragment(
+ DirectoryAccessDetailsFragment.getInstance(entry.info.packageName));
+ return true;
+ });
+ preference.addPreference(appPreference);
+ }
+ }
+}
diff --git a/src/com/android/car/settings/bluetooth/BluetoothDevicePickerActivity.java b/src/com/android/car/settings/bluetooth/BluetoothDevicePickerActivity.java
new file mode 100644
index 0000000..db89075
--- /dev/null
+++ b/src/com/android/car/settings/bluetooth/BluetoothDevicePickerActivity.java
@@ -0,0 +1,36 @@
+/*
+ * 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.bluetooth;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.android.car.settings.common.BaseCarSettingsActivity;
+
+/**
+ * Displays a list of Bluetooth devices at the request of another application. When a user selects a
+ * device from the list, its details are returned to the requester. See {@link
+ * android.bluetooth.BluetoothDevicePicker}.
+ */
+public class BluetoothDevicePickerActivity extends BaseCarSettingsActivity {
+
+ @Nullable
+ @Override
+ protected Fragment getInitialFragment() {
+ return new BluetoothDevicePickerFragment();
+ }
+}
diff --git a/src/com/android/car/settings/bluetooth/BluetoothDevicePickerFragment.java b/src/com/android/car/settings/bluetooth/BluetoothDevicePickerFragment.java
new file mode 100644
index 0000000..658690d
--- /dev/null
+++ b/src/com/android/car/settings/bluetooth/BluetoothDevicePickerFragment.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.bluetooth;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import androidx.annotation.XmlRes;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+/**
+ * Hosts {@link BluetoothDevicePickerPreferenceController} to display the list of Bluetooth
+ * devices. The progress bar is shown while this fragment is visible to indicate discovery or
+ * pairing progress.
+ */
+public class BluetoothDevicePickerFragment extends SettingsFragment {
+
+ private LocalBluetoothManager mManager;
+ private ProgressBar mProgressBar;
+
+ @Override
+ @XmlRes
+ protected int getPreferenceScreenResId() {
+ return R.xml.bluetooth_device_picker_fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mManager = BluetoothUtils.getLocalBtManager(context);
+ if (mManager == null) {
+ goBack();
+ return;
+ }
+
+ use(BluetoothDevicePickerPreferenceController.class,
+ R.string.pk_bluetooth_device_picker).setLaunchIntent(requireActivity().getIntent());
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mProgressBar = requireActivity().findViewById(R.id.progress_bar);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mManager.setForegroundActivity(requireActivity());
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mManager.setForegroundActivity(null);
+ mProgressBar.setVisibility(View.GONE);
+ }
+}
diff --git a/src/com/android/car/settings/bluetooth/BluetoothDevicePickerPreferenceController.java b/src/com/android/car/settings/bluetooth/BluetoothDevicePickerPreferenceController.java
new file mode 100644
index 0000000..0c68eb5
--- /dev/null
+++ b/src/com/android/car/settings/bluetooth/BluetoothDevicePickerPreferenceController.java
@@ -0,0 +1,144 @@
+/*
+ * 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.bluetooth;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothDevicePicker;
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.Logger;
+import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+
+/**
+ * Displays a list of Bluetooth devices for the user to select. When a device is selected, a
+ * {@link BluetoothDevicePicker#ACTION_DEVICE_SELECTED} broadcast is sent containing {@link
+ * BluetoothDevice#EXTRA_DEVICE}.
+ *
+ * <p>This is useful to other application to obtain a device without needing to implement the UI.
+ * The activity hosting this controller should be launched with an intent as detailed in {@link
+ * BluetoothDevicePicker#ACTION_LAUNCH}. This controller will filter devices as specified by {@link
+ * BluetoothDevicePicker#EXTRA_FILTER_TYPE} and deliver the broadcast to the specified {@link
+ * BluetoothDevicePicker#EXTRA_LAUNCH_PACKAGE} {@link BluetoothDevicePicker#EXTRA_LAUNCH_CLASS}
+ * component. If authentication is required ({@link BluetoothDevicePicker#EXTRA_NEED_AUTH}), this
+ * controller will initiate pairing with the device and send the selected broadcast once the device
+ * successfully pairs. If no device is selected and this controller is destroyed, a broadcast with
+ * a {@code null} {@link BluetoothDevice#EXTRA_DEVICE} is sent.
+ */
+public class BluetoothDevicePickerPreferenceController extends
+ BluetoothScanningDevicesGroupPreferenceController {
+
+ private static final Logger LOG = new Logger(BluetoothDevicePickerPreferenceController.class);
+
+ private BluetoothDeviceFilter.Filter mFilter;
+
+ private boolean mNeedAuth;
+ private String mLaunchPackage;
+ private String mLaunchClass;
+
+ private CachedBluetoothDevice mSelectedDevice;
+
+ public BluetoothDevicePickerPreferenceController(Context context, String preferenceKey,
+ FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+ super(context, preferenceKey, fragmentController, uxRestrictions);
+ }
+
+ /**
+ * Sets the intent with which {@link BluetoothDevicePickerActivity} was launched. The intent
+ * may contain {@link BluetoothDevicePicker} extras to customize the selection list and specify
+ * the destination of the selected device. See {@link BluetoothDevicePicker#ACTION_LAUNCH}.
+ */
+ public void setLaunchIntent(Intent intent) {
+ mNeedAuth = intent.getBooleanExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, false);
+ mFilter = BluetoothDeviceFilter.getFilter(
+ intent.getIntExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE,
+ BluetoothDevicePicker.FILTER_TYPE_ALL));
+ mLaunchPackage = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE);
+ mLaunchClass = intent.getStringExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS);
+ }
+
+ @Override
+ protected void checkInitialized() {
+ if (mFilter == null) {
+ throw new IllegalStateException("launch intent must be set");
+ }
+ }
+
+ @Override
+ protected BluetoothDeviceFilter.Filter getDeviceFilter() {
+ return mFilter;
+ }
+
+ @Override
+ protected void onDeviceClickedInternal(CachedBluetoothDevice cachedDevice) {
+ mSelectedDevice = cachedDevice;
+ BluetoothUtils.persistSelectedDeviceInPicker(getContext(), cachedDevice.getAddress());
+
+ if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED || !mNeedAuth) {
+ sendDevicePickedIntent(cachedDevice.getDevice());
+ getFragmentController().goBack();
+ return;
+ }
+
+ if (cachedDevice.startPairing()) {
+ LOG.d("startPairing");
+ } else {
+ BluetoothUtils.showError(getContext(), cachedDevice.getName(),
+ R.string.bluetooth_pairing_error_message);
+ refreshUi();
+ }
+ }
+
+ @Override
+ protected void onStartInternal() {
+ super.onStartInternal();
+ mSelectedDevice = null;
+ }
+
+ @Override
+ protected void onDestroyInternal() {
+ super.onDestroyInternal();
+ if (mSelectedDevice == null) {
+ // Notify that no device was selected.
+ sendDevicePickedIntent(null);
+ }
+ }
+
+ @Override
+ public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
+ super.onDeviceBondStateChanged(cachedDevice, bondState);
+ if (bondState == BluetoothDevice.BOND_BONDED && cachedDevice.equals(mSelectedDevice)) {
+ sendDevicePickedIntent(mSelectedDevice.getDevice());
+ getFragmentController().goBack();
+ }
+ }
+
+ private void sendDevicePickedIntent(BluetoothDevice device) {
+ LOG.d("sendDevicePickedIntent device: " + device + " package: " + mLaunchPackage
+ + " class: " + mLaunchClass);
+ Intent intent = new Intent(BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
+ intent.putExtra(BluetoothDevice.EXTRA_DEVICE, device);
+ if (mLaunchPackage != null && mLaunchClass != null) {
+ intent.setClassName(mLaunchPackage, mLaunchClass);
+ }
+ getContext().sendBroadcast(intent);
+ }
+}
diff --git a/src/com/android/car/settings/bluetooth/BluetoothUtils.java b/src/com/android/car/settings/bluetooth/BluetoothUtils.java
index 6bda46b..731ee0b 100644
--- a/src/com/android/car/settings/bluetooth/BluetoothUtils.java
+++ b/src/com/android/car/settings/bluetooth/BluetoothUtils.java
@@ -161,6 +161,13 @@
return false;
}
+ static void persistSelectedDeviceInPicker(Context context, String deviceAddress) {
+ SharedPreferences.Editor editor = getSharedPreferences(context).edit();
+ editor.putString(KEY_LAST_SELECTED_DEVICE, deviceAddress);
+ editor.putLong(KEY_LAST_SELECTED_DEVICE_TIME, System.currentTimeMillis());
+ editor.apply();
+ }
+
public static LocalBluetoothManager getLocalBtManager(Context context) {
return LocalBluetoothManager.getInstance(context, mOnInitCallback);
}
diff --git a/src/com/android/car/settings/common/BaseCarSettingsActivity.java b/src/com/android/car/settings/common/BaseCarSettingsActivity.java
index 132ee60..97243af 100644
--- a/src/com/android/car/settings/common/BaseCarSettingsActivity.java
+++ b/src/com/android/car/settings/common/BaseCarSettingsActivity.java
@@ -169,7 +169,7 @@
@Override
public void onBackStackChanged() {
- updateBlockingView(getCurrentFragment());
+ onUxRestrictionsChanged(getCarUxRestrictions());
}
@Override
diff --git a/src/com/android/car/settings/common/FragmentResolver.java b/src/com/android/car/settings/common/FragmentResolver.java
index 8518825..4ae48da 100644
--- a/src/com/android/car/settings/common/FragmentResolver.java
+++ b/src/com/android/car/settings/common/FragmentResolver.java
@@ -32,7 +32,10 @@
import com.android.car.settings.applications.DefaultApplicationsSettingsFragment;
import com.android.car.settings.applications.assist.ManageAssistFragment;
import com.android.car.settings.applications.defaultapps.DefaultAutofillPickerFragment;
+import com.android.car.settings.applications.specialaccess.DirectoryAccessFragment;
+import com.android.car.settings.applications.specialaccess.ModifySystemSettingsFragment;
import com.android.car.settings.applications.specialaccess.NotificationAccessFragment;
+import com.android.car.settings.applications.specialaccess.UsageAccessFragment;
import com.android.car.settings.bluetooth.BluetoothSettingsFragment;
import com.android.car.settings.datausage.DataUsageFragment;
import com.android.car.settings.datetime.DatetimeSettingsFragment;
@@ -137,9 +140,18 @@
case Settings.ACTION_VOICE_INPUT_SETTINGS:
return new ManageAssistFragment();
+ case Settings.ACTION_MANAGE_WRITE_SETTINGS:
+ return new ModifySystemSettingsFragment();
+
case Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS:
return new NotificationAccessFragment();
+ case Settings.ACTION_USAGE_ACCESS_SETTINGS:
+ return new UsageAccessFragment();
+
+ case Settings.ACTION_STORAGE_VOLUME_ACCESS_SETTINGS:
+ return new DirectoryAccessFragment();
+
case Intent.ACTION_QUICK_CLOCK:
case Settings.ACTION_DATE_SETTINGS:
return new DatetimeSettingsFragment();
diff --git a/src/com/android/car/settings/common/PreferenceController.java b/src/com/android/car/settings/common/PreferenceController.java
index b8e0622..5dfe4ee 100644
--- a/src/com/android/car/settings/common/PreferenceController.java
+++ b/src/com/android/car/settings/common/PreferenceController.java
@@ -26,8 +26,13 @@
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
+import com.android.car.settings.R;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
/**
* Controller which encapsulates the business logic associated with a {@link Preference}. All car
@@ -114,6 +119,18 @@
*/
public static final int DISABLED_FOR_USER = 3;
+ /**
+ * Indicates whether all Preferences are configured to ignore UX Restrictions Event.
+ */
+ private final boolean mAlwaysIgnoreUxRestrictions;
+
+ /**
+ * Set of the keys of Preferences that ignore UX Restrictions. When mAlwaysIgnoreUxRestrictions
+ * is configured to be false, then only the Preferences whose keys are contained in this Set
+ * ignore UX Restrictions.
+ */
+ private final Set<String> mPreferencesIgnoringUxRestrictions;
+
private final Context mContext;
private final String mPreferenceKey;
private final FragmentController mFragmentController;
@@ -132,6 +149,10 @@
mPreferenceKey = preferenceKey;
mFragmentController = fragmentController;
mUxRestrictions = uxRestrictions;
+ mPreferencesIgnoringUxRestrictions = new HashSet<String>(Arrays.asList(
+ mContext.getResources().getStringArray(R.array.config_ignore_ux_restrictions)));
+ mAlwaysIgnoreUxRestrictions =
+ mContext.getResources().getBoolean(R.bool.config_always_ignore_ux_restrictions);
}
/**
@@ -413,7 +434,9 @@
* additional driving restrictions.
*/
protected void onApplyUxRestrictions(CarUxRestrictions uxRestrictions) {
- if (CarUxRestrictionsHelper.isNoSetup(uxRestrictions)) {
+ if (!isUxRestrictionsIgnored(mAlwaysIgnoreUxRestrictions,
+ mPreferencesIgnoringUxRestrictions)
+ && CarUxRestrictionsHelper.isNoSetup(uxRestrictions)) {
mPreference.setEnabled(false);
}
}
@@ -442,4 +465,8 @@
protected boolean handlePreferenceClicked(V preference) {
return false;
}
+
+ protected boolean isUxRestrictionsIgnored(boolean allIgnores, Set prefsThatIgnore) {
+ return allIgnores || prefsThatIgnore.contains(mPreferenceKey);
+ }
}
diff --git a/src/com/android/car/settings/quicksettings/QuickSettingFragment.java b/src/com/android/car/settings/quicksettings/QuickSettingFragment.java
index 5a091ef..1e81541 100644
--- a/src/com/android/car/settings/quicksettings/QuickSettingFragment.java
+++ b/src/com/android/car/settings/quicksettings/QuickSettingFragment.java
@@ -42,6 +42,9 @@
import com.android.car.settings.users.UserIconProvider;
import com.android.car.settings.users.UserSwitcherFragment;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@@ -50,6 +53,17 @@
public class QuickSettingFragment extends BaseFragment {
// Time to delay refreshing the build info, if the clock is not correct.
private static final long BUILD_INFO_REFRESH_TIME_MS = TimeUnit.SECONDS.toMillis(5);
+ /**
+ * Indicates whether all Preferences are configured to ignore UX Restrictions Event.
+ */
+ private boolean mAllIgnoresUxRestrictions;
+
+ /**
+ * Set of the keys of Preferences that ignore UX Restrictions. When mAlwaysIgnoreUxRestrictions
+ * is configured to be false, then only the Preferences whose keys are contained in this Set
+ * ignore UX Restrictions.
+ */
+ private Set<String> mPreferencesIgnoringUxRestrictions;
private CarUserManagerHelper mCarUserManagerHelper;
private UserIconProvider mUserIconProvider;
@@ -108,6 +122,11 @@
.addTile(new CelluarTile(activity, mGridAdapter))
.addSeekbarTile(new BrightnessTile(activity));
mListView.setAdapter(mGridAdapter);
+
+ mPreferencesIgnoringUxRestrictions = new HashSet<String>(Arrays.asList(
+ getContext().getResources().getStringArray(R.array.config_ignore_ux_restrictions)));
+ mAllIgnoresUxRestrictions =
+ getContext().getResources().getBoolean(R.bool.config_always_ignore_ux_restrictions);
}
@Override
@@ -185,7 +204,10 @@
@Override
public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
// TODO: update tiles
- applyRestriction(CarUxRestrictionsHelper.isNoSetup(restrictionInfo));
+ if (!hasPreferenceIgnoringUxRestrictions(mAllIgnoresUxRestrictions,
+ mPreferencesIgnoringUxRestrictions)) {
+ applyRestriction(CarUxRestrictionsHelper.isNoSetup(restrictionInfo));
+ }
}
private void applyRestriction(boolean restricted) {
@@ -209,4 +231,8 @@
}
}
}
+
+ private boolean hasPreferenceIgnoringUxRestrictions(boolean allIgnores, Set prefsThatIgnore) {
+ return allIgnores || prefsThatIgnore.size() > 0;
+ }
}
diff --git a/src/com/android/car/settings/wifi/NetworkNameRestrictedPasswordEditTextPreference.java b/src/com/android/car/settings/wifi/NetworkNameRestrictedPasswordEditTextPreference.java
new file mode 100644
index 0000000..8f52152
--- /dev/null
+++ b/src/com/android/car/settings/wifi/NetworkNameRestrictedPasswordEditTextPreference.java
@@ -0,0 +1,67 @@
+/*
+ * 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.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.widget.Toast;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.PasswordEditTextPreference;
+
+/**
+ * Custom {@link PasswordEditTextPreference} which doesn't open the password dialog unless the
+ * network name is provided.
+ */
+public class NetworkNameRestrictedPasswordEditTextPreference extends PasswordEditTextPreference {
+
+ private String mNetworkName;
+
+ public NetworkNameRestrictedPasswordEditTextPreference(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ public NetworkNameRestrictedPasswordEditTextPreference(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public NetworkNameRestrictedPasswordEditTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NetworkNameRestrictedPasswordEditTextPreference(Context context) {
+ super(context);
+ }
+
+ /** Sets the network name. */
+ public void setNetworkName(String name) {
+ mNetworkName = name;
+ }
+
+ @Override
+ protected void onClick() {
+ if (TextUtils.isEmpty(mNetworkName)) {
+ Toast.makeText(getContext(), R.string.wifi_no_network_name, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ super.onClick();
+ }
+}
diff --git a/src/com/android/car/settings/wifi/NetworkPasswordPreferenceController.java b/src/com/android/car/settings/wifi/NetworkPasswordPreferenceController.java
index 0b82f11..0616399 100644
--- a/src/com/android/car/settings/wifi/NetworkPasswordPreferenceController.java
+++ b/src/com/android/car/settings/wifi/NetworkPasswordPreferenceController.java
@@ -22,20 +22,18 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.text.TextUtils;
-import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.android.car.settings.R;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.Logger;
-import com.android.car.settings.common.PasswordEditTextPreference;
import com.android.car.settings.common.PreferenceController;
import com.android.settingslib.wifi.AccessPoint;
/** Business logic relating to the security type and associated password. */
public class NetworkPasswordPreferenceController extends
- PreferenceController<PasswordEditTextPreference> {
+ PreferenceController<NetworkNameRestrictedPasswordEditTextPreference> {
private static final Logger LOG = new Logger(NetworkPasswordPreferenceController.class);
@@ -43,6 +41,7 @@
@Override
public void onReceive(Context context, Intent intent) {
mNetworkName = intent.getStringExtra(NetworkNamePreferenceController.KEY_NETWORK_NAME);
+ getPreference().setNetworkName(mNetworkName);
refreshUi();
}
};
@@ -66,8 +65,8 @@
}
@Override
- protected Class<PasswordEditTextPreference> getPreferenceType() {
- return PasswordEditTextPreference.class;
+ protected Class<NetworkNameRestrictedPasswordEditTextPreference> getPreferenceType() {
+ return NetworkNameRestrictedPasswordEditTextPreference.class;
}
@Override
@@ -85,7 +84,7 @@
}
@Override
- protected void updateState(PasswordEditTextPreference preference) {
+ protected void updateState(NetworkNameRestrictedPasswordEditTextPreference preference) {
if (TextUtils.isEmpty(mNetworkName)) {
getPreference().setDialogTitle(R.string.wifi_password);
} else {
@@ -95,13 +94,8 @@
}
@Override
- protected boolean handlePreferenceChanged(PasswordEditTextPreference preference,
- Object newValue) {
- if (TextUtils.isEmpty(mNetworkName)) {
- Toast.makeText(getContext(), R.string.wifi_no_network_name, Toast.LENGTH_SHORT).show();
- return true;
- }
-
+ protected boolean handlePreferenceChanged(
+ NetworkNameRestrictedPasswordEditTextPreference preference, Object newValue) {
String password = newValue.toString();
int netId = WifiUtil.connectToAccessPoint(getContext(), mNetworkName, mSecurityType,
password, /* hidden= */ true);
diff --git a/tests/robotests/src/com/android/car/settings/applications/specialaccess/DirectoryAccessPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/applications/specialaccess/DirectoryAccessPreferenceControllerTest.java
new file mode 100644
index 0000000..3da0a44
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/applications/specialaccess/DirectoryAccessPreferenceControllerTest.java
@@ -0,0 +1,199 @@
+/*
+ * 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 android.os.storage.StorageVolume.ScopedAccessProviderContract.AUTHORITY;
+import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES;
+import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.net.Uri;
+import android.os.Looper;
+
+import androidx.fragment.app.Fragment;
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+
+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.ShadowApplicationsState;
+import com.android.car.settings.testutils.ShadowContentResolver;
+import com.android.settingslib.applications.ApplicationsState;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+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.fakes.BaseCursor;
+import org.robolectric.shadow.api.Shadow;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/** Unit test for {@link DirectoryAccessPreferenceController}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowApplicationsState.class, ShadowContentResolver.class})
+public class DirectoryAccessPreferenceControllerTest {
+
+ @Mock
+ private AppEntryListManager mAppEntryListManager;
+ @Mock
+ private ApplicationsState mApplicationsState;
+ @Captor
+ private ArgumentCaptor<AppEntryListManager.AppFilterProvider> mFilterCaptor;
+ @Captor
+ private ArgumentCaptor<AppEntryListManager.Callback> mCallbackCaptor;
+
+ private Context mContext;
+ private PreferenceGroup mPreferenceGroup;
+ private PreferenceControllerTestHelper<DirectoryAccessPreferenceController> mControllerHelper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ ShadowApplicationsState.setInstance(mApplicationsState);
+ when(mApplicationsState.getBackgroundLooper()).thenReturn(Looper.getMainLooper());
+
+ mContext = RuntimeEnvironment.application;
+ mPreferenceGroup = new LogicalPreferenceGroup(mContext);
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ DirectoryAccessPreferenceController.class, mPreferenceGroup);
+ mControllerHelper.getController().mAppEntryListManager = mAppEntryListManager;
+ mControllerHelper.markState(Lifecycle.State.CREATED);
+ verify(mAppEntryListManager).init(isNull(), mFilterCaptor.capture(),
+ mCallbackCaptor.capture());
+ }
+
+ @After
+ public void tearDown() {
+ ShadowApplicationsState.reset();
+ ShadowContentResolver.reset();
+ }
+
+ @Test
+ public void onStart_startsListManager() {
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ verify(mAppEntryListManager).start();
+ }
+
+ @Test
+ public void onStop_stopsListManager() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+ verify(mAppEntryListManager).stop();
+ }
+
+ @Test
+ public void onDestroy_destroysListManager() {
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+ verify(mAppEntryListManager).destroy();
+ }
+
+ @Test
+ public void onAppEntryListChanged_addsPreferencesForEntries() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ List<ApplicationsState.AppEntry> entries = Arrays.asList(
+ createAppEntry("test.package", /* uid= */ 1),
+ createAppEntry("another.test.package", /* uid= */ 2));
+
+ mCallbackCaptor.getValue().onAppEntryListChanged(entries);
+
+ assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(2);
+ }
+
+ @Test
+ public void onPreferenceClicked_launchesDetailsFragmentForPackage() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ String packageName = "test.package";
+ List<ApplicationsState.AppEntry> entries = Collections.singletonList(
+ createAppEntry(packageName, /* uid= */ 1));
+ mCallbackCaptor.getValue().onAppEntryListChanged(entries);
+ Preference appPref = mPreferenceGroup.getPreference(0);
+
+ appPref.performClick();
+
+ ArgumentCaptor<Fragment> fragmentCaptor = ArgumentCaptor.forClass(Fragment.class);
+ verify(mControllerHelper.getMockFragmentController()).launchFragment(
+ fragmentCaptor.capture());
+ assertThat(fragmentCaptor.getValue()).isInstanceOf(DirectoryAccessDetailsFragment.class);
+ assertThat(fragmentCaptor.getValue().getArguments().getString(
+ DirectoryAccessDetailsFragment.ARG_PACKAGE_NAME)).isEqualTo(packageName);
+ }
+
+ @Test
+ public void appFilter_removesPackagesNotInScopedAccessProvider() {
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ String includedPackage = "test.package";
+ String excludedPackage = "test.package2";
+
+ BaseCursor cursor = mock(BaseCursor.class);
+ when(cursor.getCount()).thenReturn(1);
+ when(cursor.moveToNext()).thenReturn(true).thenReturn(false);
+ when(cursor.getString(TABLE_PACKAGES_COL_PACKAGE)).thenReturn(includedPackage);
+
+ Uri providerUri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(AUTHORITY)
+ .appendPath(TABLE_PACKAGES)
+ .appendPath("*")
+ .build();
+ getShadowContentResolver().setCursor(providerUri, cursor);
+
+ ApplicationsState.AppFilter filter = mFilterCaptor.getValue().getAppFilter();
+ filter.init(mContext);
+
+ assertThat(filter.filterApp(createAppEntry(includedPackage, /* uid= */ 1))).isTrue();
+ assertThat(filter.filterApp(createAppEntry(excludedPackage, /* uid= */ 2))).isFalse();
+ }
+
+ private ApplicationsState.AppEntry createAppEntry(String packageName, int uid) {
+ ApplicationInfo info = new ApplicationInfo();
+ info.packageName = packageName;
+ info.uid = uid;
+
+ ApplicationsState.AppEntry appEntry = mock(ApplicationsState.AppEntry.class);
+ appEntry.info = info;
+ appEntry.label = packageName;
+
+ return appEntry;
+ }
+
+ private ShadowContentResolver getShadowContentResolver() {
+ return Shadow.extract(mContext.getContentResolver());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothDevicePickerFragmentTest.java b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothDevicePickerFragmentTest.java
new file mode 100644
index 0000000..d6c877b
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothDevicePickerFragmentTest.java
@@ -0,0 +1,106 @@
+/*
+ * 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.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.R;
+import com.android.car.settings.testutils.FragmentController;
+import com.android.car.settings.testutils.ShadowBluetoothAdapter;
+import com.android.car.settings.testutils.ShadowBluetoothPan;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+/** Unit test for {@link BluetoothDevicePickerFragment}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowBluetoothAdapter.class, ShadowBluetoothPan.class})
+public class BluetoothDevicePickerFragmentTest {
+
+ private LocalBluetoothManager mLocalBluetoothManager;
+ private FragmentController<BluetoothDevicePickerFragment> mFragmentController;
+ private BluetoothDevicePickerFragment mFragment;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Context context = RuntimeEnvironment.application;
+ mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */
+ null);
+
+ mFragment = new BluetoothDevicePickerFragment();
+ mFragmentController = FragmentController.of(mFragment);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowBluetoothAdapter.reset();
+ }
+
+ @Test
+ public void onStart_setsBluetoothManagerForegroundActivity() {
+ mFragmentController.create().start();
+
+ assertThat(mLocalBluetoothManager.getForegroundActivity()).isEqualTo(
+ mFragment.requireActivity());
+ }
+
+ @Test
+ public void onStart_showsProgressBar() {
+ mFragmentController.create();
+ ProgressBar progressBar = findProgressBar(mFragment.requireActivity());
+ progressBar.setVisibility(View.GONE);
+
+ mFragmentController.start();
+
+ assertThat(progressBar.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @Test
+ public void onStop_clearsBluetoothManagerForegroundActivity() {
+ mFragmentController.create().start().resume().pause().stop();
+
+ assertThat(mLocalBluetoothManager.getForegroundActivity()).isNull();
+ }
+
+ @Test
+ public void onStop_hidesProgressBar() {
+ mFragmentController.setup().onPause();
+ ProgressBar progressBar = findProgressBar(mFragment.requireActivity());
+ progressBar.setVisibility(View.VISIBLE);
+
+ mFragmentController.stop();
+
+ assertThat(progressBar.getVisibility()).isEqualTo(View.GONE);
+ }
+
+ private ProgressBar findProgressBar(Activity activity) {
+ return activity.findViewById(R.id.progress_bar);
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothDevicePickerPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothDevicePickerPreferenceControllerTest.java
new file mode 100644
index 0000000..1c4934a4
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/bluetooth/BluetoothDevicePickerPreferenceControllerTest.java
@@ -0,0 +1,311 @@
+/*
+ * 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.bluetooth;
+
+import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothDevicePicker;
+import android.bluetooth.BluetoothUuid;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.ParcelUuid;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.PreferenceGroup;
+
+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.ShadowBluetoothAdapter;
+import com.android.car.settings.testutils.ShadowBluetoothPan;
+import com.android.car.settings.testutils.ShadowCarUserManagerHelper;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.Arrays;
+
+/** Unit test for {@link BluetoothDevicePickerPreferenceController}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowCarUserManagerHelper.class, ShadowBluetoothAdapter.class,
+ ShadowBluetoothPan.class})
+public class BluetoothDevicePickerPreferenceControllerTest {
+
+ @Mock
+ private CarUserManagerHelper mCarUserManagerHelper;
+ @Mock
+ private CachedBluetoothDevice mUnbondedCachedDevice;
+ @Mock
+ private BluetoothDevice mUnbondedDevice;
+ @Mock
+ private CachedBluetoothDevice mBondedCachedDevice;
+ @Mock
+ private BluetoothDevice mBondedDevice;
+ @Mock
+ private CachedBluetoothDeviceManager mCachedDeviceManager;
+ private CachedBluetoothDeviceManager mSaveRealCachedDeviceManager;
+ private LocalBluetoothManager mLocalBluetoothManager;
+ private PreferenceGroup mPreferenceGroup;
+ private PreferenceControllerTestHelper<BluetoothDevicePickerPreferenceController>
+ mControllerHelper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ ShadowCarUserManagerHelper.setMockInstance(mCarUserManagerHelper);
+ Context context = RuntimeEnvironment.application;
+
+ mLocalBluetoothManager = LocalBluetoothManager.getInstance(context, /* onInitCallback= */
+ null);
+ mSaveRealCachedDeviceManager = mLocalBluetoothManager.getCachedDeviceManager();
+ ReflectionHelpers.setField(mLocalBluetoothManager, "mCachedDeviceManager",
+ mCachedDeviceManager);
+
+ when(mUnbondedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+ when(mUnbondedCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE);
+ when(mUnbondedCachedDevice.getDevice()).thenReturn(mUnbondedDevice);
+ when(mBondedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ when(mBondedCachedDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED);
+ when(mBondedCachedDevice.getDevice()).thenReturn(mBondedDevice);
+ when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(
+ Arrays.asList(mUnbondedCachedDevice, mBondedCachedDevice));
+ // Make bonded device appear first in the list.
+ when(mBondedCachedDevice.compareTo(mUnbondedCachedDevice)).thenReturn(-1);
+ when(mUnbondedCachedDevice.compareTo(mBondedCachedDevice)).thenReturn(1);
+
+ // Make sure controller is available.
+ Shadows.shadowOf(context.getPackageManager()).setSystemFeature(
+ FEATURE_BLUETOOTH, /* supported= */ true);
+ BluetoothAdapter.getDefaultAdapter().enable();
+ getShadowBluetoothAdapter().setState(BluetoothAdapter.STATE_ON);
+
+ mPreferenceGroup = new LogicalPreferenceGroup(context);
+ mControllerHelper = new PreferenceControllerTestHelper<>(context,
+ BluetoothDevicePickerPreferenceController.class);
+ }
+
+ @After
+ public void tearDown() {
+ ShadowCarUserManagerHelper.reset();
+ ShadowBluetoothAdapter.reset();
+ ReflectionHelpers.setField(mLocalBluetoothManager, "mCachedDeviceManager",
+ mSaveRealCachedDeviceManager);
+ }
+
+ @Test
+ public void checkInitialized_noLaunchIntentSet_throwsIllegalStateException() {
+ assertThrows(IllegalStateException.class,
+ () -> mControllerHelper.setPreference(mPreferenceGroup));
+ }
+
+ @Test
+ public void onStart_appliesFilterType() {
+ // Setup device to pass the filter.
+ when(mBondedDevice.getUuids()).thenReturn(new ParcelUuid[]{BluetoothUuid.AudioSink});
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ false,
+ BluetoothDevicePicker.FILTER_TYPE_AUDIO, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
+
+ assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(1);
+ assertThat(((BluetoothDevicePreference) mPreferenceGroup.getPreference(
+ 0)).getCachedDevice()).isEqualTo(mBondedCachedDevice);
+ }
+
+ @Test
+ public void onDeviceClicked_bondedDevice_sendsPickedIntent() {
+ ComponentName component = new ComponentName("test.package", "TestClass");
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ true,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, component.getPackageName(),
+ component.getClassName());
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ BluetoothDevicePreference devicePreference =
+ (BluetoothDevicePreference) mPreferenceGroup.getPreference(0);
+
+ devicePreference.performClick();
+
+ assertThat(ShadowApplication.getInstance().getBroadcastIntents()).hasSize(1);
+ Intent pickedIntent = ShadowApplication.getInstance().getBroadcastIntents().get(0);
+ assertThat(pickedIntent.getAction()).isEqualTo(
+ BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
+ assertThat(pickedIntent.getComponent()).isEqualTo(component);
+ assertThat((BluetoothDevice) pickedIntent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE)).isEqualTo(mBondedDevice);
+ }
+
+ @Test
+ public void onDeviceClicked_bondedDevice_goesBack() {
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ true,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ BluetoothDevicePreference devicePreference =
+ (BluetoothDevicePreference) mPreferenceGroup.getPreference(0);
+
+ devicePreference.performClick();
+
+ verify(mControllerHelper.getMockFragmentController()).goBack();
+ }
+
+ @Test
+ public void onDeviceClicked_unbondedDevice_doesNotNeedAuth_sendsPickedIntent() {
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ false,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ BluetoothDevicePreference devicePreference =
+ (BluetoothDevicePreference) mPreferenceGroup.getPreference(1);
+
+ devicePreference.performClick();
+
+ Intent pickedIntent = ShadowApplication.getInstance().getBroadcastIntents().get(0);
+ assertThat(pickedIntent.getAction()).isEqualTo(
+ BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
+ }
+
+ @Test
+ public void onDeviceClicked_unbondedDevice_needsAuth_startsPairing() {
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ true,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ BluetoothDevicePreference devicePreference =
+ (BluetoothDevicePreference) mPreferenceGroup.getPreference(1);
+
+ devicePreference.performClick();
+
+ verify(mUnbondedCachedDevice).startPairing();
+ }
+
+ @Test
+ public void onDeviceClicked_unbondedDevice_needsAuth_pairingStartFails_resumesScanning() {
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ true,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ BluetoothDevicePreference devicePreference =
+ (BluetoothDevicePreference) mPreferenceGroup.getPreference(1);
+ when(mUnbondedCachedDevice.startPairing()).thenReturn(false);
+ assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+
+ devicePreference.performClick();
+
+ assertThat(BluetoothAdapter.getDefaultAdapter().isDiscovering()).isTrue();
+ }
+
+ @Test
+ public void onDeviceBondStateChanged_selectedDeviceBonded_sendsPickedIntent() {
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ true,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ BluetoothDevicePreference devicePreference =
+ (BluetoothDevicePreference) mPreferenceGroup.getPreference(1);
+
+ // Select device.
+ devicePreference.performClick();
+ // Device bonds.
+ mControllerHelper.getController().onDeviceBondStateChanged(
+ devicePreference.getCachedDevice(), BluetoothDevice.BOND_BONDED);
+
+ Intent pickedIntent = ShadowApplication.getInstance().getBroadcastIntents().get(0);
+ assertThat(pickedIntent.getAction()).isEqualTo(
+ BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
+ }
+
+ @Test
+ public void onDeviceBondStateChanged_selectedDeviceBonded_goesBack() {
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ true,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+ BluetoothDevicePreference devicePreference =
+ (BluetoothDevicePreference) mPreferenceGroup.getPreference(1);
+
+ // Select device.
+ devicePreference.performClick();
+ // Device bonds.
+ mControllerHelper.getController().onDeviceBondStateChanged(
+ devicePreference.getCachedDevice(), BluetoothDevice.BOND_BONDED);
+
+ verify(mControllerHelper.getMockFragmentController()).goBack();
+ }
+
+ @Test
+ public void onDestroy_noDeviceSelected_sendsNullPickedIntent() {
+ Intent launchIntent = createLaunchIntent(/* needsAuth= */ true,
+ BluetoothDevicePicker.FILTER_TYPE_ALL, "test.package", "TestClass");
+ mControllerHelper.getController().setLaunchIntent(launchIntent);
+ mControllerHelper.setPreference(mPreferenceGroup);
+ mControllerHelper.markState(Lifecycle.State.STARTED);
+
+ mControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_DESTROY);
+
+ Intent pickedIntent = ShadowApplication.getInstance().getBroadcastIntents().get(0);
+ assertThat(pickedIntent.getAction()).isEqualTo(
+ BluetoothDevicePicker.ACTION_DEVICE_SELECTED);
+ assertThat((BluetoothDevice) pickedIntent.getParcelableExtra(
+ BluetoothDevice.EXTRA_DEVICE)).isNull();
+ }
+
+ private Intent createLaunchIntent(boolean needAuth, int filterType, String packageName,
+ String className) {
+ Intent intent = new Intent(BluetoothDevicePicker.ACTION_LAUNCH);
+ intent.putExtra(BluetoothDevicePicker.EXTRA_NEED_AUTH, needAuth);
+ intent.putExtra(BluetoothDevicePicker.EXTRA_FILTER_TYPE, filterType);
+ intent.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_PACKAGE, packageName);
+ intent.putExtra(BluetoothDevicePicker.EXTRA_LAUNCH_CLASS, className);
+ return intent;
+ }
+
+ private ShadowBluetoothAdapter getShadowBluetoothAdapter() {
+ return (ShadowBluetoothAdapter) Shadow.extract(BluetoothAdapter.getDefaultAdapter());
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/common/BaseCarSettingsActivityTest.java b/tests/robotests/src/com/android/car/settings/common/BaseCarSettingsActivityTest.java
index cb83d85..a763b80 100644
--- a/tests/robotests/src/com/android/car/settings/common/BaseCarSettingsActivityTest.java
+++ b/tests/robotests/src/com/android/car/settings/common/BaseCarSettingsActivityTest.java
@@ -125,6 +125,59 @@
assertThat(mActivity.findDialogByTag(TEST_TAG)).isNull();
}
+ @Test
+ public void onUxRestrictionsChanged_topFragmentInBackStackHasUpdatedUxRestrictions() {
+ TestFragment fragmentA = new TestFragment();
+ TestFragment fragmentB = new TestFragment();
+
+ CarUxRestrictions oldUxRestrictions = new CarUxRestrictions.Builder(
+ /* reqOpt= */ true,
+ CarUxRestrictions.UX_RESTRICTIONS_BASELINE,
+ /* timestamp= */ 0
+ ).build();
+
+ CarUxRestrictions newUxRestrictions = new CarUxRestrictions.Builder(
+ /* reqOpt= */ true,
+ CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP,
+ /* timestamp= */ 0
+ ).build();
+
+ mActivity.launchFragment(fragmentA);
+ mActivity.onUxRestrictionsChanged(oldUxRestrictions);
+ mActivity.launchFragment(fragmentB);
+ mActivity.onUxRestrictionsChanged(newUxRestrictions);
+
+ assertThat(fragmentB.getUxRestrictions().toString())
+ .isEqualTo(newUxRestrictions.toString());
+ }
+
+ @Test
+ public void onBackStackChanged_uxRestrictionsChanged_currentFragmentHasUpdatedUxRestrictions() {
+ TestFragment fragmentA = new TestFragment();
+ TestFragment fragmentB = new TestFragment();
+
+ CarUxRestrictions oldUxRestrictions = new CarUxRestrictions.Builder(
+ /* reqOpt= */ true,
+ CarUxRestrictions.UX_RESTRICTIONS_BASELINE,
+ /* timestamp= */ 0
+ ).build();
+
+ CarUxRestrictions newUxRestrictions = new CarUxRestrictions.Builder(
+ /* reqOpt= */ true,
+ CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP,
+ /* timestamp= */ 0
+ ).build();
+
+ mActivity.launchFragment(fragmentA);
+ mActivity.onUxRestrictionsChanged(oldUxRestrictions);
+ mActivity.launchFragment(fragmentB);
+ mActivity.onUxRestrictionsChanged(newUxRestrictions);
+ mActivity.goBack();
+
+ assertThat(fragmentA.getUxRestrictions().toString())
+ .isEqualTo(newUxRestrictions.toString());
+ }
+
/** Simple instance of {@link BaseCarSettingsActivity}. */
private static class TestBaseCarSettingsActivity extends BaseCarSettingsActivity {
@@ -136,6 +189,17 @@
}
/** Simple Fragment for testing use. */
- public static class TestFragment extends Fragment {
+ public static class TestFragment extends Fragment implements
+ CarUxRestrictionsManager.OnUxRestrictionsChangedListener {
+ private CarUxRestrictions mCarUxRestrictions;
+
+ @Override
+ public void onUxRestrictionsChanged(CarUxRestrictions restrictionInfo) {
+ mCarUxRestrictions = restrictionInfo;
+ }
+
+ public CarUxRestrictions getUxRestrictions() {
+ return mCarUxRestrictions;
+ }
}
}
diff --git a/tests/robotests/src/com/android/car/settings/common/FakePreferenceController.java b/tests/robotests/src/com/android/car/settings/common/FakePreferenceController.java
index c8893cd..0762f26 100644
--- a/tests/robotests/src/com/android/car/settings/common/FakePreferenceController.java
+++ b/tests/robotests/src/com/android/car/settings/common/FakePreferenceController.java
@@ -21,6 +21,9 @@
import androidx.preference.Preference;
+import java.util.HashSet;
+import java.util.Set;
+
/**
* Concrete {@link PreferenceController} with methods for verifying behavior in tests.
*/
@@ -42,6 +45,8 @@
private Object mHandlePreferenceChangedValueArg;
private int mHandlePreferenceClickedCallCount;
private Preference mHandlePreferenceClickedArg;
+ private boolean mAllIgnoresUxRestrictions = false;
+ private Set<String> mPreferencesIgnoringUxRestrictions = new HashSet<>();
public FakePreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
@@ -175,4 +180,16 @@
Preference getHandlePreferenceClickedArg() {
return mHandlePreferenceClickedArg;
}
+
+ @Override
+ protected boolean isUxRestrictionsIgnored(boolean allIgnores, Set preferencesThatIgnore) {
+ return super.isUxRestrictionsIgnored(mAllIgnoresUxRestrictions,
+ mPreferencesIgnoringUxRestrictions);
+ }
+
+ protected void setUxRestrictionsIgnoredConfig(boolean allIgnore, Set preferencesThatIgnore) {
+ mAllIgnoresUxRestrictions = allIgnore;
+ mPreferencesIgnoringUxRestrictions = preferencesThatIgnore;
+ }
+
}
diff --git a/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTest.java
index 3c1cef3..b51db10 100644
--- a/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTest.java
@@ -47,6 +47,9 @@
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
+import java.util.HashSet;
+import java.util.Set;
+
/**
* Unit test for {@link PreferenceController}.
*/
@@ -65,11 +68,12 @@
new CarUxRestrictions.Builder(/* reqOpt= */ true,
CarUxRestrictions.UX_RESTRICTIONS_BASELINE, /* timestamp= */ 0).build();
- private Context mContext;
- @Mock
- private Preference mPreference;
private PreferenceControllerTestHelper<FakePreferenceController> mControllerHelper;
private FakePreferenceController mController;
+ private Context mContext;
+
+ @Mock
+ private Preference mPreference;
@Before
public void setUp() {
@@ -177,6 +181,58 @@
}
@Test
+ public void onUxRestrictionsChanged_restricted_allPreferencesIgnore_preferenceEnabled() {
+ // mPreference cannot be a Mock here because its real methods need to be invoked.
+ mPreference = new Preference(mContext);
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ FakePreferenceController.class, mPreference);
+ mController = mControllerHelper.getController();
+
+ Set preferencesIgnoringUxRestrictions = new HashSet();
+ mController.setUxRestrictionsIgnoredConfig(/* allIgnores= */ true,
+ preferencesIgnoringUxRestrictions);
+ mControllerHelper.markState(Lifecycle.State.CREATED);
+ mController.onUxRestrictionsChanged(NO_SETUP_UX_RESTRICTIONS);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void onUxRestrictionsChanged_restricted_thisPreferenceIgnores_preferenceEnabled() {
+ // mPreference cannot be a Mock here because its real methods need to be invoked.
+ mPreference = new Preference(mContext);
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ FakePreferenceController.class, mPreference);
+ mController = mControllerHelper.getController();
+
+ Set preferencesIgnoringUxRestrictions = new HashSet();
+ preferencesIgnoringUxRestrictions.add(PreferenceControllerTestHelper.getKey());
+ mController.setUxRestrictionsIgnoredConfig(/* allIgnores= */ false,
+ preferencesIgnoringUxRestrictions);
+ mControllerHelper.markState(Lifecycle.State.CREATED);
+ mController.onUxRestrictionsChanged(NO_SETUP_UX_RESTRICTIONS);
+
+ assertThat(mPreference.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void onUxRestrictionsChanged_restricted_uxRestrictionsNotIgnored_preferenceDisabled() {
+ // mPreference cannot be a Mock here because its real methods need to be invoked.
+ mPreference = new Preference(mContext);
+ mControllerHelper = new PreferenceControllerTestHelper<>(mContext,
+ FakePreferenceController.class, mPreference);
+ mController = mControllerHelper.getController();
+
+ Set preferencesIgnoringUxRestrictions = new HashSet();
+ mController.setUxRestrictionsIgnoredConfig(/* allIgnores= */ false,
+ preferencesIgnoringUxRestrictions);
+ mControllerHelper.markState(Lifecycle.State.CREATED);
+ mController.onUxRestrictionsChanged(NO_SETUP_UX_RESTRICTIONS);
+
+ assertThat(mPreference.isEnabled()).isFalse();
+ }
+
+ @Test
public void getAvailabilityStatus_defaultsToAvailable() {
assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE);
}
diff --git a/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTestHelper.java b/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTestHelper.java
index 96ee562..3ed36af 100644
--- a/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTestHelper.java
+++ b/tests/robotests/src/com/android/car/settings/common/PreferenceControllerTestHelper.java
@@ -50,6 +50,7 @@
public class PreferenceControllerTestHelper<T extends PreferenceController> {
private static final String PREFERENCE_KEY = "preference_key";
+
private static final CarUxRestrictions UX_RESTRICTIONS =
new CarUxRestrictions.Builder(/* reqOpt= */ true,
CarUxRestrictions.UX_RESTRICTIONS_BASELINE, /* timestamp= */ 0).build();
@@ -150,6 +151,10 @@
}
}
+ public static String getKey() {
+ return PREFERENCE_KEY;
+ }
+
/*
* Ideally we would use androidx.lifecycle.LifecycleRegistry to drive the lifecycle changes.
* However, doing so led to test flakiness with an unknown root cause. We dispatch state
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java
index e899705..e9359ec 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java
@@ -23,7 +23,10 @@
import android.content.SyncInfo;
import android.content.SyncStatusInfo;
import android.content.SyncStatusObserver;
+import android.database.Cursor;
+import android.net.Uri;
import android.os.Bundle;
+import android.os.CancellationSignal;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
@@ -55,6 +58,13 @@
private static SyncStatusObserver sStatusObserver;
@Implementation
+ public final Cursor query(Uri uri, String[] projection, Bundle queryArgs,
+ CancellationSignal cancellationSignal) {
+ return query(uri, projection, /* selection= */ null, /* selectionArgs= */
+ null, /* sortOrder= */ null, cancellationSignal);
+ }
+
+ @Implementation
protected static SyncAdapterType[] getSyncAdapterTypesAsUser(int userId) {
return sSyncAdapterTypes;
}
diff --git a/tests/robotests/src/com/android/car/settings/wifi/NetworkNameRestrictedPasswordEditTextPreferenceTest.java b/tests/robotests/src/com/android/car/settings/wifi/NetworkNameRestrictedPasswordEditTextPreferenceTest.java
new file mode 100644
index 0000000..cbcef40
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/wifi/NetworkNameRestrictedPasswordEditTextPreferenceTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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 com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.R;
+import com.android.car.settings.common.SettingsFragment;
+import com.android.car.settings.testutils.FragmentController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.shadows.ShadowDialog;
+import org.robolectric.shadows.ShadowToast;
+
+@RunWith(CarSettingsRobolectricTestRunner.class)
+public class NetworkNameRestrictedPasswordEditTextPreferenceTest {
+
+ private static final String KEY = "test_key";
+
+ private Context mContext;
+ private NetworkNameRestrictedPasswordEditTextPreference mPreference;
+
+ @Before
+ public void setUp() {
+ mContext = RuntimeEnvironment.application;
+ FragmentController<TestSettingsFragment> fragmentController = FragmentController.of(
+ new TestSettingsFragment());
+ TestSettingsFragment fragment = fragmentController.get();
+ fragmentController.setup();
+
+ mPreference = new NetworkNameRestrictedPasswordEditTextPreference(mContext);
+ mPreference.setKey(KEY);
+ fragment.getPreferenceScreen().addPreference(mPreference);
+ }
+
+ @Test
+ public void performClick_noName_toastShown() {
+ mPreference.performClick();
+
+ assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo(
+ mContext.getString(R.string.wifi_no_network_name));
+ }
+
+ @Test
+ public void performClick_hasName_showsDialog() {
+ mPreference.setNetworkName("test_name");
+ mPreference.performClick();
+
+ assertThat(ShadowDialog.getLatestDialog()).isNotNull();
+ }
+
+ /** Concrete {@link SettingsFragment} for testing. */
+ public static class TestSettingsFragment extends SettingsFragment {
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.settings_fragment;
+ }
+ }
+}
diff --git a/tests/robotests/src/com/android/car/settings/wifi/NetworkPasswordPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/wifi/NetworkPasswordPreferenceControllerTest.java
index fc14fa9..28b555a 100644
--- a/tests/robotests/src/com/android/car/settings/wifi/NetworkPasswordPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/car/settings/wifi/NetworkPasswordPreferenceControllerTest.java
@@ -29,7 +29,6 @@
import com.android.car.settings.CarSettingsRobolectricTestRunner;
import com.android.car.settings.R;
-import com.android.car.settings.common.PasswordEditTextPreference;
import com.android.car.settings.common.PreferenceControllerTestHelper;
import com.android.car.settings.testutils.ShadowLocalBroadcastManager;
import com.android.car.settings.testutils.ShadowWifiManager;
@@ -42,7 +41,6 @@
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
-import org.robolectric.shadows.ShadowToast;
import java.util.List;
@@ -52,7 +50,7 @@
private Context mContext;
private LocalBroadcastManager mLocalBroadcastManager;
- private PasswordEditTextPreference mPasswordEditTextPreference;
+ private NetworkNameRestrictedPasswordEditTextPreference mPasswordEditTextPreference;
private PreferenceControllerTestHelper<NetworkPasswordPreferenceController>
mPreferenceControllerHelper;
private NetworkPasswordPreferenceController mController;
@@ -61,7 +59,7 @@
public void setUp() {
mContext = RuntimeEnvironment.application;
mLocalBroadcastManager = LocalBroadcastManager.getInstance(mContext);
- mPasswordEditTextPreference = new PasswordEditTextPreference(mContext);
+ mPasswordEditTextPreference = new NetworkNameRestrictedPasswordEditTextPreference(mContext);
mPreferenceControllerHelper = new PreferenceControllerTestHelper<>(mContext,
NetworkPasswordPreferenceController.class, mPasswordEditTextPreference);
mController = mPreferenceControllerHelper.getController();
@@ -152,23 +150,6 @@
}
@Test
- public void handlePreferenceChanged_hasSecurity_noNetworkNameSet_showToast() {
- mPreferenceControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
- Intent intent = new Intent(NetworkSecurityPreferenceController.ACTION_SECURITY_CHANGE);
- intent.putExtra(NetworkSecurityPreferenceController.KEY_SECURITY_TYPE,
- AccessPoint.SECURITY_PSK);
- mLocalBroadcastManager.sendBroadcastSync(intent);
-
- intent = new Intent(NetworkNamePreferenceController.ACTION_NAME_CHANGE);
- intent.putExtra(NetworkNamePreferenceController.KEY_NETWORK_NAME, "");
- mLocalBroadcastManager.sendBroadcastSync(intent);
-
- mPasswordEditTextPreference.callChangeListener("password");
- assertThat(ShadowToast.getTextOfLatestToast()).isEqualTo(
- mContext.getString(R.string.wifi_no_network_name));
- }
-
- @Test
public void handlePreferenceChanged_hasSecurity_networkNameSet_wifiAdded() {
mPreferenceControllerHelper.sendLifecycleEvent(Lifecycle.Event.ON_START);
String networkName = "network_name";