Archive

Archive for the ‘Java’ Category

Google Freebase Client Library

March 30, 2013 Leave a comment

Google’s APIs Client Library for Java provides a way to utilize Google APIs using JSON and OAuth 2.0. One library provides access to Freebase and this post describes my experiences using it.

It quickly became clear that this library is a work in progress. It labels itself as beta but I’d describe it as alpha. The documentation is sparse, I couldn’t find good Java samples, and the library’s functionality is incomplete. Having said that, this library could be quite useful once completed.

Freebase’s HTTP API is well documented and you can find samples such as this that use Google’s HTTP client library but not the Freebase library.

public class FreebaseSearchExample {
 public static String API_KEY = "YOUR-API-KEY-GOES-HERE";
 public static void main(String[] args) {
  try {
    HttpTransport httpTransport = new NetHttpTransport();
    HttpRequestFactory requestFactory = httpTransport.createRequestFactory();
    JSONParser parser = new JSONParser();
    GenericUrl url = new GenericUrl("https://www.googleapis.com/freebase/v1/search");
    url.put("query", "Cee Lo Green");
    url.put("filter", "(all type:/music/artist created:\"The Lady Killer\")");
    url.put("limit", "10");
    url.put("indent", "true");
    url.put("key", FreebaseSearchExample.API_KEY);
    HttpRequest request = requestFactory.buildGetRequest(url);
    HttpResponse httpResponse = request.execute();
    JSONObject response = (JSONObject)parser.parse(httpResponse.parseAsString());
    JSONArray results = (JSONArray)response.get("result");
    for (Object result : results) {
      System.out.println(JsonPath.read(result,"$.name").toString());
    }
  } catch (Exception ex) {
    ex.printStackTrace();
  }
 }
}

All calls to Freebase are returned within a JSON envelop. One of the biggest decisions a programmer must make is how to unmarshal the reply. The example above stores the response in vanilla JSONArray object. This is all well and good but further use of the results requires sifting through arrays for the desired objects. Later I’ll show how to parse the response into a POJO.

The response from Freebase contains two parts. One, a JSON envelope that either represents a successful execution of the request or an error message. The examples I’ve seen don’t deal with error checking or handling. At this point I won’t either and leave that for another post. A successful response from MQL Read will look like this.

{
"result": [
{
"name": "Jack Abramoff",
"type": "/people/person"
},
{
"name": "Bob Ney",
"type": "/people/person"
},
{
"name": "David Safavian",
"type": "/people/person"
}
],
}

In this case “result” contains the data requested. Other API calls, such as Topic, return different name/value pairs. The envelop can also contain values such as cost, cursor, and status. The unmarshalling code needs to handle (either accept and/or ignore) all the values returned in the envelop.

The Freebase client library supports the following APIs.

Freebase API Freebase Client Class Description Functional
Text Freebase.Text Returns a blob of HTML data pertaining to a specified id. Yes
Topic Freebase.Topic Returns properties and meta-data about a topic at a specified id. Yes
Image Freebase.Image imageReturns the scaled/cropped image attached to a freebase node. No
MQL Read Freebase.Mqlread Query the Freebase database using the Metaweb query language (MQL). No
MQL Write Freebase.Mqlwrite Write to the Freebase database No

For some reason the library does not support Search or Reconciliation.

The Text API returns a result containing a blob (blurb) about the specified topic. For some reason it’s not described in the Freebase developer guide. One answer on stackoverflow describes it as …

The freebase text api (http://wiki.freebase.com/wiki/ApiText) gives you back a lot of the top blurb from the wikipedia page.

I think this is all the that Freebase (and DBPedia) store about the topic. To get the full text of the wikipedia article, you can go directly to wikipedia.

For example a call to the Text API using Albert Einstein’s mid produces the following.


Request:
https://www.googleapis.com/freebase/v1/text/m/0jcx

Response:
{ “result”: “Albert Einstein ( /ˈælbərt ˈaɪnstaɪn/; German: [ˈalbɐt ˈaɪnʃtaɪn] ( listen); 14 March 1879 – 18 April 1955) was a German theoretical physicist who developed the theory of general relativity, effecting a revolution in physics. For this achievement, Einstein is often regarded as the father of modern physics. Einstein is generally considered the most influential physicist of the 20th century. While best known for his mass–energy equivalence formula E = mc (which has been dubbed \”the world’s most famous equation\”), he received the 1921 Nobel Prize in Physics \”for his services to theoretical physics, and especially for his discovery of the law of the photoelectric effect\”. The latter was pivotal in establishing quantum theory within physics.\nNear the beginning of his career, Einstein thought that Newtonian mechanics was no longer enough to reconcile the laws of classical mechanics with the laws of the electromagnetic field. This led to the development of his special theory of relativity. He realized, however, that the principle of relativity could also be extended to gravitational fields, and with his subsequent theory of gravitation in 1916, he published a paper on the general theory” }

Here’s a sample that uses the Text client API

public class GetTest {
 public static void main(String[] args) {
  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);
  Freebase freebase = fbb.build();
  ArrayList list = new ArrayList();
  try {
   list.add("/m/0jcx");
   Freebase.Text.Get text = freebase.text().get(list);
   ContentserviceGet get = text.execute();
   System.out.println(get.toPrettyString());
  } catch (Exception e) {
  e.printStackTrace();
  }
 }
}

ContentserverGet is a simple Class class to receive the JSON envelop.

public final class ContentserviceGet extends GenericJson {

 @com.google.api.client.util.Key
 private String result;

 public String getResult() {
  return result;
 }

 public ContentserviceGet setResult(String result) {
  this.result = result;
  return this;
 }
}

@com.google.api.client.util.Key specifies that a field is a data key, optionally providing the data key name to use. The JSON parser uses this key to unmarshal the JSON “result” value into the result field. In this case things are pretty simple because we’re only dealing with a string.

The Topic API returns all the known facts for a given topic including images and text blurbs. It will return a lot of data. It’s like getting back everything but the kitchen sink. Clink on this link for an example. The request also supports a filter property to reduce the amount of information returned. A call to the Topic API returns two root level values (id and property). Property contains a map of all the associated properties. Here’s a sample that uses the Topic client API.

public class TopicTest {
 public static void main(String[] args) {
  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);
  Freebase freebase = fbb.build();
  ArrayList list = new ArrayList();
  try {
   list.add("/m/0jcx");
   Freebase.Topic.Lookup lookup = freebase.topic().lookup(list);
   TopicLookup topic = lookup.execute();
   System.out.println(topic.toPrettyString());
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
}

In this case TopicLookup, which is a little more complex than ContentserverGet, receives the JSON envelop. If suffice to say that TopicLookup is filled with the map of all the properties returned and can be programmatically traversed as desired.

The MQL Read API provides access to the Freebase database using the Metaweb query language (MQL). For example, the following call will query for Albert Einstein’s children.


Request:
https://www.googleapis.com/freebase/v1/mqlread?query={"type":"/people/person","id":"/en/albert_einstein","children":[]}

Result:
{
“result”: {
“children”: [
“Eduard Einstein”,
“Hans Albert Einstein”,
“Lieserl Einstein”,
“Ilse Einstein”,
“Margot Einstein”
],
“type”: “/people/person”,
“id”: “/en/albert_einstein”
}
}

Freebase.Mqlread reminds me of all the empty cities in China. Everything needed is there but not used. It’s currently coded as following …

Mqlread extends FreebaseRequest<Void>

This results in the query being executed but nothing returned to the caller.

I wanted to try it out so I modified the source to use a generic type …

Mqlread<T> extends FreebaseRequest<T>

I also needed to pass along the generic’s Class when instantiating Mqlread.

public Mqlread mqlread(String query, Class<?> classType) throws java.io.IOException {
  Mqlread result = new Mqlread(query, classType);
  initialize(result);
  return result;
}

public class Mqlread extends FreebaseRequest {

...

  protected Mqlread(String query, Class classType) {
    super(Freebase.this, "GET", REST_PATH, null, classType);
    this.query = Preconditions.checkNotNull(query, "Required parameter query must be specified.");
  }

...

}

The following code uses the modified Freebase.Mqlread to fetch Albert Einstein’s children.

public class MQLReadTest {
	static private final String query =
			"{\"type\":\"/people/person\",\"id\":\"/en/albert_einstein\",\"children\":[]}";

	public static void main(String[] args) {
	    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);
	    Freebase freebase = fbb.build();
	    try {
	        Freebase.Mqlread mqlRead = freebase.mqlread(query, GenericJson.class);
	        GenericJson response = mqlRead.execute();
	        System.out.println(response.toPrettyString());
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	}
}

Using GenericJson as the generic causes the parser to create a map of the response returned by the query. This is similar to what occurs with Freebase.Topic. Getting back the results as a map is all fine and well but if I’m making the query for very specific reason I’d prefer that it be parsed into a POJO that lets me directly access the data I need. This can be done but it takes a little work.

I created the Person class to hold values from Freebase’s /people/person topic. I want the JSON parser to populate the children string array with the children values returned from the query.

public class Person {
 @com.google.api.client.util.Key
 private String[] children;

 public String[] getChildren() {
  return children;
 }

 public void setChildren(String[] children) {
  this.children = children;
 }
}

I created SingleMQLReadPersonResponse to hold envelop returned by the query. The result field is typed as Person so the JSON parser should populate it accordingly.


public class SingleMQLReadPersonResponse extends GenericJson {
 @com.google.api.client.util.Key
 private Person result;

 public Person getResult() {
 return result;
 }

 public void setResult(Person result) {
 this.result = result;
 }
 }

<span style="font-family: Consolas, Monaco, monospace; font-size: 12px; line-height: 18px;">

With the following code I can directly access the person’s children.


Freebase.Mqlread mqlRead = freebase.mqlread(query, SingleMQLReadPersonResponse.class);
 SingleMQLReadPersonResponse response = mqlRead.execute();
 System.out.println(response.toPrettyString());
 Person person = response.getResult();
 System.out.println(person);
 for (String child : person.getChildren()) {
 System.out.println(child);
 }

Here’s the output …

{
"result" : {
"children" : [ "Eduard Einstein", "Hans Albert Einstein", "Lieserl Einstein", "Ilse Einstein", "Margot Einstein" ]
}
}
org.bwgz.freebase.sample.MQLReadTest$Person@3aa59d
Eduard Einstein
Hans Albert Einstein
Lieserl Einstein
Ilse Einstein
Margot Einstein

So far so good. But I don’t want to create a response envelop class for every query I plan to make. I’d rather use Java generics to create a single class that can be reused. Something like this …


public static class SingleMQLReadResponse extends GenericJson {
 @com.google.api.client.util.Key
 private T result;

 public T getResult() {
 return result;
 }

 public void setResult(T result) {
 this.result = result;
 }
 }

Here’s the code that uses it.


Freebase.Mqlread<SingleMQLReadResponse> mqlRead = freebase.mqlread(query, SingleMQLReadResponse.class);
 SingleMQLReadResponse response = mqlRead.execute();
 System.out.println(response.toPrettyString());
 Person person = response.getResult();
 System.out.println(person);

But there a problem.  Here’s the output …

{
"result" : {
"id" : "/en/albert_einstein",
"type" : "/people/person",
"children" : [ "Eduard Einstein", "Hans Albert Einstein", "Lieserl Einstein", "Ilse Einstein", "Margot Einstein" ]
}
}
java.lang.ClassCastException: com.google.api.client.util.ArrayMap cannot be cast to org.bwgz.freebase.sample.MQLReadTest$Person
at org.bwgz.freebase.sample.MQLReadTest.SingleResponseTest2(MQLReadTest.java:101)
at org.bwgz.freebase.sample.MQLReadTest.main(MQLReadTest.java:123)

While SingleMQLReadResponse has been typed to Person the mqlread method is only passed SingleMQLReadResponse.class. Java does not allow SingleMQLReadResponse<T>.class. The parser cannot determine the result field’s class (type) and it falls back to com.google.api.client.util.ArrayMap. That in turn causes the cast exception.

Luckily there a way around this. The underlying code will use the class’s type (Type) when converting JSON to a Java object. If SingleMQLReadResponse.class is passed it’s parameter types are not included. This wasn’t a problem when SingleMQLReadPersonResponse.class was passed because the result field as explicitly typed to Person. The underlying code will accept a Type instead of a Class. If we can pass in a ParameterizedType the parser will have the information it needs to convert the JSON results to the specified Java object. There are two things we need:

  • Create a ParameterizedType representing the response class we are using.
  • Get the underlying code to use Type rather than Class.

Java contains the ParameterizedTypeImpl class which will produce a ParameterizedType object. Unfortunately it is protected and can’t be accessed. Rather than write my own I just grabbed the source code and included it in my project. The code isn’t complicated and I could have written my own but why bother when it’s freely available. The following code creates the Type object I need and takes create of the first thing I needed.

ParameterizedTypeImpl.make(SingleMQLReadResponse.class, new Type[] { Person.class }, null));

The execute method is located in AbstractGoogleClientRequest which is a deeply nested superclass of Mqlread. Here’s the code for execute.


public T execute() throws IOException {
 HttpResponse response = executeUnparsed();
 // TODO(yanivi): remove workaround when feature is implemented
 // workaround for http://code.google.com/p/google-http-java-client/issues/detail?id=110
 if (Void.class.equals(responseClass)) {
 response.ignore();
 return null;
 }
 return response.parseAs(responseClass);
 }

This code parses the HTTP response using the class (responseClass) which was passed through when Mqlread was constructed. HttpResponse.parse will accept either a Class or a Type. I don’t want to mess with AbstractGoogleClientRequest but I’ve already got my mitts into Freebase.Mqlread and there are changes I can make there.

First, create a second constructor with accepts a Type and saves it.

protected Mqlread(String query, Class responseClass) {
 super(Freebase.this, "GET", REST_PATH, null, responseClass);
 this.query = Preconditions.checkNotNull(query, "Required parameter query must be specified.");
 }

// Here's the second constructor.

protected Mqlread(String query, Class responseClass, Type responseType) {
 this(query, responseClass);
 this.responseType = responseType;
 }

I still have to pass along responseClass even though it won’t be used. If I don’t the underlying code will throw a validation exception.

Second, I override the execute method.

private T execute(Type type) throws IOException {
 HttpResponse response = executeUnparsed();

 return (T) response.parseAs(type);
 }

@Override
 public T execute() throws IOException {
 return (getResponseType() != null) ? execute(getResponseType()) : super.execute();
 }

Now we I run this code everything is parsed as expected and get the same results as with SingleMQLReadPersonResponse.

Type type = ParameterizedTypeImpl.make(SingleMQLReadResponse.class, new Type[] { Person.class }, null);

Freebase.Mqlread<SingleMQLReadResponse> mqlRead = freebase.mqlread(query, SingleMQLReadResponse.class, type);
 SingleMQLReadResponse response = mqlRead.execute();
 System.out.println(response.toPrettyString());
 Person person = response.getResult();
 System.out.println(person);
 for (String child : person.getChildren()) {
 System.out.println(child);
 }

{
"result" : {
"children" : [ "Eduard Einstein", "Hans Albert Einstein", "Lieserl Einstein", "Ilse Einstein", "Margot Einstein" ]
}
}
org.bwgz.freebase.sample.MQLReadTest$Person@b3826f
Eduard Einstein
Hans Albert Einstein
Lieserl Einstein
Ilse Einstein
Margot Einstein

Some final thoughts.

  • When Mqlread is completed classes like SingleMQLReadResponse will likely exist in com.google.api.services.freebase.model.
  • The Google Client API should support Type along with Class parsing. When that occurs Mqlread can more easily support parameterized types.
  • Error handling needs to be considered. If the response returns a JSON envelop with an error message then there’s a need to easily capture and use it.

The source code is available at GitHub.

Advertisements
Categories: Freebase, Java

Working with Freebase – Part 3

February 21, 2013 Leave a comment

My next step with Freebase was to generalize the way I queried it. I decided to model my API after the Java Persistence API (JPA) which provides a POJO persistence model for object-relational mapping. In this case its an object-structured data mapping. The Freebase graph is a set of nodes and a set of links or relationships between those nodes. My API will provide a straight forward way to retrieve those nodes and relationships in the form of a POJO. This post will describe my initial efforts.

In its most primitive form a Freebase node has a unique identifier, creation time stamp, and creator. Properties specify a relationship. That can be a relationship with another node or primitive value. Unique properties have only one relationship to a node/value. Non-unique properties can have many.  There are also universal properties (name, key, type, permission, mid). Primitive values include int, float, boolean, text, rawstring, uri, datetime, key, and id.

JPA describes an entity as …

An entity is a lightweight persistence domain object. Typically an entity represents a table in a relational database, and each entity instance corresponds to a row in that table. The primary programming artifact of an entity is the entity class, although entities can use helper classes.

The persistent state of an entity is represented either through persistent fields or persistent properties. These fields or properties use object/relational mapping annotations to map the entities and entity relationships to the relational data in the underlying data store.

I will reuse this concept of entity to represent a Freebase node. For example, the following class represents a node of type /people/person.

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

	@FBProperty(property_value = "/people/person")
	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;
	}
}

Consider the simplest of queries – select all from. In SQL this would look like SELECT * FROM name. MQL does not have a true equivalent.  The following MQL would select type, id, name from node type /people/person.

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

The code for executing this query looks like this.


public class QueryTestOne {
	private static final String personQuery =
			"[{"						+
				  "\"type\": \"/people/person\","	+
				  "\"id\": null,"			+
				  "\"name\": null"			+
				  "}]";

	public static  void dump(List list) {
		if (list != null) {
			for (T result : list) {
				System.out.printf("result: %s\n", result);
			}
		}
	}

	public static void test1(EntityManager em, String query) {
		Query q = em.createQuery(query);
		List<?> list = q.getResultList();
		dump(list);
	}

	public static void test2(EntityManager em, String query, Class<?> clazz) {
		Query q = em.createQuery(query, clazz);
		List<?> list = q.getResultList();
		dump(list);
	}

	public static void main(String[] args) {
		EntityManagerFactory emf = Persistence.createEntityManagerFactory("https", "www.googleapis.com", "/freebase/v1/mqlread/", null);
		EntityManager em = emf.createEntityManager();

		test1(em, personQuery);
		test2(em, personQuery, Person.class);
	}
}

The Freebase read API will return the results in a JSON response envelop that uses the following general form.

{
  "result": [{ MQL Response Here }],
  "status": "200 OK",
  "code": "/api/status/ok",
  "transaction_id":[opaque string value]
}

result contains the body of results and can either be {} (singleton) or [] array. My API will always use an array. I created  a class MQLMultipleResultResponse to hold the response envelop. For now I’m ignoring the other items such as status.

public class MQLMultipleResultResponse {
	private T[] result;

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

The query can be executed with or without a type. If the type is absent the underlying code will use Object. The type defines what kind of of object array will be returned. For example, if Person.class is specified an array of Person will be returned.

The underlying code executes the following …

URI uri = new URI(entityManager.getScheme(), entityManager.getAuthority(), entityManager.getPath(), Util.createQuery(variables), null);

ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
ClientHttpRequest request = requestFactory.createRequest(uri, HttpMethod.GET);
ClientHttpResponse response = request.execute();
ObjectMapper objectMapper = new ObjectMapper();
MQLMultipleResultResponse body = objectMapper.readValue(response.getBody(), typeReference);
results = body.getResult();

Most of this is straight forward HTTP client and JSON calls. The use of objectMapper.readValue() is somewhat unique. Since MQLMultipleResultResponse uses a generic I initially ran into problems with the following …

objectMapper.readValue(response.getBody(), MQLMultipleResultResponse.class);

objectMapper.readValue() cannot know what type the result body will be so it defaults to a Map. The following isn’t legal …

objectMapper.readValue(response.getBody(), MQLMultipleResultResponse.class);

Fortunately Jackson provides an alternate. Instead of passing the Class you can pass a TypeReference. The following code creates the necessary TypeReference …

TypeReference<MQLMultipleResultResponse> typeReference =
  new TypeReference<MQLMultipleResultResponse>() {
    public Type getType() {
      return ParameterizedTypeImpl.make(MQLMultipleResultResponse.class, new Type[] { resultClass }, null    );
  }
};

The key is providing the correct Type. In this case the subclass ParameterizedType. ParameterizedTypeImpl implements ParameterizedType. Java contains a version of ParameterizedTypeImpl but it’s inaccessible. Implementing ParameterizedType is just complicated enough that I decided to just copy ParameterizedTypeImpl’s source code into my project. It comes with a helpful make() method.

With all this in place the test client generates the following results. In one case an array of objects and in the other an array of Person.

result: {name=Dan Milbrath, type=/people/person, id=/en/dan_milbrath}
result: {name=David Safavian, type=/people/person, id=/en/david_safavian}
result: {name=Robert Cook, type=/people/person, id=/en/robert_cook}

result: org.bwgz.freebase.model.Person@2a444: getName: Dan Milbrath; getId: /en/dan_milbrath; getType: /people/person; getClass: class org.bwgz.freebase.model.Person; }
result: org.bwgz.freebase.model.Person@c126b3: getName: David Safavian; getId: /en/david_safavian; getType: /people/person; getClass: class org.bwgz.freebase.model.Person; }
result: org.bwgz.freebase.model.Person@1c01b97: getName: Robert Cook; getId: /en/robert_cook; getType: /people/person; getClass: class org.bwgz.freebase.model.Person; }
Categories: Freebase, Java, JPA, Structured Data

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

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

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