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