1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.server.policy; 18 19 import android.animation.Animator; 20 import android.animation.ValueAnimator; 21 import android.app.AlarmManager; 22 import android.app.PendingIntent; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.hardware.display.DisplayManager; 28 import android.hardware.display.DisplayManagerInternal; 29 import android.os.SystemClock; 30 import android.util.Slog; 31 import android.view.Display; 32 import android.view.animation.LinearInterpolator; 33 34 import com.android.server.LocalServices; 35 36 import java.io.PrintWriter; 37 import java.util.concurrent.TimeUnit; 38 39 public class BurnInProtectionHelper implements DisplayManager.DisplayListener, 40 Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { 41 private static final String TAG = "BurnInProtection"; 42 43 // Default value when max burnin radius is not set. 44 public static final int BURN_IN_MAX_RADIUS_DEFAULT = -1; 45 46 private static final long BURNIN_PROTECTION_WAKEUP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1); 47 private static final long BURNIN_PROTECTION_MINIMAL_INTERVAL_MS = TimeUnit.SECONDS.toMillis(10); 48 49 private static final boolean DEBUG = false; 50 51 private static final String ACTION_BURN_IN_PROTECTION = 52 "android.internal.policy.action.BURN_IN_PROTECTION"; 53 54 private static final int BURN_IN_SHIFT_STEP = 2; 55 private static final long CENTERING_ANIMATION_DURATION_MS = 100; 56 private final ValueAnimator mCenteringAnimator; 57 58 private boolean mBurnInProtectionActive; 59 private boolean mFirstUpdate; 60 61 private final int mMinHorizontalBurnInOffset; 62 private final int mMaxHorizontalBurnInOffset; 63 private final int mMinVerticalBurnInOffset; 64 private final int mMaxVerticalBurnInOffset; 65 66 private final int mBurnInRadiusMaxSquared; 67 68 private int mLastBurnInXOffset = 0; 69 /* 1 means increasing, -1 means decreasing */ 70 private int mXOffsetDirection = 1; 71 private int mLastBurnInYOffset = 0; 72 /* 1 means increasing, -1 means decreasing */ 73 private int mYOffsetDirection = 1; 74 75 private int mAppliedBurnInXOffset = 0; 76 private int mAppliedBurnInYOffset = 0; 77 78 private final AlarmManager mAlarmManager; 79 private final PendingIntent mBurnInProtectionIntent; 80 private final DisplayManagerInternal mDisplayManagerInternal; 81 private final Display mDisplay; 82 83 private BroadcastReceiver mBurnInProtectionReceiver = new BroadcastReceiver() { 84 @Override 85 public void onReceive(Context context, Intent intent) { 86 if (DEBUG) { 87 Slog.d(TAG, "onReceive " + intent); 88 } 89 updateBurnInProtection(); 90 } 91 }; 92 93 public BurnInProtectionHelper(Context context, int minHorizontalOffset, 94 int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset, 95 int maxOffsetRadius) { 96 mMinHorizontalBurnInOffset = minHorizontalOffset; 97 mMaxHorizontalBurnInOffset = maxHorizontalOffset; 98 mMinVerticalBurnInOffset = minVerticalOffset; 99 mMaxVerticalBurnInOffset = maxVerticalOffset; 100 if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) { 101 mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius; 102 } else { 103 mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT; 104 } 105 106 mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); 107 mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 108 context.registerReceiver(mBurnInProtectionReceiver, 109 new IntentFilter(ACTION_BURN_IN_PROTECTION)); 110 Intent intent = new Intent(ACTION_BURN_IN_PROTECTION); 111 intent.setPackage(context.getPackageName()); 112 intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); 113 mBurnInProtectionIntent = PendingIntent.getBroadcast(context, 0, 114 intent, PendingIntent.FLAG_UPDATE_CURRENT); 115 DisplayManager displayManager = 116 (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); 117 mDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); 118 displayManager.registerDisplayListener(this, null /* handler */); 119 120 mCenteringAnimator = ValueAnimator.ofFloat(1f, 0f); 121 mCenteringAnimator.setDuration(CENTERING_ANIMATION_DURATION_MS); 122 mCenteringAnimator.setInterpolator(new LinearInterpolator()); 123 mCenteringAnimator.addListener(this); 124 mCenteringAnimator.addUpdateListener(this); 125 } 126 127 public void startBurnInProtection() { 128 if (!mBurnInProtectionActive) { 129 mBurnInProtectionActive = true; 130 mFirstUpdate = true; 131 mCenteringAnimator.cancel(); 132 updateBurnInProtection(); 133 } 134 } 135 136 private void updateBurnInProtection() { 137 if (mBurnInProtectionActive) { 138 // We don't want to adjust offsets immediately after the device goes into ambient mode. 139 // Instead, we want to wait until it's more likely that the user is not observing the 140 // screen anymore. 141 if (mFirstUpdate) { 142 mFirstUpdate = false; 143 } else { 144 adjustOffsets(); 145 mAppliedBurnInXOffset = mLastBurnInXOffset; 146 mAppliedBurnInYOffset = mLastBurnInYOffset; 147 mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 148 mLastBurnInXOffset, mLastBurnInYOffset); 149 } 150 // We use currentTimeMillis to compute the next wakeup time since we want to wake up at 151 // the same time as we wake up to update ambient mode to minimize power consumption. 152 // However, we use elapsedRealtime to schedule the alarm so that setting the time can't 153 // disable burn-in protection for extended periods. 154 final long nowWall = System.currentTimeMillis(); 155 final long nowElapsed = SystemClock.elapsedRealtime(); 156 // Next adjustment at least ten seconds in the future. 157 long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS; 158 // And aligned to the minute. 159 nextWall = nextWall - nextWall % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS 160 + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS; 161 // Use elapsed real time that is adjusted to full minute on wall clock. 162 final long nextElapsed = nowElapsed + (nextWall - nowWall); 163 if (DEBUG) { 164 Slog.d(TAG, "scheduling next wake-up, now wall time " + nowWall 165 + ", next wall: " + nextWall + ", now elapsed: " + nowElapsed 166 + ", next elapsed: " + nextElapsed); 167 } 168 mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextElapsed, 169 mBurnInProtectionIntent); 170 } else { 171 mAlarmManager.cancel(mBurnInProtectionIntent); 172 mCenteringAnimator.start(); 173 } 174 } 175 176 public void cancelBurnInProtection() { 177 if (mBurnInProtectionActive) { 178 mBurnInProtectionActive = false; 179 updateBurnInProtection(); 180 } 181 } 182 183 /** 184 * Gently shifts current burn-in offsets, minimizing the change for the user. 185 * 186 * Shifts are applied in following fashion: 187 * 1) shift horizontally from minimum to the maximum; 188 * 2) shift vertically by one from minimum to the maximum; 189 * 3) shift horizontally from maximum to the minimum; 190 * 4) shift vertically by one from minimum to the maximum. 191 * 5) if you reach the maximum vertically, start shifting back by one from maximum to minimum. 192 * 193 * On top of that, stay within specified radius. If the shift distance from the center is 194 * higher than the radius, skip these values and go the next position that is within the radius. 195 */ 196 private void adjustOffsets() { 197 do { 198 // By default, let's just shift the X offset. 199 final int xChange = mXOffsetDirection * BURN_IN_SHIFT_STEP; 200 mLastBurnInXOffset += xChange; 201 if (mLastBurnInXOffset > mMaxHorizontalBurnInOffset 202 || mLastBurnInXOffset < mMinHorizontalBurnInOffset) { 203 // Whoops, we went too far horizontally. Let's retract.. 204 mLastBurnInXOffset -= xChange; 205 // change horizontal direction.. 206 mXOffsetDirection *= -1; 207 // and let's shift the Y offset. 208 final int yChange = mYOffsetDirection * BURN_IN_SHIFT_STEP; 209 mLastBurnInYOffset += yChange; 210 if (mLastBurnInYOffset > mMaxVerticalBurnInOffset 211 || mLastBurnInYOffset < mMinVerticalBurnInOffset) { 212 // Whoops, we went to far vertically. Let's retract.. 213 mLastBurnInYOffset -= yChange; 214 // and change vertical direction. 215 mYOffsetDirection *= -1; 216 } 217 } 218 // If we are outside of the radius, let's try again. 219 } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT 220 && mLastBurnInXOffset * mLastBurnInXOffset + mLastBurnInYOffset * mLastBurnInYOffset 221 > mBurnInRadiusMaxSquared); 222 } 223 224 public void dump(String prefix, PrintWriter pw) { 225 pw.println(prefix + TAG); 226 prefix += " "; 227 pw.println(prefix + "mBurnInProtectionActive=" + mBurnInProtectionActive); 228 pw.println(prefix + "mHorizontalBurnInOffsetsBounds=(" + mMinHorizontalBurnInOffset + ", " 229 + mMaxHorizontalBurnInOffset + ")"); 230 pw.println(prefix + "mVerticalBurnInOffsetsBounds=(" + mMinVerticalBurnInOffset + ", " 231 + mMaxVerticalBurnInOffset + ")"); 232 pw.println(prefix + "mBurnInRadiusMaxSquared=" + mBurnInRadiusMaxSquared); 233 pw.println(prefix + "mLastBurnInOffset=(" + mLastBurnInXOffset + ", " 234 + mLastBurnInYOffset + ")"); 235 pw.println(prefix + "mOfsetChangeDirections=(" + mXOffsetDirection + ", " 236 + mYOffsetDirection + ")"); 237 } 238 239 @Override 240 public void onDisplayAdded(int i) { 241 } 242 243 @Override 244 public void onDisplayRemoved(int i) { 245 } 246 247 @Override 248 public void onDisplayChanged(int displayId) { 249 if (displayId == mDisplay.getDisplayId()) { 250 if (mDisplay.getState() == Display.STATE_DOZE 251 || mDisplay.getState() == Display.STATE_DOZE_SUSPEND) { 252 startBurnInProtection(); 253 } else { 254 cancelBurnInProtection(); 255 } 256 } 257 } 258 259 @Override 260 public void onAnimationStart(Animator animator) { 261 } 262 263 @Override 264 public void onAnimationEnd(Animator animator) { 265 if (animator == mCenteringAnimator && !mBurnInProtectionActive) { 266 mAppliedBurnInXOffset = 0; 267 mAppliedBurnInYOffset = 0; 268 // No matter how the animation finishes, we want to zero the offsets. 269 mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 0, 0); 270 } 271 } 272 273 @Override 274 public void onAnimationCancel(Animator animator) { 275 } 276 277 @Override 278 public void onAnimationRepeat(Animator animator) { 279 } 280 281 @Override 282 public void onAnimationUpdate(ValueAnimator valueAnimator) { 283 if (!mBurnInProtectionActive) { 284 final float value = (Float) valueAnimator.getAnimatedValue(); 285 mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 286 (int) (mAppliedBurnInXOffset * value), (int) (mAppliedBurnInYOffset * value)); 287 } 288 } 289 } 290