Sunday, March 24, 2013

ActionBarSherlock: Custom List Navigation

In this tutorial I'm going to cover activity navigation with ActionBarSherlock and custom navigation view, similar to Google Maps or Gmail - see full source links at the bottom of the post.



As you look at the source code, you will notice that most of the code is in this AbstractActivity class that extends SherlockActivity and provides consistent navigation across the three activities that extend it.

onCreate() method sets up the activity so that it displays an "up" icon, hides the title and uses the activity logos defined in AndroidManifest.xml for each activity - this way, the activity logo can be touched to go back to the previous one.
// Up Icon + Logo + Hide title...
getSupportActionBar().setDisplayHomeAsUpEnabled (true);
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setDisplayUseLogoEnabled  (true);
The next thing it does it configures the ActionBar list navigation with a custom NavigationListAdapter and NavigationListListener, reading the resources (logo, title, subtitle) from typed-arrays as references. Just as well, that can be changed to say implement a custom NavigationListItem to store the three together in an object if you want.
// Custom navigation list adapter...
Context    context   = getSupportActionBar().getThemedContext();
TypedArray logos     = getResources().obtainTypedArray(R.array.activity_logos);
TypedArray titles    = getResources().obtainTypedArray(R.array.activity_titles);
TypedArray subtitles = getResources().obtainTypedArray(R.array.activity_subtitles);
NavigationListAdapter navigationListApdater = new NavigationListAdapter(context, logos, titles, subtitles);
        
// Custom navigation list listener... 
NavigationListListener navigationListListener = new NavigationListListener(this);

// Set navigation mode...
getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
getSupportActionBar().setListNavigationCallbacks(navigationListApdater, navigationListListener);

Then just to finish it off, it determines the position in the navigation list for current activity - this will be used to select current activity in the navigation list onResume() - for both initial run as well as when coming back from next activity - as well as used in the listener to avoid going into a loop. That is because when the activity starts it will select the first entry in the navigation list, causing an unintended activity run.
/*
 * (non-Javadoc)
 * @see android.app.Activity#onCreate(android.os.Bundle)
 */
protected void onCreate(Bundle savedInstanceState) {
  [...]
  m_currentNavigationItem = getCurrentNavigationItem(this, titles);
  [...]
}

/*
 * (non-Javadoc)
 * @see android.app.Activity#onResume()
 */
@Override
protected void onResume() {
  /*
   * Super...
   */
   super.onResume();
     
   /*
    * Select current title in navigation list (first start or on back)...
    */
   getSupportActionBar().setSelectedNavigationItem(m_currentNavigationItem);
}

/**
 * Get navigation list index for current activity.
 * 
 * @param p_activity
 * @param p_titles
 * @return
 */
private int getCurrentNavigationItem(Activity p_activity, TypedArray p_titles) {
  String title    = p_activity.getTitle().toString();
  int    position = 0;
     
  for (int i = 0, n = p_titles.length(); i < n; i++) {
    if (p_titles.getString(i).equals(title)) {
      position = i;
      break;
    }
  }
  
  return position;
}
The NavigationListAdapter implements a SpinnerAdapter and provides custom layouts for the top ActionBar item and drop-down items.

The top item doesn't have an icon (only title and subtitle), as explained above, the activity will provide it.
/*
 * (non-Javadoc)
 * @see android.widget.Adapter#getView(int, android.view.View, android.view.ViewGroup)
 */
@Override
public View getView(int p_position, View p_convertView, ViewGroup p_parent) {
 /*
  * View...
  */
 View view = p_convertView;
 if (view == null) {
  view = m_layoutInflater.inflate(R.layout.navigation_list_item, p_parent, false);
 }
 
 /*
  * Display...
  */
 // Title...
 TextView tv_title = (TextView) view.findViewById(R.id.title);
 tv_title.setText(m_titles.getString(p_position));
 
 // Subtitle...
 TextView tv_subtitle = ((TextView) view.findViewById(R.id.subtitle));
 tv_subtitle.setText      (m_subtitles.getString(p_position));
 tv_subtitle.setVisibility("".equals(tv_subtitle.getText()) ? View.GONE : View.VISIBLE);
 
 /*
  * Return...
  */
 return view;
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 android:gravity="center_vertical|left">
 
 <TextView
  android:id="@+id/title"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:paddingLeft="0dp"
  android:singleLine="true"
  android:ellipsize="end"
  style="?attr/spinnerItemStyle" />
 
 <TextView
  android:id="@+id/subtitle"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:paddingLeft="0dp"
  android:singleLine="true"
  android:ellipsize="end"
  android:textAppearance="?attr/textAppearanceSmall"
  style="?attr/spinnerItemStyle" />
 
</LinearLayout>
The drop-down item has a logo, title and subtitle. Both allow for subtitles to be optional, hiding the subtitle when blank, vertically centering the title to take the space.
/*
 * (non-Javadoc)
 * @see android.widget.BaseAdapter#getDropDownView(int, android.view.View, android.view.ViewGroup)
 */
@Override
public View getDropDownView(int p_position, View p_convertView, ViewGroup p_parent) {
 /*
  * View...
  */
 View view = p_convertView;
 if (view == null) {
  view = m_layoutInflater.inflate(R.layout.navigation_list_dropdown_item, p_parent, false);
 }
 
 /*
  * Display...
  */

 // Icon...
 ImageView iv_logo = (ImageView) view.findViewById(R.id.logo);
 iv_logo.setImageDrawable(m_logos.getDrawable(p_position));
 
 // Title...
 TextView tv_title = (TextView) view.findViewById(R.id.title);
 tv_title.setText(m_titles.getString(p_position));
 
 // Subtitle...
 TextView tv_subtitle = ((TextView) view.findViewById(R.id.subtitle));
 tv_subtitle.setText      (m_subtitles.getString(p_position));
 tv_subtitle.setVisibility("".equals(tv_subtitle.getText()) ? View.GONE : View.VISIBLE);
 
 /*
  * Return...
  */
 return view;
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="?attr/dropdownListPreferredItemHeight"
 android:orientation="horizontal"
 style="?attr/spinnerDropDownItemStyle">
 
 <ImageView
  android:id="@+id/logo"
  android:layout_width="wrap_content"
  android:layout_height="match_parent"
  android:adjustViewBounds="true"/>
 
 <LinearLayout
  android:layout_width="wrap_content"
  android:layout_height="match_parent"
  android:orientation="vertical"
  android:gravity="center_vertical|left">
  
  <TextView 
   android:id="@+id/title"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"  
   android:singleLine="true"
   android:ellipsize="end"
   style="?attr/spinnerDropDownItemStyle" />
  
  <TextView 
   android:id="@+id/subtitle"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:singleLine="true"
   android:ellipsize="end"
   android:textAppearance="?attr/textAppearanceSmall"
   style="?attr/spinnerDropDownItemStyle" />
  
 </LinearLayout>

</LinearLayout>

Finally, NavigationListListener implements OnNavigationListener - an interesting note, it uses the item id - provided by NavigationListAdapter.getItemId() - which is in fact the title string id, to decide which activity to start. This is an alternative to using the position, which might just as well change during development, but this way you won't need to change the code to match.
/**
 * Custom navigation list listener.
 */
private class NavigationListListener implements OnNavigationListener {
 /**
  * Members
  */
 private AbstractActivity m_activity;
 
 /**
  * 
  * @param p_activity
  */
 NavigationListListener(AbstractActivity p_activity) {
  m_activity = p_activity;
 }
 
 /*
  * (non-Javadoc)
  * @see com.actionbarsherlock.app.ActionBar.OnNavigationListener#onNavigationItemSelected(int, long)
  */
 @Override
 public boolean onNavigationItemSelected(int p_itemPosition, long p_itemId) {
  /*
   * Ignore if selecting current...
   */
  if (p_itemPosition == m_activity.m_currentNavigationItem) {
   return true;
  }
  
  /*
   * Start new activity...
   */
  Intent intent = null;
  if (p_itemId == R.string.title_activity_main) {
   intent = new Intent(m_activity, MainActivity.class);
  }
  else if (p_itemId == R.string.title_activity_one) {
   intent = new Intent(m_activity, FirstActivity.class);
  }
  else if (p_itemId == R.string.title_activity_two) {
   intent = new Intent(m_activity, SecondActivity.class);
  }
  
  if (intent != null) {
   intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
   startActivity(intent);
  }
  
  return true;
 }
}
And that's about it - the activities will just need to provide a layout id and the generic AbstractActivity logic will take care of the work consistently across.

Source code (Google Code - SVN):
Resources:

Sunday, March 3, 2013

Google Analytics EasyTracker: detailed stack-traces

If you're using Google Analytics to capture crashes in your application you may have noticed that uncaught exceptions give you very little information (just the exception message):
An error occured while executing doInBackground() 

Google Analytics documentation does mention that you can use your own custom ExceptionReporter / ExceptionParserDecompiling the library though you can see how it is all implemented - I should note that this article is based on Google Analytics v2.0 b4 and may or may not work with other versions.

Instead of creating a new ExceptionReporter and attaching it as a default handler, will assign a custom ExceptionParser in the default ExceptionReporter. That is mainly because from looking at the code the suggested way will chain handlers, possibly producing duplicate reports.

The replacement will go the Application class, before anything else (you could do it if you want in any other activity, although once is enough). First EasyTracker.getInstance().setContext() call initializes the structures, calling EasyTracker.loadParameters() method - among other things it will also create an ExceptionParser and set it as a default exception handler - Thread.setDefaultUncaughtExceptionHandler()Should note that the uncaught exception handler is only set when you have set a ga_trackingId and ga_reportUncaughtExceptions = true in your analytics.xml config file.
package com.your.package;

import com.google.analytics.tracking.android.EasyTracker;
import com.google.analytics.tracking.android.ExceptionReporter;

public class Application extends android.app.Application {
  /*
   * (non-Javadoc)
   * @see android.app.Application#onCreate()
   */
  public void onCreate() {
    [...]

    /*
     * Google Analytics...
     */
    EasyTracker.getInstance().setContext(this);

    // Change uncaught exception parser...
    // Note: Checking uncaughtExceptionHandler type can be useful if clearing ga_trackingId during development to disable analytics - avoid NullPointerException.
    Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
    if (uncaughtExceptionHandler instanceof ExceptionReporter) {
      ExceptionReporter exceptionReporter = (ExceptionReporter) uncaughtExceptionHandler;
      exceptionReporter.setExceptionParser(new AnalyticsExceptionParser());
    }
  }
}

My custom AnalyticsExceptionParser uses Apache Commons Lang ExceptionUtils, but just as well you could use your own implementation.
package com.your.package.util;

import org.apache.commons.lang3.exception.ExceptionUtils;
import com.google.analytics.tracking.android.ExceptionParser;

public class AnalyticsExceptionParser implements ExceptionParser {
  /*
   * (non-Javadoc)
   * @see com.google.analytics.tracking.android.ExceptionParser#getDescription(java.lang.String, java.lang.Throwable)
   */
  public String getDescription(String p_thread, Throwable p_throwable) {
     return "Thread: " + p_thread + ", Exception: " + ExceptionUtils.getStackTrace(p_throwable);
  }
}
Now this will produce you a complete stack trace with class names and line numbers, that should be very helpful to figure out where the problem happened exactly.


* * *
You can apply similar to EasyTracker.getTracker() setting your custom ExceptionParser with EasyTracker.getTracker().setExceptionParser(...) for caught exceptions, although not necessary as probably in those cases you will want to send the exception plus some other contextual data to figure out the context, and for what you can use sendException(String description, boolean fatal) e.g.:

String response = null;
try {
  [...]
  response = doSomething();
}
catch (Throwable t) {
  // Analytics...
  EasyTracker.getTracker().sendException("response = " + response + ", " + ExceptionUtils.getStackTrace(t), false);

  // Logging...
  if (BuildConfig.DEBUG) {
    Log.e(TAG, ExceptionUtils.getStackTrace(t));
  }
}