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.util.Slog;
31import android.view.Display;
32import android.view.animation.LinearInterpolator;
33
34import com.android.server.LocalServices;
35
36import java.io.PrintWriter;
37import java.util.concurrent.TimeUnit;
38
39public 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                    || mDisplay.getState() == Display.STATE_ON_SUSPEND) {
258                startBurnInProtection();
259            } else {
260                cancelBurnInProtection();
261            }
262        }
263    }
264
265    @Override
266    public void onAnimationStart(Animator animator) {
267    }
268
269    @Override
270    public void onAnimationEnd(Animator animator) {
271        if (animator == mCenteringAnimator && !mBurnInProtectionActive) {
272            mAppliedBurnInXOffset = 0;
273            mAppliedBurnInYOffset = 0;
274            // No matter how the animation finishes, we want to zero the offsets.
275            mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 0, 0);
276        }
277    }
278
279    @Override
280    public void onAnimationCancel(Animator animator) {
281    }
282
283    @Override
284    public void onAnimationRepeat(Animator animator) {
285    }
286
287    @Override
288    public void onAnimationUpdate(ValueAnimator valueAnimator) {
289        if (!mBurnInProtectionActive) {
290            final float value = (Float) valueAnimator.getAnimatedValue();
291            mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(),
292                    (int) (mAppliedBurnInXOffset * value), (int) (mAppliedBurnInYOffset * value));
293        }
294    }
295}
296