Most of the time our mobile worker is using this application, he’ll be reading through comments, looking up a job address, getting product information, and performing other aspects of working on a specific job. Our application must supply the functional- ity for the worker to accomplish each of these job-management tasks. We examine each of these Activitys in detail in this section. The first thing we review is fetching new jobs from the server, which gives us the opportunity to discuss the JobList- Handler and the management of the jobs list used throughout the application.
12.4.1 RefreshJobs
The RefreshJobsActivity performs a simple yet vital role in the field service application. Whenever requested, the RefreshJobs Activity attempts to download a list of new jobs from the server. The UI is super simple—just a blank screen with a Progress- Dialog informing the user of the application’s prog- ress, as shown in figure 12.8.
The code for RefreshJobs is shown in listing 12.12.
The code is straightforward, as most of the heavy lift- ing is done in the JobListHandler. This code’s respon- sibility is to fetch configuration settings, initiate a request to the server, and put a mechanism in place for showing progress to the user.
package com.msi.manning.UnlockingAndroid;
// multiple imports omitted for brevity, see full source public class RefreshJobs extends Activity {
Prefs myprefs = null;
Boolean bCancel = false;
JobList mList = null;
ProgressDialog progress;
Handler progresshandler;
@Override
public void onCreate(Bundle icicle) { super.onCreate(icicle);
setContentView(R.layout.refreshjobs);
myprefs = new Prefs(this.getApplicationContext);
myprogress = ProgressDialog.show(this, "Refreshing Job List", "Please Wait",true,false);
progresshandler = new Handler() { @Override
public void handleMessage(Message msg) { switch (msg.what) {
case 0:
myprogress.setMessage("" + (String) msg.obj);
break;
case 1:
myprogress.cancel();
finish();
break;
case 2: // error occurred myprogress.cancel();
finish();
break;
}
super.handleMessage(msg);
}
Listing 12.12 RefreshJobs.java
Progress indicator
B
Set up ProgressDialog
C
Define Handler
D
Update UI with textual message
E
Handle cancel and cancel with error
F
Use openFileInput for stream
G
Figure 12.8
The ProgressDialog in use during RefreshJobs
};
Thread workthread = new Thread(new DoReadJobs());
workthread.start();
}
class DoReadJobs implements Runnable { public void run() {
InputSource is = null;
Message msg = new Message();
msg.what = 0;
try {
//Looper.prepare();
msg.obj = (Object) ("Connecting ...");
progresshandler.sendMessage(msg);
URL url = new URL(myprefs.getServer() +
"getjoblist.php?identifier=" + myprefs.getEmail());
is = new InputSource(url.openStream());
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
XMLReader xmlreader = parser.getXMLReader();
JobListHandler jlHandler = new JobListHandler(progresshandler);
xmlreader.setContentHandler(jlHandler);
msg = new Message();
msg.what = 0;
msg.obj = (Object)("Parsing ...");
progresshandler.sendMessage(msg);
xmlreader.parse(is);
msg = new Message();
msg.what = 0;
msg.obj = (Object)("Parsing Complete");
progresshandler.sendMessage(msg);
msg = new Message();
msg.what = 0;
msg.obj = (Object)("Saving Job List");
progresshandler.sendMessage(msg);
jlHandler.getList().persist();
msg = new Message();
msg.what = 0;
msg.obj = (Object)("Job List Saved.");
progresshandler.sendMessage(msg);
msg = new Message();
msg.what = 1;
progresshandler.sendMessage(msg);
} catch (Exception e) {
Log.d("CH12","Exception: " + e.getMessage());
msg = new Message();
msg.what = 2; // error occurred
msg.obj = (Object)("Caught an error retrieving Job data: " + e.getMessage());
progresshandler.sendMessage(msg);
} } } }
Initiate DoReadJobs class instance
H
Create Message object
I
Define looping construct
J
Prepare status message
1)
Prepare to parse data 1!
Instantiate JobListHandler
1@
Persist data
1#
Set status flag for completion
1$
Set status flag for error
1%
A ProgressDialogB is used to display progress information to the user. There are a number of ways to display progress in Android. This is perhaps the most straightfor- ward approach. A Handler is employed to process Message instances. Though the Handler itself is defined as an anonymous class, the code requires a reference to it for passing to the JobListHandler when parsing, which is shown in 1@. When instantiat- ing the ProgressDialogC, the arguments include
Context
Title of Dialog
Initial Textual Message
Indeterminate
Cancelable
Using true for the Indeterminate parameter means that you’re not providing any clue as to when the operation will complete (such as percentage remaining), just an indicator that something is still happening, which can be a best practice when you don’t have a good handle on how long an operation may take. A new HandlerD is created to process messages sent from the parsing routine, which will be introduced momentarily. An important class that has been mentioned but thus far not described is Message. This class is used to convey information between different threads of exe- cution. The Message class has some generic data members that may be used in a flexi- ble manner. The first of interest is the what member, which acts as a simple identifier, allowing recipients to easily jump to desired code based on the value of the what mem- ber. The most typical (and used here) approach is to evaluate the what data member via a switch statement.
In this application, a Message received with its what member equal to 0 represents a textual update message E to be displayed in the ProgressDialog. The textual data itself is passed as a String cast to an Object and stored in the obj data member of the Message. This interpretation of the what member is purely arbitrary. We could’ve used 999 as the value meaning textual update, for example. A what value of 1 or 2 indicates that the operation is complete F, and this Handler can take steps to initiate another thread of execution. For example, a value of 1 indicates successful completion, so the ProgressDialog is canceled, and the RefreshJobs Activity is completed with a call to finish(). The value of 2 for the what member has the same effect as a value of 1, but it’s provided here as an example of handling different result conditions: for exam- ple, a failure response due to an encountered error. In a production-ready applica- tion, this step should be fleshed out to perform an additional step of instruction to the user and/or a retry step. Any Message not explicitly handled by the Handler instance should be passed to the super class G. In this way, system messages may be processed.
When communicating with a remote resource, such as a remote web server in our case, it’s a good idea to perform the communications steps in a thread other than the primary GUI thread. A new ThreadH is created based on the DoReadJobs class, which
implements the Runnable Java interface. A new Message object I is instantiated and initialized. This step takes place over and over throughout the run() method of the DoReadJobs class. It’s important to not reuse a Message object, as they’re literally passed and enqueued. It’s possible for them to stack up in the receiver’s queue, so reusing a Message object will lead to losing data or corrupting data at best and Thread synchronization issues or beyond at worst.
Why are we talking about a commented-out line of code J? Great question—
because it caused so much pain in the writing of this application! A somewhat odd and confusing element of Android programming is the Looper class. This class provides static methods to help Java Threads to interact with Android. Threads by default don’t have a message loop, so presumably Messages don’t go anywhere when sent. The first call to make is Looper.prepare(), which creates a Looper for a Thread that doesn’t already have one established. Then by placing a call to the loop() method, the flow of Messages takes place. Prior to implementing this class as a Runnable interface, we experimented with performing this step in the same thread and attempted to get the ProgressDialog to work properly. That said, if you run into funny Thread/Looper messages on the Android Emulator, consider adding a call to Looper.prepare() at the beginning of your Thread and then Looper.loop() to help Messages flow.
When we want to send data to the user to inform him of our progress, we update an instance of the Message class 1) and send it to the assigned Handler.
To parse an incoming XML data stream, we create a new InputSource from the URL stream 1!. This step is required for the SAX parser. This method reads data from the network directly into the parser without a temporary storage file.
Note that the instantiation of the JobListHandler 1@ takes a reference to the progresshandler. This way the JobListHandler can (optionally) propagate messages back to the user during the parse process. Once the parse is complete, the JobList- Handler returns a JobList object, which is then persisted 1# to store the data to the local storage. Because this parsing step is complete, we let the Handler know by pass- ing a Message 1$ with the what field set to 1. If an exception occurs, we pass a message with what set to 2, indicating an error 1%.
Congratulations, your Android application has now constructed a URL object with persistently stored configuration information (user and server) and successfully con- nected over the internet to fetch XML data. That data has been parsed into a JobList containing JobEntry objects, while providing our patient mobile worker with feed- back, and subsequently storing the JobList to the filesystem for later use. Now we want to work with those jobs, because after all, those jobs have to be completed for our mobile worker friend to make a living!
12.4.2 Managing jobs: the ManageJobs Activity
The ManageJobs Activity presents a scrollable list of jobs for review and action. At the top of the screen is a simple summary indicating the number of jobs in the list, and each individual job is enumerated in a ListView.
Earlier we mentioned the importance of the JobEntry’s toString() method:
public String toString() {
return this._jobid + ": " + this._customer + ": " + this._product;
}
This method generates the String that’s used to represent the JobEntry in the List- View, as shown in figure 12.9.
The layout for this Activity’s View is simple: just a TextView and a ListView, as shown in the following listing.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/joblistview"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
>
<TextView android:id="@+id/statuslabel"
android:text="list jobs here "
android:layout_height="wrap_content"
android:layout_width="fill_parent"
/>
<ListView android:id="@+id/joblist"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
/>
</LinearLayout>
The code in listing 12.14 for the ManageJobsActivity connects a JobList to the GUI and reacts to the selec- tion of a particular job from the ListView. In addition, this class demonstrates taking the result from another, synchronously invoked Activity and processing it according to its specific requirement. For example, when a job is completed and closed, that JobEntry is updated to reflect its new status.
package com.msi.manning.UnlockingAndroid;
// multiple imports omitted for brevity, see full source
public class ManageJobs extends Activity implements OnItemClickListener { final int SHOWJOB = 1;
Prefs myprefs = null;
JobList _joblist = null;
ListView jobListView;
@Override
public void onCreate(Bundle icicle) { super.onCreate(icicle);
Listing 12.13 managejobs.xml
Listing 12.14 ManageJobs.java, which implements the ManageJobsActivity
Figure 12.9
The ManageJobsActivity lists downloaded jobs.
setContentView(R.layout.managejobs);
myprefs = new Prefs(this.getApplicationContext());
TextView tv =
(TextView) findViewById(R.id.statuslabel);
_joblist = JobList.parse(this.getApplicationContext());
if (_joblist == null) {
_joblist = new JobList(this.getApplicationContext());
}
if (_joblist.getJobCount() == 0){
tv.setText("There are No Jobs Available");
} else {
tv.setText("There are " + _joblist.getJobCount() + " jobs.");
}
jobListView = (ListView) findViewById(R.id.joblist);
ArrayAdapter<JobEntry> adapter = new ArrayAdapter<JobEntry>(this, android.R.layout.simple_list_item_1, _joblist.getAllJobs());
jobListView.setAdapter(adapter);
jobListView.setOnItemClickListener(this);
jobListView.setSelection(0);
}
public void onItemClick(AdapterView parent, View v, int position, long id) {
JobEntry je = _joblist.getJob(position);
Log.i("CH12", "job clicked! [" + je.get_jobid() + "]");
Intent jobintent = new Intent(this, ShowJob.class);
Bundle b = je.toBundle();
jobintent.putExtras(b);
startActivityForResult(jobintent, SHOWJOB);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) { case SHOWJOB:
if (resultCode == 1){
Log.d("CH12","Good Close, let's update our list");
JobEntry je = JobEntry.fromBundle(data.getExtras());
_joblist.replace(je);
} break;
} } }
The objective of this code is to display a list of available jobs to the user in a ListView
D. To display the list of jobs, we must first parse the list stored on the device B. Note that the Context argument is required to allow the JobList class access to the private file area for this application. If the parse fails, we initialize the JobList instance to a new, empty list. This is a somewhat simplistic way to handle the error without the GUI falling apart C.
Connect TextView to UI
Parse data in storage
B
Handle bad parse
C
Check for empty JobList
Process click events on List
D
Connect ListView to UI
Use a built-in list layout
Connect list with dataevents on List
Fetch job from list by ordinal
E Intent forPrepare
showing Job details Use Bundle to store Job data
F
Start
ShowJob Activity
G
Check return code
H
Extract returned JobEntry Update the list with
via replace method
I
When a specific job is selected, its details are extracted via a call to the getJob() method E. The job is stored in a Bundle, put into an Intent F, and subsequently sent to the ShowJobActivity for display and/or editing G. Note the use of the con- stant SHOWJOB as the last parameter of the startActivityForResult() method. When the called Activity returns, the second parameter to startActivityForResult() is
“passed back” when the onActivityResult() method is invoked H and the return code checked. To obtain the changed JobEntry, we need to extract it from the Intent with a call to getExtras(), which returns a Bundle. This Bundle is turned into a JobEntry instance via the static fromBundle() method of the JobEntry class. To update the list of jobs to reflect this changed JobEntry, call the replace() method I.
Now that you can view and select the job of interest, it’s time to look at just what you can do with that job. Before diving into the next section, be sure to review the Manage- Jobs code carefully to understand how the JobEntry information is passed between the two activities.
12.4.3 Working with a job with the ShowJob Activity
The ShowJob Activity is the most interesting element of the entire application, and it’s certainly the screen most useful to the mobile worker carrying around his Android-capable device and toolbox. To help in the discussion of the various features available to the user on this screen, take a look at figure 12.10.
The layout is straightforward, but this time you have some Buttons and you’ll be changing the textual description depending on the condition of a particular job’s sta- tus. A TextView is used to present job details such as address, product requiring
More on bundles
You need to pass the selected job to the ShowJob Activity, but you can’t casually pass an object from one Activity to another. You don’t want the ShowJob Activity to have to parse the list of jobs again; otherwise you could simply pass back an index to the selected job by using the integer storage methods of a Bundle.
Perhaps you could store the currently selected JobEntry (and JobList for that mat- ter) in a global data member of the Application object, had you chosen to imple- ment one. If you recall in chapter 1 when we discussed the ability of Android to dispatch Intents to any Activity registered on the device, you want to keep the ability open to an application other than your own to perhaps pass a job to you. If that were the case, using a global data member of an Application object would never work! The likelihood of such a step is low, particularly considering how the data is stored in this application. This chapter’s sample application is an exercise of evalu- ating some mechanisms you might employ to solve data movement when program- ming for Android. The chosen solution is to package the data fields of the JobEntry in a Bundle (F in listing 12.14) to move a JobEntry from one Activity to another.
In the strictest sense, you’re not moving a real JobEntry object but a representation of a JobEntry’s data members. The net of this discussion is that this method cre- ates a new Bundle by using the toBundle() method of the JobEntry.
service, and comments. The third Button will have the text property changed, depending on the status of the job. If the job’s status is marked as CLOSED, the func- tionality of the third button will change.
To support the functionality of this Activity, first the code needs to launch a new Activity to show a map of the job’s address, as shown in figure 12.11.
The second button, Get Product Info, launches a browser window to assist users in learning more about the product they’re being called on to work with. Figure 12.12 shows this in action.
The third requirement is to allow the user to close the job or to view the signature if it’s already closed; we’ll cover the details in the next section on the CloseJob Activity.
Figure 12.10 An example of a job shown in the ShowJobActivity
Figure 12.11 Viewing a job address in the Maps application
Figure 12.12 Get Product Info takes the user to a web page specific to this job.
Fortunately, the steps required for the first two operations are quite simple with Android—thanks to the Intent. The following listing and the accompanying annota- tions show you how.
package com.msi.manning.UnlockingAndroid;
// multiple imports omitted for brevity, see full source public class ShowJob extends Activity {
Prefs myprefs = null;
JobEntry je = null;
final int CLOSEJOBTASK = 1;
public void onCreate(Bundle icicle) { super.onCreate(icicle);
setContentView(R.layout.showjob);
myprefs = new Prefs(this.getApplicationContext());
StringBuilder sb = new StringBuilder();
String details = null;
Intent startingIntent = getIntent();
if (startingIntent != null) {
Bundle b = startingIntent.getExtas();
if (b == null) {
details = "bad bundle?";
} else {
je = JobEntry.fromBundle(b);
sb.append("Job Id: " + je.get_jobid() + " (" + je.get_status()+
")\n\n");
sb.append(je.get_customer() + "\n\n");
sb.append(je.get_address() + "\n" + je.get_city() + "," + je.get_state() + "\n" );
sb.append("Product : "+ je.get_product() + "\n\n");
sb.append("Comments: " + je.get_comments() + "\n\n");
details = sb.toString();
} } else {
details = "Job Information Not Found.";
TextView tv = (TextView) findViewById(R.id.details);
tv.setText(details);
return;
}
TextView tv = (TextView) findViewById(R.id.details);
tv.setText(details);
Button bmap = (Button) findViewById(R.id.mapjob);
bmap.setOnClickListener(new Button.OnClickListener() { public void onClick(View v) {
// clean up data for use in GEO query String address = je.get_address() + " " + je.get_city() + " " +
je.get_zip();
String cleanAddress = address.replace(",", "");
cleanAddress = cleanAddress.replace(' ','+');
try {
Intent geoIntent = new Intent("android.intent.action.VIEW", android.net.Uri.parse("geo:0,0?q=" +
Listing 12.15 ShowJob.java
Get Intent Extract Bundle from Intent
Update UI upon error and return
cleanAddress));
startActivity(geoIntent);
} catch (Exception ee) { }
} });
Button bproductinfo = (Button) findViewById(R.id.productinfo);
bproductinfo.setOnClickListener(new Button.OnClickListener() { public void onClick(View v) {
try {
Intent productInfoIntent = new Intent("android.intent.action.VIEW",
android.net.Uri.parse(je.get_producturl()));
startActivity(productInfoIntent);
} catch (Exception ee) { }
} });
Button bclose = (Button) findViewById(R.id.closejob);
if (je.get_status().equals("CLOSED")) {
bclose.setText("Job is Closed. View Signature");
}
bclose.setOnClickListener(new Button.OnClickListener() { public void onClick(View v) {
if (je.get_status().equals("CLOSED")) { Intent signatureIntent = new
Intent("android.intent.action.VIEW",
android.net.Uri.parse(myprefs.getServer() + "sigs/" +
je.get_jobid() + ".jpg"));
startActivity(signatureIntent);
} else {
Intent closeJobIntent = new Intent(ShowJob.this,CloseJob.class);
Bundle b = je.toBundle();
closeJobIntent.putExtras(b);
startActivityForResult(closeJobIntent,CLOSEJOBTASK);
} } });
Log.d("CH12","Job status is :" + je.get_status());
}
@Override
protected void onActivityResult(
int requestCode, int resultCode, Intent data) { switch (requestCode) {
case CLOSEJOBTASK:
if (resultCode == 1) {
this.setResult(1, "", data.getExtras());
finish();
} break;
} } }
Build and launch geo query
Obtain product info via URL
Selectively update Button label
Show Signature for CLOSED JobEntrys Initiate CloseJob
Activity
Handle newly closed JobEntry
B