Digging deeper into the code

Một phần của tài liệu Manning unlocking android a developers (Trang 344 - 361)

Most of the time our mobile worker is using this application, he will be reading through comments, looking up a job address, getting product information, and per- forming other aspects of working on a specific job. However, without a list of jobs to work on, our mobile worker will be sitting idle, not earning a dime! Therefore, the first thing to review is the fetching of new jobs. This is also a good time to discuss gath- ering the list of jobs, coming on the heels of the review of the JobListHandler. 12.4.1 RefreshJobs

The RefreshJobs Activity performs a simple yet vital role in the Field Service Application. When- ever 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 ProgressDialog informing the user of the application’s progress, as shown in figure 12.8.

The code listing for RefreshJobs is shown in listing 12.13. The code is rather straightforward, as most of the heavy lifting is done in the Job- ListHandler. This code’s responsibility 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 {

Listing 12.13 RefreshJobs.java

Figure 12.8 The ProgressDialog in use during RefreshJobs

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

} };

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 ...");

Progress indicator

B

Set up ProgressDialog C

Define Handler

D

Update UI with textual message

E

F Handle cancel and cancel with error

Use openFileInput for stream

G

Initiate DoReadJobs class instance

H

Create Message object

I

Looping construct

J Prepare

status message

1)

Prepare to 1!

parse data

1@ Instantiate JobListHandler

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

} } } }

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. While the Han- dler 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 ProgressDialog C, the arguments include:

■ Context

■ Title of Dialog

■ Initial Textual Message

■ Indeterminate

■ Cancelable

Using true for the Indeterminate parameter means that we are 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 pro- cess messages sent from the parsing routine, which is 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 execution. The Mes- sage class has some generic data members that may be used in a flexible manner. The

Persist data

1#

Set status flag for completion

1$

Set status flag for error

1%

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 member. 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 on 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 have 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 comple- tion so the ProgressDialog is canceled (dismissed would work here also), 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 is provided here as an example of handling different result conditions; for example, a failure response due to an encountered error. In a production-ready application, this step should be fleshed out to perform an additional step of instruction to the user and/or a retry step. Any Mes- sage 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 is 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 ini- tialized. This step takes place over and over throughout the run method of the DoRead- Jobs class. It is important to not reuse a Message object, as they are literally passed and enqueued. It is possible for them to stack up in the receiver’s queue, so reusing a Mes- sage object will lead to losing data or corrupting data at best and Thread synchroniza- tion issues or beyond at worst!

Why are we talking about a commented-out line of code J? Great ques- tion—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 assist Java Threads to interact with Android. Threads by default do not 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 does not 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 Runna- ble interface, I experimented with performing this step in the same thread and attempted to get the ProgressDialog to work properly. All this said, if you run into funny Thread/Looper messages on the Android Emulator, have a look at 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.

In order 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 pro- gresshandler. 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 Message1$ with the what field set to a value of 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 with persis- tently stored configuration information (user and server) and successfully connected over the internet to fetch XML data. That data has been parsed into a JobList con- taining JobEntry objects, while providing our patient mobile worker with feedback, 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 ManageJobs

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 enu- merated 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 is used to represent the JobEntry in the ListView, as shown in figure 12.9.

The layout for this Activity’s View is rather simple, just a TextView and a ListView, as shown in listing 12.14.

<?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"

Listing 12.14 managejobs.xml

Figure 12.9 MangeJobsActivity lists downloaded jobs.

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.15 for the ManageJobs Activity connects a JobList to the GUI as well as reacts to the selection 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);

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

}

Listing 12.15 ManageJobs.java implements the ManageJobsActivity

Connect TextView to UI

Parse the data in storage

B

Handle a bad parse

C

Check for an

empty JobList Connect ListView to UI

D

Process click events on List

Connect the list with the dataevents on List

Use a built-in list layout

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 List- View D. In order 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.

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 SHOWJOB as the last parameter of the startActivityForResult method. When this called Activity returns, that parameter will help the caller understand the context of the data 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 we can view and select the job of interest, it is time to look at just what we can do with that job. Before diving in to the next section, be sure to review the Man- ageJobs code carefully to understand how the JobEntry information is passed between the two activities.

12.4.3 ShowJob

The ShowJob Activity is the most interesting element of the entire application, and it is certainly the screen most useful to the mobile worker carrying around his Android-

Process click events on List Use a Bundle to

store Job data

F G

Start ShowJob Activity Prepare Intent for showing Job details

E

Fetch job from list by ordinal Check return code

H

Update the list with I

via replace method

Extract returned JobEntry

capable device and toolbox. To help in the discus- sion of the different features available to the user on this screen, take a look at figure 12.10.

The layout is very straightforward but this time we have some Buttons and we will be changing the textual description depending on the condition of a particular job’s status. A Text- View is used to present job details such as address, product requiring service, and com- ments. 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 functionality 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 fig- ure 12.11.

The second button, Get Product Info, launches a browser window to assist the user in learning more about the product he is being

called upon 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 is already closed, the details of which are covered in the next section on the CloseJob Activity.

More on Bundles

We need to pass the selected job to the ShowJob Activity, but we cannot casually pass an object from one Activity to another. We don’t want the ShowJob Activity to have to parse the list of jobs again; otherwise we could simply pass back an index to the selected job by using the integer storage methods of a Bundle. Perhaps we could store the currently selected JobEntry (and JobList for that matter) in a global data member of the Application object, should we have chosen to implement one.

If you recall in chapter 1 when we discussed the ability of Android to dispatch In- tents to any Activity registered on the device, we want to keep the ability open to an application other than our own to perhaps pass a job to us. If that were the case, using a global data member of an Application object would never work! Never mind for the moment the likelihood of such a step being low, particularly considering how the data is stored in this application. This chapter’s sample application is an exercise of evaluating some different mechanisms one might employ to solve data movement around Android. The chosen solution is to package the data fields of the JobEntry in a Bundle F (in listing 12.15) to move a JobEntry from one Activity to another.

In the strictest sense, we are moving not a real JobEntry object but a representation of a JobEntry’s data members. The net of this long discussion is that this method creates a new Bundle by using the toBundle() method of the JobEntry.

Figure 12.10 An example of a job shown in the ShowJobActivity

Fortunately, the steps required for the first two operations are quite simple with Android—thanks to the Intent. Listing 12.16 and the accompanying descriptions 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");

Listing 12.16 ShowJob.java 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.

Get Intent

Extract the Bundle from the Intent

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=" + ➥ 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);

Update UI upon error and return

Build and launch a geo query

Obtain product information via URL

Selectively update Button label

Show Signature for CLOSED JobEntrys

Initiate CloseJob Activity

Một phần của tài liệu Manning unlocking android a developers (Trang 344 - 361)

Tải bản đầy đủ (PDF)

(418 trang)