content: /*
   * Copyright (C) 2018 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.wm;
  
  import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
  import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
  import static android.view.Display.DEFAULT_DISPLAY;
  
  import android.animation.ArgbEvaluator;
  import android.animation.ValueAnimator;
  import android.app.ActivityManager;
  import android.app.ActivityThread;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.graphics.PixelFormat;
  import android.graphics.drawable.ColorDrawable;
  import android.os.Binder;
  import android.os.Handler;
  import android.os.IBinder;
  import android.os.Looper;
  import android.os.Message;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.provider.Settings;
  import android.util.DisplayMetrics;
  import android.util.Slog;
  import android.view.Display;
  import android.view.Gravity;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.ViewGroup;
  import android.view.ViewTreeObserver;
  import android.view.WindowManager;
  import android.view.animation.Animation;
  import android.view.animation.AnimationUtils;
  import android.view.animation.Interpolator;
  import android.widget.Button;
  import android.widget.FrameLayout;
  
  import com.android.internal.R;
  
  /**
   *  Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden
   *  entering immersive mode.
   */
  public class ImmersiveModeConfirmation {
      private static final String TAG = "ImmersiveModeConfirmation";
      private static final boolean DEBUG = false;
      private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution
      private static final String CONFIRMED = "confirmed";
  
      private static boolean sConfirmed;
  
      private final Context mContext;
      private final H mHandler;
      private final long mShowDelayMs;
      private final long mPanicThresholdMs;
      private final IBinder mWindowToken = new Binder();
  
      private ClingWindowView mClingWindow;
      private long mPanicTime;
      private WindowManager mWindowManager;
      // Local copy of vr mode enabled state, to avoid calling into VrManager with
      // the lock held.
      private boolean mVrModeEnabled;
      private int mLockTaskState = LOCK_TASK_MODE_NONE;
  
      ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled) {
          final Display display = context.getDisplay();
          final Context uiContext = ActivityThread.currentActivityThread().getSystemUiContext();
          mContext = display.getDisplayId() == DEFAULT_DISPLAY
                  ? uiContext : uiContext.createDisplayContext(display);
          mHandler = new H(looper);
          mShowDelayMs = getNavBarExitDuration() * 3;
          mPanicThresholdMs = context.getResources()
                  .getInteger(R.integer.config_immersive_mode_confirmation_panic);
          mVrModeEnabled = vrModeEnabled;
      }
  
      private long getNavBarExitDuration() {
          Animation exit = AnimationUtils.loadAnimation(mContext, R.anim.dock_bottom_exit);
          return exit != null ? exit.getDuration() : 0;
      }
  
      static boolean loadSetting(int currentUserId, Context context) {
          final boolean wasConfirmed = sConfirmed;
          sConfirmed = false;
          if (DEBUG) Slog.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId));
          String value = null;
          try {
              value = Settings.Secure.getStringForUser(context.getContentResolver(),
                      Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
                      UserHandle.USER_CURRENT);
              sConfirmed = CONFIRMED.equals(value);
              if (DEBUG) Slog.d(TAG, "Loaded sConfirmed=" + sConfirmed);
          } catch (Throwable t) {
              Slog.w(TAG, "Error loading confirmations, value=" + value, t);
          }
          return sConfirmed != wasConfirmed;
      }
  
      private static void saveSetting(Context context) {
          if (DEBUG) Slog.d(TAG, "saveSetting()");
          try {
              final String value = sConfirmed ? CONFIRMED : null;
              Settings.Secure.putStringForUser(context.getContentResolver(),
                      Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
                      value,
                      UserHandle.USER_CURRENT);
              if (DEBUG) Slog.d(TAG, "Saved value=" + value);
          } catch (Throwable t) {
              Slog.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t);
          }
      }
  
      void immersiveModeChangedLw(String pkg, boolean isImmersiveMode,
              boolean userSetupComplete, boolean navBarEmpty) {
          mHandler.removeMessages(H.SHOW);
          if (isImmersiveMode) {
              final boolean disabled = PolicyControl.disableImmersiveConfirmation(pkg);
              if (DEBUG) Slog.d(TAG, String.format("immersiveModeChanged() disabled=%s sConfirmed=%s",
                      disabled, sConfirmed));
              if (!disabled
                      && (DEBUG_SHOW_EVERY_TIME || !sConfirmed)
                      && userSetupComplete
                      && !mVrModeEnabled
                      && !navBarEmpty
                      && !UserManager.isDeviceInDemoMode(mContext)
                      && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
                  mHandler.sendEmptyMessageDelayed(H.SHOW, mShowDelayMs);
              }
          } else {
              mHandler.sendEmptyMessage(H.HIDE);
          }
      }
  
      boolean onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode,
              boolean navBarEmpty) {
          if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) {
              // turning the screen back on within the panic threshold
              return mClingWindow == null;
          }
          if (isScreenOn && inImmersiveMode && !navBarEmpty) {
              // turning the screen off, remember if we were in immersive mode
              mPanicTime = time;
          } else {
              mPanicTime = 0;
          }
          return false;
      }
  
      void confirmCurrentPrompt() {
          if (mClingWindow != null) {
              if (DEBUG) Slog.d(TAG, "confirmCurrentPrompt()");
              mHandler.post(mConfirm);
          }
      }
  
      private void handleHide() {
          if (mClingWindow != null) {
              if (DEBUG) Slog.d(TAG, "Hiding immersive mode confirmation");
              getWindowManager().removeView(mClingWindow);
              mClingWindow = null;
          }
      }
  
      private WindowManager.LayoutParams getClingWindowLayoutParams() {
          final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                  ViewGroup.LayoutParams.MATCH_PARENT,
                  ViewGroup.LayoutParams.MATCH_PARENT,
                  WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
                  WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                          | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
                          | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
                  PixelFormat.TRANSLUCENT);
          lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
          lp.setTitle("ImmersiveModeConfirmation");
          lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
          lp.token = getWindowToken();
          return lp;
      }
  
      private FrameLayout.LayoutParams getBubbleLayoutParams() {
          return new FrameLayout.LayoutParams(
                  mContext.getResources().getDimensionPixelSize(
                          R.dimen.immersive_mode_cling_width),
                  ViewGroup.LayoutParams.WRAP_CONTENT,
                  Gravity.CENTER_HORIZONTAL | Gravity.TOP);
      }
  
      /**
       * @return the window token that's used by all ImmersiveModeConfirmation windows.
       */
      IBinder getWindowToken() {
          return mWindowToken;
      }
  
      private class ClingWindowView extends FrameLayout {
          private static final int BGCOLOR = 0x80000000;
          private static final int OFFSET_DP = 96;
          private static final int ANIMATION_DURATION = 250;
  
          private final Runnable mConfirm;
          private final ColorDrawable mColor = new ColorDrawable(0);
          private final Interpolator mInterpolator;
          private ValueAnimator mColorAnim;
          private ViewGroup mClingLayout;
  
          private Runnable mUpdateLayoutRunnable = new Runnable() {
              @Override
              public void run() {
                  if (mClingLayout != null && mClingLayout.getParent() != null) {
                      mClingLayout.setLayoutParams(getBubbleLayoutParams());
                  }
              }
          };
  
          private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener =
                  new ViewTreeObserver.OnComputeInternalInsetsListener() {
                      private final int[] mTmpInt2 = new int[2];
  
                      @Override
                      public void onComputeInternalInsets(
                              ViewTreeObserver.InternalInsetsInfo inoutInfo) {
                          // Set touchable region to cover the cling layout.
                          mClingLayout.getLocationInWindow(mTmpInt2);
                          inoutInfo.setTouchableInsets(
                                  ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
                          inoutInfo.touchableRegion.set(
                                  mTmpInt2[0],
                                  mTmpInt2[1],
                                  mTmpInt2[0] + mClingLayout.getWidth(),
                                  mTmpInt2[1] + mClingLayout.getHeight());
                      }
                  };
  
          private BroadcastReceiver mReceiver = new BroadcastReceiver() {
              @Override
              public void onReceive(Context context, Intent intent) {
                  if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
                      post(mUpdateLayoutRunnable);
                  }
              }
          };
  
          ClingWindowView(Context context, Runnable confirm) {
              super(context);
              mConfirm = confirm;
              setBackground(mColor);
              setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
              mInterpolator = AnimationUtils
                      .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
          }
  
          @Override
          public void onAttachedToWindow() {
              super.onAttachedToWindow();
  
              DisplayMetrics metrics = new DisplayMetrics();
              getWindowManager().getDefaultDisplay().getMetrics(metrics);
              float density = metrics.density;
  
              getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
  
              // create the confirmation cling
              mClingLayout = (ViewGroup)
                      View.inflate(getContext(), R.layout.immersive_mode_cling, null);
  
              final Button ok = mClingLayout.findViewById(R.id.ok);
              ok.setOnClickListener(new OnClickListener() {
                  @Override
                  public void onClick(View v) {
                      mConfirm.run();
                  }
              });
              addView(mClingLayout, getBubbleLayoutParams());
  
              if (ActivityManager.isHighEndGfx()) {
                  final View cling = mClingLayout;
                  cling.setAlpha(0f);
                  cling.setTranslationY(-OFFSET_DP * density);
  
                  postOnAnimation(new Runnable() {
                      @Override
                      public void run() {
                          cling.animate()
                                  .alpha(1f)
                                  .translationY(0)
                                  .setDuration(ANIMATION_DURATION)
                                  .setInterpolator(mInterpolator)
                                  .withLayer()
                                  .start();
  
                          mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
                          mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                              @Override
                              public void onAnimationUpdate(ValueAnimator animation) {
                                  final int c = (Integer) animation.getAnimatedValue();
                                  mColor.setColor(c);
                              }
                          });
                          mColorAnim.setDuration(ANIMATION_DURATION);
                          mColorAnim.setInterpolator(mInterpolator);
                          mColorAnim.start();
                      }
                  });
              } else {
                  mColor.setColor(BGCOLOR);
              }
  
              mContext.registerReceiver(mReceiver,
                      new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
          }
  
          @Override
          public void onDetachedFromWindow() {
              mContext.unregisterReceiver(mReceiver);
          }
  
          @Override
          public boolean onTouchEvent(MotionEvent motion) {
              return true;
          }
      }
  
      /**
       * DO HOLD THE WINDOW MANAGER LOCK WHEN CALLING THIS METHOD
       * The reason why we add this method is to avoid the deadlock of WMG->WMS and WMS->WMG
       * when ImmersiveModeConfirmation object is created.
       */
      private WindowManager getWindowManager() {
          if (mWindowManager == null) {
              mWindowManager = (WindowManager)
                        mContext.getSystemService(Context.WINDOW_SERVICE);
          }
          return mWindowManager;
      }
  
      private void handleShow() {
          if (DEBUG) Slog.d(TAG, "Showing immersive mode confirmation");
  
          mClingWindow = new ClingWindowView(mContext, mConfirm);
  
          // we will be hiding the nav bar, so layout as if it's already hidden
          mClingWindow.setSystemUiVisibility(
                  View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
  
          // show the confirmation
          WindowManager.LayoutParams lp = getClingWindowLayoutParams();
          getWindowManager().addView(mClingWindow, lp);
      }
  
      private final Runnable mConfirm = new Runnable() {
          @Override
          public void run() {
              if (DEBUG) Slog.d(TAG, "mConfirm.run()");
              if (!sConfirmed) {
                  sConfirmed = true;
                  saveSetting(mContext);
              }
              handleHide();
          }
      };
  
      private final class H extends Handler {
          private static final int SHOW = 1;
          private static final int HIDE = 2;
  
          H(Looper looper) {
              super(looper);
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch(msg.what) {
                  case SHOW:
                      handleShow();
                      break;
                  case HIDE:
                      handleHide();
                      break;
              }
          }
      }
  
      void onVrStateChangedLw(boolean enabled) {
          mVrModeEnabled = enabled;
          if (mVrModeEnabled) {
              mHandler.removeMessages(H.SHOW);
              mHandler.sendEmptyMessage(H.HIDE);
          }
      }
  
      void onLockTaskModeChangedLw(int lockTaskState) {
          mLockTaskState = lockTaskState;
      }
  }
  