BoundSelfStoppingService

Submitted by code_admin on Fri, 07/20/2018 - 13:18

Backgroud

A common problem with android applications I write is that there is no reliable way to close application level resources. The activity life cycle has onPause and onResume but this isn’t useful for resources that need to be maintained from one activity to another.
The best solution that I have found is to create a service that counts whenever an activity binds to it. When the count reaches 0 it waits a certain amount of time then times out running the close code. Then each activity can bind to this service.
I have written this as a super class which can be reused in many applications. This comes out of the question http://stackoverflow.com/questions/23499044/android-application-close-c… on stack exchange and I am posting this in the hope that more expert android developers can evaluate my solution and let me know if there is another more optimum way.

BoundSelfStoppingService class

This class extends Service and is meant to be a base class for services created using this pattern. It accepts an inactivity timeout parameter which specifies how long to wait after the last client disconnects to terminate. It also provides a processing loop (similar to a game loop) where regular processing can be preformed. This is uses the standard Handler and Runnable classes. The loop is run on the main service thread.

  1. package metcarob.com.common.android.services;
  2.  
  3. import android.app.Service;
  4. import android.content.Intent;
  5. import android.os.Binder;
  6. import android.os.Handler;
  7. import android.os.IBinder;
  8.  
  9. /*
  10.  * This is a service class that runs a process on a periodic basis
  11.  * it counts bound connections
  12.  * when there are 0 bound methods and a set time has elapsed
  13.  * it will terminate itself
  14.  * Classes can be derived from it which will override
  15.  *  - Constructor (so set interval and inactivityTimeout
  16.  *  - startLoop
  17.  *  - LoopIteration
  18.  *  - endLoop
  19.  *  to function
  20.  *  
  21.  *  Clients should bind to this
  22.  *  and in onServiceConnected call clientConnected
  23.  *  and in onServiceDisconnected call clientDisconnected
  24.  */
  25. public class BoundSelfStoppingService extends Service {
  26.     private final IBinder mbinder = new LocalBinder();
  27.     private boolean started = false;
  28.     private boolean finished = false; //set when service is finished running
  29.     private long lastSeenClient = -1;
  30.     private long inactivityTimeout = 10000; //Milliseconds
  31.     private long loopInterval;
  32.    
  33.     public BoundSelfStoppingService(long p_inactivityTimeout, long p_loopInterval) {
  34.         inactivityTimeout = p_inactivityTimeout;
  35.         loopInterval = p_loopInterval;
  36.     }
  37.    
  38.     private int numClientsConnected = 0;
  39.     public synchronized void clientConnect() {
  40.         numClientsConnected++;
  41.         if (!started) {
  42.             lastSeenClient = System.currentTimeMillis();
  43.            
  44.             Intent i = new Intent(this,this.getClass());
  45.             this.startService(i);
  46.             started = true;
  47.             m_handler = new Handler();
  48.             m_handler.postDelayed(m_runnable, loopInterval);       
  49.             try {
  50.                 loopStart();
  51.             } catch (Exception e) {
  52.                 // TODO Auto-generated catch block
  53.                 e.printStackTrace();
  54.             }
  55.         }
  56.     }
  57.     public synchronized void clientDisconnect() {
  58.         numClientsConnected--;
  59.     }
  60.     public synchronized int getConnectionCount() {
  61.         return numClientsConnected;
  62.     }
  63.    
  64.     private Handler m_handler = null;
  65.     private Runnable m_runnable = new Runnable() {
  66.            @Override
  67.            public void run() {
  68.                long thisLoop = System.currentTimeMillis();
  69.                if (getConnectionCount()>0) {
  70.                    lastSeenClient = System.currentTimeMillis();
  71.                } else {
  72.                    //we have no clients
  73.                    if ((thisLoop-lastSeenClient)>inactivityTimeout) {
  74.                     try {
  75.                         finished = true;
  76.                         BoundSelfStoppingService.this.loopEnd();
  77.                     } catch (Exception e) {
  78.                         // TODO Auto-generated catch block
  79.                         e.printStackTrace();
  80.                     }                      
  81.                     BoundSelfStoppingService.this.stopSelf();
  82.                    }
  83.                }
  84.                try {
  85.                     if (finished) return;
  86.                     BoundSelfStoppingService.this.loopIteration(thisLoop);
  87.                 } catch (Exception e) {
  88.                     // TODO Auto-generated catch block
  89.                     e.printStackTrace();
  90.                 }
  91.              
  92.                m_handler.postDelayed(this, loopInterval);
  93.            }
  94.     }; 
  95.    
  96.     @Override
  97.     public IBinder onBind(Intent intent) {
  98.         return mbinder;
  99.     }
  100.    
  101.     public class LocalBinder extends Binder {
  102.         public BoundSelfStoppingService getService() {
  103.             return BoundSelfStoppingService.this;
  104.         }
  105.     }
  106.    
  107.    
  108.     //To be overridden on termination
  109.     public void loopStart() throws Exception {
  110.         throw new Exception("Subclasses should have overridden this");
  111.     }  
  112.  
  113.     //To be overridden to run the processing
  114.     public void loopIteration(long p_loopTime) throws Exception {
  115.         throw new Exception("Subclasses should have overridden this");
  116.     }
  117.  
  118.     //To be overridden on termination
  119.     public void loopEnd() throws Exception {
  120.         throw new Exception("Subclasses should have overridden this");
  121.     }  
  122. }

Example Derived Class

This is a simple example of a class that extends the BoundSelfStoppingService class.

  1. package metcarob.com.dev.android.test.andtoidtest;
  2.  
  3. import android.widget.Toast;
  4. import metcarob.com.common.android.services.BoundSelfStoppingService;
  5.  
  6. public class TestService extends BoundSelfStoppingService {
  7.  
  8.     public TestService() {
  9.         super(1000, 500);
  10.         msg("BS CONSTRUCTOR");
  11.     }
  12.    
  13.     private void msg(String p_msg) {
  14.         //Toast.makeText(getApplicationContext(), (String)p_msg, Toast.LENGTH_LONG).show();
  15.         System.out.println(p_msg);
  16.        
  17.     }
  18.  
  19.     @Override
  20.     public void loopStart() throws Exception {
  21.         msg("BS LOOP START");
  22.     }
  23.  
  24.     int m_loopCounter = 0;
  25.    
  26.     @Override
  27.     public void loopIteration(long p_loopTime) throws Exception {
  28.         msg("BS LOOP LOOP " + m_loopCounter + " at " + p_loopTime + " (" + getConnectionCount() + " conns)");
  29.        
  30.         m_loopCounter++;
  31.     }
  32.  
  33.     @Override
  34.     public void loopEnd() throws Exception {
  35.         msg("BS LOOP END");
  36.     }
  37. }

This simple test class demos the functionality and I used it in a test application. It overrider the loopStart/Iteration and End functions of BoundSelfStoppingService to simply provide messages. The log viewer can be user to determine that the class is performing as desired.

Example Activity Class

Finally this is an example of an activity class that uses the Service. It has a lot of standard code I will go through the specific code for using the service below.

  1. package metcarob.com.dev.android.test.andtoidtest;
  2.  
  3. import metcarob.com.common.android.services.BoundSelfStoppingService.LocalBinder;
  4. import android.support.v7.app.ActionBarActivity;
  5. import android.support.v7.app.ActionBar;
  6. import android.support.v4.app.Fragment;
  7. import android.content.ComponentName;
  8. import android.content.Context;
  9. import android.content.Intent;
  10. import android.content.ServiceConnection;
  11. import android.os.Bundle;
  12. import android.os.IBinder;
  13. import android.view.LayoutInflater;
  14. import android.view.Menu;
  15. import android.view.MenuItem;
  16. import android.view.View;
  17. import android.view.ViewGroup;
  18. import android.widget.Toast;
  19. import android.os.Build;
  20.  
  21. public class SecondActivity extends ActionBarActivity {
  22.  
  23.     @Override
  24.     protected void onCreate(Bundle savedInstanceState) {
  25.         super.onCreate(savedInstanceState);
  26.         setContentView(R.layout.activity_second);
  27.  
  28.         if (savedInstanceState == null) {
  29.             getSupportFragmentManager().beginTransaction()
  30.                     .add(R.id.container, new PlaceholderFragment()).commit();
  31.         }
  32.     }
  33.  
  34.     @Override
  35.     public boolean onCreateOptionsMenu(Menu menu) {
  36.  
  37.         // Inflate the menu; this adds items to the action bar if it is present.
  38.         getMenuInflater().inflate(R.menu.second, menu);
  39.         return true;
  40.     }
  41.  
  42.     @Override
  43.     public boolean onOptionsItemSelected(MenuItem item) {
  44.         // Handle action bar item clicks here. The action bar will
  45.         // automatically handle clicks on the Home/Up button, so long
  46.         // as you specify a parent activity in AndroidManifest.xml.
  47.         int id = item.getItemId();
  48.         if (id == R.id.action_settings) {
  49.             return true;
  50.         }
  51.         return super.onOptionsItemSelected(item);
  52.     }
  53.  
  54.     /**
  55.      * A placeholder fragment containing a simple view.
  56.      */
  57.     public static class PlaceholderFragment extends Fragment {
  58.  
  59.         public PlaceholderFragment() {
  60.         }
  61.  
  62.         @Override
  63.         public View onCreateView(LayoutInflater inflater, ViewGroup container,
  64.                 Bundle savedInstanceState) {
  65.             View rootView = inflater.inflate(R.layout.fragment_second,
  66.                     container, false);
  67.             return rootView;
  68.         }
  69.     }
  70.  
  71.    
  72.     @Override
  73.     protected void onPause() {
  74.         super.onPause();
  75.         bs.clientDisconnect();
  76.         bs = null;
  77.         unbindService(sc);
  78.         msg("onPause");
  79.     }
  80.  
  81.     @Override
  82.     protected void onResume() {
  83.         super.onResume();
  84.         Intent i = new Intent(this,metcarob.com.dev.android.test.andtoidtest.TestService.class);
  85.         bindService(i,sc,Context.BIND_AUTO_CREATE);
  86.         msg("onResume");
  87.     }
  88.  
  89.     private void msg(String p_msg) {
  90.         System.out.println(p_msg);
  91.         Toast.makeText(getApplicationContext(), (String)p_msg, Toast.LENGTH_LONG).show();
  92.     }
  93.     TestService bs;
  94.     private ServiceConnection sc = new ServiceConnection() {
  95.         @Override
  96.         public void onServiceDisconnected(ComponentName name) {
  97.             if (bs!=null) {
  98.                 bs.clientDisconnect();
  99.                 bs = null;
  100.             };
  101.             msg("service disconnected");
  102.         }
  103.         @Override
  104.         public void onServiceConnected(ComponentName name, IBinder service) {
  105.             LocalBinder binder = (LocalBinder)service;
  106.             bs = (TestService) binder.getService();
  107.             bs.clientConnect();
  108.             msg("service connected");
  109.         }
  110.     }; 
  111. }

onResume is used to create an initial binding to the TestService class defined earlier. If the service is not running it is started, otherwise the existing service is bound to.
The onPause method disconnects and unbinds the service.
TestService bs variable is created so we can interface with the service.
A ServiceConnection class is creates with onServiceDisconnected and onServiceConnected handlers implementd. These call the code to allow the service to count it’s connections.

Outstanding Issues

Although the code as is works in my test application. The service runs successfully between activities and loopEnd is only called once when the application has exited I have found that the onServiceDisconnected code is never run. This is why the onPause activity has to explicitly call clientDisconnect. There is a possible problem here since this if onServiceDisconnected is called the connection counter would be wrong. I can solve this by either working out why onServiceDisconnected is not called and fixing that or by implementing a robust service connection counter. (Give each client a random token and when it calls disconnect it can supply that token. If the service maintains a list of these tokens I can prevent double counting disconnects.)

Tags

RJM Article Type
Work Notes