S. Gökhan Topçu
gtopcu@gmail.com
In Part 1 and Part 2 of this blog series, I demonstrated how you can use started and bound services in your apps. In this blog post, I will provide an example app which uses a bound service to download some data. What makes this sample important is that it retains the service instance when the screen is rotated, and provides an example of how you can communicate back to your client activity using callback methods defined in the custom Binder implementation.
As you will recall, a bound service gets destroyed when all activities unbind. So the trick to keep a service alive is two rules:
1. Bind the service using the application context instead of the client (i.e. Activity) using getApplicationContext().bindService() instead of using Activity.bindService()
2. Keep an instance of your ServiceConnection and pass it to your new activity when the configuration changes (i.e. the screen is rotated).
MainActivity.java
package com.gtware.android;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.gtware.android.DownloaderBinder.DownloaderListener;
public class MainActivity extends Activity implements DownloaderListener {
private Button button;
private boolean unBindService = true;
private static DownloaderBinder binder;
private ServiceConnection connection;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button)findViewById(R.id.button);
}
@SuppressWarnings("deprecation")
@Override
protected void onStart() {
super.onStart();
connection = (ServiceConnection)getLastNonConfigurationInstance();
if(connection == null) {
connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
binder = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
binder = (DownloaderBinder) service;
binder.attachListener(MainActivity.this);
}
};
getApplicationContext().bindService(new Intent(getApplicationContext(), DownloaderService.class), connection, Service.BIND_AUTO_CREATE);
}
else {
binder.attachListener(this);
button.setEnabled(!binder.isDownloadInProgress());
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
unBindService = false;
super.onSaveInstanceState(outState);
}
@Override
@Deprecated
public Object onRetainNonConfigurationInstance() {
return connection;
}
@Override
protected void onDestroy() {
super.onDestroy();
//Is the activity just being rotated or destroyed for good?
//We check if onRetainNonConfigurationInstance is called to understand
Log.i(MainActivity.class.getName(), "Unbind: " + unBindService);
binder.detachListener();
if(unBindService) {
binder = null;
unbindService(connection);
}
}
public void download(View view) {
if(binder != null) {
button.setEnabled(false);
binder.download("http://download.thinkbroadband.com/5MB.zip");
}
}
@Override
public void downloadComplete(int result) {
button.setEnabled(true);
if(result == Activity.RESULT_OK) {
Toast.makeText(MainActivity.this, "Success", Toast.LENGTH_LONG).show();
}
else if(result == Activity.RESULT_CANCELED) {
Toast.makeText(MainActivity.this, "Canceled", Toast.LENGTH_LONG).show();
}
else {
Toast.makeText(MainActivity.this, "Unknown status", Toast.LENGTH_LONG).show();
}
}
}
DownloaderService.java
package com.gtware.android;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class DownloaderService extends Service {
private DownloaderBinder binder;
@Override
public void onCreate() {
super.onCreate();
binder = new DownloaderBinder();
binder.onCreate();
Log.i(DownloaderService.class.getName(), "Service created");
}
@Override
public void onDestroy() {
super.onDestroy();
binder.onDestroy();
Log.i(DownloaderService.class.getName(), "Service destroyed");
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
DownloadBinder.java
package com.gtware.android;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import android.app.Activity;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.util.Log;
public class DownloaderBinder extends Binder {
private HttpClient client;
private DownloaderListener listener;
private boolean downloadInProgress = false;
private MyHandler handler;
private class MyHandler extends Handler {
private MyHandler(Looper looper) {
super(looper);
}
public void handleMessage(Message msg) {
doDownload((String) msg.obj);
}
};
public interface DownloaderListener {
public void downloadComplete(int result);
}
public void onCreate() {
client = new DefaultHttpClient();
HandlerThread thread = new HandlerThread("DownloaderThread", Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
handler = new MyHandler(thread.getLooper());
}
public void onDestroy() {
listener = null;
client.getConnectionManager().shutdown();
}
public void attachListener(DownloaderListener listener) {
this.listener = listener;
}
public void detachListener() {
listener = null;
}
public void download(String url) {
Message msg = handler.obtainMessage(0, url);
handler.sendMessage(msg);
}
private void doDownload(String url) {
downloadInProgress = true;
Log.i(this.getClass().getName(), "Start downloading url: " + url);
HttpGet get = new HttpGet(url);
int result = Activity.RESULT_CANCELED;
try {
ResponseHandler<byte[]> responseHandler = new ResponseHandler<byte[]>() {
@Override
public byte[] handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
if(response.getStatusLine().getStatusCode() == 200 && response.getEntity() != null) {
return EntityUtils.toByteArray(response.getEntity());
}
return null;
}
};
byte[] responseBody = client.execute(get, responseHandler);
if(responseBody != null) {
File output = new File(Environment.getExternalStorageDirectory(),
Uri.parse(url).getLastPathSegment());
if(output.exists()) {
output.delete();
}
FileOutputStream fos = new FileOutputStream(output);
fos.write(responseBody);
fos.close();
result = Activity.RESULT_OK;
}
}
catch(Exception e) {
e.printStackTrace();
}
Log.i(this.getClass().getName(), "Finished, notifiying listener: " + (listener != null));
final int i = result;
if(listener != null) {
((Activity)listener).runOnUiThread(new Runnable() {
@Override
public void run() {
listener.downloadComplete(i);
}
});
}
downloadInProgress = false;
}
public boolean isDownloadInProgress() {
return downloadInProgress;
}
}
Do not forget to define the service in your manifest, as well as providing internet and external memory access permissions to your manifest. When you run the example, you will see that the service doesn't get killed when the activity is, if the screen is rotated. But it will be, if you exit the app since onSaveInstanceState() doesn't get called then. Also, do not forget to unbind the listener (activity) since you will have a huge memory leak if you keep reference to the old activity. This is a nice example of how you can retain your services through your app lifecycle and communicate back to your service clients.
Another way to communicate back to your clients would be defining a Messenger in your activity and passing it to the service with the intent used to start the service. Since Messenger is a subclass of Parcelable, you can use the method intent.putExtra("Messenger", new Messenger(myHandlerInstance)); to pass it with the intent to your service.
Further Reading:
Using Services in Android Part 1 - Started Services
Using Services in Android Part 2 - Bound Services
Services
Bound Services
AIDL
Service
IBinder
Binder
ServiceConnection
Messenger
IntentService
AsyncTask
Handler
HandlerThread
Looper
No comments:
Post a Comment
Please leave your feedback below if you found this blog useful, thanks.