Focus On: TrafficStats

To be able to have more intelligent code — code that can adapt to Internet activity on the device — Android offers the TrafficStats class. This class really is a gateway to a block of native code that reports on traffic usage for the entire device and per-application, for both received and transmitted data. This chapter will examine how you can access TrafficStats and interpret its data.

Prerequisites

Understanding this chapter requires that you have read the core chapters and understand how Android apps are set up and operate.

TrafficStats Basics

The TrafficStats class is not designed to be instantiated — you will not be invoking a constructor by calling new TrafficStats() or something like that. Rather, TrafficStats is merely a collection of static methods, mapped to native code, that provide access to point-in-time traffic values. No special permissions are needed to use any of these methods. Most of the methods were added in API Level 8 and therefore should be callable on most Android devices in use today.

Device Statistics

If you are interested in overall traffic, you will probably care most about the getTotalRxBytes() and getTotalTxBytes() on TrafficStats. These methods return received and transmitted traffic, respectively, measured in bytes.

You also have:

  1. getTotalRxPackets() and getTotalTxPackets(), if for your case measuring IP packets is a better measure than bytes
  2. getMobileRxBytes() and getMobileTxBytes(), which return the traffic going over mobile data (also included in the total)
  3. getMobileRxPackets() and getMobileTxPackets(), which are the packet counts for the mobile data connection

Per-Application Statistics

Technically, TrafficStats does not provide per-application traffic statistics. Rather, it provides per-UID traffic statistics. In most cases, the UID (user ID) of an application is unique, and therefore per-UID statistics map to per-application statistics. However, it is possible for multiple applications to share a single UID (e.g., via the android:sharedUserId manifest attribute) — in this case, TrafficStats would appear to provide traffic data for all applications sharing that UID.

There are per-UID equivalents of the first four methods listed in the previous section, replacing “Total” with “Uid”. So, to find out overall traffic for an application, you could use getUidRxBytes() and getUidTxBytes(). However, these are the only two UID-specific methods that were implemented in API Level 8. Equivalents of the others (e.g., getUidRxPackets()) were added in API Level 12. API Level 12 also added some TCP-specific methods (e.g., getUidTcpTxBytes()). Note, though, that the mobile-only method are only available at the device level; there are no UID-specific versions of those methods.

Interpreting the Results

You will get one of two types of return value from these methods.

In theory, you will get the value the method calls for (e.g., number of bytes, number of packets). The documentation does not state the time period for that value, so while it is possible that it is really “number of bytes since the device was booted”, we do not know that for certain. Hence, TrafficStats results should be used for comparison purposes, either comparing the same value over time or comparing multiple values at the same time. For example, to measure bandwidth consumption, you will need to record the TrafficStats values at one point in time, then again later — the difference between them represents the consumed bandwidth during that period of time.

In practice, while the “total” methods seem reliable, the per-UID methods may return -1. Three possible meanings are:

  1. The device is old and is not set up to measure per-UID values
  2. There has been no traffic of that type on that UID since boot, or
  3. You do not have permission to know the traffic of that type on that UID

Hence, the per-UID values are a bit “hit or miss”, which you will need to take into account.

Example: TrafficMonitor

To illustrate the use of TrafficStats methods and analysis, let us walk through the code associated with the Bandwidth/TrafficMonitor sample application. This is a simple activity that records a snapshot of the current traffic levels on startup, then again whenever you tap a button. On-screen, it will display the current value, previous value, and difference (“delta”) between them. In Logcat, it will dump the same information on a per-UID basis.

TrafficRecord

It would have been nice if TrafficStats were indeed an object that you would instantiate, that captured the traffic values at that moment in time. Alas, that is not how it was written, so we need to do that ourselves. In the TrafficMonitor project, this job is delegated to a TrafficRecord class:

package com.commonsware.android.tuning.traffic;

import android.net.TrafficStats;

class TrafficRecord {
  long tx=0;
  long rx=0;
  String tag=null;
  
  TrafficRecord() {
    tx=TrafficStats.getTotalTxBytes();
    rx=TrafficStats.getTotalRxBytes();
  }
  
  TrafficRecord(int uid, String tag) {
    tx=TrafficStats.getUidTxBytes(uid);
    rx=TrafficStats.getUidRxBytes(uid);
    this.tag=tag;
  }
}
(from Bandwidth/TrafficMonitor/app/src/main/java/com/commonsware/android/tuning/traffic/TrafficRecord.java)

There are two separate constructors, one for the total case and one for the per-UID case. The total case just logs getTotalRxBytes() and getTotalTxBytes(), while the per-UID case uses getUidRxBytes() and getUidTxBytes(). The per-UID case also stores a “tag”, which is simply a String identifying the UID for this record — as you will see, TrafficMonitor uses this for a package name.

TrafficSnapshot

An individual TrafficRecord, though, is insufficient to completely capture the traffic figures at a moment in time. We need a collection of TrafficRecord objects, one for the device (“total”) and one per running UID. The work to collect all of that is handled by a TrafficSnapshot class:

package com.commonsware.android.tuning.traffic;

import java.util.HashMap;
import android.content.Context;
import android.content.pm.ApplicationInfo;

class TrafficSnapshot {
  TrafficRecord device=null;
  HashMap<Integer, TrafficRecord> apps=
    new HashMap<Integer, TrafficRecord>();
  
  TrafficSnapshot(Context ctxt) {
    device=new TrafficRecord();
    
    HashMap<Integer, String> appNames=new HashMap<Integer, String>();
    
    for (ApplicationInfo app :
          ctxt.getPackageManager().getInstalledApplications(0)) {
      appNames.put(app.uid, app.packageName);
    }
    
    for (Integer uid : appNames.keySet()) {
      apps.put(uid, new TrafficRecord(uid, appNames.get(uid)));
    }
  }
}
(from Bandwidth/TrafficMonitor/app/src/main/java/com/commonsware/android/tuning/traffic/TrafficSnapshot.java)

The constructor uses PackageManager to iterate over all installed applications and builds up a HashMap, mapping the UID to a TrafficRecord for that UID, tagged with the application package name (e.g., com.commonsware.android.tuning.traffic). It also creates one TrafficRecord for the device as a whole.

TrafficMonitorActivity

TrafficMonitorActivity is what creates and uses TrafficSnapshot objects. This is a fairly conventional activity with a TableLayout-based UI:

<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:id="@+id/table"
  android:layout_width="match_parent"
  android:layout_height="wrap_content">

  <Button
    android:onClick="takeSnapshot"
    android:text="Take Snapshot"/>

  <TableRow>

    <TextView
      android:layout_column="1"
      android:layout_gravity="right"
      android:text="@string/received"
      android:textSize="20sp"/>

    <TextView
      android:layout_gravity="right"
      android:text="@string/sent"
      android:textSize="20sp"/>
  </TableRow>

  <TableRow>

    <TextView
      android:layout_marginRight="@dimen/margin_right"
      android:gravity="right"
      android:text="@string/latest"
      android:textSize="20sp"
      android:textStyle="bold"/>

    <TextView
      android:id="@+id/latest_rx"
      android:layout_marginRight="@dimen/margin_right"
      android:gravity="right"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/latest_tx"
      android:gravity="right"
      android:textSize="20sp"/>
  </TableRow>

  <TableRow>

    <TextView
      android:layout_marginRight="@dimen/margin_right"
      android:gravity="right"
      android:text="@string/previous"
      android:textSize="20sp"
      android:textStyle="bold"/>

    <TextView
      android:id="@+id/previous_rx"
      android:layout_marginRight="@dimen/margin_right"
      android:gravity="right"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/previous_tx"
      android:gravity="right"
      android:textSize="20sp"/>
  </TableRow>

  <TableRow>

    <TextView
      android:layout_marginRight="@dimen/margin_right"
      android:gravity="right"
      android:text="@string/delta"
      android:textSize="20sp"
      android:textStyle="bold"/>

    <TextView
      android:id="@+id/delta_rx"
      android:layout_marginRight="@dimen/margin_right"
      android:gravity="right"
      android:textSize="20sp"/>

    <TextView
      android:id="@+id/delta_tx"
      android:gravity="right"
      android:textSize="20sp"/>
  </TableRow>

</TableLayout>
(from Bandwidth/TrafficMonitor/app/src/main/res/layout/main.xml)

The activity implementation consists of three methods. There is your typical onCreate() implementation, where we initialize the UI, get our hands on the TextView widgets for output, and take the initial snapshot:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    
    latest_rx=(TextView)findViewById(R.id.latest_rx);
    latest_tx=(TextView)findViewById(R.id.latest_tx);
    previous_rx=(TextView)findViewById(R.id.previous_rx);
    previous_tx=(TextView)findViewById(R.id.previous_tx);
    delta_rx=(TextView)findViewById(R.id.delta_rx);
    delta_tx=(TextView)findViewById(R.id.delta_tx);
    
    takeSnapshot(null);
  }
(from Bandwidth/TrafficMonitor/app/src/main/java/com/commonsware/android/tuning/traffic/TrafficMonitorActivity.java)

The takeSnapshot() method creates a new TrafficSnapshot (held in a latest data member) after moving the last TrafficSnapshot to a previous data member. It then updates the TextView widgets for the latest data and, if the previous data member is not null, also for the previous snapshot and the difference between them. This alone is sufficient to update the UI, but we also want to log per-UID data to Logcat:

  public void takeSnapshot(View v) {
    previous=latest;
    latest=new TrafficSnapshot(this);
    
    latest_rx.setText(String.valueOf(latest.device.rx));
    latest_tx.setText(String.valueOf(latest.device.tx));
    
    if (previous!=null) {
      previous_rx.setText(String.valueOf(previous.device.rx));
      previous_tx.setText(String.valueOf(previous.device.tx));
      
      delta_rx.setText(String.valueOf(latest.device.rx-previous.device.rx));
      delta_tx.setText(String.valueOf(latest.device.tx-previous.device.tx));
    }
    
    ArrayList<String> log=new ArrayList<String>();
    HashSet<Integer> intersection=new HashSet<Integer>(latest.apps.keySet());
    
    if (previous!=null) {
      intersection.retainAll(previous.apps.keySet());
    }
    
    for (Integer uid : intersection) {
      TrafficRecord latest_rec=latest.apps.get(uid);
      TrafficRecord previous_rec=
            (previous==null ? null : previous.apps.get(uid));
      
      emitLog(latest_rec.tag, latest_rec, previous_rec, log);
    }
    
    Collections.sort(log);
    
    for (String row : log) {
      Log.d("TrafficMonitor", row);
    }
  }
(from Bandwidth/TrafficMonitor/app/src/main/java/com/commonsware/android/tuning/traffic/TrafficMonitorActivity.java)

One possible problem with the snapshot system is that the process list may change between snapshots. One simple way to address this is to only log to Logcat data where the application’s UID exists in both the previous and latest snapshots. Hence, takeSnapshot() uses a HashSet and retainAll() to determine which UIDs exist in both snapshots. For each of those, we call an emitLog() method to record the data to an ArrayList, which is then sorted and dumped to Logcat.

The emitLog() method builds up a line with the package name and bandwidth consumption information, assuming that there is bandwidth to report (i.e., we have a value other than -1):

  private void emitLog(CharSequence name, TrafficRecord latest_rec,
                        TrafficRecord previous_rec,
                        ArrayList<String> rows) {
    if (latest_rec.rx>-1 || latest_rec.tx>-1) {
      StringBuilder buf=new StringBuilder(name);
      
      buf.append("=");
      buf.append(String.valueOf(latest_rec.rx));
      buf.append(" received");
      
      if (previous_rec!=null) {
        buf.append(" (delta=");
        buf.append(String.valueOf(latest_rec.rx-previous_rec.rx));
        buf.append(")");
      }
      
      buf.append(", ");
      buf.append(String.valueOf(latest_rec.tx));
      buf.append(" sent");
      
      if (previous_rec!=null) {
        buf.append(" (delta=");
        buf.append(String.valueOf(latest_rec.tx-previous_rec.tx));
        buf.append(")");
      }
      
      rows.add(buf.toString());
    }
  }
(from Bandwidth/TrafficMonitor/app/src/main/java/com/commonsware/android/tuning/traffic/TrafficMonitorActivity.java)

Since the lines created by emitLog() start with the package name, and since we are sorting those before dumping them to Logcat, they appear in Logcat in sorted order by package name.

Using TrafficMonitor

Running the activity gives you the initial received and sent counts (in bytes):

The TrafficMonitor sample application, as initially launched
Figure 976: The TrafficMonitor sample application, as initially launched

Tapping Take Snapshot grabs a second snapshot and compares the two:

The TrafficMonitor sample application, after Take Snapshot was clicked
Figure 977: The TrafficMonitor sample application, after Take Snapshot was clicked

Also, Logcat will show how much was used by various apps:


08-15 14:05:10.128: DEBUG/TrafficMonitor(10283): com.amblingbooks.bookplayerpro=880 received (delta=0), 3200 sent (delta=0)
08-15 14:05:10.128: DEBUG/TrafficMonitor(10283): com.android.browser=19045241 received (delta=0), 2375847 sent (delta=0)
08-15 14:05:10.128: DEBUG/TrafficMonitor(10283): com.android.providers.downloads=27884469 received (delta=0), 9126 sent (delta=0)
08-15 14:05:10.128: DEBUG/TrafficMonitor(10283): com.android.providers.telephony=2328 received (delta=0), 4912 sent (delta=0)
08-15 14:05:10.128: DEBUG/TrafficMonitor(10283): com.android.vending=3271839 received (delta=0), 260626 sent (delta=0)
08-15 14:05:10.128: DEBUG/TrafficMonitor(10283): com.coair.mobile.android=887425 received (delta=0), 81366 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.commonsware.android.browser1=262553 received (delta=0), 7286 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.dropbox.android=6189833 received (delta=0), 4298 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.evernote=3471398 received (delta=0), 742178 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.apps.genie.geniewidget=358816 received (delta=0), 17775 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.apps.googlevoice=103255 received (delta=0), 35559 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.apps.maps=28440829 received (delta=0), 1230867 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.backup=51320 received (delta=0), 49041 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.gm=10915084 received (delta=0), 14428803 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.googlequicksearchbox=37817 received (delta=0), 12554 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.syncadapters.contacts=1955990 received (delta=0), 714893 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.voicesearch=67948 received (delta=0), 121908 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.google.android.youtube=3128 received (delta=0), 2792 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.howcast.android.app=2250407 received (delta=0), 26727 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.rememberthemilk.MobileRTM=6836605 received (delta=0), 2902904 sent (delta=0)
08-15 14:05:10.132: DEBUG/TrafficMonitor(10283): com.tripit=109499 received (delta=0), 50060 sent (delta=0)

Other Ways to Employ TrafficStats

Of course, there are more ways you could use TrafficStats than simply having an activity to report them on a button click. TrafficMonitor is merely a demonstration of using the class and providing a lightweight way to get value out of that data. Depending upon your application’s operations, though, you may wish to consider using TrafficStats in other ways, in your production code or in your test suites.

In Production

If your app is a bandwidth monitor, the need to use TrafficStats is obvious. However, even if your app does something else, you may wish to use TrafficStats to understand what is going on in terms of Internet access within your app or on the device as a whole.

For example, you might want to consider bandwidth consumption to be a metric worthy of including in the rest of the “analytics” you generate from your app. If you are using services like Flurry to monitor which activities get used and so on, you might consider also logging the amount of bandwidth your application consumes. This not only gives you much more “real world” data than you will be able to collect on your own, but it may give you ideas of how users are using your application beyond what the rest of your metrics are reporting.

Another possibility would be to include your app’s bandwidth consumption in error logs reported via libraries like ACRA. Just as device particulars can help identify certain bug report patterns, perhaps certain crashes of your app only occur when users are using a lot of bandwidth in your app, or using a lot of bandwidth elsewhere and perhaps choking your own app’s Internet access.

The chapter on bandwidth mitigation strategies will also cover a number of uses of TrafficStats for real-time adjustment of your application logic.

During Testing

You might consider adding TrafficStats-based bandwidth logging for your application in your test suites. While individual tests may or may not give you useful data, you may be able to draw trendlines over time to see if you are consuming more or less bandwidth than you used to. Take care to factor in that you may have changed the tests, in addition to changing the code that is being tested.

From a JUnit-based unit test suite, measuring bandwidth consumption is not especially hard. You can bake it into the setUp() and tearDown() methods of your test cases, either via inheritance or composition, and log the output to a file or Logcat.

From an external test engine, like monkeyrunner or NativeDriver, recording bandwidth usage is more tricky, because your test code is not running on the device or emulator. You may have to include a BroadcastReceiver in your production code that will log bandwidth usage and trigger that code via the am broadcast shell command.