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