Notify an Android service when a bound client disconnects
Asked Answered
I

2

10

I have an android service in a remote process that can have multiple bindings from different clients. My question is, how can the service get notified when a specific bound client gets disconnected unexpectedly (i.e the client has crashed)? I can't use onUnbind(), because it only gets called after all clients have been disconnected.

public class MyService extends Service {

    final Messenger mServiceMessenger = new Messenger(new IncomingHandler());

    @Override
    public IBinder onBind(Intent intent) {
        return mServiceMessenger.getBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return Service.START_STICKY;
    }

    class IncomingHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
        // Handling messages logic...
        }
    }
}
Isidore answered 6/8, 2013 at 11:26 Comment(1)
Basic problem: you don't see who is binding to you from within the service ("The system then delivers the same IBinder to any additional clients that bind"). You hand out a binder, which acts like description on how to communicate with your process (via kernel functionality). Clients can check if the binder is alive but I don't know if it's possible to check to other way. onUnbind is handled deep within Android's framework and I'm not sure if that's even guaranteed to be called in your remote process case.Nipha
P
1

you can use the IncomingHandler handler you have and send a message from the client that it will be unbinded before calling unbindService(serviceConnection), keeping arraylist of the Messengers(clients) and add/remove when a message is received.

you can also try to send dummy messages and if you get RemoteException means that the remote client is dead.

check this example http://developer.android.com/reference/android/app/Service.html

extract:

class IncomingHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_REGISTER_CLIENT:
                mClients.add(msg.replyTo);
                break;
            case MSG_UNREGISTER_CLIENT:
                mClients.remove(msg.replyTo);
                break;
            case MSG_SET_VALUE:
                mValue = msg.arg1;
                for (int i=mClients.size()-1; i>=0; i--) {
                    try {
                        mClients.get(i).send(Message.obtain(null,
                                MSG_SET_VALUE, mValue, 0));
                    } catch (RemoteException e) {
                        // The client is dead.  Remove it from the list;
                        // we are going through the list from back to front
                        // so this is safe to do inside the loop.
                        mClients.remove(i);
                    }
                }
                break;
            default:
                super.handleMessage(msg);
        }
    }
}
Pontone answered 6/8, 2013 at 11:55 Comment(9)
Are you referring to the msg.replyto() that i get in IncomingHandler.handleMessage(Message msg)?Isidore
Similar approach based on both answers: each Message contains the Messanger you need to reply to. Each Messanger contains the IBinder of the client to do the low level communication. I guess that can be used roughly like msg.replyTo.getBinder().linkToDeath() to get notified about the death of clients. Note: death != bound status (maybe).Nipha
Thanks @djodjo, i thought about using this method myself, but I have two problems with it: 1. I need to periodically check all my clients if I want to get notified when a disconnection occurred. 2. I don't really like the idea of using an exception to regularly check the state of a bound client. It's not the most elegant way, but i guess if I have no other choice, i'll use it.Isidore
@Nipha - This is an interesting solution. I'll try it and let you know. Thanks!Isidore
well you dont have any other way. basically there 2 ways to work this out in any client/server scenario. 1. client inform the server that is going out(send msg) 2. if client dies for some reason and cannot send message server has to have some way to figure that out (remoteexception handling). so 2 will not happen that often unless u really need updated info all the time and then u need some polling mechanism as sending dummy msgPontone
actually linkToDeath will work as well for remote binders, forgot about it :)Pontone
I tried saving the msg.replyTo() to an array and implementing linkToDeath() in the service, but the binderDied doesn't get called when the client connection dies... msg.replyTo.getBinder().linkToDeath(@Override public void binderDied() {//Some log}, 0)Isidore
this comes from native code so i suppose your binder really needs to go away(like end of process) not just unbindservice this to be triggered, this why you should implement this comm from the client about its status. its always the neater way. linkToDeath and exceptions can be used for special cases.Pontone
It seems that the best solution for me is using msg.replyTo().send and catching RemoteException for checking if the client is still connected, like you suggested. Thanks.Isidore
S
6

This can be done by the Binder.linkToDeath() mechanism - You'll have to ask each client to send new Binder() object that they initiated and then link to their (your clients') death. I'll explain how to preform this using AIDL files.

(You can choose any android IPC mechanism as long as you can pass Binder objects from your client's to your service)

Code Example -

Inside your .AIDL file - Create a method to pass the IBinder object from the client to the service

void registerProcessDeath(in IBinder clientDeathListener, String packageName);

On the client side - Initialize a new object and pass it to your service via AIDL interface.

public void onServiceConnected(ComponentName className, IBinder service) {
    mIMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
    //Call the registerProcessDeath method from the AIDL file and pass 
    //the new Binder object and the client's package name
    mIMyAidlInterface.registerProcessDeath(new Binder(),getPackageName());
}


On the service side -

1. Get the client's Binder and register to his linkToDeath().
2. Use helper class to handle all clients via android's IBinder.DeathRecipient class

public class MyService extends Service {
    //Helper class to handle all client's deaths.
    private volatile ClientsDeathWatcher mClientsList;

    @Override
    public IBinder onBind(Intent intent) {
        mClientsList = new ClientsDeathWatcher();
        return mStub;
    }

    private final IMyAidlInterface.Stub mStub = new IMyAidlInterface.Stub() {

        @Override
        public void registerProcessDeath(IBinder cb, String packageName){

            boolean isRegistered = mClientsList.register(cb , packageName);
        }
    };
}
//This is thread-safe helper class to handle all 
//the client death related tasks.

//All you care abut is the clientDeath() method. 
public class ClientsDeathWatcher {

    private ArrayMap<String, DeathCallBack> mCallbacks = new ArrayMap<>();

    private final class DeathCallBack implements IBinder.DeathRecipient {
        private String pn;
        private IBinder mBinder;

        DeathCallBack(String packageName,IBinder binder) {
            pn = packageName;
            mBinder = binder;
        }

        public void binderDied() {
            synchronized (mCallbacks) {
                mBinder.unlinkToDeath(this,0);
                clientDeath(pn);
            }
        }
    }

    //To be called only from thread-safe functions
    private void clientDeath(String packageName) {
        mCallbacks.remove(packageName);
        //Do your stuff here.
        //$$$$$$$$$
    }

    public boolean register(IBinder token, String packageName) {
        synchronized (mCallbacks) {
            try {
                if (!mCallbacks.containsKey(packageName)) {
                    DeathCallBack mDeathCallBack = new DeathCallBack(packageName,token);
                    mCallbacks.put(packageName, mDeathCallBack);
                    //This is where the magic happens
                    token.linkToDeath(mDeathCallBack, 0);
                }
                return true;
            } catch (RemoteException e) {
                e.printStackTrace();
                return false;
            }
        }
    }
}
Subtorrid answered 15/7, 2016 at 18:13 Comment(1)
Is a Binder considered dead on a service client unbind? Or only on process death?Lanham
P
1

you can use the IncomingHandler handler you have and send a message from the client that it will be unbinded before calling unbindService(serviceConnection), keeping arraylist of the Messengers(clients) and add/remove when a message is received.

you can also try to send dummy messages and if you get RemoteException means that the remote client is dead.

check this example http://developer.android.com/reference/android/app/Service.html

extract:

class IncomingHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_REGISTER_CLIENT:
                mClients.add(msg.replyTo);
                break;
            case MSG_UNREGISTER_CLIENT:
                mClients.remove(msg.replyTo);
                break;
            case MSG_SET_VALUE:
                mValue = msg.arg1;
                for (int i=mClients.size()-1; i>=0; i--) {
                    try {
                        mClients.get(i).send(Message.obtain(null,
                                MSG_SET_VALUE, mValue, 0));
                    } catch (RemoteException e) {
                        // The client is dead.  Remove it from the list;
                        // we are going through the list from back to front
                        // so this is safe to do inside the loop.
                        mClients.remove(i);
                    }
                }
                break;
            default:
                super.handleMessage(msg);
        }
    }
}
Pontone answered 6/8, 2013 at 11:55 Comment(9)
Are you referring to the msg.replyto() that i get in IncomingHandler.handleMessage(Message msg)?Isidore
Similar approach based on both answers: each Message contains the Messanger you need to reply to. Each Messanger contains the IBinder of the client to do the low level communication. I guess that can be used roughly like msg.replyTo.getBinder().linkToDeath() to get notified about the death of clients. Note: death != bound status (maybe).Nipha
Thanks @djodjo, i thought about using this method myself, but I have two problems with it: 1. I need to periodically check all my clients if I want to get notified when a disconnection occurred. 2. I don't really like the idea of using an exception to regularly check the state of a bound client. It's not the most elegant way, but i guess if I have no other choice, i'll use it.Isidore
@Nipha - This is an interesting solution. I'll try it and let you know. Thanks!Isidore
well you dont have any other way. basically there 2 ways to work this out in any client/server scenario. 1. client inform the server that is going out(send msg) 2. if client dies for some reason and cannot send message server has to have some way to figure that out (remoteexception handling). so 2 will not happen that often unless u really need updated info all the time and then u need some polling mechanism as sending dummy msgPontone
actually linkToDeath will work as well for remote binders, forgot about it :)Pontone
I tried saving the msg.replyTo() to an array and implementing linkToDeath() in the service, but the binderDied doesn't get called when the client connection dies... msg.replyTo.getBinder().linkToDeath(@Override public void binderDied() {//Some log}, 0)Isidore
this comes from native code so i suppose your binder really needs to go away(like end of process) not just unbindservice this to be triggered, this why you should implement this comm from the client about its status. its always the neater way. linkToDeath and exceptions can be used for special cases.Pontone
It seems that the best solution for me is using msg.replyTo().send and catching RemoteException for checking if the client is still connected, like you suggested. Thanks.Isidore

© 2022 - 2024 — McMap. All rights reserved.