Android Smoke Tracker

Will Holcomb

4 September 2013

I'm interested in tracking my behaviors over time to help get a better sense of them. I've been using Fail Log to keep track of my smoking habit. I like the app for a couple reasons:

The app is less than ideal in that:

I'd like an application that provides a web interface for analyzing the data with more graphical elements. This blog post will cover the creation of the initial data collection application.

The application is available as a APK on Github. I'll be developing using Eclipse. To start developing:

  1. Download the code from github
  2. In Eclipse: FileImport
  3. For import source use: AndroidExisting Android Code Into Workspace

The UI looks like:

The tab layout is based on the ActionBarTabsPager application from the support v13 examples. The MainActivity class handles switching between Fragments. The operable part of the class is:

protected void onCreate(Bundle savedInstanceState) {
    ⋮
    mTabsAdapter.addTab(bar.newTab().setText(R.string.habits_tab), HabitListFragment.class, null);
    mTabsAdapter.addTab(bar.newTab().setText(R.string.events_tab), EventListFragment.class, null);
    mTabsAdapter.addTab(bar.newTab().setText(R.string.goals_tab), GoalListFragment.class, null);
    mTabsAdapter.addTab(bar.newTab().setText(R.string.statistics_tab), StatisticsFragment.class, null);

The R.string entries are defined in res/values/strings.xml and the first tab loads an instance of HabitListFragment. This is a subclass of ListFragment that shows the habit names and a timer from the last time that habit was recorded.

Data is stored in a SQLite database. Rather that the data being accessed directly, queries are handled by a ContentProvider which receives a Uri and performs an operation. This method is useful for exposing data to other applications, but represents a risk for unauthorized access.

Each row in the list is an instantiation of res/layout/habit_row.xml. It includes two TextViews and a Timer which shows the time offset from a point. Updating these various items is done with a ViewBinder:

public void onActivityCreated(Bundle savedInstanceState) {
    ⋮
    String[] from = new String[] { HabitTable.COLUMN_NAME, HabitTable.COLUMN_COLOR, EventTable.COLUMN_TIME };
    int[] to = new int[] { R.id.label, R.id.color_block, R.id.timer };

    mAdapter = new SimpleCursorAdapter(getActivity(), R.layout.habit_row, null, from, to, 0);

    mAdapter.setViewBinder(new ViewBinder() {
        public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
            if(columnIndex == 2) { // Color
                view.setBackgroundColor(Color.parseColor(cursor.getString(columnIndex)));
                return true;
            }

            if(columnIndex == 3) { // Time
                Timer timer = (Timer) view;
                if(cursor.getType(columnIndex) == Cursor.FIELD_TYPE_NULL) {
                    timer.setVisibility(View.GONE);
                } else {
                    long time = cursor.getInt(columnIndex);
                    timer.setStartingTime(time * 1000);
                }
                return true;
           }

           return false;
        }
    });

You'll note that in the above code there is no database query. The Cursor is not created when the Adapter is initialized, rather that is handled asynchronously by the LoaderManager. The method which creates the Cursor is:

static final String[] HABITS_PROJECTION = new String[] {
    HabitTable.TABLE_HABIT + "." + HabitTable.COLUMN_ID,
    HabitTable.COLUMN_NAME,
    HabitTable.COLUMN_COLOR,
    "MAX(" + EventTable.COLUMN_TIME + ") as " + EventTable.COLUMN_TIME
};

public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    return new CursorLoader(getActivity(), MyHabitContentProvider.HABITS_URI, HABITS_PROJECTION, null, null, null);
}

There is also a GROUP BY statement in the ContentProvider.

When list rows are clicked a new event is created, the view is reloaded, and the visible tab switches to "Events":

public void onListItemClick(ListView l, View v, int position, long id) {
    ContentValues values = new ContentValues();
    values.put(EventTable.COLUMN_HABIT_ID, id);
    values.put(EventTable.COLUMN_TIME, Math.floor(System.currentTimeMillis() / 1000));
    getActivity().getContentResolver().insert(MyHabitContentProvider.EVENTS_URI, values);

    Toast.makeText(getActivity(), "Added new event", Toast.LENGTH_LONG).show();

    getLoaderManager().restartLoader(0, null, this);
    mAdapter.notifyDataSetChanged();
        
    ((MainActivity) getActivity()).setActiveTab(1);
}

The EventListFragment is similar to the habits list:

The primary difference is the section headers showing days. This is handled by using an ArrayAdapter rather than a CursorAdapter. Rather than items being loaded directly from the Cursor, an array of ListItems is created and HeaderedListAdapter* loads the appropriate view for each.

Also the detail view is more complex for events:

It is not possible to set the position of a spinner from a database id, so the following code is necessary to set the spinner:

int habitId = cursor.getInt(cursor.getColumnIndexOrThrow(GoalTable.COLUMN_HABIT_ID));
for(int pos = mAdapter.getCount(); pos >= 0; pos--) {
  if(mAdapter.getItemId(pos) == habitId) {
    mHabitSelect.setSelection(pos);
    break;
  }
}

The time is stored in the database in seconds rather than milliseconds, so the code for populating the date and time is:

long seconds = cursor.getInt(cursor.getColumnIndexOrThrow(EventTable.COLUMN_TIME));
eventTime.setTimeInMillis(seconds * 1000);
      
mEventDate.updateDate(eventTime.get(Calendar.YEAR),
                      eventTime.get(Calendar.MONTH),
                      eventTime.get(Calendar.DAY_OF_MONTH));
mEventTime.setCurrentHour(eventTime.get(Calendar.HOUR_OF_DAY));
mEventTime.setCurrentMinute(eventTime.get(Calendar.MINUTE));

The goal interface is essentially the same as the habits list and statistics aren't implemented yet.