Taste of Android :: Part-12


Bhaskar S 08/02/2013


Android Application Development - XI

In this Taste of Android series so far we have covered quite a bit of ground involving Activity, Intent, Handler, AsyncTask, Service, IntentService, Notification, etc.

Wouldn't it be nice to tie most of these concepts into an application ???

We will do exactly that by creating a simple Stock Alert application. The user will be able to set a Low price and/or a High price alert for one or more stock symbols and get notified when the condition occurs.

Let us create a new Android application named DroidStockAlert that will put to test many of the concepts we have touched upon so far.

We will not go step-by-step to show the various screens since we already did that in Part-2 for the DroidTipCalculator application.

First we will need to download four icons of size 16x16 and save it in the directory res/drawable-mdpi. There are tons of open source friendly icons downloadable from the site openwebgraphics.

Modify the contents of the dimens.xml file to look like the one shown in the listing 12.1 below:

Listing-12.1
<?xml version="1.0" encoding="utf-8"?>
<resources>

<dimen name="cell_width">0dp</dimen>
<dimen name="list_padding">5dp</dimen>
<dimen name="table_padding">5dp</dimen>
<dimen name="text_size">26sp</dimen>

</resources>

Next, modify the contents of the strings.xml file to look like the one shown in the listing 12.2 below:

Listing-12.2
<?xml version="1.0" encoding="utf-8"?>
<resources>

<!-- Text references -->

<string name="app_name">DroidStockAlert</string>
<string name="action_settings">Settings</string>

<string name="config_desc">Configure</string>

<string name="stock_symbol">Stock Symbol</string>
<string name="low_alert">Lower Price Alert</string>
<string name="high_alert">Higher Price Alert</string>

<string name="set_alert">Set Alert</string>
<string name="cancel_alert">Cancel Alert</string>
<string name="quit">Quit!</string>

<string name="action_name">com.polarsparc.android.STOCK_ALERT_2</string>

<!-- Array references -->

<string-array name="stocks">
<item>AAPL</item>
<item>AMZN</item>
<item>CSCO</item>
<item>GOOG</item>
<item>IBM</item>
<item>INTC</item>
<item>LNKD</item>
<item>MSFT</item>
<item>NFLX</item>
<item>ORCL</item>
</string-array>

<string-array name="opens">
<item>450</item>
<item>267</item>
<item>24</item>
<item>871</item>
<item>209</item>
<item>24</item>
<item>169</item>
<item>35</item>
<item>223</item>
<item>34</item>
</string-array>

</resources>

We will have three layout definitions files - the first is for the main stock ticker screen, the second is for each row of the stock ticker list, and the third is for setting or cancelling the low or high alert conditions. The layout file for the main stock ticker screen is named activity_stock_alert.xml, the layout file for the list row is named list_item_row.xml, and the layout file for setting or cancelling alerts is named activity_apply_cancel.xml.

The contents of the activity_stock_alert.xml layout file will look like the one shown in the listing 12.3 below:

Listing-12.3
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".StockAlertActivity" >

<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
/>

</LinearLayout>

The contents of the list_item_row.xml layout file will look like the one shown in the listing 12.4 below:

Listing-12.4
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >

<TableRow>
<TextView
android:id="@+id/symbol"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
android:text=""
/>

<TextView
android:id="@+id/price"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
android:text=""
/>

<TextView
android:id="@+id/change"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
android:text=""
/>

<ImageView
android:id="@+id/config"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
android:contentDescription="@string/config_desc"
android:src="@drawable/add" />
</TableRow>

</TableLayout>

The contents of the activity_apply_cancel.xml layout file will look like the one shown in the listing 12.5 below:

Listing-12.5
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/table_padding" >

<TextView
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:text="@string/stock_symbol"
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:typeface="sans" />

<TextView
android:id="@+id/text1"
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:text=""
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:typeface="sans" />
</TableRow>

<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/table_padding" >

<TextView
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:text="@string/low_alert"
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:typeface="sans" />

<EditText
android:id="@+id/edit1"
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:inputType="numberDecimal" />
</TableRow>

<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/table_padding" >

<TextView
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:text="@string/high_alert"
android:textSize="@dimen/text_size"
android:textStyle="bold"
android:typeface="sans" />

<EditText
android:id="@+id/edit2"
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:inputType="numberDecimal" />
</TableRow>

<TableRow
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/table_padding" >

<Button
android:id="@+id/button1"
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:text="@string/set_alert" />

<Button
android:id="@+id/button2"
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:text="@string/cancel_alert" />

<Button
android:id="@+id/button3"
android:layout_width="@dimen/cell_width"
android:layout_weight="1"
android:text="@string/quit" />
</TableRow>

</TableLayout>

The contents of the java source file TickSignal.java will look like the one shown in the listing 12.6 below:

Listing-12.6
package com.polarsparc.android.droidstockalert;

public enum TickSignal {
UP, FLAT, DOWN;
}

We will need an object to encapsulate a stock ticker information such as the stock symbol, the current price, the price change, the low price alert value, the high price alert value, etc. This information is encapsulated in the class StockTicker. The contents of the java source file StockTicker.java will look like the one shown in the listing 12.7 below:

Listing-12.7
package com.polarsparc.android.droidstockalert;

public class StockTicker {
private float open;
private float change;

private float lowAlert;
private float highAlert;

private String symbol;

public String getSymbol() {
return symbol;
}

public void setSymbol(String symbol) {
this.symbol = symbol;
}

public float getPrice() {
return open+change;
}

public void setOpen(float open) {
this.open = open;
}

public float getChange() {
return change;
}

public void setChange(float change) {
this.change = change;
}

public boolean isLowAlertSet() {
return (lowAlert > 0.0f) ? true : false;
}

public float getLowAlert() {
return lowAlert;
}

public void setLowAlert(float lowAlert) {
this.lowAlert = lowAlert;
}

public boolean isHighAlertSet() {
return (highAlert > 0.0f) ? true : false;
}

public float getHighAlert() {
return highAlert;
}

public void setHighAlert(float highAlert) {
this.highAlert = highAlert;
}

public TickSignal getTickSignal() {
if (change > 0.0f) {
return TickSignal.UP;
}
else if (change < 0.0f) {
return TickSignal.DOWN;
}
return TickSignal.FLAT;
}

public boolean isLowAlert() {
if (lowAlert > 0.0f && getPrice() <= lowAlert) {
return true;
}
return false;
}

public boolean isHighAlert() {
if (highAlert > 0.0f && getPrice() >= highAlert) {
return true;
}
return false;
}
}

In this DroidStockAlert application, the stock prices will be updated in the background in an IntentService and accessed by the main stock ticker screen Activity. This means we need a way to share across multiple Android components (Activity, Service, etc). So how can we do that ??? Enter the Android framework class android.app.Application. In order to share state across multiple Android components, one needs to create a custom implementation of Application by extending it and wrapping application state object(s). In this application, we create a custom class named StockAlertApplication.

The contents of the java source file StockAlertApplication.java will look like the one shown in the listing 12.8 below:

Listing-12.8
package com.polarsparc.android.droidstockalert;

import java.util.List;
import java.util.ArrayList;

import android.app.Application;
import android.os.Handler;
import android.util.Log;

public class StockAlertApplication extends Application {
public static int UPDATE_MSG = 1;
public static int LAUNCH_MSG = 2;

private static String TAG = "StockAlertApplication";

private ArrayList<StockTicker> items = new ArrayList<StockTicker>();

private Handler handler = null;

@Override
public void onCreate() {
super.onCreate();

String[] stocks = getResources().getStringArray(R.array.stocks);
String[] opens = getResources().getStringArray(R.array.opens);
if (stocks != null) {
for (int i = 0; i < stocks.length; i++) {
StockTicker st = new StockTicker();
st.setSymbol(stocks[i]);
st.setOpen(Float.valueOf(opens[i]));
st.setChange(0.0f);

items.add(st);
}
}

Log.i(TAG, "StockAlertApplication - onCreate(): Initialized items !!!");
}

public Handler getHandler() {
return handler;
}

public void setHandler(Handler handler) {
this.handler = handler;
}

public List<StockTicker> getStockTickerList() {
return items;
}

public boolean isLowOrHighAlertSet(int pos) {
if (pos >= 0 && pos < items.size()) {
if (items.get(pos).isLowAlertSet() || items.get(pos).isHighAlertSet()) {
return true;
}
}
return false;
}
}

The class StockAlertApplication allows us to share both the Handler object instance and a list of StockTicker object instances.

The contents of the java source file StockTickerListAdapter.java will look like the one shown in the listing 12.9 below:

Listing-12.9
package com.polarsparc.android.droidstockalert;

import java.util.List;

import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.graphics.Color;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;

public class StockTickerListAdapter extends ArrayAdapter<StockTicker> {
private static String TAG = "StockTickerListAdapter";

private int[] colors = { Color.parseColor("#202028"), Color.parseColor("#36363d") };

private List<StockTicker> items;

private Context context;

public StockTickerListAdapter(Context context, List<StockTicker> items) {
super(context, R.layout.list_item_row, items);
this.items = items;
this.context = context;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) getContext().
getSystemService(Context.LAYOUT_INFLATER_SERVICE);

convertView = inflater.inflate(R.layout.list_item_row, parent, false);
}

// Change row background color
convertView.setBackgroundColor(colors[position % colors.length]);

// Symbol
TextView symbol = (TextView)convertView.findViewById(R.id.symbol);
symbol.setText(items.get(position).getSymbol());
symbol.setTextColor(Color.parseColor("#ff9900"));

// Price
TextView price = (TextView)convertView.findViewById(R.id.price);
price.setText(String.format("%.02f", items.get(position).getPrice()));
price.setTextColor(Color.parseColor("#ff9900"));

// Change
TextView change = (TextView)convertView.findViewById(R.id.change);
change.setText(String.format("%.02f", items.get(position).getChange()));

// Change color
switch (items.get(position).getTickSignal()) {
case UP:
change.setTextColor(0xff009900); // Dark Green
break;
case FLAT:
change.setTextColor(Color.BLACK); // Black
break;
case DOWN:
change.setTextColor(Color.RED); // Red
break;
}

// Set the image to click
{
StockAlertApplication application = (StockAlertApplication) context.getApplicationContext();

ImageView config = (ImageView)convertView.findViewById(R.id.config);

if (application.isLowOrHighAlertSet(position)) {
config.setImageResource(R.drawable.modify);
}
else {
config.setImageResource(R.drawable.add);
}

config.setOnClickListener(new ImageButtonOnClickListener(position));
}

return convertView;
}

// ----- Inner Class(es) -----

private class ImageButtonOnClickListener implements View.OnClickListener {
private int position;

public ImageButtonOnClickListener(int position) {
this.position = position;
}

@Override
public void onClick(View view) {
Log.i(TAG, "StockTickerListAdapter:ImageViewOnClickListener - onClick(): position = " +
position);

StockAlertApplication application = (StockAlertApplication) context.getApplicationContext();
Handler handler = application.getHandler();
if (handler != null) {
Message msg = new Message();
msg.arg1 = position;
msg.what = StockAlertApplication.LAUNCH_MSG;
handler.sendMessage(msg);
}
}
}
}

The StockTickerListAdapter class is a custom ArrayAdapter that is used to draw each row of the ListView of the activity activity_stock_alert.xml.

Note that the rows are displayed in alternating background colors.

Each row displays three text columns (symbol, price, and change) and one image icon column. Clicking on the image of a row launches the activity activity_apply_cancel.xml. Since an activity can only be launched from the main UI Thread, we need reference to the Handler object of the main UI Thread to send it a Message.

Once the activity activity_apply_cancel.xml is launched, one can either set or cancel (previously set) Low and High Price alerts. Note that the image icon changes when there is alert set.

The contents of the java source file StockAlertActivity.java will look like the one shown in the listing 12.10 below:

Listing-12.10
package com.polarsparc.android.droidstockalert;

import java.lang.ref.WeakReference;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.app.Activity;
import android.content.Intent;
import android.widget.ArrayAdapter;
import android.widget.ListView;

public class StockAlertActivity extends Activity {

private static String launchAction = null;

private ArrayAdapter<StockTicker> adapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stock_alert);

launchAction = getResources().getString(R.string.action_name);

StockAlertApplication application = (StockAlertApplication) getApplicationContext();

adapter = new StockTickerListAdapter(this, application.getStockTickerList());

Handler handler = new UIUpdateHandler(this, adapter);

application.setHandler(handler);

final ListView listview = (ListView) findViewById(R.id.list);
listview.setAdapter(adapter);
}

@Override
protected void onStart() {
super.onStart();

Intent intent = new Intent(getBaseContext(), StockUpdaterService.class);

startService(intent);
}

// ----- Inner Class(es) -----

private static class UIUpdateHandler extends Handler {
private final Activity activity;

private final WeakReference<ArrayAdapter<StockTicker>> adapterRef;

public UIUpdateHandler(Activity activity, ArrayAdapter<StockTicker> adapter) {
this.activity = activity;
adapterRef = new WeakReference<ArrayAdapter<StockTicker>>(adapter);
}

@Override
public void handleMessage(Message msg) {
if (msg.what == StockAlertApplication.UPDATE_MSG) {
ArrayAdapter<StockTicker> adapter = adapterRef.get();
if (adapter != null) {
adapter.notifyDataSetChanged();
}
}
else if (msg.what == StockAlertApplication.LAUNCH_MSG) {
Intent in = new Intent(launchAction);
in.putExtra("position", msg.arg1);
activity.startActivity(in);
}
}
}

}

In the listing 12.10 above, notice we have created a static inner class for the Handler implementation. Why did we do this ??? If we dont do this (as was the case in Part-7, we will see the following Android warning in Eclipse:

This Handler class should be static or leaks might occur

In Java, inner class(es) implicitly hold references to their outer class. So if we had followed what we had done in Part-7, the inner Handler class would hold reference to the outer activity class. An Handler class internally uses a message queue to queue incoming Message objects for processing. If we change the orientation of the screen, then the activity is killed and if there are still pending Message objects in the message queue of the Handler, then the Java garbage collector will not clean the references. The screen orientation change will create a new activity and the corresponding Handler. Imagine doing this a few times - we have a memory leak !!!

Static inner class(es) will never hold an implicit reference to their outer class(es).

This is the recommended practice in Android programming.

When the Handler gets a message with the LAUNCH_MSG command, it starts the activity_apply_cancel.xml activity.

Similarly, when the Handler gets a message with the UPDATE_MSG command, it starts refreshes the stock ticker list on the screen.

In the onStart() method of the class StockAlertActivity, we start an IntentService in the background to update the stock prices. The IntentService is defined in the class StockUpdaterService.

The contents of the java source file StockUpdaterService.java will look like the one shown in the listing 12.11 below:

Listing-12.11
package com.polarsparc.android.droidstockalert;

import java.util.Calendar;
import java.util.List;
import java.util.Random;

import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

public class StockUpdaterService extends IntentService {
private static String TAG = "StockUpdaterService";

private boolean loop = true;

private final int REQUEST_CODE = 0;
private final int NOTIFICATION_ID = 1;

private final String TITLE = "%s Alert on %s <%.02f> @ %02d:%02d";

private final String TEXT = "%s Alert!";

private Random rand = new Random();

private Context context;

private NotificationManager notificationManager;

private BroadcastReceiver receiver;

public StockUpdaterService() {
super("StockUpdaterService");

// Setup a broadcast receiver to receive system events (like low battery)
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();

// When the battery is low, we should shut the service
if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
int level = intent.getIntExtra("level", 0);

Log.i(TAG, "StockUpdaterService - BroadcastReceiver - onReceive():
Battery level = " + level);

if (level <= 25) {
Log.w(TAG, "StockUpdaterService - BroadcastReceiver - onReceive():
Battery level LOW !!!");

stopSelf();
}
}
}
};
}

@Override
protected void onHandleIntent(Intent intent) {
Log.i(TAG, "StockUpdaterService - onHandleIntent(): Ready to start service !!!");

notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);

registerReceiver(receiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));

StockAlertApplication application = (StockAlertApplication) getApplicationContext();

context = application;

while (loop) {
try {
Thread.sleep(5000); // Sleep for 5 secs
}
catch (Exception ex) {
}

updateAndAlert(application.getStockTickerList());

Handler handler = application.getHandler();
if (handler != null) {
Message msg = new Message();
msg.what = StockAlertApplication.UPDATE_MSG;
handler.sendMessage(msg);
}
}
}

@Override
public void onDestroy() {
unregisterReceiver(receiver);

loop = false;

Log.i(TAG, "StockUpdaterService - onDestroy(): Ready to shut service !!!");
}

// ----- Private Method(s) -----

private void updateAndAlert(List<StockTicker> items) {
for (int i = 0; i < items.size(); i++) {
float change = rand.nextFloat() * 10;
if (rand.nextInt(100) % 2 == 1) {
change = change * -1.0f;
}
StockTicker st = items.get(i);
st.setChange(change);

// Check for low alert
if (st.isLowAlert()) {
Log.i(TAG, "StockUpdaterService - updateStockTicker(): Low alert on " +
st.getSymbol() + " !!!");

sendNotification(false, st.getSymbol(), st.getPrice());
}

// Check for high alert
if (st.isHighAlert()) {
Log.i(TAG, "StockUpdaterService - updateStockTicker(): High alert on " +
st.getSymbol() + " !!!");

sendNotification(true, st.getSymbol(), st.getPrice());
}
}
}

private void sendNotification(boolean high, String symbol, float price) {
String str = high ? "High" : "Low";

Calendar cal = Calendar.getInstance();

int curHour = cal.get(Calendar.HOUR_OF_DAY);
int curMinute = cal.get(Calendar.MINUTE);

int icon = high ? R.drawable.high_alert : R.drawable.low_alert;

Intent notifyIntent = new Intent(this, StockAlertActivity.class);

PendingIntent pendingIntent = PendingIntent.getActivity(context, REQUEST_CODE,
notifyIntent, Intent.FLAG_ACTIVITY_NEW_TASK);

Notification notification = new Notification.Builder(context)
.setContentTitle(String.format(TITLE, str, symbol, price, curHour, curMinute))
.setContentText(String.format(TEXT, str))
.setSmallIcon(icon)
.setContentIntent(pendingIntent)
.getNotification();
notification.defaults |= Notification.DEFAULT_SOUND;
notification.flags |= Notification.FLAG_AUTO_CANCEL;

notificationManager.notify(NOTIFICATION_ID, notification);
}

}

From the code Listing 12.11 above, the new and interesting parts are in the methods StockUpdaterService(), onHandleIntent(), and onDestroy().

From the previous topic on Service in Part-10, we know that a Service can run in the background forever. What happens when the battery becomes low ??? Do we want to still continue running, draining the battery further or as a good citizen shut off the background Service and conserve battery power ???

How would an Android application know that the battery power is low ??? Enter the Android framework class android.content.BroadcastReceiver.

One can register an implementation of BroadcastReceiver with the Android system to receive system events. The Android system broadcasts system Intent for various events such as BATTERY_LOW or CONNECTIVITY_CHANGE, etc. In our case, we will register a custom implementation of BroadcastReceiver to listen for BATTERY_CHANGED system event. When the battery capacity reaches 25%, we will shutdown the background service StockUpdaterService that updates the stock ticker prices.

From the code Listing 12.11 above, we perform the following steps at the various methods:

Note that nothing prevents us from boardcasting our own application events using Intent.

The code to handle the user setting or cancelling price alerts is defined in the class StockAlertActivity2. The contents of the java source file StockAlertActivity2.java will look like the one shown in the listing 12.12 below:

Listing-12.12
package com.polarsparc.android.droidstockalert;

import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.util.Log;

public class StockAlertActivity2 extends Activity {
private static String TAG = "StockAlertActivity2";

final Activity act = this;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_apply_cancel);

int pos = getIntent().getIntExtra("position", -1);

Log.i(TAG, "StockAlertActivity2 - onCreate(): pos = " + pos);

if (pos >= 0) {
StockAlertApplication application = (StockAlertApplication) getApplicationContext();

List<StockTicker> items = application.getStockTickerList();

final StockTicker ticker = items.get(pos);

// Symbol text
final TextView symbolTxt = (TextView) findViewById(R.id.text1);
symbolTxt.setText(ticker.getSymbol());

// Low alert fields
final EditText lowFld = (EditText) findViewById(R.id.edit1);
if (ticker.getLowAlert() > 0.0f) {
lowFld.setText(Float.toString(ticker.getLowAlert()));
}

// High alert fields
final EditText highFld = (EditText) findViewById(R.id.edit2);
if (ticker.getHighAlert() > 0.0f) {
highFld.setText(Float.toString(ticker.getHighAlert()));
}

// Apply button
final Button applyBtn = (Button) findViewById(R.id.button1);
applyBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Apply low and/or high alerts
float lowAlert = 0.0f;
float highAlert = 0.0f;

String lowStr = lowFld.getText().toString();
try {
lowAlert = Float.parseFloat(lowStr.trim());
}
catch (Throwable ex) {
}

if (lowAlert >= 0.0f) {
ticker.setLowAlert(lowAlert);
}

String highStr = highFld.getText().toString();
try {
highAlert = Float.parseFloat(highStr.trim());
}
catch (Throwable ex) {
}

if (highAlert >= 0.0f) {
ticker.setHighAlert(highAlert);
}

// Close this activty
act.finish();
}
});

// Cancel button
final Button cancelBtn = (Button) findViewById(R.id.button2);
cancelBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Reset low and high alerts
if (ticker.getLowAlert() > 0.0f) {
lowFld.setText("");
ticker.setLowAlert(0.0f);
}

if (ticker.getHighAlert() > 0.0f) {
highFld.setText("");
ticker.setHighAlert(0.0f);
}

// Close this activty
act.finish();
}
});

// Quit button
final Button quitBtn = (Button) findViewById(R.id.button3);
quitBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Close this activty
act.finish();
}
});
}
}

}

Finally, modify the contents of the AndroidManifest.xml file to look like the one shown in the listing 12.13 below:

Listing-12.13
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.polarsparc.android.droidstockalert"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="14" />

<uses-permission android:name="android.permission.BATTERY_STATS" />

<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:name=".StockAlertApplication"
android:theme="@style/AppTheme" >
<activity
android:name=".StockAlertActivity"
android:label="@string/app_name"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".StockAlertActivity2"
android:label="Set or Cancel Alert"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="com.polarsparc.android.STOCK_ALERT_2" />

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<service android:name=".StockUpdaterService" />
</application>

</manifest>

The first change to notice is the additional attribute named android:name for the tag <application>. This attribute has the value of our custom Application class StockAlertApplication. When our Android application starts up, the Android system will create an instance of this class and is made available to the application via the method call getApplicationContext() of the Context.

The second change to notice will be around permissions. By default, an Android application is granted no permissions. In order to listen to and receive the system events related to the device battery, we need to grant permissions for BATTERY_STATS. We do that by listing the required permissions between the tag <uses-permission>.

We are now ready to test our DroidStockAlert application on the virtual Android device we created in Part-1. We will create a Run Configuration for DroidStockAlert as we did in Part-2 for DroidTipCalculator.

Once the run configuration for DroidStockAlert is ready, we will Run the application and the application will come to life as shown in the following figure 12.1 below:

Main Stock Alert Image
Figure-12.1

Clicking on the Green Plus image on the stock symbol IBM will launch the activity activity_apply_cancel.xml as shown in the following figure 12.2 below:

Set Cancel Alert Image
Figure-12.2

Key in the desired Low Price numeric value in the input field at which you want the alert. For this test, let us enter a value of 200.00 and click on the Set Alert button as shown in the following figure 12.3 below:

Set Low Alert Image
Figure-12.3

When any type of alert (Low or High) is set, notice the change in the image to a Red Minus from a Green Plus as shown in the following figure 12.4 below:

Changed Image View Image
Figure-12.4

When the price for the stock symbol IBM reaches 200.00 or below, we are notified with a notification as shown in the following figure 12.5 below:

Low Price Alert Status Image
Figure-12.5

Drag the notification icon downwards with the computer mouse (to simulate the swipe down on the device) and it will reveal the Low Price alert as shown in the following figure 12.6 below:

Low Price Notification Image
Figure-12.6

Now, click on the Red Minus to reset the Low Price alert and set the High Price alert. Key in the desired High Price numeric value in the input field at which you want the alert. For this test, let us enter a value of 216.00 and click on the Set Alert button as shown in the following figure 12.7 below:

Set High Alert Image
Figure-12.7

When the price for the stock symbol IBM reaches 216.00 or above, we are notified with a notification as shown in the following figure 12.8 below:

High Price Alert Status Image
Figure-12.8

Drag the notification icon downwards with the computer mouse (to simulate the swipe down on the device) and it will reveal the High Price alert as shown in the following figure 12.9 below:

High Price Notification Image
Figure-12.9

The last part of functionality that needs to be demostrated is the handling of BATTERY_CHANGED system event.

So how do we control and simulate the battery power capacity of the Virtual Android Device ???

Well it turns out that we can connect to the Virtual Device at the network port at which the emulator is running using telnet and issue commands.

So how do we find the emulator network port to connect to ???

We point out the network port and the fact that the Virtual Device emulator is running on AC power in the following figure 12.10 below:

Port and Battery Image
Figure-12.10

From the figure 12.10 above, we determine the emulator port to connect to be 5554.

Open a terminal and using telnet, issue the following command:

$ telnet localhost 5554

The terminal window will appear as shown in the following figure 12.11 below:

Telnet Terminal Image
Figure-12.11

To turn the AC power off, issue the power ac off command in the terminal running the telnet command and then press the ENTER key.

The terminal window will appear as shown in the following figure 12.12 below:

Telnet Power Off Image
Figure-12.12

Now notice the change in the battery status in the Virtual Device as shown in the following figure 12.13 below:

AVD Battery Image
Figure-12.13

To control the battery charge capacity, issue the power capacity 40 command in the terminal running the telnet command and then press the ENTER key. This will change the battery charge to 40%. However, this will not shut the service StockUpdaterService and the stock prices will continue to change. Next, issue the power capacity 25 command in the terminal running the telnet command and then press the ENTER key. Now the service StockUpdaterService will be shut down as the battery charge has dropped to 25%.

The terminal window will appear as shown in the following figure 12.14 below: