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