blob: fef1e57504cd4b96c1797c69c878edc8ed2cb626 [file] [log] [blame]
Filip Gruszczynskic0452082015-02-25 15:27:13 -08001/*
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;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080018
Filip Gruszczynski6bc51302015-03-17 20:47:48 +000019import android.animation.Animator;
20import android.animation.ValueAnimator;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080021import android.app.AlarmManager;
22import android.app.PendingIntent;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080027import android.hardware.display.DisplayManager;
28import android.hardware.display.DisplayManagerInternal;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080029import android.os.SystemClock;
Joe LaPenna7c167022015-04-03 21:29:09 +000030import android.util.Slog;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080031import android.view.Display;
Filip Gruszczynski6bc51302015-03-17 20:47:48 +000032import android.view.animation.LinearInterpolator;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080033
34import com.android.server.LocalServices;
35
36import java.io.PrintWriter;
37import java.util.concurrent.TimeUnit;
38
Filip Gruszczynski6bc51302015-03-17 20:47:48 +000039public class BurnInProtectionHelper implements DisplayManager.DisplayListener,
40 Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080041 private static final String TAG = "BurnInProtection";
42
43 // Default value when max burnin radius is not set.
Mark Renoufc1256912015-03-11 14:38:23 -040044 public static final int BURN_IN_MAX_RADIUS_DEFAULT = -1;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080045
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 LaPenna7c167022015-04-03 21:29:09 +000049 private static final boolean DEBUG = false;
50
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080051 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 Gruszczynski6bc51302015-03-17 20:47:48 +000055 private static final long CENTERING_ANIMATION_DURATION_MS = 100;
56 private final ValueAnimator mCenteringAnimator;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080057
58 private boolean mBurnInProtectionActive;
Filip Gruszczynski6bc51302015-03-17 20:47:48 +000059 private boolean mFirstUpdate;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080060
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 LaPenna7c167022015-04-03 21:29:09 +000083 if (DEBUG) {
84 Slog.d(TAG, "onReceive " + intent);
85 }
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080086 updateBurnInProtection();
87 }
88 };
Joe LaPenna7c167022015-04-03 21:29:09 +000089
Mark Renoufc1256912015-03-11 14:38:23 -040090 public BurnInProtectionHelper(Context context, int minHorizontalOffset,
91 int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset,
92 int maxOffsetRadius) {
Mark Renoufc1256912015-03-11 14:38:23 -040093 mMinHorizontalBurnInOffset = minHorizontalOffset;
94 mMaxHorizontalBurnInOffset = maxHorizontalOffset;
95 mMinVerticalBurnInOffset = minVerticalOffset;
Filip Gruszczynski6bc51302015-03-17 20:47:48 +000096 mMaxVerticalBurnInOffset = maxVerticalOffset;
Mark Renoufc1256912015-03-11 14:38:23 -040097 if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) {
98 mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -080099 } else {
Mark Renoufc1256912015-03-11 14:38:23 -0400100 mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -0800101 }
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 Gruszczynski6bc51302015-03-17 20:47:48 +0000116
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 Gruszczynskid2e86402015-02-19 13:05:03 -0800122 }
123
124 public void startBurnInProtection() {
125 if (!mBurnInProtectionActive) {
126 mBurnInProtectionActive = true;
Filip Gruszczynski6bc51302015-03-17 20:47:48 +0000127 mFirstUpdate = true;
128 mCenteringAnimator.cancel();
Filip Gruszczynskid2e86402015-02-19 13:05:03 -0800129 updateBurnInProtection();
130 }
131 }
132
133 private void updateBurnInProtection() {
134 if (mBurnInProtectionActive) {
Filip Gruszczynski6bc51302015-03-17 20:47:48 +0000135 // 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 LaPenna7c167022015-04-03 21:29:09 +0000145 // 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 Gruszczynskid2e86402015-02-19 13:05:03 -0800151 // Next adjustment at least ten seconds in the future.
Joe LaPenna7c167022015-04-03 21:29:09 +0000152 long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS;
Filip Gruszczynskid2e86402015-02-19 13:05:03 -0800153 // And aligned to the minute.
Joe LaPenna7c167022015-04-03 21:29:09 +0000154 nextWall = nextWall - nextWall % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS
Filip Gruszczynskid2e86402015-02-19 13:05:03 -0800155 + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS;
Joe LaPenna7c167022015-04-03 21:29:09 +0000156 // 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 Gruszczynskid2e86402015-02-19 13:05:03 -0800165 } else {
166 mAlarmManager.cancel(mBurnInProtectionIntent);
Filip Gruszczynski6bc51302015-03-17 20:47:48 +0000167 mCenteringAnimator.start();
Filip Gruszczynskid2e86402015-02-19 13:05:03 -0800168 }
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 Renoufc1256912015-03-11 14:38:23 -0400214 } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT
Filip Gruszczynskid2e86402015-02-19 13:05:03 -0800215 && 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 Gruszczynski6bc51302015-03-17 20:47:48 +0000253
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 Gruszczynskid2e86402015-02-19 13:05:03 -0800282}