SimpleServiceConnection.java
package net.zer0bandwidth.android.lib.services;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.util.Log;
import java.util.Vector;
/**
* This class provides a simple, consistent implementation of a service binding,
* so that neither the service nor the consumer of that service is required to
* have its own implementation. The class provides basic methods to bind to, and
* unbind from, the service; get the bound instance of the service; bind as a
* listener to the connection itself.
*
* Note: In version 0.1.0 (#19), the class's members and inner classes were
* fundamentally modified to provide tighter access to the connected service
* class. While this makes the class more convenient to use, it is a flag day
* beyond which consumers of this class will need to be modified to catch up.
*
* @since zer0bandwidth-net/android 0.0.1 (#1)
*/
@SuppressWarnings("unused") // because it's a library
public class SimpleServiceConnection<S extends Service>
implements ServiceConnection
{
/**
* By default, the connection will bind at a level above the consumer, and
* auto-create the service if it has not yet been started.
* Used by the class's {@link #connect} methods in absence of any flag set
* specified by the caller.
*/
public static final int DEFAULT_BINDING_FLAGS =
( Context.BIND_ABOVE_CLIENT | Context.BIND_AUTO_CREATE ) ;
/**
* Because connections can take a while, the object that is waiting for the
* connection to be completed should implement this interface to receive a
* signal when the connection is established.
* @since zer0bandwidth-net/android 0.0.1 (#1)
*/
@SuppressWarnings("unused") // because it's still a library
public interface Listener<LS extends Service>
{
/** Invoked when the connection is bound. */
void onServiceConnected( SimpleServiceConnection<LS> conn ) ;
/** Invoked when the connection is unbound. */
void onServiceDisconnected( SimpleServiceConnection<LS> conn ) ;
}
/**
* The binder provided by the service needs to implement this interface,
* which provides us with a standard way to reach into the service's own
* public methods.
* @since zer0bandwidth-net/android 0.0.1 (#1)
*/
public interface InstanceBinder<BS extends Service>
extends IBinder
{
/** Provides the bound instance of the service. */
BS getServiceInstance() ;
}
/**
* Stores a consistent hint to the service class to which this connection
* will bind.
*/
protected Class<S> m_clsService = null ;
/** The bound instance. */
protected S m_srvInstance = null ;
/** Indicates whether the connection is, indeed, bound to a service. */
protected boolean m_bBound = false ;
/** A collection of objects listening to this connection. */
protected Vector<Listener<S>> m_vListeners = null ;
/**
* For log messages generated by methods of this class, or any descendant
* class, use this method, which examines the class of the instance from
* which it is called, and returns that class's simple name. This allows
* other projects to extend this class without having to override this
* method.
* @return a logging tag for this connection object
*/
protected final String getLogTag()
{ return this.getClass().getSimpleName() ; }
/** Forbid use of the default constructor. */
private SimpleServiceConnection() {}
/**
* Initializes a simple connection for a specified service class.
* @param cls the service class
*/
public SimpleServiceConnection( Class<S> cls )
{
m_clsService = cls ;
this.initListeners() ;
}
/**
* Initializes the pool of connection listeners.
* @return the connection, for fluid invocations
*/
protected SimpleServiceConnection<S> initListeners()
{
if( m_vListeners == null )
m_vListeners = new Vector<>() ;
else
m_vListeners.clear() ;
return this ;
}
/**
* Accesses the pool of listeners, verifying first that such a pool has been
* initialized.
* @return a collection of listeners to this connection
*/
protected Vector<Listener<S>> getListeners()
{
if( m_vListeners == null ) this.initListeners() ;
return m_vListeners ;
}
/**
* Registers a listener to this collection, if it is not already listening.
* @param l the listener to be added
* @return the connection, for fluid invocations
*/
public SimpleServiceConnection<S> addListener( Listener<S> l )
{
if( ! this.getListeners().contains(l) )
m_vListeners.add(l) ;
return this ;
}
/**
* Unregisters a listener to this collection, if it is found in the pool.
* @param l the listener to be removed
* @return the connection, for fluid invocations
*/
public SimpleServiceConnection<S> removeListener( Listener<S> l )
{
if( this.getListeners().contains(l) )
m_vListeners.remove(l) ;
return this ;
}
/**
* Accessor for the service class hint.
* @return the service class hint
*/
public Class<S> getServiceClass()
{ return m_clsService ; }
/**
* Indicates whether the connection is indeed bound to an instance of the
* specified class.
* @param cls the class to which we should compare our bound service
* @return true iff the specified class is the bound service's class
*/
public <C extends Service> boolean isServiceClass( Class<C> cls )
{ return( m_clsService.equals(cls) ) ; }
/**
* Indicates whether the connection is bound.
* @return an indication that the service is bound
*/
public synchronized boolean isBound()
{ return m_bBound ; }
/**
* Perhaps more useful than {@link #isBound}, this function verifies not
* only that the connection is bound, but also that the persistent reference
* to the service is not null.
* @return an indication that the connection is, indeed, connected
* @since zer0bandwidth-net/android 0.0.1 (#5)
*/
public synchronized boolean isConnected()
{ return ( m_bBound && m_srvInstance != null ) ; }
/**
* Accessor for the service instance, if the connection is bound.
* @return the bound instance of the service, if any
*/
public synchronized S getServiceInstance()
{ return m_srvInstance ; }
/**
* Attempts to connect to the service.
* The caller should not process the return value of this method immediately
* in order to access the service; rather, it should handle the consequences
* of the connection by implementing {@link Listener#onServiceConnected}.
* @param ctx the context in which to bind to the service
* @param bmFlags a mask of optional binding flags; see {@link Context}
* @return the connection, for fluid invocations
* @throws IllegalArgumentException if the context is null
*/
public synchronized SimpleServiceConnection<S> connect( Context ctx, int bmFlags )
throws IllegalArgumentException
{
if( ctx == null )
{
throw new IllegalArgumentException(
"Cannot bind to service from a null context." ) ;
}
if( this.isConnected() )
{ Log.d( this.getLogTag(), "Already connected." ) ; }
else
{ // Bind this connection to the service.
Intent sig = new Intent( ctx, m_clsService ) ;
ctx.bindService( sig, this, bmFlags ) ;
}
return this ;
}
/**
* Attempts to connect to the service, using the default binding control
* flags.
* The caller should not process the return value of this method immediately
* in order to access the service; rather, it should handle the consequences
* of the connection by implementing {@link Listener#onServiceConnected}.
* @param ctx the context in which to bind to the service
* @return the connection, for fluid invocations
* @see #DEFAULT_BINDING_FLAGS
*/
public synchronized SimpleServiceConnection<S> connect( Context ctx )
{ return this.connect( ctx, DEFAULT_BINDING_FLAGS ) ; }
/**
* Attempts to break the connection to the service. Methods that tear down
* the context in which the service was started (such as
* {@link android.app.Activity#onStop}) should call this method to free the
* service binding. The caller should not assume that the binding was
* successfully released; any other consequences of releasing this binding
* should be handled by implementing {@link Listener#onServiceDisconnected}.
* @param ctx the context in which the binding should be broken
* @return the connection, for fluid invocations
*/
public synchronized SimpleServiceConnection<S> disconnect( Context ctx )
{
if( ctx == null )
{ // Return trivially but log a warning.
Log.w( this.getLogTag(), (new StringBuilder())
.append( "Cannot disconnect from service [" )
.append( m_clsService.getCanonicalName() )
.append( "] from a null context. " )
.append( "A connection might be leaked!" )
.toString()
);
return this ;
}
if( ! this.isConnected() ) return this ; // trivially
try { ctx.unbindService(this) ; }
catch( RuntimeException x ) // includes IllegalArgumentException
{ Log.i( this.getLogTag(), "Service was already unbound." ) ; }
return this ;
}
/**
* Called by the Android OS when a the service accepts the connection's
* binding. Notifies all of the connection's listeners that the binding has
* been completed.
* @param cn the name of the service component
* @param binder a binder to the service
*/
@SuppressWarnings( "unchecked" ) // We are indeed checking the typecast.
@Override
public void onServiceConnected( ComponentName cn, IBinder binder )
{
InstanceBinder<S> srvb ;
try { srvb = ((InstanceBinder<S>)(binder)) ; }
catch( ClassCastException xCast )
{
Log.w( this.getLogTag(), (new StringBuilder())
.append( "Class [" )
.append( m_clsService.getCanonicalName() )
.append( "] does not supply a compliant service binding." )
.toString()
);
return ;
}
m_srvInstance = srvb.getServiceInstance() ;
m_bBound = true ;
for( Listener<S> l : this.getListeners() )
l.onServiceConnected(this) ;
}
/**
* Called by the Android OS when a connection's binding to the service must
* be broken. Notifies all of the connection's listeners that the binding
* has been broken.
* @param cn the name of the service component
*/
@Override
public void onServiceDisconnected( ComponentName cn )
{
m_srvInstance = null ;
m_bBound = false ;
for( Listener<S> l : this.getListeners() )
l.onServiceDisconnected(this) ;
}
}