Periodically jitter dream overlay to prevent burn-in.

Test: atest DreamOverlayContainerViewControllerTest
Bug: 213907279
Change-Id: Id43d8697c5ef032b07c6f5854445cb16f6c820b7
This commit is contained in:
Will
2022-01-12 16:54:35 -08:00
committed by William Leshner
parent ab4a170645
commit 8624015554
4 changed files with 104 additions and 3 deletions

View File

@@ -738,4 +738,6 @@
<!-- Class for the communal source connector to be used -->
<string name="config_communalSourceConnector" translatable="false"></string>
<!-- How often in milliseconds to jitter the dream overlay in order to avoid burn-in. -->
<integer name="config_dreamOverlayBurnInProtectionUpdateIntervalMillis">500</integer>
</resources>

View File

@@ -16,8 +16,11 @@
package com.android.systemui.dreams;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.Handler;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
@@ -26,6 +29,7 @@ import androidx.constraintlayout.widget.ConstraintLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.dreams.dagger.DreamOverlayModule;
import com.android.systemui.util.ViewController;
@@ -47,6 +51,15 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
// the space into which widgets are placed.
private final ViewGroup mDreamOverlayContentView;
// The maximum translation offset to apply to the overlay container to avoid screen burn-in.
private final int mMaxBurnInOffset;
// The interval in milliseconds between burn-in protection updates.
private final long mBurnInProtectionUpdateInterval;
// Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates).
private final Handler mHandler;
// A hook into the internal inset calculation where we declare the overlays as the only
// touchable regions.
private final ViewTreeObserver.OnComputeInternalInsetsListener
@@ -81,13 +94,21 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
public DreamOverlayContainerViewController(
DreamOverlayContainerView containerView,
@Named(DreamOverlayModule.DREAM_OVERLAY_CONTENT_VIEW) ViewGroup contentView,
DreamOverlayStatusBarViewController statusBarViewController) {
DreamOverlayStatusBarViewController statusBarViewController,
@Main Handler handler,
@Named(DreamOverlayModule.MAX_BURN_IN_OFFSET) int maxBurnInOffset,
@Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long
burnInProtectionUpdateInterval) {
super(containerView);
mDreamOverlayContentView = contentView;
mStatusBarViewController = statusBarViewController;
mDreamOverlayNotificationsDragAreaHeight =
mView.getResources().getDimensionPixelSize(
R.dimen.dream_overlay_notifications_drag_area_height);
mHandler = handler;
mMaxBurnInOffset = maxBurnInOffset;
mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval;
}
@Override
@@ -99,10 +120,12 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
protected void onViewAttached() {
mView.getViewTreeObserver()
.addOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
}
@Override
protected void onViewDetached() {
mHandler.removeCallbacks(this::updateBurnInOffsets);
mView.getViewTreeObserver()
.removeOnComputeInternalInsetsListener(mOnComputeInternalInsetsListener);
}
@@ -123,4 +146,15 @@ public class DreamOverlayContainerViewController extends ViewController<DreamOve
int getDreamOverlayNotificationsDragAreaHeight() {
return mDreamOverlayNotificationsDragAreaHeight;
}
private void updateBurnInOffsets() {
// These translation values change slowly, and the set translation methods are idempotent,
// so no translation occurs when the values don't change.
mView.setTranslationX(getBurnInOffset(mMaxBurnInOffset * 2, true)
- mMaxBurnInOffset);
mView.setTranslationY(getBurnInOffset(mMaxBurnInOffset * 2, false)
- mMaxBurnInOffset);
mHandler.postDelayed(this::updateBurnInOffsets, mBurnInProtectionUpdateInterval);
}
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.dreams.dagger;
import android.content.ContentResolver;
import android.content.res.Resources;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.ViewGroup;
@@ -45,6 +46,9 @@ public abstract class DreamOverlayModule {
public static final String DREAM_OVERLAY_BATTERY_CONTROLLER =
"dream_overlay_battery_controller";
public static final String DREAM_OVERLAY_CONTENT_VIEW = "dream_overlay_content_view";
public static final String MAX_BURN_IN_OFFSET = "max_burn_in_offset";
public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL =
"burn_in_protection_update_interval";
/** */
@Provides
@@ -104,4 +108,20 @@ public abstract class DreamOverlayModule {
contentResolver,
batteryController);
}
/** */
@Provides
@DreamOverlayComponent.DreamOverlayScope
@Named(MAX_BURN_IN_OFFSET)
static int providesMaxBurnInOffset(@Main Resources resources) {
return resources.getDimensionPixelSize(R.dimen.default_burn_in_prevention_offset);
}
/** */
@Provides
@Named(BURN_IN_PROTECTION_UPDATE_INTERVAL)
static long providesBurnInProtectionUpdateInterval(@Main Resources resources) {
return resources.getInteger(
R.integer.config_dreamOverlayBurnInProtectionUpdateIntervalMillis);
}
}

View File

@@ -19,10 +19,13 @@ package com.android.systemui.dreams;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.res.Resources;
import android.os.Handler;
import android.testing.AndroidTestingRunner;
import android.view.View;
import android.view.ViewGroup;
@@ -45,6 +48,8 @@ import org.mockito.MockitoAnnotations;
@RunWith(AndroidTestingRunner.class)
public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
private static final int DREAM_OVERLAY_NOTIFICATIONS_DRAG_AREA_HEIGHT = 100;
private static final int MAX_BURN_IN_OFFSET = 20;
private static final long BURN_IN_PROTECTION_UPDATE_INTERVAL = 10;
@Mock
Resources mResources;
@@ -61,6 +66,9 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
@Mock
ViewGroup mDreamOverlayContentView;
@Mock
Handler mHandler;
DreamOverlayContainerViewController mController;
@Before
@@ -74,8 +82,12 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
when(mDreamOverlayContainerView.getViewTreeObserver()).thenReturn(mViewTreeObserver);
mController = new DreamOverlayContainerViewController(
mDreamOverlayContainerView, mDreamOverlayContentView,
mDreamOverlayStatusBarViewController);
mDreamOverlayContainerView,
mDreamOverlayContentView,
mDreamOverlayStatusBarViewController,
mHandler,
MAX_BURN_IN_OFFSET,
BURN_IN_PROTECTION_UPDATE_INTERVAL);
}
@Test
@@ -129,4 +141,37 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
computeInsetsListenerCapture.getValue().onComputeInternalInsets(info);
assertNotNull(info.touchableRegion);
}
@Test
public void testBurnInProtectionStartsWhenContentViewAttached() {
mController.onViewAttached();
verify(mHandler).postDelayed(any(Runnable.class), eq(BURN_IN_PROTECTION_UPDATE_INTERVAL));
}
@Test
public void testBurnInProtectionStopsWhenContentViewDetached() {
mController.onViewDetached();
verify(mHandler).removeCallbacks(any(Runnable.class));
}
@Test
public void testBurnInProtectionUpdatesPeriodically() {
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
mController.onViewAttached();
verify(mHandler).postDelayed(
runnableCaptor.capture(), eq(BURN_IN_PROTECTION_UPDATE_INTERVAL));
runnableCaptor.getValue().run();
verify(mDreamOverlayContainerView).setTranslationX(anyFloat());
verify(mDreamOverlayContainerView).setTranslationY(anyFloat());
}
@Test
public void testBurnInProtectionReschedulesUpdate() {
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
mController.onViewAttached();
verify(mHandler).postDelayed(
runnableCaptor.capture(), eq(BURN_IN_PROTECTION_UPDATE_INTERVAL));
runnableCaptor.getValue().run();
verify(mHandler).postDelayed(runnableCaptor.getValue(), BURN_IN_PROTECTION_UPDATE_INTERVAL);
}
}