From cf663511db245a9f590ad49564496bcae9b38f0a Mon Sep 17 00:00:00 2001 From: Alexey Kuzmin Date: Tue, 24 Apr 2018 20:26:02 +0100 Subject: [PATCH 01/10] Add vibration preview Test: Open vibration intensity settings, tap on any option. The preview effect of selected intensity should be played back. Bug: 74504389 Merged-In: Ie22ecb830752edb7a7515a3a6475587e72274e51 Change-Id: Ie22ecb830752edb7a7515a3a6475587e72274e51 --- ...tificationVibrationPreferenceFragment.java | 7 +++++++ .../TouchVibrationPreferenceFragment.java | 7 +++++++ .../VibrationPreferenceFragment.java | 21 +++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java b/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java index 6340bb1417e..5f43c2d346f 100644 --- a/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/NotificationVibrationPreferenceFragment.java @@ -15,7 +15,9 @@ */ package com.android.settings.accessibility; +import android.media.AudioAttributes; import android.os.Vibrator; +import android.os.VibrationEffect; import android.provider.Settings; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -43,6 +45,11 @@ public class NotificationVibrationPreferenceFragment extends VibrationPreference return Settings.System.NOTIFICATION_VIBRATION_INTENSITY; } + @Override + protected int getPreviewVibrationAudioAttributesUsage() { + return AudioAttributes.USAGE_NOTIFICATION; + } + @Override protected int getDefaultVibrationIntensity() { Vibrator vibrator = getContext().getSystemService(Vibrator.class); diff --git a/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java b/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java index ea36833db54..1d2012452a6 100644 --- a/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/TouchVibrationPreferenceFragment.java @@ -16,7 +16,9 @@ package com.android.settings.accessibility; import android.graphics.drawable.Drawable; +import android.media.AudioAttributes; import android.os.Vibrator; +import android.os.VibrationEffect; import android.provider.Settings; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -50,6 +52,11 @@ public class TouchVibrationPreferenceFragment extends VibrationPreferenceFragmen return vibrator.getDefaultHapticFeedbackIntensity(); } + @Override + protected int getPreviewVibrationAudioAttributesUsage() { + return AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + } + @Override public void onVibrationIntensitySelected(int intensity) { // We want to keep HAPTIC_FEEDBACK_ENABLED consistent with this setting since some diff --git a/src/com/android/settings/accessibility/VibrationPreferenceFragment.java b/src/com/android/settings/accessibility/VibrationPreferenceFragment.java index e911b612e37..93fd7f5c8da 100644 --- a/src/com/android/settings/accessibility/VibrationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/VibrationPreferenceFragment.java @@ -21,8 +21,10 @@ import android.support.annotation.VisibleForTesting; import android.content.Context; import android.database.ContentObserver; import android.graphics.drawable.Drawable; +import android.media.AudioAttributes; import android.net.Uri; import android.os.Handler; +import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; import android.util.ArrayMap; @@ -105,6 +107,24 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm */ protected void onVibrationIntensitySelected(int intensity) { } + /** + * Play a vibration effect with intensity just selected by user + */ + protected void playVibrationPreview() { + Vibrator vibrator = getContext().getSystemService(Vibrator.class); + VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + AudioAttributes.Builder builder = new AudioAttributes.Builder(); + builder.setUsage(getPreviewVibrationAudioAttributesUsage()); + vibrator.vibrate(effect, builder.build()); + } + + /** + * Get the AudioAttributes usage for vibration preview. + */ + protected int getPreviewVibrationAudioAttributesUsage() { + return AudioAttributes.USAGE_UNKNOWN; + } + @Override protected List getCandidates() { List candidates = new ArrayList<>(mCandidates.values()); @@ -189,6 +209,7 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm @Override public void onChange(boolean selfChange, Uri uri) { updateCandidates(); + playVibrationPreview(); } } } From 20495a8c367fd793fe8f852694630a919b92184a Mon Sep 17 00:00:00 2001 From: Jason Monk Date: Wed, 23 May 2018 16:10:11 -0400 Subject: [PATCH 02/10] Follow SliceManager API changes Test: make Bug: 78898947 Change-Id: I5a46ccafe36ad2e0fdac745606d9907f07a86d0c --- .../settings/search/DeviceIndexUpdateJobService.java | 12 ++++++------ .../search/DeviceIndexUpdateJobServiceTest.java | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/search/DeviceIndexUpdateJobService.java b/src/com/android/settings/search/DeviceIndexUpdateJobService.java index 97b0a61ca6b..3eb904119bc 100644 --- a/src/com/android/settings/search/DeviceIndexUpdateJobService.java +++ b/src/com/android/settings/search/DeviceIndexUpdateJobService.java @@ -38,8 +38,8 @@ import java.util.concurrent.CountDownLatch; import androidx.slice.Slice; import androidx.slice.SliceItem; -import androidx.slice.SliceManager; -import androidx.slice.SliceManager.SliceCallback; +import androidx.slice.SliceViewManager; +import androidx.slice.SliceViewManager.SliceCallback; import androidx.slice.SliceMetadata; import androidx.slice.core.SliceQuery; import androidx.slice.widget.ListContent; @@ -80,7 +80,7 @@ public class DeviceIndexUpdateJobService extends JobService { } final DeviceIndexFeatureProvider indexProvider = FeatureFactory.getFactory(this) .getDeviceIndexFeatureProvider(); - final SliceManager manager = getSliceManager(); + final SliceViewManager manager = getSliceViewManager(); final Uri baseUri = new Builder() .scheme(ContentResolver.SCHEME_CONTENT) .authority(SettingsSliceProvider.SLICE_AUTHORITY) @@ -124,8 +124,8 @@ public class DeviceIndexUpdateJobService extends JobService { jobFinished(params, false); } - protected SliceManager getSliceManager() { - return SliceManager.getInstance(this); + protected SliceViewManager getSliceViewManager() { + return SliceViewManager.getInstance(this); } protected SliceMetadata getMetadata(Slice loadedSlice) { @@ -158,7 +158,7 @@ public class DeviceIndexUpdateJobService extends JobService { return null; } - protected Slice bindSliceSynchronous(SliceManager manager, Uri slice) { + protected Slice bindSliceSynchronous(SliceViewManager manager, Uri slice) { final Slice[] returnSlice = new Slice[1]; CountDownLatch latch = new CountDownLatch(1); SliceCallback callback = new SliceCallback() { diff --git a/tests/robotests/src/com/android/settings/search/DeviceIndexUpdateJobServiceTest.java b/tests/robotests/src/com/android/settings/search/DeviceIndexUpdateJobServiceTest.java index b5de9737db2..1c02b99323d 100644 --- a/tests/robotests/src/com/android/settings/search/DeviceIndexUpdateJobServiceTest.java +++ b/tests/robotests/src/com/android/settings/search/DeviceIndexUpdateJobServiceTest.java @@ -45,7 +45,7 @@ import java.util.ArrayList; import java.util.List; import androidx.slice.Slice; -import androidx.slice.SliceManager; +import androidx.slice.SliceViewManager; import androidx.slice.SliceMetadata; @RunWith(SettingsRobolectricTestRunner.class) @@ -57,17 +57,17 @@ public class DeviceIndexUpdateJobServiceTest { private Activity mActivity; private DeviceIndexUpdateJobService mJob; - private SliceManager mSliceManager; + private SliceViewManager mSliceManager; @Before public void setup() { FakeFeatureFactory.setupForTest(); mActivity = spy(Robolectric.buildActivity(Activity.class).create().visible().get()); mJob = spy(new DeviceIndexUpdateJobService()); - mSliceManager = mock(SliceManager.class); + mSliceManager = mock(SliceViewManager.class); doReturn(mActivity.getPackageName()).when(mJob).getPackageName(); - doReturn(mSliceManager).when(mJob).getSliceManager(); + doReturn(mSliceManager).when(mJob).getSliceViewManager(); doNothing().when(mJob).jobFinished(null, false); } @@ -144,4 +144,4 @@ public class DeviceIndexUpdateJobServiceTest { when(mSliceManager.getSliceDescendants(BASE_URI)).thenReturn(mUris); } -} \ No newline at end of file +} From 6719a9b73e7b72292a98db322a26396da6385db7 Mon Sep 17 00:00:00 2001 From: Chihhang Chuang Date: Thu, 29 Mar 2018 17:26:04 +0800 Subject: [PATCH 03/10] Refactor nfc preference controller - Remove BaseNfcPreferenceController. - NfcPreferenceController inherit from TogglePreferenceController. - AndroidBeamPreferenceController inherit from BasePreferenceController. - Override getIntentFilter in NfcPreferenceController to listen changes. - Add an API (hasAsyncUpdate) into BasePreferenceController to distinguish the setting which is updated asynchronously. Change-Id: I1abe4410169e305a0d6106e24c54e7f2e763fc91 Merged-In: I7c9c48ea7f1ad01a02524beabf9d30baa3db891f Fixes: 67997761 Fixes: 74887543 Test: RunSettingsRoboTests --- res/xml/connected_devices_advanced.xml | 2 + .../AdvancedConnectedDeviceController.java | 2 +- ...ancedConnectedDeviceDashboardFragment.java | 11 -- .../core/BasePreferenceController.java | 10 ++ .../settings/nfc/AndroidBeamEnabler.java | 5 - .../nfc/AndroidBeamPreferenceController.java | 55 ++++++- .../nfc/BaseNfcPreferenceController.java | 147 ------------------ .../settings/nfc/NfcAirplaneModeObserver.java | 81 ++++++++++ src/com/android/settings/nfc/NfcEnabler.java | 34 +--- .../settings/nfc/NfcPreferenceController.java | 98 +++++++++++- .../slices/SliceBroadcastReceiver.java | 8 +- ...AdvancedConnectedDeviceControllerTest.java | 3 +- .../core/BasePreferenceControllerTest.java | 5 + .../AndroidBeamPreferenceControllerTest.java | 3 +- .../nfc/NfcAirplaneModeObserverTest.java | 85 ++++++++++ .../nfc/NfcPreferenceControllerTest.java | 59 ++++++- .../slices/SliceBroadcastReceiverTest.java | 55 +++++++ .../testutils/FakeToggleController.java | 11 ++ .../testutils/shadow/ShadowNfcAdapter.java | 18 +++ 19 files changed, 471 insertions(+), 221 deletions(-) delete mode 100644 src/com/android/settings/nfc/BaseNfcPreferenceController.java create mode 100644 src/com/android/settings/nfc/NfcAirplaneModeObserver.java create mode 100644 tests/robotests/src/com/android/settings/nfc/NfcAirplaneModeObserverTest.java diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index f81fd791fde..41d3e9b6f9a 100644 --- a/res/xml/connected_devices_advanced.xml +++ b/res/xml/connected_devices_advanced.xml @@ -32,12 +32,14 @@ android:title="@string/nfc_quick_toggle_title" android:icon="@drawable/ic_nfc" android:summary="@string/nfc_quick_toggle_summary" + settings:controller="com.android.settings.nfc.NfcPreferenceController" android:order="-7"/> diff --git a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java index eb93eb178eb..3976c92f7b3 100644 --- a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java +++ b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceController.java @@ -50,7 +50,7 @@ public class AdvancedConnectedDeviceController extends BasePreferenceController */ public static int getConnectedDevicesSummaryResourceId(Context context) { final NfcPreferenceController nfcPreferenceController = - new NfcPreferenceController(context); + new NfcPreferenceController(context, NfcPreferenceController.KEY_TOGGLE_NFC); return getConnectedDevicesSummaryResourceId(nfcPreferenceController, isDrivingModeAvailable(context)); diff --git a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java index a64a430e638..9795e9faacb 100644 --- a/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java +++ b/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceDashboardFragment.java @@ -24,7 +24,6 @@ import com.android.settings.R; import com.android.settings.bluetooth.BluetoothFilesPreferenceController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.nfc.AndroidBeamPreferenceController; -import com.android.settings.nfc.NfcPreferenceController; import com.android.settings.print.PrintSettingPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; @@ -70,25 +69,15 @@ public class AdvancedConnectedDeviceDashboardFragment extends DashboardFragment Lifecycle lifecycle) { final List controllers = new ArrayList<>(); - final AndroidBeamPreferenceController beamPreferenceController = - new AndroidBeamPreferenceController(context); - controllers.add(beamPreferenceController); - controllers.add(new BluetoothFilesPreferenceController(context)); controllers.add(new BluetoothOnWhileDrivingPreferenceController(context)); final PrintSettingPreferenceController printerController = new PrintSettingPreferenceController(context); - final NfcPreferenceController nfcPreferenceController = - new NfcPreferenceController(context); if (lifecycle != null) { - lifecycle.addObserver(beamPreferenceController); lifecycle.addObserver(printerController); - lifecycle.addObserver(nfcPreferenceController); } - - controllers.add(nfcPreferenceController); controllers.add(printerController); return controllers; diff --git a/src/com/android/settings/core/BasePreferenceController.java b/src/com/android/settings/core/BasePreferenceController.java index c2d1cf52fa1..efd944866f8 100644 --- a/src/com/android/settings/core/BasePreferenceController.java +++ b/src/com/android/settings/core/BasePreferenceController.java @@ -246,6 +246,16 @@ public abstract class BasePreferenceController extends AbstractPreferenceControl return false; } + /** + * @return {@code true} if the setting update asynchronously. + *

+ * For example, a Wifi controller would return true, because it needs to update the radio + * and wait for it to turn on. + */ + public boolean hasAsyncUpdate() { + return false; + } + /** * Updates non-indexable keys for search provider. * diff --git a/src/com/android/settings/nfc/AndroidBeamEnabler.java b/src/com/android/settings/nfc/AndroidBeamEnabler.java index 66e42b64188..18087753531 100644 --- a/src/com/android/settings/nfc/AndroidBeamEnabler.java +++ b/src/com/android/settings/nfc/AndroidBeamEnabler.java @@ -20,7 +20,6 @@ import android.content.Context; import android.nfc.NfcAdapter; import android.os.UserHandle; import android.os.UserManager; -import android.support.v7.preference.Preference; import com.android.settings.R; import com.android.settingslib.RestrictedLockUtils; @@ -36,18 +35,14 @@ public class AndroidBeamEnabler extends BaseNfcEnabler { public AndroidBeamEnabler(Context context, RestrictedPreference preference) { super(context); - mPreference = preference; - mBeamDisallowedBySystem = RestrictedLockUtils.hasBaseUserRestriction(context, UserManager.DISALLOW_OUTGOING_BEAM, UserHandle.myUserId()); - if (!isNfcAvailable()) { // NFC is not supported mPreference.setEnabled(false); return; } - if (mBeamDisallowedBySystem) { mPreference.setEnabled(false); } diff --git a/src/com/android/settings/nfc/AndroidBeamPreferenceController.java b/src/com/android/settings/nfc/AndroidBeamPreferenceController.java index 6ae7fe5ccf2..b4026de15af 100644 --- a/src/com/android/settings/nfc/AndroidBeamPreferenceController.java +++ b/src/com/android/settings/nfc/AndroidBeamPreferenceController.java @@ -16,34 +16,75 @@ package com.android.settings.nfc; import android.content.Context; +import android.nfc.NfcAdapter; +import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.BasePreferenceController; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; -public class AndroidBeamPreferenceController extends BaseNfcPreferenceController { +import java.util.List; + +public class AndroidBeamPreferenceController extends BasePreferenceController + implements LifecycleObserver, OnResume, OnPause { public static final String KEY_ANDROID_BEAM_SETTINGS = "android_beam_settings"; + private final NfcAdapter mNfcAdapter; + private AndroidBeamEnabler mAndroidBeamEnabler; + private NfcAirplaneModeObserver mAirplaneModeObserver; - public AndroidBeamPreferenceController(Context context) { - super(context); + public AndroidBeamPreferenceController(Context context, String key) { + super(context, key); + mNfcAdapter = NfcAdapter.getDefaultAdapter(context); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (!isAvailable()) { + mAndroidBeamEnabler = null; return; } - mNfcEnabler = new AndroidBeamEnabler(mContext, (RestrictedPreference) mPreference); + final RestrictedPreference restrictedPreference = + (RestrictedPreference) screen.findPreference(getPreferenceKey()); + mAndroidBeamEnabler = new AndroidBeamEnabler(mContext, restrictedPreference); + + // Manually set dependencies for NFC when not toggleable. + if (!NfcPreferenceController.isToggleableInAirplaneMode(mContext)) { + mAirplaneModeObserver = new NfcAirplaneModeObserver(mContext, mNfcAdapter, + (Preference) restrictedPreference); + } } @Override - public String getPreferenceKey() { - return KEY_ANDROID_BEAM_SETTINGS; + @AvailabilityStatus + public int getAvailabilityStatus() { + return mNfcAdapter != null + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public void onResume() { + if (mAirplaneModeObserver != null) { + mAirplaneModeObserver.register(); + } + if (mAndroidBeamEnabler != null) { + mAndroidBeamEnabler.resume(); + } + } + + @Override + public void onPause() { + if (mAirplaneModeObserver != null) { + mAirplaneModeObserver.unregister(); + } + if (mAndroidBeamEnabler != null) { + mAndroidBeamEnabler.pause(); + } } } diff --git a/src/com/android/settings/nfc/BaseNfcPreferenceController.java b/src/com/android/settings/nfc/BaseNfcPreferenceController.java deleted file mode 100644 index 33d75fab672..00000000000 --- a/src/com/android/settings/nfc/BaseNfcPreferenceController.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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.nfc; - -import android.content.Context; -import android.database.ContentObserver; -import android.net.Uri; -import android.nfc.NfcAdapter; -import android.nfc.NfcManager; -import android.os.Handler; -import android.provider.Settings; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceScreen; - -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; -import com.android.settingslib.core.lifecycle.LifecycleObserver; -import com.android.settingslib.core.lifecycle.events.OnPause; -import com.android.settingslib.core.lifecycle.events.OnResume; - -import java.util.List; - -public abstract class BaseNfcPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin, LifecycleObserver, OnResume, OnPause { - - protected BaseNfcEnabler mNfcEnabler; - private NfcAdapter mNfcAdapter; - private int mAirplaneMode; - private AirplaneModeObserver mAirplaneModeObserver; - protected Preference mPreference; - - public BaseNfcPreferenceController(Context context) { - super(context); - mNfcAdapter = NfcAdapter.getDefaultAdapter(context); - } - - @Override - public void displayPreference(PreferenceScreen screen) { - super.displayPreference(screen); - - if (!isAvailable()) { - mNfcEnabler = null; - return; - } - - mPreference = screen.findPreference(getPreferenceKey()); - - // Manually set dependencies for NFC when not toggleable. - if (!isToggleableInAirplaneMode(mContext)) { - mAirplaneModeObserver = new AirplaneModeObserver(); - updateNfcPreference(); - } - } - - @Override - public void updateNonIndexableKeys(List keys) { - if (!isAvailable()) { - keys.add(getPreferenceKey()); - } - } - - @Override - public boolean isAvailable() { - return mNfcAdapter != null; - } - - public abstract String getPreferenceKey(); - - @Override - public void onResume() { - if (mAirplaneModeObserver != null) { - mAirplaneModeObserver.register(); - } - if (mNfcEnabler != null) { - mNfcEnabler.resume(); - } - } - - @Override - public void onPause() { - if (mAirplaneModeObserver != null) { - mAirplaneModeObserver.unregister(); - } - if (mNfcEnabler != null) { - mNfcEnabler.pause(); - } - } - - private void updateNfcPreference() { - final int airplaneMode = Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, mAirplaneMode); - if (airplaneMode == mAirplaneMode) { - return; - } - mAirplaneMode = airplaneMode; - boolean toggleable = mAirplaneMode != 1; - if (toggleable) { - mNfcAdapter.enable(); - } else { - mNfcAdapter.disable(); - } - mPreference.setEnabled(toggleable); - } - - public static boolean isToggleableInAirplaneMode(Context context) { - String toggleable = Settings.Global.getString(context.getContentResolver(), - Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); - return toggleable != null && toggleable.contains(Settings.Global.RADIO_NFC); - } - - private final class AirplaneModeObserver extends ContentObserver { - private final Uri AIRPLANE_MODE_URI = - Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON); - - private AirplaneModeObserver() { - super(new Handler()); - } - - public void register() { - mContext.getContentResolver().registerContentObserver(AIRPLANE_MODE_URI, false, this); - } - - public void unregister() { - mContext.getContentResolver().unregisterContentObserver(this); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - updateNfcPreference(); - } - } - -} diff --git a/src/com/android/settings/nfc/NfcAirplaneModeObserver.java b/src/com/android/settings/nfc/NfcAirplaneModeObserver.java new file mode 100644 index 00000000000..723d814391e --- /dev/null +++ b/src/com/android/settings/nfc/NfcAirplaneModeObserver.java @@ -0,0 +1,81 @@ +/* + * 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.nfc; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.nfc.NfcAdapter; +import android.os.Handler; +import android.os.Looper; +import android.provider.Settings; +import android.support.annotation.VisibleForTesting; +import android.support.v7.preference.Preference; + +/** + * NfcAirplaneModeObserver is a helper to manage the Nfc on/off when airplane mode status + * is changed. + */ +public class NfcAirplaneModeObserver extends ContentObserver { + + private final Context mContext; + private final NfcAdapter mNfcAdapter; + private final Preference mPreference; + private int mAirplaneMode; + + @VisibleForTesting + final static Uri AIRPLANE_MODE_URI = + Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON); + + public NfcAirplaneModeObserver(Context context, NfcAdapter nfcAdapter, Preference preference) { + super(new Handler(Looper.getMainLooper())); + mContext = context; + mNfcAdapter = nfcAdapter; + mPreference = preference; + updateNfcPreference(); + } + + public void register() { + mContext.getContentResolver().registerContentObserver(AIRPLANE_MODE_URI, false, this); + } + + public void unregister() { + mContext.getContentResolver().unregisterContentObserver(this); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + updateNfcPreference(); + } + + private void updateNfcPreference() { + final int airplaneMode = Settings.Global.getInt( + mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, mAirplaneMode); + if (airplaneMode == mAirplaneMode) { + return; + } + + mAirplaneMode = airplaneMode; + boolean toggleable = mAirplaneMode != 1; + if (toggleable) { + mNfcAdapter.enable(); + } else { + mNfcAdapter.disable(); + } + mPreference.setEnabled(toggleable); + } +} diff --git a/src/com/android/settings/nfc/NfcEnabler.java b/src/com/android/settings/nfc/NfcEnabler.java index 29cef993552..f39a0a1a466 100644 --- a/src/com/android/settings/nfc/NfcEnabler.java +++ b/src/com/android/settings/nfc/NfcEnabler.java @@ -18,52 +18,20 @@ package com.android.settings.nfc; import android.content.Context; import android.nfc.NfcAdapter; -import android.support.v7.preference.Preference; import android.support.v14.preference.SwitchPreference; /** * NfcEnabler is a helper to manage the Nfc on/off checkbox preference. It turns on/off Nfc * and ensures the summary of the preference reflects the current state. */ -public class NfcEnabler extends BaseNfcEnabler implements Preference.OnPreferenceChangeListener { +public class NfcEnabler extends BaseNfcEnabler { private final SwitchPreference mPreference; public NfcEnabler(Context context, SwitchPreference preference) { super(context); - mPreference = preference; } - public void resume() { - super.resume(); - if (isNfcAvailable()) { - mPreference.setOnPreferenceChangeListener(this); - } - } - - public void pause() { - super.pause(); - if (isNfcAvailable()) { - mPreference.setOnPreferenceChangeListener(null); - } - } - - public boolean onPreferenceChange(Preference preference, Object value) { - // Turn NFC on/off - - final boolean desiredState = (Boolean) value; - mPreference.setChecked(desiredState); - mPreference.setEnabled(false); - - if (desiredState) { - mNfcAdapter.enable(); - } else { - mNfcAdapter.disable(); - } - - return false; - } - @Override protected void handleNfcStateChanged(int newState) { switch (newState) { diff --git a/src/com/android/settings/nfc/NfcPreferenceController.java b/src/com/android/settings/nfc/NfcPreferenceController.java index a0678e0afab..a5d7c768ecf 100644 --- a/src/com/android/settings/nfc/NfcPreferenceController.java +++ b/src/com/android/settings/nfc/NfcPreferenceController.java @@ -16,35 +16,119 @@ package com.android.settings.nfc; import android.content.Context; +import android.content.IntentFilter; +import android.nfc.NfcAdapter; +import android.provider.Settings; +import android.text.TextUtils; + +import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; import android.support.v14.preference.SwitchPreference; -import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.TogglePreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; import com.android.settingslib.core.lifecycle.events.OnResume; +import java.util.List; -public class NfcPreferenceController extends BaseNfcPreferenceController { +public class NfcPreferenceController extends TogglePreferenceController + implements LifecycleObserver, OnResume, OnPause { public static final String KEY_TOGGLE_NFC = "toggle_nfc"; + private final NfcAdapter mNfcAdapter; + private NfcEnabler mNfcEnabler; + private NfcAirplaneModeObserver mAirplaneModeObserver; - public NfcPreferenceController(Context context) { - super(context); + public NfcPreferenceController(Context context, String key) { + super(context, key); + mNfcAdapter = NfcAdapter.getDefaultAdapter(context); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); if (!isAvailable()) { + mNfcEnabler = null; return; } - mNfcEnabler = new NfcEnabler(mContext, (SwitchPreference) mPreference); + final SwitchPreference switchPreference = + (SwitchPreference) screen.findPreference(getPreferenceKey()); + + mNfcEnabler = new NfcEnabler(mContext, switchPreference); + + // Manually set dependencies for NFC when not toggleable. + if (!isToggleableInAirplaneMode(mContext)) { + mAirplaneModeObserver = new NfcAirplaneModeObserver(mContext, + mNfcAdapter, (Preference) switchPreference); + } } @Override - public String getPreferenceKey() { - return KEY_TOGGLE_NFC; + public boolean isChecked() { + return mNfcAdapter.isEnabled(); + } + + @Override + public boolean setChecked(boolean isChecked) { + if (isChecked) { + mNfcAdapter.enable(); + } else { + mNfcAdapter.disable(); + } + return true; + } + + @Override + @AvailabilityStatus + public int getAvailabilityStatus() { + return mNfcAdapter != null + ? AVAILABLE + : UNSUPPORTED_ON_DEVICE; + } + + @Override + public IntentFilter getIntentFilter() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED); + filter.addAction(NfcAdapter.EXTRA_ADAPTER_STATE); + return filter; + } + + @Override + public boolean hasAsyncUpdate() { + return true; + } + + @Override + public boolean isSliceable() { + return TextUtils.equals(getPreferenceKey(), KEY_TOGGLE_NFC); + } + + @Override + public void onResume() { + if (mAirplaneModeObserver != null) { + mAirplaneModeObserver.register(); + } + if (mNfcEnabler != null) { + mNfcEnabler.resume(); + } + } + + @Override + public void onPause() { + if (mAirplaneModeObserver != null) { + mAirplaneModeObserver.unregister(); + } + if (mNfcEnabler != null) { + mNfcEnabler.pause(); + } + } + + public static boolean isToggleableInAirplaneMode(Context context) { + final String toggleable = Settings.Global.getString(context.getContentResolver(), + Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS); + return toggleable != null && toggleable.contains(Settings.Global.RADIO_NFC); } } diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index 80e3e3c1ef1..213bf00c028 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -106,7 +106,9 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { if (!controller.isAvailable()) { Log.w(TAG, "Can't update " + key + " since the setting is unavailable"); - updateUri(context, key, isPlatformSlice); + if (!controller.hasAsyncUpdate()) { + updateUri(context, key, isPlatformSlice); + } return; } @@ -115,7 +117,9 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { final TogglePreferenceController toggleController = (TogglePreferenceController) controller; toggleController.setChecked(isChecked); logSliceValueChange(context, key, isChecked ? 1 : 0); - updateUri(context, key, isPlatformSlice); + if (!controller.hasAsyncUpdate()) { + updateUri(context, key, isPlatformSlice); + } } private void handleSliderAction(Context context, String key, int newPosition, diff --git a/tests/robotests/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceControllerTest.java index ceea81eae8e..ce5580242f2 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/AdvancedConnectedDeviceControllerTest.java @@ -55,7 +55,8 @@ public class AdvancedConnectedDeviceControllerTest { mContext = spy(RuntimeEnvironment.application); mContentResolver = mContext.getContentResolver(); - mNfcController = new NfcPreferenceController(mContext); + mNfcController = new NfcPreferenceController(mContext, + NfcPreferenceController.KEY_TOGGLE_NFC); mShadowNfcAdapter = shadowOf(ShadowNfcAdapter.getNfcAdapter(mContext)); } diff --git a/tests/robotests/src/com/android/settings/core/BasePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/core/BasePreferenceControllerTest.java index eec4e37ccf2..ad2dabb1b7b 100644 --- a/tests/robotests/src/com/android/settings/core/BasePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/core/BasePreferenceControllerTest.java @@ -134,6 +134,11 @@ public class BasePreferenceControllerTest { assertThat(mPreferenceController.getSliceType()).isEqualTo(SliceData.SliceType.INTENT); } + @Test + public void hasAsyncUpdate_shouldReturnFalse() { + assertThat(mPreferenceController.hasAsyncUpdate()).isFalse(); + } + @Test public void settingAvailable_disabledOnDisplayPreference_preferenceEnabled() { final PreferenceScreen screen = mock(PreferenceScreen.class); diff --git a/tests/robotests/src/com/android/settings/nfc/AndroidBeamPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/nfc/AndroidBeamPreferenceControllerTest.java index df5bb64c1e8..8232afefd5f 100644 --- a/tests/robotests/src/com/android/settings/nfc/AndroidBeamPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/nfc/AndroidBeamPreferenceControllerTest.java @@ -72,7 +72,8 @@ public class AndroidBeamPreferenceControllerTest { UserManager.DISALLOW_OUTGOING_BEAM, UserHandle.myUserId())).thenReturn(false); when(NfcAdapter.getDefaultAdapter(mContext)).thenReturn(mNfcAdapter); - mAndroidBeamController = new AndroidBeamPreferenceController(mContext); + mAndroidBeamController = new AndroidBeamPreferenceController(mContext, + AndroidBeamPreferenceController.KEY_ANDROID_BEAM_SETTINGS); mAndroidBeamPreference = new RestrictedPreference(RuntimeEnvironment.application); when(mScreen.findPreference(mAndroidBeamController.getPreferenceKey())).thenReturn( mAndroidBeamPreference); diff --git a/tests/robotests/src/com/android/settings/nfc/NfcAirplaneModeObserverTest.java b/tests/robotests/src/com/android/settings/nfc/NfcAirplaneModeObserverTest.java new file mode 100644 index 00000000000..b6d28d192cb --- /dev/null +++ b/tests/robotests/src/com/android/settings/nfc/NfcAirplaneModeObserverTest.java @@ -0,0 +1,85 @@ +/* + * 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.nfc; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.nfc.NfcAdapter; +import android.provider.Settings; +import android.support.v7.preference.Preference; +import android.support.v14.preference.SwitchPreference; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowNfcAdapter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(shadows = {ShadowNfcAdapter.class}) +public class NfcAirplaneModeObserverTest { + + Context mContext; + private NfcAdapter mNfcAdapter; + private SwitchPreference mNfcPreference; + private NfcAirplaneModeObserver mNfcAirplaneModeObserver; + + @Before + public void setUp() { + mContext = ShadowApplication.getInstance().getApplicationContext(); + mNfcAdapter = NfcAdapter.getDefaultAdapter(mContext); + + mNfcPreference = new SwitchPreference(RuntimeEnvironment.application); + + mNfcAirplaneModeObserver = new NfcAirplaneModeObserver(mContext, mNfcAdapter, + (Preference) mNfcPreference); + } + + @Test + public void NfcAirplaneModeObserver_airplaneOn_shouldDisableNfc() { + ReflectionHelpers.setField(mNfcAirplaneModeObserver, + "mAirplaneMode", 0); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 1); + + mNfcAirplaneModeObserver.onChange(false, + NfcAirplaneModeObserver.AIRPLANE_MODE_URI); + + assertThat(mNfcAdapter.isEnabled()).isFalse(); + assertThat(mNfcPreference.isEnabled()).isFalse(); + } + + @Test + public void NfcAirplaneModeObserver_airplaneOff_shouldEnableNfc() { + ReflectionHelpers.setField(mNfcAirplaneModeObserver, + "mAirplaneMode", 1); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 0); + + mNfcAirplaneModeObserver.onChange(false, + NfcAirplaneModeObserver.AIRPLANE_MODE_URI); + + assertThat(mNfcAdapter.isEnabled()).isTrue(); + assertThat(mNfcPreference.isEnabled()).isTrue(); + } +} diff --git a/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java index 802e199abf2..008d1585705 100644 --- a/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/nfc/NfcPreferenceControllerTest.java @@ -19,6 +19,7 @@ package com.android.settings.nfc; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; @@ -26,8 +27,8 @@ import android.nfc.NfcAdapter; import android.nfc.NfcManager; import android.os.UserManager; import android.provider.Settings; -import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.PreferenceScreen; +import android.support.v14.preference.SwitchPreference; import com.android.settings.testutils.SettingsRobolectricTestRunner; @@ -68,8 +69,10 @@ public class NfcPreferenceControllerTest { when(mContext.getSystemService(Context.NFC_SERVICE)).thenReturn(mManager); when(NfcAdapter.getDefaultAdapter(mContext)).thenReturn(mNfcAdapter); - mNfcController = new NfcPreferenceController(mContext); + mNfcController = new NfcPreferenceController(mContext, + NfcPreferenceController.KEY_TOGGLE_NFC); mNfcPreference = new SwitchPreference(RuntimeEnvironment.application); + when(mScreen.findPreference(mNfcController.getPreferenceKey())).thenReturn(mNfcPreference); Settings.Global.putString(mContext.getContentResolver(), @@ -83,15 +86,17 @@ public class NfcPreferenceControllerTest { } @Test - public void isAvailable_hasNfc_shouldReturnTrue() { + public void getAvailabilityStatus_hasNfc_shouldReturnAvailable() { when(mNfcAdapter.isEnabled()).thenReturn(true); - assertThat(mNfcController.isAvailable()).isTrue(); + assertThat(mNfcController.getAvailabilityStatus()) + .isEqualTo(NfcPreferenceController.AVAILABLE); } @Test - public void isAvailable_noNfcAdapter_shouldReturnFalse() { + public void getAvailabilityStatus_noNfcAdapter_shouldReturnDisabledUnsupported() { ReflectionHelpers.setField(mNfcController, "mNfcAdapter", null); - assertThat(mNfcController.isAvailable()).isFalse(); + assertThat(mNfcController.getAvailabilityStatus()) + .isEqualTo(NfcPreferenceController.UNSUPPORTED_ON_DEVICE); } @Test @@ -157,4 +162,46 @@ public class NfcPreferenceControllerTest { assertThat(keys).hasSize(1); } + @Test + public void setChecked_True_nfcShouldEnable() { + mNfcController.setChecked(true); + mNfcController.onResume(); + + verify(mNfcAdapter).enable(); + } + + @Test + public void setChecked_False_nfcShouldDisable() { + mNfcController.setChecked(false); + mNfcController.onResume(); + + verify(mNfcAdapter).disable(); + } + + @Test + public void hasAsyncUpdate_shouldReturnTrue() { + assertThat(mNfcController.hasAsyncUpdate()).isTrue(); + } + + @Test + public void isToggleableInAirplaneMode_containNfc_shouldReturnTrue() { + Settings.Global.putString(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, + Settings.Global.RADIO_NFC); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 1); + + assertThat(NfcPreferenceController.isToggleableInAirplaneMode(mContext)).isTrue(); + } + + @Test + public void isToggleableInAirplaneMode_withoutNfc_shouldReturnFalse() { + Settings.Global.putString(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, + "null"); + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.AIRPLANE_MODE_ON, 1); + + assertThat(NfcPreferenceController.isToggleableInAirplaneMode(mContext)).isFalse(); + } } diff --git a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java index f03f88ed99a..5f0bc962476 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java @@ -23,6 +23,7 @@ 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.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -125,6 +126,60 @@ public class SliceBroadcastReceiverTest { assertThat(valuePair.second).isEqualTo(0); } + @Test + public void toggleUpdate_synchronously_notifyChange_should_be_called() { + // Monitor the ContentResolver + final ContentResolver resolver = spy(mContext.getContentResolver()); + doReturn(resolver).when(mContext).getContentResolver(); + + final String key = "key"; + mSearchFeatureProvider.getSearchIndexableResources().getProviderValues().clear(); + insertSpecialCase(key); + + FakeToggleController fakeToggleController = new FakeToggleController(mContext, key); + fakeToggleController.setChecked(true); + // Set the toggle setting update synchronously. + fakeToggleController.setAsyncUpdate(false); + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, key); + + assertThat(fakeToggleController.isChecked()).isTrue(); + + // Toggle setting + mReceiver.onReceive(mContext, intent); + + assertThat(fakeToggleController.isChecked()).isFalse(); + + final Uri expectedUri = SliceBuilderUtils.getUri( + SettingsSlicesContract.PATH_SETTING_ACTION + "/" + key, false); + verify(resolver).notifyChange(eq(expectedUri), eq(null)); + } + + @Test + public void toggleUpdate_asynchronously_notifyChange_should_not_be_called() { + // Monitor the ContentResolver + final ContentResolver resolver = spy(mContext.getContentResolver()); + doReturn(resolver).when(mContext).getContentResolver(); + + final String key = "key"; + mSearchFeatureProvider.getSearchIndexableResources().getProviderValues().clear(); + insertSpecialCase(key); + + FakeToggleController fakeToggleController = new FakeToggleController(mContext, key); + fakeToggleController.setChecked(true); + // Set the toggle setting update asynchronously. + fakeToggleController.setAsyncUpdate(true); + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, key); + + assertThat(fakeToggleController.isChecked()).isTrue(); + + // Toggle setting + mReceiver.onReceive(mContext, intent); + + verify(resolver, never()).notifyChange(null, null); + } + @Test public void onReceive_sliderChanged() { final String key = "key"; diff --git a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java index 680a04de106..8e408f00729 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeToggleController.java @@ -36,6 +36,8 @@ public class FakeToggleController extends TogglePreferenceController { private final int ON = 1; private final int OFF = 0; + private boolean mIsAsyncUpdate = false; + public FakeToggleController(Context context, String preferenceKey) { super(context, preferenceKey); } @@ -67,4 +69,13 @@ public class FakeToggleController extends TogglePreferenceController { public boolean isSliceable() { return true; } + + @Override + public boolean hasAsyncUpdate() { + return mIsAsyncUpdate; + } + + public void setAsyncUpdate(boolean isAsyncUpdate) { + mIsAsyncUpdate = isAsyncUpdate; + } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNfcAdapter.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNfcAdapter.java index e4421eee813..07c8e544d6a 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNfcAdapter.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowNfcAdapter.java @@ -33,6 +33,7 @@ import org.robolectric.util.ReflectionHelpers.ClassParameter; @Implements(NfcAdapter.class) public class ShadowNfcAdapter { private static boolean sReaderModeEnabled; + private boolean mIsNfcEnabled = false; @Implementation public void enableReaderMode(Activity activity, NfcAdapter.ReaderCallback callback, int flags, @@ -46,6 +47,23 @@ public class ShadowNfcAdapter { NfcAdapter.class, ClassParameter.from(Context.class, context)); } + @Implementation + public boolean isEnabled() { + return mIsNfcEnabled; + } + + @Implementation + public boolean enable() { + mIsNfcEnabled = true; + return true; + } + + @Implementation + public boolean disable() { + mIsNfcEnabled = false; + return true; + } + public static boolean isReaderModeEnabled() { return sReaderModeEnabled; } From 61227fb093c43a234d6ecf3a0033b1f1131dc875 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Wed, 23 May 2018 14:55:11 -0700 Subject: [PATCH 04/10] Add remaining whitelisted controllers Because I missed them in the long whitelist the first time... Change-Id: I9fbd7b33e06b3f2f6e5e5778f78abfdb1a52006a Merged-In: I01c8c80fe306667c1d3ac007b16fad546c5a5f40 Fixes: 79779103 Test: robotests --- res/xml/smart_battery_detail.xml | 1 + ...MagnificationGesturesPreferenceController.java | 7 +++++++ .../MagnificationNavbarPreferenceController.java | 7 +++++++ .../SmartBatteryPreferenceController.java | 6 ++++++ .../AlarmVolumePreferenceController.java | 6 ++++++ ...ificationGesturesPreferenceControllerTest.java | 15 +++++++++++++++ ...gnificationNavbarPreferenceControllerTest.java | 15 +++++++++++++++ .../SmartBatteryPreferenceControllerTest.java | 7 +++++++ .../AlarmVolumePreferenceControllerTest.java | 5 +++++ 9 files changed, 69 insertions(+) diff --git a/res/xml/smart_battery_detail.xml b/res/xml/smart_battery_detail.xml index e1246edd980..8af3e96d759 100644 --- a/res/xml/smart_battery_detail.xml +++ b/res/xml/smart_battery_detail.xml @@ -30,6 +30,7 @@ android:key="smart_battery" android:title="@string/smart_battery_title" android:summary="@string/smart_battery_summary" + settings:controller="com.android.settings.fuelgauge.SmartBatteryPreferenceController" settings:allowDividerAbove="true"/> Date: Thu, 24 May 2018 09:45:24 -0700 Subject: [PATCH 05/10] Update storage header background to colorPrimary Change-Id: I07f78cc6452846829b92d25fe9c41c6efe40a459 Merged-In: If13d5cbd2bd7db1c6d9c9f840281601ab78ba267 Fixes: 79773433 Test: visual --- res/layout/storage_summary_donut.xml | 2 +- .../android/settings/deviceinfo/StorageDashboardFragment.java | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/res/layout/storage_summary_donut.xml b/res/layout/storage_summary_donut.xml index ce7e272a85e..b183b2e9917 100644 --- a/res/layout/storage_summary_donut.xml +++ b/res/layout/storage_summary_donut.xml @@ -17,7 +17,7 @@ diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java index 8e56b12ec8f..e8f9771898a 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -57,14 +57,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -public class StorageDashboardFragment extends DashboardFragment - implements +public class StorageDashboardFragment extends DashboardFragment implements LoaderManager.LoaderCallbacks> { private static final String TAG = "StorageDashboardFrag"; private static final int STORAGE_JOB_ID = 0; private static final int ICON_JOB_ID = 1; private static final int VOLUME_SIZE_JOB_ID = 2; - private static final int OPTIONS_MENU_MIGRATE_DATA = 100; private VolumeInfo mVolume; private PrivateStorageInfo mStorageInfo; From a2ea9911010e14fe60bef262291a46425ed71cea Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Wed, 23 May 2018 13:03:27 -0700 Subject: [PATCH 06/10] Make summary text update after settings changes The uri's being pinged when changes happened were incorrect, because of a hard coded '/'. Change-Id: I4d04642bc80b97c5191bf0e1e7a5e15a2c58d6c9 Merged-In: I6735c5a60dc7df6894bd17e67d7702a7ec6c07d4 Fixes: 79779837 Test: robotest --- .../slices/SliceBroadcastReceiver.java | 13 +++++-- .../slices/SliceBroadcastReceiverTest.java | 34 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index 80e3e3c1ef1..1e413938ec9 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -27,6 +27,7 @@ import static com.android.settings.wifi.WifiSliceBuilder.ACTION_WIFI_SLICE_CHANG import android.app.slice.Slice; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; @@ -151,6 +152,7 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { sliderController.setSliderPosition(newPosition); logSliceValueChange(context, key, newPosition); + updateUri(context, key, isPlatformSlice); } /** @@ -173,8 +175,15 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { } private void updateUri(Context context, String key, boolean isPlatformDefined) { - final String path = SettingsSlicesContract.PATH_SETTING_ACTION + "/" + key; - final Uri uri = SliceBuilderUtils.getUri(path, isPlatformDefined); + final String authority = isPlatformDefined + ? SettingsSlicesContract.AUTHORITY + : SettingsSliceProvider.SLICE_AUTHORITY; + final Uri uri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(authority) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); context.getContentResolver().notifyChange(uri, null /* observer */); } } diff --git a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java index f03f88ed99a..21a02062855 100644 --- a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java +++ b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java @@ -95,8 +95,16 @@ public class SliceBroadcastReceiverTest { @Test public void onReceive_toggleChanged() { final String key = "key"; + final Uri uri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); mSearchFeatureProvider.getSearchIndexableResources().getProviderValues().clear(); insertSpecialCase(key); + final ContentResolver resolver = mock(ContentResolver.class); + doReturn(resolver).when(mContext).getContentResolver(); // Turn on toggle setting FakeToggleController fakeToggleController = new FakeToggleController(mContext, key); fakeToggleController.setChecked(true); @@ -120,6 +128,7 @@ public class SliceBroadcastReceiverTest { assertThat(namePair.first).isEqualTo(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME); assertThat(namePair.second).isEqualTo(fakeToggleController.getPreferenceKey()); + verify(resolver).notifyChange(uri, null); assertThat(valuePair.first) .isEqualTo(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE); assertThat(valuePair.second).isEqualTo(0); @@ -128,6 +137,14 @@ public class SliceBroadcastReceiverTest { @Test public void onReceive_sliderChanged() { final String key = "key"; + final Uri uri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); + final ContentResolver resolver = mock(ContentResolver.class); + doReturn(resolver).when(mContext).getContentResolver(); final int position = FakeSliderController.MAX_STEPS - 1; final int oldPosition = FakeSliderController.MAX_STEPS; mSearchFeatureProvider.getSearchIndexableResources().getProviderValues().clear(); @@ -158,6 +175,7 @@ public class SliceBroadcastReceiverTest { assertThat(namePair.first).isEqualTo(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME); assertThat(namePair.second).isEqualTo(key); + verify(resolver).notifyChange(uri, null); assertThat(valuePair.first) .isEqualTo(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE); assertThat(valuePair.second).isEqualTo(position); @@ -231,8 +249,12 @@ public class SliceBroadcastReceiverTest { // 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); + final Uri expectedUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); verify(resolver).notifyChange(eq(expectedUri), eq(null)); } @@ -268,8 +290,12 @@ public class SliceBroadcastReceiverTest { // 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); + final Uri expectedUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(SettingsSliceProvider.SLICE_AUTHORITY) + .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION) + .appendPath(key) + .build(); verify(resolver).notifyChange(eq(expectedUri), eq(null)); } From 2313b2421596b01a36bd2db0a171bbd050f1ddbc Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Thu, 24 May 2018 10:10:02 -0700 Subject: [PATCH 07/10] 2nd attempt to fix Slice strict mode. 1. Use real BluetoothAdapter instead of settingslib version. The settingslib version contains calls that violates strictmode rules. 2. Override StrictMode rules in SettingsSliceProvider when it's called in background thread. When in background, the enforcement from Slice framework (StrictMode#ThreadPolicy) is not useful and can be safely ignored. Change-Id: I68523148f4c1dc88a54e207447d21ec439478cdf Merged-In: I68523148f4c1dc88a54e207447d21ec439478cdf Fixes: 79985175 Test: robotests --- .../bluetooth/BluetoothSliceBuilder.java | 15 ++-- .../slices/SettingsSliceProvider.java | 84 +++++++++++-------- .../slices/SettingsSliceProviderTest.java | 51 +++++++++-- .../testutils/shadow/ShadowThreadUtils.java | 18 ++++ 4 files changed, 116 insertions(+), 52 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java b/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java index da759a33e5e..9a9f45588e7 100644 --- a/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java +++ b/src/com/android/settings/bluetooth/BluetoothSliceBuilder.java @@ -17,8 +17,6 @@ package com.android.settings.bluetooth; import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; -import static androidx.slice.builders.ListBuilder.ICON_IMAGE; - import android.annotation.ColorInt; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; @@ -28,6 +26,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; import android.provider.SettingsSlicesContract; +import android.support.v4.graphics.drawable.IconCompat; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; @@ -42,8 +41,6 @@ import androidx.slice.Slice; import androidx.slice.builders.ListBuilder; import androidx.slice.builders.SliceAction; -import android.support.v4.graphics.drawable.IconCompat; - public class BluetoothSliceBuilder { private static final String TAG = "BluetoothSliceBuilder"; @@ -71,7 +68,8 @@ public class BluetoothSliceBuilder { INTENT_FILTER.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); } - private BluetoothSliceBuilder() {} + private BluetoothSliceBuilder() { + } /** * Return a Bluetooth Slice bound to {@link #BLUETOOTH_URI}. @@ -80,7 +78,7 @@ public class BluetoothSliceBuilder { * Bluetooth. */ public static Slice getSlice(Context context) { - final boolean isBluetoothEnabled = isBluetoothEnabled(context); + final boolean isBluetoothEnabled = isBluetoothEnabled(); final CharSequence title = context.getText(R.string.bluetooth_settings); final IconCompat icon = IconCompat.createWithResource(context, R.drawable.ic_settings_bluetooth); @@ -115,9 +113,8 @@ public class BluetoothSliceBuilder { // handle it. } - private static boolean isBluetoothEnabled(Context context) { - final LocalBluetoothAdapter adapter = LocalBluetoothManager.getInstance(context, - null /* callback */).getBluetoothAdapter(); + private static boolean isBluetoothEnabled() { + final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); return adapter.isEnabled(); } diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 47736dfac2e..841247b1d64 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -23,23 +23,23 @@ import android.content.ContentResolver; import android.content.Intent; import android.content.IntentFilter; import android.net.Uri; +import android.os.StrictMode; import android.provider.Settings; import android.provider.SettingsSlicesContract; import android.support.annotation.VisibleForTesting; -import android.support.v4.graphics.drawable.IconCompat; import android.text.TextUtils; import android.util.ArraySet; import android.util.KeyValueListParser; import android.util.Log; import android.util.Pair; -import com.android.settings.location.LocationSliceBuilder; -import com.android.settings.overlay.FeatureFactory; +import com.android.settings.bluetooth.BluetoothSliceBuilder; import com.android.settings.core.BasePreferenceController; +import com.android.settings.location.LocationSliceBuilder; +import com.android.settings.notification.ZenModeSliceBuilder; +import com.android.settings.overlay.FeatureFactory; import com.android.settings.wifi.WifiSliceBuilder; import com.android.settings.wifi.calling.WifiCallingSliceHelper; -import com.android.settings.bluetooth.BluetoothSliceBuilder; -import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settingslib.SliceBroadcastRelay; import com.android.settingslib.utils.ThreadUtils; @@ -150,7 +150,7 @@ public class SettingsSliceProvider extends SliceProvider { @Override public void onSlicePinned(Uri sliceUri) { if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) { - registerIntentToUri(WifiSliceBuilder.INTENT_FILTER , sliceUri); + registerIntentToUri(WifiSliceBuilder.INTENT_FILTER, sliceUri); return; } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) { registerIntentToUri(ZenModeSliceBuilder.INTENT_FILTER, sliceUri); @@ -176,41 +176,51 @@ public class SettingsSliceProvider extends SliceProvider { @Override public Slice onBindSlice(Uri sliceUri) { - final Set blockedKeys = getBlockedKeys(); - final String key = sliceUri.getLastPathSegment(); - if (blockedKeys.contains(key)) { - Log.e(TAG, "Requested blocked slice with Uri: " + sliceUri); - return null; - } + final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); + try { + if (!ThreadUtils.isMainThread()) { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .permitAll() + .build()); + } + final Set blockedKeys = getBlockedKeys(); + final String key = sliceUri.getLastPathSegment(); + if (blockedKeys.contains(key)) { + Log.e(TAG, "Requested blocked slice with Uri: " + sliceUri); + return null; + } - // If adding a new Slice, do not directly match Slice URIs. - // Use {@link SlicesDatabaseAccessor}. - if (WifiCallingSliceHelper.WIFI_CALLING_URI.equals(sliceUri)) { - return FeatureFactory.getFactory(getContext()) - .getSlicesFeatureProvider() - .getNewWifiCallingSliceHelper(getContext()) - .createWifiCallingSlice(sliceUri); - } else if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) { - return WifiSliceBuilder.getSlice(getContext()); - } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) { - return ZenModeSliceBuilder.getSlice(getContext()); - } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) { - return BluetoothSliceBuilder.getSlice(getContext()); - } else if (LocationSliceBuilder.LOCATION_URI.equals(sliceUri)) { - return LocationSliceBuilder.getSlice(getContext()); - } + // If adding a new Slice, do not directly match Slice URIs. + // Use {@link SlicesDatabaseAccessor}. + if (WifiCallingSliceHelper.WIFI_CALLING_URI.equals(sliceUri)) { + return FeatureFactory.getFactory(getContext()) + .getSlicesFeatureProvider() + .getNewWifiCallingSliceHelper(getContext()) + .createWifiCallingSlice(sliceUri); + } else if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) { + return WifiSliceBuilder.getSlice(getContext()); + } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) { + return ZenModeSliceBuilder.getSlice(getContext()); + } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) { + return BluetoothSliceBuilder.getSlice(getContext()); + } else if (LocationSliceBuilder.LOCATION_URI.equals(sliceUri)) { + return LocationSliceBuilder.getSlice(getContext()); + } - SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri); - if (cachedSliceData == null) { - loadSliceInBackground(sliceUri); - return getSliceStub(sliceUri); - } + SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri); + if (cachedSliceData == null) { + loadSliceInBackground(sliceUri); + return getSliceStub(sliceUri); + } - // Remove the SliceData from the cache after it has been used to prevent a memory-leak. - if (!mSliceDataCache.containsKey(sliceUri)) { - mSliceWeakDataCache.remove(sliceUri); + // Remove the SliceData from the cache after it has been used to prevent a memory-leak. + if (!mSliceDataCache.containsKey(sliceUri)) { + mSliceWeakDataCache.remove(sliceUri); + } + return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData); + } finally { + StrictMode.setThreadPolicy(oldPolicy); } - return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData); } /** diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java index df960d8a912..21bef6131c9 100644 --- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java +++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java @@ -18,9 +18,7 @@ package com.android.settings.slices; import static android.content.ContentResolver.SCHEME_CONTENT; - import static com.google.common.truth.Truth.assertThat; - import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -38,19 +36,24 @@ import android.os.StrictMode; import android.provider.SettingsSlicesContract; import android.util.ArraySet; -import com.android.settings.location.LocationSliceBuilder; -import com.android.settings.wifi.WifiSliceBuilder; import com.android.settings.bluetooth.BluetoothSliceBuilder; +import com.android.settings.location.LocationSliceBuilder; import com.android.settings.notification.ZenModeSliceBuilder; import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeToggleController; import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowThreadUtils; +import com.android.settings.wifi.WifiSliceBuilder; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; import java.util.Arrays; import java.util.Collection; @@ -66,6 +69,7 @@ import androidx.slice.Slice; * TODO Investigate using ShadowContentResolver.registerProviderInternal(String, ContentProvider) */ @RunWith(SettingsRobolectricTestRunner.class) +@Config(shadows = ShadowThreadUtils.class) public class SettingsSliceProviderTest { private static final String KEY = "KEY"; @@ -98,6 +102,7 @@ public class SettingsSliceProviderTest { public void setUp() { mContext = spy(RuntimeEnvironment.application); mProvider = spy(new SettingsSliceProvider()); + ShadowStrictMode.reset(); mProvider.mSliceWeakDataCache = new HashMap<>(); mProvider.mSliceDataCache = new HashMap<>(); mProvider.mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(mContext); @@ -112,6 +117,7 @@ public class SettingsSliceProviderTest { @After public void cleanUp() { + ShadowThreadUtils.reset(); DatabaseTestUtils.clearDb(mContext); } @@ -184,7 +190,8 @@ public class SettingsSliceProviderTest { } @Test - public void onBindSlice_shouldNotOverrideStrictMode() { + public void onBindSlice_mainThread_shouldNotOverrideStrictMode() { + ShadowThreadUtils.setIsMainThread(true); final StrictMode.ThreadPolicy oldThreadPolicy = StrictMode.getThreadPolicy(); SliceData data = getDummyData(); mProvider.mSliceWeakDataCache.put(data.getUri(), data); @@ -195,6 +202,18 @@ public class SettingsSliceProviderTest { assertThat(newThreadPolicy.toString()).isEqualTo(oldThreadPolicy.toString()); } + @Test + @Config(shadows = ShadowStrictMode.class) + public void onBindSlice_backgroundThread_shouldOverrideStrictMode() { + ShadowThreadUtils.setIsMainThread(false); + + SliceData data = getDummyData(); + mProvider.mSliceWeakDataCache.put(data.getUri(), data); + mProvider.onBindSlice(data.getUri()); + + assertThat(ShadowStrictMode.isThreadPolicyOverridden()).isTrue(); + } + @Test public void onBindSlice_requestsBlockedSlice_retunsNull() { final String blockedKey = "blocked_key"; @@ -456,7 +475,7 @@ public class SettingsSliceProviderTest { mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); } - private SliceData getDummyData() { + private static SliceData getDummyData() { return new SliceData.Builder() .setKey(KEY) .setTitle(TITLE) @@ -468,4 +487,24 @@ public class SettingsSliceProviderTest { .setPreferenceControllerClassName(PREF_CONTROLLER) .build(); } + + @Implements(value = StrictMode.class, inheritImplementationMethods = true) + public static class ShadowStrictMode { + + private static int sSetThreadPolicyCount; + + @Resetter + public static void reset() { + sSetThreadPolicyCount = 0; + } + + @Implementation + public static void setThreadPolicy(final StrictMode.ThreadPolicy policy) { + sSetThreadPolicyCount++; + } + + public static boolean isThreadPolicyOverridden() { + return sSetThreadPolicyCount != 0; + } + } } \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java index 6b0411e9ab0..9513098e862 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java @@ -20,10 +20,18 @@ import com.android.settingslib.utils.ThreadUtils; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; @Implements(ThreadUtils.class) public class ShadowThreadUtils { + private static boolean sIsMainThread = true; + + @Resetter + public static void reset() { + sIsMainThread = true; + } + @Implementation public static void postOnBackgroundThread(Runnable runnable) { runnable.run(); @@ -33,4 +41,14 @@ public class ShadowThreadUtils { public static void postOnMainThread(Runnable runnable) { runnable.run(); } + + @Implementation + public static boolean isMainThread() { + return sIsMainThread; + } + + public static void setIsMainThread(boolean isMainThread) { + sIsMainThread = isMainThread; + } + } From 24283d3076a0e1e86bb649c4e4fdc842ec96afc8 Mon Sep 17 00:00:00 2001 From: Antony Sargent Date: Fri, 25 May 2018 12:52:24 -0700 Subject: [PATCH 08/10] Remove Advanced dropdown from "About phone" screen All the items will appear in the list instead of hiding some under an Advanced dropdown. Bug: 80302533 Test: manual (Settings->System->About phone) Change-Id: I82857ec8e027b00762c1191d9450d30dfe47016f --- res/xml/my_device_info.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/res/xml/my_device_info.xml b/res/xml/my_device_info.xml index 213ff1c1b7b..3dac8b879f2 100644 --- a/res/xml/my_device_info.xml +++ b/res/xml/my_device_info.xml @@ -19,8 +19,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:settings="http://schemas.android.com/apk/res-auto" android:key="my_device_info_pref_screen" - android:title="@string/about_settings" - settings:initialExpandedChildrenCount="7"> + android:title="@string/about_settings"> Date: Fri, 25 May 2018 22:57:03 +0100 Subject: [PATCH 09/10] Hide haptic intensity settings for devices without enough dynamic range. Despite our hope, devices prior to P18 just don't have the dynamic range to give three distinct vibration intensity settings. Given this, only show an on / off toggle for devices by default unless they explicitly opt-in to multiple intensity levels. Test: m -j RunSettingsRoboTests Bug: 80243632 Change-Id: I14ece855cf20f9fa44b0648d28f89c0053be5c5f --- res/values/config.xml | 5 ++ .../accessibility/AccessibilitySettings.java | 32 +++++++---- ...ibrationIntensityPreferenceController.java | 32 +++++++---- .../VibrationPreferenceFragment.java | 57 +++++++++++++------ ...tionIntensityPreferenceControllerTest.java | 54 +++++++++++++++++- .../VibrationPreferenceFragmentTest.java | 53 ++++++++++++++--- 6 files changed, 184 insertions(+), 49 deletions(-) diff --git a/res/values/config.xml b/res/values/config.xml index 8e55ea95e36..f46134f301b 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -134,4 +134,9 @@ true + + false diff --git a/src/com/android/settings/accessibility/AccessibilitySettings.java b/src/com/android/settings/accessibility/AccessibilitySettings.java index 72e485e7c81..4ddcc31bf27 100644 --- a/src/com/android/settings/accessibility/AccessibilitySettings.java +++ b/src/com/android/settings/accessibility/AccessibilitySettings.java @@ -779,17 +779,27 @@ public class AccessibilitySettings extends SettingsPreferenceFragment implements } private String getVibrationSummary(Context context, @VibrationIntensity int intensity) { - switch (intensity) { - case Vibrator.VIBRATION_INTENSITY_OFF: - return context.getString(R.string.accessibility_vibration_summary_off); - case Vibrator.VIBRATION_INTENSITY_LOW: - return context.getString(R.string.accessibility_vibration_summary_low); - case Vibrator.VIBRATION_INTENSITY_MEDIUM: - return context.getString(R.string.accessibility_vibration_summary_medium); - case Vibrator.VIBRATION_INTENSITY_HIGH: - return context.getString(R.string.accessibility_vibration_summary_high); - default: - return ""; + final boolean supportsMultipleIntensities = context.getResources().getBoolean( + R.bool.config_vibration_supports_multiple_intensities); + if (supportsMultipleIntensities) { + switch (intensity) { + case Vibrator.VIBRATION_INTENSITY_OFF: + return context.getString(R.string.accessibility_vibration_summary_off); + case Vibrator.VIBRATION_INTENSITY_LOW: + return context.getString(R.string.accessibility_vibration_summary_low); + case Vibrator.VIBRATION_INTENSITY_MEDIUM: + return context.getString(R.string.accessibility_vibration_summary_medium); + case Vibrator.VIBRATION_INTENSITY_HIGH: + return context.getString(R.string.accessibility_vibration_summary_high); + default: + return ""; + } + } else { + if (intensity == Vibrator.VIBRATION_INTENSITY_OFF) { + return context.getString(R.string.switch_on_text); + } else { + return context.getString(R.string.switch_off_text); + } } } diff --git a/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java b/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java index 9c81afa2f16..3fbb1230a50 100644 --- a/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java +++ b/src/com/android/settings/accessibility/VibrationIntensityPreferenceController.java @@ -81,17 +81,27 @@ public abstract class VibrationIntensityPreferenceController extends BasePrefere } public static CharSequence getIntensityString(Context context, int intensity) { - switch (intensity) { - case Vibrator.VIBRATION_INTENSITY_OFF: - return context.getText(R.string.accessibility_vibration_intensity_off); - case Vibrator.VIBRATION_INTENSITY_LOW: - return context.getText(R.string.accessibility_vibration_intensity_low); - case Vibrator.VIBRATION_INTENSITY_MEDIUM: - return context.getText(R.string.accessibility_vibration_intensity_medium); - case Vibrator.VIBRATION_INTENSITY_HIGH: - return context.getText(R.string.accessibility_vibration_intensity_high); - default: - return ""; + final boolean supportsMultipleIntensities = context.getResources().getBoolean( + R.bool.config_vibration_supports_multiple_intensities); + if (supportsMultipleIntensities) { + switch (intensity) { + case Vibrator.VIBRATION_INTENSITY_OFF: + return context.getString(R.string.accessibility_vibration_intensity_off); + case Vibrator.VIBRATION_INTENSITY_LOW: + return context.getString(R.string.accessibility_vibration_intensity_low); + case Vibrator.VIBRATION_INTENSITY_MEDIUM: + return context.getString(R.string.accessibility_vibration_intensity_medium); + case Vibrator.VIBRATION_INTENSITY_HIGH: + return context.getString(R.string.accessibility_vibration_intensity_high); + default: + return ""; + } + } else { + if (intensity == Vibrator.VIBRATION_INTENSITY_OFF) { + return context.getString(R.string.switch_off_text); + } else { + return context.getString(R.string.switch_on_text); + } } } diff --git a/src/com/android/settings/accessibility/VibrationPreferenceFragment.java b/src/com/android/settings/accessibility/VibrationPreferenceFragment.java index e911b612e37..f02c4747c96 100644 --- a/src/com/android/settings/accessibility/VibrationPreferenceFragment.java +++ b/src/com/android/settings/accessibility/VibrationPreferenceFragment.java @@ -53,28 +53,15 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm final static String KEY_INTENSITY_MEDIUM = "intensity_medium"; @VisibleForTesting final static String KEY_INTENSITY_HIGH = "intensity_high"; + // KEY_INTENSITY_ON is only used when the device doesn't support multiple intensity levels. + @VisibleForTesting + final static String KEY_INTENSITY_ON = "intensity_on"; private final Map mCandidates; private final SettingsObserver mSettingsObserver; public VibrationPreferenceFragment() { mCandidates = new ArrayMap<>(); - mCandidates.put(KEY_INTENSITY_OFF, - new VibrationIntensityCandidateInfo(KEY_INTENSITY_OFF, - R.string.accessibility_vibration_intensity_off, - Vibrator.VIBRATION_INTENSITY_OFF)); - mCandidates.put(KEY_INTENSITY_LOW, - new VibrationIntensityCandidateInfo(KEY_INTENSITY_LOW, - R.string.accessibility_vibration_intensity_low, - Vibrator.VIBRATION_INTENSITY_LOW)); - mCandidates.put(KEY_INTENSITY_MEDIUM, - new VibrationIntensityCandidateInfo(KEY_INTENSITY_MEDIUM, - R.string.accessibility_vibration_intensity_medium, - Vibrator.VIBRATION_INTENSITY_MEDIUM)); - mCandidates.put(KEY_INTENSITY_HIGH, - new VibrationIntensityCandidateInfo(KEY_INTENSITY_HIGH, - R.string.accessibility_vibration_intensity_high, - Vibrator.VIBRATION_INTENSITY_HIGH)); mSettingsObserver = new SettingsObserver(); } @@ -82,6 +69,39 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm public void onAttach(Context context) { super.onAttach(context); mSettingsObserver.register(); + if (mCandidates.isEmpty()) { + loadCandidates(context); + } + } + + private void loadCandidates(Context context) { + final boolean supportsMultipleIntensities = context.getResources().getBoolean( + R.bool.config_vibration_supports_multiple_intensities); + if (supportsMultipleIntensities) { + mCandidates.put(KEY_INTENSITY_OFF, + new VibrationIntensityCandidateInfo(KEY_INTENSITY_OFF, + R.string.accessibility_vibration_intensity_off, + Vibrator.VIBRATION_INTENSITY_OFF)); + mCandidates.put(KEY_INTENSITY_LOW, + new VibrationIntensityCandidateInfo(KEY_INTENSITY_LOW, + R.string.accessibility_vibration_intensity_low, + Vibrator.VIBRATION_INTENSITY_LOW)); + mCandidates.put(KEY_INTENSITY_MEDIUM, + new VibrationIntensityCandidateInfo(KEY_INTENSITY_MEDIUM, + R.string.accessibility_vibration_intensity_medium, + Vibrator.VIBRATION_INTENSITY_MEDIUM)); + mCandidates.put(KEY_INTENSITY_HIGH, + new VibrationIntensityCandidateInfo(KEY_INTENSITY_HIGH, + R.string.accessibility_vibration_intensity_high, + Vibrator.VIBRATION_INTENSITY_HIGH)); + } else { + mCandidates.put(KEY_INTENSITY_OFF, + new VibrationIntensityCandidateInfo(KEY_INTENSITY_OFF, + R.string.switch_off_text, Vibrator.VIBRATION_INTENSITY_OFF)); + mCandidates.put(KEY_INTENSITY_ON, + new VibrationIntensityCandidateInfo(KEY_INTENSITY_ON, + R.string.switch_on_text, getDefaultVibrationIntensity())); + } } @Override @@ -118,7 +138,10 @@ public abstract class VibrationPreferenceFragment extends RadioButtonPickerFragm final int vibrationIntensity = Settings.System.getInt(getContext().getContentResolver(), getVibrationIntensitySetting(), getDefaultVibrationIntensity()); for (VibrationIntensityCandidateInfo candidate : mCandidates.values()) { - if (candidate.getIntensity() == vibrationIntensity) { + final boolean matchesIntensity = candidate.getIntensity() == vibrationIntensity; + final boolean matchesOn = candidate.getKey().equals(KEY_INTENSITY_ON) + && vibrationIntensity != Vibrator.VIBRATION_INTENSITY_OFF; + if (matchesIntensity || matchesOn) { return candidate.getKey(); } } diff --git a/tests/robotests/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceControllerTest.java index 067f015e85e..aa9efc51f88 100644 --- a/tests/robotests/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/NotificationVibrationIntensityPreferenceControllerTest.java @@ -18,10 +18,12 @@ package com.android.settings.accessibility; import static android.provider.Settings.System.NOTIFICATION_VIBRATION_INTENSITY; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.arch.lifecycle.LifecycleOwner; import android.content.Context; +import android.content.res.Resources; import android.os.Vibrator; import android.provider.Settings; import android.support.v7.preference.Preference; @@ -48,6 +50,7 @@ public class NotificationVibrationIntensityPreferenceControllerTest { private LifecycleOwner mLifecycleOwner; private Lifecycle mLifecycle; private Context mContext; + private Resources mResources; private NotificationVibrationIntensityPreferenceController mController; private Preference mPreference; @@ -56,7 +59,11 @@ public class NotificationVibrationIntensityPreferenceControllerTest { MockitoAnnotations.initMocks(this); mLifecycleOwner = () -> mLifecycle; mLifecycle = new Lifecycle(mLifecycleOwner); - mContext = RuntimeEnvironment.application; + mContext = spy(RuntimeEnvironment.application); + mResources = spy(mContext.getResources()); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getBoolean(R.bool.config_vibration_supports_multiple_intensities)) + .thenReturn(true); mController = new NotificationVibrationIntensityPreferenceController(mContext) { @Override protected int getDefaultIntensity() { @@ -68,7 +75,6 @@ public class NotificationVibrationIntensityPreferenceControllerTest { mPreference.setSummary("Test"); when(mScreen.findPreference(mController.getPreferenceKey())) .thenReturn(mPreference); - mController.displayPreference(mScreen); } @Test @@ -80,7 +86,10 @@ public class NotificationVibrationIntensityPreferenceControllerTest { } @Test - public void updateState_shouldRefreshSummary() { + public void updateState_withMultipleIntensitySuport_shouldRefreshSummary() { + setSupportsMultipleIntensities(true); + showPreference(); + Settings.System.putInt(mContext.getContentResolver(), NOTIFICATION_VIBRATION_INTENSITY, Vibrator.VIBRATION_INTENSITY_LOW); mController.updateState(mPreference); @@ -105,4 +114,43 @@ public class NotificationVibrationIntensityPreferenceControllerTest { assertThat(mPreference.getSummary()) .isEqualTo(mContext.getString(R.string.accessibility_vibration_intensity_off)); } + + @Test + public void updateState_withoutMultipleIntensitySupport_shouldRefreshSummary() { + setSupportsMultipleIntensities(false); + showPreference(); + + Settings.System.putInt(mContext.getContentResolver(), + NOTIFICATION_VIBRATION_INTENSITY, Vibrator.VIBRATION_INTENSITY_LOW); + mController.updateState(mPreference); + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.switch_on_text)); + + Settings.System.putInt(mContext.getContentResolver(), + NOTIFICATION_VIBRATION_INTENSITY, Vibrator.VIBRATION_INTENSITY_HIGH); + mController.updateState(mPreference); + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.switch_on_text)); + + Settings.System.putInt(mContext.getContentResolver(), + NOTIFICATION_VIBRATION_INTENSITY, Vibrator.VIBRATION_INTENSITY_MEDIUM); + mController.updateState(mPreference); + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.switch_on_text)); + + Settings.System.putInt(mContext.getContentResolver(), + NOTIFICATION_VIBRATION_INTENSITY, Vibrator.VIBRATION_INTENSITY_OFF); + mController.updateState(mPreference); + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.switch_off_text)); + } + + private void setSupportsMultipleIntensities(boolean hasSupport) { + when(mResources.getBoolean(R.bool.config_vibration_supports_multiple_intensities)) + .thenReturn(hasSupport); + } + + private void showPreference() { + mController.displayPreference(mScreen); + } } diff --git a/tests/robotests/src/com/android/settings/accessibility/VibrationPreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/VibrationPreferenceFragmentTest.java index 8550cf98916..ecd2ff9ce57 100644 --- a/tests/robotests/src/com/android/settings/accessibility/VibrationPreferenceFragmentTest.java +++ b/tests/robotests/src/com/android/settings/accessibility/VibrationPreferenceFragmentTest.java @@ -20,16 +20,19 @@ import static com.android.settings.accessibility.VibrationPreferenceFragment.KEY import static com.android.settings.accessibility.VibrationPreferenceFragment.KEY_INTENSITY_LOW; import static com.android.settings.accessibility.VibrationPreferenceFragment.KEY_INTENSITY_MEDIUM; import static com.android.settings.accessibility.VibrationPreferenceFragment.KEY_INTENSITY_OFF; +import static com.android.settings.accessibility.VibrationPreferenceFragment.KEY_INTENSITY_ON; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import android.app.Activity; import android.content.Context; +import android.content.res.Resources; import android.os.UserManager; import android.os.Vibrator; import android.provider.Settings; +import com.android.settings.R; import com.android.settings.accessibility.VibrationPreferenceFragment.VibrationIntensityCandidateInfo; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; @@ -58,12 +61,11 @@ public class VibrationPreferenceFragmentTest { INTENSITY_TO_KEY.put(Vibrator.VIBRATION_INTENSITY_HIGH, KEY_INTENSITY_HIGH); } - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Activity mActivity; @Mock private UserManager mUserManager; private Context mContext; + private Resources mResources; private TestVibrationPreferenceFragment mFragment; @Before @@ -71,16 +73,18 @@ public class VibrationPreferenceFragmentTest { MockitoAnnotations.initMocks(this); FakeFeatureFactory.setupForTest(); - mContext = RuntimeEnvironment.application; + mContext = spy(RuntimeEnvironment.application); + mResources = spy(mContext.getResources()); + when(mContext.getResources()).thenReturn(mResources); mFragment = spy(new TestVibrationPreferenceFragment()); - doReturn(mUserManager).when(mActivity).getSystemService(Context.USER_SERVICE); - doReturn(mContext).when(mFragment).getContext(); - mFragment.onAttach(mActivity); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); } @Test public void changeIntensitySetting_shouldResultInCorrespondingKey() { + setSupportsMultipleIntensities(true); + mFragment.onAttach(mContext); for (Map.Entry entry : INTENSITY_TO_KEY.entrySet()) { Settings.System.putInt(mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_INTENSITY, entry.getKey()); @@ -88,13 +92,38 @@ public class VibrationPreferenceFragmentTest { } } + @Test + public void changeIntensitySetting_WithoutMultipleIntensitySupport_shouldResultInOn() { + setSupportsMultipleIntensities(false); + mFragment.onAttach(mContext); + for (int intensity : INTENSITY_TO_KEY.keySet()) { + Settings.System.putInt(mContext.getContentResolver(), + Settings.System.HAPTIC_FEEDBACK_INTENSITY, intensity); + final String expectedKey = intensity == Vibrator.VIBRATION_INTENSITY_OFF + ? KEY_INTENSITY_OFF + : KEY_INTENSITY_ON; + assertThat(mFragment.getDefaultKey()).isEqualTo(expectedKey); + } + } + @Test public void initialDefaultKey_shouldBeMedium() { + setSupportsMultipleIntensities(true); + mFragment.onAttach(mContext); assertThat(mFragment.getDefaultKey()).isEqualTo(KEY_INTENSITY_MEDIUM); } + @Test + public void initialDefaultKey_WithoutMultipleIntensitySupport_shouldBeOn() { + setSupportsMultipleIntensities(false); + mFragment.onAttach(mContext); + assertThat(mFragment.getDefaultKey()).isEqualTo(KEY_INTENSITY_ON); + } + @Test public void candidates_shouldBeSortedByIntensity() { + setSupportsMultipleIntensities(true); + mFragment.onAttach(mContext); final List candidates = mFragment.getCandidates(); assertThat(candidates.size()).isEqualTo(INTENSITY_TO_KEY.size()); VibrationIntensityCandidateInfo prevCandidate = @@ -106,6 +135,11 @@ public class VibrationPreferenceFragmentTest { } } + private void setSupportsMultipleIntensities(boolean hasSupport) { + when(mResources.getBoolean(R.bool.config_vibration_supports_multiple_intensities)) + .thenReturn(hasSupport); + } + private class TestVibrationPreferenceFragment extends VibrationPreferenceFragment { @Override protected int getPreferenceScreenResId() { @@ -129,5 +163,10 @@ public class VibrationPreferenceFragmentTest { protected int getDefaultVibrationIntensity() { return Vibrator.VIBRATION_INTENSITY_MEDIUM; } + + @Override + public Context getContext() { + return mContext; + } } } From c93dfd8f482934c4d849e82db87097f7d69d96f3 Mon Sep 17 00:00:00 2001 From: Michael Wright Date: Fri, 25 May 2018 23:37:29 +0100 Subject: [PATCH 10/10] Add michaelwr to OWNERS for haptics settings in accessibility Bug: 80243632 Test: N/A Change-Id: Id315d626af527946230affc292ed9b329efe8b5b --- src/com/android/settings/accessibility/OWNERS | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/accessibility/OWNERS b/src/com/android/settings/accessibility/OWNERS index 28dfdefabef..ba03055c736 100644 --- a/src/com/android/settings/accessibility/OWNERS +++ b/src/com/android/settings/accessibility/OWNERS @@ -1,3 +1,6 @@ # Default reviewers for this and subdirectories. pweaver@google.com -zork@google.com \ No newline at end of file +zork@google.com + +per-file HapticFeedbackIntensityPreferenceController.java = michaelwr@google.com +per-file *Vibration* = michaelwr@google.com