diff --git a/packages/SystemUI/res/values/lineage_config.xml b/packages/SystemUI/res/values/lineage_config.xml
index 79c2865567a74..ce3999cd0d48a 100644
--- a/packages/SystemUI/res/values/lineage_config.xml
+++ b/packages/SystemUI/res/values/lineage_config.xml
@@ -70,4 +70,9 @@
Possible values: 3, 3.1, 3.2, 4, 4.1, 4.2
-->
4.2
+
+
+ @*android:bool/config_enableBurnInProtection
+
+ 60
diff --git a/packages/SystemUI/res/values/lineage_dimens.xml b/packages/SystemUI/res/values/lineage_dimens.xml
index ca6e7db4c5b18..c75933439b6cc 100644
--- a/packages/SystemUI/res/values/lineage_dimens.xml
+++ b/packages/SystemUI/res/values/lineage_dimens.xml
@@ -24,4 +24,9 @@
48dp
+
+
+ 4dp
+
+ 4dp
diff --git a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt
index 5c6478ed08952..e0dcd87382dbe 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelper.kt
@@ -63,8 +63,8 @@ fun getBurnInScale(): Float {
* @param amplitude maximum value of the function
* @return a value between 0 and amplitude
*/
-private fun zigzag(x: Float, amplitude: Float, period: Float): Float {
+fun zigzag(x: Float, amplitude: Float, period: Float): Float {
val xprime = x % period / (period / 2)
val interpolationAmount = if (xprime <= 1) xprime else 2 - xprime
return MathUtils.lerp(0f, amplitude, interpolationAmount)
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
index 3b86d302ac0ad..bf23768341433 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarView.java
@@ -72,6 +72,7 @@
import com.android.systemui.navigationbar.buttons.NearestTouchFrame;
import com.android.systemui.navigationbar.buttons.RotationContextButton;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
+import com.android.systemui.navigationbar.gestural.NavigationHandle;
import com.android.systemui.recents.Recents;
import com.android.systemui.res.R;
import com.android.systemui.settings.DisplayTracker;
@@ -83,6 +84,7 @@
import com.android.systemui.statusbar.phone.AutoHideController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.LightBarTransitionsController;
+import com.android.systemui.statusbar.policy.Offset;
import com.android.wm.shell.back.BackAnimation;
import com.android.wm.shell.pip.Pip;
@@ -186,6 +188,9 @@ public class NavigationBarView extends FrameLayout {
private boolean mShowCursorKeys;
private boolean mImeVisible;
+ @Nullable
+ private ViewGroup mNavigationBarContents = null;
+
private class NavTransitionListener implements TransitionListener {
private boolean mBackTransitioning;
private boolean mHomeAppearing;
@@ -889,12 +894,31 @@ public void setAccessibilityButtonState(final boolean visible, final boolean lon
mContextualButtonGroup.setButtonVisibility(R.id.accessibility_button, visible);
}
+ public void offsetNavBar(Offset offset) {
+ if (isGesturalMode(mNavBarMode)) {
+ final NavigationHandle handle = (NavigationHandle) getHomeHandle().getCurrentView();
+ if (handle != null) {
+ handle.setTranslationY(offset.getY());
+ handle.invalidate();
+ }
+ return;
+ }
+ if (mNavigationBarContents == null) {
+ return;
+ }
+ mNavigationBarContents.setTranslationX(offset.getX());
+ mNavigationBarContents.setTranslationY(offset.getY());
+ invalidate();
+ }
+
@Override
public void onFinishInflate() {
super.onFinishInflate();
mNavigationInflaterView = findViewById(R.id.navigation_inflater);
mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
+ mNavigationBarContents = (ViewGroup) findViewById(R.id.nav_buttons);
+
updateOrientationViews();
reloadNavIcons();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 18e95781a32c2..58153cac928d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -218,6 +218,7 @@
import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneModule;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
+import com.android.systemui.statusbar.policy.BurnInProtectionController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -598,6 +599,8 @@ public int getId() {
private final SceneContainerFlags mSceneContainerFlags;
+ private final BurnInProtectionController mBurnInProtectionController;
+
/**
* Public constructor for CentralSurfaces.
*
@@ -709,7 +712,8 @@ public CentralSurfacesImpl(
UserTracker userTracker,
Provider fingerprintManager,
ActivityStarter activityStarter,
- SceneContainerFlags sceneContainerFlags
+ SceneContainerFlags sceneContainerFlags,
+ BurnInProtectionController burnInProtectionController
) {
mContext = context;
mNotificationsController = notificationsController;
@@ -805,6 +809,7 @@ public CentralSurfacesImpl(
mFingerprintManager = fingerprintManager;
mActivityStarter = activityStarter;
mSceneContainerFlags = sceneContainerFlags;
+ mBurnInProtectionController = burnInProtectionController;
mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
mStartingSurfaceOptional = startingSurfaceOptional;
@@ -1228,6 +1233,7 @@ protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) {
mShadeSurface.updateExpansionAndVisibility();
setBouncerShowingForStatusBarComponents(mBouncerShowing);
checkBarModes();
+ mBurnInProtectionController.setPhoneStatusBarView(mStatusBarView);
});
mStatusBarInitializer.initializeStatusBar();
@@ -1498,6 +1504,7 @@ protected void setUpDisableFlags(int state1, int state2) {
// Try to remove this.
protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result);
+ mBurnInProtectionController.setNavigationBarView(getNavigationBarView());
}
/**
@@ -2503,6 +2510,8 @@ public void onFinishedGoingToSleep() {
updateNotificationPanelTouchState();
getNotificationShadeWindowViewController().cancelCurrentTouch();
+ mBurnInProtectionController.stopShiftTimer();
+
if (mLaunchCameraOnFinishedGoingToSleep) {
mLaunchCameraOnFinishedGoingToSleep = false;
@@ -2668,6 +2677,7 @@ public void onFinishedWakingUp() {
}
}
updateScrimController();
+ mBurnInProtectionController.startShiftTimer();
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index b275b3f35fa4e..ac6ed5847cc80 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -22,6 +22,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.graphics.Insets;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
@@ -56,6 +57,7 @@
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
import com.android.systemui.statusbar.policy.Clock;
+import com.android.systemui.statusbar.policy.Offset;
import com.android.systemui.statusbar.window.StatusBarWindowController;
import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder;
import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel;
@@ -84,6 +86,8 @@ public class PhoneStatusBarView extends FrameLayout implements Callbacks {
private Gefingerpoken mTouchEventHandler;
private int mDensity;
private float mFontScale;
+ @Nullable
+ private ViewGroup mStatusBarContents = null;
/**
* Draw this many pixels into the left/right side of the cutout to optimally use the space
@@ -156,6 +160,7 @@ public void onFinishInflate() {
mBattery = findViewById(R.id.battery);
mClockController = new ClockController(getContext(), this);
mCutoutSpace = findViewById(R.id.cutout_space_view);
+ mStatusBarContents = (ViewGroup) findViewById(R.id.status_bar_contents);
updateResources();
}
@@ -221,6 +226,15 @@ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
return super.onApplyWindowInsets(insets);
}
+ public void offsetStatusBar(Offset offset) {
+ if (mStatusBarContents == null) {
+ return;
+ }
+ mStatusBarContents.setTranslationX(offset.getX());
+ mStatusBarContents.setTranslationY(offset.getY());
+ invalidate();
+ }
+
/**
* @return boolean indicating if we need to update the cutout location / margins
*/
@@ -336,7 +350,7 @@ private void updatePaddings() {
int statusBarPaddingStart = getResources().getDimensionPixelSize(
R.dimen.status_bar_padding_start);
- findViewById(R.id.status_bar_contents).setPaddingRelative(
+ mStatusBarContents.setPaddingRelative(
statusBarPaddingStart,
getResources().getDimensionPixelSize(R.dimen.status_bar_padding_top),
getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BurnInProtectionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BurnInProtectionController.kt
new file mode 100644
index 0000000000000..ba498dd57b88b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BurnInProtectionController.kt
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2017-2018 Paranoid Android
+ * Copyright (C) 2022 FlamingoOS 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.systemui.statusbar.policy
+
+import android.content.Context
+import android.util.Log
+
+import com.android.internal.policy.SystemBarUtils
+
+import com.android.systemui.R
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.doze.util.zigzag
+import com.android.systemui.navigationbar.NavigationBarView
+import com.android.systemui.navigationbar.NavigationModeController
+import com.android.systemui.shared.system.QuickStepContract.isGesturalMode
+import com.android.systemui.statusbar.phone.PhoneStatusBarView
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
+
+import javax.inject.Inject
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+
+private val TAG = BurnInProtectionController::class.simpleName
+
+@SysUISingleton
+class BurnInProtectionController @Inject constructor(
+ private val context: Context,
+ configurationController: ConfigurationController,
+ navigationModeController: NavigationModeController,
+) : NavigationModeController.ModeChangedListener,
+ ConfigurationListener {
+
+ private val coroutineScope = CoroutineScope(Dispatchers.Main)
+
+ private val shiftEnabled = context.resources.getBoolean(
+ R.bool.config_statusBarBurnInProtection)
+
+ private val shiftInterval = context.resources.getInteger(
+ R.integer.config_statusBarBurnInProtectionShiftInterval) * 1000L
+
+ private var navigationMode: Int = navigationModeController.addListener(this)
+
+ private var navigationBarView: NavigationBarView? = null
+ private var phoneStatusBarView: PhoneStatusBarView? = null
+
+ private var shiftJob: Job? = null
+ private var shiftCounter = 0
+
+ private var maxStatusBarOffsetX = 0
+ private var maxStatusBarOffsetY = 0
+ private var maxNavBarOffsetX = 0
+ private var maxNavBarOffsetY = 0
+
+ private var statusBarOffset = Offset.Zero
+ private var navBarOffset = Offset.Zero
+
+ init {
+ logD {
+ "shiftEnabled = $shiftEnabled, isGesturalMode = ${isGesturalMode()}"
+ }
+ configurationController.addCallback(this)
+ loadResources()
+ }
+
+ private fun loadResources() {
+ with(context.resources) {
+ maxStatusBarOffsetX = minOf(
+ getDimensionPixelSize(R.dimen.status_bar_padding_start),
+ getDimensionPixelSize(R.dimen.status_bar_padding_end),
+ getDimensionPixelSize(R.dimen.status_bar_offset_max_x)
+ ) / 2
+ maxStatusBarOffsetY = minOf(
+ SystemBarUtils.getStatusBarHeight(context) -
+ getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height_default),
+ getDimensionPixelSize(R.dimen.status_bar_offset_max_y)
+ ) / 2
+ }
+ calculateNavBarMaxOffset()
+ logD {
+ "maxStatusBarOffsetX = $maxStatusBarOffsetX, maxStatusBarOffsetY = $maxStatusBarOffsetY"
+ }
+ }
+
+ private fun calculateNavBarMaxOffset() {
+ with(context.resources) {
+ maxNavBarOffsetX = if (isGesturalMode()) {
+ 0
+ } else {
+ getDimensionPixelSize(R.dimen.floating_rotation_button_min_margin) / 4
+ }
+ maxNavBarOffsetY = if (isGesturalMode()) {
+ getDimensionPixelSize(R.dimen.navigation_handle_bottom) / 3
+ } else {
+ val frameHeight = getDimensionPixelSize(R.dimen.navigation_bar_height)
+ val buttonHeight = getDimensionPixelSize(R.dimen.navigation_icon_size)
+ (frameHeight - buttonHeight) / 3
+ }
+ }
+ logD {
+ "maxNavBarOffsetX = $maxNavBarOffsetX, maxNavBarOffsetY = $maxNavBarOffsetY"
+ }
+ }
+
+ fun setNavigationBarView(navigationBarView: NavigationBarView?) {
+ this.navigationBarView = navigationBarView
+ }
+
+ fun setPhoneStatusBarView(phoneStatusBarView: PhoneStatusBarView?) {
+ this.phoneStatusBarView = phoneStatusBarView
+ }
+
+ fun startShiftTimer() {
+ if (!shiftEnabled || (shiftJob?.isActive == true)) return
+ shiftJob = coroutineScope.launch {
+ while (isActive) {
+ val sbOffset = Offset(
+ getBurnInOffset(maxStatusBarOffsetX),
+ getBurnInOffset(maxStatusBarOffsetY)
+ )
+ val nbOffset = Offset(
+ getBurnInOffset(maxNavBarOffsetX),
+ getBurnInOffset(maxNavBarOffsetY)
+ )
+ logD {
+ "new offsets: sbOffset = $sbOffset, nbOffset = $nbOffset"
+ }
+ updateViews(sbOffset, nbOffset)
+ delay(shiftInterval)
+ shiftCounter++
+ }
+ }
+ logD {
+ "Started shift job"
+ }
+ }
+
+ private fun getBurnInOffset(maxOffset: Int): Int {
+ val amplitude = maxOffset.toFloat()
+ val period = amplitude * 2
+ val mult = if ((shiftCounter / period) % 2 == 0f) 1 else -1
+ return mult * Math.round(zigzag(shiftCounter.toFloat(), amplitude, period))
+ }
+
+ private fun updateViews(sbOffset: Offset, nbOffset: Offset) {
+ if (sbOffset != statusBarOffset) {
+ logD {
+ "Translating statusbar"
+ }
+ phoneStatusBarView?.offsetStatusBar(sbOffset)
+ statusBarOffset = sbOffset
+ }
+ if (nbOffset != navBarOffset) {
+ logD {
+ "Translating navbar"
+ }
+ navigationBarView?.offsetNavBar(nbOffset)
+ navBarOffset = nbOffset
+ }
+ }
+
+ fun stopShiftTimer() {
+ if (!shiftEnabled || (shiftJob?.isActive != true)) return
+ logD {
+ "Cancelling shift job"
+ }
+ coroutineScope.launch {
+ shiftJob?.cancelAndJoin()
+ updateViews(Offset.Zero, Offset.Zero)
+ logD {
+ "Cancelled shift job"
+ }
+ }
+ }
+
+ override fun onNavigationModeChanged(mode: Int) {
+ if (navigationMode == mode) return
+ navigationMode = mode
+ logD {
+ "onNavigationModeChanged: isGesturalMode = ${isGesturalMode()}"
+ }
+ calculateNavBarMaxOffset()
+ }
+
+ override fun onDensityOrFontScaleChanged() {
+ logD {
+ "onDensityOrFontScaleChanged"
+ }
+ loadResources()
+ }
+
+ private fun isGesturalMode() = isGesturalMode(navigationMode)
+}
+
+private inline fun logD(crossinline msg: () -> String) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, msg())
+ }
+}
+
+data class Offset(
+ val x: Int,
+ val y: Int
+) {
+ companion object {
+ val Zero = Offset(0, 0)
+ }
+}