2

Scenario

I have implemented an Android App Widget.

Clicking on the widget will launch a broadcast by using PendingIntent.getBroadcast(...).

I want to do a network request inside onReceive of the Broadcast Receiver.

(You ask why don't I use PendingIntent.getService(...) and launch an IntentService? Well that's a natural idea, but sadly due to background limitations, the service cannot be started if the app is not in foreground. You can take a look at this post.)

Problem

To prove it works, I have implemented a sample BroadcastReceiver:

class WidgetClickBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context?, intent: Intent?) {
        if (context == null || intent == null) return

        Log.i("Sira", "onReceive called")

        val pendingResult = goAsync()
        Observable.just(true).delay(3, TimeUnit.SECONDS)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe {
                Log.i("Sira", "Fake completion of network call")
                pendingResult.finish()
        }
    }

}

Yeah it works.

However, I noticed that if I tap on the widget multiple times, multiple broadcast will be created and queued up one by one until the previous one's pendingResult.finish() is called.

This can be explained by the documentation of goAsync():

Keep in mind that the work you do here will block further broadcasts until it completes, so taking advantage of this at all excessively can be counter-productive and cause later events to be received more slowly.

So I want to know if there is a way to prevent the same broadcast from firing multiple times if it is already in the queue?

Or any other way to prevent queued up calls due to crazy clicks on the widget?

Community
  • 1
  • 1
Sira Lam
  • 5,179
  • 3
  • 34
  • 68

1 Answers1

1

Edit 2: a possible solution for a widget is:
Save a timestamp to the SharedPreferences (for each action if you need it) once your action is completed.
Once the onReceive is called again check the timestamp for your preferred millis delta and only run the action again if the delta is long enough.

Edit1: the answer below does not work for widgets, I'll leave it for anyone looking for the "regular" case

I've tried quite a few things (including using a Handler and Reflection), finally I've come up with the following solution: when you receive a message you do not want to get again, unregister (that specific action) and register when the action is done. the BroadcastReceiver is below and here is the full example project

package com.exmplae.testbroadcastreceiver;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;

import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

public class SelfRegisteringBroadcastReceiver extends BroadcastReceiver {

    private static final String TAG = "SelfRegisteringBR";
    public static final String TEST_ACTION1 = "TEST_ACTION1";
    public static final String TEST_ACTION2 = "TEST_ACTION2";
    private final ArrayList<String> registeredActions = new ArrayList<>();
    private final ILogListener logListener;
    private final Object registeringLock = new Object();

    public static IntentFilter getIntentFilter() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(TEST_ACTION1);
        intentFilter.addAction(TEST_ACTION2);

        return intentFilter;
    }

    public SelfRegisteringBroadcastReceiver(ILogListener logListener) {
        this.logListener = logListener;
        registeredActions.add(TEST_ACTION1);
        registeredActions.add(TEST_ACTION2);
    }

    private void register(Context context, String action) {
        synchronized (registeringLock) {
            if (!registeredActions.contains(action)) {
                registeredActions.add(action);
                context.unregisterReceiver(this);
                register(context);
            }
        }
    }

    private void register(Context context) {
        IntentFilter intentFilter = new IntentFilter();
        for (String action : registeredActions) {
            intentFilter.addAction(action);
        }

        context.registerReceiver(this, intentFilter);
    }

    private void unregister(Context context, String action) {
        synchronized (registeringLock) {
            if (registeredActions.contains(action)) {
                registeredActions.remove(action);
                context.unregisterReceiver(this);
                register(context);
            }
        }
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        logListener.d(TAG, "onReceive");
        if (intent == null) {
            logListener.e(TAG, "intent = null");
            return;
        }

        String action = intent.getAction();
        if (action == null) {
            logListener.e(TAG, "action = null");
            return;
        }

        //noinspection IfCanBeSwitch
        if (action.equals(TEST_ACTION1)) {
            doAction1(context, TEST_ACTION1);
        } else if (action.equals(TEST_ACTION2)) {
            doAction2();
        } else {
            logListener.e(TAG, "Received unknown action: " + action);
        }
    }

    private void doAction1(final Context context, final String actionName) {
        logListener.d(TAG, "doAction1 start (and unregister)");
        unregister(context, actionName);
        Observable.just(true).delay(10, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Boolean>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        logListener.d(TAG, "doAction1 - onSubscribe");
                    }

                    @Override
                    public void onNext(Boolean aBoolean) {
                        logListener.d(TAG, "doAction1 - onNext");
                    }

                    @Override
                    public void onError(Throwable e) {
                        logListener.e(TAG, "doAction1 - onError");
                    }

                    @Override
                    public void onComplete() {
                        logListener.d(TAG, "doAction1 - onComplete (and register)");
                        register(context, actionName);
                    }
                });

        logListener.d(TAG, "doAction1 end");
    }

    private void doAction2() {
        logListener.d(TAG, "doAction2 start");
        Observable.just(true).delay(3, TimeUnit.SECONDS)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Boolean>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        logListener.d(TAG, "doAction2 - onSubscribe");
                    }

                    @Override
                    public void onNext(Boolean aBoolean) {
                        logListener.d(TAG, "doAction2 - onNext");
                    }

                    @Override
                    public void onError(Throwable e) {
                        logListener.e(TAG, "doAction2 - onError");
                    }

                    @Override
                    public void onComplete() {
                        logListener.d(TAG, "doAction2 - onComplete");
                    }
                });

        logListener.d(TAG, "doAction2 end");
    }

}
MikeL
  • 2,756
  • 2
  • 23
  • 41
  • Thank you for your answer! Yours indeed work as a local broadcast. But unfortunately the case of widget is not; and you can also observe that your action2 is not queued up as well, it is still triggered even if the previous one is running. It simply stacks up, but not queued up. For the case of widget (PendingIntent), it is queued up. – Sira Lam Dec 19 '18 at 07:44
  • @SiraLam I'm not sure I follow you, action1 is intended to work once (at the same time) and action2 multiple times - for the sake of the example. Do you mean it is not working from the widget as it does from the Main thread? – MikeL Dec 19 '18 at 07:49
  • The broadcast receiver of widget has 2 things very different from your example, (1) The broadcast is received through a pendingIntent; (2)and therefore, It does not have an intent filter, the broadcast receiver is specified directly without any action (intent filter) when building the pending Intent. – Sira Lam Dec 19 '18 at 07:52
  • I see, it's been more than a while since I've looked at widgets, my bad :) Then I have an idea, but it's hacky: you can use `SharedPreferences` to store your state and check for it before starting the action – MikeL Dec 19 '18 at 08:07
  • But the point is, I don't even have a chance to check because `onReceive` is called only after the previous one is finished lol – Sira Lam Dec 19 '18 at 08:13
  • Damn, sorry... and it's a remote view so you can't know there, right? I'm out of ideas – MikeL Dec 19 '18 at 08:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/185468/discussion-between-mikel-and-sira-lam). – MikeL Dec 19 '18 at 08:38