Documented Android Sample Application

Published on December 2016 | Categories: Documents | Downloads: 32 | Comments: 0 | Views: 494
of 59
Download PDF   Embed   Report

Android application sample

Comments

Content


3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 1/59
metagear.de
A – Sort-Of – Full-Fledged Android
Sample Application – Walked-Through
In-Depth
August 12, 2012 · Robert Söding
Show Table of Contents
1. Preface
While the sales volumes of PCs and feature cell phones stagnate, smartphone – and, since recently, tablet PC –
sales boom.
Looking at operation system distributions in the mobile sector, iOS and Android are prevalent, squeezing other
operation systems (like Symbian or Blackberry OS) out of the market (upcoming Windows Phone is still playing in
its small niche).
Although Apple has pioneered the mobile market with their iPhone and iPad, Android device sales are growing
much more rapidly in both relative and absolute terms. For smartphones, in year 2012, Android has a 59%
market share (iOS: 23%), and 145% growth per year (iOS: 89%).
Still being a niche segment as of today, tablet PC sales are estimated to rapidly grow as well. In this segment it
is forecasted that, while iOS will retain its leadership for several upcoming years, Android's relative growth is,
and will be, higher. – See chapter Android Market Share for some resources on these issues.
– The article at hand documents a sample application that puts the Android SDK (software development kit) to
work.
In contrast to just providing a Eureka! Hooray!, few-lines, blog posting that fakes its writer as an all-time
problem solver, this sample application implements a - well, not so – real-world, complex, use case, which
needs to be implemented at any rate – whether the Android APIs would fit – or wouldn't. – We've been trying to
make this application reproducible as a whole, not just lightening single aspects of it.
As this article does not start at a "Hello World" entry level, interested readers are supposed to already have
gone through introductory Android articles or tutorials (seriously). As for the server-side implementation, a basic
knowledge of Servlet and Spring Framework technologies ought to be helpful, however that's not mandantory.
Any feedback is appreciated and may be directed to [email protected].
If you don't want to install the sample application, straightly skip to the Sample Application section.
2. Prerequisites
Unfortunately, there are more than two or three steps involved in order to get the sample application
running, so this cannot be considered a Hello World sample.
Nevertheless, I've tested the deployment procedure, thus feel free to contact me in case of incidents – after
"aunt Google" hasn't brought you forward.
The application has been developed on
Eclipse 3.6 (Helios) for Java EE Developers
with the Android Development Tools (ADT) installed.
The sample application makes use of Android's Location-Based Services (LBS) and Google Maps. Due to
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 2/59
officially unresolved bugs related to using these with the Android Emulator in Android 2.2 and 2.3 (i.e., this one
), the sample application uses the
Android SDK 2.1, API level 7, along with the corresponding
Google APIs, API level 7.
This SDK version can be installed from within the Android SDK and AVD Manager, which is included with the
Android SDK :
To run the server application,
Apache Tomcat
has been used (both 6.0 and 7.0 work).
2.1. Downloading, Importing, and Running the Sample
Application
Download the zipped project sources, which comprise of the following Eclipse projects:
mg-library-android – a library of custom Android views, activities, and Android-specific utilities, which
can be re-used in any Android project
mg-pizzastore-android – the Android client (our main application)
mg-pizzastore-android-test – automated tests for the Android client
mg-pizzastore-server – the server-side application
mg-pizzastore-shared – a library of domain / model classes, which are used by both mg-pizzastore-
android and mg-pizzastore-server
Make sure that you've set up the Android SDK in the Eclipse Preferences. As mentioned in the previous
chapter, the SDK must contain the Android SDK 2.1, API level 7 version:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 3/59
To import the aforementioned projects into Eclipse, click File --> Import --> Existing Projects into
Workspace, and choose the mg-pizzastore-android.zip archive file previously downloaded.
Assign a server to the mg-pizzastore-server project by completing the following steps:
In Eclipse, open the Servers view by selecting from the main menu: Window --> Show View -->
Servers.
In the Servers view, right-click, and select New --> Server.
Choose an existing Apache Tomcat 6.0 oder 7.0 installation, and in a subsequent dialog,
assign the mg-pizzastore-server project.
At this stage, there shouldn't be any compile errors in the projects, and we're heading towards running the
sample application.
Fix the mg-pizzastore-android project properties by right-clicking the project, Properties --> Android. In
the field group Library, remove the referenced, but invalid mg-library-android project ...
... and newly add it:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 4/59
Please excuse that annoyance. I've looked at the settings files but haven't found a better solution yet.
To start the MapViewActivity, you need to obtain a custom Google Maps API Key . Next, edit the
corresponding file mg-pizzastore-android/res/layout/map_linearlayout.xml accordingly. – See chapter
MapViewActivity: General Setup for additional information.
It hasn't been hard to set up the Maps API Key, has it? ;-)
To finally run the sample application from within Eclipse, first start the mg-pizzastore-server project by
right-clicking it and selecting Run as --> Run on Server.
Next, run mg-pizzastore-android by right-clicking its project root and choosing Run as --> Android
Application (select a matching Android Virtual Device (AVD) in the Eclipse Run Configurations).
To run the tests, right-click the mg-pizzastore-android-test project in Eclipse, and select Run As -->
Android JUnit Test.
2.2. Tip: Getting the android.jar Source Code
Unfortunately, the Android SDK doesn't ship with the
source code for android.jar, and you'll probably
stumble upon that.
Google'ing around, everyone recommends to checkout
the source code from the Git source code management
system at sources.android.com . However, with 2.6
GB in download size and several installation steps
involved, that may be overkill. – Noone seems to realize
that there's a lightweight alternative:
Install the adt-addons Eclipse Plugin .
Theoretically, this should be sufficient to view the
Android class libraries' source code in Eclipse; however,
it did not work for me. – In this case find the matching
sources.zip in
$ECLIPSE_HOME/plugins/com.android.ide.eclipse.source*.
These sources may be not perfectly accurate when they don't exactly match your Android SDK revision.
3. The Sample Application
The sample application represents an imaginary pizza store, where the user can select, and order, from a list
of available pizzas. Additionally they need to fill in their address data. As a plus, the user can edit their location
in a Google Map.
At application start time, the list of pizzas is retrieved from the remote server, and at shopping cart checkout
time, the order gets submitted to that server.
3.1. Data Model
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 5/59
3.1.1. Domain Classes
The application's domain – respectively, model – classes are located at the mg-pizzastore-shared project.
The following class diagram visualizes the entities and their relationships:
The sample application's first, and
main, screen is the Pizzas List,
which lists a set of Pizzas along with
their properties.
When the user selects a Pizza into
their Shopping Cart, that shopping
cart displays a set of PizzaLineItems,
where a PizzaLineItem – while
realizing all Pizza properties – has an
additional property quantity.
In order to finally checkout their
shopping cart, the user needs to fill
in a User Data Form, which
corresponds to a UserData instance.
An Address contains user data that are required for Location-Based Services; such data include street, city,
and a Country. UserData contains an Address plus some additional data such as the user's name and phone
number.
When the user finally checks out their shopping cart, an Order will be submitted to the server.
For corresponding code see: mg-pizzastore-shared/de.metagear.pizzastore.model
3.1.2. Using the JSON ObjectMapper with the Model Objects
In the sample application, the Jackson ObjectMapper is used to marshal, resp., unmarshal, (our domain class)
instances from, resp., to, JSON -encoded strings. We're using it when Dealing with Android's
SharedPreferences as well as with the backend communications (HttpGetPizzasListTask and HttpPostTask)
toward the server.
The ObjectMapper requires some custom settings regarding the class' constructor and transient properties, as
shown on our Address model object:
@JsonIgnoreProperties({ "valid" })
public class Address implements Serializable {
// ...
public Address(String street, String zipCode, String city, Country country) {
street = street;
zipCode = zipCode;
city = city;
country = country;
}
public Address() {
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
street = street;
}
// ...
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 6/59
@JsonIgnore
public boolean isValid() {
if (StringUtils.isEmpty(street) || StringUtils.isEmpty(zipCode)
|| StringUtils.isEmpty(city) || !country.isValid()) {
return false;
} else {
return true;
}
}
}
For corresponding code see: mg-pizzastore-shared/de.metagear.pizzastore.model.Address
First-off, the mapper requires either a nillary (default) constructor or (not used in the sample application) the
@JsonCreator annotation on another, public, parametrized, constructor.
Additionally, our boolean isValid() method is transient and evaluated on-the-fly, at runtime (accordingly,
there is no matching setter). - Therefore, we're instructing the ObjectMapper to ignore the getter property
(using the @JsonIgnore annotation) and not to expect a corresponding setter property (using the
@JsonIgnoreProperties({ "valid" }) annotation).
Consult the Jackson Annotations ApiDoc in case you'd need further information.
3.2. Spring-Based Backend, and Remoting
3.2.1. Overview
As for the sample app's client/server connection, the HTTP transport protocol has been favored over other
protocols (like using RMI, for instance) in order to avoid possible firewall restrictions. From there, JSON has
been preferred over XML (or binary contents, like using Hessian ), and REST over XML Web Services,
because of performance reasons and ease of implementation with Android.
Spring Android provides a
client-side API for JSON –
(and, alternatively, XML-)
based REST operations and
Android integration. At the
server side, Spring Android is
"simply" leveraging Spring's
Web MVC Framework APIs.
– Just like other Spring
Framework domains, Spring
Android provides a set of templates, elegantly figuring out, and implementing, best practices.
For related concepts, see chapter Remoting of my previously written article on the Spring Framework .
You may also want to read chapter Spring MVC of the same article, although Spring MVC in portions has
partially evolved from Spring 2.5 to 3.0.
The sample application's MainController – residing in project mg-pizzastore-server – is basically implemented
as follows:
@Controller
@RequestMapping("/*")
public class MainController {
@RequestMapping(
value = "fetchpizzas",
method = RequestMethod.GET,
headers = "Accept=application/json")
public @ResponseBody
List<Pizza> fetchPizzasJson() {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 7/59
return getAllPizzas();
}
@RequestMapping(
value = "postorder",
method = RequestMethod.POST,
headers = "Content-Type=application/json")
public @ResponseBody
String postOrderJson(@RequestBody Order order) {
return "OK";
}
}
For corresponding code see: mg-pizzastore-server/de.metagear.pizzastore.service.MainController
We're configuring the server to listen to HTTP requests on the (exemplary) URLs http://localhost:8080/mg-
pizzastore-server/fetchpizzas and http://localhost:8080/mg-pizzastore-server/postorder in case the
application/json content type is supported, respectively, provided.
From the Emulator's device point of view, the server address ain't localhost or 127.0.0.1 (that refer to its
own loopback device) but 10.0.2.2.
As for the @Controller, @RequestMapping, and @ResponseCode annotations, see the Spring MVC reference
documentation. There is some configuration involved in WEB-INF/web.xml as well as in a couple of Spring
bean configuration files under the folder WEB-INF. These settings are both documented in my previous Spring
article and in the current Spring MVC reference documentation, both linked above.
There is no database interaction involved. Instead, the List<Pizza> getAllPizzas() method simply returns a
hard-coded list.
3.2.2. JSON Encoding
Somewhat "automagically", the server serves its response in a JSON-encoded format.
In the above code snippet you can see that the server looks for the client-side HTTP headers
Accept=application/json on HTTP GET requests, and Content-Type=application/json on HTTP POST requests.
If both classes, org.codehaus.jackson.map.ObjectMapper and org.codehaus.jackson.JsonGenerator, are on the
classpath, the Spring Framework will identify the ObjectMapper to perform object/JSON marshaling – as Spring's
MappingJacksonHttpMessageConverter has registered for the application/json MediaType and does employ that
JSON ObjectMapper.
We'll implement a custom MappingJacksonHttpMessageConverter later (in chapter Decoding the GZIP'ed
Response), however you don't really need to know about which Spring or JSON classes are involved, exactly.
(The Spring reference documentation itself doesn't even mention these internal matters.) – Just make sure
that jackson-core-asl-x.x.x.jar and jackson-mapper-asl-x.x.x.jar are on the classpath.
The sample's application's remoting techniques have been derived, and inspired, by the spring-android-
showcase project of the Spring Mobile Samples .
While mouse-clicking Windows guys might hate SpringSource for forcing them into an extra command line tool
(Git SCM ), the samples are to be checked out from the Source Code Management System (SCM) by issuing
the following command:
git clone git://git.springsource.org/spring-mobile/samples.git spring-mobile-samples
You'll also want to check out the Spring Android source code:
git clone --recursive git://git.springsource.org/spring-mobile/spring-android.git
On Ubuntu, git can be installed by simply issueing sudo apt-get install git at the command line. On
Windows, please consult the installation instructions at the Git Website .
3.2.3. GZIP Compression
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 8/59
Large HTTP downloads on mobile devices may be costly in measures of limited data tariffs, network speed,
and battery life. Thus we're compressing the HTTP response body before sending it to the client, using GZIP-
compression.
To do so, we're implementing a custom servlet Filter:
public class GzipFilter implements Filter {
// ...
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
if ((request instanceof HttpServletRequest)) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String str = httpRequest.getHeader("accept-encoding");
if ((str != null) && (str.indexOf("gzip") != -1)) {
GzipResponseWrapper wrapper = new GzipResponseWrapper(
httpResponse);
filterChain.doFilter(request, wrapper);
wrapper.finishResponse();

} else {
filterChain.doFilter(request, response);
}
}
}
// ...
}
For corresponding code see: mg-pizzastore-server/de.metagear.util.servlet.GzipFilter
In WEB-INF/web.xml, this GzipFilter is configured to intercept responses of Spring's DispatcherServlet
(which, itself, is configured to handle all requests):
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
...
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>gzipFilter</filter-name>
<filter-class>de.metagear.util.servlet.GzipFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>gzipFilter</filter-name>
<servlet-name>appServlet</servlet-name>
</filter-mapping>
For corresponding code see: mg-pizzastore-server/webapp/WEB-INF/web.xml
The GzipFilter employs the custom GzipResponseWrapper, which decorates the HttpServletResponse to output a
custom GzipResponseStream, which – in turn – uses the Java SE standard GZIPOutputStream library to encode
HttpServletResponse's ServletOutputStream.
See this article for a more in-depth discussion on this GZIP filter.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 9/59
Tomcat itself provides configuration options on GZIP compression, although these might not work with
Tomcat's mod_jk . – After all (and after the time of writing), using the PJL Compressing Filter might be
preferred over what I'd chosen for this sample application. – Looking at the source code, obviously, the PJL
Filter has been written with expert knowledge and, observably, with loads of love and care.
3.3. Android Client
Hey, finally the fun part is soon to come!
There are even screenshots to go! ;-)
3.3.1. Retrieving the Pizzas List
3.3.1.1. Overview
See the following class diagram for an overview of the main classes and interfaces involved:
Retrieving the Pizzas list, and ordering a Pizza, are two tasks of class PizzasListActivity, which is shown in
the center of the above diagram. These two tasks are accomplished by the classes HttpGetPizzasTask and
HttpPostTask (shown at the diagram's bottom), which both are components (has-a relation) of
HttpGetPizzasTask. The latter implements two certain interfaces that the task classes expect – in order to be
able to call back certain interface methods when the tasks are completed, successfully.
The upper part of the class diagram looks more complex than it is. Basically, class AsyncActivity (which can be
re-used in Android Activitys as well as in ListActivitys) shows a progress dialog before the task starts and
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 10/59
dismisses the dialog after it completes.
In case a task fails, interface AsyncActivity defines method onAsyncTaskFailed(), which needs to be
implemented by our main PizzasListActivity ( the diagram is inaccurate in not displaying the aforementioned
method).
– When the Android client application initially loads, the main Activity, PizzasListActivity, initializes the
download of an up-to-date List<Pizza>:
Several techniques and frameworks are involved.
The basic HTTP communication is accomplished by Spring's
RestTemplate , which is used by the Spring Android project.
The RestTemplate is configured to send a client-side HTTP GET
header: Accept=application/json. As mentioned in chapter JSON
Encoding, this header will be picked up at the server side as a
command to deliver JSON-encoded contents. Just like at the server
side, the Jackson JSON libraries need to be on the classpath to let
Spring Android decode JSON automatically.
From there, as the server-side response is GZIP-compressed, we're
extending Spring Android's MappingJacksonHttpMessageConverter to
decompress the server response. We're also extending Spring
Android's RestTemplate to be aware of our custom GZIP-enabled
HttpMessageConverter.
Not yet finally, the download task needs to be decoupled from the
main user interface (UI) thread into an independant thread that does
not block the UI. The Spring Android samples provide exemplary
classes to use the RestTemplate within an asynchronous Android
AsyncTask , which does threading by leveraging the
java.util.concurrent framework.
And finally, when the download thread returns, it needs to update
the state of the main UI thread. This is accomplished by using an
Android Handler .
As you can see, this is fairly complex. In the following chapters, each scope is discussed individually.
3.3.1.2. HttpGetPizzasListTask
The sample application's main Activity is PizzasListActivity, whose Activity.onCreate(Bundle) gets called
when it is created the first time. That's from where (through an intermediate method) new
HttpGetPizzasListTask(this, uri, 0).execute() gets called.
HttpGetPizzasListTask – a component of PizzasListActivity – extends Android's AsyncTask (take a brief look at
the API and APIDocs ), whose class structure the following screenshot shows:
Its main method is execute(Params...), which - simplified – calls the following methods, one after the other:
onPreExecute()
doInBackground(Params...) (which does its work in a separate, decoupled, thread)
onPostExecute(Result)
So, as mentioned in the chapter above, this task does not block the main UI thread. For details see below.
We've already mentioned Spring Android , its RestTemplate for REST/HTTP-based communications, and the
corresponding Spring Mobile Samples .
The latter's DownloadStatesTask extends Android's AsyncTask and leverages Spring Android's RestTemplate in a
separate thread.
Basically, it is defined as follows, implementing the aforementioned three basic methods:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 11/59
private class DownloadStatesTask extends AsyncTask<Void, Void, List<State>> {
@Override
protected void onPreExecute() {
// before the network request begins, show a progress indicator
showLoadingProgressDialog();
}

@Override
protected List<State> doInBackground(Void... params) {
try {
...
// Perform the HTTP GET request
ResponseEntity<State[]> responseEntity = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, State[].class);

// convert the array to a list and return it
return Arrays.asList(responseEntity.getBody());
}
catch(Exception e) {
Log.e(TAG, e.getMessage(), e);
}

return null;
}

@Override
protected void onPostExecute(List<State> result) {
// hide the progress indicator when the network request is complete
dismissProgressDialog();

// return the list of states
refreshStates(result);
}
}
}
For corresponding code see: DownloadStatesTask
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 12/59
3.3.1.3. Implementing a ProgressDialog
In the above code snippet (which stems from an Spring Android sample), the
sample DownloadStatesTask is an inner class of an Android Activity
implementation, on which the task calls the methods showLoadingProgressDialog()
and dismissProgressDialog().
In contrast to that, our own HttpGetPizzasListTask is a self-contained class that
calls these methods on an Activity that has been passed to its constructor (from where it is stored in an
instance variable). – This Activity therefore needs to implement our custom interface AsyncActivity, which is
defined as follows:
public interface AsyncActivity {
void showLoadingProgressDialog();
void dismissProgressDialog();
// ...
}
For corresponding code see: mg-library-
android/de.metagear.pizzastore.activity.async.AsyncActivity
Doing so, our pizza-downloading PizzasListActivity extends AbstractAsyncListActivity, which is shown below:
public abstract class AbstractAsyncListActivity extends ListActivity implements
AsyncActivity {
private Context context;
private ProgressDialog progressDialog;
public AbstractAsyncListActivity(Context context) {
context = context;
}
@Override
public void showLoadingProgressDialog() {
progressDialog = ProgressDialog.show(context,
context.getString(R.string.loadingProgressDialog_title),
context.getString(R.string.loadingProgressDialog_text),
true);
}
@Override
public void dismissProgressDialog() {
if (progressDialog != null) {
progressDialog.dismiss();
}
}
}
For corresponding code see: mg-library-
android/de.metagear.pizzastore.activity.async.AbstractAsyncListActivity
3.3.1.4. HTTP GET, and JSON Decoding, and Using the RestTemplate
In HttpGetPizzasListTask, the doInBackground(..) method is defined as follows:
@Override
protected List<Pizza> doInBackground(Void... params) {
try {
HttpHeaders requestHeaders = new HttpHeaders();
List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();
acceptableMediaTypes.add(MediaType.APPLICATION_JSON);
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 13/59
requestHeaders.setAccept(acceptableMediaTypes);
HttpEntity<?> requestEntity = new HttpEntity<Object>(requestHeaders);
RestTemplate restTemplate = new GzipJsonRestTemplate();
ResponseEntity<Pizza[]> responseEntity = restTemplate.exchange(uri,
HttpMethod.GET, requestEntity, Pizza[].class);
return Arrays.asList(responseEntity.getBody());
} catch (Exception e) {
...
return null;
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.task.HttpGetPizzasListTask
Basically, we just add an application/json header and, next, call RestTemplate.exchange(..).
Android's AsyncTask, from which our HttpGetPizzasListTask inherits, calls the doInBackground(..) method in a
separate thread and, next, makes the results available in onPostExecute(..).
Don't get confused because of the GzipJsonRestTemplate in the above code snippet - it's a custom subclass
of the standard RestTemplate, which is covered in the following chapter.
As already mentioned in chapter JSON Encoding (at the Server Side), the Jackson Core and Jackson Mapper
JAR files need to be on the classpath. – The RestTemplate's infrastructure will automatically detect them and
use them to decode the JSON response.
3.3.1.5. Decoding the GZIP'ed Response
Spring's RestTemplate internally prepares a List<HttpMessageConverter>s. If the aforementioned Jackson JAR files
are on the classpath, the RestTemplate also adds a MappingJacksonHttpMessageConverter to that list.
These HttpMessageConverters register for certain Java classes and Spring MediaTypes (like application/json) via
their methods canRead(..) and canWrite(..). If the RestTemplate finds a matching converter, that one will be
selected to perform the unmarshaling, resp., marshaling.
In the sample application, GzipEnabledMappingJacksonHttpMessageConverter extends
MappingJacksonHttpMessageConverter to additionally decompress the InputStream that gets returned by the
RestTemplate:
public class GzipEnabledMappingJacksonHttpMessageConverter extends
MappingJacksonHttpMessageConverter {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
if (isGzipEncoded(inputMessage)) {
InputStream inputStream = new GZIPInputStream(
inputMessage.getBody());
return objectMapper.readValue(inputStream, getJavaType(clazz));
} else {
return super.readInternal(clazz, inputMessage);
}
}
private boolean isGzipEncoded(HttpInputMessage inputMessage) {
HttpHeaders headers = inputMessage.getHeaders();
if (headers != null) {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 14/59
List<String> contentEncodings = headers.get("Content-Encoding");
if (contentEncodings != null) {
for (String contentEncoding : contentEncodings) {
if (contentEncoding != null
&& contentEncoding.toLowerCase().contains("gzip")) {
return true;
}
}
}
}
return false;
}
}
For corresponding code see: mg-library-
android/de.metagear.spring.web.GzipEnabledMappingJacksonHttpMessageConverter
We're simply overwriting readInternal(..), where we're decorating the RestTemplate's InputStream by a
java.util.zip.GZIPInputStream. Just like Spring's MappingJacksonHttpMessageConverter does, we're then letting
Jackson's ObjectMapper process the stream.
It's not possible by design to assign a HttpMessageConverter to the RestTemplate, thus we overwrite the latter:
public class GzipJsonRestTemplate extends RestTemplate {
protected List<HttpMessageConverter<?>> allMessageConverters;
protected static final boolean jacksonPresent = ClassUtils.isPresent(
"org.codehaus.jackson.map.ObjectMapper",
RestTemplate.class.getClassLoader())
&& ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator",
RestTemplate.class.getClassLoader());
public GzipJsonRestTemplate() {
super();
if (!jacksonPresent) {
throw new IllegalStateException("Jackson not present.");
}
initializeMessageConverters();
}
protected void initializeMessageConverters() {
allMessageConverters = new ArrayList<HttpMessageConverter<?>>();
allMessageConverters
.add(new GzipEnabledMappingJacksonHttpMessageConverter());
allMessageConverters.addAll(super.getMessageConverters());
}
@Override
public List<HttpMessageConverter<?>> getMessageConverters() {
return allMessageConverters;
}
@Override
protected <T> T doExecute(URI url, HttpMethod method,
RequestCallback requestCallback,
ResponseExtractor<T> responseExtractor) throws RestClientException {
// ...
ClientHttpRequest request = createRequest(url, method);
if (request.getHeaders() != null) {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 15/59
request.getHeaders().add("Accept-Encoding", "gzip,deflate");
}
// ...
}
}
For corresponding code see: mg-library-android/de.metagear.spring.web.GzipJsonRestTemplate
Basically, we're adding our GzipEnabledMappingJacksonHttpMessageConverter to the top of the
List<HttpMessageConverter<?>> that gets returned by the getMessageConverters() method.
Additionally, we're adding the client HTTP request header Accept-Encoding=gzip,deflate that our server
implementation requires.
Classes in the Spring Framework are seldomly well-extensible. For the GzipJsonRestTemplate, in example, we
had to copy & paste three private methods (with lots of code in them) that we'd rather like to leave
untouched.
Anyways, after all we can now drop-in replace the RestTemplate by our GzipJsonRestTemplate, like we do in our
HttpGetPizzasListTask class' doInBackground(..) method.
3.3.1.6. Updating the User Interface
The sample application's HttpGetPizzasListTask and HttpPostTask both extends Android's AsyncTask, which opens
a new, non-blocking thread internally and from there finally calls its onPostExecute(..) method. We're overriding
this method to provide the calling activity (PizzasListActivity in this case) with the task result – and trigger
updating its GUI.
In other words, the task thread needs to update the main GUI thread's state. And that's not allowed by default
– at least not when using Android's AlertDialogs and Toast messages.
Therefore our PizzasListActivity employs an Android Handler , to which we send a Message that is assigned a
custom Runnable of ours. The Handler processes its tasks asynchronously, in a MessageQueue, and finally runs the
Runnable in its own thread:
public class PizzasListActivity extends AbstractAsyncListActivity implements
HttpJsonPostTaskCaller, HttpJsonGetPizzasListTaskCaller<Pizza> {
protected Handler asyncTaskHandler;
// ...
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
asyncTaskHandler = new Handler();
// ...
}
// ...
@Override
public void onHttpPostTaskSucceeded(int requestCode, String serverMessage) {
Runnable callback = new Runnable() {
@Override
public void run() {
AlertDialog dialog = WidgetUtils.createOkAlertDialog(
PizzasListActivity.this, R.drawable.accept,
R.string.pizzasList_shoppingCart,
getString(R.string.pizzasList_orderCheckedOut), null);
dialog.show();
appState.getOrder().removePizzas();
showPizzasListActivity();
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 16/59
}
};
Message message = Message.obtain(asyncTaskHandler, callback);
message.sendToTarget();
}
@Override
public void onHttpGetTaskSucceeded(int requestCode, List<Pizza> pizzas) {
setListAdapter(pizzas); // this does not require employing a Handler
// for some reason
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
There are further ways to use Handlers and Messages, but I found this way to be most unobtrusive. – For
more information on using Handlers see the article Creating Dialogs (search and find "Example
ProgressDialog with a second thread") and the Handler ApiDoc .
3.3.2. Excursus: Android Activity Lifecycle
Activities are the main building blocks of Android applications – as they are for our sample application.
While this article does not discuss any details on that, it's most important for developers to be aware of the
Activity Lifecycle (so if you're unclear about that, you should read the linked document, at any rate).
For instance, in the sample application, the onPause(..) method is used to stop a LocationManager from
permanently requesting updates, the onPrepareOptionsMenu(..) method is used to enable or disable MenuItems
depending on the Activity's current state.
For lifecycle methods related to Menus see chapter Implementing Option Menus.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 17/59
3.3.3. PizzasListActivity: Faciliating Selecting From a List of Items
3.3.3.1. Displaying the Pizzas List
PizzasListActivity extends ListActivity, which displays a list of Views when a ListAdapter is assigned, which
provides a custom View for each list item.
After the List<Pizza> has been retrieved from the server (see chapter Retrieving the Pizzas List), such an
adapter gets assigned to the ListActivity:
setListAdapter(new PizzaViewAdapter(this, pizzas));
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 18/59
3.3.3.1.1. PizzaViewAdapter
The PizzaViewAdapter is defined as in the following code snippet (simplified for a start – see chapter Caching
Views):
public class PizzaViewAdapter extends BaseAdapter {
private LayoutInflater inflater;
private Context context;
private List<Pizza> pizzas;
public PizzaViewAdapter(Context context, List<Pizza> pizzas) {
this.context = context;
this.pizzas = pizzas;
this.inflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return pizzas.size();
}
@Override
public Pizza getItem(int position) {
return pizzas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 19/59
convertView = inflater.inflate(
R.layout.pizzaslist_view_tablelayout, null);

}
customizeView(position, convertView);
return convertView;
}
protected void customizeView(int position, View view) {
Pizza pizza = getItem(position);
TextView nameView = (TextView) view.findViewById(R.id.pizzasList_name);
nameView.setText(pizza.getName());
// ...
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.adapter.PizzaViewAdapter
We're overriding Adapter.getView(..) to provide the ListActivity with a custom view for each list item. That
View is inflated by a LayoutInflater from the given XML layout. In our customizeView(..) method we assign
the current Pizza's properties to TextViews and other View widgets.
The customizeView(..) method has been externalized from the getView(..) method in order to be
potentially overwritten by subclasses (e.g. the PizzaLineItemViewAdapter of our shopping cart, which
additionally displays the pizza line item's user-selected quantity, a remove pizza from list button, etc.).
3.3.3.1.2. Root XML Layout
The following code snippet shows the PizzasListActivity's root XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">
<View android:layout_width="fill_parent" android:layout_height="4sp" />
<TextView android:id="@+id/pizzasList_title" style="@style/formSubHeader"
android:text="@string/pizzasList_title" />
<View android:layout_width="fill_parent" android:layout_height="3sp" />
<ListView android:id="@+id/android:list" android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<TextView android:id="@id/android:empty" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="" />
</LinearLayout>
The ListView's and TextView's android:ids are predefined by the Android framework and are mandantory when
used with Android ListActivitys.
The ListView will be used to hold the list item Views discussed in the next chapter. The TextView will be
displayed only if the list is empty.
3.3.3.1.3. View XML Layout
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 20/59
The XML layout for each (Pizza) view is defined as follows:
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:stretchColumns="1">
<TableRow>
<TextView android:id="@+id/pizzasList_quantity" layout_width="15sp"
android:layout_height="wrap_content" android:text="qty"
android:paddingLeft="5sp" android:gravity="right" />
<TextView android:id="@+id/pizzasList_name"
android:layout_width="0dip" android:layout_height="wrap_content"
android:text="pizza name" android:textStyle="bold"
android:textScaleX="1" android:textSize="14sp"
android:paddingLeft="5sp" />
<TextView android:id="@+id/pizzasList_price"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="price" android:paddingLeft="5sp" android:gravity="right" />
<LinearLayout android:orientation="horizontal"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:paddingRight="5sp" android:paddingLeft="25sp">

<ImageView android:src="@drawable/add"
android:layout_width="wrap_content" android:layout_height="wrap_content" />
<ImageView android:id="@+id/pizzasList_removeButton"
android:src="@drawable/delete" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:paddingLeft="5sp" />
</LinearLayout>
</TableRow>
<TableRow>
<TextView android:id="@+id/pizzasList_ingredients"
android:layout_width="0dip" android:layout_height="wrap_content"
android:text="ingredients" android:textStyle="italic"
android:paddingLeft="30sp" android:layout_span="4" />
</TableRow>
</TableLayout>
For corresponding code see: mg-pizzastore-android/res/layout/pizzaslist_view_tablelayout.xml
There is a TableLayout with two TableRows. The second table row contains fewer views than the first one, so
we're assigning an android:layout_span (that would translate to colspan in HTML tables).
android:stretchColumns takes a comma-separated list of zero-based column indexes (or, alternatively, a * to
stretch all columns). These columns will be stretched if the TableLayout gets wider than its cells require. –
Analogously, there is the android:shrinkColumns property.
3.3.3.1.4. Caching Views
Getting back to our PizzaViewAdapter.getView(..) method discussed above, that method has been simplified in
the above code snippet for clarity.
Actually, it caches each View's child views (i.e., TextViews) in order to avoid unnessecary calls to
View.findViewById(int), by applying the ViewHolder pattern .
3.3.3.2. Selecting a Pizza into the Shopping Cart
In the PizzasListActivity's pizzas list, a pizza can be selected by clicking a list item. This functionality is
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 21/59
realized by assigning an AdapterView.OnItemClickListener to a ListActivity's ListView:
protected void preparePizzaClick() {
getListView().setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Pizza pizza = PizzasListActivity.getListAdapter()
.getItem(position);
appState.getOrder().add(pizza);
showPizzaSelectedDialog(pizza);
}
});
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
From within that overwritten onItemClick(..) method, we access the instance variable appState (of type
ApplicationState) and, next, implement a custom AlertDialog. Both tasks are discussed in the following two
chapters.
3.3.3.3. Excursus: Maintaining Global Application State
In chapter Data Model, we've discussed that our main model consists of an Order, which holds a list of
PizzaLineItems, plus a UserData instance along with its Address.
This model is used – queried and
edited – throughout most of our
application's activities, representing
the application's global state. This
state is held by our ApplicationState
class, extending
android.app.Application:
public class ApplicationState extends Application {
private Order order = new Order();
public void setOrder(Order order) {
this.order = order;
}
public Order getOrder() {
return order;
}
// ...
}
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.ApplicationState
This Application is set up in AndroidManifest.xml:
<manifest
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 22/59
package="de.metagear.pizzastore"
... >
<application android:icon="@drawable/icon" android:label="@string/app_name"
android:name="ApplicationState">
...
</application>
...
</manifest>
For corresponding code see: mg-pizzastore-android/AndroidManifest.xml
This Application can be accessed from all activities (but also services, etc.) – for instance, by the following
code:
ApplicationState appState = (ApplicationState) getApplication();
appState.getOrder().add(pizza);
3.3.3.4. Excursus: Creating Custom UI Messages
3.3.3.4.1. Creating an AlertDialog
From the OnItemClickListener.onItemClick(..) method, which has
been discussed in chapter Selecting a Pizza into the Shopping Cart,
we're opening an OK/Cancel AlertDialog:
protected void showPizzaSelectedDialog(Pizza pizza) {
final AlertDialog alertDialog = new AlertDialog.Builder(
PizzasListActivity.this).create();
alertDialog.setTitle(getResources().getString(
R.string.pizzasList_pizzaSelectedTitle, pizza.getName()));
alertDialog.setMessage(getResources().getString(
R.string.pizzasList_pizzaSelectedMsg));
alertDialog.setIcon(R.drawable.cart);
alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, getResources()
.getString(R.string.yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
PizzasListActivity.showPizzasCartListActivity();
}
});

alertDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getResources()
.getString(R.string.no), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
alertDialog.cancel();
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 23/59
Toast.makeText(PizzasListActivity.this,
R.string.pizzasList_pizzaAdded, Toast.LENGTH_LONG)
.show();
}
});
alertDialog.show();
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
3.3.3.4.2. Creating a Toast Message
When the user clicks "OK", the application displays the
PizzasCartListActivity screen. Otherwise, if they click "Cancel",
there's just a concise message popping up, a Toast , telling, "Pizza
Added":
Toast.makeText(PizzasListActivity.this,
R.string.pizzasList_pizzaAdded, Toast.LENGTH_LONG).show();
3.3.3.5. Implementing Option Menus
The Activity lifecycle methods related to Menus are:
boolean onCreateOptionsMenu(Menu)
boolean onPrepareOptionsMenu(Menu), and
boolean onOptionsItemSelected(MenuItem)
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 24/59
3.3.3.5.1. Creating MenuItems
In boolean onCreateOptionsMenu(Menu), we're creating a set of MenuItems:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menuItemsMap = new HashMap<Integer, MenuItem>();
menuItemsMap.put(
R.string.pizzasCart_pizzasList,
menu.add(R.string.pizzasCart_pizzasList).setIcon(
R.drawable.script_edit));
menuItemsMap.put(
R.string.pizzasList_viewShoppingCart,
menu.add(R.string.pizzasList_viewShoppingCart).setIcon(
R.drawable.cart));
// ...
return true;
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
We're adding these MenuItems to the instance variable protected Map<Integer, MenuItem> menuItemsMap
because subclasses (in this case PizzasCartListActivity) need to access and manipulate the entries. That
way, we can re-use our MenuItems.
3.3.3.5.2. Manipulating MenuItems Depending on the Current State
boolean onPrepareOptionsMenu(Menu) is the method that is called each time just before a Menu is about to be (re-
)drawn. This is the place to manipulate MenuItems to correspond to the current Activity state:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 25/59
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
setMenuItemState(R.string.pizzasCart_pizzasList, false, false);
setMenuItemState(R.string.pizzasList_viewShoppingCart, true,
!isShoppingCartEmpty());
// ...
return true;
}
protected void setMenuItemState(int itemTitleResID, boolean visible,
boolean enabled) {
MenuItem item = menuItemsMap.get(itemTitleResID);
item.setEnabled(enabled);
item.setVisible(visible);
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
3.3.3.5.3. Evaluating MenuItem Clicks
We're using boolean onOptionsItemSelected(MenuItem item) to react
on MenuItem clicks:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getTitle().equals(getString(R.string.pizzasList_viewUserData))) {
showUserDataActivity();
} else if (item.getTitle().equals(
getString(R.string.pizzasCart_pizzasList))) {
showPizzasListActivity();
} else if ...
}
return true;
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
3.3.3.6. Starting Other Activities
When the user clicks the Show Shopping Cart MenuItem, the PizzasCartListActivity is started:
protected void showPizzasCartListActivity() {
Intent intent = new Intent(this, PizzasCartListActivity.class);
startActivity(intent);
}
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 26/59
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
See chapters Passing Data to an Activity and Passing Result Data Back to the Calling Activity for
corresponding, expanded, information.
3.3.3.7. Re-Using Layout Styles
Attributes that are to be commonly re-used in XML layout files can be defined in central resource files. By
convention, the default file name is styles.xml, however that's not mandantory.
The following code snippet shows the sample application's styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="formSubHeader">
<item name="android:background">@drawable/customshape</item>
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
<item name="android:paddingLeft">5dip</item>
<item name="android:textSize">18sp</item>
<item name="android:textStyle">bold|italic</item>
</style>
<style name="formFieldLabel">
<item name="android:paddingRight">10dip</item>
<item name="android:layout_height">wrap_content</item>
</style>
<style name="formField">
<item name="android:layout_width">0dip</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_weight">1</item>
</style>
<style name="formField.text">
<item name="android:singleLine">true</item>
<item name="android:maxLength">50</item>
</style>
</resources>
For corresponding code see: mg-pizzastore-android/res/values/styles.xml
The formField.text style inherits its attributes from the formField style, inducted by the dot-style naming
convention.
In XML layout files these styles are referenced by using the style attribute, for instance:
<TextView style="@style/formFieldLabel" android:text="@string/userInfo_name" />
For further information see the article Applying Styles and Themes .
3.3.3.8. Drawing a View with Rounded Corners
You're surely wondering how this design pope managed to draw such cool rounded edges on that "Pizzas
List" TextView. ;-) – Well, here it is:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 27/59
android:shape="rectangle">
<solid android:color="#3e3e3e" />
<corners
android:bottomRightRadius="7dp"
android:bottomLeftRadius="7dp"
android:topLeftRadius="7dp"
android:topRightRadius="7dp" />
</shape>
For corresponding code see: mg-pizzastore-android/res/drawable/customshape.xml
This drawable is referenced in PizzasListActivity's main layout view definition ...
<LinearLayout ...>
...
<TextView style="@style/formSubHeader"
android:text="@string/pizzasList_title" />
...
</LinearLayout>
For corresponding code see: mg-pizzastore-android/res/layout/pizzaslist_linearlayout.xml
... which in turn references the corresponding style attribute:
<resources>
<style name="formSubHeader">
<item name="android:background">@drawable/customshape</item>
...
</style>
...
</resources>
For corresponding code see: mg-pizzastore-android/res/values/styles.xml
Unfortunately, Android XML Drawables are not really documented at all; nevertheless, there's a third party's
documentation .
Apropos visual design and best practices, we'd need to note that our sample application uses icons that
don't comply with Android's Icon Design Guidelines .
3.3.4. PizzasCartListActivity: Faciliating Reviewing The User's
Shopping Cart
The pizzas cart activity lists the pizzas that the user has selected into their shopping cart (represented by
PizzaLineItems: Pizzas plus their respective user-selected quantities).
3.3.4.1. Inheriting from PizzasListActivity
As PizzasCartListActivity shares most of its functionality with PizzasListActivity, it extends the latter, so
we're overriding a couple of methods.
There are differences to PizzasListActivity in terms of which menu items to show or hide:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 28/59
While in the PizzasListActivity the data model is a List<Pizza>,
PizzasCartListActivity's data model is a List<PizzaLineItem>.
Accordingly, the ListAdapter is of type PizzaLineItemViewAdapter.
The PizzaLineItemViewAdapter extends the PizzaViewAdapter, which
has been discussed in chapter Displaying the Pizzas List. It uses the
same XML layout (pizzaslist_view_tablelayout.xml), however
customizes certain elements (the quantity TextView and the
removeButton ImageView). – That's where our PizzaViewAdapter's
protected void customize*(..) methods come in, which are
overridden in PizzaLineItemViewAdapter:
@Override
protected <T extends Pizza> void customizeView(T pizza, ViewHolder holder) {
super.customizeView(pizza, holder);
customizeQuantityTextView(holder.getQuantityView(),
String.valueOf(((PizzaLineItem) pizza).getQuantity()));
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.adapter.PizzaLineItemViewAdapter
3.3.4.2. Adding and Removing Pizzas to and from the Cart
When the user clicks on a list item in the PizzasCartListActivity ...
@Override
protected void preparePizzaClick() {
getListView().setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
Pizza pizza = getListAdapter().getItem(position);
showPizzaSelectedDialog(pizza);
}
});
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasCartListActivity
... a corresponding AlertDialog is shown.
See chapter Creating an AlertDialog for details.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 29/59
From this dialog, from method showPizzaSelectedDialog(Pizza), the
application's underlying Data Model is manipulated:
public void add(Pizza pizza) {
PizzaLineItem pizzaItem = getPizzaLineItem(pizza);
if (pizzaItem != null) {
pizzaItem.setQuantity(pizzaItem.getQuantity() + 1);
} else {
pizzas.add(new PizzaLineItem(pizza));
}
}
For corresponding code see: mg-pizzastore-shared/de.metagear.pizzastore.model.order
After that, the ListActivity is bound to the data model (PizzasCartListActivity.setListAdapter()), the screen
state gets updated (ListActivity.setContentChanged()), and an OnClickListener gets newly re-attached
(PizzasCartListActivity.preparePizzaClick()):
protected void refreshGUI() {
setListAdapter();
onContentChanged();
preparePizzaClick();
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasCartListActivity
In effect, the new model's state (PizzaLineItem quantitys for instance) will be reflected in the GUI.
3.3.4.3. Finally, Placing the Order
If the user has filled in their user data, and if they've selected some pizzas into their shopping cart as well, the
Check Out Shopping Cart menu item gets enabled in PizzasListActivity.onPrepareOptionsMenu(Menu).
When the user clicks that menu item, and confirms the subsequent dialog,
the application HTTP POSTs the Order instance (containing a
List<PizzaLineItem> and a UserData) instance to the remote server.
Under the hood, there are techniques involved that mostly have been
discussed already: The HttpPostTask works equivalently to the
HttpGetPizzasListTask, not blocking the UI thread by extending a AsyncTask
and utilizing Spring's RestTemplate. When the task has finished
(succeeded or failed), the UI thread is updated to inform the user.
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.PizzasListActivity
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.task/HttpPostTask
See chapter HttpGetPizzasListTask for a more detailed discussion.
3.3.5. UserDataFormActivity: A Validating Input Form
When the user clicks the user data MenuItem at the pizzas list or shopping cart view, the
UserDataFormActivity is started.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 30/59
This form can be divided into two areas: fields related to the user's
location – to be used with Android location-based services (LBS)
– and additional user data.
As the sample application does provide location-based services,
there is the AddressFormActivity, which considers the location-
based fields only. – The AddressFormActivity can (could) be used
standalone, or for other purposes.
From there, the UserDataFormActivity extends the
AddressFormActivity by adding the additional user data form fields
and the action buttons.
3.3.5.1. XML Layout
3.3.5.1.1. User Data
The user data XML layout is defined as follows:
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:stretchColumns="1" android:shrinkColumns="0">
<include layout="@layout/address_form_merge" />
<TableRow>
<TextView style="@style/formSubHeader" android:layout_span="2"
android:text="@string/userInfo_userData" />
</TableRow>
<View android:layout_width="fill_parent" android:layout_height="4sp" />
<TableRow>
<TextView style="@style/formFieldLabel" android:text="@string/userInfo_name" />
<de.metagear.android.view.ValidatingEditText
android:id="@+id/userInfo_name" style="@style/formField.text" />
</TableRow>
<TableRow>
<TextView style="@style/formFieldLabel" android:text="@string/userInfo_phone" />
<de.metagear.android.view.ValidatingEditText
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 31/59
android:id="@+id/userInfo_phone" style="@style/formField.text" />
</TableRow>
<TableRow>
<TextView style="@style/formFieldLabel" android:text="@string/userInfo_email" />
<de.metagear.android.view.ValidatingEditText
android:id="@+id/userInfo_email" style="@style/formField.text" />
</TableRow>
<View android:layout_height="2dip" />
<TableRow>
<View />
<LinearLayout android:orientation="horizontal" style="@style/formField">
<Button android:id="@+id/userInfo_saveButton" style="@style/formField"
android:text="@string/userInfo_save" />
<Button android:id="@+id/userInfo_resetButton" style="@style/formField"
android:text="@string/userInfo_reset" />
</LinearLayout>
</TableRow>
</TableLayout>
For corresponding code see: mg-pizzastore-android/res/layout/userinfo_form_tablelayout.xml
We're using a TableLayout with its android:stretchColumns attribute (that takes a zero-based, comma-
separated, list of column indexes, and that's specifying which columns to stretch if the table is wider than its
columns require). There's also the android:layout_span attribute on Views.
The address layout is included from a separate file.
Empty Views are used as spacers.
There's the style attribute to apply a style that has been defined in /res/values/styles.xml. See chapter
Re-Using Layout Styles for details.
de.metagear.android.view.ValidatingEditText is a reference to a custom control of ours, which extends
Android's EditText. See chapter ValidatingEditText for more information.
3.3.5.1.2. Address
The address XML layout is defined as follows:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent"
android:stretchColumns="1" android:shrinkColumns="0">
<View android:layout_width="fill_parent" android:layout_height="4sp" />
<TableRow>
<TextView style="@style/formSubHeader" android:layout_span="2"
android:text="@string/address_location" />
</TableRow>
<View android:layout_width="fill_parent" android:layout_height="4sp" />
<TableRow>
<TextView style="@style/formFieldLabel" android:text="@string/address_street" />
<de.metagear.android.view.ValidatingEditText
android:id="@+id/address_street" style="@style/formField.text" />
</TableRow>
<TableRow>
<TextView style="@style/formFieldLabel" android:text="@string/address_zipCode" />
<de.metagear.android.view.ValidatingEditText
android:id="@+id/address_zipCode" style="@style/formField.text" />
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 32/59
</TableRow>
<TableRow>
<TextView style="@style/formFieldLabel" android:text="@string/address_city" />
<de.metagear.android.view.ValidatingEditText
android:id="@+id/address_city" style="@style/formField.text" />
</TableRow>
<TableRow>
<TextView style="@style/formFieldLabel" android:text="@string/address_country" />
<de.metagear.android.view.ValidatingSpinner
android:id="@+id/address_country" style="@style/formField" />
</TableRow>
</merge>
For corresponding code see: mg-pizzastore-android/res/layout/address_form_merge.xml
The merge element is a container that does not represent a View (and therefore saves some resources). It's
used in conjunction with a parent XML layout's include declaration.
In addition to the aforementioned custom ValidatingEditText, we're also using a custom ValidatingSpinner.
3.3.5.2. A Custom, Optionally Selectable, Spinner
Like a ListActivity, a Spinner is populated with List items that are
provided via an Adapter.
By default, there is no "unselected" state, and the first list item is
preselected, even when the user hasn't interacted with the spinner.
This could be solved by re-arranging the source list to provide an empty first item, however we address the
issue in a generic, object-orientated, fashion.
3.3.5.2.1. Decorating a SpinnerAdapter
The OptionalSelectionSpinnerAdapter decorates Android's SpinnerAdapter implementation as follows:
public class OptionalSelectionSpinnerAdapter extends BaseAdapter implements
SpinnerAdapter {
protected Context context;
protected SpinnerAdapter adapter;
public OptionalSelectionSpinnerAdapter(Context context,
SpinnerAdapter adapter) {
this.context = context;
this.adapter = adapter;
}
@Override
public int getCount() {
return adapter.getCount() + 1;
}
@Override
public Object getItem(int position) {
if (position == 0) {
return new TextView(context);
} else {
return adapter.getItem(position – 1);
}
}
@Override
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 33/59
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (position == 0) {
return new TextView(parent.getContext());
} else {
return adapter.getView(position – 1, null, parent);
}
}
@Override
public View getDropDownView(int position, View convertView, ViewGroup parent) {
View newView;
if (position == 0) {
newView = new TextView(parent.getContext());
} else {
newView = adapter.getDropDownView(position – 1, null, parent);
}
return newView;
}
}
For corresponding code see: mg-library-
android/de.metagear.android.view.OptionalSelectionSpinnerAdapter
We might have implemented Caching Views (the ViewHolder Pattern) in the above code sample.
3.3.5.2.2. Overwriting Spinner.setAdapter(..)
Basically, we're overriding setAdapter(SpinnerAdapter) to decorate the given SpinnerAdapter if that's not
already been done in the calling code:
public class OptionalSelectionSpinner extends Spinner {
// ...
@Override
public void setAdapter(SpinnerAdapter adapter) {
if (adapter instanceof OptionalSelectionSpinnerAdapter) {
super.setAdapter(adapter);
} else {
super.setAdapter(new OptionalSelectionSpinnerAdapter(context,
adapter));
}
}
}
For corresponding code see: mg-library-
android/de.metagear.android.view.OptionalSelectionSpinner
3.3.5.3. Populating and Saving the Form
3.3.5.3.1. Design and Workflow
Please note that this section does not perfectly comply with the sample project's code.
The following sequence diagram shows the involvement of different participants in managing the application's
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 34/59
central UserData instance:
Participants are the following classes:
ApplicationState: Central instance being available to all
Application-aware class instances within a certain session,
holding global application state.
It provides a UserData instance via getOrder().getUserData().
See also chapter Maintaining Global Application State.
UserDataFormActivity: Displays the user data form, from which
the UserData data model gets populated.
See also chapter UserDataFormActivity: A Validating Input
Form.
UserDataViewAdapter: A helper class that translates between the
UserData model and the corresponding EditText (etc.) fields of the
UserDataFormActivity.
See also chapter Adapting the User Data Form to the Data
Model.
SharedPrefsAdapter: A helper class that persists application state
between Application shutdowns and startups, utilizing the Android
SharedPreferences.
See also chapter Dealing with Android's SharedPreferences.
There's the following workflow involved:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 35/59
1. At application startup, the ApplicationState retrieves a UserData instance from the Android
SharedPreferences by using the SharedPrefsManager. If no UserData has been saved previously, the
SharedPrefsManager creates a new, empty, instance.
2. The UserDataFormActivity, when it comes up, queries the ApplicationState for a current UserData
instance and ...
3. ... lets the UserDataViewAdapter initialize its EditText (etc.) values (the form fields).
4. When the user clicks the Save button (provided that everything is valid), the UserDataFormActivity
utilizes the UserDataViewAdapter again – this time to populate a UserData instance from its Views' values.
5. This state change gets propagated to the - globally accessible ApplicationState ...
6. ... and – for future use beyond the current session – persisted to the Android SharedPreferences using
the SharedPrefsAdapter.
The underlying data model gets populated from Android's SharedPreferences early at application startup because
existence and validity of the UserData is required for eventually checking out the shopping cart:
protected void initializeUserDataFromSharedPrefs() {
try {
UserData userData = SharedPrefsAdapter.retrieve(this, UserData.class);
appState.getOrder().setUserData(userData);
} catch (Throwable t) {
// ...
}
}
For corresponding code see: mg-library-
android/de.metagear.pizzastore.activity.PizzasListActivity
When in the UserDataFormActivity the Save button gets clicked (and the form is valid), a UserData instance is
created from the form values and persisted to the Android SharedPreferences:
protected void saveUserData() {
UserData userData = UserDataViewAdapter.fromView(rootView);
UserDataViewAdapter.toSharedPrefs(this, userData);
appState.getOrder().setUserData(userData);
}
3.3.5.3.2. Dealing with Android's SharedPreferences
SharedPreferences are used to persist and retrieve data – Strings and primitives – between application
sessions.
In the sample application, we've standardized corresponding behavior into a separate class:
public class SharedPrefsAdapter {
public static <T> T retrieve(Context context, Class<T> valueType)
throws JsonParseException, JsonMappingException, IOException,
InstantiationException, IllegalAccessException {
String key = valueType.getName();
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
String value = prefs.getString(key, null);
if (value == null) {
return valueType.newInstance();
}
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 36/59
return new ObjectMapper().readValue(value, valueType);
}
public static <T> void persist(Context context, T object)
throws JsonGenerationException, JsonMappingException, IOException {
String key = object.getClass().getName();
String value = new ObjectMapper().writeValueAsString(object);
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(context);
Editor editor = prefs.edit();
editor.putString(key, value);
editor.commit();
}
}
For corresponding code see: mg-library-android/de.metagear.android.util.SharedPrefsAdapter
Amongst other options, SharedPreferences can be private to the corresponding Application, or publically
available to other installed applications. We're using the PreferenceManager.getDefaultSharedPreferences(..)
method, which considers private data only.
We've already mentioned that SharedPreferences only work with Strings and primitive data types. Therefore
we're using the Jackson/JSON ObjectMapper to marshal, resp. unmarshal, objects to, resp. from, JSON Strings.
(See chapter Using the JSON ObjectMapper with the Model Objects for implications on the model objects.)
When persisting data to the SharedPreferences, a kind-of-transactional SharedPreferences.Editor needs to be
employed (see the above code snippet).
3.3.5.3.3. Adapting the User Data Form to the Data Model
Getting back to the UserDataFormActivity that holds the user data form, this activity connects to its underlying
data model through employing the UserDataViewAdapter.
In addition to connecting the view to the data model, the UserDataViewAdapter also performs validation.
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.adapter.UserDataViewAdapter
For what it's worth to mention, adapters are used throughout many scopes of the sample application,
connecting different application layers or coercing transformation of different data structures – implementing
the separation of concerns paradigm.
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.adapter
For corresponding code see: mg-pizzastore-android/de.metagear.pizzastore.view.adapter
3.3.5.4. A Custom Form Field Validation Framework
Android does not really provide a framework to validate forms, and higher-level third-party validation
frameworks don't seem to exist. Thus we've implemented our own one – just like other people did .
3.3.5.4.1. ViewValidator Interface
The central interface, which all validating views must implement, is the ViewValidator:
public interface ViewValidator {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 37/59
boolean validate(View view);
/**
* Returns a localized error message (even when validation has been successful).
* @param caption the display name of the TextView (i.e., "ZIP Code")
* @return localized error message; never null
*/
String getErrorMessage(String caption);
}
3.3.5.4.2. TextView Validators
Our framework provides a number of ViewValidator implementations that validate TextViews or derived classes:
For corresponding code see: mg-library-
android/de.metagear.android.view.validation.textview
The MinLengthValidator checks if the TextView's text has a given minimum length. The
MatchingTextViewValidator checks if the TextView's text matches the text of another TextView (this could be
used with Password and Password (repeat) form fields, for instance).
The other ViewValidators all extend RegexValidator, which is implemented as in the following code snippet:
public class RegexValidator implements ViewValidator {
protected Context context;
protected Pattern pattern;
public RegexValidator(Context context, String regex) {
context = context;
pattern = Pattern.compile(regex);
}
@Override
public boolean validate(View view) {
Matcher matcher = pattern.matcher(((TextView) view).getText());
return matcher.matches();
}
@Override
public String getErrorMessage(String caption) {
return context.getString(R.string.validation_regex, caption,
pattern.toString());
}
}
3.3.5.4.3. Self-Validating Views
Currently, our little framwork provides a TextView and an OptionalSelectionSpinner, which both self-validate at
certain user-initiated GUI events – or, alternatively, are initiated programmatically.
Validating views are required to implement the ValidatingView interface, which is listed below:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 38/59
/**
* A View that validates itself by using the given
* ViewValidator.
* If not valid, the view shall display an error icon.
*/
public interface ValidatingView {
/**
* Registers the given validator.
*
* @param validator
* @param fieldDisplayNameForErrorMsg
* Localized display field name, i.e., "City" or "Postal Code".
*/
void setValidator(ViewValidator validator,
String fieldDisplayNameForErrorMsg);
/**
* Causes the view to show or hide an error icon depending on its validity.
*/
void flagOrUnflagValidationError();
/**
* Removes any error icons from the view.
*/
void unflagValidationError();
/**
* Returns whether the view's value is valid.
*/
boolean isValid();
}
3.3.5.4.4. ValidatingEditText
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 39/59
The AFAIK only facility that Android provides for form validation is
TextView.setError(CharSequence). This draws an error icon and – if the
widget gains focus – a descriptive text in popup box. Passing null to
that method unflags the error.
Design enthusiasts will instantly realize that the theme of Android's
rudimental form validation does not match Android's default – or
even custom – theme. – We'd urgently need a comprehensive, high-level, validation framework for Android.
3.3.5.4.4.1. Performing Validation
In our ValidatingEditText, we extend EditText, implementing our ViewValidator:
public class ValidatingEditText extends EditText implements ValidatingView {
private ViewValidator validator;
private String fieldDisplayNameForErrorMsg;

// ...
@Override
public void setValidator(ViewValidator validator,
String fieldDisplayNameForErrorMsg) {
this.validator = validator;
this.fieldDisplayNameForErrorMsg = fieldDisplayNameForErrorMsg;
// ...
}
@Override
public void flagOrUnflagValidationError() {
String msg = isValid() ? null : validator
.getErrorMessage(fieldDisplayNameForErrorMsg);
setError(msg);
}
@Override
public void unflagValidationError() {
setError(null);
}
@Override
public boolean isValid() {
return validator.validate(this);
}
// ...
}
We also register a number of listeners to react on user-initiated GUI events: an OnLongClickListener, an
OnKeyListener (for capturing the Enter key press event), and an OnFocusChangeListener (to validate on lost
focus events).
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingEditText
3.3.5.4.4.2. Setting Soft Keyboard Behavior
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 40/59
As a side effect, we're
also setting the EditText's
InputType to reflect the
preferred input scheme,
i.e., numbers for ZIP Code
or characters for City.
This setting will get picked
up by the soft keyboard (if
available).
protected void initInputType(ViewValidator validator) {
if (validator instanceof EmailValidator) {
setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS);
} else if (validator instanceof PhoneNumberValidator) {
setInputType(InputType.TYPE_CLASS_PHONE);
} else if (validator instanceof ZipCodeValidator) {
setInputType(InputType.TYPE_CLASS_NUMBER);
} else if (!isPassword()) {
setInputType(InputType.TYPE_CLASS_TEXT);
} else if (isPassword()) {
setInputType(InputType.TYPE_TEXT_VARIATION_PASSWORD);
}
setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
}
protected boolean isPassword() {
TransformationMethod method = getTransformationMethod();
return method != null && method instanceof PasswordTransformationMethod;
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingEditText
3.3.5.4.5. ValidatingSpinner
In contrast to TextViews, a Spinner does not provide a setError(..)
method and also does not provide means to show that error icon and
error message popup box.
We're overwriting Spinner (more specifically,
OptionalSelectionSpinner, see chapter A Custom, Optionally Selectable, Spinner) to implement our
ValidatingView interface, and we're drawing the error icon manually as shown below.
3.3.5.4.5.1. Drawing the Error Icon
By looking at Android's TextView source code, we found the error icon on disk (indicator_input_error.png)
and copied that into our project.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 41/59
The following code snippet draws this Bitmap on top of the Spinner's Canvas:
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
if (errorIconEnabled && !isValid()) {
drawErrorIcon(canvas);
}
}
protected void drawErrorIcon(Canvas canvas) {
final int ICON_RIGHT_MARGIN = 40;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.indicator_input_error);
float left = getWidth() – ICON_RIGHT_MARGIN – bitmap.getWidth();
float top = (getHeight() – bitmap.getHeight()) / 2;
// ...
canvas.drawBitmap(bitmap, left, top, new Paint());
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingSpinner
Note the central instance variable errorIconEnabled, which gets discussed in the following chapter.
3.3.5.4.5.2. Events that Trigger Validation
The error icon is shown, resp., hidden, when either the user selects a Spinner item or when validation is
requested programmatically. Note that the Spinner View will be redrawn when the user selects an item, or
when invalidate() is called (this ought to sound familiar to Swing developers).
@Override
public void setSelection(int position, boolean animate) {
super.setSelection(position, animate);
errorIconEnabled = true;
}
@Override
public void setSelection(int position) {
super.setSelection(position);
errorIconEnabled = true;
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingSpinner
@Override
public void flagOrUnflagValidationError() {
errorIconEnabled = true;
invalidate();
}
@Override
public void unflagValidationError() {
errorIconEnabled = false;
invalidate();
}
For corresponding code see: mg-library-android/de.metagear.android.view.ValidatingSpinner
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 42/59
3.3.5.4.5.3. There's No Such Error Message Popup Box as a Free
Lunch
Looking at the source code that enables TextViews' error message popups, we found that this ain't re-usable
at all. – We'd have to re-implement that functionality from scratch and therefore resigned on it for now.
A comprehensive validation framework for Android – like the ones that, e.g., JavaServer Faces or the
Spring Framework provide – would be really desirable.
3.3.6. MapViewActivity: Interacting with Maps and Location-Based
Services
3.3.6.1. General Setup
Because of a least one blocking Bug related to location-based services and the Android emulator,
the functionality covered in this chapter currently cannot be used on Android 2.2 and 2.3 (anything > API level
7). That's why our project is Android 2.1-based.
Android projects that use MapView and related libraries need to have
the corresponding Google APIs on the classpath. These can be
installed via the Android SDK and AVD Manager and need to be
referenced in the Eclipse project setup:
The Google Maps library also needs to be setup in AndroidManifest.xml. Additionally, as it accesses Google
services on the internet, our application needs to claim the corresponding permissions:
<manifest ...>
<application ...>
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 43/59
<activity ...>
</activity>
<uses-library android:name="com.google.android.maps" />
</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
For corresponding code see: mg-pizzastore-android/AndroidManifest.xml
The sample application's map activities can be started via the user data form's menu.
Our MapViewActivity extends Google's MapActivity, which is used in conjunction with Google's MapView. The
MapActivity's main XML layout is shown in the following code snippet.
For using the MapView control, a Google Maps API Key is required.
The keytool mentioned in the linked article is part of the (Sun) Java SDK (i.e., not necessarily the keytool
that may be on the PATH on Linux distributions).
In contrast to what the linked article (and other sources on the web) tell, a Google account is not required to
generate the API key.
The following code snippet shows the MapViewActivity's XML layout file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent">

<com.google.android.maps.MapView
android:id="@+id/mapView" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:clickable="true"
android:apiKey="Google Maps API Key " />
</LinearLayout>
For corresponding code see: mg-pizzastore-android/res/layout/map_linearlayout.xml
Getting back to the menu shown in the screenshot above, there are two custom MapViewModes in the sample
application, which provide slightly differing functionality. These are discussed in the following chapters.
3.3.6.2. Assuring Preconditions Upon the Device's State
The Google MapView control requires accessing the internet. From our MapViewActivity we're calling our
AndroidDeviceUtils.isNetworkConnected(..) to assure that:
public static boolean isNetworkConnectedElseAlert(Context context,
Class<?> callingClass) {
boolean isOnline = isNetworkConnected(context);
if (!isOnline) {
ErrorHandler.logAndAlert(context, callingClass,
context.getString(R.string.androidUtil_error_notOnline));
}
return isOnline;
}
public static boolean isNetworkConnected(Context context) {
ConnectivityManager manager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
return manager.getActiveNetworkInfo().isConnected();
}
For corresponding code see: mg-library-android/de.metagear.android.util.AndroidDeviceUtils
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 44/59
Additionally, location-based services require that - in our case – GPS is enabled. – Our application checks
whether GPS is enabled, and if it isn't, optionally displays the corresponding Android dialog – and then re-checks
that condition:
private static boolean isGpsEnabledElseOptionallyEnableIt(Context context) {
boolean enabled = AndroidDeviceUtils.isGpsEnabled(context);
if (!enabled) {
AndroidDeviceUtils.showGpsDisabledAlert(context);
enabled = AndroidDeviceUtils.isGpsEnabled(context);
}
return enabled;
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
The Android Location & security settings dialog is getting called by
starting an Activity with an implicit Intent:
context.startActivity(new Intent(
android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
For corresponding code see: mg-library-
android/de.metagear.android.util.AndroidDeviceUtils
3.3.6.3. Displaying a Given Location in the Map Viewer
From the Show Map menu action, we're starting our MapViewActivity
and display the location that bases on the data in the address
form.
Using our utility StringUtils.join(..), these values are translated to
the string "Am Hohen Ufer 3A, 30159, Hannover, Germany".
This String gets translated (geocoded) to a GeoPoint by using a
GeoCoder:
protected GeoPoint getGeoPointFromLocationName(String locationName) {
Geocoder geocoder = new Geocoder(this);
final int MAX_RESULTS = 1;
try {
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 45/59
List<Address> addresses = geocoder.getFromLocationName(
locationName, MAX_RESULTS);
if ((addresses == null) || (addresses.size() == 0)) {
Toast.makeText(
this,
getResources().getString(
R.string.mapViewer_noResultsForAddress,
locationName), Toast.LENGTH_LONG).show();
return null;
}
Address address = addresses.get(0);
GeoPoint geoPoint = new GeoPoint(
(int) (address.getLatitude() * 1E6),
(int) (address.getLongitude() * 1E6));
return geoPoint;
} catch (IOException e) {
// ...
return null;
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
From here we can ...
mapView.getController().animateTo(geoPoint);
... and ...
mapView.getOverlays().add(new LocationIndicatorOverlay(geoPoint));
... to draw that little blue dot at our current location.
3.3.6.3.1. Highlighting a Location on the MapView Control
A MapView allows for adding Overlays via its getOverlays().add(Overlay) method.
Our LocationIndicatorOverlay draws a little blue dot on top of the MapView,
indicating the coordinates specified by the GeoPoint passed to its constructor.
The coordinates in pixels are obtained by using the MapView's Projection; the final
blue circle is drawn by using android.graphics APIs:
public class LocationIndicatorOverlay extends Overlay {
protected GeoPoint geoPoint;
public LocationIndicatorOverlay(GeoPoint geoPoint) {
this.geoPoint = geoPoint;
}
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
super.draw(canvas, mapView, shadow);

Projection projection = mapView.getProjection();
Point point = new Point();
projection.toPixels(geoPoint, point);
Paint paint = new Paint();
paint.setARGB(250, 0, 0, 255);
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 46/59
canvas.drawCircle(point.x, point.y, 5, paint);
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.map.LocationIndicatorOverlay
3.3.6.4. The Address from Map Activity
The Address from Map activity involves several tasks and actions:
querying the Android device for its current location using the LocationManager
displaying that location in the MapView control
letting the user select an individual location in the MapView
translating coordinates to an Address (reverse geocoding)
3.3.6.4.1. Excursus: Mocking the Location in the Android Emulator
For a short time, Google Maps provides the LatLng Tooltip feature that shows the
current latitude and longitude at the mouse cursor (when pressing Shift).
To enable it, click the Google Maps Labs icon (currently titled, "New!") at the upper-
right corner of the screen. In the following dialog, enable the LatLng Tooltip feature.
Finally, using these coordinates, the Android emulator's current location can be mocked in Eclipse's Emulator
Control view.
3.3.6.4.2. Obtaining a Device's Current Location
To utilize the LocationManager, the proper permissions need to be set up in AndroidManifest.xml:
<manifest ... />
...
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
</manifest>
For corresponding code see: mg-pizzastore-android/AndroidManifest.xml
Using the GPS_PROVIDER requires ACCESS_FINE_LOCATION; using the NETWORK_PROVIDER requires
ACCESS_COARSE_LOCATION, only. (ACCESS_FINE_LOCATION covers ACCESS_COARSE_LOCATION.)
In our MapViewActivity we're using the Android LocationManager to determine the device's current Location
(actually, the last known location as the LocationManager works asynchronously):
protected void initializeLocationManager() {
locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
criteria.setPowerRequirement(Criteria.NO_REQUIREMENT);

locationProvider = locationManager.getBestProvider(criteria, true);
lastKnownLocation = locationManager
.getLastKnownLocation(locationProvider);
}
For corresponding code see: mg-pizzastore-
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 47/59
android/de.metagear.pizzastore.activity.MapViewActivity
The location provider in this case should always be GPS; I haven't checked other providers – which may
return less accurate results.
The LocationManager's Location getLastKnownLocation(String) method is not guaranteed to immediately return a
valid Location, thus we're asking the location manager to constantly request location updates in a loopback:
@Override
protected void onResume() {
super.onResume();
if (mapViewDisplayMode == MapViewDisplayMode.SELECT) {
locationManager
.requestLocationUpdates(locationProvider, 0, 0, this);
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
In case our Activity vanishes to the background, we want the location manager to stop that task:
@Override
protected void onPause() {
super.onPause();
if (mapViewDisplayMode == MapViewDisplayMode.SELECT) {
locationManager.removeUpdates(this);
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
In order to be informed when the LocationManager retrieves a Location (i.e., for the first time), the
MapViewActivity implements the LocationListener interface:
@Override
public void onLocationChanged(Location location) {
lastKnownLocation = location;
locationManager.removeUpdates(this); // we need a location only once
navigateTo(location);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
In the event of retrieving a Location, that Location gets decoded to a GeoPoint, which the MapView's
MapController can animateTo(GeoPoint):
protected void navigateTo(Location location) {
GeoPoint geoPoint = getGeoPointFromLocation(location);
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 48/59
navigateTo(geoPoint);
}
protected GeoPoint getGeoPointFromLocation(Location location) {
GeoPoint geoPoint = new GeoPoint((int) (location.getLatitude() * 1E6),
(int) (location.getLongitude() * 1E6));
return geoPoint;
}
protected void navigateTo(GeoPoint geoPoint) {
// ...
final int INITIAL_ZOOM_LEVEL = mapView.getMaxZoomLevel() – 3;
mapView.getController().animateTo(geoPoint);
mapView.getController().setZoom(INITIAL_ZOOM_LEVEL);
switch (mapViewDisplayMode) {
case SHOW:
mapView.getOverlays().add(new LocationIndicatorOverlay(geoPoint));
break;
case SELECT:
mapView.getOverlays().add(
new LocationSelectorOverlay(geoPoint, this));
break;
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
Finally, we're adding our custom LocationSelectorOverlay to the MapView's
List<Overlay>. It extends our LocationIndicatorOverlay, which highlights the given
location as discussed in chapter Highlighting a Location on the MapView Control.
For further information see the article Obtaining User Location and the
LocationManager ApiDoc . You might also want to look into the Google APIs
Add-Ons' MyLocationOverlay .
3.3.6.4.3. Faciliating Selecting a Location in the Map
When the user clicks (taps) a point in the MapViewerActivity's map, we catch that event, reverse-geocode
the corresponding GeoPoint to an android.location.Address and from there to a
de.metagear.pizzastore.model.Address, which finally gets assigned to the UserDataFormActivity's EditText fields.
These tasks are discussed in the following chapters:
3.3.6.4.3.1. Passing Data to an Activity
We've already discussed how to maintain global application state, and these concepts can be indeed used to
pass data between activities. The Android SDK's design however plans for loose coupling via Intents, of which
an instance is always assigned to an Activity, and which – the Intent – may be itself assigned extra data.
When the user clicks the Address from Map menu item, the MapViewActivity is meant to be started in
MapViewDisplayMode.SELECT mode. We're attaching that information to the Intent that gets assigned to the
MapViewActivity to be started:
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 49/59
protected void showMapView(MapViewDisplayMode mode) {
// ...
Intent intent = new Intent(this, MapViewActivity.class);
intent.putExtra(INTENT_EXTRA_KEY_MAPVIEW_DISPLAY_MODE, mode);
switch (mode) {
case SELECT:
startActivityForResult(intent,
REQUEST_CODE_SELECT_ADDRESS_IN_MAP);
break;
case SHOW:
startActivity(intent);
break;
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.AddressFormActivity
Note that AddressFormActivity is subclassed by our main UserDataFormActivity, which the user is seeing at
the stage we're currently describing.
Instead of calling startActivity(Intent), we're now calling startActivityForResult(Intent, int), where we pass
a unique request ID, int REQUEST_CODE_SELECT_ADDRESS_IN_MAP, used to relate the future result to our current
request. – The activity result will become available in our overriden Activity.onActivityResult(int, int,
Intent) method (see chapter Passing Result Data Back to the Calling Activity).
Now in our MapViewActivity, we're querying for that extra data as follows:
@Override
protected void onCreate(Bundle savedInstanceState) {
// ...
mapViewDisplayMode = (MapViewDisplayMode) getIntent()
.getExtras()
.get(UserDataFormActivity.INTENT_EXTRA_KEY_MAPVIEW_DISPLAY_MODE);
switch (mapViewDisplayMode) {
case SHOW:
// ...
break;
case SELECT:
initializeLocationManager();
if (lastKnownLocation != null) {
navigateTo(lastKnownLocation);
}
break;
// ...
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
3.3.6.4.3.2. Making a MapView Clickable (Tappable Actually)
In chapter Displaying a Given Location in the Map Viewer, we've been adding our custom
LocationIndicatorOverlay (that draws a small blue dot at the given location) to the MapView by calling the
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 50/59
MapView.getOverlays().add(Overlay) method.
The LocationSelectorOverlay extends LocationIndicatorOverlay and overrides the Overlay.onTap(..) method
(which is pretty simple):
public class LocationSelectorOverlay extends LocationIndicatorOverlay {
private OnLocationSelectedListener listener;
public LocationSelectorOverlay(GeoPoint initialLocation,
OnLocationSelectedListener listener) {
super(initialLocation);
this.listener = listener;
}
@Override
public boolean onTap(GeoPoint geoPoint, MapView mapView) {
listener.onLocationSelected(geoPoint);
return true;
}
public interface OnLocationSelectedListener {
void onLocationSelected(GeoPoint newLocation);
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.view.map.LocationSelectorOverlay
Additionally, our LocationSelectorOverlay class holds an OnLocationSelectedListener interface interface, which
our MapViewActivity implements so that we can call back its event methods.
3.3.6.4.3.3. Reverse Geocoding
At this point, the map is constructed as shown in chapter Obtaining a Device's Current Location. We're also
Making a MapView Clickable (Tappable Actually).
When the user has clicked a point at the MapView, onLocationSelected(GeoPoint) gets called:
@Override
public void onLocationSelected(GeoPoint newLocation) {
final int MAX_RESULTS = 1;
double longitude = newLocation.getLongitudeE6() / 1E6;
double latitude = newLocation.getLatitudeE6() / 1E6;
Geocoder geocoder = new Geocoder(this);
try {
List<Address> addresses = geocoder.getFromLocation(latitude,
longitude, MAX_RESULTS);
if ((addresses == null) || (addresses.size() == 0)) {
Toast.makeText(this, R.string.mapViewer_noResultsForGeoPoint,
Toast.LENGTH_LONG);
return;
}
de.metagear.pizzastore.model.Address modelAddress = AddressAddressAdapter
.fromLocationAddress(addresses.get(0));
getIntent().putExtra(INTENT_EXTRA_KEY_ADDRESS, modelAddress);
setResult(RESULT_OK, getIntent());
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 51/59
finish();
} catch (IOException e) {
// ...
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.MapViewActivity
First, we're reverse-geocoding the GeoPoint to an android.location.Address, which we then adapt to our own
data model's Address.
3.3.6.4.3.4. Passing Result Data Back to the Calling Activity
Next, we're assigning this as extra data to an Intent, which we pass to the Activity's setResult(int, Intent)
method. Finally, we call finish() to close this Activity and forward the result to the calling Activity's
onActivityResult(int, int, Intent) method.
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_SELECT_ADDRESS_IN_MAP
&& resultCode == RESULT_OK) {
Address address = (Address) data.getExtras().get(
MapViewActivity.INTENT_EXTRA_KEY_ADDRESS);
AddressViewAdapter.fromAddress(rootView, address);
}
}
For corresponding code see: mg-pizzastore-
android/de.metagear.pizzastore.activity.AddressFormActivity
The AddressViewAdapter.fromAddress(View, Address) method is not discussed at this place. It populates
fields within the given View with properties of the given Address. – We're using such adapters throughout our
sample application to faciliate separation of concerns.
3.4. Activity Testing
In the sample application,
we're exemplarily testing
our UserDataFormActivity
(see chapter
UserDataFormActivity: A
Validating Input Form).
Android provides support
for unit testing and
functional testing of
Activitys, where unit
testing means to mock
the Android environment.
Functional testing means
that our activity to be
tested is fully
instrumented with an
Android environment, and
that it is actually running
in the Android Emulator.
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 52/59
We're doing functional testing – by letting our test case, UserDataFormActivityTest, extend Android's
ActivityInstrumentationTestCase2.
To run the tests, in Eclipse select the mg-pizzastore-android-test project and choose Run As -->
Android JUnit Test from the context menu.
Tests could also be executed from automated scripts, using the adb command line tool.
See the Android Developer's Guide on Activity Testing and a related tutorial for background
information.
3.4.1. Setting Up the Android Manifest
The following code snippet shows our test project's AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.metagear.pizzastore.test" android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="7" />

<instrumentation android:targetPackage="de.metagear.pizzastore"
android:name="android.test.InstrumentationTestRunner" />

<application android:icon="@drawable/icon" android:label="@string/app_name">
<uses-library android:name="android.test.runner" />
</application>
</manifest>
For corresponding code see: mg-pizzastore-android-test/AndroidManifest.xml
The android:targetPackage equals our target project (mg-pizzastore-android) manifest's package.
3.4.2. Setting Up a Test Case
The following code snippet shows our UserDataFormActivityTest's basic setup:
public class UserDataFormActivityTest extends
ActivityInstrumentationTestCase2<UserDataFormActivity> {
private UserDataFormActivity activity;
private ApplicationState appState;
private ValidatingSpinner countriesSpinner;
private String[] editTextTexts;
private Country country;

public UserDataFormActivityTest() {
super("de.metagear.pizzastore", UserDataFormActivity.class);
}

@Override
protected void setUp() throws Exception {
super.setUp();

activity = (UserDataFormActivity) getActivity();
appState = (ApplicationState) activity.getApplication();

countriesSpinner = (ValidatingSpinner) activity
.findViewById(R.id.address_country);

editTextTexts = new String[] { "Am Hohen Ufer 3A", "30159",
"Hannover", "John Doe", "12345",
"[email protected]" };
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 53/59
country = new Country("Germany", "de");
}
}
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
First-off, we're extending the functional testing ActivityInstrumentationTestCase2, which causes our tested
Activity to be run fully instrumented, within the Android Emulator.
In the nillary constructor, we call the super constructor by passing the target application's package name and
the Class of the activity under test.
Note that this package name matches the value of the manifest package attribute of the target project's
AndroidManifest.xml, but neither the tested Activity's package name, nor its fully qualified class name, nor
anything else. (There happen to be misconceptions about the role of that parameter.)
In the setup() method, we're initializing the Activity's initial state.
This setup() method gets called automatically, and repeatedly, before each test*() method is run.
The following chapters discuss a number of test*() method types. The JUnit Framework by convention
executes all methods with the signature public void test*().
3.4.3. Testing the Activity's Initial State
While not mandantory, it's considered good practice to check assumptions on the activity's initial state, which
other test methods might rely on.
Not necessarily, but by convention, the name of the corresponding method may be testPreConditions().
In our case this method just asserts that the application's UserData instance is uninitialized:
public void testPreConditions() {
assertTrue(!appState.getOrder().getUserData().isValid());
}
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
3.4.4. Testing Form Validation
In our initializing setup() method, we've set up a set of valid form
field values that can be re-used, respectively, altered, in
subsequently executing test methods:
// ...
private String[] editTextTexts;
private Country country;
@Override
protected void setUp() throws Exception {
// ...
editTextTexts = new String[] { "Am Hohen Ufer 3A", "30159",
"Hannover", "John Doe", "12345",
"[email protected]" };
country = new Country("Germany", "de");
}
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 54/59
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
We're now assigning these values to the ValidatingViews (see chapter Self-Validating Views).
@UiThreadTest
public void testValidValues() {
assignViewValues(editTextTexts, country);
assertTrue(areAllValidatingViewsValid());
}
@UiThreadTest
public void testInvalidZipCode() {
// exactly 5 digits required for "ZIP Code":
editTextTexts[1] = "some invalid zip code";
assignViewValues(editTextTexts, country);
assertFalse(areAllValidatingViewsValid());
}
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
In testValidValues(), valid values are assigned to the ValidatingViews (see chapter Self-Validating Views).
Accordingly, these are supposed to return true from their boolean isValid() method.
In testInvalidZipCode(), we're using the same test data except of the ZIP Code ValidatingView, which is
supposed to not report validity, correspondingly.
The following code snippet just shows some private helper methods called from the public test methods shown
above:
private void assignViewValues(String[] editTextTexts, Country country) {
setEditTextText(R.id.address_street, editTextTexts[0]);
setEditTextText(R.id.address_zipCode, editTextTexts[1]);
setEditTextText(R.id.address_city, editTextTexts[2]);
WidgetUtils.setSpinnerSelectedItem(countriesSpinner, country);
setEditTextText(R.id.userInfo_name, editTextTexts[3]);
setEditTextText(R.id.userInfo_phone, editTextTexts[4]);
setEditTextText(R.id.userInfo_email, editTextTexts[5]);
}
private boolean areAllValidatingViewsValid() {
UserDataViewAdapter adapter = new UserDataViewAdapter();
adapter.assignValidators(activity.getRootView());
adapter.validateAllViews();

return adapter.isAllViewsValid();
}
private void setEditTextText(int id, String text) {
EditText editText = (EditText) activity.findViewById(id);
editText.setText(text);
}
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
3.4.7. Excursus: Attaching the Testing Thread to the UI Thread
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 55/59
In the above chapter, we've been adding the testing code to the UI thread's event queue by using the
@UiThreadTest annotation:
@UiThreadTest
public void testValidValues() {
assignViewValues(editTextTexts, country);
assertTrue(areAllValidatingViewsValid());
}
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
Equivalently, we could have used the Activity.runOnUiThread(Runnable) method .
However, like other developers , we experienced a NullPointerException without any stacktrace.
When trying to debug that Android JUnit Test in Eclipse, our breakpoints were not hit, and in contrast to
executing in run mode, the test result was "success".
See chapter Updating the User Interface for a similar concept, using an Android Handler to attach code
from another thread to the UI thread's event queue.
3.4.8. Simulating User Interaction
When the user clicks the Save button, the data they entered may
only be adapted to the data model if the values are valid.
Clicking that button in our test is programmatically invoked by using
the Button.performClick() method:
@UiThreadTest
public void testSaveButtonWithValidValues() {
UserData userData = assignViewValuesAndReturnUserDataFromView(
editTextTexts, country);
Button saveButton = (Button) activity
.findViewById(R.id.userInfo_saveButton);
saveButton.performClick();
assertEquals(appState.getOrder().getUserData(), userData);
}
@UiThreadTest
public void testSaveButtonWithInvalidValues() {
editTextTexts[1] = "some invalid zip code";
UserData userData = assignViewValuesAndReturnUserDataFromView(
editTextTexts, country);
Button saveButton = (Button) activity
.findViewById(R.id.userInfo_saveButton);
saveButton.performClick();
assertFalse(appState.getOrder().getUserData().equals(userData));
}
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 56/59
3.4.9. Testing Activity Lifecycle State Management
Android activities can be paused (moved to the background) and destroyed
(see chapter Excursus: Android Activity Lifecycle).
Our corresponding tests verify that the application's state doesn't get lost
with these events:
public void testStatePausedAndResumed() {
UserData userDataBeforePause = assignViewValuesAndReturnUserDataFromView(
editTextTexts, country);
Instrumentation instrumentation = getInstrumentation();
instrumentation.callActivityOnPause(activity);
instrumentation.callActivityOnResume(activity);
UserData userDataAfterResume = assignViewValuesAndReturnUserDataFromView(
editTextTexts, country);
assertEquals(userDataBeforePause, userDataAfterResume);
}
@UiThreadTest
public void testStateDestroyedAndCreated() {
UserData userDataBeforeDestroy = assignViewValuesAndReturnUserDataFromView(
editTextTexts, country);
activity.finish();
activity = getActivity();
UserData userDataAfterCreate = assignViewValuesAndReturnUserDataFromView(
editTextTexts, country);
assertEquals(userDataBeforeDestroy, userDataAfterCreate);
}
For corresponding code see: mg-pizzastore-android-
test/de.metagear.pizzastore.activity.test.UserDataFormActivityTest
4. Resources
All links retrieved at the date of publication. – Initially, most links had been listed in 2011. Revised in year
2012, few links may point, or redirect, to more general documentation pages due to external site
restructuring.
4.1. Primary Resources
Google, Inc.: Android Developer's Guide , developer.android.com, 2012
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 57/59
Google, Inc.: Android SDK , developer.android.com, 2012
Meier, Reto: Professional Android 2 Application Development, Wiley Publishing, Inc., 2010
Steele, James & Nelson, To: The Android Developer’s Cookbook – Building Applications with the Android
SDK, Addison-Wesley, 2012
4.2. Tools
Apache Software Foundation: Apache Tomcat , tomcat.apache.org, 2012
Chacon, Scott: Git – Fast Version Control System , git-scm.com, 2012
Eclipse Foundation: Eclipse 3.6 (Helios) for Java EE Developers , www.eclipse.org, 2012
Google, Inc.: Android Development Tools (ADT) , developer.android.com, 2012
Google, Inc.: Android SDK , developer.android.com, 2012
SnPe Informacioni sistemi: adt-addons (Additional Eclipse plugins for Android) , code.google.com, 2012
4.3. Remoting
Codehaus: Jackson JSON Processor , jackson.codehaus.org, 2012
Falkner, Jason: Two Servlet Filters Every Web Application Should Have , onjava.com, 2003
Johnson, Rod, et al.: Spring Framework 3.0 – Reference Documentation , static.springsource.org, 2010
Web MVC Framework
Owen, Sean: PJL Compressing Filter , sourceforge.net, 2012
Söding, Robert: A Comprehensive Introduction into the Spring Framework , metagear.de, 2009
Remoting
Spring MVC
SpringSource, a Division of VMWare: Spring Android , springsource.org, 2012
SpringSource, a Division of VMWare: Spring Mobile Samples , git.springsource.org, 2012
Wikimedia Foundation (Edt.): JavaScript Object Notation (JSON) , en.wikipedia.org, 2012
Wikimedia Foundation (Edt.): Representional State Transfer (REST) , en.wikipedia.org, 2012
4.4. User Interface
Actually, not all of these resources always relate to the GUI.
De Kwant, Marc: Android Form Field Validation , dekwant.eu, 2010
Google, Inc.: Android Developer Resources , developer.android.com
Activities
Managing the Activity Lifecycle
Activity and Task Design Guidelines
Adapter ViewHolder Pattern (Code Listing)
Layout Tricks: Creating Reusable UI Components ( unavailable in 2012)
Layout Tricks: Merging Layouts ( unavailable in 2012)
Google, Inc.: Android Developer's Guide , developer.android.com
Applying Styles and Themes
Creating Dialogs
Icon Design Guidelines
TableLayout
Using Shared Preferences
Google, Inc.: Android Package Index , developer.android.com
Handler
InputType
R.drawable ( list of generic images used with Android)
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 58/59
TableLayout
SharedPreferences
Guy, Romain: Android Layout Tricks #1 , #2 , #3 , #4 , www.curious-creature.org, 2009
Lew, Daniel: Android Drawable XML Documentation ( The missing manual!), idunnolol.com, 2010
Manzano, Javier: Efficient ListView's in Android: View Holder Pattern , www.jmanzano.es, 2012
4.5. Location-Based Services, and Google Maps
Google, Inc.: Android Developer's Guide , developer.android.com
Obtaining User Location
Google, Inc.: Android Developer Resources , developer.android.com
Google Map View
Google, Inc.: Obtaining a Maps API Key , code.google.com, 2012
Google, Inc.: Android Package Index , developer.android.com
LocationManager
Google, Inc. (Edt.): Issue 8816: service not available ( location-based services don't work with the
Android device emulator on Android SDK 2.2 and 2.3), code.google.com, 2010-2012
Google, Inc.: MyLocationOverlay ApiDoc , code.google.com, 2012
4.6. Activity Testing
Beck, Kent; Gamma, Erich & Saff, David: JUnit Framework , junit.sourceforge.net, 2012
Google, Inc.: Activity Testing (Developer's Guide), developer.android.com, 2012
Google, Inc.: Activity Testing (Tutorial), developer.android.com, 2012
Stack Exchange, Inc. (Edt.): NullPointerException on Activity Testing Tutorial , stackoverflow.com,
2010
4.7. Other Resources
Becker, Arno: Die Architektur von Android - Innenansichten eines Smartphone-Betriebssystems ,
www.heise.de, 2011 ( in German)
Burton, Michael: roboguice – Google Guice on Android , code.google.com, 2012
Google, Inc.: Android Developer Resources , developer.android.com
Backward Compatibility for Applications ( unavailable in 2012)
4.7.1. Alternative Frameworks for Android
Adobe, Inc.: Tour de Flex: Flex Mobile , www.adobe.com, 2012
Brian: Rolling Releases: How Apache Cordova becomes PhoneGap and Why , phonegap.com, 2012
Xamarin: Mono for Android , xamarin.com, 2012
4.7.1.1. Web-Based UI Technologies
Google, Inc.: Building Web Apps in WebView , developer.android.com, 2012
jQuery Project: jQuery Mobile: Touch-Optimized Web Framework for Smartphones & Tablets ,
jquerymobile.com, 2012
Sencha, Inc.: Sencha Touch – Mobile JavaScript Framework , www.sencha.com, 2012
4.7.2. Android Market Share
3/24/2014 Documented Android Sample Application
http://metagear.de/articles/android-sample/index.html 59/59
Allied Business Intelligence, Inc.: Android Will Seize 45% of Smartphone Market by 2016, Says ABI
Research , www.abiresearch.com, 2011
comScore, Inc.: comScore Reports February 2012 U.S. Mobile Subscriber Market Share ,
comscore.com, 2012
Gartner, Inc.: Gartner Says Worldwide Media Tablets Sales to Reach 119 Million Units in 2012 ,
www.gartner.com, 2012
IDC Corporate USA: Android- and iOS-Powered Smartphones Expand Their Share of the Market in the First
Quarter, According to IDC , www.idc.com, 2012

Sponsor Documents

Or use your account on DocShare.tips

Hide

Forgot your password?

Or register your new account on DocShare.tips

Hide

Lost your password? Please enter your email address. You will receive a link to create a new password.

Back to log-in

Close