content: /*
   * Copyright (C) 2014 AChep@xda <artemchep@gmail.com>
   *
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU General Public License
   * as published by the Free Software Foundation; either version 2
   * of the License, or (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   * MA  02110-1301, USA.
   */
  package com.achep.acdisplay.ui.activities;
  
  import android.app.KeyguardManager;
  import android.app.admin.DevicePolicyManager;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.PowerManager;
  import android.os.SystemClock;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import android.util.Log;
  import android.view.View;
  import android.view.WindowManager;
  
  import com.achep.acdisplay.App;
  import com.achep.acdisplay.R;
  import com.achep.acdisplay.Timeout;
  import com.achep.acdisplay.services.KeyguardService;
  import com.achep.acdisplay.ui.activities.base.BaseActivity;
  import com.achep.base.Device;
  import com.achep.base.tests.Check;
  import com.achep.base.utils.KeyguardUtils;
  import com.achep.base.utils.ToastUtils;
  import com.achep.base.utils.logs.TracingLog;
  import com.achep.base.utils.power.PowerUtils;
  
  import static com.achep.base.Build.DEBUG;
  
  /**
   * Activity that contains some methods to emulate system keyguard.
   *
   * @author Artem Chepurnoy
   */
  public abstract class KeyguardActivity extends BaseActivity implements
          Timeout.OnTimeoutEventListener {
  
      private static final String TAG = "KeyguardActivity";
  
      /**
       * An optional extra that contains the reason of this
       * wake up.
       */
      public static final String EXTRA_CAUSE = "cause";
      public static final String EXTRA_TURN_SCREEN_ON = "turn_screen_on";
  
      /**
       * This constant is CyanogenMod specific and should do nothing on a
       * stock Android.
       */
      private static final int PREVENT_POWER_KEY = 0x80000000;
  
      private static final int SYSTEM_UI_BASIC_FLAGS;
  
      static {
          final int f = Device.hasKitKatApi() ? View.SYSTEM_UI_FLAG_HIDE_NAVIGATION : 0;
          SYSTEM_UI_BASIC_FLAGS = f
                  | View.SYSTEM_UI_FLAG_LOW_PROFILE
                  | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                  | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                  | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
      }
  
      private static final int UNLOCKING_MAX_TIME = 150; // ms.
      private static final int PF_MAX_TIME = 2000; // ms.
  
      private BroadcastReceiver mScreenOffReceiver;
      private KeyguardManager mKeyguardManager;
      private long mUnlockingTime;
      private boolean mResumed;
      private int mExtraCause;
  
      private boolean mTimeoutPaused = true;
      private final Timeout mTimeout = new Timeout();
      private final Handler mHandler = new Handler();
  
      private PowerManager.WakeLock mWakeUpLock;
  
      private boolean mKeyguardDismissed;
      private View.OnSystemUiVisibilityChangeListener mSystemUiListener;
  
      @Override
      public void onWindowFocusChanged(boolean windowHasFocus) {
          super.onWindowFocusChanged(windowHasFocus);
          if (DEBUG) Log.d(TAG, "On window focus changed " + windowHasFocus);
  
          if (isUnlocking()) {
              if (windowHasFocus) {
                  mUnlockingTime = 0;
              } else {
                  finish();
                  return;
              }
          }
          if (mResumed) {
              populateFlags(windowHasFocus);
          }
  
          // Update system ui visibility: hide nav bar and optionally the
          // status bar.
          setSystemUiVisibilityFake();
      }
  
      private void populateFlags(boolean manualControl) {
          int windowFlags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
          int timeoutDelay = getConfig().getTimeoutNormal();
  
          if (manualControl) {
              getWindow().addFlags(windowFlags);
  
              mTimeoutPaused = false;
  
              mTimeout.resume();
              mTimeout.setTimeoutDelayed(timeoutDelay, true);
          } else {
              getWindow().clearFlags(windowFlags);
  
              mTimeoutPaused = true;
              mTimeout.setTimeoutDelayed(timeoutDelay, true);
              mTimeout.pause();
          }
      }
  
      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          if (DEBUG) Log.d(TAG, "Creating keyguard activity...");
  
          mTimeout.registerListener(this);
          mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
          registerScreenEventsReceiver();
  
          int flags = 0;
  
          // Handle intents
          final Intent intent = getIntent();
          if (intent != null) {
              if (hasWakeUpExtra(intent)) flags |= WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
              mExtraCause = intent.getIntExtra(EXTRA_CAUSE, 0);
          }
  
          // FIXME: Android dev team broke the DISMISS_KEYGUARD flag.
          // https://code.google.com/p/android-developer-preview/issues/detail?id=1902
          if (Device.hasLollipopApi()
                  && !Device.hasMarshmallowApi() // Should be fine now
                  && !mKeyguardManager.isKeyguardSecure()) {
              getWindow().addFlags(flags);
              requestDismissSystemKeyguard();
          } else {
              getWindow().addFlags(flags |
                      // Show activity above the system keyguard.
                      WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
          }
  
          // Update system ui visibility: hide nav bar and optionally the
          // status bar.
          final View decorView = getWindow().getDecorView();
          decorView.setOnSystemUiVisibilityChangeListener(
                  mSystemUiListener = new View.OnSystemUiVisibilityChangeListener() {
                      public final void onSystemUiVisibilityChange(int f) {
                          setSystemUiVisibilityFake();
                          decorView.postDelayed(new Runnable() {
                              @Override
                              public void run() {
                                  int visibility = SYSTEM_UI_BASIC_FLAGS
                                          | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
                                  if (getConfig().isFullScreen()) {
                                      visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
                                  }
                                  decorView.setSystemUiVisibility(visibility);
                              }
                          }, 100);
                      }
                  }
          );
      }
  
      public void setSystemUiVisibilityFake() {
          int visibility = SYSTEM_UI_BASIC_FLAGS | View.SYSTEM_UI_FLAG_IMMERSIVE;
          if (getConfig().isFullScreen()) {
              visibility |= View.SYSTEM_UI_FLAG_FULLSCREEN;
          }
  
          getWindow().getDecorView().setSystemUiVisibility(visibility);
      }
  
      @Override
      protected void onNewIntent(Intent intent) {
          super.onNewIntent(intent);
          // Handle intents
          if (hasWakeUpExtra(intent)) acquireWakeUpLock();
          mExtraCause = intent.getIntExtra(EXTRA_CAUSE, 0);
      }
  
      @Override
      public void onAttachedToWindow() {
          super.onAttachedToWindow();
          // Handle intents
          if (hasWakeUpExtra(getIntent())) acquireWakeUpFlags();
      }
  
      /**
       * @see #releaseWakeUpLock()
       */
      private void acquireWakeUpLock() {
          int flags = PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.SCREEN_DIM_WAKE_LOCK;
          PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
          mWakeUpLock = pm.newWakeLock(flags, "Turn the keyguard on.");
          mWakeUpLock.acquire(500); // 0.5 sec.
      }
  
      /**
       * Releases previously acquired {@link #mWakeUpLock wake up lock}, does
       * nothing if it's {@code null} or not held.
       *
       * @see #acquireWakeUpLock()
       */
      private void releaseWakeUpLock() {
          if (mWakeUpLock != null && mWakeUpLock.isHeld()) mWakeUpLock.release();
      }
  
      private void acquireWakeUpFlags() {
          getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
      }
  
      /**
       * @return {@code true} if the passed intent is not {@code null} and includes the
       * {@link #EXTRA_TURN_SCREEN_ON} set to {@code true}, {@code false}
       * otherwise.
       */
      private boolean hasWakeUpExtra(@Nullable Intent intent) {
          return intent != null && intent.getBooleanExtra(EXTRA_TURN_SCREEN_ON, false);
      }
  
      @Override
      public void onDetachedFromWindow() {
          releaseWakeUpLock();
          super.onDetachedFromWindow();
      }
  
      @Override
      protected void onDestroy() {
          if (DEBUG) Log.d(TAG, "Destroying keyguard activity...");
  
          unregisterScreenEventsReceiver();
  
          mTimeout.unregisterListener(this);
          mTimeout.clear();
  
          super.onDestroy();
      }
  
      /**
       * Registers a receiver to finish activity when screen goes off and to
       * refresh window flags on screen on. You will need to
       * {@link #unregisterScreenEventsReceiver() unregister} it later.
       *
       * @see #unregisterScreenEventsReceiver()
       */
      private void registerScreenEventsReceiver() {
          mScreenOffReceiver = new BroadcastReceiver() {
  
              @Override
              public void onReceive(Context context, Intent intent) {
                  switch (intent.getAction()) {
                      case Intent.ACTION_SCREEN_ON:
                          if (mResumed) {
                              // Fake system ui visibility state change to
                              // update flags again.
                              mSystemUiListener.onSystemUiVisibilityChange(0);
                          }
                          break;
                      case Intent.ACTION_SCREEN_OFF:
                          if (!KeyguardService.isActive) {
                              PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
                              pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Finalize the keyguard.").acquire(200);
                              KeyguardActivity.this.finish();
                          }
                          break;
                  }
              }
  
          };
  
          IntentFilter intentFilter = new IntentFilter();
          intentFilter.addAction(Intent.ACTION_SCREEN_ON);
          intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
          intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); // max allowed priority
          registerReceiver(mScreenOffReceiver, intentFilter);
      }
  
      /**
       * Unregisters the screen off receiver if it was registered previously.
       *
       * @see #registerScreenEventsReceiver()
       */
      private void unregisterScreenEventsReceiver() {
          if (mScreenOffReceiver != null) {
              unregisterReceiver(mScreenOffReceiver);
              mScreenOffReceiver = null;
          }
      }
  
      @Override
      public void onStart() {
          super.onStart();
          sendBroadcast(App.ACTION_STATE_START);
      }
  
      @Override
      protected void onResume() {
          if (DEBUG) Log.d(TAG, "Resuming keyguard activity...");
          super.onResume();
  
          mResumed = true;
          mUnlockingTime = 0;
          populateFlags(true);
          overrideHomePress(true);
  
  //        getWindow().addFlags(PREVENT_POWER_KEY);
  
          /*
          // Read the system's screen off timeout setting.
          try {
              mSystemScreenOffTimeout = Settings.System.getInt(
                      getContentResolver(),
                      Settings.System.SCREEN_OFF_TIMEOUT);
          } catch (Settings.SettingNotFoundException e) {
              mSystemScreenOffTimeout = -1;
          }
          */
  
          sendBroadcast(App.ACTION_STATE_RESUME);
      }
  
      @Override
      protected void onPause() {
          sendBroadcast(App.ACTION_STATE_PAUSE);
  
  //        getWindow().clearFlags(PREVENT_POWER_KEY);
  
          if (DEBUG) Log.d(TAG, "Pausing keyguard activity...");
          mResumed = false;
          populateFlags(false);
          overrideHomePress(false);
  
          mHandler.removeCallbacksAndMessages(null);
  
          super.onPause();
      }
  
      @Override
      public void onStop() {
          super.onStop();
  
          // Workarounds this bug:
          //
          // This annoying bug is driving me crazy! After any AcDisplay notification,
          // no matter how much time I wait, when I press the power button I will
          // directly go to launcher. Only if I turn the screen off and on again
          // I will get the lockscreen (as it should always be).
          //
          // Read more: https://plus.google.com/110348136204265282325/posts/ZYfWURptt2V
          if (mKeyguardDismissed /* only after setting the FLAG_DISMISS_KEYGUARD flag           */
                  && !isFinishing() /* otherwise flags can not be set, case it'd turn screen on */
                  && !KeyguardService.isActive /* otherwise it's fine to leave device unlocked  */
                  && !PowerUtils.isScreenOn(this) /* screen is off and it WILL kill us later    */) {
              if (DEBUG) Log.d(TAG, "Clearing the FLAG_DISMISS_KEYGUARD flag.");
              getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
              mKeyguardDismissed = false;
          }
  
          sendBroadcast(App.ACTION_STATE_STOP);
      }
  
      /**
       * Notifies Xposed {@link com.achep.acdisplay.plugins.xposed.OverrideHomeButton module}
       * to start ignoring home button press. Please, notice that it will ignore home button
       * click everywhere until you call {@code overrideHomePress(false)}
       *
       * @param override {@code true} to start ignoring, {@code false} to stop.
       * @see com.achep.acdisplay.plugins.xposed.OverrideHomeButton
       * @see #sendBroadcast(android.content.Intent)
       */
      private void overrideHomePress(boolean override) {
          sendBroadcast(override
                  ? App.ACTION_EAT_HOME_PRESS_START
                  : App.ACTION_EAT_HOME_PRESS_STOP);
      }
  
      /**
       * Same as calling {@code sendBroadcast(new Intent(action))}.
       */
      private void sendBroadcast(@NonNull String action) {
          sendBroadcast(new Intent(action));
      }
  
      @Override
      protected void onUserLeaveHint() {
          super.onUserLeaveHint();
          if (DEBUG) Log.d(TAG, "User is leaving...");
          /*
          // The user has tried to fool the keyguard.
          // Blame him ("You shall not pass!")
          // and turn the screen off.
          if (PowerUtils.isScreenOn(this)) {
              if (!mUnlocking) {
                  Log.i(TAG, "You shall not pass!");
                  lock();
              } else if (!mLocking) {
                  finish();
              }
          }
          */
      }
  
      @Override
      public void onTimeoutEvent(@NonNull Timeout timeout, int event) {
          if (DEBUG) TracingLog.v(TAG, "TIMEOUT: " + event, 5);
          switch (event) {
              case Timeout.EVENT_CHANGED:
              case Timeout.EVENT_RESUMED:
                  if (mTimeoutPaused) {
                      runOnUiThread(new Runnable() {
                          @Override
                          public void run() {
                              mTimeout.pause();
                          }
                      });
                  }
                  break;
              case Timeout.EVENT_TIMEOUT:
                  Check.getInstance().isFalse(mTimeoutPaused);
                  lock();
                  break;
          }
      }
  
      /**
       * Locks the device (and turns screen off).
       *
       * @return {@code true} if successful, {@code false} otherwise.
       * @see DevicePolicyManager#lockNow()
       */
      public boolean lock() {
          DevicePolicyManager dpm = (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
          try {
              // TODO: Respect secure lock timeout settings.
              mUnlockingTime = 0;
              dpm.lockNow();
              return true;
          } catch (SecurityException e) {
              String errorMessage = "Failed to lock the screen due to a security exception.";
              ToastUtils.showLong(this, errorMessage);
              Log.e(TAG, errorMessage);
              // Clear the FLAG_KEEP_SCREEN_ON flag to prevent the situation when
              // AcDisplay stays forever on. Normally this should never happen.
              getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
              return false; // Screw you owner!
          }
      }
  
      /**
       * Unlocks keyguard and runs {@link Runnable runnable} when unlocked.
       */
      public void unlock(@Nullable Runnable runnable) {
          unlock(runnable, true);
      }
  
      /**
       * Unlocks keyguard and runs {@link Runnable runnable} when unlocked.
       *
       * @param finish {@code true} to finish activity, {@code false} to keep it
       * @see #unlock(Runnable)
       */
      public void unlock(final @Nullable Runnable runnable, final boolean finish) {
          if (DEBUG) Log.d(TAG, "Unlocking with params: finish=" + finish);
  
          // If keyguard is disabled no need to make
          // a delay between calling this method and
          // unlocking.
          // Otherwise we need this delay to get new
          // flags applied.
          final long now = SystemClock.elapsedRealtime();
  
          mUnlockingTime = now;
          requestDismissSystemKeyguard();
  
          mHandler.post(new Runnable() {
  
              @Override
              public void run() {
                  // Loop until flag gets applied.
                  // TODO: Use somewhat trigger for detecting unlocking.
                  int delta = (int) (SystemClock.elapsedRealtime() - now);
                  if (isLocked() && !isSecure() && delta < UNLOCKING_MAX_TIME) {
                      mHandler.postDelayed(this, 30);
                      return;
                  }
  
                  if (runnable != null) runOnUiThread(runnable);
                  if (finish) {
                      performUnlock();
                  }
              }
          });
      }
  
      private void performUnlock() {
          mUnlockingTime = SystemClock.elapsedRealtime();
          mKeyguardDismissed = false;
  
          finish();
  
          boolean animate = getConfig().isUnlockAnimationEnabled() && !isPowerSaveMode();
          overridePendingTransition(0, animate
                  ? R.anim.activity_unlock
                  : 0);
      }
  
      /**
       * Sets the {@link WindowManager.LayoutParams#FLAG_DISMISS_KEYGUARD} flag
       * and marks the {@link #mKeyguardDismissed} as {@code true}.
       */
      private void requestDismissSystemKeyguard() {
          getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
          mKeyguardDismissed = true;
      }
  
      /**
       * Returns whether the device is currently locked and requires a PIN,
       * pattern or password to unlock.
       *
       * @return {@code true} if device is locked, {@code false} otherwise.
       */
      public boolean isSecure() {
          return KeyguardUtils.isDeviceLocked(mKeyguardManager);
      }
  
      /**
       * Return whether the keyguard presents.
       *
       * @return {@code true} if device is locked, {@code false} otherwise.
       */
      public boolean isLocked() {
          return mKeyguardManager.isKeyguardLocked();
      }
  
      /**
       * Return whether the keyguard is unlocking.
       *
       * @return {@code true} if the keyguard is unlocking atm, {@code false} otherwise.
       * @see #unlock(Runnable)
       * @see #PF_MAX_TIME
       */
      public boolean isUnlocking() {
          return SystemClock.elapsedRealtime() - mUnlockingTime <= PF_MAX_TIME;
      }
  
      public int getCause() {
          return mExtraCause;
      }
  
      @NonNull
      public Timeout getTimeout() {
          return mTimeout;
      }
  
      @Override
      public void onBackPressed() { /* override back button */ }
  
  }
  