The following is the first few sections of a chapter from The Busy Coder's Guide to Android Development, plus headings for the remaining major sections, to give you an idea about the content of the chapter.


In-App Diagnostics

Android has many tools to help you make sense of what is going on in your app, from complex tools like Traceview to simpler things like Logcat. Plus, if you are using an IDE, you have access to a debugger, which can let you step through code, inspect data members and other variables, and so on.

However, they all have one element in common: they are general-purpose tools. They know nothing specifically about your app, just Android apps in general. As a result, there may be information that you can gather that would be of immense benefit for debugging and diagnostic purposes, but that the general-purpose tools cannot collect for you.

More importantly, you need some way to see any diagnostic data that you collect. Logging stuff to Logcat can sometimes work, but then you have to worry about accidentally shipping that logging code in production, which would be less than ideal. And there are many cases where Logcat itself will not be a great visualization of the information.

What would be better is if we could add our own diagnostic tools to our app, for use while debugging, while excluding them from our release builds. And it would be great if we could add in these tools without changing much, if anything, of our production code to reference them. This chapter will explore how to implement such tools.

Prerequisites

In addition to the core chapters, it would be a good idea if you had read:

Also, one of the techniques bears some resemblance to the tapjacking attack, though fortunately without the privacy and security ramifications.

One of the sample apps is based on a RecyclerView sample app, and so you may wish to skim the RecyclerView material to ensure that you understand enough about what is going on with the sample.

One of the sample apps uses an embedded Web server, based on concepts and a module covered in another chapter.

The Diagnostic Activity

Having a “back door” to get at diagnostic information about a program is a time-honored technique. Alas, far too many of those back doors wind up in production code, and too many of those wind up resulting in privacy or security flaws. Yet, the approach is still used to this day.

From a GUI standpoint, these back doors usually required some sort of special key sequence to initiate (e.g., press Ctrl-Shift-Z three times in less than a second). The objective was to make them easy enough to get to but not something that would routinely get in the way. And, for those back doors that wound up shipping, eventually word would get out about the magic key sequence, leading to all sorts of trouble.

In Android, we can dispense with the magic key sequence (which is good, since we often are not using keyboards). An app can have as many launcher icons as it needs, so we just need a launcher icon to get into some custom diagnostic activity that we want. However, now we really do not want to ship this code in production, as the diagnostic activity is no longer hidden, but rather is in plain view in the user’s home screen launcher.

Fortunately, the advent of source sets with the Android Gradle Plugin, plus a reasonably robust manifest merger process, makes setting up this sort of tool fairly easy, yet keeps it out of the production code. Most of the work will be in actually writing the activity to report on whatever it is that you wanted reported on.

The Diagnostics/Activity. sample project will illustrate this process.

This app is a clone of a previous sample that retrieves Stack Overflow questions in the android tag via Square’s Retrofit library. It also uses Square’s Picasso library to load in the avatars of the people asking the questions. Picasso has an API for getting at statistics about the images that were downloaded: how many, how big, how many were already cached, and so on. The revised sample shown in this section will create a diagnostic activity that reports this information, as an illustration of having such an activity supply statistics that may be useful in tuning, debugging, etc.

The Sourceset

This project has two source sets, main and debug. main is where the production code lies; debug is where the diagnostic activity resides. The debug source set is tied to the debug build type, so only when doing a debug build will our debug code be included in the app. Since your production signing key is (hopefully) only being used by your release build type, this helps ensure that the diagnostic code does not ship with your production app.

The Manifest

Both source sets have manifests. For debug builds, the debug source set’s manifest will be merged with the main source set’s manifest to create the combined result.

The objective is to have the debug source set’s manifest have the minimum elements and attributes required to have it successfully add what it needs to the app. The more stuff in a source set’s manifest, the more likely it is that the stuff will conflict with similar stuff from main or other manifests and cause build problems.

Here, the debug manifest simply declares a new <activity>:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

  <application>
    <activity
      android:name="com.commonsware.android.debug.util.PicassoDiagnosticActivity"
      android:label="@string/picasso_diagnostics"
      android:taskAffinity="com.commonsware.android.debug.activity.PicassoDiagnosticActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>

Note that the class name for the PicassoDiagnosticActivity is fully-qualified (com.commonsware.android.debug.util.PicassoDiagnosticActivity). For the purposes of this particular diagnostic, the activity does not have to be in the same package as the rest of the app. In fact, this activity could be in a library that could be referenced by many apps, if desired.

Also note the taskAffinity for the <activity> is set to its fully-qualified class name. This helps ensure that this activity will reside in a different task than does our main UI, so that the diagnostics activity does not artificially alter BACK button processing and the like from the regular task.

Since the main source set will not contain this particular <activity> element, there are no collisions, and the manifest merger will turn out clean.

The Activity

The activity itself is rather boring.

It loads in a layout resource containing a TableLayout that will contain our Picasso diagnostic report:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent">

  <TableLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:shrinkColumns="1"
    android:stretchColumns="1">

    <TableRow>

      <TextView
        android:id="@+id/last_updated"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Last Updated"/>

      <TextView
        android:id="@+id/last_updated_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/avg_download_size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Average Download Size"/>

      <TextView
        android:id="@+id/avg_download_size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/avg_orig_size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Average Original Bitmap Size"/>

      <TextView
        android:id="@+id/avg_orig_size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/avg_xform_size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Average Transformed Bitmap Size"/>

      <TextView
        android:id="@+id/avg_xform_size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/cache_hits"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Cache Hits"/>

      <TextView
        android:id="@+id/cache_hits_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/cache_misses"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Cache Misses"/>

      <TextView
        android:id="@+id/cache_misses_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/download_count"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Download Count"/>

      <TextView
        android:id="@+id/download_count_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/max_size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Max Size"/>

      <TextView
        android:id="@+id/max_size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/orig_bitmap_count"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Original Bitmap Count"/>

      <TextView
        android:id="@+id/orig_bitmap_count_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Size"/>

      <TextView
        android:id="@+id/size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/total_dl_size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Total Download Size"/>

      <TextView
        android:id="@+id/total_dl_size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/total_orig_size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Total Original Size"/>

      <TextView
        android:id="@+id/total_orig_size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/total_xform_size"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Total Transformed Size"/>

      <TextView
        android:id="@+id/total_xform_size_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>

    <TableRow>

      <TextView
        android:id="@+id/xform_count"
        style="@style/TableText.Title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Transformed Count"/>

      <TextView
        android:id="@+id/xform_count_value"
        style="@style/TableText.Value"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    </TableRow>
  </TableLayout>
</ScrollView>

That layout, in turn, references some custom styles, to avoid having to repeat the configuration of each of the TextView widgets quite so much:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="TableText">
    <item name="android:textSize">12sp</item>
    <item name="android:layout_margin">8dp</item>
  </style>

  <style name="TableText.Title">
    <item name="android:textStyle">bold</item>
  </style>

  <style name="TableText.Value">
    <item name="android:typeface">monospace</item>
  </style>
</resources>

The activity loads the layout, gets a StatsSnapshot from Picasso containing a snapshot of the results of using Picasso, and pours the data into the various TextView widgets:

package com.commonsware.android.debug.util;

import android.app.Activity;
import android.os.Bundle;
import android.text.format.DateUtils;
import android.widget.TextView;
import com.commonsware.android.debug.activity.R;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.StatsSnapshot;

public class PicassoDiagnosticActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    StatsSnapshot ss=Picasso.with(this).getSnapshot();

    TextView tv=(TextView)findViewById(R.id.last_updated_value);

    tv.setText(DateUtils.formatDateTime(this, ss.timeStamp,
                                        DateUtils.FORMAT_SHOW_TIME));

    tv=(TextView)findViewById(R.id.avg_download_size_value);
    tv.setText(Long.toString(ss.averageDownloadSize));

    tv=(TextView)findViewById(R.id.avg_orig_size_value);
    tv.setText(Long.toString(ss.averageOriginalBitmapSize));

    tv=(TextView)findViewById(R.id.avg_xform_size_value);
    tv.setText(Long.toString(ss.averageTransformedBitmapSize));

    tv=(TextView)findViewById(R.id.cache_hits_value);
    tv.setText(Long.toString(ss.cacheHits));

    tv=(TextView)findViewById(R.id.cache_misses_value);
    tv.setText(Long.toString(ss.cacheMisses));

    tv=(TextView)findViewById(R.id.download_count_value);
    tv.setText(Long.toString(ss.downloadCount));

    tv=(TextView)findViewById(R.id.max_size_value);
    tv.setText(Long.toString(ss.maxSize));

    tv=(TextView)findViewById(R.id.orig_bitmap_count_value);
    tv.setText(Long.toString(ss.originalBitmapCount));

    tv=(TextView)findViewById(R.id.size_value);
    tv.setText(Long.toString(ss.size));

    tv=(TextView)findViewById(R.id.total_dl_size_value);
    tv.setText(Long.toString(ss.totalDownloadSize));

    tv=(TextView)findViewById(R.id.total_orig_size_value);
    tv.setText(Long.toString(ss.totalOriginalBitmapSize));

    tv=(TextView)findViewById(R.id.total_xform_size_value);
    tv.setText(Long.toString(ss.totalTransformedBitmapSize));

    tv=(TextView)findViewById(R.id.xform_count_value);
    tv.setText(Long.toString(ss.transformedBitmapCount));
  }
}

The Results

If you install the app on a device or emulator from a debug build, you will get two launcher icons. The one labeled “Picasso Diagnostics” will be the PicassoDiagnosticsActivity. If you bring up that activity after having run the main activity, you will see some information about the images that Picasso loaded:

Picasso Diagnostic Activity
Figure 1066: Picasso Diagnostic Activity

A release build, on the other hand, does not include the extra activity, its resources, or its manifest entry, since those are all in the debug source set.

Also, nothing from this affected our main source set contents. We did not have to add things to the manifest, or adjust our Java code, or anything of the sort.

The Limitations

While this sample is fairly trivial, these sorts of diagnostic activities can be as elaborate as is needed. In some cases, as with this sample, the results are reusable — so long as the app has Picasso, this code can add in the diagnostic activity.

However, this is only good for post-mortem sorts of diagnostics, where you do something in the “real” app, then head over to the diagnostic activity to see what it has to report. In many cases, this is perfectly reasonable. In other cases, the act of switching to the diagnostic activity might affect the diagnostics, if those diagnostics are dependent upon things like activity lifecycle methods. You also cannot learn anything in real time, seeing both the app and the diagnostics simultaneously (or nearly so).

However, there are other options that can improve in these areas, for situations that need such improvement.

The Diagnostic Web App

The preview of this section was traded for a bag of magic beans.

The Diagnostic Overlay

The preview of this section is in an invisible, microscopic font.