16

I currently have a periodic issue where I get an IllegalArgumentException when I call Activity.startLockTask(). My app has a device owner app installed which has allowed my package to automatically pin itself.

The code below is checking to make sure my package can lock itself. If it can then it pins itself.

Code:

if (dpm.isLockTaskPermitted(getPackageName())) {
    super.startLockTask();
}

Logcat:

java.lang.IllegalArgumentException: Invalid task, not in foreground
    at android.os.Parcel.readException(Parcel.java:1544)
    at android.os.Parcel.readException(Parcel.java:1493)
    at android.app.ActivityManagerProxy.startLockTaskMode(ActivityManagerNative.java:5223)
    at android.app.Activity.startLockTask(Activity.java:6163)

The issue is my app needs to occasionally restart itself. So we unpin, finish the activity and start it again with a new task, and then exit our process. When the activity comes back up it tries to pin itself - sometimes it works - sometimes it doesn't. I believe how we restart is probably the reason the exception is thrown but it shouldn't matter since the new activity IS in the foreground and IS focused.

Once the activity fails to pin it will continue to fail as long as it tries: If I sit there and try and pin the task every 5 seconds it will continue to fail each time. I've tried pinning in onCreate, onWindowFocusChanged, onResume, and onStart.

Does anyone know what the issue might be?

For reference:
Line 8853: https://android.googlesource.com/platform/frameworks/base/+/android-5.0.2_r1/services/core/java/com/android/server/am/ActivityManagerService.java

Randy
  • 4,351
  • 2
  • 25
  • 46
  • The use of `mFocusedActivity` in `ActivityManagerService` would seem to imply that `onWindowFocusChanged(true)` is in fact the right place to do this. Disclaimer: I haven't tried to use the task locking feature yet. – j__m Jan 09 '15 at 20:03
  • If you suspect the manner in which you restart to be the issue, then perhaps you could restart in a more orderly manner? Bind a service that runs in a separate process. Pass it a `Binder` created in the main process. Have the main process unlock the task, finish, and exit. Have the service wait for an object death notification before attempting to respawn your activity. – j__m Jan 09 '15 at 20:06
  • 1
    That's basically what we're doing. I'm going to create a simple app and see if I can get it to do the same thing. – Randy Jan 10 '15 at 04:00
  • 1
    Don't exit until you get the `onStop()` from your `finish()` call. With that clarification, I can't imagine a more orderly restart process. – j__m Jan 10 '15 at 04:05
  • That might help. Currently we're calling finish and within the same function we're killing the process. "Letting it finish" might be the right way to do this. – Randy Jan 10 '15 at 04:06
  • Did you ever conclusively identify the problem? – Kevin Krumwiede Feb 14 '15 at 20:44
  • 1
    Not yet. I haven't had the time. – Randy Feb 14 '15 at 20:59

6 Answers6

8

I have the same issue, I haven't found a proper solution yet. But this is what I currently do.

Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
        try {
            if (dpm.isLockTaskPermitted(getPackageName())) {
                super.startLockTask();
            }
        }catch (Exception exception) {
            Log.v("KioskActivity","startLockTask - Invalid task, not in foreground");
        }
    }
},1500);

It seems that the application requesting the lock hasn't yet received focus even though the onWindowFocusChanged is fired. By delaying the call to startLocktask by some time it will work. How ever there is a small period of time where the app won't be pinned/locked. I've solved this by some additional security measures (I have a long running service in the background that prevents notification shade pull downs and will close the settings window if opened).

Btw, did you ever manage to solve this in a adequate way?

Schtibb
  • 151
  • 2
  • 8
3

I had this issue and resolved it using the logic taken from the answers in this post: How can you tell when a layout has been drawn?

Basically it just ensures that the UI has been drawn first then attempts to pin.

Example Code (Put this in your onCreate method):

> findViewById(R.id.your_view).post( new Runnable() {
>             @Override
>             public void run() {
> 
>                 // Run pinning code here
>             }
>         });
C. Carter
  • 291
  • 2
  • 11
1

I know this is quite an old question, but I ran into it as well and here's how I solved it. The key is this sentence in the docs for startLockTask() (the same goes for stopLockTask() too)

Note: this method can only be called when the activity is foreground. That is, between onResume() and onPause()

I had some code paths that ended up trying to call startLockTask() before onResume(). I fixed it by ensuring the right activity state (using AndroidX lifecycle)

if(lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
    startLockTask()
}

This was sufficient for my case. You might need to do some additional logic like this (pseudocode):

if(not yet resumed) {
    doWhenResumed { // Implement this as a helper function that executes when your Activity is resumed
        startLockTask()
    }
}
curioustechizen
  • 10,572
  • 10
  • 61
  • 110
0

The error say the app is not in foreground so i have make a loop in onStart method that check if app is in foreground

 boolean completed = false;
                while (!completed)
                    if (isAppInForeground(this)) {
                        startLockTask();
                        completed = true;
                    }

The function isAppInForeground

  private boolean isAppInForeground(Context context) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        ActivityManager am = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE);
        ActivityManager.RunningTaskInfo foregroundTaskInfo = am.getRunningTasks(1).get(0);
        String foregroundTaskPackageName = foregroundTaskInfo.topActivity.getPackageName();

        return foregroundTaskPackageName.toLowerCase().equals(context.getPackageName().toLowerCase());
    } else {
        ActivityManager.RunningAppProcessInfo appProcessInfo = new ActivityManager.RunningAppProcessInfo();
        ActivityManager.getMyMemoryState(appProcessInfo);
        if (appProcessInfo.importance == IMPORTANCE_FOREGROUND 
                || appProcessInfo.importance == IMPORTANCE_VISIBLE) {
            return true;
        }

        KeyguardManager km = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
        // App is foreground, but screen is locked, so show notification
        return km.inKeyguardRestrictedInputMode();
    }
}
0

This is similar to @Schtibb answer, However I did not feel very comfortable with hard coding only a single 1500ms delay without any retry logic. It could still fail occasionally.

But what I found that did do the trick was to use a try when calling startLockTask(), and if it fails just catch the error and try again after a short delay until it succeeds:

void startLockTaskDelayed () {
    final Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            // start lock task mode if its not already active
            try {
                ActivityManager am = (ActivityManager) getSystemService(
                        Context.ACTIVITY_SERVICE);

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    if (am.getLockTaskModeState() ==
                            ActivityManager.LOCK_TASK_MODE_NONE) {
                        startLockTask();
                    }
                }

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    if (!am.isInLockTaskMode()) {
                        startLockTask();
                    }
                }
            } catch(IllegalArgumentException e) {
                Log.d("SVC0", "Was not in foreground yet, try again..");
                startLockTaskDelayed();
            }
        }
    }, 10);
}

I feel this approach is slightly more dynamic and pins the screen (almost) as soon as possible.

Logic1
  • 1,806
  • 3
  • 26
  • 43
0

I have created a separate Activity for kiosk mode

The main idea is to deliver this task from other activities and keep the lock task always in the root of stack

View.post(...) and similar with magic delay is not working for me

onResume() method with checking isFinishing() works fine, but be careful with clear task flag

<style name="AppTheme.Transparent" parent="android:style/Theme.Translucent.NoTitleBar.Fullscreen">
    <item name="android:colorPrimary">@color/colorPrimary</item>
    <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="android:colorAccent">@color/colorAccent</item>
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android: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>
class LockActivity : Activity() {

    private lateinit var adminManager: AdminManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        adminManager = AdminManager(applicationContext)
    }

    override fun onStart() {
        super.onStart()
        when (activityManager.lockTaskModeState) {
            ActivityManager.LOCK_TASK_MODE_NONE -> {
                if (true/*IT IS NEEDED*/) {
                    if (adminManager.setKioskMode(true)) {
                        // need startLockTask on resume
                        return
                    } else {
                        //toast("HAVE NO OWNER RIGHTS")
                    }
                }
                launchActivity()
                finish()
            }
            ActivityManager.LOCK_TASK_MODE_LOCKED -> {
                if (false/*IT IS NOT NEEDED*/) {
                    if (adminManager.setKioskMode(false)) {
                        stopLockTask()
                        launchActivity()
                        finish()
                        return
                    } else {
                        //toast("HAVE NO OWNER RIGHTS")
                    }
                }
                launchActivity()
            }
            else -> finishAffinity()
        }
    }

    override fun onResume() {
        super.onResume()
        if (!isFinishing) {
            if (activityManager.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_NONE) {
                startLockTask()
                launchActivity()
            }
        }
    }

    private fun launchActivity() {
        // todo startActivity depending on business logic
    }

    override fun onBackPressed() {}
}
class AdminManager(context: Context) {

    private val adminComponent = ComponentName(context, AdminReceiver::class.java)

    private val deviceManager = context.devicePolicyManager

    private val packageName = context.packageName

    @Suppress("unused")
    val isAdmin: Boolean
        get() = deviceManager.isAdminActive(adminComponent)

    val isDeviceOwner: Boolean
        get() = deviceManager.isDeviceOwnerApp(packageName)

    fun setKioskMode(enable: Boolean): Boolean {
        if (isDeviceOwner) {
            setRestrictions(enable)
            deviceManager.setKeyguardDisabled(adminComponent, enable)
            setLockTask(enable)
            return true
        }
        return false
    }

    /**
     * @throws SecurityException if {@code admin} is not a device or profile owner.
     */
    private fun setRestrictions(enable: Boolean) {
        arrayOf(
            UserManager.DISALLOW_FACTORY_RESET,
            UserManager.DISALLOW_SAFE_BOOT,
            UserManager.DISALLOW_ADD_USER
        ).forEach {
            if (enable) {
                deviceManager.addUserRestriction(adminComponent, it)
            } else {
                deviceManager.clearUserRestriction(adminComponent, it)
            }
        }
    }

    /**
     * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
     * affiliated user or profile, or the profile owner when no device owner is set.
     */
    private fun setLockTask(enable: Boolean) {
        if (enable) {
            deviceManager.setLockTaskPackages(adminComponent, arrayOf(packageName))
        } else {
            deviceManager.setLockTaskPackages(adminComponent, arrayOf())
        }
    }
}
Vlad
  • 7,997
  • 3
  • 56
  • 43
  • Could you add a comment why do you set a lock task on a separate translucent activity and then launch a new activity (LoginActivity). Since the lock task is attached to an activity, how that works for you (finishing LockActivity and opening LoginActivity)? You either should not be able to open LoginActivity or you should lose the lock. Am I wrong? – Greg Rynkowski Apr 01 '20 at 07:01
  • @GregRynkowski If you finish activity with lock task, then an error will crash the app. I close the `LockActivity` only when the app should not be in kiosk mode. Also it is importatnt to call `startLockTask()` on resume – Vlad Apr 01 '20 at 07:15
  • @GregRynkowski also be aware of long press of back button )) This will turn off the kiosk mode. I fixed this by switching back when `override fun onKeyLongPress(keyCode: Int, event: KeyEvent?)` and `if (keyCode == KeyEvent.KEYCODE_BACK)` fire – Vlad Apr 01 '20 at 07:25
  • From what you say, it seems to me that 1) you have LoginActivity 2) you put the transparent LockActivity on top 3) allow to unlock on long press of back button. So.... When you enable kiosk mode (when the Lock Activity is on top) does the user can interact with LoginActivity? – Greg Rynkowski Apr 01 '20 at 07:38