From fef9b97498b66d3e069feb2836701118bc41a63f Mon Sep 17 00:00:00 2001 From: Arc Wang Date: Mon, 21 Nov 2022 15:04:24 +0800 Subject: [PATCH 1/3] Settings 2-pane deep link vulnerabilities Settings app must not start an deep link Activity if 1. The deep link Activity is not exported. or 2. Calling package does not have the permission to start the deep link Activity. Bug: 250589026 Test: make RunSettingsRoboTests ROBOTEST_FILTER=SettingsHomepageActivityTest Change-Id: I9a3bddfa5d9d1d2e924dd6f3e5e07dca6c11664f Merged-In: I9a3bddfa5d9d1d2e924dd6f3e5e07dca6c11664f --- .../homepage/SettingsHomepageActivity.java | 36 +++++++++++++++++++ .../SettingsHomepageActivityTest.java | 35 ++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/com/android/settings/homepage/SettingsHomepageActivity.java b/src/com/android/settings/homepage/SettingsHomepageActivity.java index 183a2fbf5f5..4726059455e 100644 --- a/src/com/android/settings/homepage/SettingsHomepageActivity.java +++ b/src/com/android/settings/homepage/SettingsHomepageActivity.java @@ -25,6 +25,8 @@ import android.app.ActivityManager; import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.os.Bundle; import android.text.TextUtils; @@ -38,6 +40,7 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.Toolbar; +import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentManager; @@ -55,6 +58,7 @@ import com.android.settings.core.CategoryMixin; import com.android.settings.core.FeatureFlags; import com.android.settings.homepage.contextualcards.ContextualCardsFragment; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.password.PasswordUtils; import com.android.settingslib.Utils; import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; @@ -351,6 +355,32 @@ public class SettingsHomepageActivity extends FragmentActivity implements finish(); return; } + + if (!TextUtils.equals(PasswordUtils.getCallingAppPackageName(getActivityToken()), + getPackageName())) { + ActivityInfo targetActivityInfo = null; + try { + targetActivityInfo = getPackageManager().getActivityInfo(targetComponentName, + /* flags= */ 0); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Failed to get target ActivityInfo: " + e); + finish(); + return; + } + + if (!targetActivityInfo.exported) { + Log.e(TAG, "Must not launch an unexported Actvity for deep link"); + finish(); + return; + } + + if (!isCallingAppPermitted(targetActivityInfo.permission)) { + Log.e(TAG, "Calling app must have the permission of deep link Activity"); + finish(); + return; + } + } + targetIntent.setComponent(targetComponentName); // To prevent launchDeepLinkIntentToRight again for configuration change. @@ -386,6 +416,12 @@ public class SettingsHomepageActivity extends FragmentActivity implements startActivity(targetIntent); } + @VisibleForTesting + boolean isCallingAppPermitted(String permission) { + return TextUtils.isEmpty(permission) || PasswordUtils.isCallingAppPermitted( + this, getActivityToken(), permission); + } + private String getHighlightMenuKey() { final Intent intent = getIntent(); if (intent != null && TextUtils.equals(intent.getAction(), diff --git a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java index 4d203a8a6b0..4de8b005c3c 100644 --- a/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java +++ b/tests/robotests/src/com/android/settings/homepage/SettingsHomepageActivityTest.java @@ -20,6 +20,8 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -37,9 +39,11 @@ import androidx.fragment.app.Fragment; import com.android.settings.R; import com.android.settings.dashboard.suggestions.SuggestionFeatureProviderImpl; import com.android.settings.homepage.contextualcards.slices.BatteryFixSliceTest; +import com.android.settings.testutils.shadow.ShadowPasswordUtils; import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settingslib.core.lifecycle.HideNonSystemOverlayMixin; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -66,6 +70,11 @@ public class SettingsHomepageActivityTest { MockitoAnnotations.initMocks(this); } + @After + public void tearDown() { + ShadowPasswordUtils.reset(); + } + @Test public void launch_shouldHaveAnimationForIaFragment() { final SettingsHomepageActivity activity = Robolectric.buildActivity( @@ -195,6 +204,32 @@ public class SettingsHomepageActivityTest { & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS).isEqualTo(0); } + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void isCallingAppPermitted_emptyPermission_returnTrue() { + SettingsHomepageActivity homepageActivity = spy(new SettingsHomepageActivity()); + + assertTrue(homepageActivity.isCallingAppPermitted("")); + } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void isCallingAppPermitted_noGrantedPermission_returnFalse() { + SettingsHomepageActivity homepageActivity = spy(new SettingsHomepageActivity()); + + assertFalse(homepageActivity.isCallingAppPermitted("android.permission.TEST")); + } + + @Test + @Config(shadows = {ShadowPasswordUtils.class}) + public void isCallingAppPermitted_grantedPermission_returnTrue() { + SettingsHomepageActivity homepageActivity = spy(new SettingsHomepageActivity()); + String permission = "android.permission.TEST"; + ShadowPasswordUtils.addGrantedPermission(permission); + + assertTrue(homepageActivity.isCallingAppPermitted(permission)); + } + @Implements(SuggestionFeatureProviderImpl.class) public static class ShadowSuggestionFeatureProviderImpl { From 1d72ff642c9d88eaafe8e3b819c7ee11a9cd77e0 Mon Sep 17 00:00:00 2001 From: Zhenwei Chen Date: Tue, 22 Nov 2022 09:06:25 +0800 Subject: [PATCH 2/3] Add dock defender battery tips 1. Remove the dock defender v1 code 2. Add dock defender battery tips and update corresponding list item string Bug: 260687359 Test: Unit test passed and manual test on device Merged-In: Ib6c09df056744142f42f5e2a13252b58e54c7534 Change-Id: Ib6c09df056744142f42f5e2a13252b58e54c7534 Signed-off-by: Zhenwei Chen (cherry picked from commit 8d11d9ceea0148abe678ae17ca31c974a42699c8) --- .../ic_battery_status_protected_24dp.xml | 30 +++ .../ic_battery_status_protected_24dp.xml | 30 +++ res/values/strings.xml | 14 +- .../fuelgauge/BatteryBroadcastReceiver.java | 3 + .../settings/fuelgauge/BatteryInfo.java | 42 ++-- .../settings/fuelgauge/BatteryUtils.java | 36 ++++ .../fuelgauge/PowerUsageFeatureProvider.java | 2 +- .../PowerUsageFeatureProviderImpl.java | 2 +- .../batterytip/BatteryTipLoader.java | 2 + .../detectors/BatteryDefenderDetector.java | 9 +- .../detectors/DockDefenderDetector.java | 48 +++++ .../batterytip/tips/BatteryDefenderTip.java | 21 +- .../fuelgauge/batterytip/tips/BatteryTip.java | 17 +- .../batterytip/tips/DockDefenderTip.java | 192 +++++++++++++++++ .../BatteryBroadcastReceiverTest.java | 10 + .../settings/fuelgauge/BatteryInfoTest.java | 63 ++++++ .../PowerUsageFeatureProviderImplTest.java | 9 +- .../batterytip/BatteryTipLoaderTest.java | 1 + .../BatteryDefenderDetectorTest.java | 36 +++- .../detectors/DockDefenderDetectorTest.java | 142 +++++++++++++ .../tips/BatteryDefenderTipTest.java | 10 +- .../batterytip/tips/DockDefenderTipTest.java | 195 ++++++++++++++++++ .../settings/testutils/BatteryTestUtils.java | 2 +- 23 files changed, 853 insertions(+), 63 deletions(-) create mode 100644 res/drawable-night/ic_battery_status_protected_24dp.xml create mode 100644 res/drawable/ic_battery_status_protected_24dp.xml create mode 100644 src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetector.java create mode 100644 src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTip.java create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetectorTest.java create mode 100644 tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTipTest.java diff --git a/res/drawable-night/ic_battery_status_protected_24dp.xml b/res/drawable-night/ic_battery_status_protected_24dp.xml new file mode 100644 index 00000000000..23386cb8048 --- /dev/null +++ b/res/drawable-night/ic_battery_status_protected_24dp.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_battery_status_protected_24dp.xml b/res/drawable/ic_battery_status_protected_24dp.xml new file mode 100644 index 00000000000..8841710e43d --- /dev/null +++ b/res/drawable/ic_battery_status_protected_24dp.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 6352c2b46e0..3d8f2f414b3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6416,8 +6416,18 @@ Charging is paused Protecting battery to extend battery lifespan - - %1$s + + Charging to %1$s to protect the battery + + When your tablet is docked, charging will be paused at %1$s to extend battery lifespan + + Charging paused to protect battery + + When your tablet is docked, charging is paused at %1$s to extend battery lifespan + + Charging to full + + To protect your battery, charging will be paused at %1$s the next time your tablet is docked Learn more about charging is paused diff --git a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java index 558e0bfb391..665be1f0f2c 100644 --- a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java +++ b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java @@ -98,6 +98,7 @@ public class BatteryBroadcastReceiver extends BroadcastReceiver { final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED); + intentFilter.addAction(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION); final Intent intent = mContext.registerReceiver(this, intentFilter); updateBatteryStatus(intent, true /* forceUpdate */); @@ -132,6 +133,8 @@ public class BatteryBroadcastReceiver extends BroadcastReceiver { mBatteryHealth = batteryHealth; } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) { mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER); + } else if (BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION.equals(intent.getAction())) { + mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS); } } } diff --git a/src/com/android/settings/fuelgauge/BatteryInfo.java b/src/com/android/settings/fuelgauge/BatteryInfo.java index b1e4c34e6ea..52d6d58061c 100644 --- a/src/com/android/settings/fuelgauge/BatteryInfo.java +++ b/src/com/android/settings/fuelgauge/BatteryInfo.java @@ -42,6 +42,8 @@ import com.android.settingslib.fuelgauge.EstimateKt; import com.android.settingslib.utils.PowerUtil; import com.android.settingslib.utils.StringUtil; +import java.text.NumberFormat; + public class BatteryInfo { private static final String TAG = "BatteryInfo"; @@ -49,6 +51,7 @@ public class BatteryInfo { public CharSequence remainingLabel; public int batteryLevel; public int batteryStatus; + public int pluggedStatus; public boolean discharging = true; public boolean isOverheated; public long remainingTimeUs = 0; @@ -253,7 +256,8 @@ public class BatteryInfo { info.mBatteryUsageStats = batteryUsageStats; info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast); info.batteryPercentString = Utils.formatPercentage(info.batteryLevel); - info.mCharging = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0; + info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + info.mCharging = info.pluggedStatus != 0; info.averageTimeToDischarge = estimate.getAverageDischargeTime(); info.isOverheated = batteryBroadcast.getIntExtra( BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN) @@ -280,25 +284,37 @@ public class BatteryInfo { BatteryManager.BATTERY_STATUS_UNKNOWN); info.discharging = false; info.suggestionLabel = null; - if (info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL) { + int dockDefenderMode = BatteryUtils.getCurrentDockDefenderMode(context, info); + if ((info.isOverheated && status != BatteryManager.BATTERY_STATUS_FULL + && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED) + || dockDefenderMode == BatteryUtils.DockDefenderMode.ACTIVE) { + // Battery defender active, battery charging paused info.remainingLabel = null; int chargingLimitedResId = R.string.power_charging_limited; - info.chargeLabel = - context.getString(chargingLimitedResId, info.batteryPercentString); - } else if (chargeTimeMs > 0 && status != BatteryManager.BATTERY_STATUS_FULL) { + info.chargeLabel = context.getString(chargingLimitedResId, info.batteryPercentString); + } else if ((chargeTimeMs > 0 && status != BatteryManager.BATTERY_STATUS_FULL + && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED) + || dockDefenderMode == BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED) { + // Battery is charging to full info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs); - final CharSequence timeString = StringUtil.formatElapsedTime( - context, - PowerUtil.convertUsToMs(info.remainingTimeUs), - false /* withSeconds */, + final CharSequence timeString = StringUtil.formatElapsedTime(context, + (double) PowerUtil.convertUsToMs(info.remainingTimeUs), false /* withSeconds */, true /* collapseTimeUnit */); int resId = R.string.power_charging_duration; - info.remainingLabel = context.getString( - R.string.power_remaining_charging_duration_only, timeString); + info.remainingLabel = context.getString(R.string.power_remaining_charging_duration_only, + timeString); info.chargeLabel = context.getString(resId, info.batteryPercentString, timeString); + } else if (dockDefenderMode == BatteryUtils.DockDefenderMode.FUTURE_BYPASS) { + // Dock defender will be triggered in the future, charging will be paused at 90%. + final int extraValue = context.getResources().getInteger( + R.integer.config_battery_extra_tip_value); + final String extraPercentage = NumberFormat.getPercentInstance().format( + extraValue * 0.01f); + info.chargeLabel = context.getString(R.string.power_charging_future_paused, + info.batteryPercentString, extraPercentage); } else { - final String chargeStatusLabel = - Utils.getBatteryStatus(context, batteryBroadcast, compactStatus); + final String chargeStatusLabel = Utils.getBatteryStatus(context, batteryBroadcast, + compactStatus); info.remainingLabel = null; info.chargeLabel = info.batteryLevel == 100 ? info.batteryPercentString : resources.getString(R.string.power_charging, info.batteryPercentString, diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java index a6c48a48d6b..e9b72d0f51a 100644 --- a/src/com/android/settings/fuelgauge/BatteryUtils.java +++ b/src/com/android/settings/fuelgauge/BatteryUtils.java @@ -24,6 +24,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.BatteryConsumer; +import android.os.BatteryManager; import android.os.BatteryStats; import android.os.BatteryStatsManager; import android.os.BatteryUsageStats; @@ -33,6 +34,7 @@ import android.os.Process; import android.os.SystemClock; import android.os.UidBatteryConsumer; import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; import androidx.annotation.IntDef; @@ -72,6 +74,11 @@ public class BatteryUtils { /** Special UID for aggregated other users. */ public static final long UID_OTHER_USERS = Long.MIN_VALUE; + /** Flag to check if the dock defender mode has been temporarily bypassed */ + public static final String SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS = "dock_defender_bypass"; + + public static final String BYPASS_DOCK_DEFENDER_ACTION = "battery.dock.defender.bypass"; + @Retention(RetentionPolicy.SOURCE) @IntDef({StatusType.SCREEN_USAGE, StatusType.FOREGROUND, @@ -85,6 +92,18 @@ public class BatteryUtils { int ALL = 3; } + @Retention(RetentionPolicy.SOURCE) + @IntDef({DockDefenderMode.FUTURE_BYPASS, + DockDefenderMode.ACTIVE, + DockDefenderMode.TEMPORARILY_BYPASSED, + DockDefenderMode.DISABLED}) + public @interface DockDefenderMode { + int FUTURE_BYPASS = 0; + int ACTIVE = 1; + int TEMPORARILY_BYPASSED = 2; + int DISABLED = 3; + } + private static final String TAG = "BatteryUtils"; private static BatteryUtils sInstance; @@ -570,4 +589,21 @@ public class BatteryUtils { return -1L; } + + /** Gets the current dock defender mode */ + public static int getCurrentDockDefenderMode(Context context, BatteryInfo batteryInfo) { + if (batteryInfo.pluggedStatus == BatteryManager.BATTERY_PLUGGED_DOCK) { + if (Settings.Global.getInt(context.getContentResolver(), + SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 0) == 1) { + return DockDefenderMode.TEMPORARILY_BYPASSED; + } else if (batteryInfo.isOverheated && FeatureFactory.getFactory(context) + .getPowerUsageFeatureProvider(context) + .isExtraDefend()) { + return DockDefenderMode.ACTIVE; + } else if (!batteryInfo.isOverheated) { + return DockDefenderMode.FUTURE_BYPASS; + } + } + return DockDefenderMode.DISABLED; + } } diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java index 94a93b83be0..9b6f50f17dc 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java @@ -147,7 +147,7 @@ public interface PowerUsageFeatureProvider { /** * Gets a intent for one time bypass charge limited to resume charging. */ - Intent getResumeChargeIntent(); + Intent getResumeChargeIntent(boolean isDockDefender); /** * Returns battery history data with corresponding timestamp key. diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java index 0adfc9d1537..cc802e8d30a 100644 --- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java +++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java @@ -156,7 +156,7 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider } @Override - public Intent getResumeChargeIntent() { + public Intent getResumeChargeIntent(boolean isDockDefender) { return null; } diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java index 95145ba8216..7bdc5d5686a 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java @@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.BatteryUtils; import com.android.settings.fuelgauge.batterytip.detectors.BatteryDefenderDetector; +import com.android.settings.fuelgauge.batterytip.detectors.DockDefenderDetector; import com.android.settings.fuelgauge.batterytip.detectors.EarlyWarningDetector; import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector; import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector; @@ -74,6 +75,7 @@ public class BatteryTipLoader extends AsyncLoaderCompat> { tips.add(new EarlyWarningDetector(policy, context).detect()); tips.add(new BatteryDefenderDetector( batteryInfo, context.getApplicationContext()).detect()); + tips.add(new DockDefenderDetector(batteryInfo, context.getApplicationContext()).detect()); Collections.sort(tips); return tips; } diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java index 87d4a0b44a5..08df2e494f6 100644 --- a/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetector.java @@ -37,11 +37,10 @@ public class BatteryDefenderDetector implements BatteryTipDetector { @Override public BatteryTip detect() { - if (mBatteryInfo.isOverheated) { - final boolean extraDefend = FeatureFactory.getFactory(mContext) - .getPowerUsageFeatureProvider(mContext) - .isExtraDefend(); - return new BatteryDefenderTip(BatteryTip.StateType.NEW, extraDefend); + if (mBatteryInfo.isOverheated && !FeatureFactory.getFactory(mContext) + .getPowerUsageFeatureProvider(mContext) + .isExtraDefend()) { + return new BatteryDefenderTip(BatteryTip.StateType.NEW); } return new BatteryDefenderTip(BatteryTip.StateType.INVISIBLE); } diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetector.java new file mode 100644 index 00000000000..8a839d392d8 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetector.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 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 com.android.settings.fuelgauge.BatteryInfo; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.fuelgauge.batterytip.tips.DockDefenderTip; + +/** + * Detect whether the dock defender mode is enabled. + */ +public class DockDefenderDetector implements BatteryTipDetector { + private final BatteryInfo mBatteryInfo; + private final Context mContext; + + public DockDefenderDetector(BatteryInfo batteryInfo, Context context) { + mBatteryInfo = batteryInfo; + mContext = context; + } + + @Override + public BatteryTip detect() { + int mode = BatteryUtils.getCurrentDockDefenderMode(mContext, mBatteryInfo); + return new DockDefenderTip( + mode != BatteryUtils.DockDefenderMode.DISABLED + ? BatteryTip.StateType.NEW + : BatteryTip.StateType.INVISIBLE, + mode); + } + +} diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java index 2fb56500834..1ccc29c18e3 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTip.java @@ -32,24 +32,15 @@ import com.android.settings.widget.CardPreference; import com.android.settingslib.HelpUtils; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; -import java.text.NumberFormat; - /** * Tip to show current battery is overheated */ public class BatteryDefenderTip extends BatteryTip { private static final String TAG = "BatteryDefenderTip"; - private boolean mExtraDefend = false; public BatteryDefenderTip(@StateType int state) { - this(state, false); - } - - public BatteryDefenderTip(@StateType int state, boolean extraDefend) { - super(TipType.BATTERY_DEFENDER, state, true /* showDialog */); - mExtraDefend = extraDefend; - mShowDialog = false; + super(TipType.BATTERY_DEFENDER, state, false /* showDialog */); } private BatteryDefenderTip(Parcel in) { @@ -63,14 +54,6 @@ public class BatteryDefenderTip extends BatteryTip { @Override public CharSequence getSummary(Context context) { - if (mExtraDefend) { - final int extraValue = context.getResources() - .getInteger(R.integer.config_battery_extra_tip_value); - final String extraPercentage = NumberFormat.getPercentInstance() - .format(extraValue * 0.01f); - return context.getString( - R.string.battery_tip_limited_temporarily_extra_summary, extraPercentage); - } return context.getString(R.string.battery_tip_limited_temporarily_summary); } @@ -131,7 +114,7 @@ public class BatteryDefenderTip extends BatteryTip { final Intent intent = FeatureFactory.getFactory(context) .getPowerUsageFeatureProvider(context) - .getResumeChargeIntent(); + .getResumeChargeIntent(false); if (intent != null) { context.sendBroadcast(intent); } diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java index 5aee0291e4d..fcf5e09ba1b 100644 --- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java +++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java @@ -58,7 +58,8 @@ public abstract class BatteryTip implements Comparable, Parcelable { TipType.REDUCED_BATTERY, TipType.LOW_BATTERY, TipType.REMOVE_APP_RESTRICTION, - TipType.BATTERY_DEFENDER}) + TipType.BATTERY_DEFENDER, + TipType.DOCK_DEFENDER}) public @interface TipType { int SMART_BATTERY_MANAGER = 0; int APP_RESTRICTION = 1; @@ -69,6 +70,7 @@ public abstract class BatteryTip implements Comparable, Parcelable { int SUMMARY = 6; int REMOVE_APP_RESTRICTION = 7; int BATTERY_DEFENDER = 8; + int DOCK_DEFENDER = 9; } @VisibleForTesting @@ -78,12 +80,13 @@ public abstract class BatteryTip implements Comparable, Parcelable { TIP_ORDER.append(TipType.BATTERY_SAVER, 0); TIP_ORDER.append(TipType.LOW_BATTERY, 1); TIP_ORDER.append(TipType.BATTERY_DEFENDER, 2); - TIP_ORDER.append(TipType.APP_RESTRICTION, 3); - TIP_ORDER.append(TipType.HIGH_DEVICE_USAGE, 4); - TIP_ORDER.append(TipType.SUMMARY, 5); - TIP_ORDER.append(TipType.SMART_BATTERY_MANAGER, 6); - TIP_ORDER.append(TipType.REDUCED_BATTERY, 7); - TIP_ORDER.append(TipType.REMOVE_APP_RESTRICTION, 8); + TIP_ORDER.append(TipType.DOCK_DEFENDER, 3); + TIP_ORDER.append(TipType.APP_RESTRICTION, 4); + TIP_ORDER.append(TipType.HIGH_DEVICE_USAGE, 5); + TIP_ORDER.append(TipType.SUMMARY, 6); + TIP_ORDER.append(TipType.SMART_BATTERY_MANAGER, 7); + TIP_ORDER.append(TipType.REDUCED_BATTERY, 8); + TIP_ORDER.append(TipType.REMOVE_APP_RESTRICTION, 9); } private static final String KEY_PREFIX = "key_battery_tip"; diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTip.java new file mode 100644 index 00000000000..2ba3dcd12e2 --- /dev/null +++ b/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTip.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2022 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.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.os.Parcel; +import android.util.Log; + +import androidx.preference.Preference; + +import com.android.settings.R; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.BatteryUtils.DockDefenderMode; +import com.android.settings.overlay.FeatureFactory; +import com.android.settings.widget.CardPreference; +import com.android.settingslib.HelpUtils; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import java.text.NumberFormat; + +/** + * Tip to show dock defender status + */ +public class DockDefenderTip extends BatteryTip { + private static final String TAG = "DockDefenderTip"; + private int mMode; + + public DockDefenderTip(@StateType int state, @DockDefenderMode int mode) { + super(TipType.DOCK_DEFENDER, state, false); + mMode = mode; + } + + private DockDefenderTip(Parcel in) { + super(in); + } + + public int getMode() { + return mMode; + } + + @Override + public CharSequence getTitle(Context context) { + switch (mMode) { + case DockDefenderMode.FUTURE_BYPASS: + return context.getString(R.string.battery_tip_dock_defender_future_bypass_title, + getExtraPercentage(context)); + case DockDefenderMode.ACTIVE: + return context.getString(R.string.battery_tip_dock_defender_active_title); + case DockDefenderMode.TEMPORARILY_BYPASSED: + return context.getString( + R.string.battery_tip_dock_defender_temporarily_bypassed_title); + default: + return null; + } + } + + @Override + public CharSequence getSummary(Context context) { + switch (mMode) { + case DockDefenderMode.FUTURE_BYPASS: + return context.getString(R.string.battery_tip_dock_defender_future_bypass_summary, + getExtraPercentage(context)); + case DockDefenderMode.ACTIVE: + return context.getString(R.string.battery_tip_dock_defender_active_summary, + getExtraPercentage(context)); + case DockDefenderMode.TEMPORARILY_BYPASSED: + return context.getString( + R.string.battery_tip_dock_defender_temporarily_bypassed_summary, + getExtraPercentage(context)); + default: + return null; + } + } + + @Override + public int getIconId() { + return R.drawable.ic_battery_status_protected_24dp; + } + + @Override + public void updateState(BatteryTip tip) { + mState = tip.mState; + if (tip instanceof DockDefenderTip) { + mMode = ((DockDefenderTip) tip).mMode; + } + } + + @Override + public void log(Context context, MetricsFeatureProvider metricsFeatureProvider) { + metricsFeatureProvider.action(context, SettingsEnums.ACTION_DOCK_DEFENDER_TIP, + mState); + } + + @Override + public void updatePreference(Preference preference) { + super.updatePreference(preference); + final Context context = preference.getContext(); + + CardPreference cardPreference = castToCardPreferenceSafely(preference); + if (cardPreference == null) { + Log.e(TAG, "cast Preference to CardPreference failed"); + return; + } + + cardPreference.setSelectable(false); + switch (mMode) { + case DockDefenderMode.FUTURE_BYPASS: + case DockDefenderMode.ACTIVE: + cardPreference.setPrimaryButtonText( + context.getString(R.string.battery_tip_charge_to_full_button)); + cardPreference.setPrimaryButtonClickListener(unused -> { + resumeCharging(context); + mMode = DockDefenderMode.TEMPORARILY_BYPASSED; + context.sendBroadcast(new Intent().setAction( + BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION).setPackage( + context.getPackageName()).addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY + | Intent.FLAG_RECEIVER_FOREGROUND)); + updatePreference(preference); + }); + cardPreference.setPrimaryButtonVisible(true); + break; + case DockDefenderMode.TEMPORARILY_BYPASSED: + cardPreference.setPrimaryButtonVisible(false); + break; + default: + cardPreference.setVisible(false); + return; + } + + cardPreference.setSecondaryButtonText(context.getString(R.string.learn_more)); + //TODO: update helper string + cardPreference.setSecondaryButtonClickListener( + button -> button.startActivityForResult( + HelpUtils.getHelpIntent( + context, + context.getString(R.string.help_url_battery_defender), + /* backupContext */ ""), /* requestCode */ 0)); + cardPreference.setSecondaryButtonVisible(true); + cardPreference.setSecondaryButtonContentDescription(context.getString( + R.string.battery_tip_limited_temporarily_sec_button_content_description)); + + } + + private CardPreference castToCardPreferenceSafely(Preference preference) { + return preference instanceof CardPreference ? (CardPreference) preference : null; + } + + private void resumeCharging(Context context) { + final Intent intent = + FeatureFactory.getFactory(context) + .getPowerUsageFeatureProvider(context) + .getResumeChargeIntent(true); + if (intent != null) { + context.sendBroadcast(intent); + } + + Log.i(TAG, "send resume charging broadcast intent=" + intent); + } + + private String getExtraPercentage(Context context) { + final int extraValue = context.getResources() + .getInteger(R.integer.config_battery_extra_tip_value); + return NumberFormat.getPercentInstance() + .format(extraValue * 0.01f); + } + + public static final Creator CREATOR = new Creator() { + public BatteryTip createFromParcel(Parcel in) { + return new DockDefenderTip(in); + } + + public BatteryTip[] newArray(int size) { + return new DockDefenderTip[size]; + } + }; +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java index 867d8f44b7e..79bd84bd1ec 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java @@ -55,6 +55,7 @@ public class BatteryBroadcastReceiverTest { private BatteryBroadcastReceiver mBatteryBroadcastReceiver; private Context mContext; private Intent mChargingIntent; + private Intent mDockDefenderBypassIntent; @Before public void setUp() { @@ -72,6 +73,8 @@ public class BatteryBroadcastReceiverTest { mChargingIntent.putExtra(BatteryManager.EXTRA_SCALE, BATTERY_INTENT_SCALE); mChargingIntent .putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING); + mDockDefenderBypassIntent = new Intent(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION); + } @Test @@ -130,6 +133,13 @@ public class BatteryBroadcastReceiverTest { verify(mBatteryListener, never()).onBatteryChanged(anyInt()); } + @Test + public void testOnReceive_dockDefenderBypassed_listenerInvoked() { + mBatteryBroadcastReceiver.onReceive(mContext, mDockDefenderBypassIntent); + + verify(mBatteryListener).onBatteryChanged(BatteryUpdateType.BATTERY_STATUS); + } + @Test public void testRegister_updateBatteryStatus() { doReturn(mChargingIntent).when(mContext).registerReceiver(any(), any()); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java index c5c47d27203..209dc898f31 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryInfoTest.java @@ -36,6 +36,7 @@ import android.os.BatteryManager; import android.os.BatteryStats; import android.os.BatteryUsageStats; import android.os.SystemClock; +import android.provider.Settings; import android.util.SparseIntArray; import com.android.internal.os.BatteryStatsHistoryIterator; @@ -66,6 +67,7 @@ public class BatteryInfoTest { private static final String STATUS_CHARGING_NO_TIME = "50% - charging"; private static final String STATUS_CHARGING_TIME = "50% - 0 min left until full"; private static final String STATUS_NOT_CHARGING = "Not charging"; + private static final String STATUS_CHARGING_FUTURE_BYPASS = "50% - Charging to 12%"; private static final long REMAINING_TIME_NULL = -1; private static final long REMAINING_TIME = 2; // Strings are defined in frameworks/base/packages/SettingsLib/res/values/strings.xml @@ -97,6 +99,10 @@ public class BatteryInfoTest { mDisChargingBatteryBroadcast = BatteryTestUtils.getDischargingIntent(); mChargingBatteryBroadcast = BatteryTestUtils.getChargingIntent(); + + doReturn(false).when(mFeatureFactory.powerUsageFeatureProvider).isExtraDefend(); + Settings.Global.putInt(mContext.getContentResolver(), + BatteryUtils.SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 0); } @Test @@ -231,6 +237,7 @@ public class BatteryInfoTest { BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, mChargingBatteryBroadcast, mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000, false /* shortString */); + assertThat(info.remainingTimeUs).isEqualTo(TEST_CHARGE_TIME_REMAINING); assertThat(info.remainingLabel.toString()) .isEqualTo(TEST_CHARGE_TIME_REMAINING_STRINGIFIED); @@ -265,6 +272,62 @@ public class BatteryInfoTest { assertThat(info.chargeLabel.toString()).contains(expectedString); } + @Test + public void testGetBatteryInfo_dockDefenderActive_updateChargeString() { + final String expectedString = + mContext.getString(R.string.battery_tip_limited_temporarily_title); + doReturn(TEST_CHARGE_TIME_REMAINING / 1000) + .when(mBatteryUsageStats).getChargeTimeRemainingMs(); + doReturn(true).when(mFeatureFactory.powerUsageFeatureProvider).isExtraDefend(); + Intent intent = BatteryTestUtils.getCustomBatteryIntent(BatteryManager.BATTERY_PLUGGED_DOCK, + 50 /* level */, + 100 /* scale */, + BatteryManager.BATTERY_STATUS_CHARGING) + .putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT); + + BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, intent, + mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000, + false /* shortString */); + + assertThat(info.chargeLabel.toString()).contains(expectedString); + } + + @Test + public void testGetBatteryInfo_dockDefenderTemporarilyBypassed_updateChargeLabel() { + doReturn(REMAINING_TIME).when(mBatteryUsageStats).getChargeTimeRemainingMs(); + mChargingBatteryBroadcast + .putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD); + Settings.Global.putInt(mContext.getContentResolver(), + BatteryUtils.SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 1); + + BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, + BatteryTestUtils.getCustomBatteryIntent(BatteryManager.BATTERY_PLUGGED_DOCK, + 50 /* level */, + 100 /* scale */, + BatteryManager.BATTERY_STATUS_CHARGING), + mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000, + false /* shortString */); + + assertThat(info.chargeLabel.toString()).contains(STATUS_CHARGING_TIME); + } + + @Test + public void testGetBatteryInfo_dockDefenderFutureBypass_updateChargeLabel() { + doReturn(false).when(mFeatureFactory.powerUsageFeatureProvider).isExtraDefend(); + mChargingBatteryBroadcast + .putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_GOOD); + + BatteryInfo info = BatteryInfo.getBatteryInfo(mContext, + BatteryTestUtils.getCustomBatteryIntent(BatteryManager.BATTERY_PLUGGED_DOCK, + 50 /* level */, + 100 /* scale */, + BatteryManager.BATTERY_STATUS_CHARGING), + mBatteryUsageStats, MOCK_ESTIMATE, SystemClock.elapsedRealtime() * 1000, + false /* shortString */); + + assertThat(info.chargeLabel.toString()).contains(STATUS_CHARGING_FUTURE_BYPASS); + } + // Make our battery stats return a sequence of battery events. private void mockBatteryStatsHistory() { // Mock out new data every time iterateBatteryStatsHistory is called. diff --git a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java index 66a5e7f8bd0..648685a8d8f 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImplTest.java @@ -132,7 +132,12 @@ public class PowerUsageFeatureProviderImplTest { } @Test - public void testGetResumeChargeIntent_returnNull() { - assertThat(mPowerFeatureProvider.getResumeChargeIntent()).isNull(); + public void testGetResumeChargeIntentWithoutDockDefender_returnNull() { + assertThat(mPowerFeatureProvider.getResumeChargeIntent(false)).isNull(); + } + + @Test + public void testGetResumeChargeIntentWithDockDefender_returnNull() { + assertThat(mPowerFeatureProvider.getResumeChargeIntent(true)).isNull(); } } diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java index 95280b669bc..6d3965e2266 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java @@ -53,6 +53,7 @@ public class BatteryTipLoaderTest { BatteryTip.TipType.BATTERY_SAVER, BatteryTip.TipType.LOW_BATTERY, BatteryTip.TipType.BATTERY_DEFENDER, + BatteryTip.TipType.DOCK_DEFENDER, BatteryTip.TipType.HIGH_DEVICE_USAGE, BatteryTip.TipType.SMART_BATTERY_MANAGER}; @Mock(answer = Answers.RETURNS_DEEP_STUBS) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java index 90e7ad762a8..f81a4be8d63 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/BatteryDefenderDetectorTest.java @@ -18,10 +18,15 @@ package com.android.settings.fuelgauge.batterytip.detectors; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + +import android.content.Context; + import androidx.test.core.app.ApplicationProvider; import com.android.settings.fuelgauge.BatteryInfo; import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.testutils.FakeFeatureFactory; import org.junit.Before; import org.junit.Test; @@ -36,6 +41,9 @@ public class BatteryDefenderDetectorTest { @Mock private BatteryInfo mBatteryInfo; private BatteryDefenderDetector mBatteryDefenderDetector; + private Context mContext; + + private FakeFeatureFactory mFakeFeatureFactory; @Before public void setUp() { @@ -43,20 +51,42 @@ public class BatteryDefenderDetectorTest { mBatteryInfo.discharging = false; + mContext = ApplicationProvider.getApplicationContext(); + mBatteryDefenderDetector = new BatteryDefenderDetector( - mBatteryInfo, ApplicationProvider.getApplicationContext()); + mBatteryInfo, mContext); + + mFakeFeatureFactory = FakeFeatureFactory.setupForTest(); } @Test - public void testDetect_notOverheated_tipInvisible() { + public void testDetect_notOverheatedNotExtraDefend_tipInvisible() { mBatteryInfo.isOverheated = false; + when(mFakeFeatureFactory.powerUsageFeatureProvider.isExtraDefend()).thenReturn(false); assertThat(mBatteryDefenderDetector.detect().isVisible()).isFalse(); } @Test - public void testDetect_isOverheated_tipNew() { + public void testDetect_notOverheatedIsExtraDefend_tipInvisible() { + mBatteryInfo.isOverheated = false; + when(mFakeFeatureFactory.powerUsageFeatureProvider.isExtraDefend()).thenReturn(true); + + assertThat(mBatteryDefenderDetector.detect().isVisible()).isFalse(); + } + + @Test + public void testDetect_isOverheatedIsExtraDefend_tipInvisible() { + mBatteryInfo.isOverheated = false; + when(mFakeFeatureFactory.powerUsageFeatureProvider.isExtraDefend()).thenReturn(true); + + assertThat(mBatteryDefenderDetector.detect().isVisible()).isFalse(); + } + + @Test + public void testDetect_isOverheatedNotExtraDefend_tipNew() { mBatteryInfo.isOverheated = true; + when(mFakeFeatureFactory.powerUsageFeatureProvider.isExtraDefend()).thenReturn(false); assertThat(mBatteryDefenderDetector.detect().getState()) .isEqualTo(BatteryTip.StateType.NEW); diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetectorTest.java new file mode 100644 index 00000000000..9652a00ca18 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/DockDefenderDetectorTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2022 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BatteryManager; +import android.provider.Settings; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.fuelgauge.BatteryInfo; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.fuelgauge.batterytip.tips.BatteryTip; +import com.android.settings.fuelgauge.batterytip.tips.DockDefenderTip; +import com.android.settings.testutils.BatteryTestUtils; +import com.android.settings.testutils.FakeFeatureFactory; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class DockDefenderDetectorTest { + + private BatteryInfo mBatteryInfo; + private DockDefenderDetector mDockDefenderDetector; + private Context mContext; + private FakeFeatureFactory mFakeFeatureFactory; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + mBatteryInfo = new BatteryInfo(); + mBatteryInfo.pluggedStatus = BatteryManager.BATTERY_PLUGGED_DOCK; + mDockDefenderDetector = new DockDefenderDetector(mBatteryInfo, mContext); + Intent intent = BatteryTestUtils.getCustomBatteryIntent(BatteryManager.BATTERY_PLUGGED_DOCK, + 50 /* level */, 100 /* scale */, BatteryManager.BATTERY_STATUS_CHARGING); + doReturn(intent).when(mContext).registerReceiver(eq(null), + refEq(new IntentFilter(Intent.ACTION_BATTERY_CHANGED))); + + Settings.Global.putInt(mContext.getContentResolver(), + BatteryUtils.SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 0); + mFakeFeatureFactory = FakeFeatureFactory.setupForTest(); + } + + @Test + public void testDetect_dockDefenderTemporarilyBypassed() { + Settings.Global.putInt(mContext.getContentResolver(), + BatteryUtils.SETTINGS_GLOBAL_DOCK_DEFENDER_BYPASS, 1); + + BatteryTip batteryTip = mDockDefenderDetector.detect(); + + assertTrue(batteryTip instanceof DockDefenderTip); + assertEquals(((DockDefenderTip) batteryTip).getMode(), + BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED); + } + + @Test + public void testDetect_dockDefenderActive() { + mBatteryInfo.isOverheated = true; + doReturn(true).when(mFakeFeatureFactory.powerUsageFeatureProvider).isExtraDefend(); + + BatteryTip batteryTip = mDockDefenderDetector.detect(); + + assertTrue(batteryTip instanceof DockDefenderTip); + assertEquals(((DockDefenderTip) batteryTip).getMode(), + BatteryUtils.DockDefenderMode.ACTIVE); + } + + @Test + public void testDetect_dockDefenderFutureBypass() { + mBatteryInfo.isOverheated = false; + doReturn(false).when(mFakeFeatureFactory.powerUsageFeatureProvider).isExtraDefend(); + + BatteryTip batteryTip = mDockDefenderDetector.detect(); + + assertTrue(batteryTip instanceof DockDefenderTip); + assertEquals(((DockDefenderTip) batteryTip).getMode(), + BatteryUtils.DockDefenderMode.FUTURE_BYPASS); + } + + @Test + public void testDetect_overheatedTrue_dockDefenderDisabled() { + mBatteryInfo.isOverheated = true; + doReturn(false).when(mFakeFeatureFactory.powerUsageFeatureProvider).isExtraDefend(); + + BatteryTip batteryTip = mDockDefenderDetector.detect(); + + assertTrue(batteryTip instanceof DockDefenderTip); + assertEquals(((DockDefenderTip) batteryTip).getMode(), + BatteryUtils.DockDefenderMode.DISABLED); + } + + @Test + public void testDetect_pluggedInAC_dockDefenderDisabled() { + mBatteryInfo.pluggedStatus = BatteryManager.BATTERY_PLUGGED_AC; + + BatteryTip batteryTip = mDockDefenderDetector.detect(); + + assertTrue(batteryTip instanceof DockDefenderTip); + assertEquals(((DockDefenderTip) batteryTip).getMode(), + BatteryUtils.DockDefenderMode.DISABLED); + } + + @Test + public void testDetect_overheatedTrueAndDockDefenderNotTriggered_dockDefenderDisabled() { + doReturn(false).when(mFakeFeatureFactory.powerUsageFeatureProvider).isExtraDefend(); + mBatteryInfo.isOverheated = true; + + BatteryTip batteryTip = mDockDefenderDetector.detect(); + + assertTrue(batteryTip instanceof DockDefenderTip); + assertEquals(((DockDefenderTip) batteryTip).getMode(), + BatteryUtils.DockDefenderMode.DISABLED); + } +} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java index 6bd6b26b78c..8b6033a8811 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/BatteryDefenderTipTest.java @@ -79,19 +79,11 @@ public class BatteryDefenderTipTest { } @Test - public void getSummary_notExtraDefended_showNonExtraDefendedSummary() { + public void getSummary_showSummary() { assertThat(mBatteryDefenderTip.getSummary(mContext)) .isEqualTo(mContext.getString(R.string.battery_tip_limited_temporarily_summary)); } - @Test - public void getSummary_extraDefended_showExtraDefendedSummary() { - BatteryDefenderTip defenderTip = new BatteryDefenderTip( - BatteryTip.StateType.NEW, /* extraDefended= */ true); - - assertThat(defenderTip.getSummary(mContext).toString()).isEqualTo("12%"); - } - @Test public void getIcon_showIcon() { assertThat(mBatteryDefenderTip.getIconId()) diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTipTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTipTest.java new file mode 100644 index 00000000000..d917d8921a7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/tips/DockDefenderTipTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2022 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.util.Log; + +import androidx.preference.Preference; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.fuelgauge.BatteryUtils; +import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.widget.CardPreference; +import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowLog; + +import java.text.NumberFormat; + +@RunWith(RobolectricTestRunner.class) +public class DockDefenderTipTest { + private Context mContext; + private DockDefenderTip mDockDefenderTipFutureBypass; + private DockDefenderTip mDockDefenderTipActive; + private DockDefenderTip mDockDefenderTipTemporarilyBypassed; + private DockDefenderTip mDockDefenderTipDisabled; + private FakeFeatureFactory mFeatureFactory; + private MetricsFeatureProvider mMetricsFeatureProvider; + + @Mock + private Preference mPreference; + @Mock + private CardPreference mCardPreference; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = ApplicationProvider.getApplicationContext(); + mFeatureFactory = FakeFeatureFactory.setupForTest(); + mMetricsFeatureProvider = mFeatureFactory.metricsFeatureProvider; + + mDockDefenderTipFutureBypass = new DockDefenderTip(BatteryTip.StateType.NEW, + BatteryUtils.DockDefenderMode.FUTURE_BYPASS); + mDockDefenderTipActive = new DockDefenderTip(BatteryTip.StateType.NEW, + BatteryUtils.DockDefenderMode.ACTIVE); + mDockDefenderTipTemporarilyBypassed = new DockDefenderTip(BatteryTip.StateType.NEW, + BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED); + mDockDefenderTipDisabled = new DockDefenderTip(BatteryTip.StateType.INVISIBLE, + BatteryUtils.DockDefenderMode.DISABLED); + + doReturn(mContext).when(mPreference).getContext(); + doReturn(mContext).when(mCardPreference).getContext(); + } + + @Test + public void testGetTitle() { + assertThat(mDockDefenderTipFutureBypass.getTitle(mContext).toString()).isEqualTo( + mContext.getString(R.string.battery_tip_dock_defender_future_bypass_title, + getExtraPercentage(mContext))); + assertThat(mDockDefenderTipActive.getTitle(mContext).toString()).isEqualTo( + mContext.getString(R.string.battery_tip_dock_defender_active_title)); + assertThat(mDockDefenderTipTemporarilyBypassed.getTitle(mContext).toString()).isEqualTo( + mContext.getString(R.string.battery_tip_dock_defender_temporarily_bypassed_title)); + assertThat(mDockDefenderTipDisabled.getTitle(mContext)).isNull(); + } + + @Test + public void testGetSummary() { + assertThat(mDockDefenderTipFutureBypass.getSummary(mContext).toString()).isEqualTo( + mContext.getString(R.string.battery_tip_dock_defender_future_bypass_summary, + getExtraPercentage(mContext))); + assertThat(mDockDefenderTipActive.getSummary(mContext).toString()).isEqualTo( + mContext.getString(R.string.battery_tip_dock_defender_active_summary, + getExtraPercentage(mContext))); + assertThat(mDockDefenderTipTemporarilyBypassed.getSummary(mContext).toString()).isEqualTo( + mContext.getString(R.string.battery_tip_dock_defender_temporarily_bypassed_summary, + getExtraPercentage(mContext))); + assertThat(mDockDefenderTipDisabled.getSummary(mContext)).isNull(); + } + + @Test + public void testGetIconId() { + assertThat(mDockDefenderTipFutureBypass.getIconId()).isEqualTo( + R.drawable.ic_battery_status_protected_24dp); + } + + @Test + public void testUpdateState() { + mDockDefenderTipTemporarilyBypassed.updateState(mDockDefenderTipDisabled); + + assertThat(mDockDefenderTipTemporarilyBypassed.getState()).isEqualTo( + BatteryTip.StateType.INVISIBLE); + assertThat(mDockDefenderTipTemporarilyBypassed.getMode()).isEqualTo( + BatteryUtils.DockDefenderMode.DISABLED); + } + + @Test + public void testLog() { + mDockDefenderTipActive.log(mContext, mMetricsFeatureProvider); + + verify(mMetricsFeatureProvider).action(mContext, SettingsEnums.ACTION_DOCK_DEFENDER_TIP, + mDockDefenderTipActive.getState()); + } + + + @Test + public void testUpdatePreference_dockDefenderTipFutureBypass() { + mDockDefenderTipFutureBypass.updatePreference(mCardPreference); + + verify(mCardPreference).setPrimaryButtonVisible(true); + verify(mCardPreference).setPrimaryButtonText( + mContext.getString(R.string.battery_tip_charge_to_full_button)); + verifySecondaryButton(); + } + + @Test + public void testUpdatePreference_dockDefenderTipActive() { + mDockDefenderTipActive.updatePreference(mCardPreference); + + verify(mCardPreference).setPrimaryButtonVisible(true); + verify(mCardPreference).setPrimaryButtonText( + mContext.getString(R.string.battery_tip_charge_to_full_button)); + verifySecondaryButton(); + } + + @Test + public void testUpdatePreference_dockDefenderTipTemporarilyBypassed() { + mDockDefenderTipTemporarilyBypassed.updatePreference(mCardPreference); + + verify(mCardPreference).setPrimaryButtonVisible(false); + verify(mCardPreference, never()).setPrimaryButtonText(any()); + verifySecondaryButton(); + } + + private void verifySecondaryButton() { + verify(mCardPreference).setSecondaryButtonText(mContext.getString(R.string.learn_more)); + verify(mCardPreference).setSecondaryButtonVisible(true); + verify(mCardPreference).setSecondaryButtonContentDescription(mContext.getString( + R.string.battery_tip_limited_temporarily_sec_button_content_description)); + } + + @Test + public void updatePreference_castFail_logErrorMessage() { + mDockDefenderTipActive.updatePreference(mPreference); + + assertThat(getLastErrorLog()).isEqualTo("cast Preference to CardPreference failed"); + } + + private String getLastErrorLog() { + return ShadowLog.getLogsForTag(DockDefenderTip.class.getSimpleName()).stream().filter( + log -> log.type == Log.ERROR).reduce((first, second) -> second).orElse( + createErrorLog("No Error Log")).msg; + } + + private ShadowLog.LogItem createErrorLog(String msg) { + return new ShadowLog.LogItem(Log.ERROR, "tag", msg, null); + } + + private String getExtraPercentage(Context context) { + final int extraValue = context.getResources().getInteger( + R.integer.config_battery_extra_tip_value); + return NumberFormat.getPercentInstance().format(extraValue * 0.01f); + } + +} diff --git a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java index e4e26d2c531..a6f24309f52 100644 --- a/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/BatteryTestUtils.java @@ -37,7 +37,7 @@ public class BatteryTestUtils { BatteryManager.BATTERY_STATUS_DISCHARGING); } - private static Intent getCustomBatteryIntent(int plugged, int level, int scale, int status) { + public static Intent getCustomBatteryIntent(int plugged, int level, int scale, int status) { Intent intent = new Intent(); intent.putExtra(BatteryManager.EXTRA_PLUGGED, plugged); intent.putExtra(BatteryManager.EXTRA_LEVEL, level); From 07d208dfb1ac5585efb3c52b10820adda6ae4be4 Mon Sep 17 00:00:00 2001 From: Zhenwei Chen Date: Tue, 22 Nov 2022 04:39:03 +0800 Subject: [PATCH 3/3] Clean up useless defender dialog code Test: Unit test passed Bug: 260687359 Merged-In: If3cbe4072c891f1af6b5aa4fb624e4e486c78ad6 Change-Id: If3cbe4072c891f1af6b5aa4fb624e4e486c78ad6 Signed-off-by: Zhenwei Chen (cherry picked from commit a725bac68c9d1846cef096e4c5958b161787cd9c) --- .../batterytip/BatteryTipDialogFragment.java | 24 ----------- .../actions/BatteryDefenderAction.java | 43 ------------------- .../BatteryTipDialogFragmentTest.java | 16 ------- 3 files changed, 83 deletions(-) delete mode 100644 src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java index d4c00a44cd2..5fd3905649f 100644 --- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java +++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragment.java @@ -43,7 +43,6 @@ import com.android.settings.fuelgauge.batterytip.tips.HighUsageTip; import com.android.settings.fuelgauge.batterytip.tips.RestrictAppTip; import com.android.settings.fuelgauge.batterytip.tips.UnrestrictAppTip; -import java.text.NumberFormat; import java.util.List; /** @@ -142,29 +141,6 @@ public class BatteryTipDialogFragment extends InstrumentedDialogFragment impleme .setPositiveButton(R.string.battery_tip_unrestrict_app_dialog_ok, this) .setNegativeButton(R.string.battery_tip_unrestrict_app_dialog_cancel, null) .create(); - case BatteryTip.TipType.BATTERY_DEFENDER: - mMetricsFeatureProvider.action(context, - SettingsEnums.ACTION_TIP_BATTERY_DEFENDER, mMetricsKey); - final double chargeLimitLevel = 0.8f; - final String percentage = - NumberFormat.getPercentInstance().format(chargeLimitLevel); - final String message = context.getString( - R.string.battery_tip_limited_temporarily_dialog_msg, percentage); - final boolean isPluggedIn = isPluggedIn(); - final AlertDialog.Builder dialogBuilder = - new AlertDialog.Builder(context) - .setTitle(R.string.battery_tip_limited_temporarily_title) - .setMessage(message); - if (isPluggedIn) { - dialogBuilder - .setPositiveButton( - R.string.battery_tip_limited_temporarily_dialog_resume_charge, - this) - .setNegativeButton(R.string.okay, null); - } else { - dialogBuilder.setPositiveButton(R.string.okay, null); - } - return dialogBuilder.create(); default: throw new IllegalArgumentException("unknown type " + mBatteryTip.getType()); } diff --git a/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java b/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java deleted file mode 100644 index 824b6bee5ca..00000000000 --- a/src/com/android/settings/fuelgauge/batterytip/actions/BatteryDefenderAction.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2020 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.actions; - -import android.content.Intent; - -import com.android.settings.SettingsActivity; -import com.android.settings.overlay.FeatureFactory; - -/** - * Action to open the Support Center article - */ -public class BatteryDefenderAction extends BatteryTipAction { - private SettingsActivity mSettingsActivity; - - public BatteryDefenderAction(SettingsActivity settingsActivity) { - super(settingsActivity.getApplicationContext()); - mSettingsActivity = settingsActivity; - } - - @Override - public void handlePositiveAction(int metricsKey) { - final Intent intent = FeatureFactory.getFactory(mContext) - .getPowerUsageFeatureProvider(mContext).getResumeChargeIntent(); - if (intent != null) { - mContext.sendBroadcast(intent); - } - } -} diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java index db1159e1bcf..c5d66a60b45 100644 --- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java +++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipDialogFragmentTest.java @@ -244,20 +244,4 @@ public class BatteryTipDialogFragmentTest { assertThat(shadowDialog.getMessage()).isEqualTo( mContext.getText(R.string.battery_tip_dialog_summary_message)); } - - @Test - public void testOnCreateDialog_defenderTip_fireDialog() { - mDialogFragment = BatteryTipDialogFragment.newInstance(mDefenderTip, METRICS_KEY); - - FragmentController.setupFragment(mDialogFragment, FragmentActivity.class, - 0 /* containerViewId */, null /* bundle */); - - final AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog(); - ShadowAlertDialogCompat shadowDialog = ShadowAlertDialogCompat.shadowOf(dialog); - - assertThat(shadowDialog.getTitle()).isEqualTo( - mContext.getString(R.string.battery_tip_limited_temporarily_title)); - assertThat(shadowDialog.getMessage()).isEqualTo( - mContext.getString(R.string.battery_tip_limited_temporarily_dialog_msg, "80%")); - } }