Filip Gruszczynski | c045208 | 2015-02-25 15:27:13 -0800 | [diff] [blame] | 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; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 18 | |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 19 | import android.animation.Animator; |
| 20 | import android.animation.ValueAnimator; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 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; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 27 | import android.hardware.display.DisplayManager; |
| 28 | import android.hardware.display.DisplayManagerInternal; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 29 | import android.os.SystemClock; |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 30 | import android.util.Slog; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 31 | import android.view.Display; |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 32 | import android.view.animation.LinearInterpolator; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 33 | |
| 34 | import com.android.server.LocalServices; |
| 35 | |
| 36 | import java.io.PrintWriter; |
| 37 | import java.util.concurrent.TimeUnit; |
| 38 | |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 39 | public class BurnInProtectionHelper implements DisplayManager.DisplayListener, |
| 40 | Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener { |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 41 | private static final String TAG = "BurnInProtection"; |
| 42 | |
| 43 | // Default value when max burnin radius is not set. |
Mark Renouf | c125691 | 2015-03-11 14:38:23 -0400 | [diff] [blame] | 44 | public static final int BURN_IN_MAX_RADIUS_DEFAULT = -1; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 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 | |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 49 | private static final boolean DEBUG = false; |
| 50 | |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 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; |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 55 | private static final long CENTERING_ANIMATION_DURATION_MS = 100; |
| 56 | private final ValueAnimator mCenteringAnimator; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 57 | |
| 58 | private boolean mBurnInProtectionActive; |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 59 | private boolean mFirstUpdate; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 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) { |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 83 | if (DEBUG) { |
| 84 | Slog.d(TAG, "onReceive " + intent); |
| 85 | } |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 86 | updateBurnInProtection(); |
| 87 | } |
| 88 | }; |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 89 | |
Mark Renouf | c125691 | 2015-03-11 14:38:23 -0400 | [diff] [blame] | 90 | public BurnInProtectionHelper(Context context, int minHorizontalOffset, |
| 91 | int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset, |
| 92 | int maxOffsetRadius) { |
Mark Renouf | c125691 | 2015-03-11 14:38:23 -0400 | [diff] [blame] | 93 | mMinHorizontalBurnInOffset = minHorizontalOffset; |
| 94 | mMaxHorizontalBurnInOffset = maxHorizontalOffset; |
| 95 | mMinVerticalBurnInOffset = minVerticalOffset; |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 96 | mMaxVerticalBurnInOffset = maxVerticalOffset; |
Mark Renouf | c125691 | 2015-03-11 14:38:23 -0400 | [diff] [blame] | 97 | if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) { |
| 98 | mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 99 | } else { |
Mark Renouf | c125691 | 2015-03-11 14:38:23 -0400 | [diff] [blame] | 100 | mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 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 */); |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 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); |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 122 | } |
| 123 | |
| 124 | public void startBurnInProtection() { |
| 125 | if (!mBurnInProtectionActive) { |
| 126 | mBurnInProtectionActive = true; |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 127 | mFirstUpdate = true; |
| 128 | mCenteringAnimator.cancel(); |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 129 | updateBurnInProtection(); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | private void updateBurnInProtection() { |
| 134 | if (mBurnInProtectionActive) { |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 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 | } |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 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(); |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 151 | // Next adjustment at least ten seconds in the future. |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 152 | long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS; |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 153 | // And aligned to the minute. |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 154 | nextWall = nextWall - nextWall % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 155 | + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS; |
Joe LaPenna | 7c16702 | 2015-04-03 21:29:09 +0000 | [diff] [blame] | 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); |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 165 | } else { |
| 166 | mAlarmManager.cancel(mBurnInProtectionIntent); |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 167 | mCenteringAnimator.start(); |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 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. |
Mark Renouf | c125691 | 2015-03-11 14:38:23 -0400 | [diff] [blame] | 214 | } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 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 | } |
Filip Gruszczynski | 6bc5130 | 2015-03-17 20:47:48 +0000 | [diff] [blame] | 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 | } |
Filip Gruszczynski | d2e8640 | 2015-02-19 13:05:03 -0800 | [diff] [blame] | 282 | } |