Showing posts with label Android. Show all posts
Showing posts with label Android. Show all posts

Thursday, January 30, 2014

Contextual Action Bar with Android's ExpandableListView

I have put together a sample project on Github illustrating that the Contextual Action Bar pattern can be applied to Android's ExpandableListView, if your use case implies that you either want to select groups or the childs and do not need a mixed selection.

The magic is quite simple, it mainly consists in storing the type of selection in the ActionMode.Callback's onItemCheckedStateChanged() method:

    @Override
    public void onItemCheckedStateChanged(ActionMode mode, int position,
                                          long id, boolean checked) {
      int count = lv.getCheckedItemCount();
      if (count == 1) {
        expandableListSelectionType = ExpandableListView.getPackedPositionType(
            lv.getExpandableListPosition(position));
      }
      mode.setTitle(String.valueOf(count));
      configureMenu(mode.getMenu(), count);
    }

Then you need to implement onChildClick() and onGroupClick() listeners that check the list items once action mode is active:

    @Override
    public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
      if (mActionMode != null)  {
        if (expandableListSelectionType == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
          int flatPosition = parent.getFlatListPosition(ExpandableListView.getPackedPositionForGroup(groupPosition));
          parent.setItemChecked(
              flatPosition,
              !parent.isItemChecked(flatPosition));
          return true;
        }
      }
      return false;
    }
    @Override
    public boolean onChildClick(ExpandableListView parent, View v,
        int groupPosition, int childPosition, long id) {
      if (mActionMode != null)  {
        if (expandableListSelectionType == ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
          int flatPosition = parent.getFlatListPosition(
              ExpandableListView.getPackedPositionForChild(groupPosition,childPosition));
          parent.setItemChecked(
              flatPosition,
              !parent.isItemChecked(flatPosition));
        }
        return true;
      }
      return false;
    }

The sample project also illustrates a technique of defining the context actions as part of four groups, depending on two combined criteria:
  • actions for groups or children
  • bulk actions that can be applied to multiple items, and single item actions

Saturday, July 6, 2013

DialogFragment displaying options from a cursor held by the Activity

Continuing to chase down performance problems with Android's StrictMode in My Expenses: On several occasions, I needed to present the user a dialog for selecting an item from a cursor. Previously I was loading this cursor in the fragment without passing through a CursorLoader. Instead of loading the cursor again, if it is already held in the activity, I was looking for a way to access the activity's cursor, and came up with SelectFromCursorDialogFragment. It defines an interface (SelectFromCursorDialogListener) that the activity displaying the dialog implements in order to 1) provide the cursor (getCursor) and 2) to process the result (onItemSelected).

An example use case is the "Move transaction" command, that allows to move a transaction from one account to another, and needs to present a dialog for selecting the target account. Following code is used for displaying the dialog

      args = new Bundle();
      args.putInt("id", R.id.MOVE_TRANSACTION_COMMAND);
      args.putString("dialogTitle",getString(R.string.dialog_title_select_account));
      args.putString("column", KEY_LABEL);
      args.putLong("contextTransactionId",info.id);
      args.putInt("cursorId", ACCOUNTS_OTHER_CURSOR);
      SelectFromCursorDialogFragment.newInstance(args)
        .show(getSupportFragmentManager(), "SELECT_ACCOUNT");

SelectFromCursorDialogFragment receives a bundle with the following required keys
id
Allows to identify the context from which the dialog is opened in the onItemSelected callback.
dialogTitle
The title for the dialog.
column
the column being used for the options to be selected.
cursorId
the id under which the cursor will be made available in getCursor.
 
The bundle is handed back in the callback, so any additional information can be added that is needed for processing, in this example we store as "contextTransactionId", the transaction that the user wants to move.
In this example, we implement the interface in the following way

  @Override
  public void onItemSelected(Bundle args) {
    switch(args.getInt("id")) {
    case R.id.MOVE_TRANSACTION_COMMAND:
      Transaction.move(
          args.getLong("contextTransactionId"),
          args.getLong("result"));
      break;
    }
  }
  @Override
  public Cursor getCursor(int cursorId) {
    switch(cursorId) {
    case ACCOUNTS_OTHER_CURSOR:
      return new AllButOneCursorWrapper(mAccountsCursor,currentPosition);
    }
    return null;
  }

The "result" key in the bundle is where the selected item's id is stored.
The cursor returned for the id ACCOUNTS_OTHER_CURSOR is the solution to another challenge. The activity has a cursor for all accounts, but the target obviously needs to be a different account from the current one, so we need to exclude one row from the cursor, which is handled by the AllButOneCursorWrapper class.

EDIT: It turned out, that above solution only worked as long, as the user does not change orientation while displaying the dialog. To make the dialog react correctly to orientation changes proofed to be quite complicated. The problem is, that when the activity is recreated and the fragment instantiated, the cursor is not yet available, and you need to hook into the cursor loader's callback in order to call up the fragment and set the cursor there. SelectFromCursorDialogFragment now implements this requirement through the following changes:
  1. Instead of using the builder's setSingleChoiceItems method, an SimpleCursorAdapter is created and stored as class member variable.
  2. SelectFromCursorDialogFragment defines a setCursor method which uses the adapter's swapCursor method.
  3. the getCursor method of the SelectFromCursorDialogListener interface has a second argument tag which allows the activity to store the tag of the fragment, if it does not have the cursor the fragment needs.
  4. in the activity's onLoadFinished method, we check if there is fragment tag stored as a callback, retrieve the fragment, and use its  setCursor method.
Some overhead for a tiny detail, but worthwhile, if you pay attention, as I try to do, to have your app handle orientation changes correctly.

Thursday, July 4, 2013

Configuring Actionbar from a Fragment using CursorLoader inside a ViewPager

My Expenses allows to swipe through accounts using a ViewPager.  with a FragmentPagerAdapter, i.e, each list of transactions has its own fragment.
The actionbar has a "Reset" menu item, that should be disabled/invisible in case the account whose page is visible has no transactions. Until now, this was done in the activity's onPrepareOptionsMenu method by a database call that retrieved the row count for the current account. Not surprisingly, this call got marked as performance impeding, when I evaluated the app's performance with StrictMode.

I thought it would be easy to have the fragment take care of configuring the menu item, since it has the cursor, and knows if it has any transactions to display. Nevertheless, it took me some time to find out the working solution. First, you need to take care of the fact that the ViewPager instantiates the fragments before they become visible, so you need a mechanism, to trigger the reconfiguration when the fragment becomes visible.
I was misled by the assumption that I would have to either overwrite or use the fragment's setMenuVisibility method, but in fact this method is already called by the adapter with its argument menuVisible as true, when the page becomes visible, and it takes care of invalidating the options menu.

Having understood this, the solution becomes quite simple and can be seen in this commit.

First in the cursor loaders callback, we store in a boolean field hasItems the information if the cursor contains any rows. And if we are dealing with the fragment that is currently visible, we invalidate the options menu. This second step takes care of situations where the cursor is loaded again, when a transaction is added or deleted. I am using the methods provided by ActionBarSherlock, but the solution should work the same with the equivalent standard Android API methods.

   public void onLoadFinished(Loader arg0, Cursor c) {
     mAdapter.swapCursor(c);
     hasItems = c.getCount()>0;
     if (isVisible())
       getSherlockActivity().supportInvalidateOptionsMenu();
   }
 
   @Override
   public void onLoaderReset(Loader arg0) {
     mAdapter.swapCursor(null);
     hasItems = false;
     if (isVisible())
       getSherlockActivity().supportInvalidateOptionsMenu();
   }
 
Second, we use hasItems field in onPrepareOptionsMenu, which now is triggered in both situations, we are interested in: first when the visibility of a fragment changes, second when the cursor for the visible fragment is reloaded. 

  @Override
  public void onPrepareOptionsMenu(Menu menu) {
    if (isVisible())
      menu.findItem(R.id.RESET_ACCOUNT_COMMAND).setVisible(hasItems);
  } 

Finally, we need to call

      setHasOptionsMenu(true); 

in onCreate() in order to make sure that onPrepareOptionsMenu is called on our fragment. It does not matter that the menu item is not added by the fragment, but by the activity. The fragment is allowed to manipulate the menu created by the activity.

Sunday, December 9, 2012

Moving from jquery tabs and iframes to jekyll

For the MyExpenses web site, I had used Jquery Tabs for navigating between the different sections, and contents that were also displayed from inside the app, like the user manual and the news were displayed in iframes. That worked well, but needed considerable tweaking through Javascript for
  • being able to link to individual sections and their subpages
  • resizing the iframe dynamically
It worked well, until I wanted to integrate Disqus commenting to the News which did not fit into the iframe where the News were displayed.
While looking for a solution, I discovered that Github pages, where the website is hosted, supports Jekyll, which is a mechanism for generating static pages from templates.
With the Jekyll, the navigation is now stored in one template, that is considerably simpler and easier to maintain than the Javascript code used before, and each time I push content to Github, the site is regenerated from this template. Manual and news are still generated from Docbook through XSLT stylesheets, but those output now the YAML header that Jekyll interprets.
The new interface now uses vertical tabs (the design needs to be polished, I admit), that make the whole site display better on the smartphone, and I no longer need a special navigation-less in-app display.
The added advantage: MyExpenses now can call Android's browser for displaying content and no longer relies on its own Webview, thus no longer needs the permission to access the Internet, which understandably is a stumbling block for security sensitive users when dealing with a financial application.

Sunday, April 22, 2012

DOM vs SAX on Android

It is well documented that when working with XML on an Android device, SAX is preferable over DOM. For MyExpenses, for importing categories from an XML file, I had ignored this advice, since originally it was planned to import a limited number of small files provided with the app. But later users got the possibility to provide their own file, and it was no longer possible to exclude the case of potentially large files being provided as input.
Having implemented the import based on the SAX parser now, using Androids infrastructure for unit testing, I could easily compare the two implementations through this UnitTest, and come up with a measurement of how they perform when confronted with large files. The table lists the time the test needs to run when parsing the files with SAX or DOM on a Google Nexus S:

FileSize SAX DOM
1M 0,755 4,934
2M 1,475 8,335
4M 3,019 19,347
6M 4,756 OutOfMemoryError

Monday, March 26, 2012

Experimenting with button bar on Android as an efficient data entry interface

As suggested by a user of My Expenses, I sought for an interface that would facilitate the entry of new data and still have a consistent display of all available commands. It should also be possible to use the standard menu provided by the Android platform and triggered by the hardware menu button.
Release 1.4.8. implements these requirements. There is a simple button bar class, where each button open up a menu on long clicks, and triggers a default action on short clicks. Upon press on the menu button, a dialog offers to switch back to the traditional Android menu. There is much room for improvement with respect to the design of the popup menus.



Not sure yet how this approach will be taken up by users. If you consider it worth a try, grab MyExpenses source code at Github.

Wednesday, March 14, 2012

Multilingual documentation for an Android project with Docbook

Two interesting challenges, while writing the multilingual tutorial for My Expenses:
  1. Use one single input file facilitating translation
  2. Reference interface messages from the project's resource files instead of repeating them literally, in order to assure consistency.
For the first one, I borrowed a stylesheet from DocBook.sml that allows to generate language-specific files from the multilingual document. These can then be further processed into html and pdf with the standard Docbook XSLT.

My solution to the second one, can be found in this customization of the chunking template. Have a look at how the inline.charseq template is overridden. This is the important part:

  <xsl:choose>
  <xsl:when test="normalize-space(.)">
   .....
  </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="document($resfile)/resources/string[@name = $id]">
    </xsl:value-of></xsl:otherwise>
  </xsl:choose>

When the element has no content, localized string is looked up in the resource file, based on the role attribute of the element, stored earlier in the variable id. Now elements like <guimenuitem role="select_account"/> are rendered with their translated message.

Tuesday, March 13, 2012

Automatic generation of screenshots for a tutorial on Android app

I invested some time in finding a way to generate screenshots for the tutorial on my Android app. With now 11 images in four languages, keep the screenshots up to date when the app evolves, has become too tedious to be done manually
Monkeyrunner seems the perfect tool, but I found it very hard to have it execute a given scenario reliabily. The two main problems I encountered were:
  • Entering data in a form
  • Triggering context menu in listviews
The solution I found, involves defining a key (e.g. KEYCODE_ENVELOPE) to a backdoor in the app, that changes the activity's state in the desired way.  Then in the Monkeyrunner script, I just need to send the key at the right moment with

device.press('KEYCODE_ENVELOPE', MonkeyDevice.DOWN)

The method handling the key in the activity class is onKeyDown.
You can find examples in the classes AccountEdit, ExpenseEdit, SelectAccount and SelectCategory from here (tag r25). In the editing activities, I populate the form with data, in the selection activities, I make sure that the listview has the focus, I found no other reliable method to have the context menu triggered from Monkeyrunner.

Sunday, March 11, 2012

TextWatcher and change of device orientation

I encountered an interesting problem with a TextWatcher listening for changes in an EditText. The afterTextChanged method was called, each time, the device orientation changed. An answer on Stackoverflow let me understand what was happening: Android recreates the activity, and the automatic restoration of the state of the input fields, is happening after onCreate had finished, where the TextWatcher was added as a TextChangedListener. The solution to the problem consisted in adding the TextWatcher in onPostCreate, which is called after restoration has taken place. You can find the source in this commit of project MyExpenses on Github.

Thursday, February 16, 2012

Talking to Google Spreadsheet API from Android

Experimenting with how to store data in a Google Spreadsheet from an Android app using the Google API client java library. The example provided by Joel Edström uses and older version of the library, and for me failed to store the spreadsheet with errors "401 Invalid token". Adapting it to use the approach found in the Picasa Android Sample, I was able to make it work: SpreadheetTest2.zip

Wednesday, February 8, 2012

Android testing: configure preferences and database

I started experimenting with unit and functional tests for my Android app. It took me some time to find out how to set up the tests with special preferences and database files.
I tried to use the mock contexts that the Android testing framework provides, but ran into the following problems:
  • The support for testing content providers was not applicable, because I am still using the older, simpler design of the database helper class bassed on SQLiteOpenHelper
  • A simple RenamingDelegatingContextdid work with ActivityUnitTestCase, but testing dialogs did not work there.
  • Apparently the framework does not provide any support for renaming the default preferences file
Finally, I found a useful suggestion at Android Functional Testing vs Dependency Injection 
Using that pattern I found a way to configure my database adapter with different database file names for the live app and the test drive.

My application class reads:

public class MyApplication extends Application {
    private SharedPreferences settings;
    private String databaseName;

    @Override
    public void onCreate()
    {
        super.onCreate();
        if (settings == null)
        {
            settings = PreferenceManager.getDefaultSharedPreferences(this);
        }
        if (databaseName == null) {
          databaseName = "data";
        }
    }

    public SharedPreferences getSettings()
    {
        return settings;
    }

    public void setSettings(SharedPreferences s)
    {
        settings = s;
    }
    public String getDatabaseName() {
      return databaseName;
    }
    public void setDatabaseName(String s) {
      databaseName = s;
    }
}
And the relevant parts of the database adapter class:

public class ExpensesDbAdapter {
  private String mDatabaseName;

  public ExpensesDbAdapter(Context ctx) {
    this.mCtx = ctx;
    mDatabaseName = ((MyApplication) ctx.getApplicationContext()).getDatabaseName();
  }
  public ExpensesDbAdapter open() throws SQLException {
    mDbHelper = new DatabaseHelper(mCtx,mDatabaseName);
    mDb = mDbHelper.getWritableDatabase();
    return this;
  }
  private static class DatabaseHelper extends SQLiteOpenHelper {

    DatabaseHelper(Context context,String databaseName) {
      super(context, databaseName, null, DATABASE_VERSION);
    }
  }
And setting a different name in the test case works with

  protected void setUp() throws Exception {
    super.setUp();
    MyApplication app = (MyApplication) getInstrumentation().getTargetContext().getApplicationContext();
    app.setSettings(app.getSharedPreferences("functest",Context.MODE_PRIVATE));
    app.setDatabaseName("functest");
}
You can find the complete example in the source code of MyExpenses.