Archive

Archive for the ‘ui’ Category

Displaying a changelog

October 7, 2010 3 comments

The new (upcoming) 1.7 release of Wifi Manager has a few new features, so I’ve set out to add a changelog. I wanted the changelog to be displayed automatically just once whenever a new version is run for the first time.

Creating the activity to display the change is not too difficult. I wanted the actual changelog to be stored as a file packaged within the application, so I put the change log into res/raw/changelog.txt and res/raw-ru/changelog.txt (for Russian locale).

The code to display the contents of the change log looks like this:

	TextView textChangeLog = (TextView) findViewById(R.id.change_log);

	Resources resources = getResources();
	InputStream stream = resources.openRawResource(R.raw.changelog);

	try {
		InputStreamReader reader = new InputStreamReader(stream, "UTF-8");
		try {
			BufferedReader bufReader = new BufferedReader(reader);
			StringBuilder sb = new StringBuilder();
			String s;
			while ((s = bufReader.readLine()) != null) {
				sb.append(s);
				sb.append('\n');
			}
			textChangeLog.setText(sb);
		} catch (IOException x) {
		} finally {
			try {
				reader.close();
			} catch (IOException x) {
			}
		}
	} catch (UnsupportedEncodingException x) {
	}

The code is necessary to translate Windows style newlines into Java (Unix) style newlines. I could be more careful with creating the change log resource, but this way it works with whatever type of newlines is used in the changelog.

As you can infer from the above, raw resources get their own resource IDs (R.id.blah), and can be accessed with openRawResource.

In my application’s main activity, I have code to determine the version code (as specified in the manifest) of my application, and compare it with the value persistently stored in a SharedPrefernce:

// Somewhere in onStart():

	PackageManager packageManager = getPackageManager();
	String packageName = getPackageName();
	PackageInfo packageInfo;

	try {
		packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_META_DATA);

		int versionCode = packageInfo.versionCode;
		if (versionCode != mSettings.mOldVersionCode) {
			mSettings.mOldVersionCode = versionCode;
			mHandler.post(mShowChangeLogTask);
		}
	} catch (NameNotFoundException x) {
	}

The mSettings above is a simple wrapper around SharedPreferences that loads and stores settings and keeps them around as member variables.

But what is this part: mHandler.post(mShowChangeLogTask)?

I wanted to start the changelog activity with startActivityForResult, so that it has more of a “modal” feel to it. However, there was a problem: since I the code above is contained in onStart(), immediately invoking another activity caused the main activity to not redraw until after the user closes the change log activity. This looked pretty ugly. To avoid this problem, I made sure that Android finishes redrawing the main activity before launching the changelog activity.

// in main activity:

	private Handler mHandler = new Handler();
	private Runnable mShowChangeLogTask = new Runnable() {
		@Override
		public void run() {
			Intent intent = new Intent(WifiControlActivity.this, ChangeLogActivity.class);
			startActivityForResult(intent, Util.REQUEST_CHANGELOG);
		}
	};

The code above fixes the problem – by allowing the main activity to redraw before the changelog activity is displayed.

Finally, to make sure that the changelog is displayed just once for each new version code, I added code in the main activity’s onPause to save the updated “last known” version code (mSettings.mOldVersionCode) to shared preferences.

Advertisements
Categories: android, ui

Showing progress in standard window title bar

September 23, 2010 10 comments

I wanted to show progress indication in my application’s title bar. Android already provides a way to do this, but uses a bar-style progress indicator, whereas I wanted an “indeterminate” progress, a rotating “thing” seen in many applications.

At the same time, I wanted to only add my own progress indicator on the right, without changing the look of the title bar, even if it’s customized by the phone manufacturer.

November 22, 2010 – update.

There is an easier way to display the progress wheel, please read this post.

To do this, I needed to provide a custom title bar layout. This blog post shows the basics, but, as with other posts, focuses on creating a title bar that looks completely different from the default (which may be customed depending on the phone).

The default title bar layout can be found in the Android sdk, under platforms\android-X\data\res\layout\screen.xml. Interestingly enough, it already has a “circle” style progress bar, but its id not exported and so can’t be used by applications.

The default title bar layout uses a style for the title bar text: windowTitleStyle. This style can be referenced from applications as style="?android:attr/windowTitleStyle".

Another style we need is progressBarStyleSmallTitle, which is used for the non-accessible progress bar in the standard window title bar mentioned above.

Combining these two, we end up with this layout (save it as main_title_with_progress.xml):

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 	xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="fill_parent"
 	android:layout_height="wrap_content"
 	android:fitsSystemWindows="true">
	<TextView
 		android:id="@android:id/title"
 		style="?android:attr/windowTitleStyle"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_alignParentLeft="true"
 		android:text="@string/app_name"/>
	<ProgressBar
 		android:id="@+id/title_progress_bar"
		style="?android:attr/progressBarStyleSmallTitle"
 		android:layout_width="wrap_content"
 		android:layout_height="wrap_content"
 		android:layout_alignParentRight="true"
 		android:layout_centerVertical="true"
 		android:visibility="invisible"
 		android:indeterminateOnly="true"/>
</RelativeLayout>

To use this layout, we need to do this in the activity’s onCreate:

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Window window = getWindow();
		window.requestFeature(Window.FEATURE_CUSTOM_TITLE);
		// setContentView goes here
		window.setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.main_title_with_progress);
		// get a reference to the progress bar
		mTitleProgressBar = (ProgressBar) findViewById(R.id.title_progress_bar);
	}
	private ProgressBar mTitleProgressBar;

Note that we need to request the use of a custom title bar before setting the content view, and set the actual layout for the title bar after setting the content view.

To use the progress bar, we can just show and hide the progress bar:

	// show the progress circle
	mTitleProgressBar.setVisibility(View.VISIBLE);
	// hide the progress circle
	mTitleProgressBar.setVisibility(View.INVISIBLE);

And that’s really it.

Categories: android, ui

Per-widget options, stale widgets

June 22, 2010 9 comments

A feature often requested by users of BluetoothWidget and Wifi Manager is to be able to show widgets without text lables below. This is how I implemented this feature (not released as of yet).

Implementing the UI for configuring text label option is quite easy, all it takes is a configuration activity associated with a widget.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="70dp"
    android:minHeight="72dp"
    android:updatePeriodMillis="3600000"
    android:initialLayout="@layout/appwidget"
    android:configure="org.kman.BluetoothWidget.BTWidgetConfigure" 
     >
</appwidget-provider>

Next, we need some place to store per-widget configuration. Putting data in AppWidgetProvider doesn’t work, since it can be destroyed and recreated by Android at any time. This happens not only in low memory situation, but also when memory is plentiful.

My solution was to store per-widget settings using SharedPreferences, keyed by the widget id. The widget id is available in the config activity, and is also passed to your AppWidgetProvier’s onUpdate.

	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
		Log.i(TAG, "onUpdate, appWidgetIds.length = " + String.valueOf(appWidgetIds.length));

		final int N = appWidgetIds.length;

		// For each App Widget that belongs to this provider
		for (int i = 0; i < N; i++) {
			int widgetId = appWidgetIds[i];

			// Load widget prefs
			WidgetPrefs prefs = new WidgetPrefs();
			prefs.load(context, widgetId);

			// Build view update
			RemoteViews updateViews = buildUpdate(context, widgetId, prefs);

			// Tell the AppWidgetManager to perform an update
			appWidgetManager.updateAppWidget(widgetId, updateViews);
		}
	}

WidgetPrefs is a simple class that loads per-widget settings based on the widgetId passed in:

public class WidgetPrefs {

	public final static String PREFS_KEY = "BluetoothWidget";
	
	public final static String SHOW_LABEL_KEY = "ShowLabel";
	public final static boolean SHOW_LABEL_DEFAULT = true;

	public final static String WIDGET_PRESENT_KEY = "WidgetPresent";
	public final static boolean WIDGET_PRESENT_DEFAULT = false;	

	public boolean mShowLabel;

	public WidgetPrefs() {
		mShowLabel = true;
	}

	public boolean load(Context context, int widgetId) {
		SharedPreferences prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
		if (prefs != null) {
			String showLabelKey = SHOW_LABEL_KEY + String.valueOf(widgetId);
			mShowLabel = prefs.getBoolean(showLabelKey, SHOW_LABEL_DEFAULT);
			
			String widgetPresentKey = WIDGET_PRESENT_KEY + String.valueOf(widgetId);
			return prefs.getBoolean(widgetPresentKey, WIDGET_PRESENT_DEFAULT);
		}
		return false;
	}
}

There is a corresponding store method, that’s called from the configuration activity to store settings for this widgetId.

So far so good, but what is WIDGET_PRESENT_KEY and why does load return a boolean?

In my widgets, I often need to make updates based on system events (such as Bluetooth turning on and off, or Wifi connection changing). This is where I ran into an Android bug.

My first attempt looked like this:

	public static void updateAllWidgets(Context context, int toggleResId) {
		AppWidgetManager manager = AppWidgetManager.getInstance(context);
		ComponentName thisWidget = new ComponentName(context, BTWidget.class);
		int[] widgetIds = manager.getAppWidgetIds(thisWidget);

		for (int widgetId : widgetIds) {
			WidgetPrefs prefs = new WidgetPrefs();
			prefs.load(context, widgetId);

			Log.i(TAG, "Updating widgetId " + String.valueOf(widgetId));
			RemoteViews updateViews = buildUpdate(context, widgetId, prefs, toggleResId);
			manager.updateAppWidget(widgetId, updateViews);
		}
	}

This worked, but I was quite surprised to find out that manager.getAppWidgetIds(thisWidget) returned a really large list. It included not only the current widget id, but also Ids of widgets that were added to the home screen previously, and are long gone! For one of my widgets, this means 8 stale widget ids, for the other – 38.

Stale widget ids only appear when calling manager.getAppWidgetIds(thisWidget). The widget’s onUpdate method always has the correct widget ids – just the active ones.

So obvisouly I needed to do something about these stale widgets, since they were making my code very ineffecient.

The WIDGET_PRESENT_KEY setting value is used to determine if a given widgetId is really “live”. This value is set for a widget id when WidgetPrefs object is stored by the configuration activity, just before creating a new widget.

	public void store(Context context, int widgetId) {
		SharedPreferences prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
		if (prefs != null) {
			Editor edit = prefs.edit();

			if (edit != null) {
				String showLabelKey = SHOW_LABEL_KEY + String.valueOf(widgetId);
				edit.putBoolean(showLabelKey, mShowLabel);
				
				String widgetPresentKey = WIDGET_PRESENT_KEY + String.valueOf(widgetId);
				edit.putBoolean(widgetPresentKey, true);
				
				edit.commit();
			}
		}
	}

Then updateAllWidgets uses the boolean return value from WidgetPrefs.load to determine if there really is a widget with this id:

	public static void updateAllWidgets(Context context, int toggleResId) {
		AppWidgetManager manager = AppWidgetManager.getInstance(context);
		ComponentName thisWidget = new ComponentName(context, BTWidget.class);
		int[] widgetIds = manager.getAppWidgetIds(thisWidget);

		for (int widgetId : widgetIds) {
			WidgetPrefs prefs = new WidgetPrefs();
			if (prefs.load(context, widgetId)) {
				Log.i(TAG, "Updating widgetId " + String.valueOf(widgetId));
				RemoteViews updateViews = buildUpdate(context, widgetId, prefs, toggleResId);
				manager.updateAppWidget(widgetId, updateViews);
			}
		}
	}

If the user starts adding a widget, and cancels the configuration activity, the preferences for this widget are never stored (including the WIDGET_PRESENT_KEY), but the widgetId is saved by Android and will be included by manager.getAppWidgetIds(thisWidget). However, in updateAllWidgets, prefs.load returns false and this widget id is skipped.

When the user removes a widget from the home screen, we need to remove its preferences, including the WIDGET_PRESENT_KEY flag.

In the AppWidgetProvider subclass:

	@Override
	public void onDeleted(Context context, int[] appWidgetIds) {
		Log.i(TAG, "onDeleted, appWidgetIds.length = " + String.valueOf(appWidgetIds.length));

		for (int widgetId : appWidgetIds) {
			WidgetPrefs.delete(context, widgetId);
		}
	}

WidgetPrefs:

	public static void delete(Context context, int widgetId) {
		SharedPreferences prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
		if (prefs != null) {
			Editor edit = prefs.edit();

			if (edit != null) {
				String showLabelKey = SHOW_LABEL_KEY + String.valueOf(widgetId);
				edit.remove(showLabelKey);
				
				String widgetPresentKey = WIDGET_PRESENT_KEY + String.valueOf(widgetId);
				edit.remove(widgetPresentKey);
				edit.commit();
			}
		}
	}

This is pretty much it.

Categories: android, ui

Widgets and orientation changes

May 23, 2010 5 comments

The widget in WiFi Manager initially didn’t work correctly on the Moto Milestone after orientation changes (fixed in 1.3 and later).

The main problem was: after sliding out the keyboard, widget views were reset to their original state (as in the layout file). The widget didn’t respond to clicks, either.

This is what I found out while fixing this issue:

My Investigations

  • When orientation changes, it’s not possible for AppWidgetProvider to be notified. onUpdate is not called, and neither is any other notification method.
  • The widget’s “real” views hosted in home screen process are destroyed and recreated (as uaual, from xml layout file).
  • Android correctly uses appropriate -land or -port layout for the previous step.
  • After the layout is inflated, Android uses the most recent RemoteViews object (which it saves somewhere) to bring your widget up to date.

My recommendations

  • Create separate layouts for -port and -land orientations.
  • Orientation-dependent data presentation differences should be handled by your widget’s layouts (i.e. TextView ellipsize), not on the AppWidgetProvider side (i.e. measuring and trimming text)
  • This is the most important: make sure each RemoteViews object you push to the widget completely specifies widget state, including states of all views and all PendingIntent‘s.
Categories: android, ui

Making a launcher shortcut-like widget

April 20, 2010 2 comments

So I am making a widget, and i want i to blend in nicely with other graphics on the home screen. Most small graphics items on my home screen are launcher shortcuts. So, naturally, I want my widget to look similar, including a small text label below.

Now, Android team tells us this is a bad idea.

Android Widget design guidelines sort of tell you not to.

Android sample widget (Wikipedia “Word of the day”) doesn’t look like launcher icons.

But still – in my personal opinion – small widgets that look different from launcher icons just don’t blend in. So I’ve decided to take the risks and make my widget look like a launcher icon.

This is what I ended up with for my Wifi widget, I think it blends in quite nicely (note, this is work in progress).

Here is how this widget was created:

  • First, I defined a drawable for the text label’s background. Note that it’s a rounded rectangle with 9dp corner radius and some padding for text on both sides
  • <!-- drawable/drawable_corners.xml -->
    
    <?xml version="1.0" encoding="utf-8"?>
    
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
    	android:shape="rectangle">
    	<solid android:color="#aa222222" />
    	<corners android:radius="9dip" />
    	<padding android:left="6dp" android:right="6dp" />
    </shape>
    
  • Second, I have a background image that’s 220 by 57 pixels. This is 3 by 1 tiles, again, non-standard, but this is what I wanted for this widget.
  • Finally, this is the widget layout
  • <!-- layout/appwidget.xml -->
    
    <?xml version="1.0" encoding="utf-8"?>
    
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    	android:layout_width="220dp" android:layout_height="wrap_content"
    	android:orientation="vertical">
    
    	<AbsoluteLayout android:id="@+id/wifi_widget"
    		android:layout_marginTop="5dp" android:layout_width="fill_parent"
    		android:layout_height="57dp" android:background="@drawable/widget_bg">
              <!-- Views for the top part of the wiget are defined here -->
         </AbsoluteLayout>
    	<RelativeLayout android:layout_marginTop="4dp"
    		android:layout_width="fill_parent" android:layout_height="wrap_content">
    		<TextView android:id="@+id/WidgetLabel" android:layout_width="wrap_content"
    			android:layout_height="18dp" android:layout_centerHorizontal="true"
    			android:text="@string/app_name" android:textColor="#ffffff"
    			android:textSize="13dip" android:singleLine="true" android:ellipsize="end"
    			android:background="@drawable/drawable_corners" />
    	</RelativeLayout>
    </LinearLayout>
    

    Things to note:

  • I am using a vertically-oriented LinearLayout as the top building block.
  • Views that appear on top of the graphical part of the widget are laid out using AbsoluteLayout. Maybe I’ll work out a better way later.
  • The widget label is a TextView that uses the rounded-rectanlg drawable .xml file from up above.
  • It’s important to get spacing right, that’s why the graphical part of the widget has android:layout_marginTop="5dp", and the text label’s RelativeLayout has android:layout_marginTop="4dp".
  • Speaking of which, the RelativeLayout‘s function is to allow the text label to be horizontally centered
  • I think that’s it.

    Categories: android, ui

    Compact spinner widgets

    April 19, 2010 6 comments

    My application uses a couple of spinner widgets. With limited screen space, they just seemed too large. I tried various ways to make them smaller, like setting padding and layout margins to small values, including zero, but that didn’t help. The empty space around the spinner stayed there no matter what, I was just unable to make it go away.

    Then I had the idea to try and change the background. This turned out to be what was needed – it’s the background that defines a spinner’s size, and what looked like margins was just empty pixels in the background image.

    So here are my background .png files:

    Use them like this:

  • Create an xml file describing normal and pressed states for the background
  • <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
         <item android:state_pressed="true"
               android:drawable="@drawable/slim_spinner_pressed" />
         <item android:drawable="@drawable/slim_spinner_normal" />
     </selector>
    
  • Use the created background descriptor when declaring spinner in your layout
  • <Spinner ..... android:background="@drawable/selector_slim_spinner" ..... />
    

    This is a screenshot showing my “slim” spinners side-by-side with the regular ones (HTC Hero):

    My slim spinners vs. stock ones

    BTW: Digging through Android 1.5 sources, I found a .9.png file that is apparently the stock spinner background. It seems even larger than those shown above. So apparently, HTC had the same idea when tweaking Android for the Hero… they just didn’t go as far.

    Categories: android, ui

    Processing Widget events with PendingIntents

    April 10, 2010 1 comment

    Widgets are not just for displaying data. As simple as they are supposed to be, basic interaction with the user is still commonplace – initiated by the user’s click on the widget or one of its elements.

    The normal way of responding to clicks is by using setOnClickListener, like this:

    protected void onCreate(Bundle savedInstanceState) {
    .....
        final Button buttonStartScan = (Button) findViewById(R.id.button_start_scan);
        buttonStartScan.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                ....
        }
    });
    

    However, in a Widget (a subclass of AppWidgetProvider), event handling can’t be done the same way.

    First of all, a widget’s views exist in a separate process from its AppWidgetProvider subclass, therefore it’s not possible to obtain widget’s view objects by calling findViewById, not to mention connecting that object with an event handler in our process.

    Secondly, a widget’s AppWidgetProvider is stateless – its instances are not guaranteed to be kept around for as long as the widget appears on screen. These objects can be destroyed to save memory and re-created later as necessary.

    This is where PendingIntent can be useful. According to the documentation:

    … instances of this class are created in a certain way, and can be handed to other applications so that they can perform the action you described on your behalf at a later time. A PendingIntent itself is simply a reference to a token maintained by the system. If its owning application’s process is killed, the PendingIntent itself will remain usable from other processes that have been given it.

    So PendingIntent solves two problems described above: cross-process notifications, and AppWidgetProvider‘s transient nature.

    Now some examples:

  • Launching an activity in response to a click
  • public RemoteViews buildUpdate(Context context) {
        RemoteViews updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_id);
    
        // create an intent that describes activity launch
        Intent appIntent = new Intent(context, MyWidgetActivity.class);
    
        // convert to PendingActivity which can be pushed to the actual view inside the widget
        PendingIntent pendingAppIntent = PendingIntent.getActivity(
                                       context, 0, appIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        updateViews.setOnClickPendingIntent(R.id.desired_view_id, pendingAppIntent);
    
        return updateViews;
    }
    

    An instance of PendingIntent is created with the FLAG_UPDATE_CURRENT. This is so Android maintains just one instance of this intent.

  • Getting arbitrary view click notifications
  • This is just a little more involved. We’ll be broadcasting an intent from the widget, and receiving notifications in its provider.
    First, we need to come up with a string name for the broadcast intent. Let’s choose one: “org.myname.widget.MYCLICK_ACTION”.

    Second, mark our AppWidgetProvider as a receiver of this intent:

    <receiver android:name=".MyWidget" android:label="@string/widget_name">
        <intent-filter>
            <action android:name="org.myname.widget.MYCLICK_ACTION" />
    ................
    

    Third, create a PendingIntent and register it with a view.

    public RemoteViews buildUpdate(Context context) {
        RemoteViews updateViews = new RemoteViews(context.getPackageName(), R.layout.widget_id);
    
        Intent actionIntent = new Intent("org.myname.widget.MYCLICK_ACTION");
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
            actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        updateViews.setOnClickPendingIntent(R.id.a_certain_view, pendingIntent);
    

    Finally, add code to respond to the broadcast:

    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action.equals("org.myname.widget.MYCLICK_ACTION")) {
            // do something
        }
    }
    

    That’s about it. As can be seen, by using a PendingIntent it’s possible to create widgets that allow simple user interaction.

    Categories: android, ui