From 33b956d5741dd08a69b74f18bbd58afcc062792f Mon Sep 17 00:00:00 2001 From: Emilian Peev Date: Mon, 8 Jan 2018 18:21:53 +0000 Subject: [PATCH 01/15] Settings: Enable laser sensor toggle for user builds User builds should also have the camera lensor enable/disable developer option. Bug: 71702869 Test: Manual, make ROBOTEST_FILTER=CameraLaserSensorPreferenceControllerTest RunSettingsRoboTests -j40 Change-Id: Iaa1aadfee0351b9e5fcaa8282e4dcd7b16e9ebf4 --- .../development/CameraLaserSensorPreferenceController.java | 5 +---- .../CameraLaserSensorPreferenceControllerTest.java | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/com/android/settings/development/CameraLaserSensorPreferenceController.java b/src/com/android/settings/development/CameraLaserSensorPreferenceController.java index 41a79831bd1..3f0951335d1 100644 --- a/src/com/android/settings/development/CameraLaserSensorPreferenceController.java +++ b/src/com/android/settings/development/CameraLaserSensorPreferenceController.java @@ -57,10 +57,7 @@ public class CameraLaserSensorPreferenceController extends @Override public boolean isAvailable() { - final String buildType = SystemProperties.get(BUILD_TYPE); - return mContext.getResources().getBoolean(R.bool.config_show_camera_laser_sensor) && - (TextUtils.equals(USERDEBUG_BUILD, buildType) || TextUtils.equals(ENG_BUILD, - buildType)); + return mContext.getResources().getBoolean(R.bool.config_show_camera_laser_sensor); } @Override diff --git a/tests/robotests/src/com/android/settings/development/CameraLaserSensorPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/development/CameraLaserSensorPreferenceControllerTest.java index 1443d16d0e0..7c8f3181a71 100644 --- a/tests/robotests/src/com/android/settings/development/CameraLaserSensorPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/development/CameraLaserSensorPreferenceControllerTest.java @@ -96,11 +96,11 @@ public class CameraLaserSensorPreferenceControllerTest { } @Test - public void isAvailable_withUserBuild_shouldReturnFalse() { + public void isAvailable_withUserBuild_shouldReturnTrue() { SettingsShadowSystemProperties.set( CameraLaserSensorPreferenceController.BUILD_TYPE, USER_BUILD); - assertThat(mController.isAvailable()).isFalse(); + assertThat(mController.isAvailable()).isTrue(); } @Test From 713fa6bee0bf665f4fffdf779841315d86028ea3 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Tue, 9 Jan 2018 13:00:34 -0800 Subject: [PATCH 02/15] Hook up smart battery to Settings value. Bug: 71502850 Test: RunSettingsRoboTests Change-Id: Icbc99af8f681e67af54e3509b3eec40ac9d9668e --- .../SmartBatteryPreferenceController.java | 11 ++- .../SmartBatteryPreferenceControllerTest.java | 95 +++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/SmartBatteryPreferenceControllerTest.java diff --git a/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java b/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java index 0002dac9db9..ee5279e027f 100644 --- a/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java +++ b/src/com/android/settings/fuelgauge/SmartBatteryPreferenceController.java @@ -18,7 +18,9 @@ package com.android.settings.fuelgauge; import android.content.Context; +import android.provider.Settings; import android.support.annotation.VisibleForTesting; +import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.Preference; import com.android.settings.applications.LayoutPreference; @@ -30,6 +32,8 @@ import com.android.settings.core.BasePreferenceController; public class SmartBatteryPreferenceController extends BasePreferenceController implements Preference.OnPreferenceChangeListener { private static final String KEY_SMART_BATTERY = "smart_battery"; + private static final int ON = 1; + private static final int OFF = 0; public SmartBatteryPreferenceController(Context context) { super(context, KEY_SMART_BATTERY); @@ -37,18 +41,23 @@ public class SmartBatteryPreferenceController extends BasePreferenceController i @Override public int getAvailabilityStatus() { + // TODO(b/71502850): get Availability from API. The device may not support it. return AVAILABLE; } @Override public void updateState(Preference preference) { super.updateState(preference); + final boolean smartBatteryOn = Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.APP_STANDBY_ENABLED, ON) == ON; + ((SwitchPreference) preference).setChecked(smartBatteryOn); } @Override public boolean onPreferenceChange(Preference preference, Object newValue) { final boolean smartBatteryOn = (Boolean) newValue; - //TODO(b/71502850): use smart battery API here to update the state + Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.APP_STANDBY_ENABLED, + smartBatteryOn ? ON : OFF); return true; } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/SmartBatteryPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/SmartBatteryPreferenceControllerTest.java new file mode 100644 index 00000000000..cc5fb305f80 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/SmartBatteryPreferenceControllerTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.provider.Settings; +import android.support.v14.preference.SwitchPreference; + +import com.android.settings.TestConfig; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SmartBatteryPreferenceControllerTest { + private static final int ON = 1; + private static final int OFF = 0; + + private SmartBatteryPreferenceController mController; + private SwitchPreference mPreference; + private Context mContext; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mController = new SmartBatteryPreferenceController(mContext); + mPreference = new SwitchPreference(mContext); + } + + @Test + public void testUpdateState_smartBatteryOn_preferenceChecked() { + putSmartBatteryValue(ON); + + mController.updateState(mPreference); + + assertThat(mPreference.isChecked()).isTrue(); + } + + @Test + public void testUpdateState_smartBatteryOff_preferenceUnchecked() { + putSmartBatteryValue(OFF); + + mController.updateState(mPreference); + + assertThat(mPreference.isChecked()).isFalse(); + } + + @Test + public void testUpdateState_checkPreference_smartBatteryOn() { + mController.onPreferenceChange(mPreference, true); + + assertThat(getSmartBatteryValue()).isEqualTo(ON); + } + + @Test + public void testUpdateState_unCheckPreference_smartBatteryOff() { + mController.onPreferenceChange(mPreference, false); + + assertThat(getSmartBatteryValue()).isEqualTo(OFF); + } + + private void putSmartBatteryValue(int value) { + Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.APP_STANDBY_ENABLED, + value); + } + + private int getSmartBatteryValue() { + return Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.APP_STANDBY_ENABLED, ON); + } +} From 02b81c137e1624a1d0ae5ea80dcd126d46e061f5 Mon Sep 17 00:00:00 2001 From: Beverly Date: Mon, 8 Jan 2018 16:28:53 -0500 Subject: [PATCH 03/15] DND string fixes Bug: 63077372 Test: make ROBOTEST_FILTER=ZenModeRepeatCallersPreferenceControllerTest RunSettingsRoboTests -j40 Change-Id: Ib2002c75fe86e829bfbbca947affc28dc801cedd --- res/values/strings.xml | 4 ++-- res/xml/zen_mode_behavior_settings.xml | 2 +- .../notification/ZenModeBehaviorSettings.java | 4 +++- ...ModeRepeatCallersPreferenceController.java | 23 +++++++++++++++++-- ...RepeatCallersPreferenceControllerTest.java | 3 ++- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 79faf9b013e..52bcdbfbee7 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6824,10 +6824,10 @@ Add - TURN ON NOW + Turn on now - TURN OFF NOW + Turn off now Do Not Disturb is on until %s diff --git a/res/xml/zen_mode_behavior_settings.xml b/res/xml/zen_mode_behavior_settings.xml index 31d429bb409..57bc4fd436d 100644 --- a/res/xml/zen_mode_behavior_settings.xml +++ b/res/xml/zen_mode_behavior_settings.xml @@ -61,7 +61,7 @@ + android:title="@string/zen_mode_repeat_callers" /> diff --git a/src/com/android/settings/notification/ZenModeBehaviorSettings.java b/src/com/android/settings/notification/ZenModeBehaviorSettings.java index bfa95a75703..e2e85f3b691 100644 --- a/src/com/android/settings/notification/ZenModeBehaviorSettings.java +++ b/src/com/android/settings/notification/ZenModeBehaviorSettings.java @@ -45,7 +45,9 @@ public class ZenModeBehaviorSettings extends ZenModeSettingsBase implements Inde controllers.add(new ZenModeRemindersPreferenceController(context, lifecycle)); controllers.add(new ZenModeMessagesPreferenceController(context, lifecycle)); controllers.add(new ZenModeCallsPreferenceController(context, lifecycle)); - controllers.add(new ZenModeRepeatCallersPreferenceController(context, lifecycle)); + controllers.add(new ZenModeRepeatCallersPreferenceController(context, lifecycle, + context.getResources().getInteger(com.android.internal.R.integer + .config_zen_repeat_callers_threshold))); controllers.add(new ZenModeScreenOnPreferenceController(context, lifecycle)); controllers.add(new ZenModeScreenOffPreferenceController(context, lifecycle)); controllers.add(new ZenModeBehaviorFooterPreferenceController(context, lifecycle)); diff --git a/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java b/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java index d5c0a007119..765d407df97 100644 --- a/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java +++ b/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceController.java @@ -21,9 +21,11 @@ import android.content.Context; import android.provider.Settings; import android.support.v14.preference.SwitchPreference; import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; import android.util.Log; import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; import com.android.settingslib.core.lifecycle.Lifecycle; public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController @@ -31,8 +33,15 @@ public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePre protected static final String KEY = "zen_mode_repeat_callers"; - public ZenModeRepeatCallersPreferenceController(Context context, Lifecycle lifecycle) { + private final ZenModeBackend mBackend; + private final int mRepeatCallersThreshold; + + public ZenModeRepeatCallersPreferenceController(Context context, Lifecycle lifecycle, + int repeatCallersThreshold) { super(context, KEY, lifecycle); + + mRepeatCallersThreshold = repeatCallersThreshold; + mBackend = ZenModeBackend.getInstance(context); } @Override @@ -45,12 +54,17 @@ public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePre return true; } + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + setRepeatCallerSummary(screen.findPreference(KEY)); + } + @Override public void updateState(Preference preference) { super.updateState(preference); SwitchPreference pref = (SwitchPreference) preference; - switch (getZenMode()) { case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: case Settings.Global.ZEN_MODE_ALARMS: @@ -84,4 +98,9 @@ public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePre mBackend.saveSoundPolicy(Policy.PRIORITY_CATEGORY_REPEAT_CALLERS, allowRepeatCallers); return true; } + + private void setRepeatCallerSummary(Preference preference) { + preference.setSummary(mContext.getString(R.string.zen_mode_repeat_callers_summary, + mRepeatCallersThreshold)); + } } diff --git a/tests/robotests/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceControllerTest.java index ba52ce2133f..a7ea4c917ad 100644 --- a/tests/robotests/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/ZenModeRepeatCallersPreferenceControllerTest.java @@ -78,7 +78,8 @@ public class ZenModeRepeatCallersPreferenceControllerTest { mContentResolver = RuntimeEnvironment.application.getContentResolver(); when(mNotificationManager.getNotificationPolicy()).thenReturn(mPolicy); - mController = new ZenModeRepeatCallersPreferenceController(mContext, mock(Lifecycle.class)); + mController = new ZenModeRepeatCallersPreferenceController(mContext, mock(Lifecycle.class), + 15); ReflectionHelpers.setField(mController, "mBackend", mBackend); when(mPreferenceScreen.findPreference(mController.getPreferenceKey())).thenReturn( From ca102facf0aa11a8205c0bf66979d40878f8d508 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Tue, 2 Jan 2018 14:19:06 -0800 Subject: [PATCH 04/15] Add high usage battery tip 1. Add both model and detector 2. Move the screen usage method to BatteryUtils so we could reuse it. 3. Add and update the tests Bug: 70570352 Test: RunSettingsRoboTests Change-Id: I6a7248d9d48ee8cb6fc2c18c8c225210d49b6bc9 --- res/values/strings.xml | 15 +++ .../settings/fuelgauge/BatteryUtils.java | 25 +++++ .../settings/fuelgauge/PowerUsageSummary.java | 23 +---- .../batterytip/BatteryTipLoader.java | 3 + .../detectors/HighUsageDetector.java | 84 +++++++++++++++++ .../batterytip/tips/HighUsageTip.java | 90 ++++++++++++++++++ .../settings/fuelgauge/BatteryUtilsTest.java | 33 +++++++ .../fuelgauge/PowerUsageSummaryTest.java | 38 -------- .../detectors/HighUsageDetectorTest.java | 94 +++++++++++++++++++ 9 files changed, 346 insertions(+), 59 deletions(-) create mode 100644 src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java create mode 100644 src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 79faf9b013e..033211790d9 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4773,6 +4773,21 @@ Low battery capacity Battery can\'t provide good battery life + + Phone used heavily + + Tablet used heavily + + Device used heavily + + About %1$s used since last full charge + + Your phone was used heavily and this consumed a lot of battery. Your battery is behaving normally.\n\n Your phone was used for about %1$s since last full charge.\n\n Total usage: + + Your tablet was used heavily and this consumed a lot of battery. Your battery is behaving normally.\n\n Your tablet was used for about %1$s since last full charge.\n\n Total usage: + + Your device was used heavily and this consumed a lot of battery. Your battery is behaving normally.\n\n Your device was used for about %1$s since last full charge.\n\n Total usage: + Smart battery manager diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java index 68677fab2de..0952f1f095d 100644 --- a/src/com/android/settings/fuelgauge/BatteryUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryUtils.java @@ -345,6 +345,17 @@ public class BatteryUtils { } + /** + * Calculate the screen usage time since last full charge. + * @param batteryStatsHelper utility class that contains the screen usage data + * @return time in millis + */ + public long calculateScreenUsageTime(BatteryStatsHelper batteryStatsHelper) { + final BatterySipper sipper = findBatterySipperByType( + batteryStatsHelper.getUsageList(), BatterySipper.DrainType.SCREEN); + return sipper != null ? sipper.usageTimeMs : 0; + } + public static void logRuntime(String tag, String message, long startTime) { Log.d(tag, message + ": " + (System.currentTimeMillis() - startTime) + "ms"); } @@ -432,6 +443,20 @@ public class BatteryUtils { return batteryInfo; } + /** + * Find the {@link BatterySipper} with the corresponding {@link BatterySipper.DrainType} + */ + public BatterySipper findBatterySipperByType(List usageList, + BatterySipper.DrainType type) { + for (int i = 0, size = usageList.size(); i < size; i++) { + final BatterySipper sipper = usageList.get(i); + if (sipper.drainType == type) { + return sipper; + } + } + return null; + } + private boolean isDataCorrupted() { return mPackageManager == null || mAppOpsManager == null; } diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index 0315f032e66..507043f1557 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -369,8 +369,9 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList restartBatteryInfoLoader(); final long lastFullChargeTime = mBatteryUtils.calculateLastFullChargeTime(mStatsHelper, System.currentTimeMillis()); - updateScreenPreference(); updateLastFullChargePreference(lastFullChargeTime); + mScreenUsagePref.setSubtitle(Utils.formatElapsedTime(getContext(), + mBatteryUtils.calculateScreenUsageTime(mStatsHelper), false)); final CharSequence timeSequence = Utils.formatRelativeTime(context, lastFullChargeTime, false); @@ -393,26 +394,6 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList return new AnomalyDetectionPolicy(getContext()); } - @VisibleForTesting - BatterySipper findBatterySipperByType(List usageList, DrainType type) { - for (int i = 0, size = usageList.size(); i < size; i++) { - final BatterySipper sipper = usageList.get(i); - if (sipper.drainType == type) { - return sipper; - } - } - return null; - } - - @VisibleForTesting - void updateScreenPreference() { - final BatterySipper sipper = findBatterySipperByType( - mStatsHelper.getUsageList(), DrainType.SCREEN); - final long usageTimeMs = sipper != null ? sipper.usageTimeMs : 0; - - mScreenUsagePref.setSubtitle(Utils.formatElapsedTime(getContext(), usageTimeMs, false)); - } - @VisibleForTesting void updateLastFullChargePreference(long timeMs) { final CharSequence timeSequence = Utils.formatRelativeTime(getContext(), timeMs, false); diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java index 9c3f48c0a6b..a1db57a409a 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java @@ -23,6 +23,7 @@ import com.android.internal.os.BatteryStatsHelper; import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.detectors.BatteryTipDetector; +import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector; import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector; import com.android.settings.fuelgauge.batterytip.detectors.SummaryDetector; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; @@ -65,6 +66,8 @@ public class BatteryTipLoader extends AsyncLoader> { mVisibleTips = 0; addBatteryTipFromDetector(tips, new LowBatteryDetector(policy, batteryInfo)); + addBatteryTipFromDetector(tips, + new HighUsageDetector(getContext(), policy, mBatteryStatsHelper)); // Add summary detector at last since it need other detectors to update the mVisibleTips addBatteryTipFromDetector(tips, new SummaryDetector(policy, mVisibleTips)); diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java new file mode 100644 index 00000000000..5c2ecad1e01 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batterytip.detectors; + +import android.content.Context; +import android.os.BatteryStats; +import android.support.annotation.VisibleForTesting; +import android.text.format.DateUtils; + +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +import com.android.settings.Utils; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; +import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; +import com.android.settings.fuelgauge.batterytip.tips.SummaryTip; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Detector whether to show summary tip. This detector should be executed as the last + * {@link BatteryTipDetector} since it need the most up-to-date {@code visibleTips} + */ +public class HighUsageDetector implements BatteryTipDetector { + private BatteryTipPolicy mPolicy; + private BatteryStatsHelper mBatteryStatsHelper; + private List mHighUsageAppList; + private Context mContext; + @VisibleForTesting + BatteryUtils mBatteryUtils; + + public HighUsageDetector(Context context, BatteryTipPolicy policy, + BatteryStatsHelper batteryStatsHelper) { + mContext = context; + mPolicy = policy; + mBatteryStatsHelper = batteryStatsHelper; + mHighUsageAppList = new ArrayList<>(); + mBatteryUtils = BatteryUtils.getInstance(context); + } + + @Override + public BatteryTip detect() { + final long screenUsageTimeMs = mBatteryUtils.calculateScreenUsageTime(mBatteryStatsHelper); + //TODO(b/70570352): Change it to detect whether battery drops 25% in last 2 hours + if (mPolicy.highUsageEnabled && screenUsageTimeMs > DateUtils.HOUR_IN_MILLIS) { + final List batterySippers = mBatteryStatsHelper.getUsageList(); + for (int i = 0, size = batterySippers.size(); i < size; i++) { + final BatterySipper batterySipper = batterySippers.get(i); + if (!mBatteryUtils.shouldHideSipper(batterySipper)) { + final long foregroundTimeMs = mBatteryUtils.getProcessTimeMs( + BatteryUtils.StatusType.FOREGROUND, batterySipper.uidObj, + BatteryStats.STATS_SINCE_CHARGED); + mHighUsageAppList.add(new HighUsageTip.HighUsageApp( + mBatteryUtils.getPackageName(batterySipper.getUid()), + foregroundTimeMs)); + } + } + + mHighUsageAppList = mHighUsageAppList.subList(0, + Math.min(mPolicy.highUsageAppCount, mHighUsageAppList.size())); + Collections.sort(mHighUsageAppList, Collections.reverseOrder()); + } + + return new HighUsageTip(Utils.formatElapsedTime(mContext, screenUsageTimeMs, false), + mHighUsageAppList); + } +} diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java new file mode 100644 index 00000000000..38f2a26c943 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batterytip.tips; + +import android.app.Dialog; +import android.content.Context; + +import com.android.settings.R; + +import java.util.List; + +/** + * Tip to show general summary about battery life + */ +public class HighUsageTip extends BatteryTip { + + private final CharSequence mScreenTimeText; + private final List mHighUsageAppList; + + public HighUsageTip(CharSequence screenTimeText, List appList) { + mShowDialog = true; + mScreenTimeText = screenTimeText; + mType = TipType.HIGH_DEVICE_USAGE; + mHighUsageAppList = appList; + mState = appList.isEmpty() ? StateType.INVISIBLE : StateType.NEW; + } + + @Override + public CharSequence getTitle(Context context) { + return context.getString(R.string.battery_tip_high_usage_title); + } + + @Override + public CharSequence getSummary(Context context) { + return context.getString(R.string.battery_tip_high_usage_summary, mScreenTimeText); + } + + @Override + public int getIconId() { + return R.drawable.ic_perm_device_information_red_24dp; + } + + @Override + public void updateState(BatteryTip tip) { + mState = tip.mState; + } + + @Override + public void action() { + // do nothing + } + + @Override + public Dialog buildDialog() { + //TODO(b/70570352): build the real dialog + return null; + } + + /** + * Class representing app with high screen usage + */ + public static class HighUsageApp implements Comparable { + public final String packageName; + public final long screenOnTimeMs; + + public HighUsageApp(String packageName, long screenOnTimeMs) { + this.packageName = packageName; + this.screenOnTimeMs = screenOnTimeMs; + } + + @Override + public int compareTo(HighUsageApp o) { + return Long.compare(screenOnTimeMs, o.screenOnTimeMs); + } + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java index 1393d5718b0..844aca4c4a5 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryUtilsTest.java @@ -20,7 +20,9 @@ import static android.os.BatteryStats.Uid.PROCESS_STATE_FOREGROUND; import static android.os.BatteryStats.Uid.PROCESS_STATE_FOREGROUND_SERVICE; import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP; import static android.os.BatteryStats.Uid.PROCESS_STATE_TOP_SLEEPING; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.anyLong; @@ -141,6 +143,7 @@ public class BatteryUtilsTest { private BatteryUtils mBatteryUtils; private FakeFeatureFactory mFeatureFactory; private PowerUsageFeatureProvider mProvider; + private List mUsageList; @Before public void setUp() { @@ -194,6 +197,12 @@ public class BatteryUtilsTest { mBatteryUtils.mPowerUsageFeatureProvider = mProvider; doReturn(0L).when(mBatteryUtils).getForegroundServiceTotalTimeUs( any(BatteryStats.Uid.class), anyLong()); + + mUsageList = new ArrayList<>(); + mUsageList.add(mNormalBatterySipper); + mUsageList.add(mScreenBatterySipper); + mUsageList.add(mCellBatterySipper); + doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList(); } @Test @@ -468,4 +477,28 @@ public class BatteryUtilsTest { verify(mBatteryStatsHelper).refreshStats(BatteryStats.STATS_SINCE_CHARGED, mUserManager.getUserProfiles()); } + + @Test + public void testFindBatterySipperByType_findTypeScreen() { + BatterySipper sipper = mBatteryUtils.findBatterySipperByType(mUsageList, + BatterySipper.DrainType.SCREEN); + + assertThat(sipper).isSameAs(mScreenBatterySipper); + } + + @Test + public void testFindBatterySipperByType_findTypeApp() { + BatterySipper sipper = mBatteryUtils.findBatterySipperByType(mUsageList, + BatterySipper.DrainType.APP); + + assertThat(sipper).isSameAs(mNormalBatterySipper); + } + + @Test + public void testCalculateScreenUsageTime_returnCorrectTime() { + mScreenBatterySipper.usageTimeMs = TIME_EXPECTED_FOREGROUND; + + assertThat(mBatteryUtils.calculateScreenUsageTime(mBatteryStatsHelper)).isEqualTo( + TIME_EXPECTED_FOREGROUND); + } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java index 272890939aa..6fecf3ce928 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageSummaryTest.java @@ -247,34 +247,6 @@ public class PowerUsageSummaryTest { assertThat(mFragment.mShowAllApps).isEqualTo(!isShowApps); } - @Test - public void testFindBatterySipperByType_findTypeScreen() { - BatterySipper sipper = mFragment.findBatterySipperByType(mUsageList, - BatterySipper.DrainType.SCREEN); - - assertThat(sipper).isSameAs(mScreenBatterySipper); - } - - @Test - public void testFindBatterySipperByType_findTypeApp() { - BatterySipper sipper = mFragment.findBatterySipperByType(mUsageList, - BatterySipper.DrainType.APP); - - assertThat(sipper).isSameAs(mNormalBatterySipper); - } - - @Test - public void testUpdateScreenPreference_showCorrectSummary() { - doReturn(mScreenBatterySipper).when(mFragment).findBatterySipperByType(any(), any()); - doReturn(mRealContext).when(mFragment).getContext(); - final CharSequence expectedSummary = Utils.formatElapsedTime(mRealContext, USAGE_TIME_MS, - false); - - mFragment.updateScreenPreference(); - - assertThat(mScreenUsagePref.getSubtitle()).isEqualTo(expectedSummary); - } - @Test public void testUpdateLastFullChargePreference_showCorrectSummary() { doReturn(mRealContext).when(mFragment).getContext(); @@ -284,16 +256,6 @@ public class PowerUsageSummaryTest { assertThat(mLastFullChargePref.getSubtitle()).isEqualTo("2 hr. ago"); } - @Test - public void testUpdatePreference_usageListEmpty_shouldNotCrash() { - when(mBatteryHelper.getUsageList()).thenReturn(new ArrayList()); - doReturn(STUB_STRING).when(mFragment).getString(anyInt(), any()); - doReturn(mRealContext).when(mFragment).getContext(); - - // Should not crash when update - mFragment.updateScreenPreference(); - } - @Test public void testNonIndexableKeys_MatchPreferenceKeys() { final Context context = RuntimeEnvironment.application; diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java new file mode 100644 index 00000000000..2a719916fb0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetectorTest.java @@ -0,0 +1,94 @@ +/* + * 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.fuelgauge.batterytip.detectors; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.os.BatteryStats; +import android.text.format.DateUtils; + +import com.android.internal.os.BatterySipper; +import com.android.internal.os.BatteryStatsHelper; +import com.android.settings.TestConfig; +import com.android.settings.fuelgauge.BatteryInfo; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; +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.util.ReflectionHelpers; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class HighUsageDetectorTest { + private Context mContext; + @Mock + private BatteryStatsHelper mBatteryStatsHelper; + @Mock + private BatteryUtils mBatteryUtils; + @Mock + private BatterySipper mBatterySipper; + + private BatteryTipPolicy mPolicy; + private HighUsageDetector mHighUsageDetector; + private List mUsageList; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + mPolicy = spy(new BatteryTipPolicy(mContext)); + mHighUsageDetector = new HighUsageDetector(mContext, mPolicy, mBatteryStatsHelper); + mHighUsageDetector.mBatteryUtils = mBatteryUtils; + + mUsageList = new ArrayList<>(); + mUsageList.add(mBatterySipper); + } + + @Test + public void testDetect_disabledByPolicy_tipInvisible() { + ReflectionHelpers.setField(mPolicy, "highUsageEnabled", false); + + assertThat(mHighUsageDetector.detect().isVisible()).isFalse(); + } + + @Test + public void testDetect_containsHighUsageApp_tipVisible() { + doReturn(2 * DateUtils.HOUR_IN_MILLIS).when(mBatteryUtils).calculateScreenUsageTime( + mBatteryStatsHelper); + doReturn(mUsageList).when(mBatteryStatsHelper).getUsageList(); + doReturn(DateUtils.HOUR_IN_MILLIS).when(mBatteryUtils).getProcessTimeMs( + BatteryUtils.StatusType.FOREGROUND, mBatterySipper.uidObj, + BatteryStats.STATS_SINCE_CHARGED); + + assertThat(mHighUsageDetector.detect().isVisible()).isTrue(); + } +} From e2399b4ad1cd5268767c39ec109404f88545074e Mon Sep 17 00:00:00 2001 From: Antony Sargent Date: Tue, 9 Jan 2018 13:32:28 -0800 Subject: [PATCH 05/15] Fix alignment in location requests screen In Settings->Security & location->Location, the list of apps that have recently requested location was moved up on the screen, and this made it clear that the indentation of the app name was misaligned with the title of Preference items that came after this. It turns out our custom layout for AppPreference reserved a bit more space for icons than regular Preference items. While examining the layout file for location settings I also fixed a small nit in a comment. Fixes: 71056360 Test: Visual (go to Settings->Security & location->Location, and the app names in the list should be indented at the same level as the Preference items below) Change-Id: I87e144c83cc2d13e322f65291415af037c405257 --- res/layout/preference_app.xml | 4 ++-- res/xml/location_settings.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/res/layout/preference_app.xml b/res/layout/preference_app.xml index 8cbb6f2f8da..f9dd6b24a72 100644 --- a/res/layout/preference_app.xml +++ b/res/layout/preference_app.xml @@ -29,9 +29,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="start|center_vertical" - android:minWidth="60dp" + android:minWidth="56dp" android:orientation="horizontal" - android:paddingEnd="12dp" + android:paddingEnd="8dp" android:paddingTop="4dp" android:paddingBottom="4dp"> - + Date: Tue, 9 Jan 2018 15:22:19 -0800 Subject: [PATCH 06/15] Remove redundant assertion from test It's already asserted in other tests, and is causing flakes. Change-Id: I02ea5cf200e535410e7897a51caf1c98d1682068 Fixes: 70530199 Test: robotest --- .../settings/wifi/tether/WifiTetherPreferenceControllerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPreferenceControllerTest.java index a40cce6a3ea..2a39515e982 100644 --- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPreferenceControllerTest.java @@ -137,7 +137,6 @@ public class WifiTetherPreferenceControllerTest { mController.displayPreference(mScreen); mLifecycle.handleLifecycleEvent(ON_START); - assertThat(ShadowWifiTetherSwitchBarController.onStartCalled).isTrue(); verify(mContext).registerReceiver(eq(receiver), any(IntentFilter.class)); verify(pref).setChecked(false); } From fde637ff603ef3d28cacb221a749f3d4653760b8 Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Tue, 2 Jan 2018 14:51:01 -0800 Subject: [PATCH 07/15] Add BatteryTipDialogFragment. This DialogFragment handles all the tip related dialogs. This cl makes: 1. All the tips parcelable. 2. Add dialog for HighUsageTip. It also need adapter to populate app list in dialog. 3. Add and update tests Bug: 70570352 Test: RunSettingsRoboTests Change-Id: Ie4c986172cfc73d8746abc7457d966c8600c6145 --- res/layout/app_high_usage_item.xml | 48 ++++++++ res/layout/recycler_view.xml | 23 ++++ src/com/android/settings/Utils.java | 15 +++ .../fuelgauge/PowerUsageAnomalyDetails.java | 9 +- .../settings/fuelgauge/PowerUsageSummary.java | 2 +- .../batterytip/BatteryTipDialogFragment.java | 104 ++++++++++++++++++ .../BatteryTipPreferenceController.java | 15 ++- .../batterytip/HighUsageAdapter.java | 87 +++++++++++++++ .../fuelgauge/batterytip/HighUsageApp.java | 64 +++++++++++ .../detectors/HighUsageDetector.java | 8 +- .../fuelgauge/batterytip/tips/BatteryTip.java | 42 +++++-- .../batterytip/tips/HighUsageTip.java | 72 +++++++----- .../batterytip/tips/LowBatteryTip.java | 26 +++-- .../fuelgauge/batterytip/tips/SummaryTip.java | 25 +++-- .../src/com/android/settings/UtilsTest.java | 25 ++++- .../PowerUsageAnomalyDetailsTest.java | 13 --- .../BatteryTipPreferenceControllerTest.java | 2 +- .../batterytip/tips/BatteryTipTest.java | 39 ++++++- .../batterytip/tips/HighUsageTipTest.java | 74 +++++++++++++ 19 files changed, 601 insertions(+), 92 deletions(-) create mode 100755 res/layout/app_high_usage_item.xml create mode 100644 res/layout/recycler_view.xml create mode 100644 src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java create mode 100644 src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java create mode 100644 src/com/android/settings/fuelgauge/batterytip/HighUsageApp.java create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTipTest.java diff --git a/res/layout/app_high_usage_item.xml b/res/layout/app_high_usage_item.xml new file mode 100755 index 00000000000..473315f2ade --- /dev/null +++ b/res/layout/app_high_usage_item.xml @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/res/layout/recycler_view.xml b/res/layout/recycler_view.xml new file mode 100644 index 00000000000..a7dabe5d720 --- /dev/null +++ b/res/layout/recycler_view.xml @@ -0,0 +1,23 @@ + + + diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index cd64799f804..ad951216781 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -94,6 +94,7 @@ import android.text.TextUtils; import android.text.format.DateUtils; import android.text.style.TtsSpan; import android.util.ArraySet; +import android.util.IconDrawableFactory; import android.util.Log; import android.util.TypedValue; import android.view.LayoutInflater; @@ -1382,4 +1383,18 @@ public final class Utils extends com.android.settingslib.Utils { } return new BitmapDrawable(null, bitmap); } + + /** + * Get the {@link Drawable} that represents the app icon + */ + public static Drawable getBadgedIcon(IconDrawableFactory iconDrawableFactory, + PackageManager packageManager, String packageName, int userId) { + try { + final ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, + PackageManager.GET_META_DATA); + return iconDrawableFactory.getBadgedIcon(appInfo, userId); + } catch (PackageManager.NameNotFoundException e) { + return packageManager.getDefaultActivityIcon(); + } + } } diff --git a/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java b/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java index 0d73511e84d..143733db2b8 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java +++ b/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java @@ -31,6 +31,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; +import com.android.settings.Utils; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.fuelgauge.anomaly.Anomaly; import com.android.settings.fuelgauge.anomaly.AnomalyDialogFragment; @@ -151,12 +152,6 @@ public class PowerUsageAnomalyDetails extends DashboardFragment implements @VisibleForTesting Drawable getBadgedIcon(String packageName, int userId) { - try { - final ApplicationInfo appInfo = mPackageManager.getApplicationInfo(packageName, - PackageManager.GET_META_DATA); - return mIconDrawableFactory.getBadgedIcon(appInfo, userId); - } catch (PackageManager.NameNotFoundException e) { - return mPackageManager.getDefaultActivityIcon(); - } + return Utils.getBadgedIcon(mIconDrawableFactory, mPackageManager, packageName, userId); } } diff --git a/src/com/android/settings/fuelgauge/PowerUsageSummary.java b/src/com/android/settings/fuelgauge/PowerUsageSummary.java index 507043f1557..205ac0b15a8 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageSummary.java +++ b/src/com/android/settings/fuelgauge/PowerUsageSummary.java @@ -266,7 +266,7 @@ public class PowerUsageSummary extends PowerUsageBase implements OnLongClickList KEY_APP_LIST, lifecycle, activity, this); controllers.add(mBatteryAppListPreferenceController); mBatteryTipPreferenceController = new BatteryTipPreferenceController(context, - KEY_BATTERY_TIP, this); + KEY_BATTERY_TIP, this, this); controllers.add(mBatteryTipPreferenceController); controllers.add(new BatterySaverController(context, getLifecycle())); controllers.add(new BatteryPercentagePreferenceController(context)); diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java new file mode 100644 index 00000000000..3e091b3b44c --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batterytip; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.VisibleForTesting; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; + +import com.android.settings.R; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.fuelgauge.batterytip.BatteryTipPreferenceController.BatteryTipListener; +import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; + +/** + * Dialog Fragment to show action dialog for each anomaly + */ +public class BatteryTipDialogFragment extends InstrumentedDialogFragment implements + DialogInterface.OnClickListener { + + private static final String ARG_BATTERY_TIP = "battery_tip"; + + @VisibleForTesting + BatteryTip mBatteryTip; + + public static BatteryTipDialogFragment newInstance(BatteryTip batteryTip) { + BatteryTipDialogFragment dialogFragment = new BatteryTipDialogFragment(); + + Bundle args = new Bundle(1); + args.putParcelable(ARG_BATTERY_TIP, batteryTip); + dialogFragment.setArguments(args); + + return dialogFragment; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle bundle = getArguments(); + final Context context = getContext(); + + mBatteryTip = bundle.getParcelable(ARG_BATTERY_TIP); + + switch (mBatteryTip.getType()) { + case BatteryTip.TipType.SUMMARY: + case BatteryTip.TipType.LOW_BATTERY: + //TODO(b/70570352): add dialog + return null; + case BatteryTip.TipType.HIGH_DEVICE_USAGE: + final HighUsageTip highUsageTip = (HighUsageTip) mBatteryTip; + final RecyclerView view = (RecyclerView) LayoutInflater.from(context).inflate( + R.layout.recycler_view, + null); + view.setLayoutManager(new LinearLayoutManager(context)); + view.setAdapter(new HighUsageAdapter(context, + highUsageTip.getHighUsageAppList())); + + return new AlertDialog.Builder(context) + .setMessage(getString(R.string.battery_tip_dialog_message, + highUsageTip.getScreenTimeMs())) + .setView(view) + .setPositiveButton(android.R.string.ok, null) + .create(); + default: + throw new IllegalArgumentException("unknown type " + mBatteryTip.getType()); + } + } + + @Override + public int getMetricsCategory() { + //TODO(b/70570352): add correct metric id + return 0; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final BatteryTipListener lsn = (BatteryTipListener) getTargetFragment(); + if (lsn == null) { + return; + } + mBatteryTip.action(); + lsn.onBatteryTipHandled(mBatteryTip); + } + +} diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java index f6114052cf6..9aa70c55264 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceController.java @@ -18,6 +18,7 @@ package com.android.settings.fuelgauge.batterytip; import android.content.Context; import android.support.annotation.VisibleForTesting; +import android.support.v14.preference.PreferenceFragment; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceGroup; import android.support.v7.preference.PreferenceScreen; @@ -34,6 +35,9 @@ import java.util.Map; * Controller in charge of the battery tip group */ public class BatteryTipPreferenceController extends BasePreferenceController { + private static final String TAG = "BatteryTipPreferenceController"; + private static final int REQUEST_ANOMALY_ACTION = 0; + private BatteryTipListener mBatteryTipListener; private List mBatteryTips; private Map mBatteryTipMap; @@ -41,16 +45,18 @@ public class BatteryTipPreferenceController extends BasePreferenceController { PreferenceGroup mPreferenceGroup; @VisibleForTesting Context mPrefContext; + PreferenceFragment mFragment; public BatteryTipPreferenceController(Context context, String preferenceKey) { - this(context, preferenceKey, null); + this(context, preferenceKey, null, null); } public BatteryTipPreferenceController(Context context, String preferenceKey, - BatteryTipListener batteryTipListener) { + PreferenceFragment fragment, BatteryTipListener batteryTipListener) { super(context, preferenceKey); mBatteryTipListener = batteryTipListener; mBatteryTipMap = new HashMap<>(); + mFragment = fragment; } @Override @@ -96,7 +102,10 @@ public class BatteryTipPreferenceController extends BasePreferenceController { final BatteryTip batteryTip = mBatteryTipMap.get(preference.getKey()); if (batteryTip != null) { if (batteryTip.shouldShowDialog()) { - // build and show the dialog + BatteryTipDialogFragment dialogFragment = BatteryTipDialogFragment.newInstance( + batteryTip); + dialogFragment.setTargetFragment(mFragment, REQUEST_ANOMALY_ACTION); + dialogFragment.show(mFragment.getFragmentManager(), TAG); } else { batteryTip.action(); if (mBatteryTipListener != null) { diff --git a/src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java b/src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java new file mode 100644 index 00000000000..8b743946648 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/HighUsageAdapter.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batterytip; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.support.v7.widget.RecyclerView; +import android.util.IconDrawableFactory; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.settings.R; +import com.android.settings.Utils; + +import java.util.List; + +/** + * Adapter for the high usage app list + */ +public class HighUsageAdapter extends RecyclerView.Adapter { + private final Context mContext; + private final IconDrawableFactory mIconDrawableFactory; + private final PackageManager mPackageManager; + private final List mHighUsageAppList; + + public static class ViewHolder extends RecyclerView.ViewHolder { + public View view; + public ImageView appIcon; + public TextView appName; + public TextView appTime; + + public ViewHolder(View v) { + super(v); + view = v; + appIcon = v.findViewById(R.id.app_icon); + appName = v.findViewById(R.id.app_name); + appTime = v.findViewById(R.id.app_screen_time); + } + } + + public HighUsageAdapter(Context context, List highUsageAppList) { + mContext = context; + mHighUsageAppList = highUsageAppList; + mIconDrawableFactory = IconDrawableFactory.newInstance(context); + mPackageManager = context.getPackageManager(); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final View view = LayoutInflater.from(mContext).inflate(R.layout.app_high_usage_item, + parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + final HighUsageApp app = mHighUsageAppList.get(position); + holder.appIcon.setImageDrawable( + Utils.getBadgedIcon(mIconDrawableFactory, mPackageManager, app.packageName, + UserHandle.myUserId())); + holder.appName.setText(Utils.getApplicationLabel(mContext, app.packageName)); + holder.appTime.setText(Utils.formatElapsedTime(mContext, app.screenOnTimeMs, false)); + } + + @Override + public int getItemCount() { + return mHighUsageAppList.size(); + } +} \ No newline at end of file diff --git a/src/com/android/settings/fuelgauge/batterytip/HighUsageApp.java b/src/com/android/settings/fuelgauge/batterytip/HighUsageApp.java new file mode 100644 index 00000000000..f75ecf0e322 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/HighUsageApp.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.fuelgauge.batterytip; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Class representing app with high screen usage + */ +public class HighUsageApp implements Comparable, Parcelable { + public final String packageName; + public final long screenOnTimeMs; + + public HighUsageApp(String packageName, long screenOnTimeMs) { + this.packageName = packageName; + this.screenOnTimeMs = screenOnTimeMs; + } + + private HighUsageApp(Parcel in) { + packageName = in.readString(); + screenOnTimeMs = in.readLong(); + } + + @Override + public int compareTo(HighUsageApp o) { + return Long.compare(screenOnTimeMs, o.screenOnTimeMs); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(packageName); + dest.writeLong(screenOnTimeMs); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public HighUsageApp createFromParcel(Parcel in) { + return new HighUsageApp(in); + } + + public HighUsageApp[] newArray(int size) { + return new HighUsageApp[size]; + } + }; +} \ No newline at end of file diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java index 5c2ecad1e01..237f430c575 100644 --- a/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/HighUsageDetector.java @@ -26,6 +26,7 @@ import com.android.internal.os.BatteryStatsHelper; import com.android.settings.Utils; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy; +import com.android.settings.fuelgauge.batterytip.HighUsageApp; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; import com.android.settings.fuelgauge.batterytip.tips.SummaryTip; @@ -41,7 +42,7 @@ import java.util.List; public class HighUsageDetector implements BatteryTipDetector { private BatteryTipPolicy mPolicy; private BatteryStatsHelper mBatteryStatsHelper; - private List mHighUsageAppList; + private List mHighUsageAppList; private Context mContext; @VisibleForTesting BatteryUtils mBatteryUtils; @@ -67,7 +68,7 @@ public class HighUsageDetector implements BatteryTipDetector { final long foregroundTimeMs = mBatteryUtils.getProcessTimeMs( BatteryUtils.StatusType.FOREGROUND, batterySipper.uidObj, BatteryStats.STATS_SINCE_CHARGED); - mHighUsageAppList.add(new HighUsageTip.HighUsageApp( + mHighUsageAppList.add(new HighUsageApp( mBatteryUtils.getPackageName(batterySipper.getUid()), foregroundTimeMs)); } @@ -78,7 +79,6 @@ public class HighUsageDetector implements BatteryTipDetector { Collections.sort(mHighUsageAppList, Collections.reverseOrder()); } - return new HighUsageTip(Utils.formatElapsedTime(mContext, screenUsageTimeMs, false), - mHighUsageAppList); + return new HighUsageTip(screenUsageTimeMs, mHighUsageAppList); } } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java index 17e395ef185..eadd0e1a811 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java @@ -16,8 +16,9 @@ package com.android.settings.fuelgauge.batterytip.tips; -import android.app.Dialog; import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; import android.support.annotation.IdRes; import android.support.annotation.IntDef; import android.support.v7.preference.Preference; @@ -31,7 +32,7 @@ import java.lang.annotation.RetentionPolicy; * Each {@link BatteryTip} contains basic data(e.g. title, summary, icon) as well as the * pre-defined action(e.g. turn on battery saver) */ -public abstract class BatteryTip implements Comparable { +public abstract class BatteryTip implements Comparable, Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef({StateType.NEW, StateType.HANDLED, @@ -62,12 +63,34 @@ public abstract class BatteryTip implements Comparable { private static final String KEY_PREFIX = "key_battery_tip"; - @TipType protected int mType; - @StateType protected int mState; protected boolean mShowDialog; + BatteryTip(Parcel in) { + mType = in.readInt(); + mState = in.readInt(); + mShowDialog = in.readBoolean(); + } + + BatteryTip(int type, int state, boolean showDialog) { + mType = type; + mState = state; + mShowDialog = showDialog; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mType); + dest.writeInt(mState); + dest.writeBoolean(mShowDialog); + } + public abstract CharSequence getTitle(Context context); public abstract CharSequence getSummary(Context context); @@ -77,6 +100,7 @@ public abstract class BatteryTip implements Comparable { /** * Update the current {@link #mState} using the new {@code tip}. + * * @param tip used to update */ public abstract void updateState(BatteryTip tip); @@ -86,12 +110,6 @@ public abstract class BatteryTip implements Comparable { */ public abstract void action(); - /** - * Build the dialog to display either the info about {@link BatteryTip} or confirmation - * about the action. - */ - public abstract Dialog buildDialog(); - public Preference buildPreference(Context context) { Preference preference = new Preference(context); @@ -110,6 +128,10 @@ public abstract class BatteryTip implements Comparable { return KEY_PREFIX + mType; } + public int getType() { + return mType; + } + @StateType public int getState() { return mState; diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java index 38f2a26c943..001a48eedc3 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTip.java @@ -16,10 +16,14 @@ package com.android.settings.fuelgauge.batterytip.tips; -import android.app.Dialog; import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.VisibleForTesting; import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.fuelgauge.batterytip.HighUsageApp; import java.util.List; @@ -28,15 +32,29 @@ import java.util.List; */ public class HighUsageTip extends BatteryTip { - private final CharSequence mScreenTimeText; - private final List mHighUsageAppList; + private final long mScreenTimeMs; + @VisibleForTesting + final List mHighUsageAppList; - public HighUsageTip(CharSequence screenTimeText, List appList) { - mShowDialog = true; - mScreenTimeText = screenTimeText; - mType = TipType.HIGH_DEVICE_USAGE; + public HighUsageTip(long screenTimeMs, List appList) { + super(TipType.HIGH_DEVICE_USAGE, appList.isEmpty() ? StateType.INVISIBLE : StateType.NEW, + true /* showDialog */); + mScreenTimeMs = screenTimeMs; mHighUsageAppList = appList; - mState = appList.isEmpty() ? StateType.INVISIBLE : StateType.NEW; + } + + @VisibleForTesting + HighUsageTip(Parcel in) { + super(in); + mScreenTimeMs = in.readLong(); + mHighUsageAppList = in.createTypedArrayList(HighUsageApp.CREATOR); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeLong(mScreenTimeMs); + dest.writeTypedList(mHighUsageAppList); } @Override @@ -46,7 +64,8 @@ public class HighUsageTip extends BatteryTip { @Override public CharSequence getSummary(Context context) { - return context.getString(R.string.battery_tip_high_usage_summary, mScreenTimeText); + return context.getString(R.string.battery_tip_high_usage_summary, + Utils.formatElapsedTime(context, mScreenTimeMs, false)); } @Override @@ -64,27 +83,22 @@ public class HighUsageTip extends BatteryTip { // do nothing } - @Override - public Dialog buildDialog() { - //TODO(b/70570352): build the real dialog - return null; + public long getScreenTimeMs() { + return mScreenTimeMs; } - /** - * Class representing app with high screen usage - */ - public static class HighUsageApp implements Comparable { - public final String packageName; - public final long screenOnTimeMs; - - public HighUsageApp(String packageName, long screenOnTimeMs) { - this.packageName = packageName; - this.screenOnTimeMs = screenOnTimeMs; - } - - @Override - public int compareTo(HighUsageApp o) { - return Long.compare(screenOnTimeMs, o.screenOnTimeMs); - } + public List getHighUsageAppList() { + return mHighUsageAppList; } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public BatteryTip createFromParcel(Parcel in) { + return new HighUsageTip(in); + } + + public BatteryTip[] newArray(int size) { + return new HighUsageTip[size]; + } + }; + } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java index 8605fbb6319..4a207e0b126 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/LowBatteryTip.java @@ -16,8 +16,9 @@ package com.android.settings.fuelgauge.batterytip.tips; -import android.app.Dialog; import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; import com.android.settings.R; @@ -27,9 +28,11 @@ import com.android.settings.R; public class LowBatteryTip extends BatteryTip { public LowBatteryTip(@StateType int state) { - mShowDialog = false; - mState = state; - mType = TipType.LOW_BATTERY; + super(TipType.LOW_BATTERY, state, false /* showDialog */); + } + + private LowBatteryTip(Parcel in) { + super(in); } @Override @@ -57,9 +60,14 @@ public class LowBatteryTip extends BatteryTip { // do nothing } - @Override - public Dialog buildDialog() { - //TODO(b/70570352): create the dialog for low battery tip and add test - return null; - } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public BatteryTip createFromParcel(Parcel in) { + return new LowBatteryTip(in); + } + + public BatteryTip[] newArray(int size) { + return new LowBatteryTip[size]; + } + }; + } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java index 2a2deabfa8f..51019a8d35d 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/SummaryTip.java @@ -16,8 +16,9 @@ package com.android.settings.fuelgauge.batterytip.tips; -import android.app.Dialog; import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; import com.android.settings.R; @@ -27,9 +28,11 @@ import com.android.settings.R; public class SummaryTip extends BatteryTip { public SummaryTip(@StateType int state) { - mShowDialog = false; - mState = state; - mType = TipType.SUMMARY; + super(TipType.SUMMARY, state, false /* showDialog */); + } + + private SummaryTip(Parcel in) { + super(in); } @Override @@ -57,9 +60,13 @@ public class SummaryTip extends BatteryTip { // do nothing } - @Override - public Dialog buildDialog() { - //TODO(b/70570352): create the dialog for summary tip and add test - return null; - } + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public BatteryTip createFromParcel(Parcel in) { + return new SummaryTip(in); + } + + public BatteryTip[] newArray(int size) { + return new SummaryTip[size]; + } + }; } diff --git a/tests/robotests/src/com/android/settings/UtilsTest.java b/tests/robotests/src/com/android/settings/UtilsTest.java index f8134578117..fb571bb5cf3 100644 --- a/tests/robotests/src/com/android/settings/UtilsTest.java +++ b/tests/robotests/src/com/android/settings/UtilsTest.java @@ -4,13 +4,16 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.net.ConnectivityManager; import android.net.LinkAddress; @@ -25,6 +28,7 @@ import android.os.storage.VolumeInfo; import android.text.SpannableStringBuilder; import android.text.format.DateUtils; import android.text.style.TtsSpan; +import android.util.IconDrawableFactory; import android.widget.EditText; import android.widget.TextView; @@ -46,8 +50,8 @@ import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class UtilsTest { - private static final String PACKAGE_NAME = "com.android.app"; + private static final int USER_ID = 1; @Mock private WifiManager wifiManager; @@ -59,6 +63,12 @@ public class UtilsTest { private DevicePolicyManagerWrapper mDevicePolicyManager; @Mock private UserManager mUserManager; + @Mock + private PackageManager mPackageManager; + @Mock + private IconDrawableFactory mIconDrawableFactory; + @Mock + private ApplicationInfo mApplicationInfo; private Context mContext; @Before @@ -332,4 +342,17 @@ public class UtilsTest { assertThat(editText.getSelectionEnd()).isEqualTo(length); } + + @Test + public void testGetBadgedIcon_usePackageNameAndUserId() throws + PackageManager.NameNotFoundException { + doReturn(mApplicationInfo).when(mPackageManager).getApplicationInfo(PACKAGE_NAME, + PackageManager.GET_META_DATA); + + Utils.getBadgedIcon(mIconDrawableFactory, mPackageManager, PACKAGE_NAME, USER_ID); + + // Verify that it uses the correct user id + verify(mIconDrawableFactory).getBadgedIcon(mApplicationInfo, USER_ID); + } + } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetailsTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetailsTest.java index c992d0aef63..8aa0659671d 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetailsTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetailsTest.java @@ -67,7 +67,6 @@ public class PowerUsageAnomalyDetailsTest { private static final String PACKAGE_NAME_1 = "com.android.app1"; private static final String PACKAGE_NAME_2 = "com.android.app2"; private static final String PACKAGE_NAME_3 = "com.android.app3"; - private static final int USER_ID = 1; @Mock private SettingsActivity mSettingsActivity; @@ -198,16 +197,4 @@ public class PowerUsageAnomalyDetailsTest { assertThat(mBundle.getParcelableArrayList( PowerUsageAnomalyDetails.EXTRA_ANOMALY_LIST)).isEqualTo(mAnomalyList); } - - @Test - public void testGetBadgedIcon_usePackageNameAndUserId() throws - PackageManager.NameNotFoundException { - doReturn(mApplicationInfo).when(mPackageManager).getApplicationInfo(PACKAGE_NAME_1, - PackageManager.GET_META_DATA); - - mFragment.getBadgedIcon(PACKAGE_NAME_1, USER_ID); - - // Verify that it uses the correct user id - verify(mIconDrawableFactory).getBadgedIcon(mApplicationInfo, USER_ID); - } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceControllerTest.java index 944587fb21e..9f0c61fc9c8 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipPreferenceControllerTest.java @@ -85,7 +85,7 @@ public class BatteryTipPreferenceControllerTest { mNewBatteryTips.add(new SummaryTip(BatteryTip.StateType.INVISIBLE)); mBatteryTipPreferenceController = new BatteryTipPreferenceController(mContext, KEY_PREF, - mBatteryTipListener); + null, mBatteryTipListener); mBatteryTipPreferenceController.mPreferenceGroup = mPreferenceGroup; mBatteryTipPreferenceController.mPrefContext = mContext; } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java index eec8e760914..1c6c8683c04 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTipTest.java @@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Dialog; import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; import android.support.annotation.IdRes; import android.support.v7.preference.Preference; @@ -58,10 +60,32 @@ public class BatteryTipTest { assertThat(preference.getIcon()).isEqualTo(mContext.getDrawable(ICON_ID)); } + @Test + public void testParcelable() { + final BatteryTip batteryTip = new TestBatteryTip(); + + Parcel parcel = Parcel.obtain(); + batteryTip.writeToParcel(parcel, batteryTip.describeContents()); + parcel.setDataPosition(0); + + final BatteryTip parcelTip = new TestBatteryTip(parcel); + + assertThat(parcelTip.getTitle(mContext)).isEqualTo(TITLE); + assertThat(parcelTip.getSummary(mContext)).isEqualTo(SUMMARY); + assertThat(parcelTip.getIconId()).isEqualTo(ICON_ID); + } + /** * Used to test the non abstract methods in {@link TestBatteryTip} */ - public class TestBatteryTip extends BatteryTip { + public static class TestBatteryTip extends BatteryTip { + TestBatteryTip() { + super(TipType.SUMMARY, StateType.NEW, true); + } + + TestBatteryTip(Parcel in) { + super(in); + } @Override public String getTitle(Context context) { @@ -88,10 +112,15 @@ public class BatteryTipTest { // do nothing } - @Override - public Dialog buildDialog() { - return null; - } + public final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public BatteryTip createFromParcel(Parcel in) { + return new TestBatteryTip(in); + } + + public BatteryTip[] newArray(int size) { + return new TestBatteryTip[size]; + } + }; } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTipTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTipTest.java new file mode 100644 index 00000000000..e2f8a26cc7e --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/HighUsageTipTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.fuelgauge.batterytip.tips; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.os.Parcel; +import android.text.format.DateUtils; + +import com.android.settings.TestConfig; +import com.android.settings.fuelgauge.batterytip.HighUsageApp; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class HighUsageTipTest { + private static final String PACKAGE_NAME = "com.android.app"; + private static final long SCREEN_TIME = 30 * DateUtils.MINUTE_IN_MILLIS; + + private Context mContext; + private HighUsageTip mBatteryTip; + private List mUsageAppList; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + + mUsageAppList = new ArrayList<>(); + mUsageAppList.add(new HighUsageApp(PACKAGE_NAME, SCREEN_TIME)); + mBatteryTip = new HighUsageTip(SCREEN_TIME, mUsageAppList); + } + + @Test + public void testParcelable() { + + Parcel parcel = Parcel.obtain(); + mBatteryTip.writeToParcel(parcel, mBatteryTip.describeContents()); + parcel.setDataPosition(0); + + final HighUsageTip parcelTip = new HighUsageTip(parcel); + + assertThat(parcelTip.getTitle(mContext)).isEqualTo("Phone used heavily"); + assertThat(parcelTip.getType()).isEqualTo(BatteryTip.TipType.HIGH_DEVICE_USAGE); + assertThat(parcelTip.getState()).isEqualTo(BatteryTip.StateType.NEW); + assertThat(parcelTip.getScreenTimeMs()).isEqualTo(SCREEN_TIME); + assertThat(parcelTip.mHighUsageAppList.size()).isEqualTo(1); + final HighUsageApp app = parcelTip.mHighUsageAppList.get(0); + assertThat(app.packageName).isEqualTo(PACKAGE_NAME); + assertThat(app.screenOnTimeMs).isEqualTo(SCREEN_TIME); + } +} From 2fd1c9839c08144d51c498baf2bff3fff020c6ee Mon Sep 17 00:00:00 2001 From: jackqdyulei Date: Mon, 23 Oct 2017 18:21:33 -0700 Subject: [PATCH 08/15] Add metric id for anomaly fragments. 1. PowerUsageAnomalyDetails 2. AnomalyDialogFragment Bug: 37681923 Test: Build Change-Id: Ia82fde0fa3e61b6284dc18db3018dd223a0e435b (cherry picked from commit 725562c7484439172dece19c72b5c76943144638) --- .../android/settings/fuelgauge/PowerUsageAnomalyDetails.java | 3 +-- .../settings/fuelgauge/anomaly/AnomalyDialogFragment.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java b/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java index bf4c2afbfb6..0d73511e84d 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java +++ b/src/com/android/settings/fuelgauge/PowerUsageAnomalyDetails.java @@ -125,8 +125,7 @@ public class PowerUsageAnomalyDetails extends DashboardFragment implements @Override public int getMetricsCategory() { - //TODO(b/37681923): add correct metrics category - return 0; + return MetricsProto.MetricsEvent.FUELGAUGE_ANOMALY_DETAIL; } void refreshUi() { diff --git a/src/com/android/settings/fuelgauge/anomaly/AnomalyDialogFragment.java b/src/com/android/settings/fuelgauge/anomaly/AnomalyDialogFragment.java index 69d03b94c1b..8bea04e4ca8 100644 --- a/src/com/android/settings/fuelgauge/anomaly/AnomalyDialogFragment.java +++ b/src/com/android/settings/fuelgauge/anomaly/AnomalyDialogFragment.java @@ -23,6 +23,7 @@ import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.VisibleForTesting; +import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import com.android.settings.fuelgauge.anomaly.action.AnomalyAction; @@ -79,8 +80,7 @@ public class AnomalyDialogFragment extends InstrumentedDialogFragment implements @Override public int getMetricsCategory() { - // TODO(b/37681923): add anomaly metric id - return 0; + return MetricsProto.MetricsEvent.DIALOG_HANDLE_ANOMALY; } @Override From 533dfdfbfdaa695a8d855a4de02c931baa351425 Mon Sep 17 00:00:00 2001 From: yuemingw Date: Wed, 10 Jan 2018 16:20:43 +0000 Subject: [PATCH 09/15] Block Settings APP UI when disallow_airplane_mode is set. Test: runtest -x packages/apps/Settings/tests/unit/src/com/android/settings/core/UserRestrictionTest.java Fix: 67675441 Change-Id: Ice234cecf4de191b797ad4219852cf821720ab78 --- res/xml/network_and_internet.xml | 5 +++-- .../src/com/android/settings/core/UserRestrictionTest.java | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/res/xml/network_and_internet.xml b/res/xml/network_and_internet.xml index bc027d3554e..134f5b3546f 100644 --- a/res/xml/network_and_internet.xml +++ b/res/xml/network_and_internet.xml @@ -75,12 +75,13 @@ settings:userRestriction="no_config_mobile_networks" settings:useAdminDisabledSummary="true" /> - + android:order="5" + settings:userRestriction="no_airplane_mode"/> Date: Fri, 15 Dec 2017 10:48:40 -0800 Subject: [PATCH 10/15] Build slice from indexed data in SliceProvider Connect the SliceIndexing data to the SliceProvider, such that a query to SliceProvider can build a Slice via the indexed data from SlicesIndexingManager. We take the key from the Uri supplied to the SettingSliceProvider and find a potential matching row in the indexed data. The matched data is then used to Build a slice for the caller. Bug: 67996923 Test: robotests Change-Id: If51bfd1a05c3f3817ae720554f95a98fc7b002e1 --- .../search/SearchFeatureProvider.java | 4 + .../slices/SettingsSliceProvider.java | 22 ++- .../slices/SliceBroadcastReceiver.java | 47 +++++- .../settings/slices/SliceBuilderUtils.java | 125 ++++++++++++++++ .../android/settings/slices/SliceData.java | 2 - .../slices/SlicesDatabaseAccessor.java | 141 ++++++++++++++++++ .../settings/slices/SlicesDatabaseHelper.java | 2 +- .../slices/SlicesFeatureProvider.java | 10 ++ .../slices/SlicesFeatureProviderImpl.java | 13 +- .../settings/slices/SlicesIndexer.java | 20 ++- .../settings/slices/FakeToggleController.java | 52 +++++++ .../slices/SettingsSliceProviderTest.java | 97 ++++++++++++ .../slices/SliceBroadcastReceiverTest.java | 121 +++++++++++++++ .../slices/SlicesDatabaseAccessorTest.java | 130 ++++++++++++++++ .../slices/SlicesDatabaseUtilsTest.java | 86 +++++++++++ 15 files changed, 858 insertions(+), 14 deletions(-) create mode 100644 src/com/android/settings/slices/SliceBuilderUtils.java create mode 100644 src/com/android/settings/slices/SlicesDatabaseAccessor.java create mode 100644 tests/robotests/src/com/android/settings/slices/FakeToggleController.java create mode 100644 tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java create mode 100644 tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java create mode 100644 tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java create mode 100644 tests/robotests/src/com/android/settings/slices/SlicesDatabaseUtilsTest.java diff --git a/src/com/android/settings/search/SearchFeatureProvider.java b/src/com/android/settings/search/SearchFeatureProvider.java index 437fc86a165..b9d5045b452 100644 --- a/src/com/android/settings/search/SearchFeatureProvider.java +++ b/src/com/android/settings/search/SearchFeatureProvider.java @@ -28,6 +28,7 @@ import android.widget.Toolbar; import com.android.settings.core.FeatureFlags; import com.android.settings.dashboard.SiteMapManager; +import com.android.settings.overlay.FeatureFactory; import java.util.List; import java.util.concurrent.ExecutorService; @@ -185,6 +186,9 @@ public interface SearchFeatureProvider { } else { intent = new Intent(activity, SearchActivity.class); } + FeatureFactory.getFactory( + activity.getApplicationContext()).getSlicesFeatureProvider() + .indexSliceDataAsync(activity.getApplicationContext()); activity.startActivityForResult(intent, 0 /* requestCode */); }); } diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java index 08ea7c6fdcb..7136182dd51 100644 --- a/src/com/android/settings/slices/SettingsSliceProvider.java +++ b/src/com/android/settings/slices/SettingsSliceProvider.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.graphics.drawable.Icon; import android.net.Uri; import android.net.wifi.WifiManager; +import android.support.annotation.VisibleForTesting; import com.android.settings.R; @@ -32,13 +33,25 @@ import androidx.app.slice.SliceProvider; import androidx.app.slice.builders.ListBuilder; public class SettingsSliceProvider extends SliceProvider { + + private static final String TAG = "SettingsSliceProvider"; + public static final String SLICE_AUTHORITY = "com.android.settings.slices"; public static final String PATH_WIFI = "wifi"; public static final String ACTION_WIFI_CHANGED = "com.android.settings.slice.action.WIFI_CHANGED"; + public static final String ACTION_TOGGLE_CHANGED = + "com.android.settings.slice.action.TOGGLE_CHANGED"; + + public static final String EXTRA_SLICE_KEY = "com.android.settings.slice.extra.key"; + // TODO -- Associate slice URI with search result instead of separate hardcoded thing + + @VisibleForTesting + SlicesDatabaseAccessor mSlicesDatabaseAccessor; + public static Uri getUri(String path) { return new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) @@ -48,19 +61,26 @@ public class SettingsSliceProvider extends SliceProvider { @Override public boolean onCreateSliceProvider() { + mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext()); return true; } @Override public Slice onBindSlice(Uri sliceUri) { String path = sliceUri.getPath(); + // If adding a new Slice, do not directly match Slice URIs. + // Use {@link SlicesDatabaseAccessor}. switch (path) { case "/" + PATH_WIFI: return createWifiSlice(sliceUri); } - throw new IllegalArgumentException("Unrecognized slice uri: " + sliceUri); + + return getHoldingSlice(sliceUri); } + private Slice getHoldingSlice(Uri uri) { + return new ListBuilder(uri).build(); + } // TODO (b/70622039) remove this when the proper wifi slice is enabled. private Slice createWifiSlice(Uri sliceUri) { diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java index b6f2ab945ca..970b72a9caf 100644 --- a/src/com/android/settings/slices/SliceBroadcastReceiver.java +++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java @@ -16,7 +16,9 @@ package com.android.settings.slices; +import static com.android.settings.slices.SettingsSliceProvider.ACTION_TOGGLE_CHANGED; import static com.android.settings.slices.SettingsSliceProvider.ACTION_WIFI_CHANGED; +import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; import android.app.slice.Slice; import android.content.BroadcastReceiver; @@ -25,19 +27,34 @@ import android.content.Intent; import android.net.Uri; import android.net.wifi.WifiManager; import android.os.Handler; +import android.text.TextUtils; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.TogglePreferenceController; /** * Responds to actions performed on slices and notifies slices of updates in state changes. */ public class SliceBroadcastReceiver extends BroadcastReceiver { + private static String TAG = "SettSliceBroadcastRec"; + + /** + * TODO (b/) move wifi action into generalized case. + */ @Override - public void onReceive(Context context, Intent i) { - String action = i.getAction(); + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + String key = intent.getStringExtra(EXTRA_SLICE_KEY); + switch (action) { + case ACTION_TOGGLE_CHANGED: + handleToggleAction(context, key); + break; case ACTION_WIFI_CHANGED: WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); - boolean newState = i.getBooleanExtra(Slice.EXTRA_TOGGLE_STATE, wm.isWifiEnabled()); + boolean newState = intent.getBooleanExtra(Slice.EXTRA_TOGGLE_STATE, + wm.isWifiEnabled()); wm.setWifiEnabled(newState); // Wait a bit for wifi to update (TODO: is there a better way to do this?) Handler h = new Handler(); @@ -48,4 +65,28 @@ public class SliceBroadcastReceiver extends BroadcastReceiver { break; } } + + private void handleToggleAction(Context context, String key) { + if (TextUtils.isEmpty(key)) { + throw new IllegalStateException("No key passed to Intent for toggle controller"); + } + + BasePreferenceController controller = getBasePreferenceController(context, key); + + if (!(controller instanceof TogglePreferenceController)) { + throw new IllegalStateException("Toggle action passed for a non-toggle key: " + key); + } + + // TODO post context.getContentResolver().notifyChanged(uri, null) in the Toggle controller + // so that it's automatically broadcast to any slice. + TogglePreferenceController toggleController = (TogglePreferenceController) controller; + boolean currentValue = toggleController.isChecked(); + toggleController.setChecked(!currentValue); + } + + private BasePreferenceController getBasePreferenceController(Context context, String key) { + final SlicesDatabaseAccessor accessor = new SlicesDatabaseAccessor(context); + final SliceData sliceData = accessor.getSliceDataFromKey(key); + return SliceBuilderUtils.getPreferenceController(context, sliceData); + } } diff --git a/src/com/android/settings/slices/SliceBuilderUtils.java b/src/com/android/settings/slices/SliceBuilderUtils.java new file mode 100644 index 00000000000..3663e897217 --- /dev/null +++ b/src/com/android/settings/slices/SliceBuilderUtils.java @@ -0,0 +1,125 @@ +/* + * 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.slices; + +import static com.android.settings.slices.SettingsSliceProvider.EXTRA_SLICE_KEY; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Icon; +import android.text.TextUtils; + +import com.android.settings.SubSettings; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.TogglePreferenceController; +import com.android.settings.search.DatabaseIndexingUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import androidx.app.slice.Slice; +import androidx.app.slice.builders.ListBuilder; +import androidx.app.slice.builders.ListBuilder.RowBuilder; + +/** + * Utility class to build Slices objects and Preference Controllers based on the Database managed + * by {@link SlicesDatabaseHelper} + */ +public class SliceBuilderUtils { + + private static final String TAG = "SliceBuilder"; + + /** + * Build a Slice from {@link SliceData}. + * + * @return a {@link Slice} based on the data provided by {@param sliceData}. + * Will build an {@link Intent} based Slice unless the Preference Controller name in + * {@param sliceData} is an inline controller. + */ + public static Slice buildSlice(Context context, SliceData sliceData) { + final PendingIntent contentIntent = getContentIntent(context, sliceData); + final Icon icon = Icon.createWithResource(context, sliceData.getIconResource()); + String summaryText = sliceData.getSummary(); + String subtitleText = TextUtils.isEmpty(summaryText) + ? sliceData.getScreenTitle() + : summaryText; + + RowBuilder builder = new RowBuilder(sliceData.getUri()) + .setTitle(sliceData.getTitle()) + .setTitleItem(icon) + .setSubtitle(subtitleText) + .setContentIntent(contentIntent); + + BasePreferenceController controller = getPreferenceController(context, sliceData); + + // TODO (b/71640747) Respect setting availability. + // TODO (b/71640678) Add dynamic summary text. + + if (controller instanceof TogglePreferenceController) { + addToggleAction(context, builder, ((TogglePreferenceController) controller).isChecked(), + sliceData.getKey()); + } + + return new ListBuilder(sliceData.getUri()) + .addRow(builder) + .build(); + } + + /** + * Looks at the {@link SliceData#preferenceController} from {@param sliceData} and attempts to + * build a {@link BasePreferenceController}. + */ + public static BasePreferenceController getPreferenceController(Context context, + SliceData sliceData) { + // TODO check for context-only controller first. + try { + Class clazz = Class.forName(sliceData.getPreferenceController()); + Constructor preferenceConstructor = clazz.getConstructor(Context.class, + String.class); + return (BasePreferenceController) preferenceConstructor.newInstance( + new Object[]{context, sliceData.getKey()}); + } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | + IllegalArgumentException | InvocationTargetException | IllegalAccessException e) { + throw new IllegalStateException( + "Invalid preference controller: " + sliceData.getPreferenceController()); + } + } + + private static void addToggleAction(Context context, RowBuilder builder, boolean isChecked, + String key) { + PendingIntent actionIntent = getActionIntent(context, + SettingsSliceProvider.ACTION_TOGGLE_CHANGED, key); + builder.addToggle(actionIntent, isChecked); + } + + private static PendingIntent getActionIntent(Context context, String action, String key) { + Intent intent = new Intent(action); + intent.setClass(context, SliceBroadcastReceiver.class); + intent.putExtra(EXTRA_SLICE_KEY, key); + return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + private static PendingIntent getContentIntent(Context context, SliceData sliceData) { + Intent intent = DatabaseIndexingUtils.buildSearchResultPageIntent(context, + sliceData.getFragmentClassName(), sliceData.getKey(), sliceData.getScreenTitle(), + 0 /* TODO */); + intent.setClassName("com.android.settings", SubSettings.class.getName()); + return PendingIntent.getActivity(context, 0 /* requestCode */, intent, 0 /* flags */); + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SliceData.java b/src/com/android/settings/slices/SliceData.java index f83676af832..f72add75828 100644 --- a/src/com/android/settings/slices/SliceData.java +++ b/src/com/android/settings/slices/SliceData.java @@ -18,7 +18,6 @@ package com.android.settings.slices; import android.net.Uri; import android.text.TextUtils; - /** * Data class representing a slice stored by {@link SlicesIndexer}. * Note that {@link #key} is treated as a primary key for this class and determines equality. @@ -179,5 +178,4 @@ public class SliceData { return mKey; } } - } \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesDatabaseAccessor.java b/src/com/android/settings/slices/SlicesDatabaseAccessor.java new file mode 100644 index 00000000000..4fca63ad5fb --- /dev/null +++ b/src/com/android/settings/slices/SlicesDatabaseAccessor.java @@ -0,0 +1,141 @@ +/* + * 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.slices; + +import static com.android.settings.slices.SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import android.content.Context; +import android.os.Binder; + +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.slices.SlicesDatabaseHelper.IndexColumns; + +import androidx.app.slice.Slice; + +/** + * Class used to map a {@link Uri} from {@link SettingsSliceProvider} to a Slice. + */ +public class SlicesDatabaseAccessor { + + public static final String[] SELECT_COLUMNS = { + IndexColumns.KEY, + IndexColumns.TITLE, + IndexColumns.SUMMARY, + IndexColumns.SCREENTITLE, + IndexColumns.ICON_RESOURCE, + IndexColumns.FRAGMENT, + IndexColumns.CONTROLLER, + }; + + Context mContext; + + public SlicesDatabaseAccessor(Context context) { + mContext = context; + } + + /** + * Query the slices database and return a {@link SliceData} object corresponding to the row + * matching the key provided by the {@param uri}. Additionally adds the {@param uri} to the + * {@link SliceData} object so the {@link Slice} can bind to the {@link Uri}. + * Used when building a {@link Slice}. + */ + public SliceData getSliceDataFromUri(Uri uri) { + String key = uri.getLastPathSegment(); + Cursor cursor = getIndexedSliceData(key); + return buildSliceData(cursor, uri); + } + + /** + * Query the slices database and return a {@link SliceData} object corresponding to the row + * matching the {@param key}. + * Used when handling the action of the {@link Slice}. + */ + public SliceData getSliceDataFromKey(String key) { + Cursor cursor = getIndexedSliceData(key); + return buildSliceData(cursor, null /* uri */); + } + + private Cursor getIndexedSliceData(String path) { + verifyIndexing(); + + final String whereClause = buildWhereClause(); + final SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(mContext); + final SQLiteDatabase database = helper.getReadableDatabase(); + final String[] selection = new String[]{path}; + + Cursor resultCursor = database.query(TABLE_SLICES_INDEX, SELECT_COLUMNS, whereClause, + selection, null /* groupBy */, null /* having */, null /* orderBy */); + + int numResults = resultCursor.getCount(); + + if (numResults == 0) { + throw new IllegalStateException("Invalid Slices key from path: " + path); + } + + if (numResults > 1) { + throw new IllegalStateException( + "Should not match more than 1 slice with path: " + path); + } + + resultCursor.moveToFirst(); + return resultCursor; + } + + private String buildWhereClause() { + return new StringBuilder(IndexColumns.KEY) + .append(" = ?") + .toString(); + } + + private SliceData buildSliceData(Cursor cursor, Uri uri) { + final String key = cursor.getString(cursor.getColumnIndex(IndexColumns.KEY)); + final String title = cursor.getString(cursor.getColumnIndex(IndexColumns.TITLE)); + final String summary = cursor.getString(cursor.getColumnIndex(IndexColumns.SUMMARY)); + final String screenTitle = cursor.getString( + cursor.getColumnIndex(IndexColumns.SCREENTITLE)); + final int iconResource = cursor.getInt(cursor.getColumnIndex(IndexColumns.ICON_RESOURCE)); + final String fragmentClassName = cursor.getString( + cursor.getColumnIndex(IndexColumns.FRAGMENT)); + final String controllerClassName = cursor.getString( + cursor.getColumnIndex(IndexColumns.CONTROLLER)); + + return new SliceData.Builder() + .setKey(key) + .setTitle(title) + .setSummary(summary) + .setScreenTitle(screenTitle) + .setIcon(iconResource) + .setFragmentName(fragmentClassName) + .setPreferenceControllerClassName(controllerClassName) + .setUri(uri) + .build(); + } + + private void verifyIndexing() { + final long uidToken = Binder.clearCallingIdentity(); + try { + FeatureFactory.getFactory( + mContext).getSlicesFeatureProvider().indexSliceData(mContext); + } finally { + Binder.restoreCallingIdentity(uidToken); + } + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesDatabaseHelper.java b/src/com/android/settings/slices/SlicesDatabaseHelper.java index 18f8cc91107..448d8f17ba5 100644 --- a/src/com/android/settings/slices/SlicesDatabaseHelper.java +++ b/src/com/android/settings/slices/SlicesDatabaseHelper.java @@ -104,7 +104,7 @@ public class SlicesDatabaseHelper extends SQLiteOpenHelper { public static synchronized SlicesDatabaseHelper getInstance(Context context) { if (sSingleton == null) { - sSingleton = new SlicesDatabaseHelper(context); + sSingleton = new SlicesDatabaseHelper(context.getApplicationContext()); } return sSingleton; } diff --git a/src/com/android/settings/slices/SlicesFeatureProvider.java b/src/com/android/settings/slices/SlicesFeatureProvider.java index cbf1b75ed1b..e5bba617e48 100644 --- a/src/com/android/settings/slices/SlicesFeatureProvider.java +++ b/src/com/android/settings/slices/SlicesFeatureProvider.java @@ -13,5 +13,15 @@ public interface SlicesFeatureProvider { SliceDataConverter getSliceDataConverter(Context context); + /** + * Asynchronous call to index the data used to build Slices. + * If the data is already indexed, the data will not change. + */ + void indexSliceDataAsync(Context context); + + /** + * Indexes the data used to build Slices. + * If the data is already indexed, the data will not change. + */ void indexSliceData(Context context); } \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java index 34ef884f641..8e5bc067150 100644 --- a/src/com/android/settings/slices/SlicesFeatureProviderImpl.java +++ b/src/com/android/settings/slices/SlicesFeatureProviderImpl.java @@ -15,7 +15,7 @@ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider { @Override public SlicesIndexer getSliceIndexer(Context context) { if (mSlicesIndexer == null) { - mSlicesIndexer = new SlicesIndexer(context.getApplicationContext()); + mSlicesIndexer = new SlicesIndexer(context); } return mSlicesIndexer; } @@ -29,9 +29,14 @@ public class SlicesFeatureProviderImpl implements SlicesFeatureProvider { } @Override - public void indexSliceData(Context context) { - // TODO (b/67996923) add indexing time log + public void indexSliceDataAsync(Context context) { SlicesIndexer indexer = getSliceIndexer(context); ThreadUtils.postOnBackgroundThread(indexer); } -} + + @Override + public void indexSliceData(Context context) { + SlicesIndexer indexer = getSliceIndexer(context); + indexer.indexSliceData(); + } +} \ No newline at end of file diff --git a/src/com/android/settings/slices/SlicesIndexer.java b/src/com/android/settings/slices/SlicesIndexer.java index 0297f3fa557..a92388aa752 100644 --- a/src/com/android/settings/slices/SlicesIndexer.java +++ b/src/com/android/settings/slices/SlicesIndexer.java @@ -20,6 +20,7 @@ import android.content.ContentValues; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.VisibleForTesting; +import android.util.Log; import com.android.settings.dashboard.DashboardFragment; @@ -36,7 +37,7 @@ import java.util.List; */ class SlicesIndexer implements Runnable { - private static final String TAG = "SlicesIndexingManager"; + private static final String TAG = "SlicesIndexer"; private Context mContext; @@ -48,18 +49,27 @@ class SlicesIndexer implements Runnable { } /** - * Synchronously takes data obtained from {@link SliceDataConverter} and indexes it into a - * SQLite database. + * Asynchronously index slice data from {@link #indexSliceData()}. */ @Override public void run() { + indexSliceData(); + } + + /** + * Synchronously takes data obtained from {@link SliceDataConverter} and indexes it into a + * SQLite database + */ + protected void indexSliceData() { if (mHelper.isSliceDataIndexed()) { + Log.d(TAG, "Slices already indexed - returning."); return; } SQLiteDatabase database = mHelper.getWritableDatabase(); try { + long startTime = System.currentTimeMillis(); database.beginTransaction(); mHelper.reconstruct(mHelper.getWritableDatabase()); @@ -67,6 +77,10 @@ class SlicesIndexer implements Runnable { insertSliceData(database, indexData); mHelper.setIndexedState(); + + // TODO (b/71503044) Log indexing time. + Log.d(TAG, + "Indexing slices database took: " + (System.currentTimeMillis() - startTime)); database.setTransactionSuccessful(); } finally { database.endTransaction(); diff --git a/tests/robotests/src/com/android/settings/slices/FakeToggleController.java b/tests/robotests/src/com/android/settings/slices/FakeToggleController.java new file mode 100644 index 00000000000..1b08e3566aa --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/FakeToggleController.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.core.TogglePreferenceController; + +public class FakeToggleController extends TogglePreferenceController { + + private String settingKey = "toggle_key"; + + private final int ON = 1; + private final int OFF = 0; + + public FakeToggleController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public boolean isChecked() { + return Settings.System.getInt(mContext.getContentResolver(), + settingKey, OFF) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.System.putInt(mContext.getContentResolver(), settingKey, + isChecked ? ON : OFF); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } +} diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java new file mode 100644 index 00000000000..2af15e2d3fb --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; + +import android.content.ContentValues; +import android.content.Context; +import android.content.ContentResolver; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.DatabaseTestUtils; +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 androidx.app.slice.Slice; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SettingsSliceProviderTest { + + private final String fakeTitle = "title"; + private final String KEY = "key"; + + private Context mContext; + private SettingsSliceProvider mProvider; + private SQLiteDatabase mDb; + + @Before + public void setUp() { + mContext = spy(RuntimeEnvironment.application); + mProvider = spy(new SettingsSliceProvider()); + mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); + SlicesDatabaseHelper.getInstance(mContext).setIndexedState(); + } + + @After + public void cleanUp() { + DatabaseTestUtils.clearDb(mContext); + } + + @Test + public void testInitialSliceReturned_emmptySlice() { + Uri uri = SettingsSliceProvider.getUri(KEY); + Slice slice = mProvider.onBindSlice(uri); + + assertThat(slice.getUri()).isEqualTo(uri); + assertThat(slice.getItems()).isEmpty(); + } + + @Test + public void testUriBuilder_returnsValidSliceUri() { + Uri uri = SettingsSliceProvider.getUri(KEY); + + assertThat(uri.getScheme()).isEqualTo(ContentResolver.SCHEME_CONTENT); + assertThat(uri.getAuthority()).isEqualTo(SettingsSliceProvider.SLICE_AUTHORITY); + assertThat(uri.getLastPathSegment()).isEqualTo(KEY); + } + + private void insertSpecialCase(String key) { + ContentValues values = new ContentValues(); + values.put(SlicesDatabaseHelper.IndexColumns.KEY, key); + values.put(SlicesDatabaseHelper.IndexColumns.TITLE, fakeTitle); + values.put(SlicesDatabaseHelper.IndexColumns.SUMMARY, "s"); + values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, "s"); + values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, 1234); + values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, "test"); + values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, "test"); + + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java new file mode 100644 index 00000000000..efd1cc5e263 --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SliceBroadcastReceiverTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.sqlite.SQLiteDatabase; + +import com.android.settings.TestConfig; +import com.android.settings.search.FakeIndexProvider; +import com.android.settings.search.SearchIndexableResources; +import com.android.settings.testutils.DatabaseTestUtils; +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 java.util.HashSet; +import java.util.Set; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SliceBroadcastReceiverTest { + + private final String fakeTitle = "title"; + private final String fakeSummary = "summary"; + private final String fakeScreenTitle = "screen_title"; + private final int fakeIcon = 1234; + private final String fakeFragmentClassName = FakeIndexProvider.class.getName(); + private final String fakeControllerName = FakeToggleController.class.getName(); + + private Context mContext; + private SQLiteDatabase mDb; + private SliceBroadcastReceiver mReceiver; + + private Set mProviderClassesCopy; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); + mReceiver = new SliceBroadcastReceiver(); + mProviderClassesCopy = new HashSet<>(SearchIndexableResources.providerValues()); + SlicesDatabaseHelper helper = SlicesDatabaseHelper.getInstance(mContext); + helper.setIndexedState(); + } + + @After + public void cleanUp() { + DatabaseTestUtils.clearDb(mContext); + SearchIndexableResources.providerValues().clear(); + SearchIndexableResources.providerValues().addAll(mProviderClassesCopy); + } + + @Test + public void testOnReceive_toggleChanged() { + String key = "key"; + SearchIndexableResources.providerValues().clear(); + insertSpecialCase(key); + // Turn on toggle setting + FakeToggleController fakeToggleController = new FakeToggleController(mContext, key); + fakeToggleController.setChecked(true); + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, key); + + assertThat(fakeToggleController.isChecked()).isTrue(); + + // Toggle setting + mReceiver.onReceive(mContext, intent); + + assertThat(fakeToggleController.isChecked()).isFalse(); + } + + @Test(expected = IllegalStateException.class) + public void testOnReceive_noExtra_illegalSatetException() { + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + mReceiver.onReceive(mContext, intent); + } + + @Test(expected = IllegalStateException.class) + public void testOnReceive_emptyKey_throwsIllegalStateException() { + Intent intent = new Intent(SettingsSliceProvider.ACTION_TOGGLE_CHANGED); + intent.putExtra(SettingsSliceProvider.EXTRA_SLICE_KEY, (String) null); + mReceiver.onReceive(mContext, intent); + } + + private void insertSpecialCase(String key) { + ContentValues values = new ContentValues(); + values.put(SlicesDatabaseHelper.IndexColumns.KEY, key); + values.put(SlicesDatabaseHelper.IndexColumns.TITLE, fakeTitle); + values.put(SlicesDatabaseHelper.IndexColumns.SUMMARY, fakeSummary); + values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, fakeScreenTitle); + values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, fakeIcon); + values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, fakeFragmentClassName); + values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, fakeControllerName); + + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java new file mode 100644 index 00000000000..106e6fe4324 --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseAccessorTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.settings.slices; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; + +import com.android.settings.TestConfig; +import com.android.settings.search.FakeIndexProvider; +import com.android.settings.testutils.DatabaseTestUtils; +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; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class SlicesDatabaseAccessorTest { + + private final String fakeTitle = "title"; + private final String fakeSummary = "summary"; + private final String fakeScreenTitle = "screen_title"; + private final int fakeIcon = 1234; + private final String fakeFragmentClassName = FakeIndexProvider.class.getName(); + private final String fakeControllerName = FakePreferenceController.class.getName(); + + private Context mContext; + private SQLiteDatabase mDb; + private SlicesDatabaseAccessor mAccessor; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mAccessor = spy(new SlicesDatabaseAccessor(mContext)); + mDb = SlicesDatabaseHelper.getInstance(mContext).getWritableDatabase(); + SlicesDatabaseHelper.getInstance(mContext).setIndexedState(); + } + + @After + public void cleanUp() { + DatabaseTestUtils.clearDb(mContext); + } + + @Test + public void testGetSliceDataFromKey_validKey_validSliceReturned() { + String key = "key"; + insertSpecialCase(key); + + SliceData data = mAccessor.getSliceDataFromKey(key); + + assertThat(data.getKey()).isEqualTo(key); + assertThat(data.getTitle()).isEqualTo(fakeTitle); + assertThat(data.getSummary()).isEqualTo(fakeSummary); + assertThat(data.getScreenTitle()).isEqualTo(fakeScreenTitle); + assertThat(data.getIconResource()).isEqualTo(fakeIcon); + assertThat(data.getFragmentClassName()).isEqualTo(fakeFragmentClassName); + assertThat(data.getUri()).isNull(); + assertThat(data.getPreferenceController()).isEqualTo(fakeControllerName); + } + + @Test(expected = IllegalStateException.class) + public void testGetSliceDataFromKey_invalidKey_errorThrown() { + String key = "key"; + + mAccessor.getSliceDataFromKey(key); + } + + @Test + public void testGetSliceFromUri_validUri_validSliceReturned() { + String key = "key"; + insertSpecialCase(key); + Uri uri = SettingsSliceProvider.getUri(key); + + SliceData data = mAccessor.getSliceDataFromUri(uri); + + assertThat(data.getKey()).isEqualTo(key); + assertThat(data.getTitle()).isEqualTo(fakeTitle); + assertThat(data.getSummary()).isEqualTo(fakeSummary); + assertThat(data.getScreenTitle()).isEqualTo(fakeScreenTitle); + assertThat(data.getIconResource()).isEqualTo(fakeIcon); + assertThat(data.getFragmentClassName()).isEqualTo(fakeFragmentClassName); + assertThat(data.getUri()).isEqualTo(uri); + assertThat(data.getPreferenceController()).isEqualTo(fakeControllerName); + } + + @Test(expected = IllegalStateException.class) + public void testGetSliceFromUri_invalidUri_errorThrown() { + Uri uri = SettingsSliceProvider.getUri("durr"); + + mAccessor.getSliceDataFromUri(uri); + } + + private void insertSpecialCase(String key) { + ContentValues values = new ContentValues(); + values.put(SlicesDatabaseHelper.IndexColumns.KEY, key); + values.put(SlicesDatabaseHelper.IndexColumns.TITLE, fakeTitle); + values.put(SlicesDatabaseHelper.IndexColumns.SUMMARY, fakeSummary); + values.put(SlicesDatabaseHelper.IndexColumns.SCREENTITLE, fakeScreenTitle); + values.put(SlicesDatabaseHelper.IndexColumns.ICON_RESOURCE, fakeIcon); + values.put(SlicesDatabaseHelper.IndexColumns.FRAGMENT, fakeFragmentClassName); + values.put(SlicesDatabaseHelper.IndexColumns.CONTROLLER, fakeControllerName); + + mDb.replaceOrThrow(SlicesDatabaseHelper.Tables.TABLE_SLICES_INDEX, null, values); + } +} diff --git a/tests/robotests/src/com/android/settings/slices/SlicesDatabaseUtilsTest.java b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseUtilsTest.java new file mode 100644 index 00000000000..f22e85ffdff --- /dev/null +++ b/tests/robotests/src/com/android/settings/slices/SlicesDatabaseUtilsTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.settings.slices; + +import static com.android.settings.TestConfig.SDK_VERSION; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.net.Uri; + +import com.android.settings.TestConfig; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import androidx.app.slice.Slice; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = SDK_VERSION) +public class SlicesDatabaseUtilsTest { + + private final String KEY = "KEY"; + private final String TITLE = "title"; + private final String SUMMARY = "summary"; + private final String SCREEN_TITLE = "screen title"; + private final String FRAGMENT_NAME = "fragment name"; + private final int ICON = 1234; // I declare a thumb war + private final Uri URI = Uri.parse("content://com.android.settings.slices/test"); + private final String PREF_CONTROLLER = FakeToggleController.class.getName(); + ; + + private Context mContext; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + } + + @Test + public void testBuildSlice_returnsMatchingSlice() { + Slice slice = SliceBuilderUtils.buildSlice(mContext, getDummyData()); + + assertThat(slice).isNotNull(); // TODO improve test for Slice content + } + + @Test + public void testGetPreferenceController_buildsMatchingController() { + BasePreferenceController controller = SliceBuilderUtils.getPreferenceController(mContext, + getDummyData()); + + assertThat(controller).isInstanceOf(FakeToggleController.class); + } + + private SliceData getDummyData() { + return new SliceData.Builder() + .setKey(KEY) + .setTitle(TITLE) + .setSummary(SUMMARY) + .setScreenTitle(SCREEN_TITLE) + .setIcon(ICON) + .setFragmentName(FRAGMENT_NAME) + .setUri(URI) + .setPreferenceControllerClassName(PREF_CONTROLLER) + .build(); + } +} From c28b46f2eaa5aa68bd0adf69085ba0991a548296 Mon Sep 17 00:00:00 2001 From: Ben Lin Date: Thu, 21 Dec 2017 11:09:39 -0800 Subject: [PATCH 11/15] Introduce boolean flags to show/hide items for Language & input. This adds seven new boolean flags: config_show_phone_language config_show_virtual_keyboard_pref config_show_physical_keyboard_pref config_show_spellcheckers_settings config_show_tts_settings_summary config_show_pointer_speed config_show_vibrate_input_devices Which when individually set to false, will hide the item from "Language & input" in System. It will also hide them from surfacing in search results. Bug: 62379555 Test: Individual controller tests are all updated. Change-Id: I9ef1c3037b0bec6ffa5a627006507f4f16e534c4 --- res/values/bools.xml | 21 +++++++ .../GameControllerPreferenceController.java | 6 ++ .../PhysicalKeyboardPreferenceController.java | 2 +- .../SpellCheckerPreferenceController.java | 2 +- .../VirtualKeyboardPreferenceController.java | 2 +- .../language/LanguageAndInputSettings.java | 44 +++++++++++--- .../PhoneLanguagePreferenceController.java | 3 +- .../language/PointerSpeedController.java | 41 +++++++++++++ .../language/TtsPreferenceController.java | 4 +- tests/robotests/res/values-mcc999/config.xml | 6 ++ ...ameControllerPreferenceControllerTest.java | 14 ++++- ...sicalKeyboardPreferenceControllerTest.java | 16 ++++- .../SpellCheckerPreferenceControllerTest.java | 19 ++++++ ...rtualKeyboardPreferenceControllerTest.java | 13 +++- ...PhoneLanguagePreferenceControllerTest.java | 22 +++++-- .../language/PointerSpeedControllerTest.java | 60 +++++++++++++++++++ .../language/TtsPreferenceControllerTest.java | 12 +++- 17 files changed, 265 insertions(+), 22 deletions(-) create mode 100644 src/com/android/settings/language/PointerSpeedController.java create mode 100644 tests/robotests/src/com/android/settings/language/PointerSpeedControllerTest.java diff --git a/res/values/bools.xml b/res/values/bools.xml index 687d5bd1ddc..c490365fe4a 100644 --- a/res/values/bools.xml +++ b/res/values/bools.xml @@ -72,6 +72,27 @@ true + + true + + + true + + + true + + + true + + + true + + + true + + + true + true diff --git a/src/com/android/settings/inputmethod/GameControllerPreferenceController.java b/src/com/android/settings/inputmethod/GameControllerPreferenceController.java index 6a0e853d454..c4e998a7a84 100644 --- a/src/com/android/settings/inputmethod/GameControllerPreferenceController.java +++ b/src/com/android/settings/inputmethod/GameControllerPreferenceController.java @@ -27,6 +27,7 @@ import android.text.TextUtils; import android.view.InputDevice; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.OnPause; @@ -73,6 +74,11 @@ public class GameControllerPreferenceController extends AbstractPreferenceContro @Override public boolean isAvailable() { + // If device explicitly wants to hide this, return early. + if (!mContext.getResources().getBoolean(R.bool.config_show_vibrate_input_devices)) { + return false; + } + final int[] devices = mIm.getInputDeviceIds(); for (int deviceId : devices) { InputDevice device = mIm.getInputDevice(deviceId); diff --git a/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java b/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java index 7b0d1cee1ac..ec0aecd996f 100644 --- a/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java +++ b/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceController.java @@ -50,7 +50,7 @@ public class PhysicalKeyboardPreferenceController extends AbstractPreferenceCont @Override public boolean isAvailable() { - return true; + return mContext.getResources().getBoolean(R.bool.config_show_physical_keyboard_pref); } @Override diff --git a/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java b/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java index 270aabad305..139688978e6 100644 --- a/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java +++ b/src/com/android/settings/inputmethod/SpellCheckerPreferenceController.java @@ -51,7 +51,7 @@ public class SpellCheckerPreferenceController extends AbstractPreferenceControll @Override public boolean isAvailable() { - return true; + return mContext.getResources().getBoolean(R.bool.config_show_spellcheckers_settings); } @Override diff --git a/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java b/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java index c3443d2938c..c4dbef1f4d4 100644 --- a/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java +++ b/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceController.java @@ -47,7 +47,7 @@ public class VirtualKeyboardPreferenceController extends AbstractPreferenceContr @Override public boolean isAvailable() { - return true; + return mContext.getResources().getBoolean(R.bool.config_show_virtual_keyboard_pref); } @Override diff --git a/src/com/android/settings/language/LanguageAndInputSettings.java b/src/com/android/settings/language/LanguageAndInputSettings.java index bf611163d3f..cb60ef544e3 100644 --- a/src/com/android/settings/language/LanguageAndInputSettings.java +++ b/src/com/android/settings/language/LanguageAndInputSettings.java @@ -21,6 +21,7 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; +import android.os.Bundle; import android.provider.SearchIndexableResource; import android.provider.Settings; import android.speech.tts.TtsEngines; @@ -40,6 +41,7 @@ import com.android.settings.inputmethod.PhysicalKeyboardPreferenceController; import com.android.settings.inputmethod.SpellCheckerPreferenceController; import com.android.settings.inputmethod.VirtualKeyboardPreferenceController; import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.widget.PreferenceCategoryController; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -51,7 +53,10 @@ public class LanguageAndInputSettings extends DashboardFragment { private static final String TAG = "LangAndInputSettings"; + private static final String KEY_KEYBOARDS_CATEGORY = "keyboards_category"; private static final String KEY_TEXT_TO_SPEECH = "tts_settings_summary"; + private static final String KEY_POINTER_AND_TTS_CATEGORY = "pointer_and_tts_category"; + private static final String KEY_GAME_CONTROLLER_CATEGORY = "game_controller_settings_category"; private static final String KEY_PHYSICAL_KEYBOARD = "physical_keyboard_pref"; @Override @@ -92,20 +97,45 @@ public class LanguageAndInputSettings extends DashboardFragment { final List controllers = new ArrayList<>(); // Language controllers.add(new PhoneLanguagePreferenceController(context)); - controllers.add(new SpellCheckerPreferenceController(context)); - controllers.add(new UserDictionaryPreferenceController(context)); - controllers.add(new TtsPreferenceController(context, new TtsEngines(context))); + // Input - controllers.add(new VirtualKeyboardPreferenceController(context)); - controllers.add(new PhysicalKeyboardPreferenceController(context, lifecycle)); + final VirtualKeyboardPreferenceController virtualKeyboardPreferenceController = + new VirtualKeyboardPreferenceController(context); + final PhysicalKeyboardPreferenceController physicalKeyboardPreferenceController = + new PhysicalKeyboardPreferenceController(context, lifecycle); + controllers.add(virtualKeyboardPreferenceController); + controllers.add(physicalKeyboardPreferenceController); + controllers.add(new PreferenceCategoryController(context, + KEY_KEYBOARDS_CATEGORY, + Arrays.asList(virtualKeyboardPreferenceController, + physicalKeyboardPreferenceController))); + + // Pointer and Tts + final TtsPreferenceController ttsPreferenceController = + new TtsPreferenceController(context, new TtsEngines(context)); + controllers.add(ttsPreferenceController); + final PointerSpeedController pointerController = new PointerSpeedController(context); + controllers.add(pointerController); + controllers.add(new PreferenceCategoryController(context, + KEY_POINTER_AND_TTS_CATEGORY, + Arrays.asList(pointerController, ttsPreferenceController))); + + // Input Assistance + controllers.add(new SpellCheckerPreferenceController(context)); + controllers.add(new DefaultAutofillPreferenceController(context)); + controllers.add(new UserDictionaryPreferenceController(context)); + + // Game Controller final GameControllerPreferenceController gameControllerPreferenceController = new GameControllerPreferenceController(context); if (lifecycle != null) { lifecycle.addObserver(gameControllerPreferenceController); } - controllers.add(gameControllerPreferenceController); - controllers.add(new DefaultAutofillPreferenceController(context)); + controllers.add(new PreferenceCategoryController(context, + KEY_GAME_CONTROLLER_CATEGORY, + Arrays.asList(gameControllerPreferenceController))); + return controllers; } diff --git a/src/com/android/settings/language/PhoneLanguagePreferenceController.java b/src/com/android/settings/language/PhoneLanguagePreferenceController.java index 49f7f214618..1fd7db7a46b 100644 --- a/src/com/android/settings/language/PhoneLanguagePreferenceController.java +++ b/src/com/android/settings/language/PhoneLanguagePreferenceController.java @@ -41,7 +41,8 @@ public class PhoneLanguagePreferenceController extends AbstractPreferenceControl @Override public boolean isAvailable() { - return mContext.getAssets().getLocales().length > 1; + return mContext.getResources().getBoolean(R.bool.config_show_phone_language) + && mContext.getAssets().getLocales().length > 1; } @Override diff --git a/src/com/android/settings/language/PointerSpeedController.java b/src/com/android/settings/language/PointerSpeedController.java new file mode 100644 index 00000000000..857751cdfcf --- /dev/null +++ b/src/com/android/settings/language/PointerSpeedController.java @@ -0,0 +1,41 @@ +/* + * 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.language; + +import android.content.Context; + +import com.android.settings.core.BasePreferenceController; +import com.android.settings.R; + +import android.support.annotation.VisibleForTesting; + + +public class PointerSpeedController extends BasePreferenceController { + + @VisibleForTesting static final String KEY_POINTER_SPEED = "pointer_speed"; + + public PointerSpeedController(Context context) { + super(context, KEY_POINTER_SPEED); + } + + @AvailabilityStatus + public int getAvailabilityStatus() { + return mContext.getResources().getBoolean(R.bool.config_show_pointer_speed) + ? AVAILABLE + : DISABLED_UNSUPPORTED; + } +} diff --git a/src/com/android/settings/language/TtsPreferenceController.java b/src/com/android/settings/language/TtsPreferenceController.java index ef26d5ffb18..f19047b5b88 100644 --- a/src/com/android/settings/language/TtsPreferenceController.java +++ b/src/com/android/settings/language/TtsPreferenceController.java @@ -20,6 +20,7 @@ import android.content.Context; import android.speech.tts.TtsEngines; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; public class TtsPreferenceController extends AbstractPreferenceController @@ -37,7 +38,8 @@ public class TtsPreferenceController extends AbstractPreferenceController @Override public boolean isAvailable() { - return !mTtsEngines.getEngines().isEmpty(); + return !mTtsEngines.getEngines().isEmpty() && + mContext.getResources().getBoolean(R.bool.config_show_tts_settings_summary); } @Override diff --git a/tests/robotests/res/values-mcc999/config.xml b/tests/robotests/res/values-mcc999/config.xml index e10fee1e080..7bb80e888c3 100644 --- a/tests/robotests/res/values-mcc999/config.xml +++ b/tests/robotests/res/values-mcc999/config.xml @@ -31,4 +31,10 @@ false false false + false + false + false + false + false + false diff --git a/tests/robotests/src/com/android/settings/inputmethod/GameControllerPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/GameControllerPreferenceControllerTest.java index 2716fc38554..220aa6d7621 100644 --- a/tests/robotests/src/com/android/settings/inputmethod/GameControllerPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/inputmethod/GameControllerPreferenceControllerTest.java @@ -18,6 +18,7 @@ package com.android.settings.inputmethod; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,6 +35,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.ArrayList; @@ -44,18 +46,18 @@ import java.util.List; @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class GameControllerPreferenceControllerTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Context mContext; @Mock private InputManager mInputManager; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private InputDevice mInputDevice; + private Context mContext; private GameControllerPreferenceController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); when(mContext.getSystemService(Context.INPUT_SERVICE)).thenReturn(mInputManager); mController = new GameControllerPreferenceController(mContext); } @@ -110,6 +112,14 @@ public class GameControllerPreferenceControllerTest { assertThat(mController.isAvailable()).isFalse(); } + @Test + @Config(qualifiers = "mcc999") + public void testIsAvailable_ifDisabled_shouldReturnFalse() { + mController = new GameControllerPreferenceController(mContext); + + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void updateNonIndexableKeys_shouldIncludeCategoryAndPrefKeys() { when(mInputManager.getInputDeviceIds()).thenReturn(new int[]{}); diff --git a/tests/robotests/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceControllerTest.java index 8f9b2c5bdb0..356cfb4eb27 100644 --- a/tests/robotests/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/inputmethod/PhysicalKeyboardPreferenceControllerTest.java @@ -18,6 +18,7 @@ package com.android.settings.inputmethod; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -37,6 +38,7 @@ 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) @@ -65,10 +67,22 @@ public class PhysicalKeyboardPreferenceControllerTest { } @Test - public void shouldAlwaysBeAvailable() { + public void testPhysicalKeyboard_byDefault_shouldBeShown() { + final Context context = spy(RuntimeEnvironment.application.getApplicationContext()); + mController = new PhysicalKeyboardPreferenceController(context, null); + assertThat(mController.isAvailable()).isTrue(); } + @Test + @Config(qualifiers = "mcc999") + public void testPhysicalKeyboard_ifDisabled_shouldNotBeShown() { + final Context context = spy(RuntimeEnvironment.application.getApplicationContext()); + mController = new PhysicalKeyboardPreferenceController(context, null); + + assertThat(mController.isAvailable()).isFalse(); + } + @Test @Config(shadows = { ShadowInputDevice.class, diff --git a/tests/robotests/src/com/android/settings/inputmethod/SpellCheckerPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/SpellCheckerPreferenceControllerTest.java index 4457cc0339d..c41a03ba15c 100644 --- a/tests/robotests/src/com/android/settings/inputmethod/SpellCheckerPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/inputmethod/SpellCheckerPreferenceControllerTest.java @@ -16,7 +16,10 @@ package com.android.settings.inputmethod; +import static org.mockito.Mockito.spy; + import android.content.Context; +import android.content.res.Resources; import android.support.v7.preference.Preference; import android.view.textservice.SpellCheckerInfo; import android.view.textservice.TextServicesManager; @@ -46,6 +49,9 @@ public class SpellCheckerPreferenceControllerTest { private Context mContext; @Mock private TextServicesManager mTextServicesManager; + @Mock + private Resources mResources; + private Context mAppContext; private Preference mPreference; private SpellCheckerPreferenceController mController; @@ -56,10 +62,23 @@ public class SpellCheckerPreferenceControllerTest { mAppContext = RuntimeEnvironment.application; when(mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)) .thenReturn(mTextServicesManager); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getBoolean(R.bool.config_show_spellcheckers_settings)).thenReturn(true); mPreference = new Preference(mAppContext); mController = new SpellCheckerPreferenceController(mContext); } + @Test + public void testSpellChecker_byDefault_shouldBeShown() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + public void testSpellChecker_ifDisabled_shouldNotBeShown() { + when(mResources.getBoolean(R.bool.config_show_spellcheckers_settings)).thenReturn(false); + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void updateState_NoSpellerChecker_shouldSetSummaryToDefault() { when(mTextServicesManager.isSpellCheckerEnabled()).thenReturn(true); diff --git a/tests/robotests/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceControllerTest.java index fe2b0df3865..c0787251d46 100644 --- a/tests/robotests/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/inputmethod/VirtualKeyboardPreferenceControllerTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.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; @@ -75,10 +76,20 @@ public class VirtualKeyboardPreferenceControllerTest { } @Test - public void shouldAlwaysBeAvailable() { + public void testVirtualKeyboard_byDefault_shouldBeShown() { + final Context context = spy(RuntimeEnvironment.application.getApplicationContext()); + mController = new VirtualKeyboardPreferenceController(context); assertThat(mController.isAvailable()).isTrue(); } + @Test + @Config(qualifiers = "mcc999") + public void testVirtualKeyboard_ifDisabled_shouldNotBeShown() { + final Context context = spy(RuntimeEnvironment.application.getApplicationContext()); + mController = new VirtualKeyboardPreferenceController(context); + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void updateState_noEnabledIMEs_setEmptySummary() { mController.updateState(mPreference); diff --git a/tests/robotests/src/com/android/settings/language/PhoneLanguagePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/language/PhoneLanguagePreferenceControllerTest.java index 6984e6a5bc1..90a4a129ba6 100644 --- a/tests/robotests/src/com/android/settings/language/PhoneLanguagePreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/language/PhoneLanguagePreferenceControllerTest.java @@ -17,10 +17,13 @@ package com.android.settings.language; import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.res.AssetManager; import android.support.v7.preference.Preference; import com.android.settings.TestConfig; @@ -33,6 +36,7 @@ import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import java.util.ArrayList; @@ -42,34 +46,44 @@ import java.util.List; @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class PhoneLanguagePreferenceControllerTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Context mContext; @Mock private Preference mPreference; + @Mock + private AssetManager mAssets; + + private Context mContext; private FakeFeatureFactory mFeatureFactory; private PhoneLanguagePreferenceController mController; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + when(mContext.getAssets()).thenReturn(mAssets); mFeatureFactory = FakeFeatureFactory.setupForTest(); mController = new PhoneLanguagePreferenceController(mContext); } @Test public void testIsAvailable_hasMultipleLocales_shouldReturnTrue() { - when(mContext.getAssets().getLocales()).thenReturn(new String[] {"en", "de"}); + when(mAssets.getLocales()).thenReturn(new String[] {"en", "de"}); assertThat(mController.isAvailable()).isTrue(); } @Test public void testIsAvailable_hasSingleLocales_shouldReturnFalse() { - when(mContext.getAssets().getLocales()).thenReturn(new String[] {"en"}); + when(mAssets.getLocales()).thenReturn(new String[] {"en"}); assertThat(mController.isAvailable()).isFalse(); } + @Test + @Config(qualifiers = "mcc999") + public void testIsAvailable_ifDisabled_shouldReturnFalse() { + assertThat(mController.isAvailable()).isFalse(); + } + @Test public void testUpdateState_shouldUpdateSummary() { final String testSummary = "test"; diff --git a/tests/robotests/src/com/android/settings/language/PointerSpeedControllerTest.java b/tests/robotests/src/com/android/settings/language/PointerSpeedControllerTest.java new file mode 100644 index 00000000000..32217dfff6d --- /dev/null +++ b/tests/robotests/src/com/android/settings/language/PointerSpeedControllerTest.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.language; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.spy; + +import android.content.Context; + +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; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class PointerSpeedControllerTest { + + private Context mContext; + private PointerSpeedController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application.getApplicationContext()); + mController = new PointerSpeedController(mContext); + } + + @Test + public void testDeviceAdministrators_byDefault_shouldBeShown() { + assertThat(mController.isAvailable()).isTrue(); + } + + @Test + @Config(qualifiers = "mcc999") + public void testDeviceAdministrators_ifDisabled_shouldNotBeShown() { + assertThat(mController.isAvailable()).isFalse(); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/language/TtsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/language/TtsPreferenceControllerTest.java index 15067c9f548..18cb12a5c71 100644 --- a/tests/robotests/src/com/android/settings/language/TtsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/language/TtsPreferenceControllerTest.java @@ -18,6 +18,7 @@ package com.android.settings.language; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.content.Context; @@ -45,19 +46,19 @@ import java.util.List; @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class TtsPreferenceControllerTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private Context mContext; @Mock private TtsEngines mTtsEngines; @Mock private PreferenceScreen mScreen; + private Context mContext; private TtsPreferenceController mController; private Preference mPreference; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); mController = new TtsPreferenceController(mContext, mTtsEngines); mPreference = new Preference(RuntimeEnvironment.application); @@ -89,4 +90,11 @@ public class TtsPreferenceControllerTest { assertThat(mPreference.isVisible()).isTrue(); } + + @Test + @Config(qualifiers = "mcc999") + public void testIsAvailable_ifDisabled_shouldReturnFalse() { + + assertThat(mController.isAvailable()).isFalse(); + } } From 6f6a7c6412768c191bf44dea96c54415087ac3eb Mon Sep 17 00:00:00 2001 From: Ajay Nadathur Date: Wed, 10 Jan 2018 13:03:44 -0800 Subject: [PATCH 12/15] Fix NPE in SetupChooseLockpattern screen bug: 71807569 Test: Manually tested and verified no longer crash Change-Id: I9b6a20377f2cf7ffb9be12de7bfa59f8b1063625 --- res/layout-land/choose_lock_pattern.xml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/res/layout-land/choose_lock_pattern.xml b/res/layout-land/choose_lock_pattern.xml index b54a245feb9..cba6173a77f 100644 --- a/res/layout-land/choose_lock_pattern.xml +++ b/res/layout-land/choose_lock_pattern.xml @@ -84,10 +84,20 @@ android:id="@+id/headerText" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="16dp" + android:layout_marginTop="10dp" android:gravity="?attr/suwGlifHeaderGravity" android:textSize="18sp"/> +