Sunday, 2 November 2014

Calculator widget tutorial - Part 1

I have decided to implement a widget for the unit converter and what former ideas I had for the widget were thrown out the window due to some catches with widgets. The process of developing GUI for widgets is very different from that used normally in activities. Android widgets documentation tries its best to explain how to implement the fairly convoluted code for a widget but I got somewhat lost in all the information so here's a tutorial that explains how to receive click events from a widget. Luckily, Android Studio generates a template that provides a useful baseline. The tutorial is based on Android Studio 0.8.14 (Beta). This tutorial will display 4 pseudo text fields (you will see what I mean later on) and they change their content randomly when clicked on.


First off, create a new App Widget using Ctrl+N in an existing project and fill in the wizard which is straight forward.



This generates a couple of files and modifies a couple of files including the Android Manifest and strings.xml. First we will start with the Android manifest:
        <receiver android:name="com.thompson.example.CalculatorWidget" >
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/calculator_widget_info" />
        </receiver>
The addition to the manifest lets the Android OS knows the app can generate a widget and provides information about the widget. The name on line 1 is the class implementing AppWidgetProvider which interfaces with the widget to receive events and dispatch drawing updates. Line 3 lists the action the widget can receive through Intents. Line 7 can be left alone and line 8 is a resource file to display to the user when the user is browsing through widgets to add. The default is worthless so should be changed to one of your own image resources or removed entirely (in this case, the app icon is used which is pretty unimaginative). I used a screenshot of the layout file for mine:


A couple of files are also produced which are: calculator_widget_info.xml, RelativeLayout.xml, CalculatorWidget.java and calculator_widget_info.png as a "nodpi" image resource. The first contains information you specified in the wizard outlining how big the widget should be and so on. You can customise the information in this file if you need to. The layout file and widget provider class we will go into later. Finally, the preview image is junk so just delete it (make sure you specify your own image in the manifest as mentioned above or delete the android:resource attribute).

Note that in my case, the wizard also inserted the following permissions into the Android manifest file. I deleted this code as I didn't need the permissions and they were also causing errors:
    <android:uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="18" />
    <android:uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <android:uses-permission
        android:name="android.permission.READ_EXTERNAL_STORAGE"
        android:maxSdkVersion="18" />
Next we go into the layout. Widgets operate differently from activities by using remote views and this limits what you can do enormously. The only UI components allowed are:
And you can't use descendants of these component either. The most glaring omission is EditText which was why my plans were thrown out the window.  On top of this. only certain layouts are allowed (see Android docs for more info). Anyway, the following layout code should be copied and pasted into RelativeLayout.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:dividerPadding="2dp" >


    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textIsSelectable="true"
            android:singleLine="true"
            android:ems="8"
            android:id="@+id/ucQtyAInput"
            android:hint="Quantity A"
            android:nextFocusForward="@+id/ucUnitAInput"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:id="@id/ucUnitAInput"
            android:nextFocusForward="@+id/ucQtyBInput"
            android:hint="Unit A"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

    </LinearLayout>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textIsSelectable="true"
            android:singleLine="true"
            android:ems="8"
            android:id="@id/ucQtyBInput"
            android:nextFocusForward="@+id/ucUnitBInput"
            android:hint="Quantity B"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:singleLine="true"
            android:nextFocusForward="@id/ucQtyAInput"
            android:id="@id/ucUnitBInput"
            android:hint="Unit B"
            style="@android:style/Widget.EditText"
            android:layout_weight="1"/>

    </LinearLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Status label"
        android:layout_margin="2dp"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:textColor="@color/errorText"
        android:id="@+id/ucStatusLabel"  />

</LinearLayout>
The layout file is like any normal layout file except for the restrictions mentioned earlier. Two things to note, I have styled the TextView to look like EditText (to create pseudo text fields but this is optional) and I have not used String resources to make it easier to copy and paste (although you should extract those strings to strings.xml to allow for possibility of internationalisation). 

Finally, we will edit the widget provider class. First, add the following constants:
    private static String
            ucUpdateAction = "ucUpdateAction",
            extraKey = "com.thompson.example.CalculatorWidget.fieldPressed";
And then add code to the updateAppWidget method as well as a helper method:
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), 
                R.layout.unit_converter_widget);
        addClickIntent(context, views, R.id.ucQtyAInput);
        addClickIntent(context, views, R.id.ucQtyBInput);
        addClickIntent(context, views, R.id.ucUnitAInput);
        addClickIntent(context, views, R.id.ucUnitBInput);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    private static void addClickIntent(Context c, RemoteViews rv, int compID) {
        Intent intent = new Intent(c, CalculatorWidget.class);
        intent.setAction(ucUpdateAction);
        intent.putExtra(extraKey, compID);
        PendingIntent pi = PendingIntent.getBroadcast(c, compID, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        rv.setOnClickPendingIntent(compID, pi);
    }
The helper method initialises the components with click listeners but this is done differently in widgets. The click listener instead triggers an intent which is listened to by the widget provider. In this example, we pass around the ID of the component that was clicked so we know which one to update (line 17). The PendingIntent wraps around the intent and has a couple of properties when set in line 18. You provide the context followed by a sender's code, the intent to wrap around and then some flags. The sender's code is used to allow you to retrieve the intent later on or to cancel it. If you require different intents for multiple component (like this example), make sure the sender's code is unique or they will replace each other. I have used the component's ID number as they are guaranteed to be unique. The PendingIntent.FLAG_UPDATE_CURRENT flag indicates that the most up to date intent data should be passed on to the receiver in the Intent's bundle. This sets the widget to send intents when the text fields are clicked on. 

We finally receive the events by overriding onReceive
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(ucUpdateAction)) {
            int compID = intent.getIntExtra(extraKey, -1);
            if (compID == -1) return;
            RemoteViews rv = new RemoteViews(context.getPackageName(),
                    R.layout.calculator_widget);
            int r = (int) (Math.random() * 100);
            rv.setTextViewText(compID, "Pressed... " + r);
            ComponentName cn = new ComponentName(context, CalculatorWidget.class);
            AppWidgetManager.getInstance(context).updateAppWidget(cn, rv);
        } else {
            super.onReceive(context, intent);
        }
    }
The method simply filters out events that we are interested in and updates the text field with randomish text when clicked. Note lines 10 and 11 is how you should update the component for redraw. For each of the components you are allowed to use in widgets, there are convenience methods in AppWidgetManager to update those components accordingly. That's it, you should be able to run the code now. You should see the text field update with random numbers as you tap them. In the next part, we will receive events from buttons and update the text fields based on what buttons were pressed.



The full CalculatorWidget.java source file can be downloaded here.

Update:
A very hard to notice bug in the code above occurs when the widget is reloaded from memory. The RemoteViews loads the configuration last written to it so if you update the widget but do not update everything else, the widget will be loaded incorrectly. In this case, you need to reapply the Pending click events to all 4 fields for each update. The better approach is to create a data object and save all information you need to the data object and then save the data object to persistent memory (e.g. using shared preferences). You then update the entire widget using the data object ensuring nothing is missed when the widget is updated. I.e. the widget loads the last RemoteViews you supplied so if you do not specify everything in the RemoteViews, then when the widget is reloaded, only the functionality you made to the last provided RemoteViews gets applied.