cantrowitz / RxBroadcast

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Support abortBroadcast

thekalinga opened this issue · comments

Currently the documentation does not talk about how to abort broadcasts incase of ordered broadcast.

Is this supported?

@thekalinga Can you please elaborate? abortBroadcast only applies to ordered broadcasts and not LocalBroadcast.

Sorry.. I completely missed the target in my original question.

If I send an ordered broadcast & register a receiver using RxBroadcast.fromBroadcast, since I dont have access to underlying broadcast receiver in the callback, I will not be able to abort the broadcast if I chose to cancel propagation.

Updated the original question

Take a look at #5 and let me know if that works for you.

Can you split the interface OrderedBroadcastAborter into two interfaces. It helps with Java 8 lambdas.

i.e to use API you defined right now, in Java 8 the uasge will look like

RxBroadcast.fromBroadcast(context, intentFilter, new OrderedBroadcastAborter() {
  boolean shouldAbortBroadcast(Context context, Intent intent) {
   return true;
  }

  boolean shouldClearAbortBroadcast(Context context, Intent intent) {
   return false;
  }
});

If you split the interface into two OrderedBroadcastAborter, OrderedBroadcastCascadeEnabler (or some better names)

interface OrderedBroadcastAborter {
  boolean shouldAbort(Context context, Intent intent);
}

interface OrderedBroadcastCascadeEnabler {
  boolean shouldEnableCascade(Context context, Intent intent);
}

class RxBroadcast {
    // rest of the implementation
  public static Observable<Intent> fromBroadcast(Context context, IntentFilter intentFilter,, OrderedBroadcastAborter aborter, OrderedBroadcastCascadeEnabler cascadeEnabler) {
    // rest of the implementation
  }
    // rest of the implementation
}

Now, the usage will look like

RxBroadcast.fromBroadcast(context, intentFilter, (context, intent) -> true, (context, intent) -> false);

Thanks for your prompt response.

I am not sure I completely agree that this is the better solution. There is no requirement that you have to create your object as an anonymous inner class.

I think the following is much clearer:

final OrderedBroadcastAborter alwaysAbortOrderedBroadcast = new OrderedBroadcastAborter() {
  boolean shouldAbortBroadcast(Context context, Intent intent) {
   return true;
  }

  boolean shouldClearAbortBroadcast(Context context, Intent intent) {
   return false;
  }
};
.....

RxBroadcast.fromBroadcast(context, intentFilter, alwaysAbortOrderedBroadcast);

I feel that what you are proposing for the ease of inline lambdas, makes the library less precise for the following reasons:

  1. Makes the API less clear, when reading:

    intentFilter, (context, intent) -> true, (context, intent) -> false)

    the average reader has to revert to documentation to understand what is happening

  2. This spreads the concerns of one concept and puts them in two places, it is not clear that they have tightly coupled interactions

Happy to discuss this more if you have other opinions or cases that support your needs.

You have IDE to tell what a given arg is (Ctrl+q)

If you want readability, you can make it happen with what I have defined. Its all about the preference.

For eg., you make what I write more readable with the following code. Here you have option to define

  1. inline (one I mentioned earlier)
  2. within method itself as lambda does not take much boilerplate
  3. without much boilerplate involved in java inline interface implementation (anonymous or not)
  4. at the class level with no visible pollution within the method
OrderedBroadcastAborter alwaysAbort = (context, intent) -> true;
OrderedBroadcastCascadeEnabler dontCascade = (context, intent) -> false;

RxBroadcast.fromBroadcast(context, intentFilter, alwaysAbort, dontCascade);

Reg. whether both are related/not, I agree with you to some extent that they are related, but we also need to look at how frequently people use both together.

Does most of the users who abort also allow cascade/vice versa, in my opinion not.

If not, why club them in the same place where you get not much advantage. Besides, the users have the flexibility to use short forms if they so chose (and rely on IDE if so chose).

Am open for your views

Even better, if you split

OrderedBroadcastAborter alwaysAbort = (context, intent) -> true;
OrderedBroadcastCascadeEnabler dontCascade = (context, intent) -> false;

// eg. 1; default
RxBroadcast.fromBroadcast(context, intentFilter);

// eg. 2; alwaysAbort
RxBroadcast.fromBroadcast(context, intentFilter, alwaysAbort);

//eg. 3, dontCascade
RxBroadcast.fromBroadcast(context, intentFilter, dontCascade);

//eg. 4, alwaysAbort & dontCascade
RxBroadcast.fromBroadcast(context, intentFilter, alwaysAbort, dontCascade);

I have not used clearAbortBroadcast, But I believe alwaysAbort and dontCascade seems to be mutually exclusive (mostly). So we can eliminate option 4 completely. If not, we allow the flexibility in the API.

Please let me know your thoughts

From this code, it looks like they are mutually exclusive.

if (orderedBroadcastAborter.shouldAbortBroadcast(context, intent)) {
                            abortBroadcast();
                        } else if (orderedBroadcastAborter.shouldClearAbortBroadcast(
                                context,
                                intent)) {
                            clearAbortBroadcast();
}

Since they are mutually exclusive, we should not be clubbing them in the same interface.

If we club them in a single interface, user is forced to implement both methods (mutually exclusive), of which one method will never be used for any given flow.

So my view is, we should have two interfaces & support eg.1, 2 & 3 usecases & discard eg. 4 above

I'm looking to make the api as explicit as possible, taking in the same parameters types that do different things Option 4 in your example is bad api design -- see Josh Bloch for more details. There is no way to mess up the ordering with the approach I outlined. I will attempt below to establish why I think that #4 is the only option (an abort paired with a clear).

I am also trying to look at this holistically for all users (not just your use case). I am guessing the intention of Android is not not simply abort all broadcasts that pass an intent-filter but rather selectively abort/cancel abort based on some business logic from the intent. Otherwise it seems that either something malicious is being performed or you are simply not using the broadcasts properly. I feel that my hypothesis is somewhat supported by the following-- the single most used case of aborting broadcasts seem to be replacement SMS applications (quick git search). Google noticed this and changed broadcasts to handle this (http://android-developers.blogspot.com/2013/10/getting-your-sms-apps-ready-for-kitkat.html). If their intent was to always capture the broadcast and never let it propagate it seems like this would encourage a never ending rat race to have the highest priority on the system.

I would also say that from my example you have copied, this illustrates that they are not in fact exclusive, they are closely coupled in their behavior. One cannot cancel an abort if one aborts before.

That said, thank you for the insightful comments. I do find them useful. I will come up with another approach that will satisfy both of us.

Please take a look at #6, I think this addresses both of our concerns.

I'm looking to make the api as explicit as possible, taking in the same parameters types that do different things Option 4 in your example is bad api design -- see Josh Bloch for more details.

This statement is not applicable as the interface for aboter is OrderedBroadcastAborter which is different from cascade enabler OrderedBroadcastCascadeEnabler.

from my last post

OrderedBroadcastAborter alwaysAbort = (context, intent) -> true;
OrderedBroadcastCascadeEnabler dontCascade = (context, intent) -> false;

Secondly, I use abort in my application in the following manner.

Lets say you need to support conversation in your app and whenever the other party send a message to you, if you are already in the conversation screen, the you would expect the conversation to show a prompt (no blocking) saying "A new message has been received. Tap here to update conversation".

If it happens that you are not in conversation screen, you would expect a notification in the notification drawer.

The conversation screen automatically registers a broadcast receiver (with higher priority) on create and unregisters it during destroy. This broadcast receiver will abort request if the conversation message corresponds to the current conversation the user is viewing (remember that there can be many conversations), otherwise allows the broadcast to propagate to the low priority notification generating receiver.

This notification receiver can be registered in the manifest as a global broadcast receiver with lower priority as a fallback.

Your proxy implementation seems to be a reasonable middle ground, with scope for expansion.

Thanks for your prompt responses.

Looking at the whole thing again, proxy implementation is the correct implementation, neither the java 8 option nor the single interface is correct, as these are callback interfaces & can only be used at the end of onNext processing.

i.e with normal ordered broadcast; there are no assumptions about the underlying implementation.

void onReceive(...) {
  // do something; section 1
 abortBroadcast(); // call the underlying broadcast receiver; not cache the flag locally
 // do something else; section 2
  if (something else happens) {
   clearAbortBroadcast(); // call the underlying broadcast receiver; not sure about the usecase; why you would do this
  }
 // do something else; section 3
}

i.e if there is an exception at section 2 in the code above, should the underlying android implementation abort broadcast (or) proceed with broadcast. I dont know. I think we should let Android worry about how to handle this as it knows best.

We can only achieve this if we allow the user to make calls on broadcast receiver just the way he would do in a non-rx flow.

The callback implementation is wrong as it makes assumptions that calling abortBroadcast & clearAbortBroadcast at the end is equivalent to calling them in the middle, which might not be a reasonable assumption to make.

handler.onNext(intent);

// we can only apply the changes at the end; but in the original implementation, user has the flexibility to call this any any point in the broadcast handling
if (delegate.shouldAbortRequest()) {
  receiver.abortRequest();
}

if (delegate.shouldClearAbortRequest) {
  receiver.clearAbortRequest();
}

I believe your implementation of proxy is both enhancement & java 8 friendly and most importantly correct.

However, I think you should rename OrderedBroadcastStrategy -> BroadcastReceiverProxyAware (or something more descriptive), as it can potentially deal with any future expansion of BroadcastReceiverProxy, not just ordered broadcast

Do let me know your thoughts.

Sorry, I just went over the new implementation again. It does not address the concerns I raised above.

I think onNext should send the intent & proxy

i.e

class IntentAndProxy {
  public Intent intent;
  public BroadcastReceiverProxy proxy;
}

Only then we will be matching the contract of the Broadcast receiver ion the truest sense.

For eg., if we send above value in onNext, the calling code can look like this

RxBroadcast.fromBroadcast(context, intentFilter).onNext(v -> {
  // do something with v.intent; section 1
  v.proxy.abortBroadcast(); // call the underlying broadcast receiver
  // do something else; section 2
  if (something else happens) {
   v.proxy.clearAbortBroadcast(); // call the underlying broadcast receiver; not sure about the usecase on why you would do this
  }
 // do something else; section 3
}).subscribe();

Please let me know your thoughts.

I don't want to spend too much time on the first part but essentially:

(context, intent) -> boolean

is the same "type" and makes the user look up documentation when doing a code review (or using emacs/vim) when IDE is not present. But I don't think this part is relevant to continue our exploration of the API.

It seems from your latest comment that you agree with the approach (except for the naming) so I think we are making good progress.

The only other point I'd like to make is that given this new information of how you are using it, I might be so bold as to suggest that you do it differently. In my opinion I don't think this is the appropriate use case for ordered broadcasts. I feel that the purpose is to determine at a system level of when/where to propagate system events. You seem to be doing this selectively for UI purposes in your application. The Chomecast Companion Library (CCL) has a very similar use case (don't show the notification when the app is open, show the notification controls when the app is not in foreground) https://github.com/googlecast/CastCompanionLibrary-android/blob/master/src/com/google/android/libraries/cast/companionlibrary/cast/BaseCastManager.java#L433 There are many other ways to tackle this, but this is just one example. I fear that you might have unintended and hard to reproduce outcomes based on your current strategy. You are essentially changing your entire state based on UI (Imagine if you had some other broadcast receiver that did some other non-ui work, in your current strategy that part would be skipped). But I digress outside the scope of this library/PR.

I really am not following your point on onNext() and calling the proxy after.

The proxy is not part of the Rx stream. It's purpose is to alert the Android system as to whether or not to propagate the broadcasted intent past the current receiver. What purpose would you need to have this in the onNext()?

The proxy has all of the state it needs from the broadcast receiver, namely the Context and the Intent. Any decisions can and should be made with that information. I think the confusion is coming from your Global UI state that is leaking into this conversation.

One could easily inject a Singleton UI counter into the OrderedBroadcastStrategy implementation and handle this as well.

Please construct a specific use case that illustrates your point if this is not the answer you are looking for. I think you will find that the current proposed solution handles your needs.

Sample android code responsible to handle ordered broadcasts

Implementation 1:

List<BroadcastReceiver> highestToLowest = new ArrayList<>();
  booelan shouldAbort;
  for (BroadcastReceiver receiver : highestToLowest) {
    if (shouldAbort) {
      break;
    }
    try {
       receiver.onAbort(v -> shouldAbort = true);
       receiver.onClearAbort(v -> shouldAbort = false);
       receiver.handle(intent);
     } catch(Exception e) {
        //log and move on
     }
   }
}

Implementation 2:

List<BroadcastReceiver> highestToLowest = new ArrayList<>();
  booelan shouldAbort;
  for (BroadcastReceiver receiver : highestToLowest) {
    if (shouldAbort) {
      break;
    }
    try {
      receiver.handle(intent);
      if (receiver.clearAbort()) {
        shouldAbort = false
      } else {
        shouldAbort = receiver.shouldAbort();
      }
    } catch(Exception e) {
       //log and move on
    }
  }

Do you see how both of these can change the way the errors are handled during onNext handling?

With native broadcast approach (and the ugly option I proposed), if exception happens at section 2, above implementation 1 will abort the broadcast altogether. If however, it android chose implementation 2, then exception would not cause the broadcast to be aborted.

If we however, chose to use callback approach (i.e onNext followed by handleOrderedBroadcast), both android implementations would not abort the call.

Our implementation needs to behave the same way as it would behave in a native broadcast way.

I'm afraid the comments are not the best place to describe what you are attempting to describe. I'm a bit confused at your main concern, hazily I am guessing it has to do with the point of the exception vs the point of the handling.

Probably the proper way to discuss this would be with some actual code. Would you be agreeable to either make a test project or better yet, some test cases in this library to demonstrate your point? As it is, it is spread out over multiple comments and pretty hard to follow. The other benefit is that whatever you are proposing could be cemented in with test cases and never regressed.

Thanks!

I'll create a vanilla java project and test cases to describe what I'm talking about. I'll not be using android as the implementation, as broadcast invoker implementation can potentially vary from version to version

I have created a sample project at

https://github.com/thekalinga/callback-trouble

Please go over the code and let me know if you still have any questions

I have responded with this thekalinga/callback-trouble#1

I think the comments are self-explanatory, but if you have any questions, let me know, I'm planning on merging this PR today.

The assumption we should make is that the library code works. I'm not sure why you are throwing exception in the library code.

If you look at other non-rx & rx with IntentAndProxy, the exception happens in the user's code who is handling the broadcast. You modified the library to code(RxBroadcast) to match the output across all three variations.

Non-Rx

System.out.println("Received by highest priority receiver"); //1. Log
abortBroadcast(); // 2. abort
throw new RuntimeException("Intentional exception"); // 3. exception

Rx with IntentAndProxy

System.out.println("Received by highest priority receiver"); //1. Log
v.proxy.abortBroadcast(); // 2. abort
throw new RuntimeException("Intentional exception"); // 3. exception

Rx

System.out.println("Received by highest priority receiver"); //1. Log
throw new RuntimeException("Intentional exception"); // 2. exception

In Rx case user does not have provision to call abort..() because of the inherent limitation in the API.

Please let me know if thats not convincing.

What criteria are you using to decide to abort?

The user has the choice to abort whenever he wants.

With IntentAndProxy approach, user decides when to abort in the flow (exactly the same way he does in non-rx approach) & the framework (Android) decides when to abort the broadcast (vanilla/exception tolerating). The library(RxBroadcast) does not make assumptions, it just delegates the call to the underlying framework when the user choses (just as native).

But with Rx alone approach, the library(RxBroadcast) is forcing user when this abort will actually be applied against the framework(Andoird), i.e after the user is done processing the event (after onNext is done), which is not correct.

Hmm, I actually think we are moving farther apart on this.

I am unclear why calling abort after onNext() (though still in the receiver) would have any negative effects. OrderedBroadcasts are exactly that-- ordered, it waits for the previous receiver to complete and optionally abort before continuing. The position where it is called inside the receiver has zero effect.

As to why you want to throw an exception, tell a receiver to abort (but never call it) is unknown to me.

If I make an assumption since the context is lacking, that in your Rx and Rx with IntentAndProxy examples, you are calling this after the intent has been emitted, correct? If that is the case, you are calling abort after onNext is called as well. If you are doing something else, I don't think it is properly explained.

The design of this API allows users to supply an OrderedBroadcastStrategy for aborting/canceling. All of the logic should be contained in there. Much as it would in an overridden BroadcastReceiver, it has the same inputs (context, intent) and if you can make the decision with just those pieces in vanilla Android, you can do the same in RxBroadcast.

I also strongly feel that any API should be opinionated and as easy for the majority to use. Introducing non-standard objects such as 'IntentAndProxy' will confuse the majority of users. Everyone expects an intent to be emitted from a BroadcastReceiver.

I will add an example to the sample app to show how to use this. Hopefully that will clear things up.

Closed by #6