8.3: Transferring Data Efficiently

Contents:

Transferring data is an essential part of most Android applications, but it can negatively affect battery life and increase data usage costs. Using the wireless radio to transfer data is potentially one of your app's most significant sources of battery drain.

Users care about battery drain because they would rather use their mobile device without it connected to the charger. And users care about data usage, because every bit of data transferred can cost them money.

In this chapter, you learn how your app's networking activity affects the device's radio hardware so you can minimize the battery drain associated with network activity. You also learn how to wait for the proper conditions to accomplish resource-intensive tasks.

Wireless radio state

A fully active wireless radio consumes significant power. To conserve power when not in use, the radio transitions between different energy states. However, there is a trade-off between conserving power and the time it takes to power up when needed.

For a typical 3G network the radio has these three energy states:

  1. Full power: Used when a connection is active. Allows the device to transfer data at its highest possible rate.
  2. Low power: An intermediate state that uses about 50% less battery.
  3. Standby: The minimal energy state, during which no network connection is active or required.

While the low and standby states use much less battery, they also introduce latency to network requests. Returning to full power from the low state takes around 1.5 seconds, while moving from standby to full can take over 2 seconds.

Android uses a state machine to determine how to transition between states. To minimize latency, the state machine waits a short time before it transitions to the lower energy states. Radio transitioning for the 3G wireless state machine

The radio state machine on each device, particularly the associated transition delay ("tail time") and startup latency, vary based on the wireless radio technology employed (2G, 3G, LTE, etc.) and is defined and configured by the carrier network over which the device is operating.

This chapter describes a representative state machine for a typical 3G wireless radio, based on data provided by AT&T. However, the general principles and resulting best practices are applicable for all wireless radio implementations.

As with any best practices, there are trade-offs that you need to consider for your own app development.

Bundling network transfers

Every time you create a new network connection, the radio transitions to the full power state. In the case of the 3G radio state machine described above, it remains at full power for the duration of your transfer, followed by 5 seconds of tail time, followed by 12 seconds at the low energy state before turning off. So, for a typical 3G device, every data transfer session causes the radio to draw power for almost 20 seconds.

What this means in practice:

  • An app that transfers unbundled data for 1 second every 18 seconds keeps the wireless radio always active.
  • By comparison, the same app bundling transfers for 3 seconds of every minute keeps the radio in the high power state for only 8 seconds, and in the low power state for an additional 12 seconds.

The second example allows the radio to be idle for 40 seconds out of every minute, resulting in a massive reduction in battery consumption. Relative wireless radio power use for bundled versus unbundled transfers

It's important to bundle and queue up your data transfers. You can bundle transfers that are due to occur within a time window and make them all happen simultaneously, ensuring that the radio draws power for as little time as possible.

Prefetching

To prefetch data means that your app takes a guess at what content or data the user will want next, and fetches it ahead of time. For example, when the user looks at the first part of an article, a good guess is to prefetch the next part. Or, if a user is watching a video, fetching the next minutes of the video is also a good guess.

Prefetching data is an effective way to reduce the number of independent data transfer sessions. Prefetching allows you to download all the data you are likely to need for a given time period in a single burst, over a single connection, at full capacity. This reduces the number of radio activations required to download the data. As a result, you not only conserve battery life, but also improve latency for the user, lower the required bandwidth, and reduce download times.

Prefetching has trade-offs. If you download too much or the wrong data, you might increase battery drain. And if you download at the wrong time, users may end up waiting. Optimizing prefetching data is an advanced topic not covered in this course, but the following guidelines cover common situations.

How aggressively you prefetch depends on the size of the data being downloaded and the likelihood of it being used. As a rough guide, based on the state machine described above, for data that has a 50% chance of being used within the current user session, you can typically prefetch for around 6 seconds (approximately 1-2 Mb) before the potential cost of downloading unused data matches the potential savings of not downloading that data to begin with.

Generally speaking, it's good practice to prefetch data such that you only need to initiate another download every 2 to 5 minutes, and on the order of 1 to 5 megabytes.

Following this principle, large downloads—such as video files—should be downloaded in chunks at regular intervals (every 2 to 5 minutes), effectively prefetching only the video data likely to be viewed in the next few minutes.

Prefetching example

Many news apps attempt to reduce bandwidth by downloading headlines only after a category has been selected, full articles only when the user wants to read them, and thumbnails just as they scroll into view.

Using this approach, the radio is forced to remain active for the majority of a news-reading session as users scroll headlines, change categories, and read articles. Not only that, but the constant switching between energy states results in significant latency when switching categories or reading articles.

Here's a better approach:

  1. Prefetch a reasonable amount of data at startup, beginning with the first set of news headlines and thumbnails. This ensures a quick startup time.
  2. Continue with the remaining headlines, the remaining thumbnails, and the article text for each article from the first set of headlines.

Monitor connectivity state

Devices can network using different types of hardware:

  • Wireless radios use varying amounts of battery depending on technology, and higher bandwidth consumes more energy. Higher bandwidth means you can prefetch more aggressively, downloading more data during the same amount of time. However, perhaps less intuitively, because the tail-time battery cost is relatively higher, it is also more efficient to keep the radio active for longer periods during each transfer session to reduce the frequency of updates.
  • WiFi radio uses significantly less battery than wireless and offers greater bandwidth.

Perform data transfers when connected over Wi-Fi whenever possible.

You can use the ConnectivityManager to determine the active wireless radio and modify your prefetching routines depending on network type:

ConnectivityManager cm =
   (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
TelephonyManager tm =
    (TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);

NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
int PrefetchCacheSize = DEFAULT_PREFETCH_CACHE;

switch (activeNetwork.getType()) {
    case (ConnectivityManager.TYPE_WIFI):
        PrefetchCacheSize = MAX_PREFETCH_CACHE; break;
    case (ConnectivityManager.TYPE_MOBILE): {
        switch (tm.getNetworkType()) {
           case (TelephonyManager.NETWORK_TYPE_LTE |
                  TelephonyManager.NETWORK_TYPE_HSPAP):
                PrefetchCacheSize *= 4;
                break;
            case (TelephonyManager.NETWORK_TYPE_EDGE |
                  TelephonyManager.NETWORK_TYPE_GPRS):
               PrefetchCacheSize /= 2;
               break;
            default: break;
        }
        break;
      }
  default: break;
  }

The system sends out broadcast intents when the connectivity state changes, so you can listen for these changes using a BroadcastReceiver.

Monitor battery state

To minimize battery drain, monitor the state of your battery and wait for specific conditions before initiating a battery-intensive operation.

The BatteryManager broadcasts all battery and charging details in a broadcast Intent that includes the charging status.

To check the current battery status, examine the broadcast intent:

IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = context.registerReceiver(null, ifilter);
// Are we charging / charged?
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                     status == BatteryManager.BATTERY_STATUS_FULL;

// How are we charging?
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;

If you want to react to changes in the battery charging state, use a BroadcastReceiver registered for the battery status actions:

<receiver android:name=".PowerConnectionReceiver">
  <intent-filter>
    <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>
    <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/>
  </intent-filter>
</receiver>

Broadcast intents are also delivered when the battery level changes in a significant way:

"android.intent.action.BATTERY_LOW"
"android.intent.action.BATTERY_OKAY"

JobScheduler

Constantly monitoring the connectivity and battery status of the device can be a challenge, and it requires using components such as broadcast receivers, which can consume system resources even when your app isn't running. Because transferring data efficiently is such a common task, the Android SDK provides a class that makes this much easier: JobScheduler.

Introduced in API level 21, JobScheduler allows you to schedule a task around specific conditions (rather than a specific time as with AlarmManager).

JobScheduler has three components:

  1. JobInfo uses the builder pattern to set the conditions for the task.
  2. JobService is a wrapper around the Service class where the task is actually completed.
  3. JobScheduler schedules and cancels tasks.
    Note: JobScheduler is only available from API 21+. There is no backwards compatible version for prior API releases. If your app targets devices with earlier API levels, you might find the FirebaseJobDispatcher a useful alternative.

1. JobInfo

Set the job conditions by constructing a JobInfo object using the JobInfo.Builder class. The JobInfo.Builder class is instantiated from a constructor that takes two arguments: a job ID (which can be used to cancel the job), and the ComponentName of the JobService that contains the task. Your JobInfo.Builder must set at least one, non-default condition for the job. For example:

JobScheduler scheduler = (JobScheduler) getSystemService(JOB_SCHEDULER_SERVICE);
ComponentName serviceName = new ComponentName(getPackageName(),
NotificationJobService.class.getName());
JobInfo.Builder builder = new JobInfo.Builder(JOB_ID, serviceName);
builder.setRequiredNetworkType(NETWORK_TYPE_UNMETERED);
JobInfo jobInfo = builder.build();
Note: See the related practical for a complete example.

The JobInfo.Builder class has many set() methods that allow you to determine the conditions of the task. Below is a list of available constraints with their respective set() methods and class constants:

  • Backoff/Retry policy: Determines when how the task should be rescheduled if it fails. Set this condition using the setBackoffCriteria() method, which takes two arguments: the initial time to wait after the task fails, and the backoff strategy. The backoff strategy argument can be one of two constants: BACKOFF_POLICY_LINEAR or BACKOFF_POLICY_EXPONENTIAL. This defaults to {30 seconds, Exponential}.
  • Minimum Latency: The minimum amount of time to wait before completing the task. Set this condition using the setMinimumLatency() method, which takes a single argument: the amount of time to wait in milliseconds.
  • Override Deadline: The maximum time to wait before running the task, even if other conditions aren't met. Set this condition using the setOverrideDeadline() method, which is the maximum time to wait in milliseconds.
  • Periodic: Repeats the task after a certain amount of time. Set this condition using the setPeriodic() method, passing in the repetition interval. This condition is mutually exclusive with the minimum latency and override deadline conditions: setting setPeriodic() with one of them results in an error.
  • Persisted: Sets whether the job is persisted across system reboots. For this condition to work, your app must hold the RECEIVE_BOOT_COMPLETED permission. Set this condition using the setPersisted() method, passing in a boolean that indicates whether or not to persist the task.
  • Required Network Type: The kind of network type your job needs. If the network isn't necessary, you don't need to call this function, because the default is NETWORK_TYPE_NONE. Set this condition using the setRequiredNetworkType() method, passing in one of the following constants: NETWORK_TYPE_NONE, NETWORK_TYPE_ANY, NETWORK_TYPE_NOT_ROAMING, NETWORK_TYPE_UNMETERED.
  • Required Charging State: Whether or not the device needs to be plugged in to run this job. Set this condition using the setRequiresCharging() method, passing in a boolean. The default is false.
  • Requires Device Idle: Whether or not the device needs to be in idle mode to run this job. "Idle mode" means that the device isn't in use and hasn't been for some time, as loosely defined by the system. When the device is in idle mode, it's a good time to perform resource-heavy jobs. Set this condition using the setRequiresDeviceIdle() method, passing in a boolean. The default is false.

2. JobService

Once the conditions for a task are met, the framework launches a subclass of JobService, which is where you implement the task itself. The JobService runs on the UI thread, so you need to offload blocking operations to a worker thread.

Declare the JobService subclass in the Android Manifest, and include the BIND_JOB_SERVICE permission:

<service android:name="MyJobService"
              android:permission="android.permission.BIND_JOB_SERVICE" />

In your subclass of JobService, override two methods, onStartJob() and onStopJob().

onStartJob()

The system calls onStartJob() and automatically passes in a JobParameters object, which the system creates with information about your job. If your task contains long-running operations, offload the work onto a separate thread. The onStartJob() method returns a boolean: true if your task has been offloaded to a separate thread (meaning it might not be completed yet) and false if there is no more work to be done.

Use the jobFinished() method from any thread to tell the system that your task is complete. This method takes two parameters: the JobParameters object that contains information about the task, and a boolean that indicates whether the task needs to be rescheduled, according to the defined backoff policy.

onStopJob()

The system calls onStopJob() if it determines that you must stop execution of your job even before you've call jobFinished(). This happens if the requirements that you specified when you scheduled the job are no longer met.

Examples:

  • If you request WiFi with setRequiredNetworkType() but the user turns off WiFi while while your job is executing, the system calls onStopJob().
  • If you specify setRequiresDeviceIdle() but the user starts interacting with the device while your job is executing, the system calls onStopJob().

You're responsible for how your app behaves when it receives onStopJob(), so don't ignore it. This method returns a boolean, indicating whether you'd like to reschedule the job based on the defined backoff policy, or drop the task.

3. JobScheduler

The final part of scheduling a task is to use the JobScheduler class to schedule the job. To obtain an instance of this class, call getSystemService(JOB_SCHEDULER_SERVICE). Then schedule a job using the schedule() method, passing in the JobInfo object you created with the JobInfo.Builder. For example:

mScheduler.schedule(myJobInfo);

The framework is intelligent about when you receive callbacks, and it attempts to batch and defer them as much as possible. Typically, if you don't specify a deadline on your job, the system can run it at any time, depending on the current state of the JobScheduler object's internal queue; however, it might be deferred as long as until the next time the device is connected to a power source.

To cancel a job, call cancel(), passing in the job ID from the JobInfo.Builder object, or call cancelAll(). For example:

mScheduler.cancelAll();

The related exercises and practical documentation is in Android Developer Fundamentals: Practicals.

Learn more

results matching ""

    No results matching ""