/* * Copyright (C) 2015 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.server.policy; import android.animation.Animator; import android.animation.ValueAnimator; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManagerInternal; import android.os.SystemClock; import android.util.Slog; import android.view.Display; import android.view.animation.LinearInterpolator; import com.android.server.LocalServices; import java.io.PrintWriter; import java.util.concurrent.TimeUnit; public class BurnInProtectionHelper implements DisplayManager.DisplayListener, Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { private static final String TAG = "BurnInProtection"; // Default value when max burnin radius is not set. public static final int BURN_IN_MAX_RADIUS_DEFAULT = -1; private static final long BURNIN_PROTECTION_WAKEUP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1); private static final long BURNIN_PROTECTION_MINIMAL_INTERVAL_MS = TimeUnit.SECONDS.toMillis(10); private static final boolean DEBUG = false; private static final String ACTION_BURN_IN_PROTECTION = "android.internal.policy.action.BURN_IN_PROTECTION"; private static final int BURN_IN_SHIFT_STEP = 2; private static final long CENTERING_ANIMATION_DURATION_MS = 100; private final ValueAnimator mCenteringAnimator; private boolean mBurnInProtectionActive; private boolean mFirstUpdate; private final int mMinHorizontalBurnInOffset; private final int mMaxHorizontalBurnInOffset; private final int mMinVerticalBurnInOffset; private final int mMaxVerticalBurnInOffset; private final int mBurnInRadiusMaxSquared; private int mLastBurnInXOffset = 0; /* 1 means increasing, -1 means decreasing */ private int mXOffsetDirection = 1; private int mLastBurnInYOffset = 0; /* 1 means increasing, -1 means decreasing */ private int mYOffsetDirection = 1; private int mAppliedBurnInXOffset = 0; private int mAppliedBurnInYOffset = 0; private final AlarmManager mAlarmManager; private final PendingIntent mBurnInProtectionIntent; private final DisplayManagerInternal mDisplayManagerInternal; private final Display mDisplay; private BroadcastReceiver mBurnInProtectionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) { Slog.d(TAG, "onReceive " + intent); } updateBurnInProtection(); } }; public BurnInProtectionHelper(Context context, int minHorizontalOffset, int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset, int maxOffsetRadius) { mMinHorizontalBurnInOffset = minHorizontalOffset; mMaxHorizontalBurnInOffset = maxHorizontalOffset; mMinVerticalBurnInOffset = minVerticalOffset; mMaxVerticalBurnInOffset = maxVerticalOffset; if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) { mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius; } else { mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT; } mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); context.registerReceiver(mBurnInProtectionReceiver, new IntentFilter(ACTION_BURN_IN_PROTECTION)); Intent intent = new Intent(ACTION_BURN_IN_PROTECTION); intent.setPackage(context.getPackageName()); intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); mBurnInProtectionIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); mDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); displayManager.registerDisplayListener(this, null /* handler */); mCenteringAnimator = ValueAnimator.ofFloat(1f, 0f); mCenteringAnimator.setDuration(CENTERING_ANIMATION_DURATION_MS); mCenteringAnimator.setInterpolator(new LinearInterpolator()); mCenteringAnimator.addListener(this); mCenteringAnimator.addUpdateListener(this); } public void startBurnInProtection() { if (!mBurnInProtectionActive) { mBurnInProtectionActive = true; mFirstUpdate = true; mCenteringAnimator.cancel(); updateBurnInProtection(); } } private void updateBurnInProtection() { if (mBurnInProtectionActive) { // We don't want to adjust offsets immediately after the device goes into ambient mode. // Instead, we want to wait until it's more likely that the user is not observing the // screen anymore. if (mFirstUpdate) { mFirstUpdate = false; } else { adjustOffsets(); mAppliedBurnInXOffset = mLastBurnInXOffset; mAppliedBurnInYOffset = mLastBurnInYOffset; mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), mLastBurnInXOffset, mLastBurnInYOffset); } // We use currentTimeMillis to compute the next wakeup time since we want to wake up at // the same time as we wake up to update ambient mode to minimize power consumption. // However, we use elapsedRealtime to schedule the alarm so that setting the time can't // disable burn-in protection for extended periods. final long nowWall = System.currentTimeMillis(); final long nowElapsed = SystemClock.elapsedRealtime(); // Next adjustment at least ten seconds in the future. long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS; // And aligned to the minute. nextWall = nextWall - nextWall % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS; // Use elapsed real time that is adjusted to full minute on wall clock. final long nextElapsed = nowElapsed + (nextWall - nowWall); if (DEBUG) { Slog.d(TAG, "scheduling next wake-up, now wall time " + nowWall + ", next wall: " + nextWall + ", now elapsed: " + nowElapsed + ", next elapsed: " + nextElapsed); } mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextElapsed, mBurnInProtectionIntent); } else { mAlarmManager.cancel(mBurnInProtectionIntent); mCenteringAnimator.start(); } } public void cancelBurnInProtection() { if (mBurnInProtectionActive) { mBurnInProtectionActive = false; updateBurnInProtection(); } } /** * Gently shifts current burn-in offsets, minimizing the change for the user. * * Shifts are applied in following fashion: * 1) shift horizontally from minimum to the maximum; * 2) shift vertically by one from minimum to the maximum; * 3) shift horizontally from maximum to the minimum; * 4) shift vertically by one from minimum to the maximum. * 5) if you reach the maximum vertically, start shifting back by one from maximum to minimum. * * On top of that, stay within specified radius. If the shift distance from the center is * higher than the radius, skip these values and go the next position that is within the radius. */ private void adjustOffsets() { do { // By default, let's just shift the X offset. final int xChange = mXOffsetDirection * BURN_IN_SHIFT_STEP; mLastBurnInXOffset += xChange; if (mLastBurnInXOffset > mMaxHorizontalBurnInOffset || mLastBurnInXOffset < mMinHorizontalBurnInOffset) { // Whoops, we went too far horizontally. Let's retract.. mLastBurnInXOffset -= xChange; // change horizontal direction.. mXOffsetDirection *= -1; // and let's shift the Y offset. final int yChange = mYOffsetDirection * BURN_IN_SHIFT_STEP; mLastBurnInYOffset += yChange; if (mLastBurnInYOffset > mMaxVerticalBurnInOffset || mLastBurnInYOffset < mMinVerticalBurnInOffset) { // Whoops, we went to far vertically. Let's retract.. mLastBurnInYOffset -= yChange; // and change vertical direction. mYOffsetDirection *= -1; } } // If we are outside of the radius, let's try again. } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT && mLastBurnInXOffset * mLastBurnInXOffset + mLastBurnInYOffset * mLastBurnInYOffset > mBurnInRadiusMaxSquared); } public void dump(String prefix, PrintWriter pw) { pw.println(prefix + TAG); prefix += " "; pw.println(prefix + "mBurnInProtectionActive=" + mBurnInProtectionActive); pw.println(prefix + "mHorizontalBurnInOffsetsBounds=(" + mMinHorizontalBurnInOffset + ", " + mMaxHorizontalBurnInOffset + ")"); pw.println(prefix + "mVerticalBurnInOffsetsBounds=(" + mMinVerticalBurnInOffset + ", " + mMaxVerticalBurnInOffset + ")"); pw.println(prefix + "mBurnInRadiusMaxSquared=" + mBurnInRadiusMaxSquared); pw.println(prefix + "mLastBurnInOffset=(" + mLastBurnInXOffset + ", " + mLastBurnInYOffset + ")"); pw.println(prefix + "mOfsetChangeDirections=(" + mXOffsetDirection + ", " + mYOffsetDirection + ")"); } @Override public void onDisplayAdded(int i) { } @Override public void onDisplayRemoved(int i) { } @Override public void onDisplayChanged(int displayId) { if (displayId == mDisplay.getDisplayId()) { if (mDisplay.getState() == Display.STATE_DOZE || mDisplay.getState() == Display.STATE_DOZE_SUSPEND) { startBurnInProtection(); } else { cancelBurnInProtection(); } } } @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { if (animator == mCenteringAnimator && !mBurnInProtectionActive) { mAppliedBurnInXOffset = 0; mAppliedBurnInYOffset = 0; // No matter how the animation finishes, we want to zero the offsets. mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 0, 0); } } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } @Override public void onAnimationUpdate(ValueAnimator valueAnimator) { if (!mBurnInProtectionActive) { final float value = (Float) valueAnimator.getAnimatedValue(); mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), (int) (mAppliedBurnInXOffset * value), (int) (mAppliedBurnInYOffset * value)); } } }