BurnInProtectionHelper.java revision 6bc51303b53ad96d63a078c57fadbd1dbc0ed826
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 17package com.android.server.policy; 18 19import android.animation.Animator; 20import android.animation.ValueAnimator; 21import android.app.AlarmManager; 22import android.app.PendingIntent; 23import android.content.BroadcastReceiver; 24import android.content.Context; 25import android.content.Intent; 26import android.content.IntentFilter; 27import android.hardware.display.DisplayManager; 28import android.hardware.display.DisplayManagerInternal; 29import android.os.SystemClock; 30import android.view.Display; 31import android.view.animation.LinearInterpolator; 32 33import com.android.server.LocalServices; 34 35import java.io.PrintWriter; 36import java.util.concurrent.TimeUnit; 37 38public class BurnInProtectionHelper implements DisplayManager.DisplayListener, 39 Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { 40 private static final String TAG = "BurnInProtection"; 41 42 // Default value when max burnin radius is not set. 43 public static final int BURN_IN_MAX_RADIUS_DEFAULT = -1; 44 45 private static final long BURNIN_PROTECTION_WAKEUP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1); 46 private static final long BURNIN_PROTECTION_MINIMAL_INTERVAL_MS = TimeUnit.SECONDS.toMillis(10); 47 48 private static final String ACTION_BURN_IN_PROTECTION = 49 "android.internal.policy.action.BURN_IN_PROTECTION"; 50 51 private static final int BURN_IN_SHIFT_STEP = 2; 52 private static final long CENTERING_ANIMATION_DURATION_MS = 100; 53 private final ValueAnimator mCenteringAnimator; 54 55 private boolean mBurnInProtectionActive; 56 private boolean mFirstUpdate; 57 58 private final int mMinHorizontalBurnInOffset; 59 private final int mMaxHorizontalBurnInOffset; 60 private final int mMinVerticalBurnInOffset; 61 private final int mMaxVerticalBurnInOffset; 62 63 private final int mBurnInRadiusMaxSquared; 64 65 private int mLastBurnInXOffset = 0; 66 /* 1 means increasing, -1 means decreasing */ 67 private int mXOffsetDirection = 1; 68 private int mLastBurnInYOffset = 0; 69 /* 1 means increasing, -1 means decreasing */ 70 private int mYOffsetDirection = 1; 71 72 private final AlarmManager mAlarmManager; 73 private final PendingIntent mBurnInProtectionIntent; 74 private final DisplayManagerInternal mDisplayManagerInternal; 75 private final Display mDisplay; 76 77 private BroadcastReceiver mBurnInProtectionReceiver = new BroadcastReceiver() { 78 @Override 79 public void onReceive(Context context, Intent intent) { 80 updateBurnInProtection(); 81 } 82 }; 83 84 public BurnInProtectionHelper(Context context, int minHorizontalOffset, 85 int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset, 86 int maxOffsetRadius) { 87 mMinHorizontalBurnInOffset = minHorizontalOffset; 88 mMaxHorizontalBurnInOffset = maxHorizontalOffset; 89 mMinVerticalBurnInOffset = minVerticalOffset; 90 mMaxVerticalBurnInOffset = maxVerticalOffset; 91 if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) { 92 mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius; 93 } else { 94 mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT; 95 } 96 97 mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); 98 mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 99 context.registerReceiver(mBurnInProtectionReceiver, 100 new IntentFilter(ACTION_BURN_IN_PROTECTION)); 101 Intent intent = new Intent(ACTION_BURN_IN_PROTECTION); 102 intent.setPackage(context.getPackageName()); 103 intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); 104 mBurnInProtectionIntent = PendingIntent.getBroadcast(context, 0, 105 intent, PendingIntent.FLAG_UPDATE_CURRENT); 106 DisplayManager displayManager = 107 (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); 108 mDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY); 109 displayManager.registerDisplayListener(this, null /* handler */); 110 111 mCenteringAnimator = ValueAnimator.ofFloat(1f, 0f); 112 mCenteringAnimator.setDuration(CENTERING_ANIMATION_DURATION_MS); 113 mCenteringAnimator.setInterpolator(new LinearInterpolator()); 114 mCenteringAnimator.addListener(this); 115 mCenteringAnimator.addUpdateListener(this); 116 } 117 118 public void startBurnInProtection() { 119 if (!mBurnInProtectionActive) { 120 mBurnInProtectionActive = true; 121 mFirstUpdate = true; 122 mCenteringAnimator.cancel(); 123 updateBurnInProtection(); 124 } 125 } 126 127 private void updateBurnInProtection() { 128 if (mBurnInProtectionActive) { 129 // We don't want to adjust offsets immediately after the device goes into ambient mode. 130 // Instead, we want to wait until it's more likely that the user is not observing the 131 // screen anymore. 132 if (mFirstUpdate) { 133 mFirstUpdate = false; 134 } else { 135 adjustOffsets(); 136 mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 137 mLastBurnInXOffset, mLastBurnInYOffset); 138 } 139 // Next adjustment at least ten seconds in the future. 140 long next = SystemClock.elapsedRealtime() + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS; 141 // And aligned to the minute. 142 next = next - next % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS 143 + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS; 144 mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mBurnInProtectionIntent); 145 } else { 146 mAlarmManager.cancel(mBurnInProtectionIntent); 147 mCenteringAnimator.start(); 148 } 149 } 150 151 public void cancelBurnInProtection() { 152 if (mBurnInProtectionActive) { 153 mBurnInProtectionActive = false; 154 updateBurnInProtection(); 155 } 156 } 157 158 /** 159 * Gently shifts current burn-in offsets, minimizing the change for the user. 160 * 161 * Shifts are applied in following fashion: 162 * 1) shift horizontally from minimum to the maximum; 163 * 2) shift vertically by one from minimum to the maximum; 164 * 3) shift horizontally from maximum to the minimum; 165 * 4) shift vertically by one from minimum to the maximum. 166 * 5) if you reach the maximum vertically, start shifting back by one from maximum to minimum. 167 * 168 * On top of that, stay within specified radius. If the shift distance from the center is 169 * higher than the radius, skip these values and go the next position that is within the radius. 170 */ 171 private void adjustOffsets() { 172 do { 173 // By default, let's just shift the X offset. 174 final int xChange = mXOffsetDirection * BURN_IN_SHIFT_STEP; 175 mLastBurnInXOffset += xChange; 176 if (mLastBurnInXOffset > mMaxHorizontalBurnInOffset 177 || mLastBurnInXOffset < mMinHorizontalBurnInOffset) { 178 // Whoops, we went too far horizontally. Let's retract.. 179 mLastBurnInXOffset -= xChange; 180 // change horizontal direction.. 181 mXOffsetDirection *= -1; 182 // and let's shift the Y offset. 183 final int yChange = mYOffsetDirection * BURN_IN_SHIFT_STEP; 184 mLastBurnInYOffset += yChange; 185 if (mLastBurnInYOffset > mMaxVerticalBurnInOffset 186 || mLastBurnInYOffset < mMinVerticalBurnInOffset) { 187 // Whoops, we went to far vertically. Let's retract.. 188 mLastBurnInYOffset -= yChange; 189 // and change vertical direction. 190 mYOffsetDirection *= -1; 191 } 192 } 193 // If we are outside of the radius, let's try again. 194 } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT 195 && mLastBurnInXOffset * mLastBurnInXOffset + mLastBurnInYOffset * mLastBurnInYOffset 196 > mBurnInRadiusMaxSquared); 197 } 198 199 public void dump(String prefix, PrintWriter pw) { 200 pw.println(prefix + TAG); 201 prefix += " "; 202 pw.println(prefix + "mBurnInProtectionActive=" + mBurnInProtectionActive); 203 pw.println(prefix + "mHorizontalBurnInOffsetsBounds=(" + mMinHorizontalBurnInOffset + ", " 204 + mMaxHorizontalBurnInOffset + ")"); 205 pw.println(prefix + "mVerticalBurnInOffsetsBounds=(" + mMinVerticalBurnInOffset + ", " 206 + mMaxVerticalBurnInOffset + ")"); 207 pw.println(prefix + "mBurnInRadiusMaxSquared=" + mBurnInRadiusMaxSquared); 208 pw.println(prefix + "mLastBurnInOffset=(" + mLastBurnInXOffset + ", " 209 + mLastBurnInYOffset + ")"); 210 pw.println(prefix + "mOfsetChangeDirections=(" + mXOffsetDirection + ", " 211 + mYOffsetDirection + ")"); 212 } 213 214 @Override 215 public void onDisplayAdded(int i) { 216 } 217 218 @Override 219 public void onDisplayRemoved(int i) { 220 } 221 222 @Override 223 public void onDisplayChanged(int displayId) { 224 if (displayId == mDisplay.getDisplayId()) { 225 if (mDisplay.getState() == Display.STATE_DOZE 226 || mDisplay.getState() == Display.STATE_DOZE_SUSPEND) { 227 startBurnInProtection(); 228 } else { 229 cancelBurnInProtection(); 230 } 231 } 232 } 233 234 @Override 235 public void onAnimationStart(Animator animator) { 236 } 237 238 @Override 239 public void onAnimationEnd(Animator animator) { 240 if (animator == mCenteringAnimator && !mBurnInProtectionActive) { 241 // No matter how the animation finishes, we want to zero the offsets. 242 mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 0, 0); 243 } 244 } 245 246 @Override 247 public void onAnimationCancel(Animator animator) { 248 } 249 250 @Override 251 public void onAnimationRepeat(Animator animator) { 252 } 253 254 @Override 255 public void onAnimationUpdate(ValueAnimator valueAnimator) { 256 if (!mBurnInProtectionActive) { 257 final float value = (Float) valueAnimator.getAnimatedValue(); 258 mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 259 (int) (mLastBurnInXOffset * value), (int) (mLastBurnInYOffset * value)); 260 } 261 } 262} 263