| /* |
| * 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_FIRST_WAKEUP_INTERVAL_MS = |
| TimeUnit.MINUTES.toMillis(1); |
| private static final long BURNIN_PROTECTION_SUBSEQUENT_WAKEUP_INTERVAL_MS = |
| TimeUnit.MINUTES.toMillis(2); |
| 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. |
| final long interval = mFirstUpdate |
| ? BURNIN_PROTECTION_FIRST_WAKEUP_INTERVAL_MS |
| : BURNIN_PROTECTION_SUBSEQUENT_WAKEUP_INTERVAL_MS; |
| 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 % interval)) + interval; |
| // 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 |
| || mDisplay.getState() == Display.STATE_ON_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)); |
| } |
| } |
| } |