/*
* 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 android.support.annotation.NonNull;
import android.support.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 */ }
}