Archive

Archive for the ‘Mobile’ Category

Quotation Content Provider

March 23, 2013 Leave a comment

It’s time to have my quotation content provider to take direct advantage of Freebase’s . Here are a list of provider requirements:

  1. It must always have content to provide. It cannot rely on fetching content via a network. It therefore must have some initialization data when the application is installed. 
  2. As quotations are consumed the provider will replace those quotations with new ones from Freebase.

This iteration will implement the initial connections to Freebase. Later version will refine it.

The provider will use Android’s SQLite to cache the content. I’ll start out with one table that holds the quotation, author’s name, and URL to author’s image. Later, as more data is needed, I’ll add additional tables and normalize the model.

After taking a look at android.provider.ContactsContract I decided to mimic this pattern. QuotationContract describes the  quotation table and how to use a Uri to reference a row in the table.

Every quotation in Freebase has a unique id so we’ll use that id as the primary key in the table. This means the Uri for a quotation is the authority Uri plus the quotation table name plus the free base id. This Uri becomes the standard way to identify a quotation. It will be used throughout the code. For example …

content://org.bwgz.quotation/quotation/quotationsbook/quote/8185

references a quotation from Ralph Waldo Emerson.

A foolish consistency is the hobgoblin of little minds, adored by little statesmen and philosophers and divines.

public final class QuotationContract {
	public static final String AUTHORITY = "org.bwgz.quotation";
	public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);

	protected interface StatusColumns {
		public enum State {
			UNINITIALIZED(0), INITIALIZED(1);

		    private final int value;
		    State(int value) {
		        this.value = value;
		    }
		    public int getValue() {
		    	return value;
		    }
		}

		public static final String STATE		= "state";
		public static final String MODIFIED		= "modified";
	}

	protected interface QuotationColumns {
		public static final String QUOTATION	= "quotation";
		public static final String AUTHOR_NAME	= "author_name";
		public static final String AUTHOR_IMAGE	= "author_image";
	}

	public static class Quotation implements BaseColumns, StatusColumns, QuotationColumns {
		public static final String TABLE = "quotation";

		private Quotation() {
		}

		public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, TABLE);

		public static Uri getUri(String id) {
			return Uri.parse(CONTENT_URI + id);
		}

		public static String getId(Uri uri) {
	    	return uri.getPath().substring(Quotation.CONTENT_URI.getPath().length(), uri.getPath().length());
		}

		/**
		 * The MIME type of {@link #CONTENT_URI} providing a directory of
		 * quotations.
		 */
		public static final String CONTENT_TYPE = "vnd.android.cursor.dir/quotation";

		/**
		 * The MIME type of a {@link #CONTENT_URI} a single quotation.
		 */
		public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/quotation";

	}
}

QuotationSQLiteHelper, an extension of SQLiteOpenHelper, provides methods to create the database, initialize it, and CRUD operations.

QuotationSQLiteHelper, an extension of SQLiteOpenHelper, provides methods to create the database, initialize it, and CRUD operations. When the database is created it is also initializes the quotation table from a CSV file containing quotes and their Freebase id.  The author name and image URL are not initialized. We’ll use a lazy fetch to get those when needed. The state column is set to uninitialized because those fields aren’t set.

QuotationSyncAdapter.onPerformSync now expects to be passed the Uri of quotation requiring synchronization. It parses the Freebase quotation id from the Uri, builds a Freebase query, and executes it. If the query is successful it will update the table using a batch operation that calls the provider’s update method. If that update is successful the provider will notify any objects that had requested notification of changes to that Uri.

	private MQLQueryBuilder builder = new MQLQueryBuilder();

	private Quotation findQuotationFromFreebase(String id) {
		Quotation quotation = new Quotation();

		quotation.setId(id);
		String query = builder.createQuery(Quotation.class, null, quotation);
		Log.d(TAG, String.format("query: %s", query));

		FreebaseQuery fbQuery = new FreebaseQuery();
		org.bwgz.freebase.model.Quotation result = fbQuery.getResult(query, Quotation.class);
		Log.d(TAG, String.format("result: %s\n", result));

		return result;
	}

	@Override
	public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
		Log.d(TAG, String.format("onPerformSync - account: %s  extras: %s  authority: %s  provider: %s  syncResult: %s", account, extras, authority, provider, syncResult));

		for (String key : extras.keySet()) {
			Log.d(TAG, String.format("%s: %s", key, extras.get(key)));
		}

		String string = extras.getString(SYNC_EXTRAS_QUOTATION_UPDATE);
		if (string != null) {
			Uri uri = Uri.parse(string);
	    	String _id = org.bwgz.quotation.content.provider.QuotationContract.Quotation.getId(uri);
			Log.d(TAG, String.format("_id: %s", _id));

			Quotation quotation = findQuotationFromFreebase(_id);
			if (quotation != null) {
				ArrayList operationList = new ArrayList();

				ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(uri);
				builder.withValue(org.bwgz.quotation.content.provider.QuotationContract.Quotation.QUOTATION, quotation.getName());
				builder.withValue(org.bwgz.quotation.content.provider.QuotationContract.Quotation.AUTHOR_NAME, quotation.getAuthor().getName());
				builder.withValue(org.bwgz.quotation.content.provider.QuotationContract.Quotation.AUTHOR_IMAGE, quotation.getAuthor().getId());
				builder.withSelection("_id = ?", new String[] { _id });
				operationList.add(builder.build());

				try {
					getContext().getContentResolver().applyBatch(QuotationContract.AUTHORITY, operationList);
				} catch (RemoteException e) {
					e.printStackTrace();
				} catch (OperationApplicationException e) {
					e.printStackTrace();
				}
			}
		}
	}

When QuoteActivity resumes it determine which quotation, using the quotation’s Uri, it should be displaying. If the quotation has not be initialized it will:

  1. Display a “waiting” message.
  2. Register a ContentObserver for the Uri.
  3. Request that the content provider update (sync) the quotation.

The sync request is non-blocking. The onResume method returns and the application continues to run normally. Meanwhile, the sync request runs in the background and if successful the ContentObserver (QuotationContentObserver) will be notified by the provider when the table has been updated. QuotationContentObserver then fetches the quotation from the database and updates the display accordingly.

	class QuotationContentObserver extends ContentObserver {
		private Uri uri;

		public QuotationContentObserver() {
	        super(new Handler());
	    }

		public Uri getUri() {
			return uri;
		}

		public void setUri(Uri uri) {
			this.uri = uri;
		}

	    @Override
	    public void onChange(boolean selfChange) {
	        super.onChange(selfChange);
	        Log.d(TAG, "QuoteContentObserver.onChange( " + selfChange + ")");

	        String quotation = getQuotation(uri);
	        if (quotation != null) {
	        	setQuote(quotation);
	        }

	        String author = getAuthor(uri);
	        if (author != null) {
	        	setAuthor(author);
	        }

	        String authorImage = getAuthorImage(uri);
	        if (authorImage != null) {
	        	setAuthorImage(authorImage);
	        }
	    }
	}

    ...

    protected void onResume() {
    	super.onResume();
		Log.d(TAG, String.format("onResume"));

		Intent intent = getIntent();
		Uri uri = intent.getData();
		Log.d(TAG, String.format("intent: %s  uri: %s", intent, uri));

		if (uri == null) {
			uri = getRandomQuotationUri();
			Log.d(TAG, String.format("random uri: %s", uri));
		}

		if (isQuotationInitialized(uri)) {
			setQuote(getQuotation(uri));
			setAuthor(getAuthor(uri));
	        setAuthorImage(getAuthorImage(uri));
		}
		else {
			quotationObserver.setUri(uri);
			getContentResolver().registerContentObserver(uri, false, quotationObserver);

			setQuote("Waiting for quotation to load ...");
			setAuthor("");

			Bundle extras = new Bundle();
	        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
	        extras.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false);
	        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
	        extras.putString(QuotationSyncAdapter.SYNC_EXTRAS_QUOTATION_UPDATE, uri.toString());
			ContentResolver.requestSync(new QuotationAccount(), QuotationContract.AUTHORITY, extras);
		}
    }

The quote widget works similarly. Now the user clicks on the quote widget it will send an intent containing the quote’s Uri to the quote activity. The activity provides further details such as the author’s name and image.

device-2013-03-22-220229 device-2013-03-22-220213

Freebase also provides an image service. The application fetches the author’s image rather than trying to store it. The fetch is done in the background using an AsyncTask.

public class QuoteActivity extends Activity {

        ...

	public class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
	    ImageView bmImage;

	    public DownloadImageTask(ImageView bmImage) {
	        this.bmImage = bmImage;
	    }

	    protected Bitmap doInBackground(String... urls) {
	        String urldisplay = urls[0];
	        Bitmap bitmap = null;
	        try {
	            InputStream in = new java.net.URL(urldisplay).openStream();
	            bitmap = BitmapFactory.decodeStream(in);
	        } catch (Exception e) {
	            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
	        }
	        return bitmap;
	    }

	    protected void onPostExecute(Bitmap result) {
	        bmImage.setImageBitmap(result);
	    }
	}

        ...

	public void setAuthorImage(String image) {
		Log.d(TAG, String.format("setAuthorImage - image: %s", image));
		ImageView imageView = (ImageView) findViewById(R.id.image);
		Uri uri = Uri.parse("https://usercontent.googleapis.com/freebase/v1/image" + image + "?maxwidth=200&maxheight=200&pad=true");
		Log.d(TAG, String.format("setAuthorImage - uri: %s", uri));
		new DownloadImageTask(imageView).execute(uri.toString());
	}

       ....
}
Advertisements

Fortune Files

February 8, 2013 Leave a comment

Creating a quote of the day application requires quotes. So where does one go to get a lots of free quotes that are actually interesting. One place is a fortune file.

A fortune file contains a set of quotes similar to those found in a fortune cookie. Fortune files comprise the underlying database of the fortune program which appeared over 30 years with the release of Version 7 UNIX.

Since then creating and publishing fortune files has become a cottage industry. A quick search of the web will turn up numerous fortune files reflecting a wide variety genres. I download one from here. I contains over 9000 quotes. Next, how to incorporate it into my application?

The first step invokes preparing the fortune file for quick random lookup. A fortune file is a text file. Each line in the file contains a unique quote.

Rocks might teach us life's secrets, were it not for the language barrier.
Romance addiction is an invention of Western culture. — Anne Schaeff

The quote of the day widgets will random choose one of the quotes from the fortune file each day. This adds the following application requirements:

  • The widgets and activity need a common way to get the current day’s quote.
  • Each day the widgets need to be updated with new quotes.

An Android Service will provide an method to get quotes.

QuoteService extends IntentService. 

IntentService is a base class for Services that handle asynchronous requests (expressed as Intents) on demand. Clients send requests through startService(Intent) calls; the service is started as needed, handles each Intent in turn using a worker thread, and stops itself when it runs out of work.

This “work queue processor” pattern is commonly used to offload tasks from an application’s main thread. The IntentService class exists to simplify this pattern and take care of the mechanics. To use it, extend IntentService and implement onHandleIntent(Intent). IntentService will receive the Intents, launch a worker thread, and stop the service as appropriate.

All requests are handled on a single worker thread — they may take as long as necessary (and will not block the application’s main loop), but only one request will be processed at a time.

The initial version of QuoteService reads the Fortune file that’s been packaged as a raw resource and caches the quotes in an array. Not a great final solution but sufficient for now.

QuoteService.java

public class QuoteService extends IntentService {
	static private String TAG = QuoteService.class.getSimpleName();

	private final IBinder binder = new QuoteBinder();

	public class QuoteBinder extends Binder {
		public QuoteService getService() {
            	return QuoteService.this;
		}
	}

	private final Random random = new Random();

	private String[] quotes = { "Never put off until tomorrow what you can do the day after tomorrow. -- Mark Twain" };

	public String[] getQuotes() {
		return quotes;
	}

	public void setQuotes(String[] quotes) {
		this.quotes = quotes;
	}

	public String getQuote(int index) {
		return quotes != null ? quotes[index] : null;
	}

	public String qetRandomQuote() {
		return getQuote(random.nextInt(quotes.length));
	}

	public QuoteService() {
		super(TAG);
		Log.d(TAG, String.format("QuoteService"));
	}

	@Override
	public IBinder onBind(Intent intent) {
		Log.d(TAG, String.format("onBind - intent: %s", intent));
		return binder;
	}

	@Override
	public void onCreate() {
		Log.d(TAG, String.format("onCreate"));

		InputStream inputStream = this.getResources().openRawResource(R.raw.fortunes);
		Reader reader = new InputStreamReader(inputStream);
		BufferedReader in = new BufferedReader(reader);

		List<String> list = new ArrayList<String>();
		try {
			String string;

			while ((string = in.readLine()) != null) {
				Log.d(TAG, String.format("string: %s", string));
				list.add(string);
			}
			in.close();
		} catch (IOException e) {
		}

		quotes = list.toArray(new String[list.size()]);
		Log.d(TAG, String.format("quotes read: %d", quotes.length));
	}

	@Override
	public void onDestroy() {
		Log.d(TAG, String.format("onDestroy"));
	}

	@Override
	protected void onHandleIntent(Intent intent) {
		Log.d(TAG, String.format("onHandleIntent - intent: %s", intent));
	}
}

When QuoteActivity starts it binds to QuoteService. When the asynchronous connection to the service is made QuoteActivity’s views are then be updated if currently empty. If QuoteActivity is started with an Intent that contains a quote then that quote will be display. This allows QuoteActivity to be launched from a widget. The widget’s quote is passed along. Right now QuoteActivity just reproduces the quote. Later it will display more more information related to that quote.

QuoteActivity.java

public class QuoteActivity extends Activity {
	static private String TAG = QuoteActivity.class.getSimpleName();
	static public String QUOTE = "org.bwgz.qotd.activity.QuoteActivity.QUOTE";

	private QuoteService service;
	private String quote;
	private boolean connected = false;

	public boolean isConnected() {
		return connected;
	}

	public void setConnected(boolean connected) {
		this.connected = connected;
	}

	public String getQuote() {
		return quote;
	}

	public QuoteService getService() {
		return service;
	}

	public void setService(QuoteService service) {
		this.service = service;
	}

	public void setQuote(String quote) {
		this.quote = quote;

    		TextView textView = (TextView)findViewById(R.id.quote);
    		textView.setText(quote);
	}

	private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className, IBinder binder) {
    		Log.d(TAG, String.format("onServiceConnected - className: %s  service: %s", className, binder));

            setConnected(true);
            setService(((QuoteBinder) binder).getService());

            if (getQuote() == null) {
            	setQuote(getService().qetRandomQuote());
            }

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
    		Log.d(TAG, String.format("onServiceDisconnected - name: %s", name));

            setConnected(false);
        }
	};

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.d(TAG, String.format("onCreate - savedInstanceState: %s", savedInstanceState));

        	setContentView(R.layout.activity_quote);
	}

    @Override
    protected void onStart() {
        super.onStart();
		Log.d(TAG, String.format("onStart"));

        bindService(new Intent(this, QuoteService.class), connection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onResume() {
	super.onResume();
	Log.d(TAG, String.format("onResume"));

	Intent intent = getIntent();
	String quote = intent.getStringExtra(QUOTE);
	Log.d(TAG, String.format("intent: %s  quote: %s", intent, quote));

	if (quote != null) {
		setQuote(quote);
	}
    }

    @Override
    protected void onPause() {
	super.onPause();
	Log.d(TAG, String.format("onPause"));
    }

    @Override
    protected void onStop() {
	super.onStop();
	Log.d(TAG, String.format("onStop"));

	if (isConnected()) {
            unbindService(connection);
            setConnected(false);
        }
    }

    @Override
    protected void onDestroy() {
	super.onDestroy();
	Log.d(TAG, String.format("onDestroy"));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
	MenuInflater inflater = getMenuInflater();
	inflater.inflate(R.menu.options, menu);
	return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
	if (item.getItemId() == android.R.id.home || item.getItemId() == 0) {
            return false;
	}
	if (item.getItemId() == R.id.developer) {
    	    Intent intent = new Intent(this, DeveloperActivity.class);
    	    startActivity(intent);
    	}

	return true;
    }
}

Things aren’t were a little more complicated with QuoteWidgetProvider. Android does not allow a BroadcastReceiver to bind to a Service.

Because AppWidgetProvider is an extension of BroadcastReceiver, your process is not guaranteed to keep running after the callback methods return (see BroadcastReceiver for information about the broadcast lifecycle). If your App Widget setup process can take several seconds (perhaps while performing web requests) and you require that your process continues, consider starting a Service in the onUpdate()method. From within the Service, you can perform your own updates to the App Widget without worrying about the AppWidgetProvider closing down due to an Application Not Responding (ANR) error.

I created QuoteOfTheDayService to act as a proxy for QuoteWidgetProvider. QuoteOfTheDayService does a few things:

  • It binds to QuoteService so that it can access the quotes.
  • It ensures all the widgets have a random quote.
  • Using a custom Handler it refreshes all the widgets with new random quotes when the current day rolls over.

QuoteOfTheDayService.java

public class QuoteOfTheDayService extends IntentService {
	static private String TAG = QuoteOfTheDayService.class.getSimpleName();
	static public String APP_WIDGET_IDS = "org.bwgz.qotd.service.QuoteOfTheDayService.APP_WIDGET_IDS";

        private QuoteService quoteService;
	private boolean connected = false;

	public QuoteService getQuoteService() {
		return quoteService;
	}

	public void setQuoteService(QuoteService quoteService) {
		this.quoteService = quoteService;
	}

        public boolean isConnected() {
		return connected;
	}

	public void setConnected(boolean connected) {
		this.connected = connected;
	}

	static private class AlarmHandler extends Handler {
    	private QuoteOfTheDayService qotdService;

		public AlarmHandler(QuoteOfTheDayService service) {
    		this.qotdService = service;
    	}

        @Override
        public void handleMessage(Message msg) {
            ComponentName widget = new ComponentName(qotdService, QuoteWidgetProvider.class);
            AppWidgetManager manager = AppWidgetManager.getInstance(qotdService);

            qotdService.updateAppWidgets(manager.getAppWidgetIds(widget));
            qotdService.setAlarm();
        }
    }

    private AlarmHandler handler = new AlarmHandler(this);

	private long getTest() {
		Calendar calendar = GregorianCalendar.getInstance();
		int year = calendar.get(Calendar.YEAR);
		int month = calendar.get(Calendar.MONTH);
		int day = calendar.get(Calendar.DAY_OF_MONTH);
		int hour = calendar.get(Calendar.HOUR_OF_DAY);
		int minute = calendar.get(Calendar.MINUTE);
		int second = calendar.get(Calendar.SECOND);

		calendar = new GregorianCalendar(year, month, day, hour, minute + 1, second);
		Log.d(TAG, String.format("next: %s", calendar));

		return calendar.getTimeInMillis();
	}

	@SuppressWarnings("unused")
	private long getMidnight() {
		Calendar calendar = GregorianCalendar.getInstance();
		int year = calendar.get(Calendar.YEAR);
		int month = calendar.get(Calendar.MONTH);
		int day = calendar.get(Calendar.DAY_OF_MONTH);

		calendar = new GregorianCalendar(year, month, day);
		Log.d(TAG, String.format("today: %s", calendar));
		calendar.roll(Calendar.DATE, true);
		Log.d(TAG, String.format("tomorrow: %s", calendar));

		return calendar.getTimeInMillis();
	}

	public void setAlarm() {
		long next = getTest();
		long now = System.currentTimeMillis();
		long delta = next - now;
		Log.d(TAG, String.format(" next: %d", next));
		Log.d(TAG, String.format("  now: %d", now));
		Log.d(TAG, String.format("delta: %d", delta));

		handler.sendMessageDelayed(handler.obtainMessage(), delta);
	}

	private ServiceConnection connection = new ServiceConnection() {
		@Override
		public void onServiceConnected(ComponentName name, IBinder binder) {
    		Log.d(TAG, String.format("onServiceConnected - name: %s  binder: %s", name, binder));

    		setConnected(true);

    		setQuoteService(((QuoteBinder) binder).getService());
    		Log.d(TAG, String.format("service: %s", getQuoteService()));

                ComponentName widget = new ComponentName(QuoteOfTheDayService.this, QuoteWidgetProvider.class);
                AppWidgetManager manager = AppWidgetManager.getInstance(QuoteOfTheDayService.this);

    		updateAppWidgets(manager.getAppWidgetIds(widget));
		}

		@Override
		public void onServiceDisconnected(ComponentName name) {
    		Log.d(TAG, String.format("onServiceDisconnected - name: %s", name));

    		setConnected(false);
		}
    };

	public QuoteOfTheDayService() {
		super(TAG);
	}

	private void updateAppWidgets(int[] appWidgetIds) {
		Log.d(TAG, String.format("updateWidgets - appWidgetIds: %s", appWidgetIds));

		if (appWidgetIds!= null) {
	        for (int appWidgetId : appWidgetIds) {
	    		Log.d(TAG, String.format("widgetId: %s", appWidgetId));
	    		String quote = getQuoteService() != null ? getQuoteService().qetRandomQuote() : new String();

	        	RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.widget_quote);
	        	remoteViews.setTextViewText(R.id.quote, quote);

	        	Intent intent = new Intent(this, QuoteActivity.class);
	        	intent.putExtra(QuoteActivity.QUOTE, quote);
	                PendingIntent pendingIntent = PendingIntent.getActivity(this, appWidgetId, intent, PendingIntent.FLAG_CANCEL_CURRENT);

	        	remoteViews.setOnClickPendingIntent(R.id.widget, pendingIntent);

	                AppWidgetManager manager = AppWidgetManager.getInstance(QuoteOfTheDayService.this);
	        	manager.updateAppWidget(appWidgetId, remoteViews);
	        }
		}
	}

	@Override
	public void onCreate() {
		Log.d(TAG, String.format("onCreate"));

		bindService(new Intent(this, QuoteService.class), connection, Context.BIND_AUTO_CREATE);
		setAlarm();
	}

	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		Log.d(TAG, String.format("onStartCommand - intent: %s  flags: %d  startId: %d", intent, flags, startId));

		int[] appWidgetIds = intent.getIntArrayExtra(APP_WIDGET_IDS);
		updateAppWidgets(appWidgetIds);

		return Service.START_NOT_STICKY;
	}

	@Override
	public void onDestroy() {
            Log.d(TAG, String.format("onDestroy"));

            if (isConnected()) {
                unbindService(connection);
                setConnected(false);
            }
	}

	@Override
	protected void onHandleIntent(Intent intent) {
	}

}

There were a few tricky bits. One was getting each widget to pass along its quote when clicked. I keep getting all the widgets pass the quote of the last widget in the list. Even though all the widgets has a different quote they same quote was passed with the Intent. The important parts was …

PendingIntent pendingIntent = PendingIntent.getActivity(this, appWidgetId, intent, PendingIntent.FLAG_CANCEL_CURRENT);

The documentation states …

public static PendingIntent getActivity (Context context, int requestCode, Intent intent, int flags, Bundle options)

requestCode Private request code for the sender (currently not used).

In fact you can specify the widget id in the requestCode parameter.

When QuoteWidgetProvider starts QuoteOfTheDayService  it passes the id’s of the widgets needing updating.  Here’s what it looks link now.

QuoteWidgetProvider.java

 public class QuoteWidgetProvider extends AppWidgetProvider {
     static private String TAG = QuoteWidgetProvider.class.getSimpleName();

     @Override
     public void onEnabled(Context context) {
         Log.d(TAG, String.format("onEnabled  - context %s", context));
     }

     @Override
     public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
         Log.d(TAG, String.format("onUpdate - context %s  appWidgetManager: %s  appWidgetIds: %s", context, appWidgetManager, appWidgetIds));

Intent intent = new Intent(context, QuoteOfTheDayService.class);
         intent.putExtra(QuoteOfTheDayService.APP_WIDGET_IDS, appWidgetIds);
         context.startService(intent);
     }

     @Override
     public void onDeleted(Context context, int[] appWidgetIds) {
         Log.d(TAG, String.format("onDeleted - context %s  appWidgetIds: %s", context, appWidgetIds));
     }

     @Override
     public void onDisabled (Context context) {
         Log.d(TAG, String.format("onDisabled   - context %s", context));

         context.stopService(new Intent(context, QuoteOfTheDayService.class));
     }
 }
 

The code is available at GitHub.

Categories: Android, Bruce's Posts, Java

What I’m I Working With

February 6, 2013 Leave a comment

Supporting a variety of Android devices can be challenging. To begin with there are currently 17 versions of Android. Then there’s the wide variety of screen sizes and densities. When developing an Android application it would be nice to quickly and easily understand the device’s properties.  I decided to created a simple library which displayed the following property sets:

  •  Display – General information about a display, such as its size, density, and font scaling.
  • OS – Information about the current build, extracted from system properties.
  • System – System related information.

With this library in place my application can invoke it from a menu option that is available while I develop it.

I created an Fragment sub-class to be used within the various Activities. The fragment contains a ListView and requires a ListAdapter and List from its sub-classes.

SimpleListViewFragment.java

public abstract class SimpleListViewFragment extends Fragment {
    abstract protected ListAdapter getAdapter(Context context);

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_simplelistview, container, false);

        ListView screenPropertiesView = (ListView) view.findViewById(R.id.listView);
        screenPropertiesView.setAdapter(getAdapter(container.getContext()));

        return view;
    }
}

fragment_simplelistview.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >
    </ListView>

</LinearLayout>

Next is an other abstract class SimpleTwoLineListViewFragment which provides the adapter and requires the list from the sub-class.

SimpleTwoLineListViewFragment.java

public abstract class SimpleTwoLineListViewFragment extends SimpleListViewFragment {
	abstract protected List getList();

	protected ArrayAdapter getAdapter(Context context) {
		return new ArrayAdapter(context, android.R.layout.simple_list_item_2, getList()){
	        @Override
	        public View getView(int position, View convertView, ViewGroup parent){
	            TwoLineListItem view;
	            if(convertView == null){
	                LayoutInflater inflater = (LayoutInflater)parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	                view = (TwoLineListItem)inflater.inflate(android.R.layout.simple_list_item_2, null);
	            }else{
	                view = (TwoLineListItem)convertView;
	            }
	            T data = getItem(position);
	            view.getText1().setText(data.getName());
	            view.getText2().setText(data.getValue());

	            return view;
	        }
	    };
	}
}

Finally, there are the three classes DisplayPropertiesFragmentOSPropertiesFragment, and SystemPropertiesFragment which provide the properties in a List the properties.

DisplayPropertiesFragment.java

public class DisplayPropertiesFragment extends SimpleTwoLineListViewFragment {
	private String getDensityString(int density) {
		String string;

		if (density <= DisplayMetrics.DENSITY_LOW) {
			string = "low";
		}
		else if (density <= DisplayMetrics.DENSITY_MEDIUM) {
			string = "medium";
		}
		else if (density <= DisplayMetrics.DENSITY_TV) {
			string = "tv";
		}
		else if (density <= DisplayMetrics.DENSITY_HIGH) {
			string = "high";
		}
		else {
			string = "xhigh";
		}

		return string;
	}

	@Override
	protected List getList() {
		List list = new ArrayList();

		DisplayMetrics metrics = getResources().getDisplayMetrics();

		list.add(new TwoLineData("Width (pixels)", Integer.toString(metrics.widthPixels)));
		list.add(new TwoLineData("Height (pixels)", Integer.toString(metrics.heightPixels)));
		list.add(new TwoLineData("Density", Double.toString(metrics.density)));
		list.add(new TwoLineData("Density DPI", String.format("%d (%s)", metrics.densityDpi, getDensityString(metrics.densityDpi))));
		list.add(new TwoLineData("Scaled Density", Double.toString(metrics.scaledDensity)));
		list.add(new TwoLineData("xdpi", Double.toString(metrics.xdpi)));
		list.add(new TwoLineData("ydpi", Double.toString(metrics.ydpi)));

		return list;
	}

Android introduced fragments in Android 3.0 (API level 11) . I’ve got a level 10 device and what my application to support that.  Fortunately Android provides a Support Library which will add API’s not available for older platform versions.

To use these fragments I created DeveloperFragmentActivity. This class is designed to be called by an Intent with an extra string defining the name of the fragment class to instantiate and embedded (via Fragment.replace()).

DeveloperFragmentActivity.java

public class DeveloperFragmentActivity extends FragmentActivity {

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_developer);

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
    	String fragmentClass = this.getIntent().getStringExtra("fragment");

        try {
			Fragment fragment = (Fragment) Class.forName(fragmentClass).newInstance();

	        fragmentTransaction.replace(R.id.mainFragement, fragment);
	        fragmentTransaction.commit();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
    }
}

DeveloperActivity and DeveloperPropertiesFragment bring it all together.

DeveloperActivity.java

public class DeveloperActivity extends FragmentActivity {

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_developer);

    FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();

    Fragment fragment = new DeveloperPropertiesFragment();
    fragmentTransaction.replace(R.id.mainFragement, fragment);
    fragmentTransaction.commit();
    }
}

DeveloperPropertiesFragment.java

class ListData {
private String title;
private String description;
private String fragment;

public ListData(String title, String description, String fragment) {
this.setTitle(title);
this.setDescription(description);
this.setFragment(fragment);
}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getDescription() {
		return description;
	}

	public void setDescription(String description) {
		this.description = description;
	}

	public String getFragment() {
		return fragment;
	}

	public void setFragment(String fragment) {
		this.fragment = fragment;
	}
}

public class DeveloperPropertiesFragment extends SimpleListViewFragment<ListData> {
	private List<ListData> list;

	private List<ListData> getList() {
		return list;
	}

	@Override
public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		list = new ArrayList<ListData>();
		list.add(new ListData(getString(R.string.display_item_title), getString(R.string.display_item_description), DisplayPropertiesFragment.class.getName()));
		list.add(new ListData(getString(R.string.os_item_title), getString(R.string.os_item_description), OSPropertiesFragment.class.getName()));
		list.add(new ListData(getString(R.string.system_item_title), getString(R.string.system_item_description), SystemPropertiesFragment.class.getName()));
	}

	@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		View view = super.onCreateView(inflater, container, savedInstanceState);

		ListView listView = (ListView) view.findViewById(R.id.listView);
		listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);

		listView.setOnItemClickListener(new OnItemClickListener() {
			@Override
			public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
			    Intent intent = new Intent(view.getContext(), DeveloperFragmentActivity.class);
			    intent.putExtra("fragment", getList().get(position).getFragment());
			    startActivity(intent);
			}
		});

		return view;
	}

	protected ArrayAdapter<ListData> getAdapter(Context context) {
		return new ArrayAdapter<ListData>(context, android.R.layout.simple_list_item_2, getList()){
	        @Override
	        public View getView(int position, View convertView, ViewGroup parent){
	        	TwoLineListItem view;

	            if(convertView == null){
	                LayoutInflater inflater = (LayoutInflater)parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	                view = (TwoLineListItem)inflater.inflate(android.R.layout.simple_list_item_2, null);
	            }else{
	                view = (TwoLineListItem)convertView;
	            }

	            ListData data = getItem(position);
	            view.getText1().setText(data.getTitle());
	            view.getText2().setText(data.getDescription());

	            return view;
	        }
	    };
	}
}

The result is a set of ListView’s that display a variety of device properties.

developer sample

The code is available at GitHub.

Categories: Android, Java

You can quote me on that.

January 31, 2013 Leave a comment

quotation_iconI’m developing a mobile application that displays a famous quote and allows the user to share it. I’ll use an iterative approach that builds up the application over time; my personal version of Scrum. Start out with a simple application with a  limited set of canned quotes and grow out from there. 

I’m starting out with Android. Android open source development is much easier and cheaper than iOS or a Windows phone. The biggest issue I find with Android is version support. Android has gone through significant changes over the last few years. For example the popular ActionBar was added to Android 3.0 (API level 11)  two years ago.  I’d like my application to support Android 2.3.3 Gingerbread (API level 10) and above. I found that widget settings on an Android 2.3.7 device didn’t work the same as on an Android 4.2 emulated device. Picking the version sweat spot is something that you always need to keep you eye on.

The first version will be an Android application with a simple view and associated widget. My first version includes only the very basics:

  • An Activity with a TextView.
  • A widget with a TextView and ImageView.
  • A static string containing a quote.

Here are couple of screen shots from an Nexus emulation.

This home screen contains the Quote of the Day widget.

Quote of the Day application.

Samsung Developer has a nice emulation application available from their Remote Test Lab. I comes up much faster than AVD and comes with the skin. The only downside I’ve found so far is that it will only run for a predefined period of time.

The code is available at GitHub.

Categories: Android, Bruce's Posts, Java, Mobile