Reputation: 868
I have an Android Service that I'd like to take actions whenever the device is locked.
I'd like to clarify that:
I add those points because I've found no end of people asking the same question and getting one of those responses. I'm happy that they did, because the answers have been useful and/or educational, but they aren't what I'm looking for now.
Upvotes: 4
Views: 4549
Reputation: 868
I've put together a potential solution, but there are some major caveats that come with it.
General approach: Detect the "screen off" event then periodically check if the device has become locked. This is far from ideal, but there does not seem to be any manner to detect when the device is locked. Basically, "there is no right way to do this so you need to hack something together".
Credit: This is based on the suggestion from @Markus in the comments combined with bits of code from the answers to the linked questions plus some extra grunt work of my own.
Caveats:
AndroidManifest.xml
Add a startup activity:
<activity android:name=".StartLockMonitorActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Add a broadcast receiver:
<receiver android:name=".StateReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
Add the main service:
<service
android:name=".LockMonitor"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.sample.screenmonitor.LockMonitor.ACTION_START_SERVICE"/>
</intent-filter>
</service>
Add a permission:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
res/values/styles.xml
Add transparent style:
<style name="Theme.Transparent" parent="android:Theme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
res/values/colors.xml
Add transparent colour:
<color name="transparent">#00000000</color>
StartLockMonitorActivity.java
This is the main entry point, it just kicks the service:
package com.sample.screenmonitor;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
public class StartLockMonitorActivity extends AppCompatActivity {
public static final String TAG = "LockMonitor-SLM";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.w(TAG, "Starting service...");
final Intent newIntent = new Intent(this, LockMonitor.class);
newIntent.setAction(LockMonitor.ACTION_CHECK_LOCK);
startService(newIntent);
Toast.makeText(this, "Starting Lock Monitor Service", Toast.LENGTH_LONG).show();
finish();
}
}
StateReceiver.java
This restarts the service when the device reboots. The first time the service starts it adds some additional filters (see the comments in LockMonitor.java
describing why this isn't done in the manifest).
package com.sample.screenmonitor;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
public class StateReceiver extends BroadcastReceiver {
public static final String TAG = "LockMonitor-SR";
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive: redirect intent to LockMonitor");
final Intent newIntent = new Intent(context, LockMonitor.class);
newIntent.setAction(LockMonitor.ACTION_CHECK_LOCK);
newIntent.putExtra(LockMonitor.EXTRA_STATE, intent.getAction());
context.startService(newIntent);
}
}
LockMonitor.java
package com.sample.screenmonitor;
import android.app.KeyguardManager;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.os.PowerManager;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.Timer;
import java.util.TimerTask;
public class LockMonitor extends Service {
public static final String TAG = "LockMonitor";
public static final String ACTION_CHECK_LOCK = "com.sample.screenmonitor.LockMonitor.ACTION_CHECK_LOCK";
public static final String EXTRA_CHECK_LOCK_DELAY_INDEX = "com.sample.screenmonitor.LockMonitor.EXTRA_CHECK_LOCK_DELAY_INDEX";
public static final String EXTRA_STATE = "com.sample.screenmonitor.LockMonitor.EXTRA_STATE";
BroadcastReceiver receiver = null;
static final Timer timer = new Timer();
CheckLockTask checkLockTask = null;
public LockMonitor() {
Log.d(TAG, "LockMonitor constructor");
}
@Override
public void onDestroy() {
Log.d(TAG, "LM.onDestroy");
super.onDestroy();
if (receiver != null) {
unregisterReceiver(receiver);
receiver = null;
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d(TAG, "LM.onStartCommand");
if (intent != null && intent.getAction() == ACTION_CHECK_LOCK) {
checkLock(intent);
}
if (receiver == null) {
// Unlike other broad casted intents, for these you CANNOT declare them in the Android Manifest;
// instead they must be registered in an IntentFilter.
receiver = new StateReceiver();
IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_ON);
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_USER_PRESENT);
registerReceiver(receiver, filter);
}
return START_STICKY;
}
void checkLock(final Intent intent) {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
final boolean isProtected = keyguardManager.isKeyguardSecure();
final boolean isLocked = keyguardManager.inKeyguardRestrictedInputMode();
final boolean isInteractive = powerManager.isInteractive();
final int delayIndex = getSafeCheckLockDelay(intent.getIntExtra(EXTRA_CHECK_LOCK_DELAY_INDEX, -1));
Log.i(TAG,
String.format("LM.checkLock with state=%s, isProtected=%b, isLocked=%b, isInteractive=%b, delay=%d",
intent != null ? intent.getStringExtra(EXTRA_STATE) : "",
isProtected, isLocked, isInteractive, checkLockDelays[delayIndex])
);
if (checkLockTask != null) {
Log.i(TAG, String.format("LM.checkLock: cancelling CheckLockTask[%x]", System.identityHashCode(checkLockTask)));
checkLockTask.cancel();
}
if (isProtected && !isLocked && !isInteractive) {
checkLockTask = new CheckLockTask(this, delayIndex);
Log.i(TAG, String.format("LM.checkLock: scheduling CheckLockTask[%x] for %d ms", System.identityHashCode(checkLockTask), checkLockDelays[delayIndex]));
timer.schedule(checkLockTask, checkLockDelays[delayIndex]);
} else {
Log.d(TAG, "LM.checkLock: no need to schedule CheckLockTask");
if (isProtected && isLocked) {
Log.e(TAG, "Do important stuff here!");
}
}
}
static final int SECOND = 1000;
static final int MINUTE = 60 * SECOND;
// This tracks the deltas between the actual options of 5s, 15s, 30s, 1m, 2m, 5m, 10m
// It also includes an initial offset and some extra times (for safety)
static final int[] checkLockDelays = new int[] { 1*SECOND, 5*SECOND, 10*SECOND, 20*SECOND, 30*SECOND, 1*MINUTE, 3*MINUTE, 5*MINUTE, 10*MINUTE, 30*MINUTE };
static int getSafeCheckLockDelay(final int delayIndex) {
final int safeDelayIndex;
if (delayIndex >= checkLockDelays.length) {
safeDelayIndex = checkLockDelays.length - 1;
} else if (delayIndex < 0) {
safeDelayIndex = 0;
} else {
safeDelayIndex = delayIndex;
}
Log.v(TAG, String.format("getSafeCheckLockDelay(%d) returns %d", delayIndex, safeDelayIndex));
return safeDelayIndex;
}
class CheckLockTask extends TimerTask {
final int delayIndex;
final Context context;
CheckLockTask(final Context context, final int delayIndex) {
this.context = context;
this.delayIndex = delayIndex;
}
@Override
public void run() {
Log.i(TAG, String.format("CLT.run [%x]: redirect intent to LockMonitor", System.identityHashCode(this)));
final Intent newIntent = new Intent(context, LockMonitor.class);
newIntent.setAction(ACTION_CHECK_LOCK);
newIntent.putExtra(EXTRA_CHECK_LOCK_DELAY_INDEX, getSafeCheckLockDelay(delayIndex + 1));
context.startService(newIntent);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "LM.onBind");
return null;
}
}
Upvotes: 5