Skip to content

Instantly share code, notes, and snippets.

@RobLewis
Created December 1, 2018 17:59
Show Gist options
  • Save RobLewis/fe7fad0381b761bae2842dc0d2cd8f2c to your computer and use it in GitHub Desktop.
Save RobLewis/fe7fad0381b761bae2842dc0d2cd8f2c to your computer and use it in GitHub Desktop.
Bind and Unbind to an Android Service reactively
package net.grlewis.wifithermocouple;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.disposables.Disposable;
/*
* Motivation:
* An Android Service is similar to an Activity, but has no user interface.
* As the name implies, it provides services to one or more Activities in your App.
* A Started Service is launched by the App and can run independently in the background.
* A Bound Service is "bound" when an Activity wants to use it, and "unbound" when the Activity is finished with it
* A Service can be Bound, Started, or both.
* (Android destroys a pure Bound Service when the last Activity unbinds from it.)
*
* The process of binding to a Service is a bit complicated, and needlessly so when your Activity
* simply wants to call methods that the Service provides (you can read about it in the Android documentation).
*
* This class was designed to simplify the process and make it "reactive" by creating an Observable
* that your Activity can subscribe to when it wants to connect to the Service, and dispose when it's finished.
* The Observable emits a single item: a reference to the connected Service that you can use to call its methods:
* serviceRef.someServiceMethod( );
* (Note by the way that it is legal to call static methods in the Service using this instance reference,
* though the compiler will warn you; use the annotation '@SuppressWarnings( "static-access" )' to prevent this.)
*
*
* Use:
* Create YourService by extending Service (or some subclass; remote services under a different process aren't supported)
* YourService (typically in onCreate()) calls:
* Binder mBinder = new ReactiveServiceConnection.RSCBinder( this ); // Service passes instance of itself
* YourService overrides onBind( Intent ) and returns mBinder [Intent can pass additional Extra info if desired]
* YourService overrides onUnbind( Intent ) [Return true if you would like to have the Service's onRebind( Intent ) method
* later called when new clients bind to it. Mine just calls super. Could probably skip this unless want to return true.]
* The client Activity creates an instance of this ReactiveServiceConnection class and subscribes to it, for example:
* Disposable mServiceConnectionDisposable = new ReactiveServiceConnection( getApplicationContext(), YourService.class )
* .subscribe( serviceRef -> { <save the reference to YourService and use it to call its methods> };
* Before subscribing, the client can modify the Intent to be used via getBindIntent() and setBindIntent()
* YourService is bound on subscribing and the client Subscriber's onSubscribe() receives a Disposable to close the connection
* When YourService is connected a reference to it is emitted via the Subscriber's onNext(); the reference can be used to call Service methods
* When the client disposes, the reference is nulled, Service unbound, and disposed set true (Subscriber's onComplete() is not called)
* The following RuntimeExceptions may be passed to the Subscriber's onError() method:
* If there is a problem binding to the Service (it doesn't exist or binding isn't allowed): a ReactiveServiceBindingException
* If your Activity tries to bind using a different IBinder: a ReactiveServiceConnectionException
* If the connection is lost: a ReactiveServiceDisconnectionException (should never happen)
* Several methods are provided to retrieve information about the connection.
* Failing to dispose the connection (typically in your Activity's onStop()) may cause memory leaks.
* Don't forget to declare YourService in the App's manifest.
*
*/
class ReactiveServiceConnection extends Observable<Service> implements ServiceConnection {
private static final String TAG = ReactiveServiceConnection.class.getSimpleName(); // for logging debug messages
private Observer<? super Service> subscriber; // Subscriber passed by the .subscribe() call (which gets forwarded to our .subscribeActual())
private RSCDisposable disposable; // returned to the Subscriber's .onSubscribe() method
private boolean disposed; // tracks state for reporting by .isDisposed()
private boolean okToBind; // set by .bindService(), indicating Service exists and binding is allowed
private String serviceClassName; // name of the Service class
private Service serviceRef; // reference used to call Service methods (emitted to Subscriber's onNext())
private final Class serviceClass; // passed to constructor
private ComponentName serviceComponentName; // returned by .onServiceConnected()
private Intent bindIntent; // created by constructor; can be modified via getter/setter
private final Context context; // passed to constructor
// Disposable that is returned to the Subscriber's onSubscribe( Disposable )
// RxJava docs are unclear whether Subscriber's onComplete() should be called on disposal,
// but when a Subscriber disposes, it should know that no more emissions are to be expected
private class RSCDisposable implements Disposable {
@Override // if called, unbind Service
public synchronized void dispose( ) {
disposed = true;
context.unbindService( ReactiveServiceConnection.this ); // return the parent instance (ServiceConnection implementation)
serviceRef = null;
//subscriber.onComplete(); // RxJava docs are unclear whether this should be called (doesn't seem to be any need to)
}
@Override
public synchronized boolean isDisposed( ) {
return disposed;
}
}
// Binder that is instantiated and used by the Service we're connecting to
// constructor is called by the Service instance, passing 'this'
// The Service overrides onBind() and returns this instance of RSCBinder
public static class RSCBinder extends Binder {
private Service boundService;
// constructor
RSCBinder( Service service ) { boundService = service; } // normally 'this' is passed for Service
// extension method added to Binder to return reference to Service
public Service getService() { return boundService; }
}
public static class ReactiveServiceBindingException extends RuntimeException {
public ReactiveServiceBindingException( String message ) { super( message ); }
}
public static class ReactiveServiceConnectionException extends RuntimeException {
public ReactiveServiceConnectionException( String message ) { super( message ); }
}
public static class ReactiveServiceDisconnectionException extends RuntimeException {
public ReactiveServiceDisconnectionException( String message ) { super( message ); }
}
// constructor
// Client (Activity) calls this and subscribes to the created instance
public ReactiveServiceConnection( Context callingContext, Class serviceClass ) {
super(); // call the main Observable constructor
serviceClassName = serviceClass.getName();
this.serviceClass = serviceClass;
context = callingContext;
disposable = new RSCDisposable( );
bindIntent = new Intent( callingContext, this.serviceClass );
}
@Override // this is implemented to create the Observable (it's called by the parent Observable's subscribe())
// it will be called when the Activity subscribes to receive the connection
protected void subscribeActual( Observer<? super Service> observer ) {
subscriber = observer;
disposed = false;
subscriber.onSubscribe( disposable ); // send back the Disposable
// establish the connection (rest happens in onServiceConnected() callback)
okToBind = context.bindService( bindIntent, this, Context.BIND_AUTO_CREATE ); // 'this' is ServiceConnection implementation
if( !okToBind ) {
disposed = true;
subscriber.onError( new ReactiveServiceBindingException( "Requested " + serviceClassName + " does not exist or binding not allowed" ) );
}
}
// ServiceConnection implementation
@Override // receives the RSCBinder
public void onServiceConnected( ComponentName name, IBinder service ) {
if( service instanceof RSCBinder ) { // make sure the IBinder is compatible with this scheme
serviceComponentName = name;
serviceRef = ((RSCBinder) service).getService( ); // cast the IBinder to our special Binder and get the Service reference
disposed = false;
subscriber.onNext( serviceRef ); // emit the service reference to subscribing Activity
} else {
disposed = true;
subscriber.onError( new ReactiveServiceConnectionException( "IBinder in onServiceConnected callback from "
+ name.toShortString() + " is not an instance of RSCBinder" ) );
}
}
@Override // this is only supposed to be called when a remote Service crashes (not our situation)
public void onServiceDisconnected( ComponentName name ) {
context.unbindService( this );
disposed = true;
serviceRef = null;
subscriber.onError( new ReactiveServiceDisconnectionException( "The Service " + name.toShortString() + " has gone away." ) );
}
// @Override
// Called when the binding to this connection is dead. This means the interface will never receive another connection.
// The application will need to unbind and rebind the connection to activate it again.
// public void onBindingDied( ComponentName name ) { }
// @Override
// Called when YourService returns null from its onBind() method (should not normally happen).
// This indicates that the attempting service binding represented by this ServiceConnection will never become usable.
// public void onNullBinding( ComponentName name ) { }
public ComponentName getServiceComponentName() { return serviceComponentName; }
public Class getServiceClass() { return serviceClass; }
public String getServiceClassName() { return serviceClassName; }
public boolean serviceIsOkToBind( ) { return okToBind; } // not set until we actually try to bind (by subscribing)
public Service getServiceRef( ) { return serviceRef; } // the same Service reference emitted to Subscriber's onNext()
// getter/setter that can be used to add EXTRA information to the Intent
public Intent getBindIntent( ) { return bindIntent; }
public void setBindIntent( Intent bindIntent ) { this.bindIntent = bindIntent; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment