Simplifying service binding with dynamic proxies

Quickly share your Android Code Snippets here...

Simplifying service binding with dynamic proxies

Postby kloffy » Tue Oct 20, 2009 10:14 pm

Here's some code I've been tinkering with. First, let's look at some example usage. Suppose you have a service and an activity defined as follows:
Syntax: [ Download ] [ Hide ]
Using xml Syntax Highlighting
  1. <?xml version="1.0" encoding="utf-8"?>
  2.  
  3. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  4.  
  5.         package="com.example.service"
  6.  
  7.         android:versionCode="1"
  8.  
  9.         android:versionName="1.0">
  10.  
  11.         <application android:icon="@drawable/icon" android:label="@string/app_name">
  12.  
  13.         <!-- Service -->
  14.  
  15.         <service android:name=".service.Service"
  16.  
  17.                 android:enabled="true"
  18.  
  19.                 android:exported="true"
  20.  
  21.                 android:process=":remote">
  22.  
  23.                 <intent-filter>
  24.  
  25.                         <action android:name="com.example.service.IService" />
  26.  
  27.                 </intent-filter>
  28.  
  29.         </service>
  30.  
  31.         <!-- Activity -->
  32.  
  33.         <activity android:name=".app.Activity" android:label="@string/app_name">
  34.  
  35.                 <intent-filter>
  36.  
  37.                         <action android:name="android.intent.action.MAIN" />
  38.  
  39.                         <category android:name="android.intent.category.LAUNCHER" />
  40.  
  41.                 </intent-filter>
  42.  
  43.         </activity>
  44.  
  45.         </application>
  46.  
  47.         <uses-sdk android:minSdkVersion="3" />
  48.  
  49. </manifest>
Parsed in 0.004 seconds, using GeSHi 1.0.8.4

And your service supports this interface:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. package com.example.service;
  2.  
  3.  
  4.  
  5. interface IService {
  6.  
  7.         int getPid();
  8.  
  9. }
Parsed in 0.029 seconds, using GeSHi 1.0.8.4

Suppose we have an Activity with three buttons to bind/unbind/check the service. This is how you would use my connection proxy to interact with the service from the activity:
Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. package com.example.service.app;
  2.  
  3.  
  4.  
  5. import android.content.*;
  6.  
  7. import android.os.*;
  8.  
  9. import android.util.*;
  10.  
  11. import android.view.*;
  12.  
  13. import android.widget.*;
  14.  
  15.  
  16.  
  17. import com.example.service.*;
  18.  
  19.  
  20.  
  21. public class Activity extends android.app.Activity {
  22.  
  23.         public static final String TAG = Activity.class.getSimpleName();
  24.  
  25.        
  26.  
  27.         private IService service = Connection.create(IService.class);
  28.  
  29.        
  30.  
  31.         @Override
  32.  
  33.         public void onCreate(Bundle savedInstanceState)
  34.  
  35.         {
  36.  
  37.                 super.onCreate(savedInstanceState);
  38.  
  39.  
  40.  
  41.                 setContentView(R.layout.main);
  42.  
  43.  
  44.  
  45.                 final IConnection connection = (IConnection)service;
  46.  
  47.                
  48.  
  49.                 final Button bindServiceButton = (Button) findViewById(R.id.bindservice);
  50.  
  51.                 bindServiceButton.setOnClickListener(new View.OnClickListener() {
  52.  
  53.                         public void onClick(View view) {
  54.  
  55.                                 connection.bind(Activity.this, Context.BIND_AUTO_CREATE);
  56.  
  57.                         }
  58.  
  59.                 });
  60.  
  61.                 final Button unbindServiceButton = (Button) findViewById(R.id.unbindservice);
  62.  
  63.                 unbindServiceButton.setOnClickListener(new View.OnClickListener() {
  64.  
  65.                         public void onClick(View view) {
  66.  
  67.                                 connection.unbind(Activity.this);
  68.  
  69.                         }
  70.  
  71.                 });
  72.  
  73.                 final Button checkServiceButton = (Button) findViewById(R.id.checkservice);
  74.  
  75.                 unbindServiceButton.setOnClickListener(new View.OnClickListener() {
  76.  
  77.                         public void onClick(View view) {
  78.  
  79.                                 Toast.makeText(Activity.this, "The service is" + (connection.isConnected()?"":" not") + " connected.", Toast.LENGTH_SHORT).show();
  80.  
  81.                         }
  82.  
  83.                 });
  84.  
  85.  
  86.  
  87.                 connection.addOnConnected(new Runnable() {
  88.  
  89.                         @Override
  90.  
  91.                         public void run() {
  92.  
  93.                                 try {
  94.  
  95.                                         Log.w(TAG, "Connected. (Pid: " + service.getPid() + ")");
  96.  
  97.                                         bindServiceButton.setEnabled(false);
  98.  
  99.                                         unbindServiceButton.setEnabled(true);
  100.  
  101.                                 } catch (RemoteException e) {
  102.  
  103.                                         e.printStackTrace();
  104.  
  105.                                 }
  106.  
  107.                         }
  108.  
  109.                 });
  110.  
  111.        
  112.  
  113.                 connection.addOnDisconnected(new Runnable() {
  114.  
  115.                         @Override
  116.  
  117.                         public void run() {
  118.  
  119.                                 Log.w(TAG, "Disconnected.");
  120.  
  121.                                 bindServiceButton.setEnabled(true);
  122.  
  123.                                 unbindServiceButton.setEnabled(false);
  124.  
  125.                         }
  126.  
  127.                 });
  128.  
  129.  
  130.  
  131.                 connection.bind(this, Context.BIND_AUTO_CREATE);
  132.  
  133.         }
  134.  
  135.  
  136.  
  137.         @Override
  138.  
  139.         public void onDestroy() {
  140.  
  141.                 final IConnection connection = (IConnection)service;
  142.  
  143.                 connection.unbind(this);
  144.  
  145.                 super.onDestroy();
  146.  
  147.         }
  148.  
  149. }
Parsed in 0.040 seconds, using GeSHi 1.0.8.4

Now, at first this may seem similar to the way it is done in the standard android API. However, there are several simplifications. The ServiceConnection is managed by the proxy. The user can use IConnection::isConnected() to query whether the service is bound. It is very easy for the user to invoke the methods of the IService interface. If the user invokes a method from the IService interface while the service is not bound, an exception will be raised (I'm not sure yet if it's a good idea to use RemoteException for this). The user can also run code when a binding is created or destroyed (addOnConnected/addOnDisconnected). OnDisconnected also fires on IConnection::unbind() so the state transitions are always:

[align=center][Unbound]---onConnected-->[Bound] and [Bound]---onDiconnected-->[Unbound][/align]
(Unlike the asymetrical ServiceConnection::onServiceConnected/ServiceConnection::onServiceDisconnected, where onServiceDisconnected is only called if the service is stopped/killed.)

Any comments or suggestions so far?
kloffy
Freshman
Freshman
 
Posts: 2
Joined: Tue Oct 20, 2009 9:17 pm

Top

Postby kloffy » Tue Oct 20, 2009 10:44 pm

And now part two: How is this accomplished? Beware, the following code is WIP, but it works pretty well.

First, there's another small proxy called ObjectCollection. This one is just a nicer way to invoke a method on a collection of objects. It really has nothing to do with the service binding. (It would have been better to leave this one out, but I've already implemented it like this...)

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. public class ObjectCollection<T> implements InvocationHandler {
  2.  
  3.         private Class<T> clazz = null;
  4.  
  5.         private Collection<T> objects = new HashSet<T>();
  6.  
  7.        
  8.  
  9.         @SuppressWarnings("unchecked")
  10.  
  11.         public static <T> T create(Class<T> clazz){
  12.  
  13.                 return (T)Proxy.newProxyInstance(Collection.class.getClassLoader(), new Class[] { Collection.class, clazz }, new ObjectCollection<T>(clazz));
  14.  
  15.         }
  16.  
  17.         private ObjectCollection(Class<T> clazz) {
  18.  
  19.                 this.clazz = clazz;
  20.  
  21.         }
  22.  
  23.         @Override
  24.  
  25.         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  26.  
  27.                 if(method.getDeclaringClass()==Object.class) {
  28.  
  29.                         return method.invoke(this, args);
  30.  
  31.                 } else if(method.getDeclaringClass()==Collection.class) {
  32.  
  33.                         return method.invoke(objects, args);
  34.  
  35.                 } else if(method.getDeclaringClass()==clazz) {
  36.  
  37.                         for(T object: objects) {
  38.  
  39.                                 method.invoke(object, args);
  40.  
  41.                         }
  42.  
  43.                         return null;
  44.  
  45.                 }
  46.  
  47.                 throw new Exception("This should never throw.");
  48.  
  49.         }
  50.  
  51. }
Parsed in 0.038 seconds, using GeSHi 1.0.8.4

This is the IConnection interface:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. public interface IConnection {
  2.  
  3.         public boolean isConnected();
  4.  
  5.         public void bind(Context context, int flags);
  6.  
  7.         public void unbind(Context context);
  8.  
  9.         public void addOnConnected(Runnable run);
  10.  
  11.         public void removeOnConnected(Runnable run);
  12.  
  13.         public void addOnDisconnected(Runnable run);
  14.  
  15.         public void removeOnDisconnected(Runnable run);
  16.  
  17. }
Parsed in 0.036 seconds, using GeSHi 1.0.8.4

Finally, here is the interesting part, the connection:

Syntax: [ Download ] [ Hide ]
Using java Syntax Highlighting
  1. public class Connection<T> implements InvocationHandler, IConnection, android.content.ServiceConnection {
  2.  
  3.         private Class<T> clazz = null;
  4.  
  5.         private Method asInterface = null;
  6.  
  7.         private T service = null;
  8.  
  9.         private Runnable connected = ObjectCollection.create(Runnable.class);
  10.  
  11.         private Runnable disconnected = ObjectCollection.create(Runnable.class);
  12.  
  13.  
  14.  
  15.         @SuppressWarnings("unchecked")
  16.  
  17.         public static <T> T create(Class<T> clazz){
  18.  
  19.                 return (T)Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] { IConnection.class, clazz }, new Connection<T>(clazz));
  20.  
  21.         }
  22.  
  23.         private Connection(Class<T> clazz) {
  24.  
  25.                 try {
  26.  
  27.                         this.clazz = clazz;
  28.  
  29.                         this.asInterface = Class.forName(clazz.getCanonicalName()+"$Stub").getMethod("asInterface", IBinder.class);
  30.  
  31.                 } catch (Exception e) { /* Never throws. */ }
  32.  
  33.         }
  34.  
  35.         @Override
  36.  
  37.         public boolean isConnected() {
  38.  
  39.                 return service!=null;
  40.  
  41.         }
  42.  
  43.         @Override
  44.  
  45.         public void bind(Context context, int flags) {
  46.  
  47.                 if(!isConnected()) {
  48.  
  49.                         context.bindService(new Intent(clazz.getName()), this, flags);
  50.  
  51.                 }
  52.  
  53.         }
  54.  
  55.         @Override
  56.  
  57.         public void unbind(Context context) {
  58.  
  59.                 if(isConnected()) {
  60.  
  61.                         context.unbindService(this);
  62.  
  63.                         onServiceDisconnected(null);
  64.  
  65.                 }
  66.  
  67.         }
  68.  
  69.         @Override
  70.  
  71.         @SuppressWarnings("unchecked")
  72.  
  73.         public void addOnConnected(Runnable run) {
  74.  
  75.                 ((Collection)connected).add(run);
  76.  
  77.         }
  78.  
  79.         @Override
  80.  
  81.         @SuppressWarnings("unchecked")
  82.  
  83.         public void removeOnConnected(Runnable run) {
  84.  
  85.                 ((Collection)connected).remove(run);
  86.  
  87.         }
  88.  
  89.         @Override
  90.  
  91.         @SuppressWarnings("unchecked")
  92.  
  93.         public void addOnDisconnected(Runnable run) {
  94.  
  95.                 ((Collection)disconnected).add(run);
  96.  
  97.         }
  98.  
  99.         @Override
  100.  
  101.         @SuppressWarnings("unchecked")
  102.  
  103.         public void removeOnDisconnected(Runnable run) {
  104.  
  105.                 ((Collection)disconnected).remove(run);
  106.  
  107.         }
  108.  
  109.         @Override
  110.  
  111.         @SuppressWarnings("unchecked")
  112.  
  113.         public void onServiceConnected(ComponentName className, IBinder boundService) {
  114.  
  115.                 try {
  116.  
  117.                         service = (T)asInterface.invoke(null, boundService);
  118.  
  119.                 } catch (Exception e) { /* Never throws. */ }
  120.  
  121.                 connected.run();
  122.  
  123.         }
  124.  
  125.         @Override
  126.  
  127.         public void onServiceDisconnected(ComponentName className) {
  128.  
  129.                 service = null;
  130.  
  131.                 disconnected.run();
  132.  
  133.         }
  134.  
  135.         @Override
  136.  
  137.         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  138.  
  139.                 if(method.getDeclaringClass()==Object.class) {
  140.  
  141.                         return method.invoke(this, args);
  142.  
  143.                 } else if(method.getDeclaringClass()==IConnection.class) {
  144.  
  145.                         return method.invoke(this, args);
  146.  
  147.                 } else if(method.getDeclaringClass()==clazz) {
  148.  
  149.                         if(!isConnected()) throw new RemoteException();
  150.  
  151.                         return method.invoke(service, args);
  152.  
  153.                 }
  154.  
  155.                 throw new Exception("This should never throw.");
  156.  
  157.         }
  158.  
  159. }
Parsed in 0.049 seconds, using GeSHi 1.0.8.4

Any feedback is appreciated!
kloffy
Freshman
Freshman
 
Posts: 2
Joined: Tue Oct 20, 2009 9:17 pm

Top

Return to Code Snippets for Android

Who is online

Users browsing this forum: No registered users and 5 guests