Archive

Archive for the ‘Bruce's Posts’ Category

Quotation Chrome Extension

December 4, 2013 1 comment

I’m creating a Chrome extension that allows a user to search for a quotation. I’d like to have a toolbar popup which allows the user to enter a search query and receive a list of matching quotations.

toolbar-search-1280x800

Creating and installing an extension is straight forward and already well documented. I’ll focus on the more specific aspects of the extension. Version one will do the following:

  • Provide a browser action which installs a toolbar item in Chrome’s toolbar.
  • When the user clicks on the extension’s toolbar icon a popup with a simple input field and search button will appear.
  • Upon clicking on the search button the extension will query Freebase for matching quotations and display the results in the popup.

Every Chrome extension contains a manifest file (manifest.json) which defines it. Here’s part of my manifest …

"browser_action": {
 "default_popup": "popup.html"
 },</pre>
"content_security_policy": "script-src 'self' https://apis.google.com https://ajax.googleapis.com https://ssl.google-analytics.com; object-src 'self'"
<pre>

When the Quotation toolbar icon is clicked popup.html will be displayed as a popup below the icon. The content_security_policy setting is required to allow calls to Google’s client and analytic API’s.

Using JQuery I’ll create a callback when the search button is clicked.

From popup.html …

</pre>
<form>
<table style="border-spacing: 0px; width: 80%;">
<tbody>
<tr>
<td style="margin: 0px; padding: 0px;">
<input type="search" id="query-field" style="margin: 0px; padding: 5px 3px 3px 7px; width: 100%; font-size: 15px;" autofocus/>
</td>
<td style="margin: 0px; padding: 0px; background-color: RoyalBlue;" align="center">
<input type="image" src="search.png" alt="Search" />
</td>
</tr>
</tbody>
</table>
 </form>
<pre>

The search script will listen for a click on the search button and when it occurs will send a search request to the Freebase search API and update the result list. Here’s an abridged version of the code.


function search() {</pre>
query = $('#query-field').val();
request = gapi.client.request({
'path': '/freebase/v1/search',
'params': {
'query': query,
'filter': '(any type:/media_common/quotation)',
'cursor': cursor,
'limit': limit
}
 });
 request.execute(function(json) {
length = json.result.length;

if (length != 0) {
$("#quotation-list").html('');
for (var i = 0; i < length; i++) {
var quotation = json.result[i].name;
var mid = json.result[i].mid;
var url = "http://quotation.bwgz.org/quotation" + mid;

         var id = "item-" + i;

         $("#quotation-list").append('<div class="result"><a id="' + id + '" href="' + url + '" data-mid="' + mid + '" target="_blank">' + json.result[i].name + '</a></div>');
}
 }
<span style="font-family: Consolas, Monaco, monospace; font-size: 12px; line-height: 18px;">});</span>

successI use the Google Client API to call Freebase’s search API. It returns a set of search results in a JSON wrapper. The code loops through the results and updates the page accordingly.

I limit results to 10 items per page and additional code handles the paging.  The Freebase search API will only supply the first 220 highest scoring results.  While this is a bit of an annoying limitation few users would page through more results than that.

Clicking on a search results causes the browser to open a new tab and sends the user to quotation.bwgz.org where they’ll see more details on the quotation.
Quotation
 

Advertisements

Google Freebase Client Library – Topic API

The following comment was recently posted on the Freebase discussion mailing list …

Yes, we are actually deprecating it [Text] in favor of getting a description from the Topic API. The good part is that the new solution will return you entity descriptions across 40 languages from Wikipedia (on top of course of any user entered descriptions).
We haven’t announced it yet since we just finished that feature, but we will do so soon.

That prompted me to look again at Google’s Freebase client library. The Topic API returns all the known facts for a given topic including images and text blurbs. At first blush it appeared to be a bit of overkill for what I needed but a second looked showed that it was pretty straight forward to deal with.

It takes a bit time to become comfortable with Topic’s response. It’s a hierarchy of maps and lists represented by a JSON object. Here’s how I wrapped my brain around it.

At the root level of the response there is an object ({}) named “property”. It’s a map of domain properties. If you didn’t filter the request then you’re getting back all the domains associated with the topic. If you did apply a filter then you’ll only get back those domains you specified.

This is a response fragment containing a domain object.

"/type/object/name": {
"valuetype": "string",
"values": [
{
"text": "William Shakespeare",
"lang": "en",
"value": "William Shakespeare",
"creator": "/user/santiago_aguiar",
"timestamp": "2011-03-15T12:51:40.000Z"
}
],
"count": 39.0
}

Each domain is an object ({}) with the following properties:

Name Type Description
valuetype string The name of the “property value”
values array Any array ([]) of values where a value is a object ({}).
count integer The total number of values that exist in Freebase.”values” is an array of objects.

The values array contains objects with a consistent form.

If the value type is “float”, “int”, “bool”, “datetime”, “uri”, “key”, or “object” each object in the array will be of the following form.

Name Type Description
text string
lang string The language type
value many This can one of the following:

  • literal – a primitive such as a string, integer, or boolean
  • foreign key – the id (a string) of a topic contained in another dataset
  • topic reference – the id (a string) of another topic
creator string Id of the value’s creator.
timestamp string When the value was created.

There can be additional properties. For example, the domain /common/topic/description will add a “citation” object.

"/common/topic/description": {
"valuetype": "string",
"values": [
{
...
"citation": {
"provider": "Wikipédia",
"statement": "Description licensed under the Creative Commons Attribution-ShareAlike License (http://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License).",
"uri": "http://fr.wikipedia.org/wiki/William_Shakespeare"
}
}
],
"count": 39.0
}

Things are straight forward when the value type is a literal, foreign key, or topic reference. In those cases “value” holds a holds something which can be dealt with simply.

When the value type is “compound” things get a bit more interesting.

Name Type Description
text string
lang string The language type
id string The id (a string) of the topic contained in “property”.
creator string Id of the value’s creator.
timestamp string When the value was created.
property string A map of the topic’s (see id) properties.

A “compound” object nests another topic as a property value. The “value” property is gone and replaced by “id” and “property”. “id” holds the id of the topic described in “property”. This parallels the response that Topic returns when queried.

With that understanding in place I tried out the Topic API found in the Google Freebase client library. Here’s some sample code.


HttpTransport httpTransport = new NetHttpTransport();
JsonFactory jsonFactory = new JacksonFactory();
HttpRequestInitializer httpRequestInitializer = new HttpRequestInitializer() {
  @Override
  public void initialize(HttpRequest request) throws IOException {}
};

Freebase.Builder fbb = new Freebase.Builder(httpTransport, jsonFactory, httpRequestInitializer);
fbb.setApplicationName("freebase-test");
Freebase freebase = fbb.build();

try {
 Freebase.Topic.Lookup lookup = freebase.topic().lookup(Arrays.asList(mid));
 TopicLookup topic = lookup.execute();
 if (topic != null) {
  System.out.printf("topic: %s\n", topic.getId());

  Property property = topic.getProperty();
  if (property != null) {
   System.out.println(property);
   }
  }
 }
} catch (IOException e) {
 e.printStackTrace();
}

So far so good. The client code executes correctly and I get back a valid response. But at this point I hit a wall. The Property class doesn’t do anything specific with the response. It’s a sub-class of GenericJson and that means all the properties are held in a very generic way. I was a bit surprised by this given that there are classes (TopicPropertyvalue and TopicValue) in place to hold topic properties and values. Since this library is a work in progress maybe the work hasn’t progressed that far. I didn’t want to go slogging through maps and arrays so I modified Property to use them. I changed its super class from GenricJson to ArrayMap<String, TopicPropertyvalue>. With that in place I could now output the first value of all the topic’s properites quite simply …

// for each domain in the property map
for (Object name : topic.getProperty().keySet()) {
 TopicPropertyvalue tpv = property.get(name); // get the domain name
 String valuetype = tpv.getValuetype();       // get the value type
 List<TopicValue> values = tpv.getValues();   // get the list of values
 // translate the value type to the key needed to get the "property value"
 // this will translate to value, id, or property
 String key = ValueType.valueOf(valuetype.toUpperCase()).getKey();
 Object value = values.get(0).get(key);       // from the first value object get the "primary value"

 System.out.printf("\t domain: %s primary value (%s): %s\n", name, valuetype, value);
}

Not too bad.

Categories: Bruce's Posts, Freebase

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());
	}

       ....
}

Working with Freebase – Part 2

February 14, 2013 Leave a comment

In part 1 I demonstrated a method to generate a Freebase MQL query from an annotated class that is intended to mimic JPA’s ORM behavior. Next I’ll show how to use the Spring RestTemplate to execute the query and create an array of objects from the results. The following code fragment illustrates the steps.

RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJacksonHttpMessageConverter());

URI uri = new URI("https", "www.googleapis.com", "/freebase/v1/mqlread/", Util.createQuery(variables), null);
ResponseEntity entity = restTemplate.getForEntity(uri, PersonsResponse.class);
PersonsResponse response = entity.getBody();
Person[] persons = response.getResult();

The first step is the create a RestTemplate and then add a JSON message converter (MappingJacksonHttpMessageConverter) to it. Freebase returns the its results in JSON format. An array of results will look like this …

{
"code":          "/api/status/ok",
"result": [
{
"type": "/people/person",
"id": "/en/dan_milbrath",
"gender": {
"type": "/people/gender",
"id": "/en/male",
"name": "Male"
},
"name": "Dan Milbrath"
},
{
"type": "/people/person",
"id": "/en/david_safavian",
"gender": {
"type": "/people/gender",
"id": "/en/male",
"name": "Male"
},
"name": "David Safavian"
},
{
"type": "/people/person",
"id": "/en/robert_cook",
"gender": {
"type": "/people/gender",
"id": "/en/male",
"name": "Male"
},
"name": "Robert Cook"
}
],
"status":        "200 OK",
"transaction_id": "cache;cache02.p01.sjc1:8101;2013-02-14T07:31:07Z;0064"
}

ResponseEntity holds the code, status, and transaction_id properties. Typing it to an appropriate class allows it to hold the result returned in the JSON format. In this case it is PersonsResponse which is a subclass of MQLMultipleResultResponse typed to Person.

class PersonsResponse extends MQLMultipleResultResponse<Person>  {
}

public class MQLMultipleResultResponse<T> {
	private String cursor;
	private T[] result;

	public T[] getResult() {
		return result;
	}
	public void setResult(T[] result) {
		this.result = result;
	}
	public String getCursor() {
		return cursor;
	}
	public void setCursor(String cursor) {
		this.cursor = cursor;
	}
}

The code is available at GitHub.

Categories: Bruce's Posts, Java, Spring

Working with Freebase – Part 1

February 14, 2013 Leave a comment

Freebase provides a remote read service API for accessing Freebase database using the Metaweb query language (MQL). Using an HTTP endpoint an application can send a MQL query and received a result set in the response. The result set is returned in the JSON (JavaScript Object Notation) format.

In my case I wanted to query the database for quotes. For example, return quotations from the database. The following MQL returns all quotations associated with a person in the database.

[{
"type": "/people/person",
"id": null,
"name": null,
"/people/person/quotations": [{
"type": "/media_common/quotation",
"id": null,
"name": null,
}]
}]

This produces a results similar to this:

{
"code":          "/api/status/ok",
"result": [
{
"/people/person/quotations": [
{
"id":   "/en/first_thing_we_do_lets_kill_all_the_lawyers",
"name": "First thing we do, let's kill all the lawyers.",
"type": "/media_common/quotation"
},
{
"id":   "/en/o_brave_new_world_that_has_such_people_int",
"name": "...O brave new world, That has such people in't!",
"type": "/media_common/quotation"
}
],
"id":   "/en/william_shakespeare",
"name": "William Shakespeare",
"type": "/people/person"
},
{
"/people/person/quotations": [
{
"id":   "/m/02hylj7",
"name": "...the shifty, hangdog look which announces that an Englishman is about to talk French.",
"type": "/media_common/quotation"
},
{
"id":   "/en/routine_is_the_death_to_heroism",
"name": "Routine is the death to heroism.",
"type": "/media_common/quotation"
}
],
"id":   "/en/p_g_wodehouse",
"name": "P. G. Wodehouse",
"type": "/people/person"
}
],
"status":        "200 OK",
"transaction_id": "cache;cache02.p01.sjc1:8101;2013-02-14T07:31:07Z;0064"
}

In addition to executing the query I want the code to follow an ORM (Object Relational Mapping) style pattern. The code should know how to build the query, execute it, and return the results as objects. I used JPA annotations as a model for creating the mappings.

I created two annotations:

  • FBEntity – Describes which Freebase resource type the class is associated with.
  • FBProperty – Describes which Freebase property the method or field is associated with.

Using those annotations I create the following classes to represent a person and gender.

Person.java

@FBEntity(type = "/people/person")
public class Person {

	@FBProperty(property_value = "/people/person")
	private String type;
	@FBProperty
	private String id;
	@FBProperty
	private String name;
	@FBProperty
	private Gender gender;

	public String getType() {
		return type;
	}
	public void setType(String type) {
		this.type = type;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
	public Gender getGender() {
		return gender;
	}
	public void setGender(Gender gender) {
		this.gender = gender;
	}
}

Gender.java

@FBEntity(type = "/people/gender")
public class Gender {

	@FBProperty(property_value = "/people/gender")
	private String type;
	@FBProperty
	private String id;
	@FBProperty
	private String name;

	public String getType() {
		return type;
	}
	public void setType(String type) {
		this.type = type;
	}
	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
}

A query builder class uses the annotations to create a MQL query. This is where all the heavy lifting occurs. The builder will parse the class and for each field/method with an annotation it will generate the necessary MQL. It accepts special directives such as limit which are associated with a class. These directives can be used to control the behavior of the query.  It also accepts an instance of the object being queried and use it’s values to refine the query.

MQLQueryBuilder.java

public class MQLQueryBuilder {
	static private final String OPEN_SQUARE = "[";
	static private final String CLOSE_SQUARE = "]";
	static private final String OPEN_BRACKET = "{";
	static private final String CLOSE_BRACKET = "}";
	static private final String EMPTY = "";
	static private final String SPACE = " ";
	static private final String QUOTE = "\"";
	static private final String COMMA = ",";
	static private final String NEW_LINE = "\n";

	static public int PROPERTY_COMPACT	= 0;
	static public int PROPERTY_PRETTY	= 1;

	private String space = EMPTY;
	private String new_line = EMPTY;

	public MQLQueryBuilder(int property) {
		if (property == PROPERTY_COMPACT) {
			space = EMPTY;
			new_line = EMPTY;
		}
		else if (property == PROPERTY_PRETTY) {
			space = SPACE;
			new_line = NEW_LINE;
		}
	}

	public MQLQueryBuilder() {
		this(PROPERTY_COMPACT);
	}

	private String indent(int tab) {
		StringBuffer buffer = new StringBuffer();
		for (int i = 0; i < tab * 2; i++) {
			buffer.append(space);
		}
		return buffer.toString();
	}

	private String quote(String string) {
		return new StringBuffer().append(QUOTE).append(string).append(QUOTE).toString();
	}

	private Object getValueFromObject(Class<?> clazz, Object object, Field field) {
		//System.out.printf("clazz: %s  object: %s  field: %s\n", clazz, object, field);
		Object value = null;
		String name = "get".concat(field.getName()).toLowerCase();

		Method method = null;

		for (Method m : clazz.getMethods()) {
			if (m.getName().toLowerCase().equals(name)) {
				try {
					method = clazz.getMethod(m.getName());
				} catch (NoSuchMethodException e) {
					e.printStackTrace();
				} catch (SecurityException e) {
					e.printStackTrace();
				}
				break;
			}
		}

		if (method != null) {
			try {
				value = method.invoke(object);
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}

		return value;
	}

	private String createQuery(Class<?> clazz, Map<Class<?>, MQLProperty[]> map, Object object, Stack<Class<?>> stack, int tab) {
		StringBuffer sb = new StringBuffer();
		String indent = indent(tab);

		if (clazz.isArray()) {
			sb.append(String.format("%s", OPEN_SQUARE));
			sb.append(createQuery(clazz.getComponentType(), map, object, stack, tab + 1));
			sb.append(String.format("%s", CLOSE_SQUARE));
		}
		else {
			FBEntity fbEntity = clazz.getAnnotation(FBEntity.class);

			if (fbEntity != null) {
				stack.push(clazz);
				boolean next = false;
				sb.append(String.format("%s%s", OPEN_BRACKET, new_line));

				for (Field field : clazz.getDeclaredFields()) {
					Class<?> type = field.getType();
					if (!stack.contains(type.isArray() ? type.getComponentType() : type)) {
						FBProperty fbProperty = field.getAnnotation(FBProperty.class);
						if (fbProperty != null) {
							String name = quote((fbProperty.property_name().length() != 0) ? fbProperty.property_name() : field.getName());
							String value = null;

							if (type.isArray()) {
								value = createQuery(type, map, object != null ? getValueFromObject(clazz, object, field) : null, stack, tab + 1);
							}
							else {
								fbEntity = type.getAnnotation(FBEntity.class);
								if (fbEntity != null) {
									value = createQuery(type, map, object != null ? getValueFromObject(clazz, object, field) : null, stack, tab + 1);
								}
								else {
									if (object != null) {
										Object o = getValueFromObject(clazz, object, field);
										if (o != null) {
											value = quote(o.toString());
										}
									}

									if (value == null) {
										value = fbProperty.property_value().length() != 0 ? quote(fbProperty.property_value()) : null;
									}
								}
							}

							sb.append(String.format("%s%s%s:%s%s", next ? (COMMA + new_line) : EMPTY, indent, name, space, value));
							next = true;
						}
					}
				}

				if (map != null) {
					MQLProperty[] directives = map.get(clazz);
					if (directives != null) {
						for (MQLProperty directive : directives) {
							String name = quote(directive.getName());
							String value = directive.getValue() != null ? directive.getValue() instanceof String ? quote(directive.getValue().toString()) : directive.getValue().toString() : null;
							sb.append(String.format("%s%s%s:%s%s", next ? (COMMA + new_line) : EMPTY, indent, name, space, value));
							next = true;
						}
					}
				}

				sb.append(String.format("%s%s%s", new_line, indent, CLOSE_BRACKET));
				stack.pop();
			}
		}

		return sb.toString();
	}

	public String createQuery(Class<?> clazz, Map<Class<?>, MQLProperty[]> map, Object object) {
		return createQuery(clazz, map, object, new Stack<Class<?>>(), 0);
	}

	public String createQuery(Class<?> clazz, Map<Class<?>, MQLProperty[]> map) {
		return createQuery(clazz, map, null);
	}

	public String createQuery(Class<?> clazz) {
		return createQuery(clazz, null);
	}
}

The following test program will produces two queries. One will return all persons and the other only males. The limit directive limits the query to just three results. Passing an instance of Person with the Gender set to “Male” will refine the results to only males.

PersonTestOne.java

public class PersonTestOne {

	public static void main(String[] args) {
		Map<Class<?>, MQLProperty[]> map = new HashMap<Class<?>, MQLProperty[]>();

		MQLProperty[] directives = new MQLProperty[] {
				new MQLProperty("limit", new Integer(3))
		};
		map.put(Person.class, directives);

		MQLQueryBuilder builder = new MQLQueryBuilder(
				MQLQueryBuilder.PROPERTY_PRETTY);
		String mql = builder.createQuery(Person[].class, map);
		System.out.println(mql);

		Gender gender = new Gender();
		gender.setName("Male");
		Person person = new Person();
		person.setGender(gender);
		mql = builder.createQuery(Person[].class, map, person);
		System.out.println(mql);
	}
}

Running the program generates the following MQL.

[{
 "type": "/people/person",
 "id": null,
 "name": null,
 "gender": {
 "type": "/people/gender",
   "id": null,
   "name": null
   },
 "limit": 3
 }]

[{
 "type": "/people/person",
 "id": null,
 "name": null,
 "gender": {
 "type": "/people/gender",
   "id": null,
   "name": "Male"
   },
 "limit": 3
 }]

The next step is to execute the query in a manner that populates an array of objects with the results.

Categories: Bruce's Posts

In Search of Quotes

February 14, 2013 2 comments

Wikiquote

My search continues for a source of interesting quotations that I can incorporate in my mobile application. Wikimedia’s Wikiquote appeared to be an excellent source. It holds thousands of quotes. Wikiquote describes itself as a free compendium of quotations that is being written collaboratively by the readers.  The trick is how to access those quotes with an API.

Wikiquote is powered by MediaWiki the software that runs various Wikimedia sites such as Wikipedia. MediaWiki provides a web service API to access its pages. But while the page contents is easily accessed it is not structured in way that allows it to be easily parsed into discrete data elements. In other words, extract quotes from any given Wikiquote page isn’t straight forward. I could write a parser but I suspected that something like this had already been done. This suspicion lead me to  DBpedia.

DBpediaDBpedia describes itself as …

… a crowd-sourced community effort to extract structured information from Wikipedia and to make this information available on the Web. DBpedia allows you to make sophisticated queries against Wikipedia, and to link other data sets on the Web to Wikipedia data.

DBpedia regularly extracts data from Wikipedia and stores it using a Resource Description Framework (RDF) model for data interchange. Those resources can be remotely queried using SPARQL a query language to RDF.  DBpedia’s ontology contains a quotation property. Unfortunately when I started querying DBpedia for resources that had quotes very few were returned. Apparently Wikiquote is not one of the Wikimedia sites that DBpedia sources from. So while DBpedia looked promising it turned out to be a dead end.

Freebase

More searching lead me to Freebase. Freebase is very similar to DBpedia. It is an open collection of structured data that can be accessed using a remote API. Here too data is pulled from a variety of sources such as Wikipedia and stored as a  graph model comprised of nodes (data objects) and relationships between nodes. This model can be queried with Freebase’s proprietary Metaweb Query Language (MQL).

For example, the follow query will return quotations for Albert Einstein.

[{
  "type": "/people/person",
  "id": null,
  "name": "Albert Einstein",
  "gender": {
    "type": "/people/gender",
    "id": null,
    "name": null
    },
  "/people/person/quotations": [{
      "type": "/media_common/quotation",
      "id": null,
      "name": null,
      "subjects": [],
      "limit": 5
      }]
  }]

Here are the results …

<em id="__mceDel">{
  "code":          "/api/status/ok",
  "result": [{
    "/people/person/quotations": [
      {
        "id":       "/en/imagination_is_more_important_than_knowledge",
        "name":     "Imagination is more important than knowledge.",
        "subjects": [],
        "type":     "/media_common/quotation"
      },
      {
        "id":       "/m/02kpjn_",
        "name":     "Great spirits have always encountered violent opposition from mediocre minds.",
        "subjects": [],
        "type":     "/media_common/quotation"
      },
      {
        "id":       "/m/02nrfj2",
        "name":     "Not everything that counts can be counted, and not everything that can be counted counts.",
        "subjects": [],
        "type":     "/media_common/quotation"
      },
      {
        "id":   "/quotationsbook/quote/21171",
        "name": "If men as individuals surrender to the call of their elementary instincts, avoiding pain and seeking satisfaction only for their own selves, the result for them all taken together must be a state of insecurity, of fear, and of promiscuous misery.",
        "subjects": [
          "Instinct"
        ],
        "type": "/media_common/quotation"
      },
      {
        "id":   "/quotationsbook/quote/23603",
        "name": "The ideals which have always shone before me and filled me with the joy of living are goodness, beauty, and truth.",
        "subjects": [
          "Life and Living"
        ],
        "type": "/media_common/quotation"
      }
    ],
    "gender": {
      "id":   "/en/male",
      "name": "Male",
      "type": "/people/gender"
    },
    "id":   "/en/albert_einstein",
    "name": "Albert Einstein",
    "type": "/people/person"
  }],
  "status":        "200 OK",
  "transaction_id": "cache;cache02.p01.sjc1:8101;2013-02-14T06:45:22Z;0064"
}

With Freebase I now had an online source for thousands of interesting quotes. The next step was how best to use them.

Categories: Bruce's Posts, Java

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