/* * 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.services; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.AsyncTask; import android.os.PowerManager; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.achep.acdisplay.Config; import com.achep.acdisplay.Presenter; import com.achep.acdisplay.R; import com.achep.acdisplay.services.switches.AlarmSwitch; import com.achep.acdisplay.services.switches.InactiveTimeSwitch; import com.achep.acdisplay.services.switches.NoNotifiesSwitch; import com.achep.acdisplay.services.switches.PhoneCallSwitch; import com.achep.acdisplay.utils.tasks.RunningTasks; import com.achep.base.AppHeap; import com.achep.base.content.ConfigBase; import com.achep.base.tests.Check; import com.achep.base.utils.PackageUtils; import com.achep.base.utils.power.PowerUtils; import static com.achep.base.Build.DEBUG; /** * Created by Artem on 16.02.14. * * @author Artem Chepurnoy */ public class KeyguardService extends SwitchService { private static final String TAG = "KeyguardService"; /** * {@code true} if the {@link KeyguardService keyguard service is running} * and functioning, {@code false} otherwise. */ public static boolean isActive = false; /** * Starts or stops this service as required by settings and device's state. */ public static void handleState(Context context) { Config config = Config.getInstance(); boolean onlyWhileChargingOption = !config.isEnabledOnlyWhileCharging() || PowerUtils.isPlugged(context); if (config.isEnabled() && config.isKeyguardEnabled() && onlyWhileChargingOption) { BathService.startService(context, KeyguardService.class); } else { BathService.stopService(context, KeyguardService.class); } } private static final int ACTIVITY_LAUNCH_MAX_TIME = 1000; private PhoneCallSwitch mPhoneCallSwitch; private ActivityMonitorThread mActivityMonitorThread; private String mPackageName; private final Presenter mPresenter = Presenter.getInstance(); private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { mScreenReceiver.onReceive(context, intent); switch (intent.getAction()) { case Intent.ACTION_SCREEN_ON: String activityName = null; long activityChangeTime = 0; if (mActivityMonitorThread != null) { //noinspection SynchronizeOnNonFinalField synchronized (mActivityMonitorThread) { mActivityMonitorThread.monitor(); activityName = mActivityMonitorThread.topActivityName; activityChangeTime = mActivityMonitorThread.topActivityTime; } } stopMonitorThread(); if (mPhoneCallSwitch.isCalling()) { mPresenter.kill(); return; } long now = SystemClock.elapsedRealtime(); boolean becauseOfActivityLaunch = now - activityChangeTime < ACTIVITY_LAUNCH_MAX_TIME && activityName != null && !activityName.startsWith(mPackageName); if (DEBUG) Log.d(TAG, "Screen is on: activity_flag=" + becauseOfActivityLaunch); if (becauseOfActivityLaunch) { // Finish AcDisplay activity so it won't shown // after exiting from newly launched one. mPresenter.kill(); } else if (mLocked) startGui(); // Normal launch break; case Intent.ACTION_SCREEN_OFF: mLocked = mPresenter.isLocked(); performLockWithDelay(); break; } } }; private boolean mLocked; /** * {@code true} if the screen if actually off, * {@code false} otherwise. */ private boolean mScreenOff; /** * The {@link SystemClock#elapsedRealtime()} of when the screen has * been turned off. */ private long mScreenOffTimestamp; @Nullable private AsyncTask<Void, Void, Void> mDelayedLockTask; @NonNull private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { case Intent.ACTION_SCREEN_ON: if (DEBUG) Log.i(TAG, "Screen on"); mScreenOffTimestamp = 0; mScreenOff = false; cancelLockWithDelay(); break; case Intent.ACTION_SCREEN_OFF: if (DEBUG) Log.i(TAG, "Screen off"); mScreenOffTimestamp = SystemClock.elapsedRealtime(); mScreenOff = true; break; } } }; private void startGuiGhost() { startGui(); } private void startGui() { Presenter.getInstance().tryStartGuiCauseKeyguard(getContext()); } private void performLockWithDelay() { if (DEBUG) Log.i(TAG, "Trying to perform the delayed lock..."); final long now = SystemClock.elapsedRealtime(); final int d = Config.getInstance().getKeyguardLockDelayMillis(); final int delay = d - (int) (now - mScreenOffTimestamp); final int delayMax = 30000; if (delay <= 0 || delay > delayMax) { performLock(); } else if (delay > delayMax) { // TODO: Allow delays more than 5/30s without lags. // I can do it using the Android Alarm system. } else mDelayedLockTask = new AsyncTask<Void, Void, Void>() { @Override protected void onPreExecute() { super.onPreExecute(); if (DEBUG) Log.i(TAG, "Starting the delay thread..."); if (delay > delayMax) throw new RuntimeException(); // Do you really want it? // Don't let the OS to go into a deep sleep until we // finish our job. PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE); pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Delayed lock.").acquire(delay + 100); } @Override protected Void doInBackground(Void... params) { try { Thread.sleep(delay); } catch (InterruptedException e) { /* totally fine */ } return null; } @Override protected void onPostExecute(Void v) { super.onPostExecute(v); Check.getInstance().isTrue(mScreenOff); performLock(); } }.execute(); } private void performLock() { if (DEBUG) Log.i(TAG, "Trying to perform the lock: screen_off=" + mScreenOff); if (mScreenOff) { mLocked = true; startGuiGhost(); startMonitorThread(); } } private void cancelLockWithDelay() { if (mDelayedLockTask != null) mDelayedLockTask.cancel(true); } @NonNull @Override public Switch[] onBuildSwitches() { Config config = Config.getInstance(); ConfigBase.Option noNotifies = config.getOption(Config.KEY_KEYGUARD_WITHOUT_NOTIFICATIONS); ConfigBase.Option respectIt = config.getOption(Config.KEY_KEYGUARD_RESPECT_INACTIVE_TIME); return new Switch[]{ mPhoneCallSwitch = new PhoneCallSwitch(getContext(), this), new AlarmSwitch(getContext(), this), // Options new NoNotifiesSwitch(getContext(), this, noNotifies, true), new InactiveTimeSwitch(getContext(), this, respectIt), }; } @NonNull @Override public String getLabel() { return getContext().getString(R.string.service_bath_keyguard); } @Override public void onCreate() { super.onCreate(); final Context context = getContext(); mPackageName = PackageUtils.getName(context); mScreenOff = !PowerUtils.isScreenOn(context); // Register base receiver that is watching ACTION_SCREEN_ON, // ACTION_SCREEN_OFF and does completely nothing except // saving the screen off timestamp. IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_SCREEN_ON); intentFilter.addAction(Intent.ACTION_SCREEN_OFF); intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); // highest priority context.registerReceiver(mScreenReceiver, intentFilter); } @Override public void onStart(@Nullable Object... objects) { final Context context = getContext(); // Register receiver IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_SCREEN_ON); intentFilter.addAction(Intent.ACTION_SCREEN_OFF); intentFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); // highest priority context.registerReceiver(mReceiver, intentFilter); if (mScreenOff) { // Make sure the app is launched mLocked = mPresenter.isLocked(); performLockWithDelay(); } isActive = true; } @Override public void onStop(@Nullable Object... objects) { final Context context = getContext(); context.unregisterReceiver(mReceiver); stopMonitorThread(); cancelLockWithDelay(); if (mScreenOff) { // Make sure that the app is not // waiting in the shade. mPresenter.kill(); } isActive = false; } @Override public void onDestroy() { final Context context = getContext(); context.unregisterReceiver(mScreenReceiver); super.onDestroy(); // Watch for the leaks AppHeap.getRefWatcher().watch(this); } private void startMonitorThread() { stopMonitorThread(); mActivityMonitorThread = new ActivityMonitorThread(getContext()); mActivityMonitorThread.start(); } private void stopMonitorThread() { if (mActivityMonitorThread == null) { return; // Nothing to stop } mActivityMonitorThread.running = false; mActivityMonitorThread.interrupt(); mActivityMonitorThread = null; } /** * Thread that monitors the current top activity. * * @author Artem Chepurnoy */ private static class ActivityMonitorThread extends Thread { /** * How frequently should we check top running activity. The * values is in millis. */ private static final long PERIOD = 15 * 60 * 1000; // 15 min. public volatile long topActivityTime; public volatile String topActivityName; public volatile boolean running = true; @NonNull private final Context mContext; public ActivityMonitorThread(@NonNull Context context) { if (DEBUG) Log.d(TAG, "Activity monitor thread has been initiated."); mContext = context; } @Override public void run() { super.run(); while (running) { monitor(); try { Thread.sleep(PERIOD); } catch (InterruptedException e) { /* unused */ } } } /** * Checks what activity is the latest. */ public synchronized void monitor() { String topActivity = RunningTasks.getInstance().getRunningTasksTop(mContext); if (TextUtils.equals(topActivity, topActivityName)) { return; } topActivityName = topActivity; topActivityTime = SystemClock.elapsedRealtime(); Log.i(TAG, "New top activity is " + topActivityName); } //-- DEBUG ---------------------------------------------------------------- /* Only for debug purposes! */ private final Object dFinalizeWatcher = DEBUG ? new Object() { /** * Logs the notifications' removal. */ @Override protected void finalize() throws Throwable { try { Log.d(TAG, "Activity monitor thread has died."); } finally { super.finalize(); } } } : null; } }