From 7c9f3ff74e7219fb609d1992774f8f213d969dba Mon Sep 17 00:00:00 2001 From: Pengquan Meng Date: Wed, 11 Apr 2018 12:53:03 -0700 Subject: [PATCH 01/20] Fixed ApnEditor crash issue This root caused is that we closed the managed cursor which lifecycle is managed by Activity. Actually, we don't need the managed cursor in this case, just use the normal cursor and close it after we got the apn data from the database. Bug: 77894798 Test: make ROBOTEST_FILTER=ApnEditorTest -j40 RunSettingsRoboTests Change-Id: I6eb80bbd53354e00e871e974f520668dcbceac63 --- .../android/settings/network/ApnEditor.java | 19 +++++++++++++------ .../settings/network/ApnEditorTest.java | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/network/ApnEditor.java b/src/com/android/settings/network/ApnEditor.java index 61f12438262..d0ecb71f681 100644 --- a/src/com/android/settings/network/ApnEditor.java +++ b/src/com/android/settings/network/ApnEditor.java @@ -1195,12 +1195,19 @@ public class ApnEditor extends SettingsPreferenceFragment } } - private ApnData getApnDataFromUri(Uri uri) { - ApnData apnData; - try (Cursor cursor = getActivity().managedQuery( - uri, sProjection, null /* selection */, null /* sortOrder */)) { - cursor.moveToFirst(); - apnData = new ApnData(uri, cursor); + @VisibleForTesting + ApnData getApnDataFromUri(Uri uri) { + ApnData apnData = null; + try (Cursor cursor = getContentResolver().query( + uri, + sProjection, + null /* selection */, + null /* selectionArgs */, + null /* sortOrder */)) { + if (cursor != null) { + cursor.moveToFirst(); + apnData = new ApnData(uri, cursor); + } } if (apnData == null) { diff --git a/tests/robotests/src/com/android/settings/network/ApnEditorTest.java b/tests/robotests/src/com/android/settings/network/ApnEditorTest.java index f3315e5153a..1b34fd3f98b 100644 --- a/tests/robotests/src/com/android/settings/network/ApnEditorTest.java +++ b/tests/robotests/src/com/android/settings/network/ApnEditorTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -115,6 +116,24 @@ public class ApnEditorTest { mApnEditorUT.sNotSet = "Not Set"; } + @Test + public void testApnEditor_doesNotUseManagedQuery() { + mApnEditorUT.getApnDataFromUri(Mockito.mock(Uri.class)); + + verify(mActivity, never()).managedQuery( + any(Uri.class), + any(String[].class), + any(String.class), + any(String.class)); + + verify(mActivity, never()).managedQuery( + any(Uri.class), + any(String[].class), + any(String.class), + any(String[].class), + any(String.class)); + } + @Test public void testSetStringValue_valueChanged_shouldSetValue() { // GIVEN an APN value which is different than the APN value in database From d391f460761c383cbbf8553ed7ca96def124966b Mon Sep 17 00:00:00 2001 From: Chalard Jean Date: Mon, 16 Apr 2018 13:09:03 +0900 Subject: [PATCH 02/20] Use the SSID of the network in the no internet dialog Bug: 77865258 Test: manual & roboelectric Change-Id: I72f1f74bf004f30a2c38b4c3af977ff09360d1e8 --- src/com/android/settings/wifi/WifiNoInternetDialog.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/com/android/settings/wifi/WifiNoInternetDialog.java b/src/com/android/settings/wifi/WifiNoInternetDialog.java index 6b7b2db26be..b175665a943 100644 --- a/src/com/android/settings/wifi/WifiNoInternetDialog.java +++ b/src/com/android/settings/wifi/WifiNoInternetDialog.java @@ -25,6 +25,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkRequest; +import android.net.wifi.WifiInfo; import android.os.Bundle; import android.provider.Settings; import android.util.Log; @@ -107,14 +108,15 @@ public final class WifiNoInternetDialog extends AlertActivity implements mCM.registerNetworkCallback(request, mNetworkCallback); final NetworkInfo ni = mCM.getNetworkInfo(mNetwork); - if (ni == null || !ni.isConnectedOrConnecting()) { + final NetworkCapabilities nc = mCM.getNetworkCapabilities(mNetwork); + if (ni == null || !ni.isConnectedOrConnecting() || nc == null) { Log.d(TAG, "Network " + mNetwork + " is not connected: " + ni); finish(); return; } - mNetworkName = ni.getExtraInfo(); + mNetworkName = nc.getSSID(); if (mNetworkName != null) { - mNetworkName = mNetworkName.replaceAll("^\"|\"$", ""); // Remove double quotes + mNetworkName = WifiInfo.removeDoubleQuotes(mNetworkName); } createDialog(); From fbe721b1b39d48ef8665267774b54f5f04b20d58 Mon Sep 17 00:00:00 2001 From: Beverly Date: Mon, 16 Apr 2018 12:27:52 -0400 Subject: [PATCH 03/20] Get rid of charging sounds toggle Users can no longer toggle on/off the charging sound. Test: manual Bug: 77912907 Change-Id: Ie050acab3972cb1fa6716c70afb6403db4d9c887 --- res/xml/sound_settings.xml | 5 ----- src/com/android/settings/notification/SoundSettings.java | 4 ---- 2 files changed, 9 deletions(-) diff --git a/res/xml/sound_settings.xml b/res/xml/sound_settings.xml index 735b3b7d986..6e469b6a457 100644 --- a/res/xml/sound_settings.xml +++ b/res/xml/sound_settings.xml @@ -125,11 +125,6 @@ android:key="screen_locking_sounds" android:title="@string/screen_locking_sounds_title" /> - - - Date: Thu, 12 Apr 2018 14:08:12 -0700 Subject: [PATCH 04/20] Remove swipe up setting page if the feature is not available Bug: 77974864 Test: Manual test and RoboTests Change-Id: Ie23a01f254d65e5888f91b5b1cb80df40976c569 --- .../gestures/SwipeUpPreferenceController.java | 12 +++++++++ .../SwipeUpPreferenceControllerTest.java | 27 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/gestures/SwipeUpPreferenceController.java b/src/com/android/settings/gestures/SwipeUpPreferenceController.java index a20cd9738e7..c3abd464ee8 100644 --- a/src/com/android/settings/gestures/SwipeUpPreferenceController.java +++ b/src/com/android/settings/gestures/SwipeUpPreferenceController.java @@ -16,8 +16,11 @@ package com.android.settings.gestures; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.UserHandle; import android.os.UserManager; @@ -33,6 +36,7 @@ public class SwipeUpPreferenceController extends GesturePreferenceController { private final int ON = 1; private final int OFF = 0; + private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; private static final String PREF_KEY_VIDEO = "gesture_swipe_up_video"; private final UserManager mUserManager; @@ -42,6 +46,14 @@ public class SwipeUpPreferenceController extends GesturePreferenceController { } static boolean isGestureAvailable(Context context) { + final ComponentName recentsComponentName = ComponentName.unflattenFromString( + context.getString(com.android.internal.R.string.config_recentsComponentName)); + final Intent quickStepIntent = new Intent(ACTION_QUICKSTEP) + .setPackage(recentsComponentName.getPackageName()); + if (context.getPackageManager().resolveService(quickStepIntent, + PackageManager.MATCH_SYSTEM_ONLY) == null) { + return false; + } return true; } diff --git a/tests/robotests/src/com/android/settings/gestures/SwipeUpPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/gestures/SwipeUpPreferenceControllerTest.java index f9a6e9cfbc1..360609bd169 100644 --- a/tests/robotests/src/com/android/settings/gestures/SwipeUpPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/gestures/SwipeUpPreferenceControllerTest.java @@ -17,12 +17,14 @@ package com.android.settings.gestures; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Matchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; import android.hardware.Sensor; import android.hardware.SensorManager; import android.os.UserManager; @@ -38,7 +40,8 @@ import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowPackageManager; import java.util.ArrayList; import java.util.List; @@ -47,15 +50,35 @@ import java.util.List; public class SwipeUpPreferenceControllerTest { private Context mContext; + private ShadowPackageManager mPackageManager; private SwipeUpPreferenceController mController; + + private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; private static final String KEY_SWIPE_UP = "gesture_swipe_up"; @Before public void setUp() { mContext = RuntimeEnvironment.application; + mPackageManager = Shadows.shadowOf(mContext.getPackageManager()); mController = new SwipeUpPreferenceController(mContext, KEY_SWIPE_UP); } + @Test + public void testIsGestureAvailable_matchingServiceExists_shouldReturnTrue() { + final ComponentName recentsComponentName = ComponentName.unflattenFromString( + mContext.getString(com.android.internal.R.string.config_recentsComponentName)); + final Intent quickStepIntent = new Intent(ACTION_QUICKSTEP) + .setPackage(recentsComponentName.getPackageName()); + mPackageManager.addResolveInfoForIntent(quickStepIntent, new ResolveInfo()); + + assertThat(SwipeUpPreferenceController.isGestureAvailable(mContext)).isTrue(); + } + + @Test + public void testIsGestureAvailable_noMatchingServiceExists_shouldReturnFalse() { + assertThat(SwipeUpPreferenceController.isGestureAvailable(mContext)).isFalse(); + } + @Test public void testIsChecked_configIsSet_shouldReturnTrue() { // Set the setting to be enabled. From ac23bb5d54db8ee571f0881c0b8171e0fab54ec6 Mon Sep 17 00:00:00 2001 From: hughchen Date: Tue, 17 Apr 2018 17:14:42 +0800 Subject: [PATCH 05/20] Remove the divider * Remove the divider that below the "Previously connected devices" Bug: 78153629 Test: Build Change-Id: I298f3bf396c929f025978431577cba9d6aa5f678 --- res/xml/connected_devices.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/xml/connected_devices.xml b/res/xml/connected_devices.xml index 5b371faf8d6..1e9d97eaa90 100644 --- a/res/xml/connected_devices.xml +++ b/res/xml/connected_devices.xml @@ -52,8 +52,7 @@ android:fragment="com.android.settings.connecteddevice.BluetoothDashboardFragment" android:key="bluetooth_settings" android:title="@string/bluetooth_settings_title" - android:icon="@drawable/ic_settings_bluetooth" - settings:allowDividerAbove="true"/> + android:icon="@drawable/ic_settings_bluetooth"/> From 0ddba4c119d2d1c35fa4c4038e5192bf5761be2c Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Tue, 17 Apr 2018 09:11:43 -0400 Subject: [PATCH 06/20] Show suppression msg under ringer slider Instead of replacing the rigner slider, because the stream isn't actually muted. Change-Id: Id2c60d6c570084ed8febcaeb3b096a487f572312 Fixes: 77641315 Test: robotests --- res/layout/preference_volume_slider.xml | 6 ++++-- .../RingVolumePreferenceController.java | 14 +++++++++----- .../settings/notification/SoundSettings.java | 14 ++++++++++---- .../notification/VolumeSeekBarPreference.java | 3 +-- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/res/layout/preference_volume_slider.xml b/res/layout/preference_volume_slider.xml index 7e146b88ba8..89ecec02c5b 100644 --- a/res/layout/preference_volume_slider.xml +++ b/res/layout/preference_volume_slider.xml @@ -72,9 +72,10 @@ android:orientation="vertical"/> - - + diff --git a/src/com/android/settings/notification/RingVolumePreferenceController.java b/src/com/android/settings/notification/RingVolumePreferenceController.java index ea071fa6fd7..e74b1104d8c 100644 --- a/src/com/android/settings/notification/RingVolumePreferenceController.java +++ b/src/com/android/settings/notification/RingVolumePreferenceController.java @@ -17,6 +17,8 @@ package com.android.settings.notification; import android.app.NotificationManager; +import android.arch.lifecycle.LifecycleObserver; +import android.arch.lifecycle.OnLifecycleEvent; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -31,6 +33,7 @@ import android.os.Vibrator; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; import com.android.settings.Utils; +import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.Objects; @@ -58,6 +61,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr updateRingerMode(); } + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @Override public void onResume() { super.onResume(); @@ -66,6 +70,7 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr updatePreferenceIcon(); } + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) @Override public void onPause() { super.onPause(); @@ -118,11 +123,10 @@ public class RingVolumePreferenceController extends VolumeSeekBarPreferenceContr private void updatePreferenceIcon() { if (mPreference != null) { - mPreference.showIcon(mSuppressor != null - ? com.android.internal.R.drawable.ic_audio_ring_notif_mute - : mRingerMode == AudioManager.RINGER_MODE_VIBRATE || wasRingerModeVibrate() - ? com.android.internal.R.drawable.ic_audio_ring_notif_vibrate - : com.android.internal.R.drawable.ic_audio_ring_notif); + mPreference.showIcon( + mRingerMode == AudioManager.RINGER_MODE_VIBRATE || wasRingerModeVibrate() + ? com.android.internal.R.drawable.ic_audio_ring_notif_vibrate + : com.android.internal.R.drawable.ic_audio_ring_notif); } } diff --git a/src/com/android/settings/notification/SoundSettings.java b/src/com/android/settings/notification/SoundSettings.java index 4c9ee38afa2..dbf8ecf55bf 100644 --- a/src/com/android/settings/notification/SoundSettings.java +++ b/src/com/android/settings/notification/SoundSettings.java @@ -146,10 +146,16 @@ public class SoundSettings extends DashboardFragment { @Override public void onAttach(Context context) { super.onAttach(context); - use(AlarmVolumePreferenceController.class).setCallback(mVolumeCallback); - use(MediaVolumePreferenceController.class).setCallback(mVolumeCallback); - use(RingVolumePreferenceController.class).setCallback(mVolumeCallback); - use(NotificationVolumePreferenceController.class).setCallback(mVolumeCallback); + ArrayList volumeControllers = new ArrayList<>(); + volumeControllers.add(use(AlarmVolumePreferenceController.class)); + volumeControllers.add(use(MediaVolumePreferenceController.class)); + volumeControllers.add(use(RingVolumePreferenceController.class)); + volumeControllers.add(use(NotificationVolumePreferenceController.class)); + + for (VolumeSeekBarPreferenceController controller : volumeControllers) { + controller.setCallback(mVolumeCallback); + getLifecycle().addObserver(controller); + } } // === Volumes === diff --git a/src/com/android/settings/notification/VolumeSeekBarPreference.java b/src/com/android/settings/notification/VolumeSeekBarPreference.java index 8a48e95c166..d7b5e521a50 100644 --- a/src/com/android/settings/notification/VolumeSeekBarPreference.java +++ b/src/com/android/settings/notification/VolumeSeekBarPreference.java @@ -196,8 +196,7 @@ public class VolumeSeekBarPreference extends SeekBarPreference { if (mSuppressionTextView != null && mSeekBar != null) { mSuppressionTextView.setText(mSuppressionText); final boolean showSuppression = !TextUtils.isEmpty(mSuppressionText); - mSuppressionTextView.setVisibility(showSuppression ? View.VISIBLE : View.INVISIBLE); - mSeekBar.setVisibility(showSuppression ? View.INVISIBLE : View.VISIBLE); + mSuppressionTextView.setVisibility(showSuppression ? View.VISIBLE : View.GONE); } } From 280589bfe5caf5b9dc78df7d169e296c049e1b17 Mon Sep 17 00:00:00 2001 From: Adrian Roos Date: Tue, 17 Apr 2018 17:22:05 +0200 Subject: [PATCH 07/20] Settings: Add synonyms to display cutout emulation setting Test: Search for "notch" or "display cutout", verify emulation setting shows up Change-Id: Iaf036c965c233e63d9cc9729a0ecd13d8e6a8881 Fixes: 78163992 --- res/values/strings.xml | 3 +++ res/xml/development_settings.xml | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 44e7e98762a..9de50601442 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9267,6 +9267,9 @@ Simulate a display with a cutout + + display cutout, notch + None diff --git a/res/xml/development_settings.xml b/res/xml/development_settings.xml index 77d4a7dffb3..c491980153c 100644 --- a/res/xml/development_settings.xml +++ b/res/xml/development_settings.xml @@ -382,7 +382,8 @@ + android:title="@string/display_cutout_emulation" + settings:keywords="@string/display_cutout_emulation_keywords" /> From 5685e9065e111e528bcd383dfbbdbd36f5db0442 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Tue, 17 Apr 2018 10:15:51 -0700 Subject: [PATCH 08/20] Remove empty_search_image from Settings. The image now lives in SettingsIntelligence. Having another copy here creates deadweight and confusion. Bug: 74410371 Test: rebuild Change-Id: Id90fd412a629fcd663cd88cbe5da46e511055adc --- res/drawable/empty_search_results.xml | 30 --------------------------- 1 file changed, 30 deletions(-) delete mode 100644 res/drawable/empty_search_results.xml diff --git a/res/drawable/empty_search_results.xml b/res/drawable/empty_search_results.xml deleted file mode 100644 index 916210712ba..00000000000 --- a/res/drawable/empty_search_results.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - \ No newline at end of file From a711ed833099de30f589733cbb2c9b03a19097a6 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Thu, 1 Mar 2018 19:02:07 -0800 Subject: [PATCH 09/20] Add A11y Slices Add AccessibilityPreferenceController, which wraps all a11y settings since they are share common infrastructure for enabling, current value, and availability. We add an overlay for OEMs to declare their bundled a11y services. This is the only list of services that will be possible to enabled via Settings slices. Accessibility Slices are built by getting a list of valid services, and indexing the service names as a key in the Slices DB. When they are built at runtime, they use the generic A11yPrefController to get the status and enable/disable the service. Bug: 67997836 Bug: 67997672 Test: robotests Change-Id: I66f905bf1c55eecb937945c4675c12bcbc96d698 --- res/values/config.xml | 3 + .../accessibility/AccessibilitySettings.java | 27 ++-- ...ccessibilitySlicePreferenceController.java | 108 +++++++++++++ .../slices/SettingsSliceProvider.java | 2 +- .../settings/slices/SliceBuilderUtils.java | 18 +-- .../android/settings/slices/SliceData.java | 8 +- .../settings/slices/SliceDataConverter.java | 74 ++++++++- .../slices/SlicesDatabaseAccessor.java | 6 +- .../settings/slices/SlicesIndexer.java | 2 +- .../grandfather_slice_controller_not_in_xml | 4 +- tests/robotests/res/values-mcc999/config.xml | 5 + ...sibilitySlicePreferenceControllerTest.java | 148 ++++++++++++++++++ .../slices/SliceBuilderUtilsTest.java | 6 +- .../slices/SliceDataConverterTest.java | 105 +++++++++++-- 14 files changed, 467 insertions(+), 49 deletions(-) create mode 100644 src/com/android/settings/accessibility/AccessibilitySlicePreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/accessibility/AccessibilitySlicePreferenceControllerTest.java diff --git a/res/values/config.xml b/res/values/config.xml index ec611f09694..e3ec74f2636 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -128,4 +128,7 @@ doesn't interact well with scroll view --> true + + + diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index e0e8e4e0df9..3f7bd2582c1 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -342,6 +342,21 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements return super.onPreferenceTreeClick(preference); } + public static CharSequence getServiceSummary(Context context, AccessibilityServiceInfo info, + boolean serviceEnabled) { + final String serviceState = serviceEnabled + ? context.getString(R.string.accessibility_summary_state_enabled) + : context.getString(R.string.accessibility_summary_state_disabled); + final CharSequence serviceSummary = info.loadSummary(context.getPackageManager()); + final String stateSummaryCombo = context.getString( + R.string.preference_summary_default_combination, + serviceState, serviceSummary); + + return (TextUtils.isEmpty(serviceSummary)) + ? serviceState + : stateSummaryCombo; + } + private void handleToggleTextContrastPreferenceClick() { Settings.Secure.putInt(getContentResolver(), Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, @@ -543,15 +558,9 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements preference.setSummary(R.string.accessibility_summary_state_stopped); description = getString(R.string.accessibility_description_state_stopped); } else { - final String serviceState = serviceEnabled ? - getString(R.string.accessibility_summary_state_enabled) : - getString(R.string.accessibility_summary_state_disabled); - final CharSequence serviceSummary = info.loadSummary(getPackageManager()); - final String stateSummaryCombo = getString( - R.string.preference_summary_default_combination, - serviceState, serviceSummary); - preference.setSummary((TextUtils.isEmpty(serviceSummary)) ? serviceState - : stateSummaryCombo); + final CharSequence serviceSummary = getServiceSummary(getContext(), info, + serviceEnabled); + preference.setSummary(serviceSummary); } // Disable all accessibility services that are not permitted. diff --git a/src/com/android/settings/accessibility/AccessibilitySlicePreferenceController.java b/src/com/android/settings/accessibility/AccessibilitySlicePreferenceController.java new file mode 100644 index 00000000000..6b9a480ff81 --- /dev/null +++ b/src/com/android/settings/accessibility/AccessibilitySlicePreferenceController.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; +import android.view.accessibility.AccessibilityManager; + +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.accessibility.AccessibilityUtils; + +import java.util.List; +import java.util.Set; + +/** + * PreferenceController for accessibility services to be used by Slices. + * Wraps the common logic which enables accessibility services and checks their availability. + *

+ * Should not be used in a {@link com.android.settings.dashboard.DashboardFragment}. + */ +public class AccessibilitySlicePreferenceController extends TogglePreferenceController { + + private final ComponentName mComponentName; + + private final int ON = 1; + private final int OFF = 0; + + public AccessibilitySlicePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mComponentName = ComponentName.unflattenFromString(getPreferenceKey()); + + if (mComponentName == null) { + throw new IllegalArgumentException( + "Illegal Component Name from: " + preferenceKey); + } + } + + @Override + public CharSequence getSummary() { + final AccessibilityServiceInfo serviceInfo = getAccessibilityServiceInfo(); + return serviceInfo == null + ? "" : AccessibilitySettings.getServiceSummary(mContext, serviceInfo, isChecked()); + } + + @Override + public boolean isChecked() { + final ContentResolver contentResolver = mContext.getContentResolver(); + final boolean accessibilityEnabled = Settings.Secure.getInt(contentResolver, + Settings.Secure.ACCESSIBILITY_ENABLED, OFF) == ON; + + if (!accessibilityEnabled) { + return false; + } + + final Set componentNames = + AccessibilityUtils.getEnabledServicesFromSettings(mContext); + + return componentNames.contains(mComponentName); + } + + @Override + public boolean setChecked(boolean isChecked) { + if (getAccessibilityServiceInfo() == null) { + return false; + } + AccessibilityUtils.setAccessibilityServiceState(mContext, mComponentName, isChecked); + return isChecked == isChecked(); // Verify that it was probably changed. + } + + @Override + public int getAvailabilityStatus() { + // Return unsupported when the service is disabled or not installed. + return getAccessibilityServiceInfo() == null ? DISABLED_UNSUPPORTED : AVAILABLE; + } + + private AccessibilityServiceInfo getAccessibilityServiceInfo() { + final AccessibilityManager accessibilityManager = mContext.getSystemService( + AccessibilityManager.class); + final List serviceList = + accessibilityManager.getInstalledAccessibilityServiceList(); + + for (AccessibilityServiceInfo serviceInfo : serviceList) { + if (mComponentName.equals(serviceInfo.getComponentName())) { + return serviceInfo; + } + } + + return null; + } +} diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 1a73ea760e5..d8f990a194d 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -148,7 +148,7 @@ public class SettingsSliceProvider extends SliceProvider { void loadSlice(Uri uri) { long startBuildTime = System.currentTimeMillis(); - SliceData sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri); + final SliceData sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri); mSliceDataCache.put(uri, sliceData); getContext().getContentResolver().notifyChange(uri, null /* content observer */); diff --git a/src/com/android/settings/slices/SliceBuilderUtils.java b/src/com/android/settings/slices/SliceBuilderUtils.java index a2479a2da05..8b5dd71f056 100644 --- a/src/com/android/settings/slices/SliceBuilderUtils.java +++ b/src/com/android/settings/slices/SliceBuilderUtils.java @@ -113,13 +113,13 @@ public class SliceBuilderUtils { * - key *

* Examples of valid paths are: - * - intent/wifi - * - intent/bluetooth - * - action/wifi - * - action/accessibility/servicename + * - /intent/wifi + * - /intent/bluetooth + * - /action/wifi + * - /action/accessibility/servicename * * @param uri of the Slice. Follows pattern outlined in {@link SettingsSliceProvider}. - * @return Pair whose first element {@code true} if the path is prepended with "action", and + * @return Pair whose first element {@code true} if the path is prepended with "intent", and * second is a key. */ public static Pair getPathData(Uri uri) { @@ -133,10 +133,10 @@ public class SliceBuilderUtils { throw new IllegalArgumentException("Uri (" + uri + ") has incomplete path: " + path); } - final boolean isInline = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_ACTION, + final boolean isIntent = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_INTENT, split[1]); - return new Pair<>(isInline, split[2]); + return new Pair<>(isIntent, split[2]); } /** @@ -215,8 +215,8 @@ public class SliceBuilderUtils { static Intent getContentIntent(Context context, SliceData sliceData) { final Uri contentUri = new Uri.Builder().appendPath(sliceData.getKey()).build(); final Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context, - sliceData.getFragmentClassName(), sliceData.getKey(), sliceData.getScreenTitle(), - 0 /* TODO */); + sliceData.getFragmentClassName(), sliceData.getKey(), + sliceData.getScreenTitle().toString(), 0 /* TODO */); intent.setClassName(context.getPackageName(), SubSettings.class.getName()); intent.setData(contentUri); return intent; diff --git a/src/com/android/settings/slices/SliceData.java b/src/com/android/settings/slices/SliceData.java index c02b1135699..2caf6e6be17 100644 --- a/src/com/android/settings/slices/SliceData.java +++ b/src/com/android/settings/slices/SliceData.java @@ -57,7 +57,7 @@ public class SliceData { private final String mSummary; - private final String mScreenTitle; + private final CharSequence mScreenTitle; private final int mIconResource; @@ -84,7 +84,7 @@ public class SliceData { return mSummary; } - public String getScreenTitle() { + public CharSequence getScreenTitle() { return mScreenTitle; } @@ -146,7 +146,7 @@ public class SliceData { private String mSummary; - private String mScreenTitle; + private CharSequence mScreenTitle; private int mIconResource; @@ -175,7 +175,7 @@ public class SliceData { return this; } - public Builder setScreenTitle(String screenTitle) { + public Builder setScreenTitle(CharSequence screenTitle) { mScreenTitle = screenTitle; return this; } diff --git a/src/com/android/settings/slices/SliceDataConverter.java b/src/com/android/settings/slices/SliceDataConverter.java index 7cf1994f9ed..27724bfddf5 100644 --- a/src/com/android/settings/slices/SliceDataConverter.java +++ b/src/com/android/settings/slices/SliceDataConverter.java @@ -23,7 +23,12 @@ import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_PLATFO import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_SUMMARY; import static com.android.settings.core.PreferenceXmlParserUtils.METADATA_TITLE; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.os.Bundle; @@ -32,9 +37,14 @@ import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; +import android.view.accessibility.AccessibilityManager; +import com.android.settings.accessibility.AccessibilitySlicePreferenceController; import com.android.settings.core.PreferenceXmlParserUtils; import com.android.settings.core.PreferenceXmlParserUtils.MetadataFlag; +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilitySettings; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.overlay.FeatureFactory; import com.android.settings.search.DatabaseIndexingUtils; @@ -46,10 +56,16 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** - * Converts {@link DashboardFragment} to {@link SliceData}. + * Converts all Slice sources into {@link SliceData}. + * This includes: + * - All {@link DashboardFragment DashboardFragments} indexed by settings search + * - Accessibility services */ class SliceDataConverter { @@ -101,6 +117,8 @@ class SliceDataConverter { mSliceData.addAll(providerSliceData); } + final List a11ySliceData = getAccessibilitySliceData(); + mSliceData.addAll(a11ySliceData); return mSliceData; } @@ -208,4 +226,58 @@ class SliceDataConverter { } return xmlSliceData; } + + private List getAccessibilitySliceData() { + final List sliceData = new ArrayList<>(); + + final String accessibilityControllerClassName = + AccessibilitySlicePreferenceController.class.getName(); + final String fragmentClassName = AccessibilitySettings.class.getName(); + final CharSequence screenTitle = mContext.getText(R.string.accessibility_settings); + + final SliceData.Builder sliceDataBuilder = new SliceData.Builder() + .setFragmentName(fragmentClassName) + .setScreenTitle(screenTitle) + .setPreferenceControllerClassName(accessibilityControllerClassName); + + final Set a11yServiceNames = new HashSet<>(); + Collections.addAll(a11yServiceNames, mContext.getResources() + .getStringArray(R.array.config_settings_slices_accessibility_components)); + final List installedServices = getAccessibilityServiceInfoList(); + final PackageManager packageManager = mContext.getPackageManager(); + + for (AccessibilityServiceInfo a11yServiceInfo : installedServices) { + final ResolveInfo resolveInfo = a11yServiceInfo.getResolveInfo(); + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + final String packageName = serviceInfo.packageName; + final ComponentName componentName = new ComponentName(packageName, serviceInfo.name); + final String flattenedName = componentName.flattenToString(); + + if (!a11yServiceNames.contains(flattenedName)) { + continue; + } + + final String title = resolveInfo.loadLabel(packageManager).toString(); + int iconResource = resolveInfo.getIconResource(); + if (iconResource == 0) { + iconResource = R.mipmap.ic_accessibility_generic; + } + + sliceDataBuilder.setKey(flattenedName) + .setTitle(title) + .setIcon(iconResource) + .setSliceType(SliceData.SliceType.SWITCH); + + sliceData.add(sliceDataBuilder.build()); + } + + return sliceData; + } + + @VisibleForTesting + List getAccessibilityServiceInfoList() { + final AccessibilityManager accessibilityManager = AccessibilityManager.getInstance( + mContext); + return accessibilityManager.getInstalledAccessibilityServiceList(); + } } \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesDatabaseAccessor.java b/src/com/android/settings/slices/SlicesDatabaseAccessor.java index 82b3506f739..7823d217cc8 100644 --- a/src/com/android/settings/slices/SlicesDatabaseAccessor.java +++ b/src/com/android/settings/slices/SlicesDatabaseAccessor.java @@ -76,7 +76,7 @@ public class SlicesDatabaseAccessor { */ public SliceData getSliceDataFromKey(String key) { Cursor cursor = getIndexedSliceData(key); - return buildSliceData(cursor, null /* uri */, false /* isInlineOnly */); + return buildSliceData(cursor, null /* uri */, false /* isIntentOnly */); } private Cursor getIndexedSliceData(String path) { @@ -111,7 +111,7 @@ public class SlicesDatabaseAccessor { .toString(); } - private SliceData buildSliceData(Cursor cursor, Uri uri, boolean isInlineOnly) { + private SliceData buildSliceData(Cursor cursor, Uri uri, boolean isIntentOnly) { final String key = cursor.getString(cursor.getColumnIndex(IndexColumns.KEY)); final String title = cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE)); final String summary = cursor.getString(cursor.getColumnIndex(IndexColumns.SUMMARY)); @@ -127,7 +127,7 @@ public class SlicesDatabaseAccessor { int sliceType = cursor.getInt( cursor.getColumnIndex(IndexColumns.SLICE_TYPE)); - if (!isInlineOnly) { + if (isIntentOnly) { sliceType = SliceData.SliceType.INTENT; } diff --git a/src/com/android/settings/slices/SlicesIndexer.java b/src/com/android/settings/slices/SlicesIndexer.java index d7de7bc27a9..e2ab40d407d 100644 --- a/src/com/android/settings/slices/SlicesIndexer.java +++ b/src/com/android/settings/slices/SlicesIndexer.java @@ -104,7 +104,7 @@ class SlicesIndexer implements Runnable { values.put(IndexColumns.KEY, dataRow.getKey()); values.put(IndexColumns.TITLE, dataRow.getTitle()); values.put(IndexColumns.SUMMARY, dataRow.getSummary()); - values.put(IndexColumns.SCREENTITLE, dataRow.getScreenTitle()); + values.put(IndexColumns.SCREENTITLE, dataRow.getScreenTitle().toString()); values.put(IndexColumns.ICON_RESOURCE, dataRow.getIconResource()); values.put(IndexColumns.FRAGMENT, dataRow.getFragmentClassName()); values.put(IndexColumns.CONTROLLER, dataRow.getPreferenceController()); diff --git a/tests/robotests/assets/grandfather_slice_controller_not_in_xml b/tests/robotests/assets/grandfather_slice_controller_not_in_xml index 5a0999739df..d2274e65faf 100644 --- a/tests/robotests/assets/grandfather_slice_controller_not_in_xml +++ b/tests/robotests/assets/grandfather_slice_controller_not_in_xml @@ -1,2 +1,4 @@ com.android.settings.testutils.FakeToggleController -com.android.settings.testutils.FakeSliderController \ No newline at end of file +com.android.settings.testutils.FakeSliderController +com.android.settings.core.TogglePreferenceControllerTest$FakeToggle +com.android.settings.accessibility.AccessibilitySlicePreferenceController diff --git a/tests/robotests/res/values-mcc999/config.xml b/tests/robotests/res/values-mcc999/config.xml index 39620d8e0a5..cecc9c59c59 100644 --- a/tests/robotests/res/values-mcc999/config.xml +++ b/tests/robotests/res/values-mcc999/config.xml @@ -62,4 +62,9 @@ false false true + + + + fake_package/fake_service + diff --git a/tests/robotests/src/com/android/settings/accessibility/AccessibilitySlicePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySlicePreferenceControllerTest.java new file mode 100644 index 00000000000..fe6d1a3f5fd --- /dev/null +++ b/tests/robotests/src/com/android/settings/accessibility/AccessibilitySlicePreferenceControllerTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.settings.accessibility; + +import static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.DISABLED_UNSUPPORTED; + +import static com.google.common.truth.Truth.assertThat; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.provider.Settings; +import android.view.accessibility.AccessibilityManager; + +import com.android.settings.accessibility.AccessibilitySlicePreferenceController; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.accessibility.AccessibilityUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowAccessibilityManager; +import org.xmlpull.v1.XmlPullParserException; + +@RunWith(SettingsRobolectricTestRunner.class) +public class AccessibilitySlicePreferenceControllerTest { + + private final String PACKAGE_NAME = "com.android.settings.fake"; + private final String CLASS_NAME = "com.android.settings.fake.classname"; + private final String SERVICE_NAME = PACKAGE_NAME + "/" + CLASS_NAME; + + private Context mContext; + + private AccessibilitySlicePreferenceController mController; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + + final ContentResolver contentResolver = mContext.getContentResolver(); + Settings.Secure.putInt(contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED, 1 /* on */); + Settings.Secure.putString(contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + SERVICE_NAME); + + // Register the fake a11y Service + ShadowAccessibilityManager shadowAccessibilityManager = Shadow.extract( + RuntimeEnvironment.application.getSystemService(AccessibilityManager.class)); + shadowAccessibilityManager.setInstalledAccessibilityServiceList(getFakeServiceList()); + + mController = new AccessibilitySlicePreferenceController(mContext, SERVICE_NAME); + } + + @Test + public void getAvailability_availableService_returnsAvailable() { + assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void getAvailability_unknownService_returnsUnsupported() { + AccessibilitySlicePreferenceController controller = + new AccessibilitySlicePreferenceController(mContext, "fake_service/name"); + + assertThat(controller.getAvailabilityStatus()).isEqualTo(DISABLED_UNSUPPORTED); + } + + @Test + public void setChecked_availableService_serviceIsEnabled() { + mController.setChecked(true); + + assertThat(mController.isChecked()).isTrue(); + } + + @Test + public void setNotChecked_availableService_serviceIsDisabled() { + mController.setChecked(false); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void isChecked_serviceEnabled_returnsTrue() { + AccessibilityUtils.setAccessibilityServiceState(mContext, + ComponentName.unflattenFromString(mController.getPreferenceKey()), true); + + assertThat(mController.isChecked()).isTrue(); + } + + @Test + public void isChecked_serviceNotEnabled_returnsFalse() { + AccessibilitySlicePreferenceController controller = + new AccessibilitySlicePreferenceController(mContext, "fake_service/name"); + + assertThat(controller.isChecked()).isFalse(); + } + + @Test(expected = IllegalArgumentException.class) + public void illegalServiceName_exceptionThrown() { + new AccessibilitySlicePreferenceController(mContext, "not_split_by_slash"); + } + + private List getFakeServiceList() { + final List infoList = new ArrayList<>(); + + final ServiceInfo serviceInfo = new ServiceInfo(); + serviceInfo.packageName = PACKAGE_NAME; + serviceInfo.name = CLASS_NAME; + + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.serviceInfo = serviceInfo; + + try { + final AccessibilityServiceInfo info = new AccessibilityServiceInfo(resolveInfo, + mContext); + ComponentName componentName = new ComponentName(PACKAGE_NAME, CLASS_NAME); + info.setComponentName(componentName); + infoList.add(info); + } catch (XmlPullParserException | IOException e) { + + } + + return infoList; + } +} diff --git a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java index 59eb7ceceaf..32cbd42cf54 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java @@ -277,7 +277,7 @@ public class SliceBuilderUtilsTest { final Pair pathPair = SliceBuilderUtils.getPathData(uri); - assertThat(pathPair.first).isFalse(); + assertThat(pathPair.first).isTrue(); assertThat(pathPair.second).isEqualTo(KEY); } @@ -291,7 +291,7 @@ public class SliceBuilderUtilsTest { final Pair pathPair = SliceBuilderUtils.getPathData(uri); - assertThat(pathPair.first).isTrue(); + assertThat(pathPair.first).isFalse(); assertThat(pathPair.second).isEqualTo(KEY); } @@ -316,7 +316,7 @@ public class SliceBuilderUtilsTest { final Pair pathPair = SliceBuilderUtils.getPathData(uri); - assertThat(pathPair.first).isTrue(); + assertThat(pathPair.first).isFalse(); assertThat(pathPair.second).isEqualTo(KEY + "/" + KEY); } diff --git a/tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java b/tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java index 36c27548c13..adc7a96e7e1 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceDataConverterTest.java @@ -17,8 +17,22 @@ package com.android.settings.slices; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.text.TextUtils; + +import com.android.settings.R; +import com.android.settings.accessibility.AccessibilitySettings; +import com.android.settings.accessibility.AccessibilitySlicePreferenceController; import com.android.settings.search.FakeIndexProvider; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.search.SearchFeatureProviderImpl; @@ -32,17 +46,29 @@ import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import java.util.ArrayList; import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) public class SliceDataConverterTest { - private final String fakeKey = "key"; - private final String fakeTitle = "title"; - private final String fakeSummary = "summary"; - private final String fakeScreenTitle = "screen_title"; - private final String fakeFragmentClassName = FakeIndexProvider.class.getName(); - private final String fakeControllerName = FakePreferenceController.class.getName(); + private final String FAKE_KEY = "key"; + private final String FAKE_TITLE = "title"; + private final String FAKE_SUMMARY = "summary"; + private final String FAKE_SCREEN_TITLE = "screen_title"; + private final String FAKE_FRAGMENT_CLASSNAME = FakeIndexProvider.class.getName(); + private final String FAKE_CONTROLLER_NAME = FakePreferenceController.class.getName(); + + private final String ACCESSIBILITY_FRAGMENT = AccessibilitySettings.class.getName(); + private final String A11Y_CONTROLLER_NAME = + AccessibilitySlicePreferenceController.class.getName(); + private final String FAKE_SERVICE_NAME = "fake_service"; + private final String FAKE_ACCESSIBILITY_PACKAGE = "fake_package"; + private final String FAKE_A11Y_SERVICE_NAME = + FAKE_ACCESSIBILITY_PACKAGE + "/" + FAKE_SERVICE_NAME; + private final int FAKE_ICON = 1234; + + private Context mContext; private SliceDataConverter mSliceDataConverter; private SearchFeatureProvider mSearchFeatureProvider; @@ -50,7 +76,8 @@ public class SliceDataConverterTest { @Before public void setUp() { - mSliceDataConverter = new SliceDataConverter(RuntimeEnvironment.application); + mContext = RuntimeEnvironment.application; + mSliceDataConverter = spy(new SliceDataConverter(RuntimeEnvironment.application)); mSearchFeatureProvider = new SearchFeatureProviderImpl(); mFakeFeatureFactory = FakeFeatureFactory.setupForTest(); mFakeFeatureFactory.searchFeatureProvider = mSearchFeatureProvider; @@ -68,20 +95,64 @@ public class SliceDataConverterTest { mSearchFeatureProvider.getSearchIndexableResources().getProviderValues() .add(FakeIndexProvider.class); + doReturn(getFakeService()).when(mSliceDataConverter).getAccessibilityServiceInfoList(); + List sliceDataList = mSliceDataConverter.getSliceData(); - assertThat(sliceDataList).hasSize(1); - SliceData fakeSlice = sliceDataList.get(0); + assertThat(sliceDataList).hasSize(2); + SliceData fakeSlice0 = sliceDataList.get(0); + SliceData fakeSlice1 = sliceDataList.get(1); - assertThat(fakeSlice.getKey()).isEqualTo(fakeKey); - assertThat(fakeSlice.getTitle()).isEqualTo(fakeTitle); - assertThat(fakeSlice.getSummary()).isEqualTo(fakeSummary); - assertThat(fakeSlice.getScreenTitle()).isEqualTo(fakeScreenTitle); + // Should not assume the order of the data list. + if (TextUtils.equals(fakeSlice0.getKey(), FAKE_KEY)) { + assertFakeSlice(fakeSlice0); + assertFakeA11ySlice(fakeSlice1); + } else { + assertFakeSlice(fakeSlice1); + assertFakeA11ySlice(fakeSlice0); + } + } + + private void assertFakeSlice(SliceData fakeSlice) { + assertThat(fakeSlice.getKey()).isEqualTo(FAKE_KEY); + assertThat(fakeSlice.getTitle()).isEqualTo(FAKE_TITLE); + assertThat(fakeSlice.getSummary()).isEqualTo(FAKE_SUMMARY); + assertThat(fakeSlice.getScreenTitle()).isEqualTo(FAKE_SCREEN_TITLE); assertThat(fakeSlice.getIconResource()).isNotNull(); assertThat(fakeSlice.getUri()).isNull(); - assertThat(fakeSlice.getFragmentClassName()).isEqualTo(fakeFragmentClassName); - assertThat(fakeSlice.getPreferenceController()).isEqualTo(fakeControllerName); - assertThat(fakeSlice.getSliceType()).isEqualTo(SliceData.SliceType.SLIDER); // from XML + assertThat(fakeSlice.getFragmentClassName()).isEqualTo(FAKE_FRAGMENT_CLASSNAME); + assertThat(fakeSlice.getPreferenceController()).isEqualTo(FAKE_CONTROLLER_NAME); + assertThat(fakeSlice.getSliceType()).isEqualTo(SliceData.SliceType.SLIDER); assertThat(fakeSlice.isPlatformDefined()).isTrue(); // from XML } + + private void assertFakeA11ySlice(SliceData fakeSlice) { + assertThat(fakeSlice.getKey()).isEqualTo(FAKE_A11Y_SERVICE_NAME); + assertThat(fakeSlice.getTitle()).isEqualTo(FAKE_TITLE); + assertThat(fakeSlice.getSummary()).isNull(); + assertThat(fakeSlice.getScreenTitle()).isEqualTo( + mContext.getString(R.string.accessibility_settings)); + assertThat(fakeSlice.getIconResource()).isEqualTo(FAKE_ICON); + assertThat(fakeSlice.getUri()).isNull(); + assertThat(fakeSlice.getFragmentClassName()).isEqualTo(ACCESSIBILITY_FRAGMENT); + assertThat(fakeSlice.getPreferenceController()).isEqualTo(A11Y_CONTROLLER_NAME); + } + + // This is fragile. Should be replaced by a proper fake Service if possible. + private List getFakeService() { + List serviceInfoList = new ArrayList<>(); + AccessibilityServiceInfo serviceInfo = spy(new AccessibilityServiceInfo()); + + ResolveInfo resolveInfo = spy(new ResolveInfo()); + resolveInfo.serviceInfo = new ServiceInfo(); + resolveInfo.serviceInfo.name = FAKE_SERVICE_NAME; + resolveInfo.serviceInfo.packageName = FAKE_ACCESSIBILITY_PACKAGE; + doReturn(FAKE_TITLE).when(resolveInfo).loadLabel(any(PackageManager.class)); + doReturn(FAKE_ICON).when(resolveInfo).getIconResource(); + + doReturn(resolveInfo).when(serviceInfo).getResolveInfo(); + serviceInfoList.add(serviceInfo); + + return serviceInfoList; + } } \ No newline at end of file From 7acd13f3e463575e6338dada36c1bf9d188aade3 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Thu, 12 Apr 2018 16:40:07 -0700 Subject: [PATCH 10/20] Update a Slice Uri when unavailable When a slice becomes unavailable, it should not update the underlying data even if the view has not changed. When we receive a change from an unavailable slice, notifychange on the Uri and do not change the underlying data. Change-Id: I91851ab668e4aece669fd65c14e0dc4ec2edefdf Fixes: 77980406 Test: robotests --- .../slices/SliceBroadcastReceiver.java | 16 +++- .../slices/SliceBroadcastReceiverTest.java | 85 ++++++++++++++++++- .../slices/SliceBuilderUtilsTest.java | 10 +-- .../testutils/FakeSliderController.java | 6 +- .../testutils/FakeToggleController.java | 5 +- .../FakeUnavailablePreferenceController.java | 4 +- 6 files changed, 111 insertions(+), 15 deletions(-) rename tests/robotests/src/com/android/settings/{slices => testutils}/FakeUnavailablePreferenceController.java (82%) diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index 80b7519430a..d2a6d101052 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -54,16 +54,16 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); final String key = intent.getStringExtra(EXTRA_SLICE_KEY); - final boolean isPlatformDefined = intent.getBooleanExtra(EXTRA_SLICE_PLATFORM_DEFINED, + final boolean isPlatformSlice = intent.getBooleanExtra(EXTRA_SLICE_PLATFORM_DEFINED, false /* default */); switch (action) { case ACTION_TOGGLE_CHANGED: - handleToggleAction(context, key, isPlatformDefined); + handleToggleAction(context, key, isPlatformSlice); break; case ACTION_SLIDER_CHANGED: int newPosition = intent.getIntExtra(Slice.EXTRA_RANGE_VALUE, -1); - handleSliderAction(context, key, newPosition); + handleSliderAction(context, key, newPosition, isPlatformSlice); break; case ACTION_WIFI_CHANGED: WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); @@ -95,6 +95,7 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { if (!controller.isAvailable()) { Log.w(TAG, "Can't update " + key + " since the setting is unavailable"); updateUri(context, key, isPlatformSlice); + return; } // TODO post context.getContentResolver().notifyChanged(uri, null) in the Toggle controller @@ -107,7 +108,8 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { updateUri(context, key, isPlatformSlice); } - private void handleSliderAction(Context context, String key, int newPosition) { + private void handleSliderAction(Context context, String key, int newPosition, + boolean isPlatformSlice) { if (TextUtils.isEmpty(key)) { throw new IllegalArgumentException( "No key passed to Intent for slider controller. Use extra: " + EXTRA_SLICE_KEY); @@ -123,6 +125,12 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { throw new IllegalArgumentException("Slider action passed for a non-slider key: " + key); } + if (!controller.isAvailable()) { + Log.w(TAG, "Can't update " + key + " since the setting is unavailable"); + updateUri(context, key, isPlatformSlice); + return; + } + final SliderPreferenceController sliderController = (SliderPreferenceController) controller; final int maxSteps = sliderController.getMaxSteps(); if (newPosition < 0 || newPosition > maxSteps) { diff --git a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java index 0cdb2f43d59..0b6e4b5bb32 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java @@ -18,17 +18,28 @@ package com.android.settings.slices; 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.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.slice.Slice; +import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.database.ContentObserver; import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.provider.Settings; +import android.provider.SettingsSlicesContract; import android.util.Pair; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.settings.core.BasePreferenceController; import com.android.settings.search.FakeIndexProvider; import com.android.settings.search.SearchFeatureProvider; import com.android.settings.search.SearchFeatureProviderImpl; @@ -36,6 +47,7 @@ import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeSliderController; import com.android.settings.testutils.FakeToggleController; +import com.android.settings.testutils.FakeUnavailablePreferenceController; import com.android.settings.testutils.SettingsRobolectricTestRunner; import org.junit.After; @@ -65,7 +77,7 @@ public class SliceBroadcastReceiverTest { @Before public void setUp() { - mContext = RuntimeEnvironment.application; + mContext = spy(RuntimeEnvironment.application); mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); mReceiver = new SliceBroadcastReceiver(); SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(mContext); @@ -192,6 +204,77 @@ public class SliceBroadcastReceiverTest { mReceiver.onReceive(mContext, intent); } + @Test + public void toggleUpdate_unavailableUriNotified() { + // Monitor the ContentResolver + final ContentResolver resolver = spy(mContext.getContentResolver()); + doReturn(resolver).when(mContext).getContentResolver(); + + // Disable Setting + Settings.Global.putInt(mContext.getContentResolver(), + FakeToggleController.AVAILABILITY_KEY, + BasePreferenceController.DISABLED_UNSUPPORTED); + + // Insert Fake Toggle into Database + final String key = "key"; + mSearchFeatureProvider.getSearchIndexableResources().getProviderValues().clear(); + insertSpecialCase(FakeToggleController.class, key); + + // Turn on toggle setting + final FakeToggleController fakeToggleController = new FakeToggleController(mContext, key); + fakeToggleController.setChecked(true); + + // Build Action + final Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, key); + + // Trigger Slice change + mReceiver.onReceive(mContext, intent); + + // Check the value is the same and the Uri has been notified. + assertThat(fakeToggleController.isChecked()).isTrue(); + final Uri expectedUri = SliceBuilderUtils.getUri( + SettingsSlicesContract.PATH_SETTING_ACTION + "/" + key, false); + verify(resolver).notifyChange(eq(expectedUri), eq(null)); + } + + @Test + public void sliderUpdate_unavailableUriNotified() { + // Monitor the ContentResolver + final ContentResolver resolver = spy(mContext.getContentResolver()); + doReturn(resolver).when(mContext).getContentResolver(); + + // Disable Setting + Settings.Global.putInt(mContext.getContentResolver(), + FakeSliderController.AVAILABILITY_KEY, + BasePreferenceController.DISABLED_UNSUPPORTED); + + // Insert Fake Slider into Database + final String key = "key"; + final int position = FakeSliderController.MAX_STEPS - 1; + final int oldPosition = FakeSliderController.MAX_STEPS; + mSearchFeatureProvider.getSearchIndexableResources().getProviderValues().clear(); + insertSpecialCase(FakeSliderController.class, key); + + // Set slider setting + final FakeSliderController fakeSliderController = new FakeSliderController(mContext, key); + fakeSliderController.setSliderPosition(oldPosition); + + // Build action + final Intent intent = new Intent(SettingsSliceProvider.ACTION_SLIDER_CHANGED); + intent.putExtra(Slice.EXTRA_RANGE_VALUE, position); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, key); + + // Trigger Slice change + mReceiver.onReceive(mContext, intent); + + // Check position is the same and the Uri has been notified. + assertThat(fakeSliderController.getSliderPosition()).isEqualTo(oldPosition); + final Uri expectedUri = SliceBuilderUtils.getUri( + SettingsSlicesContract.PATH_SETTING_ACTION + "/" + key, false); + verify(resolver).notifyChange(eq(expectedUri), eq(null)); + } + private void insertSpecialCase(String key) { insertSpecialCase(fakeControllerName, key); } diff --git a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java index 59eb7ceceaf..0672c8679ea 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import android.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; @@ -40,6 +39,7 @@ import com.android.settings.core.BasePreferenceController; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeSliderController; import com.android.settings.testutils.FakeToggleController; +import com.android.settings.testutils.FakeUnavailablePreferenceController; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.SliceTester; @@ -324,7 +324,7 @@ public class SliceBuilderUtilsTest { public void testUnsupportedSlice_validTitleSummary() { final SliceData data = getDummyData(FakeUnavailablePreferenceController.class, SliceData.SliceType.SWITCH); - Settings.System.putInt(mContext.getContentResolver(), + Settings.Global.putInt(mContext.getContentResolver(), FakeUnavailablePreferenceController.AVAILABILITY_KEY, BasePreferenceController.DISABLED_UNSUPPORTED); @@ -337,7 +337,7 @@ public class SliceBuilderUtilsTest { public void testDisabledForUserSlice_validTitleSummary() { final SliceData data = getDummyData(FakeUnavailablePreferenceController.class, SliceData.SliceType.SWITCH); - Settings.System.putInt(mContext.getContentResolver(), + Settings.Global.putInt(mContext.getContentResolver(), FakeUnavailablePreferenceController.AVAILABILITY_KEY, BasePreferenceController.DISABLED_FOR_USER); @@ -350,7 +350,7 @@ public class SliceBuilderUtilsTest { public void testDisabledDependentSettingSlice_validTitleSummary() { final SliceData data = getDummyData(FakeUnavailablePreferenceController.class, SliceData.SliceType.INTENT); - Settings.System.putInt(mContext.getContentResolver(), + Settings.Global.putInt(mContext.getContentResolver(), FakeUnavailablePreferenceController.AVAILABILITY_KEY, BasePreferenceController.DISABLED_DEPENDENT_SETTING); @@ -372,7 +372,7 @@ public class SliceBuilderUtilsTest { public void testUnavailableUnknownSlice_validTitleSummary() { final SliceData data = getDummyData(FakeUnavailablePreferenceController.class, SliceData.SliceType.SWITCH); - Settings.System.putInt(mContext.getContentResolver(), + Settings.Global.putInt(mContext.getContentResolver(), FakeUnavailablePreferenceController.AVAILABILITY_KEY, BasePreferenceController.UNAVAILABLE_UNKNOWN); diff --git a/tests/robotests/src/com/android/settings/testutils/FakeSliderController.java b/tests/robotests/src/com/android/settings/testutils/FakeSliderController.java index f4f91ed2382..530bdee97f3 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeSliderController.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeSliderController.java @@ -19,13 +19,14 @@ package com.android.settings.testutils; import android.content.Context; import android.provider.Settings; -import com.android.settings.core.BasePreferenceController; import com.android.settings.core.SliderPreferenceController; public class FakeSliderController extends SliderPreferenceController { private final String settingKey = "fake_slider_key"; + public static final String AVAILABILITY_KEY = "fake_slider_availability_key"; + public static final int MAX_STEPS = 9; public FakeSliderController(Context context, String key) { @@ -49,6 +50,7 @@ public class FakeSliderController extends SliderPreferenceController { @Override public int getAvailabilityStatus() { - return BasePreferenceController.AVAILABLE; + return Settings.Global.getInt(mContext.getContentResolver(), + AVAILABILITY_KEY, AVAILABLE); } } diff --git a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java index c984c6ceca4..d0ce76fe60a 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java @@ -26,6 +26,8 @@ public class FakeToggleController extends TogglePreferenceController { private String settingKey = "toggle_key"; + public static final String AVAILABILITY_KEY = "fake_toggle_availability_key"; + private final int ON = 1; private final int OFF = 0; @@ -47,6 +49,7 @@ public class FakeToggleController extends TogglePreferenceController { @Override public int getAvailabilityStatus() { - return AVAILABLE; + return Settings.Global.getInt(mContext.getContentResolver(), + AVAILABILITY_KEY, AVAILABLE); } } diff --git a/tests/robotests/src/com/android/settings/slices/FakeUnavailablePreferenceController.java b/tests/robotests/src/com/android/settings/testutils/FakeUnavailablePreferenceController.java similarity index 82% rename from tests/robotests/src/com/android/settings/slices/FakeUnavailablePreferenceController.java rename to tests/robotests/src/com/android/settings/testutils/FakeUnavailablePreferenceController.java index a7e5d7562aa..1ceaad864e1 100644 --- a/tests/robotests/src/com/android/settings/slices/FakeUnavailablePreferenceController.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeUnavailablePreferenceController.java @@ -1,4 +1,4 @@ -package com.android.settings.slices; +package com.android.settings.testutils; import android.content.Context; import android.provider.Settings; @@ -15,7 +15,7 @@ public class FakeUnavailablePreferenceController extends BasePreferenceControlle @Override public int getAvailabilityStatus() { - return Settings.System.getInt(mContext.getContentResolver(), + return Settings.Global.getInt(mContext.getContentResolver(), AVAILABILITY_KEY, 0); } } From 6730a061b1c297eca8f38ab3cfff620da10cbdb3 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Mon, 26 Mar 2018 17:30:20 -0700 Subject: [PATCH 11/20] Add getDescendants to Settings Slice Provider getDescendants allows another app to collect a list of all valid Uris for Settings Slices. Important for search, assistant or launcher apps. This means searching our database for all valid keys and building a list of Uris for each of those keys. Fixes: 77808328 Test: robotests Change-Id: I3ae27e4661a7dcaab50b091ae2730013118af8a2 --- .../slices/SettingsSliceProvider.java | 86 ++++++++ .../settings/slices/SliceBuilderUtils.java | 2 +- .../slices/SlicesDatabaseAccessor.java | 45 ++++- .../slices/SettingsSliceProviderTest.java | 188 ++++++++++++++++++ .../slices/SliceBuilderUtilsTest.java | 6 +- .../slices/SlicesDatabaseAccessorTest.java | 98 ++++++--- 6 files changed, 392 insertions(+), 33 deletions(-) diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 1a73ea760e5..4b4b9c2bd90 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -18,19 +18,26 @@ package com.android.settings.slices; import android.app.PendingIntent; import android.app.slice.SliceManager; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Icon; import android.net.Uri; import android.net.wifi.WifiManager; +import android.provider.SettingsSlicesContract; import android.support.annotation.VisibleForTesting; import android.support.v4.graphics.drawable.IconCompat; +import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import com.android.settings.R; import com.android.settingslib.utils.ThreadUtils; import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.WeakHashMap; @@ -144,6 +151,85 @@ public class SettingsSliceProvider extends SliceProvider { return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData); } + /** + * Get a list of all valid Uris based on the keys indexed in the Slices database. + *

+ * This will return a list of {@link Uri uris} depending on {@param uri}, following: + * 1. Authority & Full Path -> Only {@param uri}. It is only a prefix for itself. + * 2. Authority & No path -> A list of authority/action/$KEY$, where + * {@code $KEY$} is a list of all Slice-enabled keys for the authority. + * 3. Authority & action path -> A list of authority/action/$KEY$, where + * {@code $KEY$} is a list of all Slice-enabled keys for the authority. + * 4. Empty authority & path -> A list of Uris with all keys for both supported authorities. + * 5. Else -> Empty list. + *

+ * Note that the authority will stay consistent with {@param uri}, and the list of valid Slice + * keys depends on if the authority is {@link SettingsSlicesContract#AUTHORITY} or + * {@link #SLICE_AUTHORITY}. + * + * @param uri The uri to look for descendants under. + * @returns all valid Settings uris for which {@param uri} is a prefix. + */ + @Override + public Collection onGetSliceDescendants(Uri uri) { + final List descendants = new ArrayList<>(); + final Pair pathData = SliceBuilderUtils.getPathData(uri); + + if (pathData != null) { + // Uri has a full path and will not have any descendants. + descendants.add(uri); + return descendants; + } + + final String authority = uri.getAuthority(); + final String pathPrefix = uri.getPath(); + final boolean isPathEmpty = pathPrefix.isEmpty(); + + // No path nor authority. Return all possible Uris. + if (isPathEmpty && TextUtils.isEmpty(authority)) { + final List platformKeys = mSlicesDatabaseAccessor.getSliceKeys( + true /* isPlatformSlice */); + final List oemKeys = mSlicesDatabaseAccessor.getSliceKeys( + false /* isPlatformSlice */); + final List allUris = buildUrisFromKeys(platformKeys, + SettingsSlicesContract.AUTHORITY); + allUris.addAll(buildUrisFromKeys(oemKeys, SettingsSliceProvider.SLICE_AUTHORITY)); + + return allUris; + } + + // Path is anything but empty, "action", or "intent". Return empty list. + if (!isPathEmpty + && !TextUtils.equals(pathPrefix, "/" + SettingsSlicesContract.PATH_SETTING_ACTION) + && !TextUtils.equals(pathPrefix, + "/" + SettingsSlicesContract.PATH_SETTING_INTENT)) { + // Invalid path prefix, there are no valid Uri descendants. + return descendants; + } + + // Can assume authority belongs to the provider. Return all Uris for the authority. + final boolean isPlatformUri = TextUtils.equals(authority, SettingsSlicesContract.AUTHORITY); + final List keys = mSlicesDatabaseAccessor.getSliceKeys(isPlatformUri); + return buildUrisFromKeys(keys, authority); + } + + private List buildUrisFromKeys(List keys, String authority) { + final List descendants = new ArrayList<>(); + + final Uri.Builder builder = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION); + + final String newUriPathPrefix = SettingsSlicesContract.PATH_SETTING_ACTION + "/"; + for (String key : keys) { + builder.path(newUriPathPrefix + key); + descendants.add(builder.build()); + } + + return descendants; + } + @VisibleForTesting void loadSlice(Uri uri) { long startBuildTime = System.currentTimeMillis(); diff --git a/src/com/android/settings/slices/SliceBuilderUtils.java b/src/com/android/settings/slices/SliceBuilderUtils.java index a2479a2da05..e6ba4a8e856 100644 --- a/src/com/android/settings/slices/SliceBuilderUtils.java +++ b/src/com/android/settings/slices/SliceBuilderUtils.java @@ -130,7 +130,7 @@ public class SliceBuilderUtils { // Example: "/action/wifi" -> [{}, "action", "wifi"] // "/action/longer/path" -> [{}, "action", "longer/path"] if (split.length != 3) { - throw new IllegalArgumentException("Uri (" + uri + ") has incomplete path: " + path); + return null; } final boolean isInline = TextUtils.equals(SettingsSlicesContract.PATH_SETTING_ACTION, diff --git a/src/com/android/settings/slices/SlicesDatabaseAccessor.java b/src/com/android/settings/slices/SlicesDatabaseAccessor.java index 82b3506f739..0509f9c1f9f 100644 --- a/src/com/android/settings/slices/SlicesDatabaseAccessor.java +++ b/src/com/android/settings/slices/SlicesDatabaseAccessor.java @@ -29,6 +29,9 @@ import android.util.Pair; import com.android.settings.overlay.FeatureFactory; import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns; +import java.util.ArrayList; +import java.util.List; + import androidx.slice.Slice; /** @@ -51,10 +54,12 @@ public class SlicesDatabaseAccessor { // Cursor value for boolean true private final int TRUE = 1; - Context mContext; + private final Context mContext; + private final SlicesDatabaseHelper mHelper; public SlicesDatabaseAccessor(Context context) { mContext = context; + mHelper = SlicesDatabaseHelper.getInstance(mContext); } /** @@ -79,16 +84,44 @@ public class SlicesDatabaseAccessor { return buildSliceData(cursor, null /* uri */, false /* isInlineOnly */); } + /** + * @return a list of keys in the Slices database matching on {@param isPlatformSlice}. + */ + public List getSliceKeys(boolean isPlatformSlice) { + final String whereClause; + + if (isPlatformSlice) { + whereClause = IndexColumns.PLATFORM_SLICE + " = 1"; + } else { + whereClause = IndexColumns.PLATFORM_SLICE + " = 0"; + } + + final SQLiteDatabase database = mHelper.getReadableDatabase(); + final String[] columns = new String[]{IndexColumns.KEY}; + final List keys = new ArrayList<>(); + + try (final Cursor resultCursor = database.query(TABLE_SLICES_INDEX, columns, whereClause, + null /* selection */, null /* groupBy */, null /* having */, null /* orderBy */)) { + if (!resultCursor.moveToFirst()) { + return keys; + } + + do { + keys.add(resultCursor.getString(0 /* key index */)); + } while (resultCursor.moveToNext()); + } + + return keys; + } + private Cursor getIndexedSliceData(String path) { verifyIndexing(); final String whereClause = buildKeyMatchWhereClause(); - final SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(mContext); - final SQLiteDatabase database = helper.getReadableDatabase(); + final SQLiteDatabase database = mHelper.getReadableDatabase(); final String[] selection = new String[]{path}; - - Cursor resultCursor = database.query(TABLE_SLICES_INDEX, SELECT_COLUMNS_ALL, whereClause, - selection, null /* groupBy */, null /* having */, null /* orderBy */); + final Cursor resultCursor = database.query(TABLE_SLICES_INDEX, SELECT_COLUMNS_ALL, + whereClause, selection, null /* groupBy */, null /* having */, null /* orderBy */); int numResults = resultCursor.getCount(); diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java index d54d16fc6a4..b5399eaa951 100644 --- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java +++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java @@ -17,6 +17,8 @@ package com.android.settings.slices; +import static android.content.ContentResolver.SCHEME_CONTENT; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; @@ -41,6 +43,7 @@ import org.robolectric.RuntimeEnvironment; import androidx.slice.Slice; +import java.util.Collection; import java.util.HashMap; /** @@ -114,7 +117,190 @@ public class SettingsSliceProviderTest { assertThat(cachedData).isNull(); } + @Test + public void getDescendantUris_fullActionUri_returnsSelf() { + final Uri uri = SliceBuilderUtils.getUri( + SettingsSlicesContract.PATH_SETTING_ACTION + "/key", true); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).containsExactly(uri); + } + + @Test + public void getDescendantUris_fullIntentUri_returnsSelf() { + final Uri uri = SliceBuilderUtils.getUri( + SettingsSlicesContract.PATH_SETTING_ACTION + "/key", true); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).containsExactly(uri); + } + + @Test + public void getDescendantUris_wrongPath_returnsEmpty() { + final Uri uri = SliceBuilderUtils.getUri("invalid_path", true); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).isEmpty(); + } + + @Test + public void getDescendantUris_invalidPath_returnsEmpty() { + final String key = "platform_key"; + insertSpecialCase(key, true /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath("invalid") + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).isEmpty(); + } + + @Test + public void getDescendantUris_platformSlice_doesNotReturnOEMSlice() { + insertSpecialCase("oem_key", false /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).isEmpty(); + } + + @Test + public void getDescendantUris_oemSlice_doesNotReturnPlatformSlice() { + insertSpecialCase("platform_key", true /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).isEmpty(); + } + + @Test + public void getDescendantUris_oemSlice_returnsOEMUriDescendant() { + final String key = "oem_key"; + insertSpecialCase(key, false /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .build(); + final Uri expectedUri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).containsExactly(expectedUri); + } + + @Test + public void getDescendantUris_oemSliceNoPath_returnsOEMUriDescendant() { + final String key = "oem_key"; + insertSpecialCase(key, false /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .build(); + final Uri expectedUri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).containsExactly(expectedUri); + } + + @Test + public void getDescendantUris_platformSlice_returnsPlatformUriDescendant() { + final String key = "platform_key"; + insertSpecialCase(key, true /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .build(); + final Uri expectedUri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).containsExactly(expectedUri); + } + + @Test + public void getDescendantUris_platformSliceNoPath_returnsPlatformUriDescendant() { + final String key = "platform_key"; + insertSpecialCase(key, true /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .build(); + final Uri expectedUri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).containsExactly(expectedUri); + } + + @Test + public void getDescendantUris_noAuthorityNorPath_returnsAllUris() { + final String platformKey = "platform_key"; + final String oemKey = "oemKey"; + insertSpecialCase(platformKey, true /* isPlatformSlice */); + insertSpecialCase(oemKey, false /* isPlatformSlice */); + final Uri uri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .build(); + final Uri expectedPlatformUri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSlicesContract.AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(platformKey) + .build(); + final Uri expectedOemUri = new Uri.Builder() + .scheme(SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(oemKey) + .build(); + + final Collection descendants = mProvider.onGetSliceDescendants(uri); + + assertThat(descendants).containsExactly(expectedPlatformUri, expectedOemUri); + } + private void insertSpecialCase(String key) { + insertSpecialCase(key, true); + } + + private void insertSpecialCase(String key, boolean isPlatformSlice) { ContentValues values = new ContentValues(); values.put(SlicesDatabaseHelper.IndexColumns.KEY, key); values.put(SlicesDatabaseHelper.IndexColumns.TITLE, TITLE); @@ -123,6 +309,8 @@ public class SettingsSliceProviderTest { values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, 1234); values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, "test"); values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, "test"); + values.put(SlicesDatabaseHelper.IndexColumns.PLATFORM_SLICE, isPlatformSlice); + values.put(SlicesDatabaseHelper.IndexColumns.SLICE_TYPE, SliceData.SliceType.INTENT); mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); } diff --git a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java index 59eb7ceceaf..a2a9e8fdbf3 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceBuilderUtilsTest.java @@ -295,14 +295,16 @@ public class SliceBuilderUtilsTest { assertThat(pathPair.second).isEqualTo(KEY); } - @Test(expected = IllegalArgumentException.class) + @Test public void getPathData_noKey_returnsNull() { final Uri uri = new Uri.Builder() .authority(SettingsSliceProvider.SLICE_AUTHORITY) .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) .build(); - SliceBuilderUtils.getPathData(uri); + final Pair pathPair = SliceBuilderUtils.getPathData(uri); + + assertThat(pathPair).isNull(); } @Test diff --git a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java index 77c9891da18..45ec06430b2 100644 --- a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java +++ b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java @@ -18,6 +18,7 @@ package com.android.settings.slices; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.spy; import android.content.ContentValues; @@ -35,15 +36,17 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; +import java.util.List; + @RunWith(SettingsRobolectricTestRunner.class) public class SlicesDatabaseAccessorTest { - private final String fakeTitle = "title"; - private final String fakeSummary = "summary"; - private final String fakeScreenTitle = "screen_title"; - private final int fakeIcon = 1234; - private final String fakeFragmentClassName = FakeIndexProvider.class.getName(); - private final String fakeControllerName = FakePreferenceController.class.getName(); + private final String FAKE_TITLE = "title"; + private final String FAKE_SUMMARY = "summary"; + private final String FAKE_SCREEN_TITLE = "screen_title"; + private final int FAKE_ICON = 1234; + private final String FAKE_FRAGMENT_NAME = FakeIndexProvider.class.getName(); + private final String FAKE_CONTROLLER_NAME = FakePreferenceController.class.getName(); private Context mContext; private SQLiteDatabase mDb; @@ -70,13 +73,13 @@ public class SlicesDatabaseAccessorTest { SliceData data = mAccessor.getSliceDataFromKey(key); assertThat(data.getKey()).isEqualTo(key); - assertThat(data.getTitle()).isEqualTo(fakeTitle); - assertThat(data.getSummary()).isEqualTo(fakeSummary); - assertThat(data.getScreenTitle()).isEqualTo(fakeScreenTitle); - assertThat(data.getIconResource()).isEqualTo(fakeIcon); - assertThat(data.getFragmentClassName()).isEqualTo(fakeFragmentClassName); + assertThat(data.getTitle()).isEqualTo(FAKE_TITLE); + assertThat(data.getSummary()).isEqualTo(FAKE_SUMMARY); + assertThat(data.getScreenTitle()).isEqualTo(FAKE_SCREEN_TITLE); + assertThat(data.getIconResource()).isEqualTo(FAKE_ICON); + assertThat(data.getFragmentClassName()).isEqualTo(FAKE_FRAGMENT_NAME); assertThat(data.getUri()).isNull(); - assertThat(data.getPreferenceController()).isEqualTo(fakeControllerName); + assertThat(data.getPreferenceController()).isEqualTo(FAKE_CONTROLLER_NAME); } @Test(expected = IllegalStateException.class) @@ -96,13 +99,13 @@ public class SlicesDatabaseAccessorTest { SliceData data = mAccessor.getSliceDataFromUri(uri); assertThat(data.getKey()).isEqualTo(key); - assertThat(data.getTitle()).isEqualTo(fakeTitle); - assertThat(data.getSummary()).isEqualTo(fakeSummary); - assertThat(data.getScreenTitle()).isEqualTo(fakeScreenTitle); - assertThat(data.getIconResource()).isEqualTo(fakeIcon); - assertThat(data.getFragmentClassName()).isEqualTo(fakeFragmentClassName); + assertThat(data.getTitle()).isEqualTo(FAKE_TITLE); + assertThat(data.getSummary()).isEqualTo(FAKE_SUMMARY); + assertThat(data.getScreenTitle()).isEqualTo(FAKE_SCREEN_TITLE); + assertThat(data.getIconResource()).isEqualTo(FAKE_ICON); + assertThat(data.getFragmentClassName()).isEqualTo(FAKE_FRAGMENT_NAME); assertThat(data.getUri()).isEqualTo(uri); - assertThat(data.getPreferenceController()).isEqualTo(fakeControllerName); + assertThat(data.getPreferenceController()).isEqualTo(FAKE_CONTROLLER_NAME); } @Test(expected = IllegalStateException.class) @@ -111,15 +114,62 @@ public class SlicesDatabaseAccessorTest { mAccessor.getSliceDataFromUri(uri); } + @Test + public void getDescendantUris_platformSlice_doesNotReturnOEMSlice() { + final String key = "oem_key"; + final boolean isPlatformSlice = false; + insertSpecialCase(key, isPlatformSlice); + final List keys = mAccessor.getSliceKeys(!isPlatformSlice); + + assertThat(keys).isEmpty(); + } + + @Test + public void getDescendantUris_oemSlice_doesNotReturnPlatformSlice() { + final String key = "platform_key"; + final boolean isPlatformSlice = true; + insertSpecialCase(key, isPlatformSlice); + final List keys = mAccessor.getSliceKeys(!isPlatformSlice); + + assertThat(keys).isEmpty(); + } + + @Test + public void getDescendantUris_oemSlice_returnsOEMUriDescendant() { + final String key = "oem_key"; + final boolean isPlatformSlice = false; + insertSpecialCase(key, isPlatformSlice); + final List keys = mAccessor.getSliceKeys(isPlatformSlice); + + assertThat(keys).containsExactly(key); + } + + @Test + public void getDescendantUris_platformSlice_returnsPlatformUriDescendant() { + final String key = "platform_key"; + final boolean isPlatformSlice = true; + insertSpecialCase(key, isPlatformSlice); + final List keys = mAccessor.getSliceKeys(isPlatformSlice); + + assertThat(keys).containsExactly(key); + } + private void insertSpecialCase(String key) { + insertSpecialCase(key, true); + } + + private void insertSpecialCase(String key, boolean isPlatformSlice) { ContentValues values = new ContentValues(); values.put(SlicesDatabaseHelper.IndexColumns.KEY, key); - values.put(SlicesDatabaseHelper.IndexColumns.TITLE, fakeTitle); - values.put(SlicesDatabaseHelper.IndexColumns.SUMMARY, fakeSummary); - values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, fakeScreenTitle); - values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, fakeIcon); - values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, fakeFragmentClassName); - values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, fakeControllerName); + values.put(SlicesDatabaseHelper.IndexColumns.TITLE, FAKE_TITLE); + values.put(SlicesDatabaseHelper.IndexColumns.SUMMARY, FAKE_SUMMARY); + values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, FAKE_SCREEN_TITLE); + values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, FAKE_ICON); + values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, FAKE_FRAGMENT_NAME); + values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, FAKE_CONTROLLER_NAME); + values.put(SlicesDatabaseHelper.IndexColumns.PLATFORM_SLICE, isPlatformSlice); + values.put(SlicesDatabaseHelper.IndexColumns.SLICE_TYPE, SliceData.SliceType.INTENT); + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); } From d35c8d67061bdbf5454fe5728157a25b6bc68d75 Mon Sep 17 00:00:00 2001 From: Salvador Martinez Date: Tue, 17 Apr 2018 11:02:44 -0700 Subject: [PATCH 12/20] Update preference to set password field The preference for the wifi tether password was having the input type set incorrectly. This could lead to strange keyboard behavior and the password being remembered by the keyboard which is bad. This CL sets the appropriate field where needed. Test: robotests Bug: 77853620 Change-Id: I2a72fc863cd3779aed98a0df95500b652bb51cc8 --- .../widget/ValidatedEditTextPreference.java | 2 +- .../WifiTetherPasswordPreferenceController.java | 1 + .../widget/ValidatedEditTextPreferenceTest.java | 2 +- ...iTetherPasswordPreferenceControllerTest.java | 17 +++++++++++++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/widget/ValidatedEditTextPreference.java b/src/com/android/settings/widget/ValidatedEditTextPreference.java index a5bab1cd980..707da000f14 100644 --- a/src/com/android/settings/widget/ValidatedEditTextPreference.java +++ b/src/com/android/settings/widget/ValidatedEditTextPreference.java @@ -74,7 +74,7 @@ public class ValidatedEditTextPreference extends CustomEditTextPreference { editText.removeTextChangedListener(mTextWatcher); if (mIsPassword) { editText.setInputType( - InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); editText.setMaxLines(1); } editText.addTextChangedListener(mTextWatcher); diff --git a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java index b38558c1aeb..5ba0583b6ab 100644 --- a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java +++ b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java @@ -55,6 +55,7 @@ public class WifiTetherPasswordPreferenceController extends WifiTetherBasePrefer mPassword = generateRandomPassword(); } ((ValidatedEditTextPreference) mPreference).setValidator(this); + ((ValidatedEditTextPreference) mPreference).setIsPassword(true); ((ValidatedEditTextPreference) mPreference).setIsSummaryPassword(true); updatePasswordDisplay((EditTextPreference) mPreference); } diff --git a/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java b/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java index 5ba9f8abe58..865422c9354 100644 --- a/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java +++ b/tests/robotests/src/com/android/settings/widget/ValidatedEditTextPreferenceTest.java @@ -116,7 +116,7 @@ public class ValidatedEditTextPreferenceTest { mPreference.onBindDialogView(mView); assertThat(editText.getInputType() - & (InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_CLASS_TEXT)) + & (InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD | InputType.TYPE_CLASS_TEXT)) .isNotEqualTo(0); } diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java index 60faa2e0702..7e757ad5f84 100644 --- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java @@ -145,4 +145,21 @@ public class WifiTetherPasswordPreferenceControllerTest { assertThat(mController.getSecuritySettingForPassword()) .isEqualTo(WifiConfiguration.KeyMgmt.WPA2_PSK); } + + @Test + public void updateDisplay_shouldSetInputType() { + // Set controller password to anything and verify is set. + mController.displayPreference(mScreen); + mController.onPreferenceChange(mPreference, "1"); + assertThat(mController.getPassword()).isEqualTo("1"); + + // Create a new config using different password + final WifiConfiguration config = new WifiConfiguration(); + config.preSharedKey = "test_1234"; + when(mWifiManager.getWifiApConfiguration()).thenReturn(config); + + // Call updateDisplay and verify it's changed. + mController.updateDisplay(); + assertThat(mPreference.isPassword()).isTrue(); + } } From 014ed74845479e4d439b3b2c17fee70644d616da Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Tue, 17 Apr 2018 13:33:41 -0700 Subject: [PATCH 13/20] Disable autofill on choose password layout Change-Id: I5f09282cfa07c546172d0921e02db98b4b4c2fff Fixes: 78173714 Test: manual --- res/layout/choose_lock_password.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/res/layout/choose_lock_password.xml b/res/layout/choose_lock_password.xml index f1b631e3ae5..2c94524069e 100644 --- a/res/layout/choose_lock_password.xml +++ b/res/layout/choose_lock_password.xml @@ -20,6 +20,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:icon="@drawable/ic_lock" + android:importantForAutofill="noExcludeDescendants" settings:suwFooter="@layout/choose_lock_password_footer" settings:suwHeaderText="@string/lockpassword_choose_your_screen_lock_header"> From f0125b830a5781ec2d03aca5e799e1b09eccbfb7 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Tue, 17 Apr 2018 14:30:08 -0700 Subject: [PATCH 14/20] Fix text alignment with rtl languages in entity header. TextViews should align at viewStart to make numerics like version number display in RTL format. Also cleaned up style so we can share the same style on all entity header textviews. Change-Id: I25a93d33e9fb95a9eba7b368438cb33bb67a434a Fixes: 78173905 Test: visual --- res/layout-sw320dp/settings_entity_header.xml | 25 +++++------------- res/layout/settings_entity_header.xml | 26 +++++-------------- res/values/styles.xml | 9 +++++++ 3 files changed, 21 insertions(+), 39 deletions(-) diff --git a/res/layout-sw320dp/settings_entity_header.xml b/res/layout-sw320dp/settings_entity_header.xml index 79e12a92720..9a46adfda22 100644 --- a/res/layout-sw320dp/settings_entity_header.xml +++ b/res/layout-sw320dp/settings_entity_header.xml @@ -62,35 +62,22 @@ + android:layout_height="wrap_content"/> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> diff --git a/res/layout/settings_entity_header.xml b/res/layout/settings_entity_header.xml index b534c5ee3a3..6698f3fe870 100644 --- a/res/layout/settings_entity_header.xml +++ b/res/layout/settings_entity_header.xml @@ -55,36 +55,22 @@ + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> diff --git a/res/values/styles.xml b/res/values/styles.xml index 23ec20708ad..6ca47150a1e 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -423,6 +423,15 @@ 16sp + +