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