From 09c44bf78dd5ed694ab224c0164a61c786f6f305 Mon Sep 17 00:00:00 2001 From: Wang wenrui Date: Mon, 13 Feb 2017 12:51:24 +0800 Subject: [PATCH 01/10] Fix screen freeze when changing display size in Multi-Window mode The device screen freezes when launching two Display size settings screen and changing display size in Multi-Window mode. To fix this issue, OnSeekBarChangeListener should be set in onStart() to avoid onProgressChanged() is called during onRestoreInstanceState which is caused by display size change. Fixes: 70253030 Test: manual - open two Display size settings in Multi-Window mode. Change-Id: I779954aeeb1da526ba1b0fac2676e2f7d8289352 --- .../PreviewSeekBarPreferenceFragment.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/com/android/settings/PreviewSeekBarPreferenceFragment.java b/src/com/android/settings/PreviewSeekBarPreferenceFragment.java index 544999a3806..f5f3017af79 100644 --- a/src/com/android/settings/PreviewSeekBarPreferenceFragment.java +++ b/src/com/android/settings/PreviewSeekBarPreferenceFragment.java @@ -58,6 +58,7 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc private DotsPageIndicator mPageIndicator; private TextView mLabel; + private LabeledSeekBar mSeekBar; private View mLarger; private View mSmaller; @@ -110,19 +111,17 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc // seek bar. final int max = Math.max(1, mEntries.length - 1); - final LabeledSeekBar seekBar = (LabeledSeekBar) content.findViewById(R.id.seek_bar); - seekBar.setLabels(mEntries); - seekBar.setMax(max); - seekBar.setProgress(mInitialIndex); - seekBar.setOnSeekBarChangeListener(new onPreviewSeekBarChangeListener()); + mSeekBar = (LabeledSeekBar) content.findViewById(R.id.seek_bar); + mSeekBar.setLabels(mEntries); + mSeekBar.setMax(max); mSmaller = content.findViewById(R.id.smaller); mSmaller.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - final int progress = seekBar.getProgress(); + final int progress = mSeekBar.getProgress(); if (progress > 0) { - seekBar.setProgress(progress - 1, true); + mSeekBar.setProgress(progress - 1, true); } } }); @@ -131,9 +130,9 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc mLarger.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - final int progress = seekBar.getProgress(); - if (progress < seekBar.getMax()) { - seekBar.setProgress(progress + 1, true); + final int progress = mSeekBar.getProgress(); + if (progress < mSeekBar.getMax()) { + mSeekBar.setProgress(progress + 1, true); } } }); @@ -141,7 +140,7 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc if (mEntries.length == 1) { // The larger and smaller buttons will be disabled when we call // setPreviewLayer() later in this method. - seekBar.setEnabled(false); + mSeekBar.setEnabled(false); } final Context context = getContext(); @@ -172,6 +171,21 @@ public abstract class PreviewSeekBarPreferenceFragment extends SettingsPreferenc return root; } + @Override + public void onStart() { + super.onStart(); + // Set SeekBar listener here to avoid onProgressChanged() is called + // during onRestoreInstanceState(). + mSeekBar.setProgress(mCurrentIndex); + mSeekBar.setOnSeekBarChangeListener(new onPreviewSeekBarChangeListener()); + } + + @Override + public void onStop() { + super.onStop(); + mSeekBar.setOnSeekBarChangeListener(null); + } + /** * Creates new configuration based on the current position of the SeekBar. */ From 13197bcc1eecde37773a4778b8ab2bfd6b3fb979 Mon Sep 17 00:00:00 2001 From: Hemant Gupta Date: Wed, 4 Nov 2015 16:38:15 +0530 Subject: [PATCH 02/10] Bluetooth: Add support for PBAP UI preference Usecase: 1) Connect with carkit that supports PBAP/MAP 2) Try to uncheck the checkbox for contact sharing from device sub settings menu Expected Result: 1) DUT should display "Connected (No Media or Phone)" 1) Should be able to disconnect when checkbox moves from selected->unselected, and carkit should be able to connect when checkbox is selected again Observation: 1) No UI updates when PBAP/MAP are connected. 2) On unchecking Contact sharing checkbox nothing happens. PBAP connection remains active and user is not asked to disconnect profile level connection. Fix: Add support for PBAP UI preference in SettingsLib. Also Fix issues related to MAP profile addition/removal. Test: Connect from carkit, try disconnecting from sub settings menu by unchecking Contact Sharing checkbox and check if PBAP is successfully disconnected. Bug: 35014213 Change-Id: I4981aa063b2541b58ce1d36e3576578cbeb02acc --- .../bluetooth/DeviceProfilesSettings.java | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) mode change 100755 => 100644 src/com/android/settings/bluetooth/DeviceProfilesSettings.java diff --git a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java old mode 100755 new mode 100644 index a76ed460585..8facab0040a --- a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java +++ b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java @@ -172,7 +172,11 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp mProfileContainer.removeAllViews(); for (LocalBluetoothProfile profile : mCachedDevice.getConnectableProfiles()) { CheckBox pref = createProfilePreference(profile); - mProfileContainer.addView(pref); + // MAP and PBAP profiles would be added based on permission access + if (!((profile instanceof PbapServerProfile) || + (profile instanceof MapProfile))) { + mProfileContainer.addView(pref); + } if (profile instanceof A2dpProfile) { BluetoothDevice device = mCachedDevice.getDevice(); @@ -191,6 +195,7 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp } final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); + Log.d(TAG, "addPreferencesForProfiles: pbapPermission = " + pbapPermission); // Only provide PBAP cabability if the client device has requested PBAP. if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { final PbapServerProfile psp = mManager.getProfileManager().getPbapProfile(); @@ -200,6 +205,7 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp final MapProfile mapProfile = mManager.getProfileManager().getMapProfile(); final int mapPermission = mCachedDevice.getMessagePermissionChoice(); + Log.d(TAG, "addPreferencesForProfiles: mapPermission = " + mapPermission); if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) { CheckBox mapPreference = createProfilePreference(mapProfile); mProfileContainer.addView(mapPreference); @@ -251,15 +257,6 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp private void onProfileClicked(LocalBluetoothProfile profile, CheckBox profilePref) { BluetoothDevice device = mCachedDevice.getDevice(); - if (KEY_PBAP_SERVER.equals(profilePref.getTag())) { - final int newPermission = mCachedDevice.getPhonebookPermissionChoice() - == CachedBluetoothDevice.ACCESS_ALLOWED ? CachedBluetoothDevice.ACCESS_REJECTED - : CachedBluetoothDevice.ACCESS_ALLOWED; - mCachedDevice.setPhonebookPermissionChoice(newPermission); - profilePref.setChecked(newPermission == CachedBluetoothDevice.ACCESS_ALLOWED); - return; - } - if (!profilePref.isChecked()) { // Recheck it, until the dialog is done. profilePref.setChecked(true); @@ -268,6 +265,12 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp if (profile instanceof MapProfile) { mCachedDevice.setMessagePermissionChoice(BluetoothDevice.ACCESS_ALLOWED); } + if (profile instanceof PbapServerProfile) { + mCachedDevice.setPhonebookPermissionChoice(BluetoothDevice.ACCESS_ALLOWED); + refreshProfilePreference(profilePref, profile); + // PBAP server is not preffered profile and cannot initiate connection, so return + return; + } if (profile.isPreferred(device)) { // profile is preferred but not connected: disable auto-connect if (profile instanceof PanProfile) { @@ -301,10 +304,17 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp DialogInterface.OnClickListener disconnectListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - device.disconnect(profile); - profile.setPreferred(device.getDevice(), false); - if (profile instanceof MapProfile) { - device.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); + + // Disconnect only when user has selected OK otherwise ignore + if (which == DialogInterface.BUTTON_POSITIVE) { + device.disconnect(profile); + profile.setPreferred(device.getDevice(), false); + if (profile instanceof MapProfile) { + device.setMessagePermissionChoice(BluetoothDevice.ACCESS_REJECTED); + } + if (profile instanceof PbapServerProfile) { + device.setPhonebookPermissionChoice(BluetoothDevice.ACCESS_REJECTED); + } } refreshProfilePreference(findProfile(profile.toString()), profile); } @@ -341,6 +351,19 @@ public final class DeviceProfilesSettings extends InstrumentedDialogFragment imp for (LocalBluetoothProfile profile : mCachedDevice.getRemovedProfiles()) { CheckBox profilePref = findProfile(profile.toString()); if (profilePref != null) { + + if (profile instanceof PbapServerProfile) { + final int pbapPermission = mCachedDevice.getPhonebookPermissionChoice(); + Log.d(TAG, "refreshProfiles: pbapPermission = " + pbapPermission); + if (pbapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) + continue; + } + if (profile instanceof MapProfile) { + final int mapPermission = mCachedDevice.getMessagePermissionChoice(); + Log.d(TAG, "refreshProfiles: mapPermission = " + mapPermission); + if (mapPermission != CachedBluetoothDevice.ACCESS_UNKNOWN) + continue; + } Log.d(TAG, "Removing " + profile.toString() + " from profile list"); mProfileContainer.removeView(profilePref); } From 9e19121a00b85d7618bf294f70afa0f205e3ac00 Mon Sep 17 00:00:00 2001 From: Adam Lesinski Date: Tue, 14 Nov 2017 11:10:08 -0800 Subject: [PATCH 03/10] Update the way OMS records details about overlays Use flags, since we keep introducing boolean traits which would be wasteful to encode as booleans. Bug: 69383160 Test: builds Change-Id: Ic09ae1086c7afa039b5df11e4c8c968125784586 Merged-In: Ic09ae1086c7afa039b5df11e4c8c968125784586 --- src/com/android/settings/display/ThemePreferenceController.java | 2 +- .../android/settings/display/ThemePreferenceControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/android/settings/display/ThemePreferenceController.java b/src/com/android/settings/display/ThemePreferenceController.java index 3bb58b15d0c..5eeb3906c07 100644 --- a/src/com/android/settings/display/ThemePreferenceController.java +++ b/src/com/android/settings/display/ThemePreferenceController.java @@ -125,7 +125,7 @@ public class ThemePreferenceController extends AbstractPreferenceController impl private boolean isChangeableOverlay(String packageName) { try { PackageInfo pi = mPackageManager.getPackageInfo(packageName, 0); - return pi != null && !pi.isStaticOverlay; + return pi != null && (pi.overlayFlags & PackageInfo.FLAG_OVERLAY_STATIC) == 0; } catch (PackageManager.NameNotFoundException e) { return false; } diff --git a/tests/unit/src/com/android/settings/display/ThemePreferenceControllerTest.java b/tests/unit/src/com/android/settings/display/ThemePreferenceControllerTest.java index 69c8c54c672..8c6e8da0db7 100644 --- a/tests/unit/src/com/android/settings/display/ThemePreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/display/ThemePreferenceControllerTest.java @@ -119,7 +119,7 @@ public class ThemePreferenceControllerTest { return info; }); PackageInfo pi = new PackageInfo(); - pi.isStaticOverlay = true; + pi.overlayFlags |= PackageInfo.FLAG_OVERLAY_STATIC; when(mMockPackageManager.getPackageInfo(eq("com.android.Theme1"), anyInt())).thenReturn(pi); when(mMockPackageManager.getPackageInfo(eq("com.android.Theme2"), anyInt())).thenReturn( new PackageInfo()); From 988199e2023107795570105679725d12c993fe48 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Fri, 1 Dec 2017 10:36:22 -0800 Subject: [PATCH 04/10] Add ConnectedUsbDeviceUpdater The usb updater listens to usb update and notify ConnectedDeviceGroupController to add/remove preference. This cl: 1. Add ConntectedusbDeviceUpdater 2. Extract the UsbConnectionBroadcastReceiver since it would be used both in controller and updater. 3. Add tests Bug: 69333961 Test: RunSettingsRoboTests Change-Id: Ic3b045a6faa4eba57d9b0c089ea1656141cc0220 --- .../ConnectedDeviceGroupController.java | 18 ++- .../ConnectedUsbDeviceUpdater.java | 92 +++++++++++++ .../UsbConnectionBroadcastReceiver.java | 76 +++++++++++ .../UsbModePreferenceController.java | 60 ++------- .../ConnectedDeviceGroupControllerTest.java | 18 ++- .../ConnectedUsbDeviceUpdaterTest.java | 89 +++++++++++++ .../UsbConnectionBroadcastReceiverTest.java | 126 ++++++++++++++++++ 7 files changed, 421 insertions(+), 58 deletions(-) create mode 100644 src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdater.java create mode 100644 src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiver.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdaterTest.java create mode 100644 tests/robotests/src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiverTest.java diff --git a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java index a0b5cb85697..3cccc15782f 100644 --- a/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java +++ b/src/com/android/settings/connecteddevice/ConnectedDeviceGroupController.java @@ -19,6 +19,7 @@ import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceGroup; import android.support.v7.preference.PreferenceScreen; + import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.bluetooth.BluetoothDeviceUpdater; import com.android.settings.bluetooth.ConnectedBluetoothDeviceUpdater; @@ -42,26 +43,31 @@ public class ConnectedDeviceGroupController extends AbstractPreferenceController @VisibleForTesting PreferenceGroup mPreferenceGroup; private BluetoothDeviceUpdater mBluetoothDeviceUpdater; + private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater; public ConnectedDeviceGroupController(DashboardFragment fragment, Lifecycle lifecycle) { super(fragment.getContext()); - init(lifecycle, new ConnectedBluetoothDeviceUpdater(fragment, this)); + init(lifecycle, new ConnectedBluetoothDeviceUpdater(fragment, this), + new ConnectedUsbDeviceUpdater(fragment.getContext(), this)); } @VisibleForTesting ConnectedDeviceGroupController(DashboardFragment fragment, Lifecycle lifecycle, - BluetoothDeviceUpdater bluetoothDeviceUpdater) { + BluetoothDeviceUpdater bluetoothDeviceUpdater, + ConnectedUsbDeviceUpdater connectedUsbDeviceUpdater) { super(fragment.getContext()); - init(lifecycle, bluetoothDeviceUpdater); + init(lifecycle, bluetoothDeviceUpdater, connectedUsbDeviceUpdater); } @Override public void onStart() { mBluetoothDeviceUpdater.registerCallback(); + mConnectedUsbDeviceUpdater.registerCallback(); } @Override public void onStop() { + mConnectedUsbDeviceUpdater.unregisterCallback(); mBluetoothDeviceUpdater.unregisterCallback(); } @@ -70,8 +76,10 @@ public class ConnectedDeviceGroupController extends AbstractPreferenceController super.displayPreference(screen); mPreferenceGroup = (PreferenceGroup) screen.findPreference(KEY); mPreferenceGroup.setVisible(false); + mBluetoothDeviceUpdater.setPrefContext(screen.getContext()); mBluetoothDeviceUpdater.forceUpdate(); + mConnectedUsbDeviceUpdater.initUsbPreference(screen.getContext()); } @Override @@ -100,10 +108,12 @@ public class ConnectedDeviceGroupController extends AbstractPreferenceController } } - private void init(Lifecycle lifecycle, BluetoothDeviceUpdater bluetoothDeviceUpdater) { + private void init(Lifecycle lifecycle, BluetoothDeviceUpdater bluetoothDeviceUpdater, + ConnectedUsbDeviceUpdater connectedUsbDeviceUpdater) { if (lifecycle != null) { lifecycle.addObserver(this); } mBluetoothDeviceUpdater = bluetoothDeviceUpdater; + mConnectedUsbDeviceUpdater = connectedUsbDeviceUpdater; } } diff --git a/src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdater.java b/src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdater.java new file mode 100644 index 00000000000..0468b0f5a35 --- /dev/null +++ b/src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdater.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 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.connecteddevice; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.VisibleForTesting; + +import com.android.settings.R; +import com.android.settings.deviceinfo.UsbBackend; +import com.android.settings.deviceinfo.UsbModeChooserActivity; +import com.android.settings.widget.GearPreference; + +/** + * Controller to maintain connected usb device + */ +public class ConnectedUsbDeviceUpdater { + private Context mContext; + private UsbBackend mUsbBackend; + private DevicePreferenceCallback mDevicePreferenceCallback; + @VisibleForTesting + GearPreference mUsbPreference; + @VisibleForTesting + UsbConnectionBroadcastReceiver mUsbReceiver; + + private UsbConnectionBroadcastReceiver.UsbConnectionListener mUsbConnectionListener = + (connected) -> { + if (connected) { + mUsbPreference.setSummary( + UsbModePreferenceController.getSummary(mUsbBackend.getCurrentMode())); + mDevicePreferenceCallback.onDeviceAdded(mUsbPreference); + } else { + mDevicePreferenceCallback.onDeviceRemoved(mUsbPreference); + } + }; + + public ConnectedUsbDeviceUpdater(Context context, + DevicePreferenceCallback devicePreferenceCallback) { + this(context, devicePreferenceCallback, new UsbBackend(context)); + } + + @VisibleForTesting + ConnectedUsbDeviceUpdater(Context context, DevicePreferenceCallback devicePreferenceCallback, + UsbBackend usbBackend) { + mContext = context; + mDevicePreferenceCallback = devicePreferenceCallback; + mUsbBackend = usbBackend; + mUsbReceiver = new UsbConnectionBroadcastReceiver(context, mUsbConnectionListener); + } + + public void registerCallback() { + // This method could handle multiple register + mUsbReceiver.register(); + } + + public void unregisterCallback() { + mUsbReceiver.unregister(); + } + + public void initUsbPreference(Context context) { + mUsbPreference = new GearPreference(context, null /* AttributeSet */); + mUsbPreference.setTitle(R.string.usb_pref); + mUsbPreference.setIcon(R.drawable.ic_usb); + mUsbPreference.setSelectable(false); + mUsbPreference.setOnGearClickListener((GearPreference p) -> { + final Intent intent = new Intent(mContext, UsbModeChooserActivity.class); + mContext.startActivity(intent); + }); + + forceUpdate(); + } + + private void forceUpdate() { + // Register so we can get the connection state from sticky intent. + //TODO(b/70336520): Use an API to get data instead of sticky intent + mUsbReceiver.register(); + mUsbConnectionListener.onUsbConnectionChanged(mUsbReceiver.isConnected()); + } +} diff --git a/src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiver.java b/src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiver.java new file mode 100644 index 00000000000..07a76915af4 --- /dev/null +++ b/src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiver.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2017 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.connecteddevice; + + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.hardware.usb.UsbManager; + +/** + * Receiver to receive usb update and use {@link UsbConnectionListener} to invoke callback + */ +public class UsbConnectionBroadcastReceiver extends BroadcastReceiver { + private Context mContext; + private UsbConnectionListener mUsbConnectionListener; + private boolean mListeningToUsbEvents; + private boolean mConnected; + + public UsbConnectionBroadcastReceiver(Context context, + UsbConnectionListener usbConnectionListener) { + mContext = context; + mUsbConnectionListener = usbConnectionListener; + } + + @Override + public void onReceive(Context context, Intent intent) { + mConnected = intent != null + && intent.getExtras().getBoolean(UsbManager.USB_CONNECTED); + if (mUsbConnectionListener != null) { + mUsbConnectionListener.onUsbConnectionChanged(mConnected); + } + } + + public void register() { + if (!mListeningToUsbEvents) { + final IntentFilter intentFilter = new IntentFilter(UsbManager.ACTION_USB_STATE); + final Intent intent = mContext.registerReceiver(this, intentFilter); + mConnected = intent != null + && intent.getExtras().getBoolean(UsbManager.USB_CONNECTED); + mListeningToUsbEvents = true; + } + } + + public void unregister() { + if (mListeningToUsbEvents) { + mContext.unregisterReceiver(this); + mListeningToUsbEvents = false; + } + } + + public boolean isConnected() { + return mConnected; + } + + /** + * Interface definition for a callback to be invoked when usb connection is changed. + */ + interface UsbConnectionListener { + void onUsbConnectionChanged(boolean connected); + } +} diff --git a/src/com/android/settings/connecteddevice/UsbModePreferenceController.java b/src/com/android/settings/connecteddevice/UsbModePreferenceController.java index a6cb9be1e7f..869352006c5 100644 --- a/src/com/android/settings/connecteddevice/UsbModePreferenceController.java +++ b/src/com/android/settings/connecteddevice/UsbModePreferenceController.java @@ -15,17 +15,12 @@ */ package com.android.settings.connecteddevice; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.hardware.usb.UsbManager; -import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; -import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.deviceinfo.UsbBackend; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -44,19 +39,21 @@ public class UsbModePreferenceController extends AbstractPreferenceController public UsbModePreferenceController(Context context, UsbBackend usbBackend) { super(context); mUsbBackend = usbBackend; - mUsbReceiver = new UsbConnectionBroadcastReceiver(); + mUsbReceiver = new UsbConnectionBroadcastReceiver(mContext, (connected) -> { + updateSummary(mUsbPreference); + }); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); mUsbPreference = screen.findPreference(KEY_USB_MODE); - updataSummary(mUsbPreference); + updateSummary(mUsbPreference); } @Override public void updateState(Preference preference) { - updataSummary(preference); + updateSummary(preference); } @Override @@ -79,8 +76,7 @@ public class UsbModePreferenceController extends AbstractPreferenceController mUsbReceiver.register(); } - @VisibleForTesting - int getSummary(int mode) { + public static int getSummary(int mode) { switch (mode) { case UsbBackend.MODE_POWER_SINK | UsbBackend.MODE_DATA_NONE: return R.string.usb_summary_charging_only; @@ -96,11 +92,11 @@ public class UsbModePreferenceController extends AbstractPreferenceController return 0; } - private void updataSummary(Preference preference) { - updataSummary(preference, mUsbBackend.getCurrentMode()); + private void updateSummary(Preference preference) { + updateSummary(preference, mUsbBackend.getCurrentMode()); } - private void updataSummary(Preference preference, int mode) { + private void updateSummary(Preference preference, int mode) { if (preference != null) { if (mUsbReceiver.isConnected()) { preference.setEnabled(true); @@ -112,40 +108,4 @@ public class UsbModePreferenceController extends AbstractPreferenceController } } - private class UsbConnectionBroadcastReceiver extends BroadcastReceiver { - private boolean mListeningToUsbEvents; - private boolean mConnected; - - @Override - public void onReceive(Context context, Intent intent) { - boolean connected = intent != null - && intent.getExtras().getBoolean(UsbManager.USB_CONNECTED); - if (connected != mConnected) { - mConnected = connected; - updataSummary(mUsbPreference); - } - } - - public void register() { - if (!mListeningToUsbEvents) { - IntentFilter intentFilter = new IntentFilter(UsbManager.ACTION_USB_STATE); - Intent intent = mContext.registerReceiver(this, intentFilter); - mConnected = intent != null - && intent.getExtras().getBoolean(UsbManager.USB_CONNECTED); - mListeningToUsbEvents = true; - } - } - - public void unregister() { - if (mListeningToUsbEvents) { - mContext.unregisterReceiver(this); - mListeningToUsbEvents = false; - } - } - - public boolean isConnected() { - return mConnected; - } - } - } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java index f9efc0bf64d..aa5eb67342f 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedDeviceGroupControllerTest.java @@ -37,6 +37,7 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; @@ -45,13 +46,17 @@ import org.robolectric.annotation.Config; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class ConnectedDeviceGroupControllerTest { + private static final String PREFERENCE_KEY_1 = "pref_key_1"; + @Mock private DashboardFragment mDashboardFragment; @Mock private ConnectedBluetoothDeviceUpdater mConnectedBluetoothDeviceUpdater; @Mock - private PreferenceScreen mPreferenceScreen; + private ConnectedUsbDeviceUpdater mConnectedUsbDeviceUpdater; @Mock + private PreferenceScreen mPreferenceScreen; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private PreferenceManager mPreferenceManager; private PreferenceGroup mPreferenceGroup; @@ -66,30 +71,33 @@ public class ConnectedDeviceGroupControllerTest { mContext = RuntimeEnvironment.application; mPreference = new Preference(mContext); + mPreference.setKey(PREFERENCE_KEY_1); mLifecycle = new Lifecycle(() -> mLifecycle); mPreferenceGroup = spy(new PreferenceScreen(mContext, null)); doReturn(mPreferenceManager).when(mPreferenceGroup).getPreferenceManager(); doReturn(mContext).when(mDashboardFragment).getContext(); mConnectedDeviceGroupController = new ConnectedDeviceGroupController(mDashboardFragment, - mLifecycle, mConnectedBluetoothDeviceUpdater); + mLifecycle, mConnectedBluetoothDeviceUpdater, mConnectedUsbDeviceUpdater); mConnectedDeviceGroupController.mPreferenceGroup = mPreferenceGroup; } @Test - public void testOnDeviceAdded_firstAdd_becomeVisible() { + public void testOnDeviceAdded_firstAdd_becomeVisibleAndPreferenceAdded() { mConnectedDeviceGroupController.onDeviceAdded(mPreference); assertThat(mPreferenceGroup.isVisible()).isTrue(); + assertThat(mPreferenceGroup.findPreference(PREFERENCE_KEY_1)).isEqualTo(mPreference); } @Test - public void testOnDeviceRemoved_lastRemove_becomeInvisible() { + public void testOnDeviceRemoved_lastRemove_becomeInvisibleAndPreferenceRemoved() { mPreferenceGroup.addPreference(mPreference); mConnectedDeviceGroupController.onDeviceRemoved(mPreference); assertThat(mPreferenceGroup.isVisible()).isFalse(); + assertThat(mPreferenceGroup.getPreferenceCount()).isEqualTo(0); } @Test @@ -117,9 +125,11 @@ public class ConnectedDeviceGroupControllerTest { // register the callback in onStart() mLifecycle.handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event.ON_START); verify(mConnectedBluetoothDeviceUpdater).registerCallback(); + verify(mConnectedUsbDeviceUpdater).registerCallback(); // unregister the callback in onStop() mLifecycle.handleLifecycleEvent(android.arch.lifecycle.Lifecycle.Event.ON_STOP); verify(mConnectedBluetoothDeviceUpdater).unregisterCallback(); + verify(mConnectedUsbDeviceUpdater).unregisterCallback(); } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdaterTest.java b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdaterTest.java new file mode 100644 index 00000000000..16cd3a7a94f --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/ConnectedUsbDeviceUpdaterTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2017 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.connecteddevice; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import com.android.settings.R; +import com.android.settings.TestConfig; +import com.android.settings.deviceinfo.UsbBackend; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class ConnectedUsbDeviceUpdaterTest { + private Context mContext; + private ConnectedUsbDeviceUpdater mDeviceUpdater; + + @Mock + private UsbConnectionBroadcastReceiver mUsbReceiver; + @Mock + private DevicePreferenceCallback mDevicePreferenceCallback; + @Mock + private UsbBackend mUsbBackend; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mDeviceUpdater = new ConnectedUsbDeviceUpdater(mContext, mDevicePreferenceCallback, + mUsbBackend); + mDeviceUpdater.mUsbReceiver = mUsbReceiver; + } + + @Test + public void testInitUsbPreference_preferenceInit() { + mDeviceUpdater.initUsbPreference(mContext); + + assertThat(mDeviceUpdater.mUsbPreference.getTitle()).isEqualTo("USB"); + assertThat(mDeviceUpdater.mUsbPreference.getIcon()).isEqualTo(mContext.getDrawable( + R.drawable.ic_usb)); + assertThat(mDeviceUpdater.mUsbPreference.isSelectable()).isFalse(); + } + + @Test + public void testInitUsbPreference_usbConnected_preferenceAdded() { + doReturn(true).when(mUsbReceiver).isConnected(); + + mDeviceUpdater.initUsbPreference(mContext); + + verify(mDevicePreferenceCallback).onDeviceAdded(mDeviceUpdater.mUsbPreference); + } + + @Test + public void testInitUsbPreference_usbDisconnected_preferenceRemoved() { + doReturn(false).when(mUsbReceiver).isConnected(); + + mDeviceUpdater.initUsbPreference(mContext); + + verify(mDevicePreferenceCallback).onDeviceRemoved(mDeviceUpdater.mUsbPreference); + } + +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiverTest.java new file mode 100644 index 00000000000..06bd5b7834d --- /dev/null +++ b/tests/robotests/src/com/android/settings/connecteddevice/UsbConnectionBroadcastReceiverTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2017 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.connecteddevice; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.hardware.usb.UsbManager; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; + +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class UsbConnectionBroadcastReceiverTest { + private Context mContext; + private UsbConnectionBroadcastReceiver mReceiver; + private ShadowApplication mShadowApplication; + + @Mock + private UsbConnectionBroadcastReceiver.UsbConnectionListener mListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mShadowApplication = ShadowApplication.getInstance(); + mContext = RuntimeEnvironment.application; + mReceiver = new UsbConnectionBroadcastReceiver(mContext, mListener); + } + + @Test + public void testOnReceive_usbConnected_invokeCallback() { + final Intent intent = new Intent(); + intent.putExtra(UsbManager.USB_CONNECTED, true); + + mReceiver.onReceive(mContext, intent); + + verify(mListener).onUsbConnectionChanged(true); + } + + @Test + public void testOnReceive_usbDisconnected_invokeCallback() { + final Intent intent = new Intent(); + intent.putExtra(UsbManager.USB_CONNECTED, false); + + mReceiver.onReceive(mContext, intent); + + verify(mListener).onUsbConnectionChanged(false); + } + + @Test + public void testRegister_invokeMethodTwice_registerOnce() { + mReceiver.register(); + mReceiver.register(); + + final List receivers = mShadowApplication.getReceiversForIntent( + new Intent(UsbManager.ACTION_USB_STATE)); + assertHasOneUsbConnectionBroadcastReceiver(receivers); + } + + @Test + public void testUnregister_invokeMethodTwice_unregisterOnce() { + mReceiver.register(); + mReceiver.unregister(); + mReceiver.unregister(); + + final List receivers = mShadowApplication.getReceiversForIntent( + new Intent(UsbManager.ACTION_USB_STATE)); + assertHasNoUsbConnectionBroadcastReceiver(receivers); + } + + private void assertHasOneUsbConnectionBroadcastReceiver(List receivers) { + boolean hasReceiver = false; + for (final BroadcastReceiver receiver : receivers) { + if (receiver instanceof UsbConnectionBroadcastReceiver) { + // If hasReceiver is true, then we're at the second copy of it so fail. + assertWithMessage( + "Only one instance of UsbConnectionBroadcastReceiver should be " + + "registered").that( + hasReceiver).isFalse(); + hasReceiver = true; + } + } + assertThat(hasReceiver).isTrue(); + } + + private void assertHasNoUsbConnectionBroadcastReceiver(List receivers) { + for (final BroadcastReceiver receiver : receivers) { + assertThat(receiver instanceof UsbConnectionBroadcastReceiver).isFalse(); + } + } +} \ No newline at end of file From d055937664bb023f509fd9c31f9b6ff43fd916b2 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Mon, 4 Dec 2017 13:57:31 -0800 Subject: [PATCH 05/10] Create BluetoothSwitchPreference In the new design, bluetooth preference is not MasterSwitchPreference any more. This cl creates BluetoothSwitchPreference while reuse the BluetoothEnabler. Future cl will remove the BluetoothMasterSwitchPreference when P feature is finalized. Bug: 69333961 Test: RunSettingsRoboTests Change-Id: Ie1f934b4e93a6758a1b0cf83bb5098585a635c2a --- res/values/strings.xml | 2 + res/xml/connected_devices_advanced.xml | 5 +- ...toothMasterSwitchPreferenceController.java | 1 + .../BluetoothSwitchPreferenceController.java | 161 ++++++++++++++++++ ...ancedConnectedDeviceDashboardFragment.java | 7 +- ...uetoothSwitchPreferenceControllerTest.java | 135 +++++++++++++++ 6 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 src/com/android/settings/bluetooth/BluetoothSwitchPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/bluetooth/BluetoothSwitchPreferenceControllerTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 7331d7265f7..45aadf38d12 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -393,6 +393,8 @@ Your devices Pair new device + + Allow device to pair and connect to bluetooth devices Currently connected diff --git a/res/xml/connected_devices_advanced.xml b/res/xml/connected_devices_advanced.xml index 946151f297e..57a2580c20e 100644 --- a/res/xml/connected_devices_advanced.xml +++ b/res/xml/connected_devices_advanced.xml @@ -19,10 +19,11 @@ android:key="connected_devices_screen" android:title="@string/connected_devices_dashboard_title"> - Date: Fri, 8 Dec 2017 12:55:26 -0800 Subject: [PATCH 06/10] Add device name preference in pairing page Also refactor the preference controller 1. Extend from BasePreferenceController. 2. pass in the preference key. Then it could be reused in different places with different key. Bug: 69333961 Test: Screenshot | RunSettingsRoboTests Change-Id: I773ca022baa326481045c1659565c9a21111200a --- res/xml/bluetooth_pairing_detail.xml | 4 ++- ...uetoothDeviceNamePreferenceController.java | 25 +++++++++++++------ ...toothDeviceRenamePreferenceController.java | 25 +++++++++++++------ .../bluetooth/BluetoothPairingDetail.java | 11 ++++---- .../settings/bluetooth/BluetoothSettings.java | 5 +++- ...hDeviceRenamePreferenceControllerTest.java | 5 ++-- 6 files changed, 50 insertions(+), 25 deletions(-) diff --git a/res/xml/bluetooth_pairing_detail.xml b/res/xml/bluetooth_pairing_detail.xml index e60da75f8cc..e654a3cfa2b 100644 --- a/res/xml/bluetooth_pairing_detail.xml +++ b/res/xml/bluetooth_pairing_detail.xml @@ -19,7 +19,9 @@ android:title="@string/bluetooth_pairing_pref_title"> + android:key="bt_pair_rename_devices" + android:title="@string/bluetooth_device_name" + android:summary="@string/summary_placeholder" /> getPreferenceControllers(Context context) { - List controllers = new ArrayList<>(); - mDeviceNamePrefController = new BluetoothDeviceNamePreferenceController(context, - getLifecycle()); - controllers.add(mDeviceNamePrefController); + final List controllers = new ArrayList<>(); + controllers.add( + new BluetoothDeviceRenamePreferenceController(context, KEY_RENAME_DEVICES, this, + getLifecycle())); return controllers; } diff --git a/src/com/android/settings/bluetooth/BluetoothSettings.java b/src/com/android/settings/bluetooth/BluetoothSettings.java index 72d8023983a..3acd4775102 100644 --- a/src/com/android/settings/bluetooth/BluetoothSettings.java +++ b/src/com/android/settings/bluetooth/BluetoothSettings.java @@ -73,6 +73,7 @@ public class BluetoothSettings extends DeviceListPreferenceFragment implements I static final String KEY_PAIRED_DEVICES = "paired_devices"; @VisibleForTesting static final String KEY_FOOTER_PREF = "footer_preference"; + private static final String KEY_RENAME_DEVICES = "bt_rename_device"; @VisibleForTesting PreferenceGroup mPairedDevicesCategory; @@ -369,7 +370,9 @@ public class BluetoothSettings extends DeviceListPreferenceFragment implements I controllers.add(mDeviceNamePrefController); controllers.add(mPairingPrefController); controllers.add(new BluetoothFilesPreferenceController(context)); - controllers.add(new BluetoothDeviceRenamePreferenceController(context, this, lifecycle)); + controllers.add( + new BluetoothDeviceRenamePreferenceController(context, KEY_RENAME_DEVICES, this, + lifecycle)); return controllers; } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceControllerTest.java index cde95cd5b16..62a0d42177d 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDeviceRenamePreferenceControllerTest.java @@ -47,6 +47,7 @@ import org.robolectric.annotation.Config; public class BluetoothDeviceRenamePreferenceControllerTest { private static final String DEVICE_NAME = "Nightshade"; + private static final String PREF_KEY = "bt_rename_devices"; @Mock private LocalBluetoothAdapter mLocalAdapter; @@ -66,10 +67,10 @@ public class BluetoothDeviceRenamePreferenceControllerTest { mContext = spy(RuntimeEnvironment.application); mPreference = new Preference(mContext); - mPreference.setKey(BluetoothDeviceRenamePreferenceController.PREF_KEY); + mPreference.setKey(PREF_KEY); mController = new BluetoothDeviceRenamePreferenceController( - mContext, mFragment, mLocalAdapter); + mContext, PREF_KEY, mFragment, mLocalAdapter); } @Test From 251407a3a4dfd3b9c690c28d7c994cc51ff90b68 Mon Sep 17 00:00:00 2001 From: jeffreyhuang Date: Fri, 8 Dec 2017 14:39:03 -0800 Subject: [PATCH 07/10] Do not show advanced dropdown if only one setting Change-Id: I56eff1198636d0f0b95f4aa58eb90efc346c3f65 Fixes: 69402845 Test: make RunSettingsRoboTests -j40 --- .../system/SystemDashboardFragment.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/com/android/settings/system/SystemDashboardFragment.java b/src/com/android/settings/system/SystemDashboardFragment.java index c01bfcc08ff..b344d8b0df4 100644 --- a/src/com/android/settings/system/SystemDashboardFragment.java +++ b/src/com/android/settings/system/SystemDashboardFragment.java @@ -16,8 +16,12 @@ package com.android.settings.system; import android.content.Context; +import android.os.Bundle; import android.os.UserManager; import android.provider.SearchIndexableResource; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceGroup; +import android.support.v7.preference.PreferenceScreen; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; @@ -40,6 +44,17 @@ public class SystemDashboardFragment extends DashboardFragment { private static final String KEY_RESET = "reset_dashboard"; + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + final PreferenceScreen screen = getPreferenceScreen(); + // We do not want to display an advanced button if only one setting is hidden + if (getVisiblePreferenceCount(screen) == screen.getInitialExpandedChildrenCount() + 1) { + screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE); + } + } + @Override public int getMetricsCategory() { return MetricsProto.MetricsEvent.SETTINGS_SYSTEM_CATEGORY; @@ -74,6 +89,19 @@ public class SystemDashboardFragment extends DashboardFragment { return controllers; } + private int getVisiblePreferenceCount(PreferenceGroup group) { + int visibleCount = 0; + for (int i = 0; i < group.getPreferenceCount(); i++) { + final Preference preference = group.getPreference(i); + if (preference instanceof PreferenceGroup) { + visibleCount += getVisiblePreferenceCount((PreferenceGroup) preference); + } else if (preference.isVisible()) { + visibleCount++; + } + } + return visibleCount; + } + /** * For Search. */ @@ -88,7 +116,8 @@ public class SystemDashboardFragment extends DashboardFragment { } @Override - public List getPreferenceControllers(Context context) { + public List getPreferenceControllers( + Context context) { return buildPreferenceControllers(context); } From 6e447d69bcc39cd6126d2b0a5d540888ddc9a325 Mon Sep 17 00:00:00 2001 From: Doris Ling Date: Thu, 7 Dec 2017 12:38:04 -0800 Subject: [PATCH 08/10] Add controllers for iinstant app related preferences. For AppInfoDashboardFragment: - add app installer preference into the preference screen instead of creating it dynamically. - add controller for the App Installer, Instant App buttons, and instant app domains preferences. Bug: 69384089 Test: make RunSettingsRoboTests Change-Id: I8d362cacb78077c173130018c33c4d00abfe9843 --- res/xml/app_info_settings.xml | 11 ++ .../AppInfoDashboardFragment.java | 124 ++++----------- .../settings/applications/AppStoreUtil.java | 5 - .../AppInstallerInfoPreferenceController.java | 69 ++++++++ ...InstallerPreferenceCategoryController.java | 35 +++++ ...InstantAppButtonsPreferenceController.java | 75 +++++++++ ...InstantAppDomainsPreferenceController.java | 60 +++++++ .../InstantAppButtonsController.java | 1 - .../AppInfoDashboardFragmentTest.java | 104 ------------- ...InstallerInfoPreferenceControllerTest.java | 147 ++++++++++++++++++ ...antAppButtonsPreferenceControllerTest.java | 133 ++++++++++++++++ ...antAppDomainsPreferenceControllerTest.java | 115 ++++++++++++++ 12 files changed, 672 insertions(+), 207 deletions(-) create mode 100644 src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java create mode 100644 src/com/android/settings/applications/appinfo/AppInstallerPreferenceCategoryController.java create mode 100644 src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java create mode 100644 src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceControllerTest.java diff --git a/res/xml/app_info_settings.xml b/res/xml/app_info_settings.xml index 664210be1c3..99c76b8936f 100644 --- a/res/xml/app_info_settings.xml +++ b/res/xml/app_info_settings.xml @@ -132,6 +132,17 @@ + + + + + + + mCallbacks = new ArrayList<>(); @@ -180,7 +176,7 @@ public class AppInfoDashboardFragment extends DashboardFragment @VisibleForTesting ActionButtonPreference mActionButtons; - private InstantAppButtonsController mInstantAppButtonsController; + private InstantAppButtonsPreferenceController mInstantAppButtonPreferenceController; /** * Callback to invoke when app info has been changed. @@ -335,8 +331,6 @@ public class AppInfoDashboardFragment extends DashboardFragment } setHasOptionsMenu(true); - - addDynamicPrefs(); } @Override @@ -381,6 +375,10 @@ public class AppInfoDashboardFragment extends DashboardFragment controllers.add(new AppOpenByDefaultPreferenceController(context, this)); controllers.add(new AppPermissionPreferenceController(context, this, packageName)); controllers.add(new AppVersionPreferenceController(context, this)); + controllers.add(new InstantAppDomainsPreferenceController(context, this)); + final AppInstallerInfoPreferenceController appInstallerInfoPreferenceController = + new AppInstallerInfoPreferenceController(context, this, packageName); + controllers.add(appInstallerInfoPreferenceController); for (AbstractPreferenceController controller : controllers) { mCallbacks.add((Callback) controller); @@ -388,6 +386,9 @@ public class AppInfoDashboardFragment extends DashboardFragment // The following are controllers for preferences that don't need to refresh the preference // state when app state changes. + mInstantAppButtonPreferenceController = + new InstantAppButtonsPreferenceController(context, this, packageName); + controllers.add(mInstantAppButtonPreferenceController); controllers.add(new AppBatteryPreferenceController(context, this, packageName, lifecycle)); controllers.add(new AppMemoryPreferenceController(context, this, lifecycle)); controllers.add(new DefaultHomeShortcutPreferenceController(context, packageName)); @@ -407,6 +408,9 @@ public class AppInfoDashboardFragment extends DashboardFragment controllers.add(new PreferenceCategoryController( context, KEY_ADVANCED_APP_INFO_CATEGORY, advancedAppInfoControllers)); + controllers.add(new AppInstallerPreferenceCategoryController( + context, Arrays.asList(appInstallerInfoPreferenceController))); + return controllers; } @@ -444,8 +448,6 @@ public class AppInfoDashboardFragment extends DashboardFragment .styleActionBar(activity) .bindHeaderButtons(); - mInstantAppDomainsPreference = - (AppDomainsPreference) findPreference(KEY_INSTANT_APP_SUPPORTED_LINKS); } @Override @@ -536,24 +538,6 @@ public class AppInfoDashboardFragment extends DashboardFragment } } - /** - * Utility method to hide and show specific preferences based on whether the app being displayed - * is an Instant App or an installed app. - */ - @VisibleForTesting - void prepareInstantAppPrefs() { - final boolean isInstant = AppUtils.isInstant(mPackageInfo.applicationInfo); - if (isInstant) { - Set handledDomainSet = Utils.getHandledDomains(mPm, mPackageInfo.packageName); - String[] handledDomains = handledDomainSet.toArray(new String[handledDomainSet.size()]); - mInstantAppDomainsPreference.setTitles(handledDomains); - // Dummy values, unused in the implementation - mInstantAppDomainsPreference.setValues(new int[handledDomains.length]); - } else { - getPreferenceScreen().removePreference(mInstantAppDomainsPreference); - } - } - // Utility method to set application label and icon. private void setAppLabelAndIcon(PackageInfo pkgInfo) { final View appSnippet = mHeader.findViewById(R.id.entity_header); @@ -642,7 +626,6 @@ public class AppInfoDashboardFragment extends DashboardFragment checkForceStop(); setAppLabelAndIcon(mPackageInfo); initUninstallButtons(); - prepareInstantAppPrefs(); // Update the preference summaries. Activity context = getActivity(); @@ -676,7 +659,8 @@ public class AppInfoDashboardFragment extends DashboardFragment return true; } - protected AlertDialog createDialog(int id, int errorCode) { + @VisibleForTesting + AlertDialog createDialog(int id, int errorCode) { switch (id) { case DLG_DISABLE: return new AlertDialog.Builder(getActivity()) @@ -722,10 +706,7 @@ public class AppInfoDashboardFragment extends DashboardFragment .setNegativeButton(R.string.dlg_cancel, null) .create(); } - if (mInstantAppButtonsController != null) { - return mInstantAppButtonsController.createDialog(id); - } - return null; + return mInstantAppButtonPreferenceController.createDialog(id); } private void uninstallPkg(String packageName, boolean allUsers, boolean andDisable) { @@ -870,57 +851,6 @@ public class AppInfoDashboardFragment extends DashboardFragment || (mUserManager.isSplitSystemUser() && userCount == 2); } - private void addDynamicPrefs() { - if (UserManager.get(getContext()).isManagedProfile()) { - return; - } - addAppInstallerInfoPref(getPreferenceScreen()); - maybeAddInstantAppButtons(); - } - - private void addAppInstallerInfoPref(PreferenceScreen screen) { - String installerPackageName = - AppStoreUtil.getInstallerPackageName(getContext(), mPackageName); - - final CharSequence installerLabel = Utils.getApplicationLabel(getContext(), - installerPackageName); - if (installerLabel == null) { - return; - } - final int detailsStringId = AppUtils.isInstant(mPackageInfo.applicationInfo) - ? R.string.instant_app_details_summary - : R.string.app_install_details_summary; - PreferenceCategory category = new PreferenceCategory(getPrefContext()); - category.setTitle(R.string.app_install_details_group_title); - screen.addPreference(category); - Preference pref = new Preference(getPrefContext()); - pref.setTitle(R.string.app_install_details_title); - pref.setKey("app_info_store"); - pref.setSummary(getString(detailsStringId, installerLabel)); - - Intent intent = - AppStoreUtil.getAppStoreLink(getContext(), installerPackageName, mPackageName); - if (intent != null) { - pref.setIntent(intent); - } else { - pref.setEnabled(false); - } - category.addPreference(pref); - } - - @VisibleForTesting - void maybeAddInstantAppButtons() { - if (AppUtils.isInstant(mPackageInfo.applicationInfo)) { - LayoutPreference buttons = (LayoutPreference) findPreference(KEY_INSTANT_APP_BUTTONS); - mInstantAppButtonsController = mApplicationFeatureProvider - .newInstantAppButtonsController(this, - buttons.findViewById(R.id.instant_app_button_container), - id -> showDialogInner(id, 0)) - .setPackageName(mPackageName) - .show(); - } - } - private void onPackageRemoved() { getActivity().finishActivity(SUB_INFO_FRAGMENT); getActivity().finishAndRemoveTask(); @@ -1041,9 +971,9 @@ public class AppInfoDashboardFragment extends DashboardFragment mFinishing = true; } - private void showDialogInner(int id, int moveErrorCode) { + public void showDialogInner(int id, int moveErrorCode) { DialogFragment newFragment = - AppInfoBase.MyAlertDialogFragment.newInstance(id, moveErrorCode); + MyAlertDialogFragment.newInstance(id, moveErrorCode); newFragment.setTargetFragment(this, 0); newFragment.show(getFragmentManager(), "dialog " + id); } @@ -1094,8 +1024,8 @@ public class AppInfoDashboardFragment extends DashboardFragment public static void startAppInfoFragment(Class fragment, int titleRes, String pkg, int uid, Activity source, int request, int sourceMetricsCategory) { Bundle args = new Bundle(); - args.putString(AppInfoBase.ARG_PACKAGE_NAME, pkg); - args.putInt(AppInfoBase.ARG_PACKAGE_UID, uid); + args.putString(ARG_PACKAGE_NAME, pkg); + args.putInt(ARG_PACKAGE_UID, uid); Intent intent = Utils.onBuildStartFragmentIntent(source, fragment.getName(), args, null, titleRes, null, false, sourceMetricsCategory); @@ -1116,16 +1046,16 @@ public class AppInfoDashboardFragment extends DashboardFragment public Dialog onCreateDialog(Bundle savedInstanceState) { int id = getArguments().getInt(ARG_ID); int errorCode = getArguments().getInt("moveError"); - Dialog dialog = ((AppInfoBase) getTargetFragment()).createDialog(id, errorCode); + Dialog dialog = ((AppInfoDashboardFragment) getTargetFragment()) + .createDialog(id, errorCode); if (dialog == null) { throw new IllegalArgumentException("unknown id " + id); } return dialog; } - public static AppInfoBase.MyAlertDialogFragment newInstance(int id, int errorCode) { - AppInfoBase.MyAlertDialogFragment - dialogFragment = new AppInfoBase.MyAlertDialogFragment(); + public static MyAlertDialogFragment newInstance(int id, int errorCode) { + MyAlertDialogFragment dialogFragment = new MyAlertDialogFragment(); Bundle args = new Bundle(); args.putInt(ARG_ID, id); args.putInt("moveError", errorCode); diff --git a/src/com/android/settings/applications/AppStoreUtil.java b/src/com/android/settings/applications/AppStoreUtil.java index f9b95b0610b..13e551692c6 100644 --- a/src/com/android/settings/applications/AppStoreUtil.java +++ b/src/com/android/settings/applications/AppStoreUtil.java @@ -16,11 +16,9 @@ package com.android.settings.applications; - import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; -import android.net.Uri; import android.util.Log; // This class provides methods that help dealing with app stores. @@ -43,9 +41,6 @@ public class AppStoreUtil { } catch (IllegalArgumentException e) { Log.e(LOG_TAG, "Exception while retrieving the package installer of " + packageName, e); } - if (installerPackageName == null) { - return null; - } return installerPackageName; } diff --git a/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java b/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java new file mode 100644 index 00000000000..2449004b525 --- /dev/null +++ b/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceController.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2017 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.applications.appinfo; + +import android.content.Context; +import android.content.Intent; +import android.os.UserManager; +import android.support.v7.preference.Preference; + +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.applications.AppInfoDashboardFragment; +import com.android.settings.applications.AppStoreUtil; +import com.android.settingslib.applications.AppUtils; + +public class AppInstallerInfoPreferenceController extends AppInfoPreferenceControllerBase { + + private static final String KEY_APP_INSTALLER_INFO = "app_info_store"; + + private final String mPackageName; + private final String mInstallerPackage; + private final CharSequence mInstallerLabel; + + public AppInstallerInfoPreferenceController(Context context, AppInfoDashboardFragment parent, + String packageName) { + super(context, parent, KEY_APP_INSTALLER_INFO); + mPackageName = packageName; + mInstallerPackage = AppStoreUtil.getInstallerPackageName(mContext, mPackageName); + mInstallerLabel = Utils.getApplicationLabel(mContext, mInstallerPackage); + } + + @Override + public int getAvailabilityStatus() { + if (UserManager.get(mContext).isManagedProfile()) { + return DISABLED_FOR_USER; + } + return mInstallerLabel!= null ? AVAILABLE : DISABLED_FOR_USER; + } + + @Override + public void updateState(Preference preference) { + final int detailsStringId = AppUtils.isInstant(mParent.getPackageInfo().applicationInfo) + ? R.string.instant_app_details_summary + : R.string.app_install_details_summary; + preference.setSummary(mContext.getString(detailsStringId, mInstallerLabel)); + + Intent intent = AppStoreUtil.getAppStoreLink(mContext, mInstallerPackage, mPackageName); + if (intent != null) { + preference.setIntent(intent); + } else { + preference.setEnabled(false); + } + } + +} diff --git a/src/com/android/settings/applications/appinfo/AppInstallerPreferenceCategoryController.java b/src/com/android/settings/applications/appinfo/AppInstallerPreferenceCategoryController.java new file mode 100644 index 00000000000..0e6ffe8dcd2 --- /dev/null +++ b/src/com/android/settings/applications/appinfo/AppInstallerPreferenceCategoryController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2017 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.applications.appinfo; + +import android.content.Context; + +import com.android.settings.widget.PreferenceCategoryController; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.List; + +public class AppInstallerPreferenceCategoryController extends PreferenceCategoryController { + + private static final String KEY_APP_INSTALLER_INFO_CATEGORY = "app_installer"; + + public AppInstallerPreferenceCategoryController(Context context, + List childrenControllers) { + super(context, KEY_APP_INSTALLER_INFO_CATEGORY, childrenControllers); + } + +} diff --git a/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java b/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java new file mode 100644 index 00000000000..e35fa76b85f --- /dev/null +++ b/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceController.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2017 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.applications.appinfo; + +import android.app.AlertDialog; +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.applications.AppInfoDashboardFragment; +import com.android.settings.applications.ApplicationFeatureProvider; +import com.android.settings.applications.LayoutPreference; +import com.android.settings.applications.instantapps.InstantAppButtonsController; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.overlay.FeatureFactory; +import com.android.settingslib.applications.AppUtils; + +public class InstantAppButtonsPreferenceController extends BasePreferenceController { + + private static final String KEY_INSTANT_APP_BUTTONS = "instant_app_buttons"; + + private final AppInfoDashboardFragment mParent; + private final String mPackageName; + private InstantAppButtonsController mInstantAppButtonsController; + + public InstantAppButtonsPreferenceController(Context context, AppInfoDashboardFragment parent, + String packageName) { + super(context, KEY_INSTANT_APP_BUTTONS); + mParent = parent; + mPackageName = packageName; + } + + @Override + public int getAvailabilityStatus() { + return AppUtils.isInstant(mParent.getPackageInfo().applicationInfo) + ? AVAILABLE : DISABLED_FOR_USER; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + LayoutPreference buttons = + (LayoutPreference) screen.findPreference(KEY_INSTANT_APP_BUTTONS); + mInstantAppButtonsController = getApplicationFeatureProvider() + .newInstantAppButtonsController(mParent, + buttons.findViewById(R.id.instant_app_button_container), + id -> mParent.showDialogInner(id, 0)) + .setPackageName(mPackageName) + .show(); + } + + public AlertDialog createDialog(int id) { + return mInstantAppButtonsController.createDialog(id); + } + + @VisibleForTesting + ApplicationFeatureProvider getApplicationFeatureProvider() { + return FeatureFactory.getFactory(mContext).getApplicationFeatureProvider(mContext); + } +} diff --git a/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java b/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java new file mode 100644 index 00000000000..1d2229127da --- /dev/null +++ b/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceController.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 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.applications.appinfo; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.support.v7.preference.Preference; + +import com.android.settings.Utils; +import com.android.settings.applications.AppDomainsPreference; +import com.android.settings.applications.AppInfoDashboardFragment; +import com.android.settingslib.applications.AppUtils; + +import java.util.Set; + +public class InstantAppDomainsPreferenceController extends AppInfoPreferenceControllerBase { + + private static final String KEY_INSTANT_APP_SUPPORTED_LINKS = + "instant_app_launch_supported_domain_urls"; + + private PackageManager mPackageManager; + + public InstantAppDomainsPreferenceController(Context context, AppInfoDashboardFragment parent) { + super(context, parent, KEY_INSTANT_APP_SUPPORTED_LINKS); + mPackageManager = mContext.getPackageManager(); + } + + @Override + public int getAvailabilityStatus() { + return AppUtils.isInstant(mParent.getPackageInfo().applicationInfo) + ? AVAILABLE : DISABLED_FOR_USER; + } + + @Override + public void updateState(Preference preference) { + final AppDomainsPreference instantAppDomainsPreference = (AppDomainsPreference) preference; + final Set handledDomainSet = + Utils.getHandledDomains(mPackageManager, mParent.getPackageInfo().packageName); + final String[] handledDomains = + handledDomainSet.toArray(new String[handledDomainSet.size()]); + instantAppDomainsPreference.setTitles(handledDomains); + // Dummy values, unused in the implementation + instantAppDomainsPreference.setValues(new int[handledDomains.length]); + } + +} diff --git a/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java b/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java index 28e612c98ff..42474a8b297 100644 --- a/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java +++ b/src/com/android/settings/applications/instantapps/InstantAppButtonsController.java @@ -21,7 +21,6 @@ import android.app.Fragment; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.content.pm.PackageManager; import android.os.UserHandle; import android.view.View; import android.widget.Button; diff --git a/tests/robotests/src/com/android/settings/applications/AppInfoDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/applications/AppInfoDashboardFragmentTest.java index d710d7c1953..e742549e720 100644 --- a/tests/robotests/src/com/android/settings/applications/AppInfoDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/applications/AppInfoDashboardFragmentTest.java @@ -23,13 +23,10 @@ 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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.AlertDialog; import android.app.AppOpsManager; -import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -37,16 +34,10 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.os.UserManager; -import android.support.v7.preference.Preference; -import android.support.v7.preference.PreferenceManager; -import android.support.v7.preference.PreferenceScreen; -import android.view.View; import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.TestConfig; -import com.android.settings.applications.instantapps.InstantAppButtonsController; -import com.android.settings.applications.instantapps.InstantAppButtonsController.ShowDialogDelegate; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.widget.ActionButtonPreferenceTest; @@ -246,101 +237,6 @@ public final class AppInfoDashboardFragmentTest { verify(mAppDetail.mActionButtons).setButton2Visible(false); } - @Test - public void instantApps_buttonControllerHandlesDialog() { - InstantAppButtonsController mockController = mock(InstantAppButtonsController.class); - ReflectionHelpers.setField( - mAppDetail, "mInstantAppButtonsController", mockController); - // Make sure first that button controller is not called for supported dialog id - AlertDialog mockDialog = mock(AlertDialog.class); - when(mockController.createDialog(InstantAppButtonsController.DLG_CLEAR_APP)) - .thenReturn(mockDialog); - assertThat(mAppDetail.createDialog(InstantAppButtonsController.DLG_CLEAR_APP, 0)) - .isEqualTo(mockDialog); - verify(mockController).createDialog(InstantAppButtonsController.DLG_CLEAR_APP); - } - - // A helper class for testing the InstantAppButtonsController - it lets us look up the - // preference associated with a key for instant app buttons and get back a mock - // LayoutPreference (to avoid a null pointer exception). - public static class InstalledAppDetailsWithMockInstantButtons extends InstalledAppDetails { - @Mock - private LayoutPreference mInstantButtons; - - public InstalledAppDetailsWithMockInstantButtons() { - super(); - MockitoAnnotations.initMocks(this); - } - - @Override - public Preference findPreference(CharSequence key) { - if (key == "instant_app_buttons") { - return mInstantButtons; - } - return super.findPreference(key); - } - } - - @Test - public void instantApps_instantSpecificButtons() { - // Make this app appear to be instant. - ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", - (InstantAppDataProvider) (i -> true)); - final PackageInfo packageInfo = mock(PackageInfo.class); - - final InstalledAppDetailsWithMockInstantButtons - fragment = new InstalledAppDetailsWithMockInstantButtons(); - ReflectionHelpers.setField(fragment, "mPackageInfo", packageInfo); - ReflectionHelpers.setField(fragment, "mApplicationFeatureProvider", - mFeatureFactory.applicationFeatureProvider); - - final InstantAppButtonsController buttonsController = - mock(InstantAppButtonsController.class); - when(buttonsController.setPackageName(nullable(String.class))) - .thenReturn(buttonsController); - when(mFeatureFactory.applicationFeatureProvider.newInstantAppButtonsController( - nullable(Fragment.class), nullable(View.class), nullable(ShowDialogDelegate.class))) - .thenReturn(buttonsController); - - fragment.maybeAddInstantAppButtons(); - verify(buttonsController).setPackageName(nullable(String.class)); - verify(buttonsController).show(); - } - - @Test - public void instantApps_removeCorrectPref() { - PreferenceScreen mockPreferenceScreen = mock(PreferenceScreen.class); - PreferenceManager mockPreferenceManager = mock(PreferenceManager.class); - AppDomainsPreference mockAppDomainsPref = mock(AppDomainsPreference.class); - PackageInfo mockPackageInfo = mock(PackageInfo.class); - PackageManager mockPackageManager = mock(PackageManager.class); - ReflectionHelpers.setField( - mAppDetail, "mInstantAppDomainsPreference", mockAppDomainsPref); - ReflectionHelpers.setField( - mAppDetail, "mPreferenceManager", mockPreferenceManager); - ReflectionHelpers.setField( - mAppDetail, "mPackageInfo", mockPackageInfo); - ReflectionHelpers.setField( - mAppDetail, "mPm", mockPackageManager); - when(mockPreferenceManager.getPreferenceScreen()).thenReturn(mockPreferenceScreen); - - ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", - (InstantAppDataProvider) (i -> false)); - mAppDetail.prepareInstantAppPrefs(); - - // For the non instant case we remove the app domain pref, and leave the launch pref - verify(mockPreferenceScreen).removePreference(mockAppDomainsPref); - - // For the instant app case we remove the launch preff, and leave the app domain pref - ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", - (InstantAppDataProvider) (i -> true)); - - mAppDetail.prepareInstantAppPrefs(); - // Will be 1 still due to above call - verify(mockPreferenceScreen, times(1)) - .removePreference(mockAppDomainsPref); - } - @Test public void onActivityResult_uninstalledUpdates_shouldInvalidateOptionsMenu() { doReturn(true).when(mAppDetail).refreshUi(); diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java new file mode 100644 index 00000000000..ffbc8f55954 --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/appinfo/AppInstallerInfoPreferenceControllerTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2017 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.applications.appinfo; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.UserManager; +import android.support.v7.preference.Preference; + +import com.android.settings.TestConfig; +import com.android.settings.applications.AppInfoDashboardFragment; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class AppInstallerInfoPreferenceControllerTest { + + @Mock + private UserManager mUserManager; + @Mock + private PackageManager mPackageManager; + @Mock + private ApplicationInfo mAppInfo; + @Mock + private AppInfoDashboardFragment mFragment; + @Mock + private Preference mPreference; + + private Context mContext; + private AppInstallerInfoPreferenceController mController; + + @Before + public void setUp() throws PackageManager.NameNotFoundException { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + final String installerPackage = "Installer1"; + when(mPackageManager.getInstallerPackageName(anyString())).thenReturn(installerPackage); + when(mPackageManager.getApplicationInfo(eq(installerPackage), anyInt())) + .thenReturn(mAppInfo); + mController = new AppInstallerInfoPreferenceController(mContext, mFragment, "Package1"); + } + + @Test + public void getAvailabilityStatus_managedProfile_shouldReturnDisabled() { + when(mUserManager.isManagedProfile()).thenReturn(true); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(mController.DISABLED_FOR_USER); + } + + @Test + public void getAvailabilityStatus_noAppLabel_shouldReturnDisabled() { + when(mUserManager.isManagedProfile()).thenReturn(false); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(mController.DISABLED_FOR_USER); + } + + @Test + public void getAvailabilityStatus_hasAppLabel_shouldReturnAvailable() { + when(mUserManager.isManagedProfile()).thenReturn(false); + when(mAppInfo.loadLabel(mPackageManager)).thenReturn("Label1"); + mController = new AppInstallerInfoPreferenceController(mContext, mFragment, "Package1"); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(mController.AVAILABLE); + } + + @Test + public void updateState_shouldSetSummary() { + final PackageInfo packageInfo = mock(PackageInfo.class); + packageInfo.applicationInfo = mAppInfo; + when(mFragment.getPackageInfo()).thenReturn(packageInfo); + + mController.updateState(mPreference); + + verify(mPreference).setSummary(any()); + } + + @Test + public void updateState_noAppStoreLink_shouldDisablePreference() { + final PackageInfo packageInfo = mock(PackageInfo.class); + packageInfo.applicationInfo = mAppInfo; + when(mFragment.getPackageInfo()).thenReturn(packageInfo); + when(mPackageManager.resolveActivity(any(), anyInt())).thenReturn(null); + + mController.updateState(mPreference); + + verify(mPreference).setEnabled(false); + } + + @Test + public void updateState_hasAppStoreLink_shouldSetPreferenceIntent() { + final PackageInfo packageInfo = mock(PackageInfo.class); + packageInfo.applicationInfo = mAppInfo; + when(mFragment.getPackageInfo()).thenReturn(packageInfo); + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.packageName = "Pkg1"; + resolveInfo.activityInfo.name = "Name1"; + when(mPackageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo); + + mController.updateState(mPreference); + + verify(mPreference, never()).setEnabled(false); + verify(mPreference).setIntent(any(Intent.class)); + } + +} diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceControllerTest.java new file mode 100644 index 00000000000..121659538a4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/appinfo/InstantAppButtonsPreferenceControllerTest.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2017 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.applications.appinfo; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.support.v7.preference.PreferenceScreen; +import android.view.View; + +import com.android.settings.TestConfig; +import com.android.settings.applications.AppInfoDashboardFragment; +import com.android.settings.applications.LayoutPreference; +import com.android.settings.applications.instantapps.InstantAppButtonsController; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.instantapps.InstantAppDataProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class InstantAppButtonsPreferenceControllerTest { + + @Mock + private PackageManager mPackageManager; + @Mock + private ApplicationInfo mAppInfo; + @Mock + private AppInfoDashboardFragment mFragment; + + private Context mContext; + private InstantAppButtonsPreferenceController mController; + private FakeFeatureFactory mFeatureFactory; + + @Before + public void setUp() throws PackageManager.NameNotFoundException { + MockitoAnnotations.initMocks(this); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mContext = spy(RuntimeEnvironment.application); + final PackageInfo packageInfo = mock(PackageInfo.class); + packageInfo.applicationInfo = mAppInfo; + when(mFragment.getPackageInfo()).thenReturn(packageInfo); + mController = + spy(new InstantAppButtonsPreferenceController(mContext, mFragment, "Package1")); + } + + @Test + public void getAvailabilityStatus_notInstantApp_shouldReturnDisabled() { + ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", + (InstantAppDataProvider) (i -> false)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(mController.DISABLED_FOR_USER); + } + + @Test + public void getAvailabilityStatus_isInstantApp_shouldReturnAvailable() { + ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", + (InstantAppDataProvider) (i -> true)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(mController.AVAILABLE); + } + + @Test + public void displayPreference_shouldSetPreferenceTitle() { + final PreferenceScreen screen = mock(PreferenceScreen.class); + final LayoutPreference preference = mock(LayoutPreference.class); + when(screen.findPreference(mController.getPreferenceKey())).thenReturn(preference); + when(mController.getApplicationFeatureProvider()) + .thenReturn(mFeatureFactory.applicationFeatureProvider); + final InstantAppButtonsController buttonsController = + mock(InstantAppButtonsController.class); + when(buttonsController.setPackageName(nullable(String.class))) + .thenReturn(buttonsController); + when(mFeatureFactory.applicationFeatureProvider.newInstantAppButtonsController( + nullable(Fragment.class), nullable(View.class), + nullable(InstantAppButtonsController.ShowDialogDelegate.class))) + .thenReturn(buttonsController); + + mController.displayPreference(screen); + + verify(buttonsController).setPackageName(nullable(String.class)); + verify(buttonsController).show(); + } + + @Test + public void createDialog_shouldReturnDialogFromButtonController() { + final InstantAppButtonsController buttonsController = + mock(InstantAppButtonsController.class); + ReflectionHelpers.setField( + mController, "mInstantAppButtonsController", buttonsController); + final AlertDialog mockDialog = mock(AlertDialog.class); + when(buttonsController.createDialog(InstantAppButtonsController.DLG_CLEAR_APP)) + .thenReturn(mockDialog); + + assertThat(mController.createDialog(InstantAppButtonsController.DLG_CLEAR_APP)) + .isEqualTo(mockDialog); + } + +} diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceControllerTest.java new file mode 100644 index 00000000000..f1776e88e7d --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/appinfo/InstantAppDomainsPreferenceControllerTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2017 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.applications.appinfo; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.IntentFilterVerificationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.util.ArraySet; + +import com.android.settings.TestConfig; +import com.android.settings.applications.AppDomainsPreference; +import com.android.settings.applications.AppInfoDashboardFragment; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.applications.AppUtils; +import com.android.settingslib.applications.instantapps.InstantAppDataProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class InstantAppDomainsPreferenceControllerTest { + + @Mock + private PackageManager mPackageManager; + @Mock + private ApplicationInfo mAppInfo; + @Mock + private AppInfoDashboardFragment mFragment; + @Mock + private AppDomainsPreference mPreference; + + private Context mContext; + private InstantAppDomainsPreferenceController mController; + + @Before + public void setUp() throws PackageManager.NameNotFoundException { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + final PackageInfo packageInfo = mock(PackageInfo.class); + packageInfo.applicationInfo = mAppInfo; + packageInfo.packageName = "Package1"; + when(mFragment.getPackageInfo()).thenReturn(packageInfo); + mController = new InstantAppDomainsPreferenceController(mContext, mFragment); + } + + @Test + public void getAvailabilityStatus_notInstantApp_shouldReturnDisabled() { + ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", + (InstantAppDataProvider) (i -> false)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(mController.DISABLED_FOR_USER); + } + + @Test + public void getAvailabilityStatus_isInstantApp_shouldReturnAvailable() { + ReflectionHelpers.setStaticField(AppUtils.class, "sInstantAppDataProvider", + (InstantAppDataProvider) (i -> true)); + + assertThat(mController.getAvailabilityStatus()).isEqualTo(mController.AVAILABLE); + } + + @Test + public void updateState_shouldSetPreferenceTitle() { + final String[] domain = { "Domain1" }; + final ArraySet domains = new ArraySet<>(); + domains.add(domain[0]); + final List infoList = new ArrayList<>(); + final IntentFilterVerificationInfo info = + new IntentFilterVerificationInfo("Package1", domains); + infoList.add(info); + + when(mPackageManager.getIntentFilterVerifications("Package1")).thenReturn(infoList); + + mController.updateState(mPreference); + + verify(mPreference).setTitles(domain); + } + +} From 7d2b4f5fc7ee7d054af1344c74ec5d439c8c25ce Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Mon, 11 Dec 2017 09:57:54 -0800 Subject: [PATCH 09/10] Add PrefController in XML support Add the ability to define a Preference Controller in xml using the 'controller' tag. This is useful for two reasons: - It allows the controllers to be instantiated via reflection for Slices and Dashboard fragment - Removes the requirement that controllers be defined manually in Fragments In order to be instantiable, they must have a unified construction following either: ClassName(Context) ClassName(Context, String) Also added a robotest that verifies that all controllers defined in XML follow the constructor schema, and extend BasePreferenceController. Test: robotests Bug: 67996923 Change-Id: I304b35dc666daebecf0c9e286696f3f2a510704a --- res/values/attrs.xml | 1 + res/xml/display_settings.xml | 5 +- res/xml/system_dashboard_fragment.xml | 12 +- ...pSettingsActivityPreferenceController.java | 22 +- ...ionalSystemUpdatePreferenceController.java | 19 +- .../SystemUpdatePreferenceController.java | 29 +- .../GesturesSettingPreferenceController.java | 25 +- .../settings/search/XmlParserUtils.java | 4 + .../system/SystemDashboardFragment.java | 11 +- .../robotests/res/xml-mcc999/about_legal.xml | 3 +- .../core/XmlControllerAttributeTest.java | 275 ++++++++++++++++++ .../SystemUpdatePreferenceControllerTest.java | 10 +- .../settings/search/XmlParserUtilTest.java | 10 + 13 files changed, 355 insertions(+), 71 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/core/XmlControllerAttributeTest.java diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 2ba7919a1b5..e3fa0700853 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -97,6 +97,7 @@ + diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml index f9f5d2b6d42..333fd2af705 100644 --- a/res/xml/display_settings.xml +++ b/res/xml/display_settings.xml @@ -25,8 +25,9 @@ - + settings:keywords="@string/keywords_display_brightness_level" + settings:controller="com.android.settings.display.AutoBrightnessPreferenceController"> + + android:fragment="com.android.settings.gestures.GestureSettings" + settings:controller="com.android.settings.gestures.GesturesSettingPreferenceController"/> + android:order="-60" + settings:controller="com.android.settings.backup.BackupSettingsActivityPreferenceController"> @@ -44,14 +46,16 @@ android:title="@string/system_update_settings_list_item_title" android:summary="@string/summary_placeholder" android:icon="@drawable/ic_system_update" - android:order="-30"> + android:order="-30" + settings:controller="com.android.settings.deviceinfo.SystemUpdatePreferenceController"> + android:order="-31" + settings:controller="com.android.settings.deviceinfo.AdditionalSystemUpdatePreferenceController"> diff --git a/src/com/android/settings/backup/BackupSettingsActivityPreferenceController.java b/src/com/android/settings/backup/BackupSettingsActivityPreferenceController.java index afc13b47d1c..7a7530cfc8a 100644 --- a/src/com/android/settings/backup/BackupSettingsActivityPreferenceController.java +++ b/src/com/android/settings/backup/BackupSettingsActivityPreferenceController.java @@ -22,31 +22,29 @@ import android.os.UserManager; import android.support.v7.preference.Preference; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; -public class BackupSettingsActivityPreferenceController extends - AbstractPreferenceController implements PreferenceControllerMixin { +public class BackupSettingsActivityPreferenceController extends BasePreferenceController { + private static final String TAG = "BackupSettingActivityPC"; + private static final String KEY_BACKUP_SETTINGS = "backup_settings"; - private static final String TAG = "BackupSettingActivityPC" ; private final UserManager mUm; private final BackupManager mBackupManager; public BackupSettingsActivityPreferenceController(Context context) { - super(context); + super(context, KEY_BACKUP_SETTINGS); mUm = (UserManager) context.getSystemService(Context.USER_SERVICE); mBackupManager = new BackupManager(context); } @Override - public boolean isAvailable() { - return mUm.isAdminUser(); - } - - @Override - public String getPreferenceKey() { - return KEY_BACKUP_SETTINGS; + public int getAvailabilityStatus() { + return mUm.isAdminUser() + ? AVAILABLE + : DISABLED_UNSUPPORTED; } @Override @@ -57,4 +55,4 @@ public class BackupSettingsActivityPreferenceController extends ? R.string.accessibility_feature_state_on : R.string.accessibility_feature_state_off); } -} +} \ No newline at end of file diff --git a/src/com/android/settings/deviceinfo/AdditionalSystemUpdatePreferenceController.java b/src/com/android/settings/deviceinfo/AdditionalSystemUpdatePreferenceController.java index 06bdb3fae81..f91ed4e69a5 100644 --- a/src/com/android/settings/deviceinfo/AdditionalSystemUpdatePreferenceController.java +++ b/src/com/android/settings/deviceinfo/AdditionalSystemUpdatePreferenceController.java @@ -17,26 +17,23 @@ package com.android.settings.deviceinfo; import android.content.Context; +import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; import com.android.settingslib.core.AbstractPreferenceController; -public class AdditionalSystemUpdatePreferenceController extends - AbstractPreferenceController implements PreferenceControllerMixin { +public class AdditionalSystemUpdatePreferenceController extends BasePreferenceController { private static final String KEY_UPDATE_SETTING = "additional_system_update_settings"; public AdditionalSystemUpdatePreferenceController(Context context) { - super(context); + super(context, KEY_UPDATE_SETTING); } @Override - public boolean isAvailable() { + public int getAvailabilityStatus() { return mContext.getResources().getBoolean( - com.android.settings.R.bool.config_additional_system_update_setting_enable); + com.android.settings.R.bool.config_additional_system_update_setting_enable) + ? AVAILABLE + : DISABLED_UNSUPPORTED; } - - @Override - public String getPreferenceKey() { - return KEY_UPDATE_SETTING; - } -} +} \ No newline at end of file diff --git a/src/com/android/settings/deviceinfo/SystemUpdatePreferenceController.java b/src/com/android/settings/deviceinfo/SystemUpdatePreferenceController.java index d8a64a82113..92c33d86e4b 100644 --- a/src/com/android/settings/deviceinfo/SystemUpdatePreferenceController.java +++ b/src/com/android/settings/deviceinfo/SystemUpdatePreferenceController.java @@ -30,11 +30,9 @@ import android.util.Log; import com.android.settings.R; import com.android.settings.Utils; -import com.android.settings.core.PreferenceControllerMixin; -import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settings.core.BasePreferenceController; -public class SystemUpdatePreferenceController extends AbstractPreferenceController implements - PreferenceControllerMixin { +public class SystemUpdatePreferenceController extends BasePreferenceController { private static final String TAG = "SysUpdatePrefContr"; @@ -42,19 +40,16 @@ public class SystemUpdatePreferenceController extends AbstractPreferenceControll private final UserManager mUm; - public SystemUpdatePreferenceController(Context context, UserManager um) { - super(context); - mUm = um; + public SystemUpdatePreferenceController(Context context) { + super(context, KEY_SYSTEM_UPDATE_SETTINGS); + mUm = UserManager.get(context); } @Override - public boolean isAvailable() { - return mUm.isAdminUser(); - } - - @Override - public String getPreferenceKey() { - return KEY_SYSTEM_UPDATE_SETTINGS; + public int getAvailabilityStatus() { + return mUm.isAdminUser() + ? AVAILABLE + : DISABLED_UNSUPPORTED; } @Override @@ -62,14 +57,14 @@ public class SystemUpdatePreferenceController extends AbstractPreferenceControll super.displayPreference(screen); if (isAvailable()) { Utils.updatePreferenceToSpecificActivityOrRemove(mContext, screen, - KEY_SYSTEM_UPDATE_SETTINGS, + getPreferenceKey(), Utils.UPDATE_PREFERENCE_FLAG_SET_TITLE_TO_MATCHING_ACTIVITY); } } @Override public boolean handlePreferenceTreeClick(Preference preference) { - if (KEY_SYSTEM_UPDATE_SETTINGS.equals(preference.getKey())) { + if (TextUtils.equals(getPreferenceKey(), preference.getKey())) { CarrierConfigManager configManager = (CarrierConfigManager) mContext.getSystemService(CARRIER_CONFIG_SERVICE); PersistableBundle b = configManager.getConfig(); @@ -108,4 +103,4 @@ public class SystemUpdatePreferenceController extends AbstractPreferenceControll mContext.getApplicationContext().sendBroadcast(intent); } } -} +} \ No newline at end of file diff --git a/src/com/android/settings/gestures/GesturesSettingPreferenceController.java b/src/com/android/settings/gestures/GesturesSettingPreferenceController.java index d1b47b21301..819b12861c2 100644 --- a/src/com/android/settings/gestures/GesturesSettingPreferenceController.java +++ b/src/com/android/settings/gestures/GesturesSettingPreferenceController.java @@ -23,27 +23,26 @@ import android.support.v7.preference.Preference; import com.android.internal.hardware.AmbientDisplayConfiguration; import com.android.settings.R; +import com.android.settings.core.BasePreferenceController; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.AbstractPreferenceController; import java.util.List; -public class GesturesSettingPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { - - private static final String KEY_GESTURES_SETTINGS = "gesture_settings"; - +public class GesturesSettingPreferenceController extends BasePreferenceController { private final AssistGestureFeatureProvider mFeatureProvider; private List mGestureControllers; + private static final String KEY_GESTURES_SETTINGS = "gesture_settings"; + public GesturesSettingPreferenceController(Context context) { - super(context); + super(context, KEY_GESTURES_SETTINGS); mFeatureProvider = FeatureFactory.getFactory(context).getAssistGestureFeatureProvider(); } @Override - public boolean isAvailable() { + public int getAvailabilityStatus() { if (mGestureControllers == null) { mGestureControllers = GestureSettings.buildPreferenceControllers(mContext, null /* lifecycle */, new AmbientDisplayConfiguration(mContext)); @@ -52,12 +51,9 @@ public class GesturesSettingPreferenceController extends AbstractPreferenceContr for (AbstractPreferenceController controller : mGestureControllers) { isAvailable = isAvailable || controller.isAvailable(); } - return isAvailable; - } - - @Override - public String getPreferenceKey() { - return KEY_GESTURES_SETTINGS; + return isAvailable + ? AVAILABLE + : DISABLED_UNSUPPORTED; } @Override @@ -83,5 +79,4 @@ public class GesturesSettingPreferenceController extends AbstractPreferenceContr } preference.setSummary(summary); } - -} +} \ No newline at end of file diff --git a/src/com/android/settings/search/XmlParserUtils.java b/src/com/android/settings/search/XmlParserUtils.java index b4ffc532f7c..27c5cd36593 100644 --- a/src/com/android/settings/search/XmlParserUtils.java +++ b/src/com/android/settings/search/XmlParserUtils.java @@ -71,6 +71,10 @@ public class XmlParserUtils { return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_keywords); } + public static String getController(Context context, AttributeSet attrs) { + return getData(context, attrs, R.styleable.Preference, R.styleable.Preference_controller); + } + public static int getDataIcon(Context context, AttributeSet attrs) { final TypedArray ta = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Preference); diff --git a/src/com/android/settings/system/SystemDashboardFragment.java b/src/com/android/settings/system/SystemDashboardFragment.java index c01bfcc08ff..94e4d800d59 100644 --- a/src/com/android/settings/system/SystemDashboardFragment.java +++ b/src/com/android/settings/system/SystemDashboardFragment.java @@ -67,7 +67,7 @@ public class SystemDashboardFragment extends DashboardFragment { private static List buildPreferenceControllers(Context context) { final List controllers = new ArrayList<>(); - controllers.add(new SystemUpdatePreferenceController(context, UserManager.get(context))); + controllers.add(new SystemUpdatePreferenceController(context)); controllers.add(new AdditionalSystemUpdatePreferenceController(context)); controllers.add(new BackupSettingsActivityPreferenceController(context)); controllers.add(new GesturesSettingPreferenceController(context)); @@ -88,17 +88,18 @@ public class SystemDashboardFragment extends DashboardFragment { } @Override - public List getPreferenceControllers(Context context) { + public List getPreferenceControllers( + Context context) { return buildPreferenceControllers(context); } @Override public List getNonIndexableKeys(Context context) { List keys = super.getNonIndexableKeys(context); - keys.add((new BackupSettingsActivityPreferenceController(context) - .getPreferenceKey())); + keys.add((new BackupSettingsActivityPreferenceController( + context).getPreferenceKey())); keys.add(KEY_RESET); return keys; } }; -} +} \ No newline at end of file diff --git a/tests/robotests/res/xml-mcc999/about_legal.xml b/tests/robotests/res/xml-mcc999/about_legal.xml index 53a2b897391..3e008cb7bad 100644 --- a/tests/robotests/res/xml-mcc999/about_legal.xml +++ b/tests/robotests/res/xml-mcc999/about_legal.xml @@ -30,5 +30,6 @@ + android:title="bears_bears_bears" + settings:controller="mind_flayer"/> \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/core/XmlControllerAttributeTest.java b/tests/robotests/src/com/android/settings/core/XmlControllerAttributeTest.java new file mode 100644 index 00000000000..ed4e815c6e4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/core/XmlControllerAttributeTest.java @@ -0,0 +1,275 @@ +package com.android.settings.core; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.content.Context; +import android.content.res.XmlResourceParser; +import android.provider.SearchIndexableResource; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Xml; + +import com.android.settings.R; +import com.android.settings.TestConfig; +import com.android.settings.search.DatabaseIndexingUtils; +import com.android.settings.search.Indexable; +import com.android.settings.search.SearchIndexableResources; +import com.android.settings.search.XmlParserUtils; +import com.android.settings.security.SecuritySettings; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +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.xmlpull.v1.XmlPullParser; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class XmlControllerAttributeTest { + + // List of classes that are too hard to mock in order to retrieve xml information. + private final List illegalClasses = new ArrayList<>( + Arrays.asList( + SecuritySettings.class + )); + + // List of XML that could be retrieved from the illegalClasses list. + private final List whitelistXml = new ArrayList<>( + Arrays.asList( + R.xml.security_settings_misc, + R.xml.security_settings_lockscreen_profile, + R.xml.security_settings_lockscreen, + R.xml.security_settings_chooser, + R.xml.security_settings_pattern_profile, + R.xml.security_settings_pin_profile, + R.xml.security_settings_password_profile, + R.xml.security_settings_pattern, + R.xml.security_settings_pin, + R.xml.security_settings_password, + R.xml.security_settings, + R.xml.security_settings_status + )); + + private static final String NO_VALID_CONSTRUCTOR_ERROR = + "Controllers added in XML need a constructor following either:" + + "\n\tClassName(Context)\n\tClassName(Context, String)" + + "\nThese controllers are missing a valid constructor:\n"; + + private static final String NOT_BASE_PREF_CONTROLLER_ERROR = + "Controllers added in XML need to extend com.android.settings.core" + + ".BasePreferenceController\nThese controllers do not:\n"; + + private static final String BAD_CLASSNAME_ERROR = + "The following controllers set in the XML did not have valid class names:\n"; + + private static final String BAD_CONSTRUCTOR_ERROR = + "The constructor provided by the following classes were insufficient to instantiate " + + "the object. It could be due to being an interface, abstract, or an " + + "IllegalAccessException. Please fix the following classes:\n"; + + Context mContext; + + private Set mProviderClassesCopy; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mProviderClassesCopy = new HashSet<>(SearchIndexableResources.providerValues()); + } + + @After + public void cleanUp() { + SearchIndexableResources.providerValues().clear(); + SearchIndexableResources.providerValues().addAll(mProviderClassesCopy); + } + + @Test + public void testAllIndexableXML_onlyValidBasePreferenceControllersAdded() { + Set xmlSet = getIndexableXml(); + xmlSet.addAll(whitelistXml); + + List xmlControllers = new ArrayList<>(); + Set invalidConstructors = new HashSet<>(); + Set invalidClassHierarchy = new HashSet<>(); + Set badClassNameControllers = new HashSet<>(); + Set badConstructorControllers = new HashSet<>(); + + for (int resId : xmlSet) { + xmlControllers.addAll(getXmlControllers(resId)); + } + + for (String controllerClassName : xmlControllers) { + Class clazz = getClassFromClassName(controllerClassName); + + if (clazz == null) { + badClassNameControllers.add(controllerClassName); + continue; + } + + Constructor constructor = getConstructorFromClass(clazz); + + if (constructor == null) { + invalidConstructors.add(controllerClassName); + continue; + } + + Object controller = getObjectFromConstructor(constructor); + if (controller == null) { + badConstructorControllers.add(controllerClassName); + continue; + } + + if (!(controller instanceof BasePreferenceController)) { + invalidClassHierarchy.add(controllerClassName); + } + } + + final String invalidConstructorError = buildErrorMessage(NO_VALID_CONSTRUCTOR_ERROR, + invalidConstructors); + final String invalidClassHierarchyError = buildErrorMessage(NOT_BASE_PREF_CONTROLLER_ERROR, + invalidClassHierarchy); + final String badClassNameError = buildErrorMessage(BAD_CLASSNAME_ERROR, + badClassNameControllers); + final String badConstructorError = buildErrorMessage(BAD_CONSTRUCTOR_ERROR, + badConstructorControllers); + + assertWithMessage(invalidConstructorError).that(invalidConstructors).isEmpty(); + assertWithMessage(invalidClassHierarchyError).that(invalidClassHierarchy).isEmpty(); + assertWithMessage(badClassNameError).that(badClassNameControllers).isEmpty(); + assertWithMessage(badConstructorError).that(badConstructorControllers).isEmpty(); + } + + private Set getIndexableXml() { + Set xmlResSet = new HashSet(); + + Collection indexableClasses = SearchIndexableResources.providerValues(); + indexableClasses.removeAll(illegalClasses); + + for (Class clazz : indexableClasses) { + + Indexable.SearchIndexProvider provider = DatabaseIndexingUtils.getSearchIndexProvider( + clazz); + + if (provider == null) { + continue; + } + + List resources = provider.getXmlResourcesToIndex(mContext, + true); + + if (resources == null) { + continue; + } + + for (SearchIndexableResource resource : resources) { + // Add '0's anyway. It won't break the test. + xmlResSet.add(resource.xmlResId); + } + } + return xmlResSet; + } + + private List getXmlControllers(int xmlResId) { + List xmlControllers = new ArrayList<>(); + + XmlResourceParser parser; + try { + parser = mContext.getResources().getXml(xmlResId); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + // Parse next until start tag is found + } + + final int outerDepth = parser.getDepth(); + final AttributeSet attrs = Xml.asAttributeSet(parser); + String controllerClassName; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + controllerClassName = XmlParserUtils.getController(mContext, attrs); + // If controller is not indexed, then it is not compatible with + if (!TextUtils.isEmpty(controllerClassName)) { + xmlControllers.add(controllerClassName); + } + } + } catch (Exception e) { + // Assume an issue with robolectric resources + } + return xmlControllers; + } + + private String buildErrorMessage(String errorSummary, Set errorClasses) { + final StringBuilder error = new StringBuilder(errorSummary); + for (String c : errorClasses) { + error.append(c).append("\n"); + } + return error.toString(); + } + + private Class getClassFromClassName(String className) { + Class clazz = null; + try { + clazz = Class.forName(className); + } catch (ClassNotFoundException e) { + } + return clazz; + } + + private Constructor getConstructorFromClass(Class clazz) { + Constructor constructor = null; + try { + constructor = clazz.getConstructor(Context.class); + } catch (NoSuchMethodException e) { + } + + if (constructor != null) { + return constructor; + } + + try { + constructor = clazz.getConstructor(Context.class, String.class); + } catch (NoSuchMethodException e) { + } + + return constructor; + } + + private Object getObjectFromConstructor(Constructor constructor) { + Object controller = null; + + try { + controller = constructor.newInstance(mContext); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + IllegalArgumentException e) { + } + + if (controller != null) { + return controller; + } + + try { + controller = constructor.newInstance(mContext, "key"); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + IllegalArgumentException e) { + } + + return controller; + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/SystemUpdatePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/SystemUpdatePreferenceControllerTest.java index 05670e28faa..1fd543042c1 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/SystemUpdatePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/SystemUpdatePreferenceControllerTest.java @@ -57,7 +57,9 @@ public class SystemUpdatePreferenceControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - mController = new SystemUpdatePreferenceController(mContext, mUserManager); + + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + mController = new SystemUpdatePreferenceController(mContext); mPreference = new Preference(RuntimeEnvironment.application); mPreference.setKey(mController.getPreferenceKey()); when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); @@ -82,7 +84,7 @@ public class SystemUpdatePreferenceControllerTest { mController.updateNonIndexableKeys(keys); - assertThat(keys.size()).isEqualTo(1); + assertThat(keys).hasSize(1); } @Test @@ -94,8 +96,8 @@ public class SystemUpdatePreferenceControllerTest { @Test public void updateState_shouldSetToAndroidVersion() { - mController = new SystemUpdatePreferenceController( - RuntimeEnvironment.application, mUserManager); + mController = new SystemUpdatePreferenceController(RuntimeEnvironment.application); + mController.updateState(mPreference); assertThat(mPreference.getSummary()) diff --git a/tests/robotests/src/com/android/settings/search/XmlParserUtilTest.java b/tests/robotests/src/com/android/settings/search/XmlParserUtilTest.java index 6050b32396f..2bec503afe5 100644 --- a/tests/robotests/src/com/android/settings/search/XmlParserUtilTest.java +++ b/tests/robotests/src/com/android/settings/search/XmlParserUtilTest.java @@ -128,6 +128,16 @@ public class XmlParserUtilTest { assertThat(key).isNull(); } + @Test + @Config(qualifiers = "mcc999") + public void testControllerAttribute_returnsValidData() { + XmlResourceParser parser = getChildByType(R.xml.about_legal, "Preference"); + final AttributeSet attrs = Xml.asAttributeSet(parser); + + String controller = XmlParserUtils.getController(mContext, attrs); + assertThat(controller).isEqualTo("mind_flayer"); + } + @Test public void testDataSummaryInvalid_ReturnsNull() { XmlResourceParser parser = getParentPrimedParser(R.xml.display_settings); From 5393991573fceda6dd93c9cbfe7ca8cf0b814bce Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Mon, 11 Dec 2017 16:53:40 -0800 Subject: [PATCH 10/10] Add power save action to BatteryBroadcastReceiver This cl makes BatteryBroadcastReceiver also listen to update about battery saver. Bug: 70530651 Test: RunSettingsRoboTests Change-Id: I76b2f1b1047aa195ee9d8ff2a8a330cea31039d4 --- .../fuelgauge/BatteryBroadcastReceiver.java | 36 +++++++++++-------- .../BatteryBroadcastReceiverTest.java | 9 +++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java index f7a2b9a58a6..d0f4080a4e2 100644 --- a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java +++ b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java @@ -20,16 +20,18 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.PowerManager; import android.support.annotation.VisibleForTesting; import com.android.settings.Utils; /** * Use this broadcastReceiver to listen to the battery change, and it will invoke - * {@link OnBatteryChangedListener} if any of the following happens: + * {@link OnBatteryChangedListener} if any of the followings has been changed: * - * 1. Battery level has been changed - * 2. Battery status has been changed + * 1. Battery level(e.g. 100%->99%) + * 2. Battery status(e.g. plugged->unplugged) + * 3. Battery saver(e.g. off->on) */ public class BatteryBroadcastReceiver extends BroadcastReceiver { @@ -58,8 +60,11 @@ public class BatteryBroadcastReceiver extends BroadcastReceiver { } public void register() { - final Intent intent = mContext.registerReceiver(this, - new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); + intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); + + final Intent intent = mContext.registerReceiver(this, intentFilter); updateBatteryStatus(intent, true /* forceUpdate */); } @@ -68,15 +73,18 @@ public class BatteryBroadcastReceiver extends BroadcastReceiver { } private void updateBatteryStatus(Intent intent, boolean forceUpdate) { - if (intent != null && mBatteryListener != null && Intent.ACTION_BATTERY_CHANGED.equals( - intent.getAction())) { - String batteryLevel = Utils.getBatteryPercentage(intent); - String batteryStatus = Utils.getBatteryStatus( - mContext.getResources(), intent); - if (forceUpdate || !batteryLevel.equals(mBatteryLevel) || !batteryStatus.equals( - mBatteryStatus)) { - mBatteryLevel = batteryLevel; - mBatteryStatus = batteryStatus; + if (intent != null && mBatteryListener != null) { + if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) { + final String batteryLevel = Utils.getBatteryPercentage(intent); + final String batteryStatus = Utils.getBatteryStatus( + mContext.getResources(), intent); + if (forceUpdate || !batteryLevel.equals(mBatteryLevel) || !batteryStatus.equals( + mBatteryStatus)) { + mBatteryLevel = batteryLevel; + mBatteryStatus = batteryStatus; + mBatteryListener.onBatteryChanged(); + } + } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) { mBatteryListener.onBatteryChanged(); } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java index a163a43989b..c75dbf496d7 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java @@ -18,6 +18,7 @@ package com.android.settings.fuelgauge; import android.content.Context; import android.content.Intent; import android.os.BatteryManager; +import android.os.PowerManager; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; @@ -82,6 +83,14 @@ public class BatteryBroadcastReceiverTest { verify(mBatteryListener).onBatteryChanged(); } + @Test + public void testOnReceive_powerSaveModeChanged_listenerInvoked() { + mBatteryBroadcastReceiver.onReceive(mContext, + new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)); + + verify(mBatteryListener).onBatteryChanged(); + } + @Test public void testOnReceive_batteryDataNotChanged_listenerNotInvoked() { final String batteryLevel = Utils.getBatteryPercentage(mChargingIntent);