From 4293d98e632d4a6c429684acb3506f8bf7864697 Mon Sep 17 00:00:00 2001 From: Fabian Kozynski Date: Tue, 22 Mar 2022 15:46:24 -0400 Subject: [PATCH] Add animation for power saver dialog This dialog appears the first time Power Saver is enabled, if it's triggered from QS. Because there's a roundtrip through SettingsLib and a broadcast, we cannot just pass the view. Instead, save a (weak) reference to the view in the controller that can be retrieved when it's time to show the dialog. Test: manual Test: atest SystemUITests Fixes: 225183234 Change-Id: Ife049c5ce4eaf7823cd8ba636f511ada401308e3 --- .../power/PowerNotificationWarnings.java | 27 +++++- .../systemui/qs/tiles/BatterySaverTile.java | 7 +- .../statusbar/policy/BatteryController.java | 29 +++++- .../policy/BatteryControllerImpl.java | 21 +++- .../power/PowerNotificationWarningsTest.java | 97 ++++++++++++++++++- .../systemui/qs/tiles/BatterySaverTileTest.kt | 28 ++++++ .../policy/BatteryControllerTest.java | 52 +++++++++- .../utils/leaks/FakeBatteryController.java | 6 ++ 8 files changed, 256 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index 2435497193e46..3e00a5f74d8fc 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -18,6 +18,7 @@ package com.android.systemui.power; import static android.app.PendingIntent.FLAG_IMMUTABLE; +import android.app.Dialog; import android.app.KeyguardManager; import android.app.Notification; import android.app.NotificationManager; @@ -60,20 +61,25 @@ import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.settingslib.utils.PowerUtil; import com.android.systemui.R; import com.android.systemui.SystemUIApplication; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.phone.SystemUIDialog; +import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.NotificationChannels; import com.android.systemui.volume.Events; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.text.NumberFormat; import java.util.Locale; import java.util.Objects; import javax.inject.Inject; +import dagger.Lazy; + /** */ @SysUISingleton @@ -164,11 +170,15 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private ActivityStarter mActivityStarter; private final BroadcastSender mBroadcastSender; + private final Lazy mBatteryControllerLazy; + private final DialogLaunchAnimator mDialogLaunchAnimator; + /** */ @Inject public PowerNotificationWarnings(Context context, ActivityStarter activityStarter, - BroadcastSender broadcastSender) { + BroadcastSender broadcastSender, Lazy batteryControllerLazy, + DialogLaunchAnimator dialogLaunchAnimator) { mContext = context; mNoMan = mContext.getSystemService(NotificationManager.class); mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE); @@ -176,6 +186,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { mReceiver.init(); mActivityStarter = activityStarter; mBroadcastSender = broadcastSender; + mBatteryControllerLazy = batteryControllerLazy; + mDialogLaunchAnimator = dialogLaunchAnimator; mUseSevereDialog = mContext.getResources().getBoolean(R.bool.config_severe_battery_dialog); } @@ -685,8 +697,19 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { } d.setShowForAllUsers(true); d.setOnDismissListener((dialog) -> mSaverConfirmation = null); - d.show(); + WeakReference ref = mBatteryControllerLazy.get().getLastPowerSaverStartView(); + if (ref != null && ref.get() != null && ref.get().isAggregatedVisible()) { + mDialogLaunchAnimator.showFromView(d, ref.get()); + } else { + d.show(); + } mSaverConfirmation = d; + mBatteryControllerLazy.get().clearLastPowerSaverStartView(); + } + + @VisibleForTesting + Dialog getSaverConfirmationDialog() { + return mSaverConfirmation; } private boolean isEnglishLocale() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java index 7d8a28fc011ee..1004fcae38277 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BatterySaverTile.java @@ -116,6 +116,11 @@ public class BatterySaverTile extends QSTileImpl implements public void handleSetListening(boolean listening) { super.handleSetListening(listening); mSetting.setListening(listening); + if (!listening) { + // If we stopped listening, it means that the tile is not visible. In that case, we + // don't need to save the view anymore + mBatteryController.clearLastPowerSaverStartView(); + } } @Override @@ -128,7 +133,7 @@ public class BatterySaverTile extends QSTileImpl implements if (getState().state == Tile.STATE_UNAVAILABLE) { return; } - mBatteryController.setPowerSaveMode(!mPowerSave); + mBatteryController.setPowerSaveMode(!mPowerSave, view); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java index 95a7316f7a587..ecaa28b0d4eb5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy; import android.annotation.Nullable; +import android.view.View; import com.android.systemui.Dumpable; import com.android.systemui.demomode.DemoMode; @@ -24,6 +25,7 @@ import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChang import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.ref.WeakReference; public interface BatteryController extends DemoMode, Dumpable, CallbackController { @@ -35,7 +37,32 @@ public interface BatteryController extends DemoMode, Dumpable, /** * Sets if the current device is in power save mode. */ - void setPowerSaveMode(boolean powerSave); + default void setPowerSaveMode(boolean powerSave) { + setPowerSaveMode(powerSave, null); + } + + /** + * Sets if the current device is in power save mode. + * + * Can pass the view that triggered the request. + */ + void setPowerSaveMode(boolean powerSave, @Nullable View view); + + /** + * Gets a reference to the last view used when called {@link #setPowerSaveMode}. + */ + @Nullable + default WeakReference getLastPowerSaverStartView() { + return null; + } + + /** + * Clears the last view used when called {@link #setPowerSaveMode}. + * + * Immediately after calling this, a call to {@link #getLastPowerSaverStartView()} should return + * {@code null}. + */ + default void clearLastPowerSaverStartView() {} /** * Returns {@code true} if the device is currently plugged in. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java index 9e2c478fbd695..1e71dea29eba8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java @@ -28,6 +28,7 @@ import android.os.Handler; import android.os.PowerManager; import android.os.PowerSaveState; import android.util.Log; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -45,8 +46,10 @@ import com.android.systemui.power.EnhancedEstimates; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; /** * Default implementation of a {@link BatteryController}. This controller monitors for battery @@ -85,6 +88,11 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC private Estimate mEstimate; private boolean mFetchingEstimate = false; + // Use AtomicReference because we may request it from a different thread + // Use WeakReference because we are keeping a reference to a View that's not as long lived + // as this controller. + private AtomicReference> mPowerSaverStartView = new AtomicReference<>(); + @VisibleForTesting public BatteryControllerImpl( Context context, @@ -141,10 +149,21 @@ public class BatteryControllerImpl extends BroadcastReceiver implements BatteryC } @Override - public void setPowerSaveMode(boolean powerSave) { + public void setPowerSaveMode(boolean powerSave, View view) { + if (powerSave) mPowerSaverStartView.set(new WeakReference<>(view)); BatterySaverUtils.setPowerSaveMode(mContext, powerSave, /*needFirstTimeWarning*/ true); } + @Override + public WeakReference getLastPowerSaverStartView() { + return mPowerSaverStartView.get(); + } + + @Override + public void clearLastPowerSaverStartView() { + mPowerSaverStartView.set(null); + } + @Override public void addCallback(@NonNull BatteryController.BatteryStateChangeCallback cb) { synchronized (mChangeCallbacks) { 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 a156820ad141f..1ffa9dd57aa9c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java @@ -25,29 +25,48 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.content.IntentFilter; import android.os.BatteryManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.UserHandle; import android.test.suitebuilder.annotation.SmallTest; - -import androidx.test.runner.AndroidJUnit4; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.View; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; +import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.NotificationChannels; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.lang.ref.WeakReference; @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper public class PowerNotificationWarningsTest extends SysuiTestCase { public static final String FORMATTED_45M = "0h 45m"; @@ -55,14 +74,34 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); private PowerNotificationWarnings mPowerNotificationWarnings; + @Mock + private BatteryController mBatteryController; + @Mock + private DialogLaunchAnimator mDialogLaunchAnimator; + @Mock + private View mView; + + private BroadcastReceiver mReceiver; + @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + Context wrapper = new ContextWrapper(mContext) { + @Override + public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, + IntentFilter filter, String broadcastPermission, Handler scheduler, int flags) { + mReceiver = receiver; + return null; + } + }; + // Test Instance. mContext.addMockSystemService(NotificationManager.class, mMockNotificationManager); ActivityStarter starter = mDependency.injectMockDependency(ActivityStarter.class); BroadcastSender broadcastSender = mDependency.injectMockDependency(BroadcastSender.class); - mPowerNotificationWarnings = new PowerNotificationWarnings(mContext, starter, - broadcastSender); + mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter, + broadcastSender, () -> mBatteryController, mDialogLaunchAnimator); BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1, BatteryManager.BATTERY_HEALTH_GOOD, 5, 15); mPowerNotificationWarnings.updateSnapshot(snapshot); @@ -168,4 +207,52 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { mPowerNotificationWarnings.mUsbHighTempDialog.dismiss(); } + + @Test + public void testDialogStartedFromLauncher_viewVisible() { + when(mBatteryController.getLastPowerSaverStartView()) + .thenReturn(new WeakReference<>(mView)); + when(mView.isAggregatedVisible()).thenReturn(true); + + Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION); + intent.putExtras(new Bundle()); + + mReceiver.onReceive(mContext, intent); + + verify(mDialogLaunchAnimator).showFromView(any(), eq(mView)); + + mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss(); + } + + @Test + public void testDialogStartedNotFromLauncher_viewNotVisible() { + when(mBatteryController.getLastPowerSaverStartView()) + .thenReturn(new WeakReference<>(mView)); + when(mView.isAggregatedVisible()).thenReturn(false); + + Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION); + intent.putExtras(new Bundle()); + + mReceiver.onReceive(mContext, intent); + + verify(mDialogLaunchAnimator, never()).showFromView(any(), any()); + + assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue(); + mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss(); + } + + @Test + public void testDialogShownNotFromLauncher() { + when(mBatteryController.getLastPowerSaverStartView()).thenReturn(null); + + Intent intent = new Intent(BatterySaverUtils.ACTION_SHOW_START_SAVER_CONFIRMATION); + intent.putExtras(new Bundle()); + + mReceiver.onReceive(mContext, intent); + + verify(mDialogLaunchAnimator, never()).showFromView(any(), any()); + + assertThat(mPowerNotificationWarnings.getSaverConfirmationDialog().isShowing()).isTrue(); + mPowerNotificationWarnings.getSaverConfirmationDialog().dismiss(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt index 1bf83513d4725..3d9205ee03540 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BatterySaverTileTest.kt @@ -21,6 +21,7 @@ import android.os.Handler import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper +import android.view.View import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.systemui.SysuiTestCase @@ -38,6 +39,9 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.`when` +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -63,6 +67,8 @@ class BatterySaverTileTest : SysuiTestCase() { private lateinit var qsLogger: QSLogger @Mock private lateinit var batteryController: BatteryController + @Mock + private lateinit var view: View private lateinit var secureSettings: SecureSettings private lateinit var testableLooper: TestableLooper private lateinit var tile: BatterySaverTile @@ -105,4 +111,26 @@ class BatterySaverTileTest : SysuiTestCase() { assertEquals(USER + 1, tile.mSetting.currentUser) } + + @Test + fun testClickingPowerSavePassesView() { + tile.onPowerSaveChanged(true) + tile.handleClick(view) + + tile.onPowerSaveChanged(false) + tile.handleClick(view) + + verify(batteryController).setPowerSaveMode(true, view) + verify(batteryController).setPowerSaveMode(false, view) + } + + @Test + fun testStopListeningClearsViewInController() { + clearInvocations(batteryController) + tile.handleSetListening(true) + verify(batteryController, never()).clearLastPowerSaverStartView() + + tile.handleSetListening(false) + verify(batteryController).clearLastPowerSaverStartView() + } } \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java index 2577dbdbb593c..b714df50106e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java @@ -18,6 +18,10 @@ package com.android.systemui.statusbar.policy; import static android.os.BatteryManager.EXTRA_PRESENT; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker; + import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -30,20 +34,24 @@ import android.os.PowerSaveState; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.View; +import com.android.dx.mockito.inline.extended.StaticInOrder; +import com.android.settingslib.fuelgauge.BatterySaverUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.power.EnhancedEstimates; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; - +import org.mockito.MockitoSession; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -53,11 +61,19 @@ public class BatteryControllerTest extends SysuiTestCase { @Mock private PowerManager mPowerManager; @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock private DemoModeController mDemoModeController; + @Mock private View mView; private BatteryControllerImpl mBatteryController; + private MockitoSession mMockitoSession; + @Before public void setUp() { MockitoAnnotations.initMocks(this); + mMockitoSession = mockitoSession() + .initMocks(this) + .mockStatic(BatterySaverUtils.class) + .startMocking(); + mBatteryController = new BatteryControllerImpl(getContext(), mock(EnhancedEstimates.class), mPowerManager, @@ -68,6 +84,11 @@ public class BatteryControllerTest extends SysuiTestCase { mBatteryController.init(); } + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + @Test public void testBatteryInitialized() { Assert.assertTrue(mBatteryController.mHasReceivedBattery); @@ -135,4 +156,33 @@ public class BatteryControllerTest extends SysuiTestCase { // THEN it is informed about the battery state verify(cb, atLeastOnce()).onBatteryUnknownStateChanged(true); } + + @Test + public void testBatteryUtilsCalledOnSetPowerSaveMode() { + mBatteryController.setPowerSaveMode(true, mView); + mBatteryController.setPowerSaveMode(false, mView); + + StaticInOrder inOrder = inOrder(staticMockMarker(BatterySaverUtils.class)); + inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), true, true)); + inOrder.verify(() -> BatterySaverUtils.setPowerSaveMode(getContext(), false, true)); + } + + @Test + public void testSaveViewReferenceWhenSettingPowerSaveMode() { + mBatteryController.setPowerSaveMode(false, mView); + + Assert.assertNull(mBatteryController.getLastPowerSaverStartView()); + + mBatteryController.setPowerSaveMode(true, mView); + + Assert.assertSame(mView, mBatteryController.getLastPowerSaverStartView().get()); + } + + @Test + public void testClearViewReference() { + mBatteryController.setPowerSaveMode(true, mView); + mBatteryController.clearLastPowerSaverStartView(); + + Assert.assertNull(mBatteryController.getLastPowerSaverStartView()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBatteryController.java b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBatteryController.java index 50c1e73f6aaca..9ca4db4c1843b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBatteryController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBatteryController.java @@ -16,6 +16,7 @@ package com.android.systemui.utils.leaks; import android.os.Bundle; import android.testing.LeakCheck; +import android.view.View; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; @@ -46,6 +47,11 @@ public class FakeBatteryController extends BaseLeakChecker