Given an Android intent service whose job is do background network communication (e.g. make a REST call to synchronize data), when the intent service catches an IOException
, what is a good practice for recovering from the error?
Let's assume the amount of data transferred is small enough that we're content retry the network operation from scratch. If the device lost network connection, we want to be notified when connectivity is restored and try again. If we didn't lose connection, we assume the server or its network link is down, and want to try again after a delay.
It is not critical to complete the communication operation as soon as possible, but quicker does mean a better user experience, although it must be traded off against bandwidth usage and battery life.
Hopefully, this is a common requirement and the functionality is baked into Android. If so, where is it, or if not, what would the code to intelligently restart the intent service look like?
I created a helper class that examines the network state and waits for the network to become available, or if it is available, delays using an exponential backoff.
/**
* Handles error recovery for background network operations.
*
* Recovers from inability to perform background network operations by applying a capped exponential backoff, or if connectivity is lost, retrying after it is
* restored. The goal is to balance app responsiveness with battery, network, and server resource use.
*
* Methods on this class are expected to be called from the UI thread.
* */
public final class ConnectivityRetryManager {
private static final int INITIAL_DELAY_MILLISECONDS = 5 * 1000;
private static final int MAX_DELAY_MILLISECONDS = 5 * 60 * 1000;
private int delay;
public ConnectivityRetryManager() {
reset();
}
/**
* Called after a network operation succeeds. Resets the delay to the minimum time and unregisters the listener for restoration of network connectivity.
*/
public void reset() {
delay = INITIAL_DELAY_MILLISECONDS;
}
/**
* Retries after a delay or when connectivity is restored. Typically called after a network operation fails.
*
* The delay increases (up to a max delay) each time this method is called. The delay resets when {@link reset} is called.
*/
public void retryLater(final Runnable retryRunnable) {
// Registers to retry after a delay. If there is no Internet connection, always uses the maximum delay.
boolean isInternetAvailable = isInternetAvailable();
delay = isInternetAvailable ? Math.min(delay * 2, MAX_DELAY_MILLISECONDS) : MAX_DELAY_MILLISECONDS;
new RetryReciever(retryRunnable, isInternetAvailable);
}
/**
* Indicates whether network connectivity exists.
*/
public boolean isInternetAvailable() {
NetworkInfo network = ((ConnectivityManager) App.getContext().getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
return network != null && network.isConnected();
}
/**
* Calls a retry runnable after a timeout or when the network is restored, whichever comes first.
*/
private class RetryReciever extends BroadcastReceiver implements Runnable {
private final Handler handler = new Handler();
private final Runnable retryRunnable;
private boolean isInternetAvailable;
public RetryReciever(Runnable retryRunnable, boolean isInternetAvailable) {
this.retryRunnable = retryRunnable;
this.isInternetAvailable = isInternetAvailable;
handler.postDelayed(this, delay);
App.getContext().registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
}
@Override
public void onReceive(Context context, Intent intent) {
boolean wasInternetAvailable = isInternetAvailable;
isInternetAvailable = isInternetAvailable();
if (isInternetAvailable && !wasInternetAvailable) {
reset();
handler.post(this);
}
}
@Override
public void run() {
handler.removeCallbacks(this);
App.getContext().unregisterReceiver(this);
retryRunnable.run();
}
}
}