Home | History | Annotate | Download | only in policy
      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;
     18 
     19 import android.animation.Animator;
     20 import android.animation.ValueAnimator;
     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;
     27 import android.hardware.display.DisplayManager;
     28 import android.hardware.display.DisplayManagerInternal;
     29 import android.os.SystemClock;
     30 import android.util.Slog;
     31 import android.view.Display;
     32 import android.view.animation.LinearInterpolator;
     33 
     34 import com.android.server.LocalServices;
     35 
     36 import java.io.PrintWriter;
     37 import java.util.concurrent.TimeUnit;
     38 
     39 public 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 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) {
     83             if (DEBUG) {
     84                 Slog.d(TAG, "onReceive " + intent);
     85             }
     86             updateBurnInProtection();
     87         }
     88     };
     89 
     90     public BurnInProtectionHelper(Context context, int minHorizontalOffset,
     91             int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset,
     92             int maxOffsetRadius) {
     93         mMinHorizontalBurnInOffset = minHorizontalOffset;
     94         mMaxHorizontalBurnInOffset = maxHorizontalOffset;
     95         mMinVerticalBurnInOffset = minVerticalOffset;
     96         mMaxVerticalBurnInOffset = maxVerticalOffset;
     97         if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) {
     98             mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius;
     99         } else {
    100             mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT;
    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 */);
    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);
    122     }
    123 
    124     public void startBurnInProtection() {
    125         if (!mBurnInProtectionActive) {
    126             mBurnInProtectionActive = true;
    127             mFirstUpdate = true;
    128             mCenteringAnimator.cancel();
    129             updateBurnInProtection();
    130         }
    131     }
    132 
    133     private void updateBurnInProtection() {
    134         if (mBurnInProtectionActive) {
    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             }
    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();
    151             // Next adjustment at least ten seconds in the future.
    152             long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS;
    153             // And aligned to the minute.
    154             nextWall = nextWall - nextWall % BURNIN_PROTECTION_WAKEUP_INTERVAL_MS
    155                     + BURNIN_PROTECTION_WAKEUP_INTERVAL_MS;
    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);
    165         } else {
    166             mAlarmManager.cancel(mBurnInProtectionIntent);
    167             mCenteringAnimator.start();
    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.
    214         } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT
    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     }
    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     }
    282 }
    283