Services

We avail of two Android classes, BroadcastReceiver and IntentService, to asynchronously refresh local data obtained by querying a cloud-based service. The BroadcastReceiver receives intents sent by a Context method, sendBroadcast. The IntentService handles asynchronous requests. Our approach is to initialize an alarm in the BroadcastReceiver to exercise the IntentService at interverals configurable from the settings. When the alarm fires, the local data is asynchronously refreshed.

BroadcastReceiver

We create a subclass called BootReceiver. When Context.sendBroadcast is invoked with an appropriate intent as a parameter BroadcastReceiver.onReceive is then called. This method possesses the following functionality:

  • Queries the settings to determine the frequency at which data is refreshed.
  • Prepares an intent destined for our RefreshService class, a sub-class of IntentService. RefreshService is define shortly.
  • Configures an alarm to send the intent to RefreshService at the specified frequency.

You will see later in the lab that we choose to invoke Context.sendBroadcast in MyRentApp.onCreate and also when the refresh frequency is changed in the settings.

We shall also demonstrate how, as an alternative, to set the alarm when the Android device reboots. See the Stack Overflow article Trying to start a service on boot on Android where some of the typical problems encountered are discussed.

Create this file, BootReceiver.java and locate it in a new folder named receivers in myrent.


package org.wit.myrent.receivers;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;

import org.json.JSONException;
import org.wit.myrent.R;
import org.wit.myrent.services.RefreshService;

import java.io.IOException;

/**
 * Method 1: If permission set and BootReceiver registered in manifest file then
 * BootReceiver.onReceive method will be invoked by system when device started.
 * Method 2: Create bespoke intent filter action in BootReceiver receiver in manifest and 
 * broadcast an intent from MyApp with this action as parameter. This is approach used
 * in our code for easier debugging into BootReceiver.
 * In this onReceive method we set the interval at which the alarm should trigger.
 * This will be either a default value or a value input by user in preference settings.
 * Note that the system units are milliseconds. Even AlarmManager.INTERVAL_FIFTEEN_MINUTES
 * resolves to milliseconds. The user input frequency units is minutes.
 * Debugging has proved impossible using BOOT_COMPLETED action (see manifest).
 * The approach adopted is to sent an intent from the app and again from the setting if
 * the user changes the refresh frequency.
 */
public class BootReceiver extends BroadcastReceiver
{
  private final int NUMBER_MILLIS_PER_MINUTE = 60000;
  private final int ONE_MINUTE = 60000;
  private static final long DEFAULT_INTERVAL = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
  public static int REQUESTCODE = -1;
  private String tag = "org.wit.myrent";

  @Override
  public void onReceive(Context context, Intent intent)
  {
    Log.d(tag, "In BootReceiver.onReceive");

    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    // The settings key
    String key = context.getResources().getString(R.string.refresh_interval_preference_key);
    Log.d(tag, "Settings refresh interval key " + key);
    long interval = DEFAULT_INTERVAL; // 15 minutes but units are millis so this is Constant Value: 900000 (0x00000000000dbba0)
    Log.d(tag,"DEFAULT_INTERVAL " + (interval/NUMBER_MILLIS_PER_MINUTE) + " minute(s)");
    // Use default interval if fail to retrieve valid settings input interval
    String value = prefs.getString(key, Long.toString(DEFAULT_INTERVAL/NUMBER_MILLIS_PER_MINUTE));
    Log.d(tag, "Settings refresh interval value (user-input) " + value + " minute(s)");
    if (NumUtil.isPositiveNumber(value))
    {
      interval = Long.parseLong(value) * NUMBER_MILLIS_PER_MINUTE; // minutes to millis
      Log.d(tag,"Using refresh interval obtained from settings " + interval);
    }
    // Set an arbitrary minimum interval value of a minute to avoid overloading service.
    interval = interval < ONE_MINUTE ? ONE_MINUTE : interval;

    // Prepare an PendingIntent with a view to triggering RefreshService
    PendingIntent operation = PendingIntent.getService(
        context,
        REQUESTCODE,
        new Intent(context, RefreshService.class),
        PendingIntent.FLAG_UPDATE_CURRENT
    );

    AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
    alarmManager.cancel(operation);//cancel any existing alarms with matching intent
    alarmManager.setInexactRepeating(AlarmManager.RTC, System.currentTimeMillis(), interval, operation);

    Log.d(tag, "BootReceiver alarm repeats every: " + (interval/NUMBER_MILLIS_PER_MINUTE) + " minute(s).");
  }
}

/**
 * Class containing single method to validate a string resolves to an integer
 */
class NumUtil
{

  /**
   * Verifies that a string parses to a positive number
   * @param str The string to be verified
   * @return If string comprises digits 0 to 9 only, returns true, else false.
   */
  public static boolean isPositiveNumber(String str)
  {
    // check for empty string
    if (str.compareTo("") == 0)
      return false;

    // if any non-digit char found return false
    for (char c : str.toCharArray())
    {
      if (!Character.isDigit(c))
        return false;
    }
    return true;
  }

}

IntentService

Create a folder services in myrent and subclass IntentService as follows.

package org.wit.myrent.services;

import android.app.IntentService;
import android.content.Intent;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

import org.wit.myrent.activities.ResidenceListFragment;
import org.wit.myrent.app.MyRentApp;
import org.wit.myrent.models.Residence;

import java.io.IOException;
import java.util.List;

import retrofit.Call;
import retrofit.Response;

public class RefreshService extends IntentService
{
  private String tag = "MyRent";
  MyRentApp app;
  public RefreshService()
  {
    super("RefreshService");
    app = MyRentApp.getApp();
  }

  @Override
  protected void onHandleIntent(Intent intent)
  {
    Intent localIntent = new Intent(ResidenceListFragment.BROADCAST_ACTION);
    Call<List<Residence>> call = (Call<List<Residence>>) app.residenceService.getResidences();
    try
    {
      Response<List<Residence>> response = call.execute();
      app.portfolio.refreshResidences(response.body());
      LocalBroadcastManager.getInstance(this).sendBroadcast(localIntent);
    }
    catch (IOException e)
    {

    }
  }

  @Override
  public void onDestroy()
  {
    super.onDestroy();
    Log.i(tag, "RefreshService instance destroyed");
  }

}

The alarm we configured in an earlier step results in an intent being sent to RefreshService and handled in onHandleIntent. When this method exits the RefreshService instance is destroyed.

In the try-catch block the residence list is refreshed, a local intent is created and broadcast. In a later step we shall configure ResidenceListFragment to accept this intent and update the displayed list of residences.

ResidenceListFragment

Here we shall introduce code to

  • accept the intent broadcast by RefreshService,
  • update the displayed list of residences.

The following import statements are required:

import android.content.IntentFilter;
import android.support.v4.content.LocalBroadcastManager;
import android.content.BroadcastReceiver;

Add a string to uniquely identify ResidenceListFragment as the target of the RefreshService sendBroadcast:

  public static final String BROADCAST_ACTION = "org.wit.myrent.activities.ResidenceListFragment";

Declare an IntentFilter field and initialize it in a private convenience method registerBroadcastReceiver which we invoke in onCreate:


  private IntentFilter intentFilter;
  private void registerBroadcastReceiver()
  {
    intentFilter = new IntentFilter(BROADCAST_ACTION);
    ResponseReceiver responseReceiver = new ResponseReceiver();
    // Registers the ResponseReceiver and its intent filters
    LocalBroadcastManager.getInstance(getActivity()).registerReceiver(responseReceiver, intentFilter);
  }
  @Override
  public void onCreate(Bundle savedInstanceState) {
  ...

  registerBroadcastReceiver();

Declare and initialize a residence list field in the ResidenceAdapter class:


  class ResidenceAdapter extends ArrayAdapter<Residence>
  {
    ...
    List<Residence> residences;

    public ResidenceAdapter(Context context, ArrayList<Residence> residences) {
      ...
      this.residences = residences;
    }

    ...
  }

Create an inner class ResponseReceiver, a subclass of BroadcastReceiver whose purpose here is to receive updates triggered within RefreshService. Each RefreshService broadcast results in invoking ResponseReceiver.onReceive, the sole purpose of which is to obtain the lastest list of residences and notify the adapter that the data set has changed. This results in refreshing the rendered list.

public class ResidenceListFragment ... {
  ...
  ...
  //Broadcast receiver for receiving status updates from the IntentService
  private class ResponseReceiver extends BroadcastReceiver
  {
    //private void ResponseReceiver() {}
    // Called when the BroadcastReceiver gets an Intent it's registered to receive
    @Override
    public void onReceive(Context context, Intent intent)
    {
      //refreshDonationList();
      adapter.residences = app.portfolio.residences;
      adapter.notifyDataSetChanged();
    }
  }
}

Settings

We shall make the some changes to the settings.xml and strings.xml files:

  • Introduce default values
    • username: ICTSkills
    • password: secret
    • nmr_residences: 10
    • refesh_interval: 15
  • Provide a string value for the refresh interval key to facilitate referencing it from anywhere within the app. This will avoid referencing a hard-wired string.

Here is the modified settings.xml:

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
  <EditTextPreference
      android:key="username"
      android:defaultValue="ICTSkills"
      android:summary="@string/username_summary"
      android:title="@string/username"/>

  <EditTextPreference
      android:key="password"
      android:defaultValue="secret"
      android:summary="@string/password_summary"
      android:title="@string/password"
      android:inputType="textPassword"/>

  <EditTextPreference
      android:key="nmr_residences"
      android:defaultValue="10"
      android:summary="@string/nmr_residences_summary"
      android:title="@string/nmr_residences"
      android:inputType="text"/>

  <EditTextPreference
      android:key="@string/refresh_interval_preference_key"
      android:defaultValue="15"
      android:summary="@string/set_refresh_interval_summary"
      android:title="@string/set_refresh_interval"
      android:inputType="text"/>

</PreferenceScreen>

Add this element to the end of strings.xml:

  <string name="refresh_interval_preference_key">refresh_interval</string>

We may now access this string from within a fragment, for example, as follows:

String refresh_interval = getActivity().getResources().getString(R.string.refresh_interval_preference_key)

Manifest

Since we shall be accessing a cloud service it is necessary to include a permission as follows:

  <uses-permission android:name="android.permission.INTERNET" />

We are using two services, BootReceiver and RefreshService.

    <receiver android:name=".receivers.BootReceiver"
              android:exported="false">
      <intent-filter >
        <action android:name="org.wit.myrent.receivers.SEND_BROADCAST"/>
      </intent-filter>
    </receiver>
    <service android:name=".services.RefreshService"
             android:exported="false"/>

In the cased of BootReceiver, the Intent action is a bespoke one:

org.wit.myrent.receivers.SEND_BROADCAST

We have chosen this string to be unique within the system - the choice of action name components such as SEND_BROADCAST is arbitrary.

If you wish that the automatic refresh process is initiated on booting the device (emulator, phone or tablet) then you should add or replace the above intent filter with the following:


<intent-filter >
  <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>

Whether you simply add or add and replace is a programmatic decision.

  • Should you decide to replace, then there are implications elsewhere in the code where you may have used org.wit.myrent.receivers.SEND_BROADCAST.

Start services

In MyRentApp we broadcast an intent directed to the BroadcastReceiver.

We also rebroadcast the intent if a user changes the refresh frequency in the settings.

Add this line of code to the end of MyRentApp.onCreate:

    sendBroadcast(new Intent("org.wit.myrent.receivers.SEND_BROADCAST"));

In SettingsFragment.onSharedPreferenceChanged add this code:

    // If the refresh frequency is changed send a broadcast so that alarm appropriately reset.
    String refreshIntervalKey = getActivity().getResources().getString(R.string.refresh_interval_preference_key);
    if(key.equals(refreshIntervalKey)) {
      getActivity().sendBroadcast(new Intent("org.wit.myrent.receivers.SEND_BROADCAST"));
    }

These code changes necessitate an import statement:

import android.content.Intent;

Test as follows:

  • Set the refresh frequency at the minimum 1 minute on two devices, A & B.
  • On device A create a series of residences on the cloud server.
  • Observe the residence list on device B: it should sync after a minute or so.
  • Change the number of residences on the cloud using device A.
  • Change the refresh frequency in the settings in device B to 2 minutes. At the end of this period its residence list should sync to the cloud.

The application at the end of this lab is available for reference here: myrent-14