4

I have an Android Service that I'd like to take actions whenever the device is locked.

I'd like to clarify that:

  • I am not interested in the screen on/off state.
  • I am aware of how to use a BroadcastReceiver with Intent.ACTION_USER_PRESENT and KeyguardManager.inKeyguardRestrictedInputMode to check for when the device is unlocked.
  • I am not interested in a solution that requires constantly checking the lock state.
  • I don't want to lock or unlock the device myself.
  • I cannot rely on an Activity being in the foreground to handle onResume.

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.

TrevorWiley
  • 848
  • 1
  • 7
  • 16
  • I assume you've already read this http://stackoverflow.com/questions/3170563/android-detect-phone-lock-event but I'll put it in anyway – Populus Feb 01 '16 at 19:57
  • @Populus, I keep stumbling onto that one and getting excited for a few seconds until I realize that I've seen it before. The answers describe detecting screen off (which isn't the same as locked) or using onPause (which isn't applicable to a background service). – TrevorWiley Feb 01 '16 at 20:12
  • Have you tried this? http://stackoverflow.com/questions/8317331/detecting-when-screen-is-locked – Andrei Mărcuţ Feb 01 '16 at 20:17
  • @Markus, that one doesn't help either. The answers tell how to check the current lock state (not how to take action when the state changes) or how to be informed when the screen state changes to off (which isn't the same as device locked). – TrevorWiley Feb 01 '16 at 20:20
  • You need to combine the two answers to make your own service. When you get SCREEN_OFF broadcast, check keyguard status :) – Andrei Mărcuţ Feb 01 '16 at 20:23
  • I thought of that, but it doesn't work. Depending on the security settings, the device might become locked immediately, or several seconds or even minutes later. – TrevorWiley Feb 01 '16 at 20:26
  • 1
    There should be a finite, known maximum time, and you could use timers for these checks. IMHO, t's a reasonable solution - you're not "always checking the state" - just after it turns off the screen and for the known lock timeout settings. – Andrei Mărcuţ Feb 01 '16 at 20:28
  • @Markus, that isn't a bad idea. I could have a backoff timer that triggers shortly after each of the time periods. I may have to consider how different manufacturers handle this, and I'd have to see how Doze impacts this. Could you add that as a answer and I'll select it if nothing 'better' is offered. – TrevorWiley Feb 01 '16 at 20:36
  • I'm afraid you're gonna have to code it yourself, if you will find it a suitable idea. This is a very particular solution for your specific need and I would have to ask you a lot of questions before even starting. Good luck! – Andrei Mărcuţ Feb 01 '16 at 20:51
  • That's okay, I can do the grunt work, I just didn't want to leave a good idea unrecognized. – TrevorWiley Feb 01 '16 at 20:55
  • Thing is, it's the 'grunt work' that deserves the credit as it could turn useful for others too. Don't forget to share it once you're done! – Andrei Mărcuţ Feb 01 '16 at 21:46

1 Answers1

5

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:

  • Other manufacturers may have different lock periods.
  • Device policy (e.g.: Android for Work) could change to enforce periods after we previously determined that the device would not lock during that period (e.g.: device could suddenly lock and we wouldn't detect for several minutes).
  • Device could be locked remotely by Android Device Manager.
  • Device could be locked by another application (e.g.: bluetooth based lock mechanism).
  • Untested but I suspect there are issues in this code if the user turns device on and off quickly several times.
  • Untested with Doze.
  • Untested, but suspect there might be issues around switching users.
  • I have not tested this in anger, there may well be other issues.
  • Anyone actually using this approach should probably do a bit of a re-arch; what is presented below is just a proof of concept.

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;
    }
}
TrevorWiley
  • 848
  • 1
  • 7
  • 16