zeus
zeus

Reputation: 13367

LocalBroadcastManager is now deprecated, how to send data from service to activity?

I have a service that needs to notify the main activity. I use LocalBroadcastManager, and it works fine, but LocalBroadcastManager has been deprecated.

This is my actual code in the service:

public void onTokenRefresh() {
      
    /* build the intent */
    Intent intent = new Intent(ACTION_TOKENREFRESHED);
    intent.putExtra("token", "xxx");
    
    /* send the data to registered receivers */
    try{
      LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
    } catch (Throwable e){
      //no exception handling
    }  
  
  }

In the main activity, I get informed of the notification like this :

context.registerReceiver(broadcastReceiver, intentFilter);

What can I use now to remove the deprecated warning? All examples I found regarding sending data from service to activity use LocalBroadcastManager. Can someone give me a workable model for migrating my existing code?

NOTE

In my example, The onTokenRefresh is called from inside a background thread. That is very important because it means I can simultaneously receive several onTokenRefresh, and I must forward all these tokens to the activity. Most of the offered solutions use live data but make a declaration like :

public static final MutableLiveData<String> tokenLiveData = new MutableLiveData<>();

Background Thread1:
tokenLiveData.postValue(Token1);

Background Thread2 (at same time):
tokenLiveData.postValue(Token2);

Will forward ALL tokens received simultaneously to the main activity that observes the tokenLiveData? Will the main activity always receive for sure token1 and token2?

Upvotes: 15

Views: 9192

Answers (6)

Lemahe
Lemahe

Reputation: 1

If you only want to receive data in a single class at a time you can create an object that passes the information:

    object NotificationManager {

       private var notification: ((String) -> Unit)? = null

       fun updateNotificationMessage(message: String) {
           notification?.invoke(message)
       }

       fun setNotificationCallback(callback: (String) -> Unit) {
           this.notification = callback
       }
  }

To send the message:

NotificationManager.updateNotificationMessage("MSG")

To listen to the callback and receive the message:

NotificationManager.setNotificationCallback { msg ->
     ...
}

Upvotes: 0

Dinh Lam
Dinh Lam

Reputation: 765

I think you can consinder to use EventBus, link here: https://github.com/greenrobot/EventBus

FAQ: https://greenrobot.github.io/EventBus/

Q: How's EventBus different to Android's BroadcastReceiver/Intent system?

A: Unlike Android's BroadcastReceiver/Intent system, EventBus uses standard Java classes as events and offers a more convenient API. EventBus is intended for a lot more uses cases where you wouldn't want to go through the hassle of setting up Intents, preparing Intent extras, implementing broadcast receivers, and extracting Intent extras again. Also, EventBus comes with a much lower overhead.

So you can declare you data class like:

public class RefreshTokenData {
  private String token;
  private String...
  ...// Other field
}

In your activity, simple way:

@Subscribe(threadMode = ThreadMode.MAIN)  
public void onRefreshToken(RefreshTokenData data) {
    // Do something
}

You can switch to other thread instead main by ThreadMode.

In your service:

public void onTokenRefresh() {
      
    final RefreshTokenData refreshTokenData = new RefreshTokenData("xxx", ...);
    
    EventBus.getDefault().post(refreshTokenData);
}

After the token will be send to activity in method onRefreshToken declared above.

Don't forget unsubscribe your activity:

@Override
public void onStart() {
     super.onStart();
     EventBus.getDefault().register(this);
}

@Override
public void onStop() {
     super.onStop();
     EventBus.getDefault().unregister(this);
}

More details: https://greenrobot.org/eventbus/documentation/how-to-get-started/

Thread Mode and your note

Link: https://greenrobot.org/eventbus/documentation/delivery-threads-threadmode/

I suggest to use ThreadMode: MAIN_ORDERED, you will get all your refresh token sequentially because With MAIN_ORDERED, the first event handler will finish, and then the second one will be invoked at a later point in time.

Upvotes: 1

MariosP
MariosP

Reputation: 9113

As per the official documentation of LocalBroadcastManager:

This class is deprecated. LocalBroadcastManager is an application-wide event bus and embraces layer violations in your app: any component may listen events from any other. You can replace usage of LocalBroadcastManager with other implementation of observable pattern, depending on your usecase suitable options may be androidx.lifecycle.LiveData or reactive streams.

So you can replace it with other observable pattern like LiveData or reactive streams. Because LiveData is most popular nowadays and it is lifecycle-aware which ensures that it updates app component observers like activities, fragments, or services which have an active lifecycle state only below i will describe the steps to replace the LocalBroadcastManager using LiveData.

1.)Declare a NotificationLiveData class which extends from LiveData<T> where T is a String in the below example:

public class NotificationLiveData extends LiveData<String> {

    public void setNotification(String message){
        postValue(message);
    }
}

2.)Declare a NotificationManager class which is responsible to update the message like below:

public class NotificationManager {

    private static final NotificationLiveData notificationLiveData = new NotificationLiveData();

    public static void updateNotificationMessage(String message){
        notificationLiveData.setNotification(message);
    }

    public static NotificationLiveData getNotificationLiveData() {
        return notificationLiveData;
    }
}

3.)In every Activity you want to listen for updates you can observe the change like below:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    NotificationManager.getNotificationLiveData().observe(this, notificationObserver);
}

private final Observer<String> notificationObserver = new Observer<String>() {
    @Override
    public void onChanged(String s) {
        //do anything after a change happened
    }
};

4.)In every Fragment you want to listen for updates you can observe the change like below:

public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    NotificationManager.getNotificationLiveData().observe(getViewLifecycleOwner(), notificationObserver);
} 

private final Observer<String> notificationObserver = new Observer<String>() {
    @Override
    public void onChanged(String s) {
        //do anything after a change happened
    }
};

5.)From your Service you can send a message like below:

NotificationManager.updateNotificationMessage("My message");

6.)Finally don't forget to remove the observer from any Activity or Fragment in onDestroy() method like below:

@Override
protected void onDestroy() {
    super.onDestroy();
    NotificationManager.getNotificationLiveData().removeObserver(notificationObserver);
}

The above will listen the change only when the Activity/Fragment has an active lifecycle state. In case you want to listen always the change independent of the lifecycle state you can use observeForever instead of observe like below:

NotificationManager.getNotificationLiveData().observeForever(notificationObserver);

Update:

As per your new requirement having multiple of background Threads which are sending simultaneously a number of Tokens you cannot use anymore the postValue method as it does not guarantee that the specific Token will be received from the observer if the postValue is called a lot of times simultaneously so in that case only the latest token will be received to the observer. You can test this if you call the postValue() and immediately call the getValue() method where you may not receive the value you have just set. However this can be solved only by using the setValue method which is executed from the Main Thread only. When the Main Thread sets the value, then you will get immediately the value in the observer but be careful the observe listens the change only if the Activity/Fragment has an active lifecycle state. If you want to listen always the change independent of the lifecycle you must use the observeForever instead.

So you need to change the NotificationLiveData class to use the setValue in the Main Thread like below:

public class NotificationLiveData extends MutableLiveData<String> {

    public void setNotification(String message){
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                setValue(message);
            }
        });
    }
}

But in the Multithreading environment it is better to use RxJava which uses reactive streams for sending bunch of data simultaneously. RxJava has a better level of abstraction around threading which simplifies the implementation of complex concurrent behavior so it suits better in this case. Below i will describe how you can achieve this using RxJava instead of LiveData.

RxJava - Approach

1.)First declare in your app dependencies the below Reactivex implementations:

implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'io.reactivex.rxjava3:rxjava:3.1.5'

Here is the official documentation of ReactiveX/RxAndroid for more details.

2.)Declare the new NotificationManager class to be like the below:

public class NotificationManager {

    private static final Subject<String> notificationSubject = PublishSubject.create();

    public static void updateNotificationMessage(String message){
        notificationSubject.onNext(message);
    }

    public static Subject<String> getNotificationSubject() {
        return notificationSubject;
    }
} 

The PublishSubject represents an Observer and an Observable at the same time, allowing multicasting events from a single source to multiple child Observers. It emits to an observer all the subsequent items of the source Observable at the time of the subscription.

3.)You can subscribe to any Activity like the below sample:

Disposable disposable = NotificationManager.getNotificationSubject()
        .doOnNext(s -> { Log.d("OnNext=", s);})
        .doOnComplete(() -> { })
        .doOnError(throwable -> { })
        .subscribe();

The doOnNext is called every time you do a call to the notificationSubject.onNext(token).

4.)In your Service in each of your Threads you can emit simultaneously the new tokens like the below sample:

Thread thread1 = new Thread() {
    public void run() {
        for (int i = 0; i < 10; i++) {
            NotificationManager.updateNotificationMessage("Thread1:" + i);
        }
    }
};
thread1.start();

Thread thread2 = new Thread() {
    public void run() {
        for (int i = 0; i < 10; i++) {
            NotificationManager.updateNotificationMessage("Thread2:" + i);
        }
    }
};
thread2.start();

5.) Finally in onDestroy() method of your Activity you have to dispose the PublishSubject like below:

@Override
protected void onDestroy() {
  super.onDestroy();
  disposable.dispose();
}

And the results of the above multithreading example will be something like the below:

D/OnNext=: Thread2:0
D/OnNext=: Thread2:1
D/OnNext=: Thread2:2
D/OnNext=: Thread2:3 
D/OnNext=: Thread2:4
D/OnNext=: Thread2:5
D/OnNext=: Thread1:0
D/OnNext=: Thread2:6
D/OnNext=: Thread2:7
D/OnNext=: Thread1:1
D/OnNext=: Thread2:8
D/OnNext=: Thread1:2
D/OnNext=: Thread2:9
D/OnNext=: Thread1:3
D/OnNext=: Thread1:4
D/OnNext=: Thread1:5
D/OnNext=: Thread1:6
D/OnNext=: Thread1:7
D/OnNext=: Thread1:8
D/OnNext=: Thread1:9

Also if you know when the transmission of data is finished you can simply use the notificationSubject.onComplete() method and the .doOnComplete(() -> { }) will be called in the Activity and automatically disposes the PublishSubject Observable. A similar call can be used when something goes wrong using the notificationSubject.onError(new Throwable()) which triggers the Activity .doOnError(throwable -> { }) callback.

Upvotes: 11

Aorlinn
Aorlinn

Reputation: 778

If you have the possibility to use LiveData, this would be the simplest and cleanest option. If you have complex data which you don't want to share within the LiveData or if it's not thread save, you can just use LiveData for signaling and read the particular data in a thread save way.

However, if this is not possible/accurate there are options with event busses, such as GreenRobot (there is that guide for instance). But they may lack some of the same problems, why they deprecated LocalBroadcastManager.

An alternative is to use a global broadcast. If you have complex data you might use it for signaling only and add a ContentResolver or similar to get the data. But note that it is not very performant, as it's handled by the OS and trying to find any appropriate BroadcastRecievers on the device and doing a lot of RAM transactions (you find it in that video from Google)

So for a local only used Service you could use IBinder/Binder.

If you have simple data, you use transact(int code, Parcel data, Parcel reply, int flags), where the third parameter is the data you want back as an answer from the Service. But there are two restrictions:

  1. It's a parcel, so only serializable data is possible
  2. From the service you cannot directly notify the client (Activity in your case).

If you have more complex data, you can implement a specific method in your Binder class and use parameters and return values.

If you implement a LiveData in the Binder, you can use that for notification or, if the data is not very complex (like the token in your case) to contain the whole data.

You would implement it like that:

public class MyService extends Service
{
    protected MutableLiveData<String> mToken;

    public MyService()
    {
        mToken = new MutableLiveData<>("current token");
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        return new Binder();
    }

    public class Binder extends android.os.Binder
    {
        public void changeToken(String newToken)
        {
            mToken.postValue(newToken);
        }

        public LiveData<String> getToken()
        {
            return mToken;
        }
    }
}

and your Activity:

public class MainActivity extends AppCompatActivity
{
    private ActivityMainBinding binding;
    private ServiceConnection mConnection;
    
    @Override
    protected void onStart()
    {
        super.onStart();
        // Bind to LocalService
        Intent intent = new Intent(this, MyService.class);
        mConnection = new ServiceConnection();
        // If your service is already running, this will just bind it.
        // If it's not running, it will also start it because of the second argument
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop()
    {
        super.onStop();
        if (mConnection != null)
        {
            unbindService(mConnection);
        }
    }

    /**
     * Defines callbacks for service binding, passed to bindService()
     */
    private class ServiceConnection implements android.content.ServiceConnection
    {
        @Override
        public void onServiceConnected(ComponentName className,
                                       IBinder service)
        {
            // We've bound to MyService.Binder, cast the IBinder and get MyService.Binder instance
            MyService.Binder binder = (MyService.Binder) service;
            binder.getToken().observe(MainActivity.this, (newToken) -> binding.Token.setText(newToken));
            binding.StartButton.setOnClickListener((click) -> binder.changeToken("newToken" + new Random().nextInt()));
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0)
        {
            mConnection = null;
        }
    }
}

I tested the example, it's working ;-)

If for any reason you cannot bind Service and Activity that tight, you can instead establish a LiveData (of whatever kind) in your Application class, listen to it in your Activity and change it from your Service. But only do that, if the Service is a) very independent from your Activity and b) running most of the time. Otherwise you will have LiveData that is inappropriate most of the time.

I personally often use "Services". Not in the means of how Android describe them, but singleton classes that are responsible for some part of my application (you might also call them controllers, even though it's not completely accurate). I then register them as singleton with Hilt (Dagger some time ago). In your case you could then establish LiveData on such a "Service".

Update

Because of your update that you must not loose any of these tokens, the answer is a bit trickier.

If you are sure, that the update might not happen more frequent than, let's say every 10 seconds, you can safely use the code above. If it can happen more frequent or you are not sure then don't.

Why? Because there are two methods on LiveData: setValue and postValue. setValue performes the notification immediately and so every new token will be dispatched to the observer. But it can only be called from the main thread, which is not the case for you. So you need to call postValue which posts a task to the end of the internal task list of the main thread. If a new call to postValue is done before the notification is executed, no new task is added to the queue and only the latest value will be delivered.

What can you do instead? You might alter the service to the following:

public class MyService extends Service
{
    protected MutableLiveData<Integer> mVersion = new MutableLiveData<>(0);
    protected ConcurrentLinkedQueue<String> mTokens = new ConcurrentLinkedQueue<>();

    public void newToken(String token)
    {
        mTokens.offer(token);
        mVersion.postValue(mVersion.getValue() + 1);
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        return new Binder();
    }

    public class Binder extends android.os.Binder
    {
        public AbstractQueue<String> getTokens()
        {
            return mTokens;
        }

        public LiveData<Integer> getSignal()
        {
            return mVersion;
        }
    }
}

On your activity you just call mTokens.poll() as long as the queue is not empty. But this only works, if your service only runs, when the activity is present (more or less), and your activity is the only one that binds to your service (=> is the only consumer). If the service might run in background, this could eventually end in a memory overflow, if you have lots of new tokens. Plus you could loose your data, if for any reason your service is shut down by the OS.

A better way is to persist the data somewhere and only use the LiveData to keep the version. Your activity can then keep a local variable containing the latest version and only read the remaining tokens. To do so you can use several approaches:

There are of course more ways, but as I don't know enough about the tokens and what you want to do with them I cannot even say, which of these approaches fits the best for you.

Upvotes: 5

Squti
Squti

Reputation: 4487

Make a service class and define a LiveData to replace the LocalBroadcastManager responsibility like so:

//This service sends an example token ten times to its observers
public class MyService extends Service {
    //Define a LiveData to observe in activity
    public static final MutableLiveData<String> tokenLiveData = new MutableLiveData<>();

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        //You need a separate thread if you don not use IntentService

        Thread thread1 = new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    //send random strings az an example token ten times.
                    //You can remove this loop and replace it with your logic
                    String token1 = UUID.randomUUID().toString();
                    new Handler(Looper.getMainLooper()).post(() -> sendTokenToObserver("Thread1: " + token1));

                }
            }
        };
        thread1.start();

        Thread thread2 = new Thread() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    String token2 = UUID.randomUUID().toString();
                    new Handler(Looper.getMainLooper()).post(() -> sendTokenToObserver("Thread2: " + token2));
                }
            }
        };
        thread2.start();
        return START_STICKY;
    }

    //Post token to observers
    public void sendTokenToObserver(String token) {
        tokenLiveData.setValue(token);

    }
}

Then start the service in the activity and observe the LiveData like below:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //You can observe the emitted token here and do what you want(Show in view or notification). 
        //I've just logged it to the console.
        startService(new Intent(this,MyService.class));
        MyService.tokenLiveData.observe(this, token -> Log.d("token", token));
    }
}

You can also start the service from another activity and observe it in the MainActivity;

Upvotes: 7

Aniruddh Parihar
Aniruddh Parihar

Reputation: 3104

The Android Interface Definition Language (AIDL) is analogous to other IDLs you would possibly have worked with. It allows you to define the programming interface that both the client and repair agree upon so as to speak with one another using interprocess communication (IPC). Traditionally the Android way a process cannot access the memory of some other process. So in order to be able to communicate they decompose their objects into primitives that the Operating System can understand well, and marshall the objects across that boundary for you. The code to try to do that marshaling is tedious to write down, so Android handles it for you with AIDL.

Note: Using AIDL is important as long as you permit clients from different applications to access your service for IPC and need to handle multithreading in your service.

But Before you go off start designing your own AIDL, do note that calls to an AIDL are straightaway right function calls! So you shouldn’t assume about the thread during the execution of whom the decision occurs. What happens is different counting on whether the decision is from a thread within the local process or a foreign process.

Specifically

  1. Calls made up of the local process are executed within the same thread that’s making the decision. If this call is from your main UI thread often then that will continue to exec within the AIDL interface, however in case if it is another one (thread) then that one will execute yours within the service!

  2. Calls from a foreign process are dispatched from a thread pool the platform maintains inside your own process. you want to be prepared for incoming calls from unknown threads, with multiple calls happening at an equivalent time.

  3. The oneway keyword decides how the behavior of the remote call would be, When used, a foreign call doesn’t block; it simply sends the transaction data and immediately returns. The implementation of the interface eventually receives this as a daily call from the Binder thread pool as a traditional remote call. If oneway is employed with an area call, there’s no impact and therefore the call remains synchronous.

Defining an AIDL Interface

You must define your AIDL interface in a .aidl file using the Java programming language syntax, then reserve it within the ASCII text file (in the src/ directory) of both the appliance hosting the service and the other application that binds to the service. To create a .aidl file use this link!

Well, here’s an example:

Process A needs info of Call status to work out whether it must change Call Type (for instance changing voice to video). you’ll get call status from certain listeners but to vary Call type from Audio to Video, Process A needs a hook to vary. This “Hook” or way of adjusting calls is usually a part of “> a part of Telephony Classes which are part of Telephony Process. So as to get such a piece of information from the Telephony process, One may write a telephony service (which runs as a neighborhood of android telephony process), which can allow you to question or change call type. Since Process A(Client) here is using this remote Service which communicates with the Telephony process to change call type, it must have an interface to speak to the service. Since Telephony service is that the provider and Process A (client) is that the user, they both got to agree on an interface (protocol) they will understand and cling to. Such an interface is AIDL, which allows you to speak (via a foreign service) to the Telephony process and obtain some work done.

Simply put in laymen’s terms, AIDL is an “agreement” the Client gets, which tells it about the way to ask for service. The service itself will have a replica of that agreement(since it published for its clients). Service will then implement details on how it handles once an invitation arrives or say when someone is lecturing it

So process A requests to vary call via Service, Service gets the request, it talks to telephony process(since it’s a part of it) and changes call to video.

An important point to notice is, AIDL is merely necessary for a multithreading environment. you’ll do away with Binders if you do not get to affect multithreaded arch. Example: Process A needs info of Call status to work out whether it must change Call Type (for example Audio to Video Call or Vice-versa). you’ll get call status from certain listeners but to vary Call type from Audio to Video, Process A needs a hook to vary. This “Hook” or way of adjusting calls is usually a part of “> a part of Telephony Classes which are part of Telephony Process. So as to get such information from the Telephony process, One may write a telephony service (which runs as a neighborhood of the android telephony process), which can allow you to question or change call type. Since Process A(Client) here is using this remote Service which communicates with the Telephony process to change call type, it must have an interface to speak to the service. Since Telephony service is that the provider and Process A (client) is that the user, they both got to agree on an interface (protocol) they will understand and cling to. Such an interface is AIDL, which allows you to speak (via a foreign service) to the Telephony process and obtain some work done.

Simply put in laymen’s terms, AIDL is an “agreement” the Client gets, which tells it about the way to ask for service. The service itself will have a replica of that agreement(since it published for its clients). Service will then implement details on how it handles once an invitation arrives or say when someone is lecturing it

So process A requests to vary call via Service, Service gets the request, it talks to telephony process(since it’s a part of it) and changes call to video.

An important point to notice is, AIDL is merely necessary for a multithreading environment. you’ll do away with Binders if you do not get to affect multithreaded arch.

Here’s a code snippet to guide you in action:

// Declare any non-default types
// here with import statements
interface AidlInterface {
        int add(int x,int y);
        int sub(int x,int y);
    }

The AIDL Service for the above code is:

public class AidlServiceGfG extends Service {

    private static final String TAG = "AIDLServiceLogs";
    private static final String className = " AidlService";

    public AidlService() {
        Log.i(TAG, className+" Constructor");
    }

    @Override
    public IBinder onBind(Intent intent) {
        // GfG Example of Binder
        Log.i(TAG, className+" onBind");
        return iCalculator.asBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, className+" onCreate");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(TAG, className+" onDestroy");
    }

    ICalculator.Stub iCalculator = new ICalculator.Stub() {
        @Override
        public int add(int x, int y) throws RemoteException {
            Log.i(TAG, className+" add Thread Name: "+Thread.currentThread().getName());
            int z = x+y;
            return z;
        }

        @Override
        public int sub(int x, int y) throws RemoteException {
            Log.i(TAG, className+" add Thread Name: "+Thread.currentThread().getName());
            int z = x-y;
            return z;
        }
    };

}

And here’s the service connection:

// Returns the stub
ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.i(TAG, className + " onServiceConnected");
            iCalculator = ICalculator.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

            unbindService(serviceConnection);
        }
    };

Conclusion

AIDL uses the binder kernel driver to form calls. once you make a call, a way identifier and every one of the objects are packed onto a buffer and copied to a foreign process where a binder thread waits to read the info. When calls are made within the same process and therefore the same backend, no proxy objects exist, then calls are direct with no packing or unpacking.

Upvotes: 1

Related Questions