Sergio
Sergio

Reputation: 9917

Android Intent Service that pauses when no internet connection

I have an intent service that I pass database ID's to. The service then fetches the relevant row from the database. It then send this data to a web server using volley.

The Handle Intent method checks for an internet connection and, if none is found, sleeps the thread before checking again. This feels wrong and naughty but I do need the service to wait for internet.

I also need the service to handle the work queue in the order it is populated.

Here is the current code. Is there a better way to handle this scenario?

public class CommandUploadService extends IntentService {

// Binder given to clients
private final IBinder mBinder = new LocalBinder();
private ServiceCallbacks serviceCallbacks;

public void setCallbacks(ServiceCallbacks callbacks) {
    serviceCallbacks = callbacks;
}

/**
 * Class used for the client Binder.  Because we know this service always
 * runs in the same process as its clients, we don't need to deal with IPC.
 */
public class LocalBinder extends Binder {
    public CommandUploadService getService() {
        // Return this instance of LocalService so clients can call public methods
        return CommandUploadService.this;
    }
}

// TODO: Rename actions, choose action names that describe tasks that this
// IntentService can perform, e.g. ACTION_FETCH_NEW_ITEMS
private static final String ACTION_UPLOAD_COMMAND = "com.brandfour.tooltracker.services.action.UPLOAD_COMMAND";


// TODO: Rename parameters
private static final String ID = "com.brandfour.tooltracker.services.id";

/**
 * Starts this service to perform action Foo with the given parameters. If
 * the service is already performing a task this action will be queued.
 *
 * @see IntentService
 */
// TODO: Customize helper method
public static void startActionUploadCommand(Context context, String actionID) {
    Intent intent = new Intent(context, CommandUploadService.class);
    intent.setAction(ACTION_UPLOAD_COMMAND);
    intent.putExtra(ID, actionID);
    context.startService(intent);
}

/**
 * Unless you provide binding for your service, you don't need to implement this
 * method, because the default implementation returns null.
 *
 * @param intent
 * @see Service#onBind
 */
@Override
public IBinder onBind(Intent intent) {
    return mBinder;
}

public CommandUploadService() {
    super("CommandUploadService");
}


@Override
protected void onHandleIntent(Intent intent) {
    if (intent != null) {
        final String action = intent.getAction();
        if (ACTION_UPLOAD_COMMAND.equals(action)) {
            final String id = intent.getStringExtra(ID);
            handleActionUpload(id);
        }
    }
}

/**
 * Handle action Foo in the provided background thread with the provided
 * parameters.
 */
private void handleActionUpload(String actionID) {


    final ActionCommand ac = new RushSearch().whereId(actionID).findSingle(ActionCommand.class);

    serviceCallbacks.refreshList();

    ConnectionManager manager = new ConnectionManager();

    Boolean connected = manager.isNetworkOnline(this);
    while (connected == false) {
        connected = manager.isNetworkOnline(this);
        SystemClock.sleep(10000);
    }

    JSONObject json = new JSONObject();
    JSONObject wrapper = new JSONObject();
    try {
        json.put("Command", ac.getCommand());
        json.put("TimeStamp", ac.getTimeStamp());
        json.put("State", ac.getState());

        wrapper.put("command", json);
    } catch (Exception e) {

    }

    String url = "[ommitted]";
    JsonObjectRequest jsonObjReq = new JsonObjectRequest(Request.Method.POST,
            url, wrapper,
            new Response.Listener<JSONObject>() {

                @Override
                public void onResponse(JSONObject response) {
                    ac.setState("SENT");
                    ac.save();
                    serviceCallbacks.complete();
                }
            }, new Response.ErrorListener() {

        @Override
        public void onErrorResponse(VolleyError error) {
            VolleyLog.d("BRANDFOUR", "Error: " + error.getMessage());
        }
    }) {


        @Override
        public String getBodyContentType() {
            return "application/json; charset=utf-8";
        }


    };

    // Adding request to request queue
    RequestQueue queue = Volley.newRequestQueue(this);
    queue.add(jsonObjReq);


}

public interface ServiceCallbacks {
    void complete();

    void refreshList();
}

}

Upvotes: 3

Views: 4359

Answers (3)

Haris Qurashi
Haris Qurashi

Reputation: 2124

Do not pause your IntentService or Thread if you don't have internet connection, use BroadcastReceiver for android.net.conn.CONNECTIVITY_CHANGE. So if you call your intent service first check network availability if not than save your database ID's in SharedPreferences or in app database, when your BroadcastReceiver shows a valid internet connection fetch data from database or SharedPreferences and start your intent service.

Update

Create a database table in your app, with _id, name (optional), HTTP(s) url, status and your database foreign_key_id (currently you are using)

Register your BroadcastReceiver at activity or application level (prefer application level), whenever you try to send your data first check the internet connection if its available send otherwise add your current request to above created database table.

Now you have both database table (which have all incomplete HTTP requests) and a broadcast receiver, when your phone connects to internet your broadcast receiver will you than simply start your CommandUploadService and fetch all rows having status incomplete and execute your HTTP request(s) and update your row row with status complete.

Upvotes: 1

Janus Varmarken
Janus Varmarken

Reputation: 2336

If the order of requests was not important, I would simply cancel the request and restart the same IntentService as part of onHandleIntent. Something like this:

protected void onHandleIntent(Intent intent) {
    // ... Code to get the actionID ...
    Boolean connected = manager.isNetworkOnline(this);
    if (!connected) {
         this.startService(intent);
         return;
    }
    // .. connected to internet, run code that fires the request ...
}

However this approach would cause the current request that cannot be handled due to connectivity issues to be put at the end of the work-queue, and you state that this breaks your logic.

Another issue with this approach is that you keep restarting the service until internet connectivity comes back, and this might be rough on the battery.

Now, a different solution could be to ditch your IntentService and create a regular Service instead. Let us name this service UploadService. UploadService should be started (to keep it running) but also use service binding (for the purpose of communication).

UploadService should manage an internal work-queue that ensures that your requests are handled in the proper order. You should expose a method to queue a request through your IBinder implementation.

The primary functionality of UploadService should be a method that fetches (but does not remove! - use peek()) the front of the queue. Let us name this method handleRequest. If the queue is empty, you should shutdown UploadService. If the queue is not empty, you should spawn an AsyncTask that handles the request placed at the front of the queue. If the request is successfully handled, you remove the front of the queue during onPostExecute and make a new call to handleRequest to check if there are other requests queued. If the the request fails - most likely due to loss of internet connectivity - you do NOT remove the front element during onPostExecute. Instead you check to see if internet connectivity has been lost. If that is indeed the case, you register a BroadcastReceiver that listens for internet connectivity. This BroadcastReceiver should call handleRequest when internet connectivity is once again established to resume processing requests.

A pseudo-code implementation of the approach described above would look something like this:

public class UploadService extends Service {

    private final BroadtcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()) {
                boolean connected;
                // Use extras to verify that connection has been re-established...
                if (connected) {
                    // Unregister until we lose network connectivity again.
                    UploadService.this.unregisterReceiver(this);
                    // Resume handling requests.
                    UploadService.this.handleRequest();
                }
            }
        }
    };

    private final Queue<RequestData> mRequestQueue = new XXXQueue<RequestData>(); // Choose Queue implementation. 

    private final UploadServiceBinder mBinder = new UploadServiceBinder();

    public class UploadServiceBinder extends Binder {
        public void enqueueRequest(RequestData requestData) {
            UploadService.this.mRequestQueue.offer(requestData);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    private void handleRequest() {
        RequestData request = mRequestsQueue.peek();
        if (request == null) {
            // No more requests to process.
            // Shutdown self.
            stopSelf();
        } else {
            // Process the request at the head of the queue.
            new Request().execute(request);
        }
    }

    private class Request extends AsyncTask<RequestData, Void, Boolean> {
        @Override
        protected void doInBackground(RequestData... requests) {
            try {
                // ... Code that executes the web request ...
                // Return true if request succeeds.
                return true;
            } catch(IOException ioe) {
                // Request failed, return false.
                return false;
            }
        }

        @Override
        protected void onPostExecute(Boolean success) {
            if (success) {
                // Remove request from work queue.
                UploadService.this.mRequestQueue.remove();
                // Continue by processing next request.
                UploadService.this.handleRequest();
            } else {
                // Request failed, properly due to network error.
                // Keep request at the head of the queue, i.e. do not remove it from the queue.
                // Check current internet connectivity
                ConnectionManager manager = new ConnectionManager();
                boolean connected = manager.isNetworkOnline(UploadService.this);
                if (connected) {
                    // If connected, something else went wrong.
                    // Retry request right away.
                    UploadService.this.handleRequest();
                } else {
                    // Lack of internet.
                    // Register receiver in order to resume processing requests once internet connectivity is restored.
                    IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
                    UploadService.this.registerReceiver(UploadService.this.mReceiver, filter);
                }
            }
        }
    }
}

Upvotes: 1

jtt
jtt

Reputation: 13541

You should not be polling for internet, you should instead listen (via BroadcastReceiver) for when connectivity state changes: @ http://developer.android.com/reference/android/net/ConnectivityManager.html

Excerpt

public static final String CONNECTIVITY_ACTION

Added in API level 1
A change in network connectivity has occurred. A default connection has either been established or lost. The NetworkInfo for the affected network is sent as an extra; it should be consulted to see what kind of connectivity event occurred.

If this is a connection that was the result of failing over from a disconnected network, then the FAILOVER_CONNECTION boolean extra is set to true.

For a loss of connectivity, if the connectivity manager is attempting to connect (or has already connected) to another network, the NetworkInfo for the new network is also passed as an extra. This lets any receivers of the broadcast know that they should not necessarily tell the user that no data traffic will be possible. Instead, the receiver should expect another broadcast soon, indicating either that the failover attempt succeeded (and so there is still overall data connectivity), or that the failover attempt failed, meaning that all connectivity has been lost.

For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY is set to true if there are no connected networks at all.

Constant Value: "android.net.conn.CONNECTIVITY_CHANGE"

Upvotes: 0

Related Questions