Refactor code to support overridden low battery warning

This adds support for enabling the hybrid notification using data
from Device Health Services.

Test: SystemUITests
Bug: 27567513
Change-Id: I5fae3d85f2d4956210bb067ff7c8b14146c8c89c
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 98537a1..2d61c66 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -61,11 +61,26 @@
     <!-- When the battery is low, this is displayed to the user in a dialog.  The title of the low battery alert.  [CHAR LIMIT=NONE]-->
     <string name="battery_low_title">Battery is low</string>
 
+    <!-- When the battery is low and hybrid notifications are enabled, this is displayed to the user in a dialog.
+         The title of the low battery alert.  [CHAR LIMIT=NONE]-->
+    <string name="battery_low_title_hybrid">Battery is low. Turn on Battery Saver</string>
+
     <!-- A message that appears when the battery level is getting low in a dialog.  This is
-        appened to the subtitle of the low battery alert.  "percentage" is the percentage of battery
+        appended to the subtitle of the low battery alert.  "percentage" is the percentage of battery
         remaining [CHAR LIMIT=none]-->
     <string name="battery_low_percent_format"><xliff:g id="percentage">%s</xliff:g> remaining</string>
 
+    <!-- A message that appears when the battery remaining estimate is low in a dialog.  This is
+    appended to the subtitle of the low battery alert.  "percentage" is the percentage of battery
+    remaining. "time" is the amount of time remaining before the phone runs out of battery [CHAR LIMIT=none]-->
+    <string name="battery_low_percent_format_hybrid"><xliff:g id="percentage">%s</xliff:g> remaining, about <xliff:g id="time">%s</xliff:g> left based on your usage</string>
+
+    <!-- A message that appears when the battery remaining estimate is low in a dialog and insufficient
+    data was present to say it is customized to the user. This is appended to the subtitle of the
+    low battery alert.  "percentage" is the percentage of battery remaining. "time" is the amount
+     of time remaining before the phone runs out of battery [CHAR LIMIT=none]-->
+    <string name="battery_low_percent_format_hybrid_short"><xliff:g id="percentage">%s</xliff:g> remaining, about <xliff:g id="time">%s</xliff:g> left</string>
+
     <!-- Same as battery_low_percent_format, with a notice about battery saver if on. [CHAR LIMIT=none]-->
     <string name="battery_low_percent_format_saver_started"><xliff:g id="percentage">%s</xliff:g> remaining. Battery Saver is on.</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index e7e70af..7403ddc 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -40,6 +40,8 @@
 import com.android.systemui.plugins.PluginManager;
 import com.android.systemui.plugins.PluginManagerImpl;
 import com.android.systemui.plugins.VolumeDialogController;
+import com.android.systemui.power.EnhancedEstimates;
+import com.android.systemui.power.EnhancedEstimatesImpl;
 import com.android.systemui.power.PowerNotificationWarnings;
 import com.android.systemui.power.PowerUI;
 import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
@@ -310,6 +312,8 @@
 
         mProviders.put(OverviewProxyService.class, () -> new OverviewProxyService(mContext));
 
+        mProviders.put(EnhancedEstimates.class, () -> new EnhancedEstimatesImpl());
+
         // Put all dependencies above here so the factory can override them if it wants.
         SystemUIFactory.getInstance().injectDependencies(mProviders, mContext);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimates.java b/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimates.java
new file mode 100644
index 0000000..8f41a60
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimates.java
@@ -0,0 +1,8 @@
+package com.android.systemui.power;
+
+public interface EnhancedEstimates {
+
+    boolean isHybridNotificationEnabled();
+
+    Estimate getEstimate();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimatesImpl.java b/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimatesImpl.java
new file mode 100644
index 0000000..d447542
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/power/EnhancedEstimatesImpl.java
@@ -0,0 +1,16 @@
+package com.android.systemui.power;
+
+import android.util.Log;
+
+public class EnhancedEstimatesImpl implements EnhancedEstimates {
+
+    @Override
+    public boolean isHybridNotificationEnabled() {
+        return false;
+    }
+
+    @Override
+    public Estimate getEstimate() {
+        return null;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/power/Estimate.java b/packages/SystemUI/src/com/android/systemui/power/Estimate.java
new file mode 100644
index 0000000..12a8f0a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/power/Estimate.java
@@ -0,0 +1,11 @@
+package com.android.systemui.power;
+
+public class Estimate {
+    public final long estimateMillis;
+    public final boolean isBasedOnUsage;
+
+    public Estimate(long estimateMillis, boolean isBasedOnUsage) {
+        this.estimateMillis = estimateMillis;
+        this.isBasedOnUsage = isBasedOnUsage;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
index c29b362..736286f 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java
@@ -17,40 +17,40 @@
 package com.android.systemui.power;
 
 import android.app.Notification;
-import android.app.NotificationChannel;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.content.DialogInterface.OnDismissListener;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
 import android.media.AudioAttributes;
-import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.PowerManager;
-import android.os.SystemClock;
 import android.os.UserHandle;
-import android.provider.Settings;
 import android.support.annotation.VisibleForTesting;
+import android.text.format.DateUtils;
 import android.util.Slog;
 
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
 import com.android.settingslib.Utils;
 import com.android.systemui.R;
 import com.android.systemui.SystemUI;
-import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 import com.android.systemui.util.NotificationChannels;
 
 import java.io.PrintWriter;
 import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
 
 public class PowerNotificationWarnings implements PowerUI.WarningsUI {
     private static final String TAG = PowerUI.TAG + ".Notification";
@@ -96,8 +96,9 @@
     private long mScreenOffTime;
     private int mShowing;
 
-    private long mBucketDroppedNegativeTimeMs;
+    private long mWarningTriggerTimeMs;
 
+    private Estimate mEstimate;
     private boolean mWarning;
     private boolean mPlaySound;
     private boolean mInvalidCharger;
@@ -130,14 +131,22 @@
     public void update(int batteryLevel, int bucket, long screenOffTime) {
         mBatteryLevel = batteryLevel;
         if (bucket >= 0) {
-            mBucketDroppedNegativeTimeMs = 0;
+            mWarningTriggerTimeMs = 0;
         } else if (bucket < mBucket) {
-            mBucketDroppedNegativeTimeMs = System.currentTimeMillis();
+            mWarningTriggerTimeMs = System.currentTimeMillis();
         }
         mBucket = bucket;
         mScreenOffTime = screenOffTime;
     }
 
+    @Override
+    public void updateEstimate(Estimate estimate) {
+        mEstimate = estimate;
+        if (estimate.estimateMillis <= PowerUI.THREE_HOURS_IN_MILLIS) {
+            mWarningTriggerTimeMs = System.currentTimeMillis();
+        }
+    }
+
     private void updateNotification() {
         if (DEBUG) Slog.d(TAG, "updateNotification mWarning=" + mWarning + " mPlaySound="
                 + mPlaySound + " mInvalidCharger=" + mInvalidCharger);
@@ -171,25 +180,43 @@
         mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, n, UserHandle.ALL);
     }
 
-    private void showWarningNotification() {
-        final int textRes = R.string.battery_low_percent_format;
+    protected void showWarningNotification() {
         final String percentage = NumberFormat.getPercentInstance().format((double) mBatteryLevel / 100.0);
 
+        // get standard notification copy
+        String title = mContext.getString(R.string.battery_low_title);
+        String contentText = mContext.getString(R.string.battery_low_percent_format, percentage);
+
+        // override notification copy if hybrid notification enabled
+        if (mEstimate != null) {
+            title = mContext.getString(R.string.battery_low_title_hybrid);
+            contentText = mContext.getString(
+                    mEstimate.isBasedOnUsage
+                            ? R.string.battery_low_percent_format_hybrid
+                            : R.string.battery_low_percent_format_hybrid_short,
+                    percentage,
+                    getTimeRemainingFormatted());
+        }
+
         final Notification.Builder nb =
                 new Notification.Builder(mContext, NotificationChannels.BATTERY)
                         .setSmallIcon(R.drawable.ic_power_low)
                         // Bump the notification when the bucket dropped.
-                        .setWhen(mBucketDroppedNegativeTimeMs)
+                        .setWhen(mWarningTriggerTimeMs)
                         .setShowWhen(false)
-                        .setContentTitle(mContext.getString(R.string.battery_low_title))
-                        .setContentText(mContext.getString(textRes, percentage))
+                        .setContentTitle(title)
+                        .setContentText(contentText)
                         .setOnlyAlertOnce(true)
                         .setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING))
-                        .setVisibility(Notification.VISIBILITY_PUBLIC)
-                        .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError));
+                        .setVisibility(Notification.VISIBILITY_PUBLIC);
         if (hasBatterySettings()) {
             nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS));
         }
+        // Make the notification red if the percentage goes below a certain amount or the time
+        // remaining estimate is disabled
+        if (mEstimate == null || mBucket < 0) {
+            nb.setColor(Utils.getColorAttr(mContext, android.R.attr.colorError));
+        }
         nb.addAction(0,
                 mContext.getString(R.string.battery_saver_start_action),
                 pendingBroadcast(ACTION_START_SAVER));
@@ -201,6 +228,23 @@
         mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL);
     }
 
+    @VisibleForTesting
+    String getTimeRemainingFormatted() {
+        final Locale currentLocale = mContext.getResources().getConfiguration().getLocales().get(0);
+        MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.NARROW);
+
+        final long remainder = mEstimate.estimateMillis % DateUtils.HOUR_IN_MILLIS;
+        final long hours = TimeUnit.MILLISECONDS.toHours(
+                mEstimate.estimateMillis - remainder);
+        // round down to the nearest 15 min for now to not appear overly precise
+        final long minutes = TimeUnit.MILLISECONDS.toMinutes(
+                remainder - (remainder % TimeUnit.MINUTES.toMillis(15)));
+        final Measure hoursMeasure = new Measure(hours, MeasureUnit.HOUR);
+        final Measure minutesMeasure = new Measure(minutes, MeasureUnit.MINUTE);
+
+        return frmt.formatMeasures(hoursMeasure, minutesMeasure);
+    }
+
     private PendingIntent pendingBroadcast(String action) {
         return PendingIntent.getBroadcastAsUser(mContext,
                 0, new Intent(action), 0, UserHandle.CURRENT);
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index c1a3623..a8d4271 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -52,6 +52,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
 
 public class PowerUI extends SystemUI {
     static final String TAG = "PowerUI";
@@ -59,6 +60,7 @@
     private static final long TEMPERATURE_INTERVAL = 30 * DateUtils.SECOND_IN_MILLIS;
     private static final long TEMPERATURE_LOGGING_INTERVAL = DateUtils.HOUR_IN_MILLIS;
     private static final int MAX_RECENT_TEMPS = 125; // TEMPERATURE_LOGGING_INTERVAL plus a buffer
+    static final long THREE_HOURS_IN_MILLIS = DateUtils.HOUR_IN_MILLIS * 3;
 
     private final Handler mHandler = new Handler();
     private final Receiver mReceiver = new Receiver();
@@ -68,9 +70,11 @@
     private WarningsUI mWarnings;
     private final Configuration mLastConfiguration = new Configuration();
     private int mBatteryLevel = 100;
+    private long mTimeRemaining = Long.MAX_VALUE;
     private int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN;
     private int mPlugType = 0;
     private int mInvalidCharger = 0;
+    private EnhancedEstimates mEnhancedEstimates;
 
     private int mLowBatteryAlertCloseLevel;
     private final int[] mLowBatteryReminderLevels = new int[2];
@@ -83,8 +87,8 @@
     private long mNextLogTime;
     private IThermalService mThermalService;
 
-    // We create a method reference here so that we are guaranteed that we can remove a callback
     // by using the same instance (method references are not guaranteed to be the same object
+    // We create a method reference here so that we are guaranteed that we can remove a callback
     // each time they are created).
     private final Runnable mUpdateTempCallback = this::updateTemperatureWarning;
 
@@ -94,6 +98,7 @@
                 mContext.getSystemService(Context.HARDWARE_PROPERTIES_SERVICE);
         mScreenOffTime = mPowerManager.isScreenOn() ? -1 : SystemClock.elapsedRealtime();
         mWarnings = Dependency.get(WarningsUI.class);
+        mEnhancedEstimates = Dependency.get(EnhancedEstimates.class);
         mLastConfiguration.setTo(mContext.getResources().getConfiguration());
 
         ContentObserver obs = new ContentObserver(mHandler) {
@@ -231,21 +236,9 @@
                     return;
                 }
 
-                boolean isPowerSaver = mPowerManager.isPowerSaveMode();
-                if (!plugged
-                        && !isPowerSaver
-                        && (bucket < oldBucket || oldPlugged)
-                        && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
-                        && bucket < 0) {
+                // Show the correct version of low battery warning if needed
+                maybeShowBatteryWarning(plugged, oldPlugged, oldBucket, bucket);
 
-                    // only play SFX when the dialog comes up or the bucket changes
-                    final boolean playSound = bucket != oldBucket || oldPlugged;
-                    mWarnings.showLowBatteryWarning(playSound);
-                } else if (isPowerSaver || plugged || (bucket > oldBucket && bucket > 0)) {
-                    mWarnings.dismissLowBatteryWarning();
-                } else {
-                    mWarnings.updateLowBatteryWarning();
-                }
             } else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
                 mScreenOffTime = SystemClock.elapsedRealtime();
             } else if (Intent.ACTION_SCREEN_ON.equals(action)) {
@@ -256,7 +249,65 @@
                 Slog.w(TAG, "unknown intent: " + intent);
             }
         }
-    };
+    }
+
+    protected void maybeShowBatteryWarning(boolean plugged, boolean oldPlugged, int oldBucket,
+        int bucket) {
+        boolean isPowerSaver = mPowerManager.isPowerSaveMode();
+        // only play SFX when the dialog comes up or the bucket changes
+        final boolean playSound = bucket != oldBucket || oldPlugged;
+        long oldTimeRemaining = mTimeRemaining;
+        if (mEnhancedEstimates.isHybridNotificationEnabled()) {
+            final Estimate estimate = mEnhancedEstimates.getEstimate();
+            // Turbo is not always booted once SysUI is running so we have ot make sure we actually
+            // get data back
+            if (estimate != null) {
+                mTimeRemaining = estimate.estimateMillis;
+                mWarnings.updateEstimate(estimate);
+            }
+        }
+
+        if (shouldShowLowBatteryWarning(plugged, oldPlugged, oldBucket, bucket, oldTimeRemaining,
+                mTimeRemaining,
+                isPowerSaver, mBatteryStatus)) {
+            mWarnings.showLowBatteryWarning(playSound);
+        } else if (shouldDismissLowBatteryWarning(plugged, oldBucket, bucket, mTimeRemaining,
+                isPowerSaver)) {
+            mWarnings.dismissLowBatteryWarning();
+        } else {
+            mWarnings.updateLowBatteryWarning();
+        }
+    }
+
+    @VisibleForTesting
+    boolean shouldShowLowBatteryWarning(boolean plugged, boolean oldPlugged, int oldBucket,
+            int bucket, long oldTimeRemaining, long timeRemaining,
+            boolean isPowerSaver, int mBatteryStatus) {
+        return !plugged
+                && !isPowerSaver
+                && (((bucket < oldBucket || oldPlugged) && bucket < 0)
+                        || (mEnhancedEstimates.isHybridNotificationEnabled()
+                                && timeRemaining < THREE_HOURS_IN_MILLIS
+                                && isHourLess(oldTimeRemaining, timeRemaining)))
+                && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN;
+    }
+
+    private boolean isHourLess(long oldTimeRemaining, long timeRemaining) {
+        final long dif = oldTimeRemaining - timeRemaining;
+        return dif >= TimeUnit.HOURS.toMillis(1);
+    }
+
+    @VisibleForTesting
+    boolean shouldDismissLowBatteryWarning(boolean plugged, int oldBucket, int bucket,
+            long timeRemaining, boolean isPowerSaver) {
+        final boolean hybridWouldDismiss = mEnhancedEstimates.isHybridNotificationEnabled()
+                && timeRemaining > THREE_HOURS_IN_MILLIS;
+        final boolean standardWouldDismiss = (bucket > oldBucket && bucket > 0);
+        return isPowerSaver
+                || plugged
+                || (standardWouldDismiss && (!mEnhancedEstimates.isHybridNotificationEnabled()
+                        || hybridWouldDismiss));
+    }
 
     private void initTemperatureWarning() {
         ContentResolver resolver = mContext.getContentResolver();
@@ -428,6 +479,7 @@
 
     public interface WarningsUI {
         void update(int batteryLevel, int bucket, long screenOffTime);
+        void updateEstimate(Estimate estimate);
         void dismissLowBatteryWarning();
         void showLowBatteryWarning(boolean playSound);
         void dismissInvalidChargerWarning();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
index 7f07e0c..3e37cfe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java
@@ -38,6 +38,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.util.NotificationChannels;
 
+import java.util.concurrent.TimeUnit;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,6 +47,9 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class PowerNotificationWarningsTest extends SysuiTestCase {
+
+    public static final String FORMATTED_45M = "0h 45m";
+    public static final String FORMATTED_HOUR = "1h 0m";
     private final NotificationManager mMockNotificationManager = mock(NotificationManager.class);
     private PowerNotificationWarnings mPowerNotificationWarnings;
 
@@ -147,4 +151,22 @@
         verify(mMockNotificationManager, times(1)).cancelAsUser(anyString(),
                 eq(SystemMessage.NOTE_THERMAL_SHUTDOWN), any());
     }
+
+    @Test
+    public void testGetTimeRemainingFormatted_roundsDownTo15() {
+        mPowerNotificationWarnings.updateEstimate(
+                new Estimate(TimeUnit.MINUTES.toMillis(57), true));
+        String time = mPowerNotificationWarnings.getTimeRemainingFormatted();
+
+        assertTrue("time:" + time + ", expected: " + FORMATTED_45M, time.equals(FORMATTED_45M));
+    }
+
+    @Test
+    public void testGetTimeRemainingFormatted_keepsMinutesWhenZero() {
+        mPowerNotificationWarnings.updateEstimate(
+                new Estimate(TimeUnit.MINUTES.toMillis(65), true));
+        String time = mPowerNotificationWarnings.getTimeRemainingFormatted();
+
+        assertTrue("time:" + time + ", expected: " + FORMATTED_HOUR, time.equals(FORMATTED_HOUR));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
index e4734a4..fdb7f8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerUITest.java
@@ -19,12 +19,15 @@
 import static android.os.HardwarePropertiesManager.TEMPERATURE_SHUTDOWN;
 import static android.provider.Settings.Global.SHOW_TEMPERATURE_WARNING;
 
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.os.BatteryManager;
 import android.os.HardwarePropertiesManager;
 import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
@@ -37,6 +40,7 @@
 import com.android.systemui.power.PowerUI.WarningsUI;
 import com.android.systemui.statusbar.phone.StatusBar;
 
+import java.util.concurrent.TimeUnit;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -46,14 +50,23 @@
 @SmallTest
 public class PowerUITest extends SysuiTestCase {
 
+    private static final boolean UNPLUGGED = false;
+    private static final boolean POWER_SAVER_OFF = false;
+    private static final int ABOVE_WARNING_BUCKET = 1;
+    public static final int BELOW_WARNING_BUCKET = -1;
+    public static final long BELOW_HYBRID_THRESHOLD = TimeUnit.HOURS.toMillis(2);
+    public static final long ABOVE_HYBRID_THRESHOLD = TimeUnit.HOURS.toMillis(4);
     private HardwarePropertiesManager mHardProps;
     private WarningsUI mMockWarnings;
     private PowerUI mPowerUI;
+    private EnhancedEstimates mEnhacedEstimates;
 
     @Before
     public void setup() {
         mMockWarnings = mDependency.injectMockDependency(WarningsUI.class);
+        mEnhacedEstimates = mDependency.injectMockDependency(EnhancedEstimates.class);
         mHardProps = mock(HardwarePropertiesManager.class);
+
         mContext.putComponent(StatusBar.class, mock(StatusBar.class));
         mContext.addMockSystemService(Context.HARDWARE_PROPERTIES_SERVICE, mHardProps);
 
@@ -128,6 +141,180 @@
         verify(mMockWarnings).showHighTemperatureWarning();
     }
 
+    @Test
+    public void testShouldShowLowBatteryWarning_showHybridOnly_returnsShow() {
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        mPowerUI.start();
+
+        // unplugged device that would not show the non-hybrid notification but would show the
+        // hybrid
+        boolean shouldShow =
+                mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET,
+                        ABOVE_WARNING_BUCKET, Long.MAX_VALUE, BELOW_HYBRID_THRESHOLD,
+                        POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD);
+        assertTrue(shouldShow);
+    }
+
+    @Test
+    public void testShouldShowLowBatteryWarning_showHybrid_showStandard_returnsShow() {
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        mPowerUI.start();
+
+        // unplugged device that would show the non-hybrid notification and the hybrid
+        boolean shouldShow =
+                mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, Long.MAX_VALUE, BELOW_HYBRID_THRESHOLD,
+                        POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD);
+        assertTrue(shouldShow);
+    }
+
+    @Test
+    public void testShouldShowLowBatteryWarning_showStandardOnly_returnsShow() {
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        mPowerUI.start();
+
+        // unplugged device that would show the non-hybrid but not the hybrid
+        boolean shouldShow =
+                mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, Long.MAX_VALUE, ABOVE_HYBRID_THRESHOLD,
+                        POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD);
+        assertTrue(shouldShow);
+    }
+
+    @Test
+    public void testShouldShowLowBatteryWarning_deviceHighBattery_returnsNoShow() {
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        mPowerUI.start();
+
+        // unplugged device that would show the neither due to battery level being good
+        boolean shouldShow =
+                mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET,
+                        ABOVE_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, ABOVE_HYBRID_THRESHOLD,
+                        POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD);
+        assertFalse(shouldShow);
+    }
+
+    @Test
+    public void testShouldShowLowBatteryWarning_devicePlugged_returnsNoShow() {
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        mPowerUI.start();
+
+        // plugged device that would show the neither due to being plugged
+        boolean shouldShow =
+                mPowerUI.shouldShowLowBatteryWarning(!UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, BELOW_HYBRID_THRESHOLD,
+                        POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD);
+        assertFalse(shouldShow);
+   }
+
+    @Test
+    public void testShouldShowLowBatteryWarning_deviceBatteryStatusUnkown_returnsNoShow() {
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        mPowerUI.start();
+
+        // Unknown battery status device that would show the neither due
+        boolean shouldShow =
+                mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, BELOW_HYBRID_THRESHOLD,
+                        !POWER_SAVER_OFF, BatteryManager.BATTERY_STATUS_UNKNOWN);
+        assertFalse(shouldShow);
+    }
+
+    @Test
+    public void testShouldShowLowBatteryWarning_batterySaverEnabled_returnsNoShow() {
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        mPowerUI.start();
+
+        // BatterySaverEnabled device that would show the neither due to battery saver
+        boolean shouldShow =
+                mPowerUI.shouldShowLowBatteryWarning(UNPLUGGED, UNPLUGGED, ABOVE_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, BELOW_HYBRID_THRESHOLD,
+                        !POWER_SAVER_OFF, BatteryManager.BATTERY_HEALTH_GOOD);
+        assertFalse(shouldShow);
+    }
+
+    @Test
+    public void testShouldDismissLowBatteryWarning_dismissWhenPowerSaverEnabled() {
+        mPowerUI.start();
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        // device that gets power saver turned on should dismiss
+        boolean shouldDismiss =
+                mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, !POWER_SAVER_OFF);
+        assertTrue(shouldDismiss);
+    }
+
+    @Test
+    public void testShouldDismissLowBatteryWarning_dismissWhenPlugged() {
+        mPowerUI.start();
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+
+        // device that gets plugged in should dismiss
+        boolean shouldDismiss =
+                mPowerUI.shouldDismissLowBatteryWarning(!UNPLUGGED, BELOW_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF);
+        assertTrue(shouldDismiss);
+    }
+
+    @Test
+    public void testShouldDismissLowBatteryWarning_dismissHybridSignal_showStandardSignal_shouldShow() {
+        mPowerUI.start();
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+        // would dismiss hybrid but not non-hybrid should not dismiss
+        boolean shouldDismiss =
+                mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF);
+        assertFalse(shouldDismiss);
+    }
+
+    @Test
+    public void testShouldDismissLowBatteryWarning_showHybridSignal_dismissStandardSignal_shouldShow() {
+        mPowerUI.start();
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+
+        // would dismiss non-hybrid but not hybrid should not dismiss
+        boolean shouldDismiss =
+                mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET,
+                        ABOVE_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, POWER_SAVER_OFF);
+        assertFalse(shouldDismiss);
+    }
+
+    @Test
+    public void testShouldDismissLowBatteryWarning_showBothSignal_shouldShow() {
+        mPowerUI.start();
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+
+        // should not dismiss when both would not dismiss
+        boolean shouldDismiss =
+                mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET,
+                        BELOW_WARNING_BUCKET, BELOW_HYBRID_THRESHOLD, POWER_SAVER_OFF);
+        assertFalse(shouldDismiss);
+    }
+
+    @Test
+    public void testShouldDismissLowBatteryWarning_dismissBothSignal_shouldDismiss() {
+        mPowerUI.start();
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(true);
+
+        //should dismiss if both would dismiss
+        boolean shouldDismiss =
+                mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET,
+                        ABOVE_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF);
+        assertTrue(shouldDismiss);
+    }
+
+    @Test
+    public void testShouldDismissLowBatteryWarning_dismissStandardSignal_hybridDisabled_shouldDismiss() {
+        mPowerUI.start();
+        when(mEnhacedEstimates.isHybridNotificationEnabled()).thenReturn(false);
+
+        // would dismiss non-hybrid with hybrid disabled should dismiss
+        boolean shouldDismiss =
+                mPowerUI.shouldDismissLowBatteryWarning(UNPLUGGED, BELOW_WARNING_BUCKET,
+                        ABOVE_WARNING_BUCKET, ABOVE_HYBRID_THRESHOLD, POWER_SAVER_OFF);
+        assertTrue(shouldDismiss);
+    }
+
     private void setCurrentTemp(float temp) {
         when(mHardProps.getDeviceTemperatures(DEVICE_TEMPERATURE_SKIN, TEMPERATURE_CURRENT))
                 .thenReturn(new float[] { temp });