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_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