3

My activity starts a service which runs a CountDownTimer. The timer sends broadcasts back to the activity as it counts down. The activity processes the broadcasts in the onReceive method of a BroadcastReceiver. All of this works fine.

My problem comes when the following events happen in this order:

  1. App is stopped (via onPause())
  2. Timer finishes
  3. App is resumed (via onResume())

When the app is resumed the service is no longer sending broadcasts, so the activity does not know how much time is left on the timer or if it's finished. This prevents the activity from updating the UI.

I've tried a dozen ways of dealing with this, and read through many Stack Overflow questions and answers, but I've yet to find a solution. I would think that there's a way to pick up a broadcast that was sent while the activity was not active, but I've yet to find a way.

For the record, here is my relevant Activity and Service code:

activity.java

// Start service
timerIntent.putExtra("totalLength", totalLength);
this.startService(timerIntent);

// ...

// BroadcastReceiver
private BroadcastReceiver br = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getExtras() != null && inSession) {
            session.setRemaining(intent.getExtras().getLong("millisUntilFinished"));
            updateProgress();
        }
    }
};

// ...

// onResume
@Override
public void onResume() {
    super.onResume();
    registerReceiver(br, new IntentFilter(TimerService.COUNTDOWN_TS));
}

service.java

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    long length = intent.getExtras().getLong("totalLength");

    countDownTimer = new CountDownTimer(length, 1000) {
        @Override
        public void onTick(long millisUntilFinished) {
            timerServiceIntent.putExtra("millisUntilFinished", millisUntilFinished);
            sendBroadcast(timerServiceIntent);
        }

        @Override
        public void onFinish() {
        }
    };

    countDownTimer.start();

    return super.onStartCommand(intent, flags, startId);
}

What's the best way to process the broadcasts that the service sent while the activity was stopped?

Alex Johnson
  • 958
  • 8
  • 23
  • Please explain exactly what you mean by "app is stopped". "I would think that there's a way to pick up a broadcast that was sent while the activity was not active" -- broadcasts are not a message queue. – CommonsWare May 05 '16 at 11:51
  • When I say "app is stopped," I mean that the activity's onStop() method has run (because the screen has turned off, another app is active, etc.). I'll update the question. – Alex Johnson May 05 '16 at 11:55

4 Answers4

3

Use the BroadcastReceiver to store the last request (SharedPreferences perhaps) it received and check it when the Activity starts.

Alternatively, instead of processing a countdown using broadcasts, just store the time that the countdown would end. The Activity can then handle the countdown all by itself as it knows when it should end. Using a service and broadcasts seem to be a little over-engineered for such a simple task.

Update: From the way you have described your task, I see you needing to handle 2 scenarios. This is how I would likely do it.

Assuming that "XYZ" is the service\intent\whatever starting the countdown and "ABC" is the Activity displaying the progress. (ABC and XYZ could be the same activity if that is what you wanted)

Requirements: When the countdown starts, I would make XYZ store the time that the countdown should end in SharedPreferences.

  1. ABC is already running when the countdown starts. As Commonsware said, the Eventbus model is excellent for handling this scenario so long as XYZ and ABC are running in the same process. Just fire an event to read the preference value and count down to the specified time. If the user closes ABC and reopens it, Scenario 2 will kick in.

  2. ABC is not running. Check in OnResume whether the countdown time has elapsed. If not, set up ABC to display the countdown again. If there is no countdown active, do something else.

If you also need to do something when the countdown has elapsed regardless of whether you have a UI active, then again Commonsware's suggestion of AlarmManager is perfect.

Kuffs
  • 35,581
  • 10
  • 79
  • 92
  • I'm definitely getting the feeling that my solution is over-engineered (based on your and @commonsware's answers). How would I return to the activity if it had been paused/stopped? AlarmManager? I'm honestly just having a hard time figuring out what is "best practice" for a situation like this. – Alex Johnson May 05 '16 at 13:14
  • I wouldn't say there is a "best practice" but rather a "best practice for you". Use the simplest method that gets the job done. Avoid starting unnecessary services though. My solution would work for the task as described but if you also have another requirement that you didn't specify or another scenario to handle, that could change the answer. See update – Kuffs May 05 '16 at 13:45
1

Let's pretend for a moment that using a Service with a CountDownTimer to track some passage of time for the purposes of updating an Activity actually is a good idea. It's not out of the question, assuming that the Service is actually doing something for real and this timing thing is some by-product.

An activity does not receive broadcasts while stopped, mostly for performance/battery reasons. Instead, the activity needs to pull in the current status when it starts, then use events (e.g., your current broadcasts) to be informed of changes in the data while it is started.

This would be simplified by using something like greenrobot's EventBus and their sticky events, as the activity would automatically get the last event when it subscribes to get events. Using greenrobot's EventBus for this purpose would also reduce the security and performance issues that you are introducing by your use of system broadcasts to talk between two Java classes in the same process.

Also, please stick with lifecycle pairs. onResume() is not the counterpart to onStop(). onStart() is the counterpart to onStop(); onResume() is the counterpart to onPause(). Initializing something in one pair (e.g., onResume()) and cleaning it up in the other pair (e.g., onStop()) runs the risk of double-initialization or double-cleanup errors.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • The point of the service is just to run the timer. I'm basing the model off of this answer: http://stackoverflow.com/a/22498307/3292279. What would be a better way to do this? Basically my app just creates a timer, counts down, and then plays a sound when the timer finishes. Also, edited my question to reflect that onPause() and onResume() are paired. – Alex Johnson May 05 '16 at 13:02
  • @AlexJohnson: Use `AlarmManager` to get control at the end of the time to play your sound (or perhaps raise a `Notification`). If you want to show an in-activity countdown, follow Kuffs' suggestion of tracking the end time, and then use something super-cheap like [a `postDelayed()` loop](https://github.com/commonsguy/cw-omnibus/tree/master/Threads/PostDelayed) to update your UI. [Only have a service running when it is actively delivering value to the user](https://commonsware.com/blog/2014/07/27/role-services.html). Watching the clock tick does not qualify. – CommonsWare May 05 '16 at 13:08
  • OK. Thank you for the pointers. I'll work on this and see how I can simplify. – Alex Johnson May 05 '16 at 13:25
1

What's the best way to process the broadcasts that the service sent while the activity was stopped?

Using sticky broadcast intents from the service and then retrieving them from the activity would be a way to process the broadcasts that the service sent while the activity was stopped. I can only offer that as a possible solution rather than claiming it is the "best way".

http://developer.android.com/reference/android/content/Context.html#sendStickyBroadcast(android.content.Intent)

They have however, been deprecated since API level 21 due to security concerns.

0

Instead of using Normal broadcast you can use Ordered broadcast (sent with Context.sendOrderedBroadcast). For this along with defining a BroadcastReceiver in your activity you required to define BroadcastReceiver in your manifest with same intentfilter. Only change is while registering BroadcastReceiver in your activity you need to set priority to high, so that when your activity is running and activity's BroadcastReceiver is registered it gets called first, and inside onReceive of this BroadcastReceiver you can use abortBroadcast for getting the BroadcastReceiver called which is defined in your android manifest. Now when your activity is not running the BroadcastReceiver defined in your android manifest will get called. So this way you can have the status and if you wish you can display updates to user by notification even if your activity is not running.

Rajen Raiyarela
  • 5,526
  • 4
  • 21
  • 41