content: /*
   * Copyright (C) 2015 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License
   */
  
  package com.android.server.policy;
  
  import android.animation.Animator;
  import android.animation.ValueAnimator;
  import android.app.AlarmManager;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.hardware.display.DisplayManager;
  import android.hardware.display.DisplayManagerInternal;
  import android.os.SystemClock;
  import android.util.Slog;
  import android.view.Display;
  import android.view.animation.LinearInterpolator;
  
  import com.android.server.LocalServices;
  
  import java.io.PrintWriter;
  import java.util.concurrent.TimeUnit;
  
  public class BurnInProtectionHelper implements DisplayManager.DisplayListener,
          Animator.AnimatorListener, ValueAnimator.AnimatorUpdateListener {
      private static final String TAG = "BurnInProtection";
  
      // Default value when max burnin radius is not set.
      public static final int BURN_IN_MAX_RADIUS_DEFAULT = -1;
  
      private static final long BURNIN_PROTECTION_FIRST_WAKEUP_INTERVAL_MS =
              TimeUnit.MINUTES.toMillis(1);
      private static final long BURNIN_PROTECTION_SUBSEQUENT_WAKEUP_INTERVAL_MS =
              TimeUnit.MINUTES.toMillis(2);
      private static final long BURNIN_PROTECTION_MINIMAL_INTERVAL_MS = TimeUnit.SECONDS.toMillis(10);
  
      private static final boolean DEBUG = false;
  
      private static final String ACTION_BURN_IN_PROTECTION =
              "android.internal.policy.action.BURN_IN_PROTECTION";
  
      private static final int BURN_IN_SHIFT_STEP = 2;
      private static final long CENTERING_ANIMATION_DURATION_MS = 100;
      private final ValueAnimator mCenteringAnimator;
  
      private boolean mBurnInProtectionActive;
      private boolean mFirstUpdate;
  
      private final int mMinHorizontalBurnInOffset;
      private final int mMaxHorizontalBurnInOffset;
      private final int mMinVerticalBurnInOffset;
      private final int mMaxVerticalBurnInOffset;
  
      private final int mBurnInRadiusMaxSquared;
  
      private int mLastBurnInXOffset = 0;
      /* 1 means increasing, -1 means decreasing */
      private int mXOffsetDirection = 1;
      private int mLastBurnInYOffset = 0;
      /* 1 means increasing, -1 means decreasing */
      private int mYOffsetDirection = 1;
  
      private int mAppliedBurnInXOffset = 0;
      private int mAppliedBurnInYOffset = 0;
  
      private final AlarmManager mAlarmManager;
      private final PendingIntent mBurnInProtectionIntent;
      private final DisplayManagerInternal mDisplayManagerInternal;
      private final Display mDisplay;
  
      private BroadcastReceiver mBurnInProtectionReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (DEBUG) {
                  Slog.d(TAG, "onReceive " + intent);
              }
              updateBurnInProtection();
          }
      };
  
      public BurnInProtectionHelper(Context context, int minHorizontalOffset,
              int maxHorizontalOffset, int minVerticalOffset, int maxVerticalOffset,
              int maxOffsetRadius) {
          mMinHorizontalBurnInOffset = minHorizontalOffset;
          mMaxHorizontalBurnInOffset = maxHorizontalOffset;
          mMinVerticalBurnInOffset = minVerticalOffset;
          mMaxVerticalBurnInOffset = maxVerticalOffset;
          if (maxOffsetRadius != BURN_IN_MAX_RADIUS_DEFAULT) {
              mBurnInRadiusMaxSquared = maxOffsetRadius * maxOffsetRadius;
          } else {
              mBurnInRadiusMaxSquared = BURN_IN_MAX_RADIUS_DEFAULT;
          }
  
          mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
          mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
          context.registerReceiver(mBurnInProtectionReceiver,
                  new IntentFilter(ACTION_BURN_IN_PROTECTION));
          Intent intent = new Intent(ACTION_BURN_IN_PROTECTION);
          intent.setPackage(context.getPackageName());
          intent.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
          mBurnInProtectionIntent = PendingIntent.getBroadcast(context, 0,
                  intent, PendingIntent.FLAG_UPDATE_CURRENT);
          DisplayManager displayManager =
                  (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
          mDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
          displayManager.registerDisplayListener(this, null /* handler */);
  
          mCenteringAnimator = ValueAnimator.ofFloat(1f, 0f);
          mCenteringAnimator.setDuration(CENTERING_ANIMATION_DURATION_MS);
          mCenteringAnimator.setInterpolator(new LinearInterpolator());
          mCenteringAnimator.addListener(this);
          mCenteringAnimator.addUpdateListener(this);
      }
  
      public void startBurnInProtection() {
          if (!mBurnInProtectionActive) {
              mBurnInProtectionActive = true;
              mFirstUpdate = true;
              mCenteringAnimator.cancel();
              updateBurnInProtection();
          }
      }
  
      private void updateBurnInProtection() {
          if (mBurnInProtectionActive) {
              // We don't want to adjust offsets immediately after the device goes into ambient mode.
              // Instead, we want to wait until it's more likely that the user is not observing the
              // screen anymore.
              final long interval = mFirstUpdate
                  ? BURNIN_PROTECTION_FIRST_WAKEUP_INTERVAL_MS
                  : BURNIN_PROTECTION_SUBSEQUENT_WAKEUP_INTERVAL_MS;
              if (mFirstUpdate) {
                  mFirstUpdate = false;
              } else {
                  adjustOffsets();
                  mAppliedBurnInXOffset = mLastBurnInXOffset;
                  mAppliedBurnInYOffset = mLastBurnInYOffset;
                  mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(),
                          mLastBurnInXOffset, mLastBurnInYOffset);
              }
              // We use currentTimeMillis to compute the next wakeup time since we want to wake up at
              // the same time as we wake up to update ambient mode to minimize power consumption.
              // However, we use elapsedRealtime to schedule the alarm so that setting the time can't
              // disable burn-in protection for extended periods.
              final long nowWall = System.currentTimeMillis();
              final long nowElapsed = SystemClock.elapsedRealtime();
              // Next adjustment at least ten seconds in the future.
              long nextWall = nowWall + BURNIN_PROTECTION_MINIMAL_INTERVAL_MS;
              // And aligned to the minute.
              nextWall = (nextWall - (nextWall % interval)) + interval;
              // Use elapsed real time that is adjusted to full minute on wall clock.
              final long nextElapsed = nowElapsed + (nextWall - nowWall);
              if (DEBUG) {
                  Slog.d(TAG, "scheduling next wake-up, now wall time " + nowWall
                          + ", next wall: " + nextWall + ", now elapsed: " + nowElapsed
                          + ", next elapsed: " + nextElapsed);
              }
              mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, nextElapsed,
                      mBurnInProtectionIntent);
          } else {
              mAlarmManager.cancel(mBurnInProtectionIntent);
              mCenteringAnimator.start();
          }
      }
  
      public void cancelBurnInProtection() {
          if (mBurnInProtectionActive) {
              mBurnInProtectionActive = false;
              updateBurnInProtection();
          }
      }
  
      /**
       * Gently shifts current burn-in offsets, minimizing the change for the user.
       *
       * Shifts are applied in following fashion:
       * 1) shift horizontally from minimum to the maximum;
       * 2) shift vertically by one from minimum to the maximum;
       * 3) shift horizontally from maximum to the minimum;
       * 4) shift vertically by one from minimum to the maximum.
       * 5) if you reach the maximum vertically, start shifting back by one from maximum to minimum.
       *
       * On top of that, stay within specified radius. If the shift distance from the center is
       * higher than the radius, skip these values and go the next position that is within the radius.
       */
      private void adjustOffsets() {
          do {
              // By default, let's just shift the X offset.
              final int xChange = mXOffsetDirection * BURN_IN_SHIFT_STEP;
              mLastBurnInXOffset += xChange;
              if (mLastBurnInXOffset > mMaxHorizontalBurnInOffset
                      || mLastBurnInXOffset < mMinHorizontalBurnInOffset) {
                  // Whoops, we went too far horizontally. Let's retract..
                  mLastBurnInXOffset -= xChange;
                  // change horizontal direction..
                  mXOffsetDirection *= -1;
                  // and let's shift the Y offset.
                  final int yChange = mYOffsetDirection * BURN_IN_SHIFT_STEP;
                  mLastBurnInYOffset += yChange;
                  if (mLastBurnInYOffset > mMaxVerticalBurnInOffset
                          || mLastBurnInYOffset < mMinVerticalBurnInOffset) {
                      // Whoops, we went to far vertically. Let's retract..
                      mLastBurnInYOffset -= yChange;
                      // and change vertical direction.
                      mYOffsetDirection *= -1;
                  }
              }
              // If we are outside of the radius, let's try again.
          } while (mBurnInRadiusMaxSquared != BURN_IN_MAX_RADIUS_DEFAULT
                  && mLastBurnInXOffset * mLastBurnInXOffset + mLastBurnInYOffset * mLastBurnInYOffset
                          > mBurnInRadiusMaxSquared);
      }
  
      public void dump(String prefix, PrintWriter pw) {
          pw.println(prefix + TAG);
          prefix += "  ";
          pw.println(prefix + "mBurnInProtectionActive=" + mBurnInProtectionActive);
          pw.println(prefix + "mHorizontalBurnInOffsetsBounds=(" + mMinHorizontalBurnInOffset + ", "
                  + mMaxHorizontalBurnInOffset + ")");
          pw.println(prefix + "mVerticalBurnInOffsetsBounds=(" + mMinVerticalBurnInOffset + ", "
                  + mMaxVerticalBurnInOffset + ")");
          pw.println(prefix + "mBurnInRadiusMaxSquared=" + mBurnInRadiusMaxSquared);
          pw.println(prefix + "mLastBurnInOffset=(" + mLastBurnInXOffset + ", "
                  + mLastBurnInYOffset + ")");
          pw.println(prefix + "mOfsetChangeDirections=(" + mXOffsetDirection + ", "
                  + mYOffsetDirection + ")");
      }
  
      @Override
      public void onDisplayAdded(int i) {
      }
  
      @Override
      public void onDisplayRemoved(int i) {
      }
  
      @Override
      public void onDisplayChanged(int displayId) {
          if (displayId == mDisplay.getDisplayId()) {
              if (mDisplay.getState() == Display.STATE_DOZE
                      || mDisplay.getState() == Display.STATE_DOZE_SUSPEND
                      || mDisplay.getState() == Display.STATE_ON_SUSPEND) {
                  startBurnInProtection();
              } else {
                  cancelBurnInProtection();
              }
          }
      }
  
      @Override
      public void onAnimationStart(Animator animator) {
      }
  
      @Override
      public void onAnimationEnd(Animator animator) {
          if (animator == mCenteringAnimator && !mBurnInProtectionActive) {
              mAppliedBurnInXOffset = 0;
              mAppliedBurnInYOffset = 0;
              // No matter how the animation finishes, we want to zero the offsets.
              mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(), 0, 0);
          }
      }
  
      @Override
      public void onAnimationCancel(Animator animator) {
      }
  
      @Override
      public void onAnimationRepeat(Animator animator) {
      }
  
      @Override
      public void onAnimationUpdate(ValueAnimator valueAnimator) {
          if (!mBurnInProtectionActive) {
              final float value = (Float) valueAnimator.getAnimatedValue();
              mDisplayManagerInternal.setDisplayOffsets(mDisplay.getDisplayId(),
                      (int) (mAppliedBurnInXOffset * value), (int) (mAppliedBurnInYOffset * value));
          }
      }
  }
  