[go: nahoru, domu]

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_WAKEUP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1);
47    private static final long BURNIN_PROTECTION_MINIMAL_INTERVAL_MS = TimeUnit.SECONDS.toMillis(10);
48
49    private static final boolean DEBUG = false;
50
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;
55    private static final long CENTERING_ANIMATION_DURATION_MS = 100;
56    private final ValueAnimator mCenteringAnimator;
57
58    private boolean mBurnInProtectionActive;
59    private boolean mFirstUpdate;
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 int mAppliedBurnInXOffset = 0;
76    private int mAppliedBurnInYOffset = 0;
77
78    private final AlarmManager mAlarmManager;
79    private final PendingIntent mBurnInProtectionIntent;
80    private final DisplayManagerInternal mDisplayManagerInternal;
81    private final Display mDisplay;
82
83    private BroadcastReceiver mBurnInProtectionReceiver = new BroadcastReceiver() {
84        @Override
85        public void onReceive(Context context, Intent intent) {
86            if (DEBUG) {
87                Slog.d(TAG, "onReceive " + intent);
88            }
89            updateBurnInProtection();
90        }
91    };
92
93    public BurnInProtectionHelper(Context context, int minHorizontalOffset,
94            int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset,
95            int maxOffsetRadius) {
96        mMinHorizontalBurnInOffset = minHorizontalOffset;
97        mMaxHorizontalBurnInOffset = maxHorizontalOffset;
98        mMinVerticalBurnInOffset = minVerticalOffset;
99        mMaxVerticalBurnInOffset = maxVerticalOffset;
100        if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) {
101            mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius;
102        } else {
103            mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT;
104        }
105
106        mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
107        mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
108        context.registerReceiver(mBurnInProtectionReceiver,
109                new IntentFilter(ACTION_BURN_IN_PROTECTION));
110        Intent intent = new Intent(ACTION_BURN_IN_PROTECTION);
111        intent.setPackage(context.getPackageName());
112        intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
113        mBurnInProtectionIntent = PendingIntent.getBroadcast(context, 0,
114                intent, PendingIntent.FLAG_UPDATE_CURRENT);
115        DisplayManager displayManager =
116                (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
117        mDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
118        displayManager.registerDisplayListener(this, null /* handler */);
119
120        mCenteringAnimator = ValueAnimator.ofFloat(1f, 0f);
121        mCenteringAnimator.setDuration(CENTERING_ANIMATION_DURATION_MS);
122        mCenteringAnimator.setInterpolator(new LinearInterpolator());
123        mCenteringAnimator.addListener(this);
124        mCenteringAnimator.addUpdateListener(this);
125    }
126
127    public void startBurnInProtection() {
128        if (!mBurnInProtectionActive) {
129            mBurnInProtectionActive = true;
130            mFirstUpdate = true;
131            mCenteringAnimator.cancel();
132            updateBurnInProtection();
133        }
134    }
135
136    private void updateBurnInProtection() {
137        if (mBurnInProtectionActive) {
138            // We don't want to adjust offsets immediately after the device goes into ambient mode.
139            // Instead, we want to wait until it's more likely that the user is not observing the
140            // screen anymore.
141            if (mFirstUpdate) {
142                mFirstUpdate = false;
143            } else {
144                adjustOffsets();
145                mAppliedBurnInXOffset = mLastBurnInXOffset;
146                mAppliedBurnInYOffset = mLastBurnInYOffset;
147                mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(),
148                        mLastBurnInXOffset, mLastBurnInYOffset);
149            }
150            // We use currentTimeMillis to compute the next wakeup time since we want to wake up at
151            // the same time as we wake up to update ambient mode to minimize power consumption.
152            // However, we use elapsedRealtime to schedule the alarm so that setting the time can't
153            // disable burn-in protection for extended periods.
154            final long nowWall = System.currentTimeMillis();
155            final long nowElapsed = SystemClock.elapsedRealtime();
156            // Next adjustment at least ten seconds in the future.
157            long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS;
158            // And aligned to the minute.
159            nextWall = nextWall - nextWall % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS
160                    + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS;
161            // Use elapsed real time that is adjusted to full minute on wall clock.
162            final long nextElapsed = nowElapsed + (nextWall - nowWall);
163            if (DEBUG) {
164                Slog.d(TAG, "scheduling next wake-up, now wall time " + nowWall
165                        + ", next wall: " + nextWall + ", now elapsed: " + nowElapsed
166                        + ", next elapsed: " + nextElapsed);
167            }
168            mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextElapsed,
169                    mBurnInProtectionIntent);
170        } else {
171            mAlarmManager.cancel(mBurnInProtectionIntent);
172            mCenteringAnimator.start();
173        }
174    }
175
176    public void cancelBurnInProtection() {
177        if (mBurnInProtectionActive) {
178            mBurnInProtectionActive = false;
179            updateBurnInProtection();
180        }
181    }
182
183    /**
184     * Gently shifts current burn-in offsets, minimizing the change for the user.
185     *
186     * Shifts are applied in following fashion:
187     * 1) shift horizontally from minimum to the maximum;
188     * 2) shift vertically by one from minimum to the maximum;
189     * 3) shift horizontally from maximum to the minimum;
190     * 4) shift vertically by one from minimum to the maximum.
191     * 5) if you reach the maximum vertically, start shifting back by one from maximum to minimum.
192     *
193     * On top of that, stay within specified radius. If the shift distance from the center is
194     * higher than the radius, skip these values and go the next position that is within the radius.
195     */
196    private void adjustOffsets() {
197        do {
198            // By default, let's just shift the X offset.
199            final int xChange = mXOffsetDirection * BURN_IN_SHIFT_STEP;
200            mLastBurnInXOffset += xChange;
201            if (mLastBurnInXOffset > mMaxHorizontalBurnInOffset
202                    || mLastBurnInXOffset < mMinHorizontalBurnInOffset) {
203                // Whoops, we went too far horizontally. Let's retract..
204                mLastBurnInXOffset -= xChange;
205                // change horizontal direction..
206                mXOffsetDirection *= -1;
207                // and let's shift the Y offset.
208                final int yChange = mYOffsetDirection * BURN_IN_SHIFT_STEP;
209                mLastBurnInYOffset += yChange;
210                if (mLastBurnInYOffset > mMaxVerticalBurnInOffset
211                        || mLastBurnInYOffset < mMinVerticalBurnInOffset) {
212                    // Whoops, we went to far vertically. Let's retract..
213                    mLastBurnInYOffset -= yChange;
214                    // and change vertical direction.
215                    mYOffsetDirection *= -1;
216                }
217            }
218            // If we are outside of the radius, let's try again.
219        } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT
220                && mLastBurnInXOffset * mLastBurnInXOffset + mLastBurnInYOffset * mLastBurnInYOffset
221                        > mBurnInRadiusMaxSquared);
222    }
223
224    public void dump(String prefix, PrintWriter pw) {
225        pw.println(prefix + TAG);
226        prefix += "  ";
227        pw.println(prefix + "mBurnInProtectionActive=" + mBurnInProtectionActive);
228        pw.println(prefix + "mHorizontalBurnInOffsetsBounds=(" + mMinHorizontalBurnInOffset + ", "
229                + mMaxHorizontalBurnInOffset + ")");
230        pw.println(prefix + "mVerticalBurnInOffsetsBounds=(" + mMinVerticalBurnInOffset + ", "
231                + mMaxVerticalBurnInOffset + ")");
232        pw.println(prefix + "mBurnInRadiusMaxSquared=" + mBurnInRadiusMaxSquared);
233        pw.println(prefix + "mLastBurnInOffset=(" + mLastBurnInXOffset + ", "
234                + mLastBurnInYOffset + ")");
235        pw.println(prefix + "mOfsetChangeDirections=(" + mXOffsetDirection + ", "
236                + mYOffsetDirection + ")");
237    }
238
239    @Override
240    public void onDisplayAdded(int i) {
241    }
242
243    @Override
244    public void onDisplayRemoved(int i) {
245    }
246
247    @Override
248    public void onDisplayChanged(int displayId) {
249        if (displayId == mDisplay.getDisplayId()) {
250            if (mDisplay.getState() == Display.STATE_DOZE
251                    || mDisplay.getState() == Display.STATE_DOZE_SUSPEND) {
252                startBurnInProtection();
253            } else {
254                cancelBurnInProtection();
255            }
256        }
257    }
258
259    @Override
260    public void onAnimationStart(Animator animator) {
261    }
262
263    @Override
264    public void onAnimationEnd(Animator animator) {
265        if (animator == mCenteringAnimator && !mBurnInProtectionActive) {
266            mAppliedBurnInXOffset = 0;
267            mAppliedBurnInYOffset = 0;
268            // No matter how the animation finishes, we want to zero the offsets.
269            mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 0, 0);
270        }
271    }
272
273    @Override
274    public void onAnimationCancel(Animator animator) {
275    }
276
277    @Override
278    public void onAnimationRepeat(Animator animator) {
279    }
280
281    @Override
282    public void onAnimationUpdate(ValueAnimator valueAnimator) {
283        if (!mBurnInProtectionActive) {
284            final float value = (Float) valueAnimator.getAnimatedValue();
285            mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(),
286                    (int) (mAppliedBurnInXOffset * value), (int) (mAppliedBurnInYOffset * value));
287        }
288    }
289}
290