CanProgram
CanProgram

Reputation: 373

How start different activity (and recover state) if service is running?

I've got a fairly complete location tracking app, but one problem is still bugging me. When launched, the app needs to display the second activity if the tracking service is currently running.

However, if the app is tracking and MainActivity has been killed and then the user opens the app via the regular launcher icon, they're taken to LoginActivity. LoginActivity is the typical entry point for the app, as defined in the manifest:

<intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>

How can I have MainActivity be used as an alternate entry point if LocationService is currently tracking? How should it recover its previous state data?


Steps I think would be required:

  1. When tracking starts, MainActivity stores the DB IDs of the Accounts and Assets in SharedPreferences
  2. When LoginActivity is created, check whether LocationService is running
  3. If so, launch MainActivity with a special "restore from SharedPreferences" extra
  4. MainActivity.onCreate() displays a loading dialog and gets the DB IDs from SharedPreferences
  5. Broadcast to ReportingService that we need to fetch the objects corresponding to the DB IDs
  6. Listen for response, then update the UI and cancel the loading dialog

What's the best way to approach these problems?

Upvotes: 3

Views: 321

Answers (2)

CanProgram
CanProgram

Reputation: 373

I solved my problem thanks to David Wasser's helpful guidance. I've included everything that's required in this answer to help anyone else who encounters this problem.


As soon as LoginActivity is created, it (indirectly) checks whether we're tracking:

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);

    // It's possible that we're already tracking. If so, we want to skip LoginActivity and start MainActivity.
    if(Utilities.doesTrackingPendingIntentExist(this))
    {
        if(Utilities.isLocationServiceRunning(this))
        {
            recreateMainActivity();
        }
        else
        {
            Log.e(TAG, "TRACKING? PendingIntent exists, but LocationService isn't running.");
            Utilities.deleteTrackingPendingIntent(this);
        }
    }

    // LocationService wasn't running, so we can display the login screen and proceed as normal
    setContentView(R.layout.activity__login);
    ...

If so, it grabs the PendingIntent that LocationService had created for the notification and it uses it to start MainActivity.

private void recreateMainActivity()
{
    // This intent is an abstract description of what we want to accomplish: starting MainActivity
    Intent intentToStartMainActivity = new Intent(this, MainActivity.class);

    // Get the PendingIntent that's stored in the notification (using the "requestCode" that LocationService used
    // when it created the PendingIntent)
    PendingIntent pendingIntent = PendingIntent.getActivity
    (
        this, LocationService.NOTIFICATION_ID, intentToStartMainActivity, PendingIntent.FLAG_NO_CREATE
    );

    try
    {
        Log.i(TAG, "LocationService is running. Attempting to recreate MainActivity!");

        // Now start MainActivity with the Intent wrapped in the PendingIntent (which also contains the required data in extras)
        pendingIntent.send();
        finish();
    }
    catch(PendingIntent.CanceledException e)
    {
        Log.e(TAG, "It seems that our PendingIntent was cancelled. Hmmm....", e);
    }
}

Here's the Utilities function we use to determine whether we're tracking. It checks whether a matching PendingIntent already exists based on the ID and the Intent. If the PendingIntent is null, that means no match was found, so we take that to mean that the notification doesn't exist and we aren't tracking. In API 23+, you can directly check if a notification exists, which would be slightly safer than this (as the PendingNotification could continue to exist after the notification is gone if the service is unexpectedly killed).

public static boolean doesTrackingPendingIntentExist(Context context)
{
    Intent intentToStartMainActivity = new Intent(context, MainActivity.class);

    // Get the PendingIntent that's stored in the notification (using the "requestCode" that LocationService used
    // when it created the PendingIntent)
    PendingIntent pendingIntent = PendingIntent.getActivity
    (
        context, LocationService.NOTIFICATION_ID, intentToStartMainActivity, PendingIntent.FLAG_NO_CREATE
    );

    if(pendingIntent == null)
    {
        Log.i(TAG, "TRACKING? No matching PendingIntent found. LocationService probably isn't running.");
        return false;
    }
    else
    {
        Log.i(TAG, "TRACKING? A matching PendingIntent was found. LocationService seems to be running.");
        return true;
    }
}

An alternate method that checks whether service is running by looping through all running services looking for a name match. Since my LocationService doesn't always die immediately after onDestroy(), this alone isn't a perfectly reliable way to check whether we're tracking. It can be combined with the other method for a more certain determination of tracking status.

public static boolean isLocationServiceRunning(Context context)
{
    Log.i(TAG, "TRACKING? Reviewing all services to see if LocationService is running.");

    ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

    // Go through every service until we find LocationService
    for(ActivityManager.RunningServiceInfo service : activityManager.getRunningServices(Integer.MAX_VALUE))
    {
        Log.v(TAG, "TRACKING?    service.getClassName() = " + service.service.getClassName());

        if(LocationService.class.getName().equals(service.service.getClassName()))
        {
            Log.i(TAG, "TRACKING? LocationService is running!");
            return true;
        }
    }

    Log.i(TAG, "TRACKING? LocationService is NOT running.");
    return false;
}

Caution: It's very important that LocationService cancels the PendingIntent when it's done tracking or this won't work. Unfortunately, there's no guarantee that LocationService.onDestroy() will be called by the OS. Android could kill it without calling that. It runs with foreground priority, so it's unlikely to be unexpectedly killed, but it could result in the PendingIntent existing while you're not tracking.

Combining both of those utility functions is the safest way to determine whether we're tracking.

Side note: I tried using a static volatile boolean to track the tracking state in LocationService, but the different processes seemed to use different ClassLoaders have their own memory spaces (thanks David). If your code is all in the same process, that approach may work for you.

Upvotes: 1

David Wasser
David Wasser

Reputation: 95568

In LoginActivity.onCreate() you should check to see if the tracking services are running and if so, immediately forward the user to MainActivity. You want to do this as if the user clicked on the Notification, so that you can use the extras in the PendingIntent that you have stored in the Notification. No problem.

In LoginActivity.onCreate() do this:

// Find the PendingIntent that is stored in the Notification
Intent notificationIntent = new Intent(this, MainActivity.class);
// Add any ACTION or DATA or flags that you added when you created the
//  Intent and PendingIntent when you created the Notification

// Now get the `PendingIntent` that is stored in the notification
//  (make sure you use the same "requestCode" as you did when creating
//  the PendingIntent that you stored in the Notification)
PendingIntent pendingIntent = PendingIntent.getActivity(this,
    requestCode, notificationIntent, PendingIntent.FLAG_NO_CREATE);
// Now start MainActivity with the Intent wrapped in the PendingIntent
//  (it contains the extras)
pendingIntent.send();
// Finish LoginActivity (if you usually do that when you launch MainActivity)
finish();

Upvotes: 1

Related Questions