Introduction
This article is a complete description of an Android gps application.
Links to all the Android code used in this application are provided.
The WalkersMap.zip has the complete project for this application.
I use eclipse Indigo with the Android SDK package. Open the PROJECT FILE
and eclipse should load this application’s code.
Android has a complete description on how to load the SDK, however I found two problems.
There is a problem when using windows 7. We cant load the android package
when eclipse is stored in C:\program. It will load if you run eclipse as administrator.
I stored eclipse in the video folder and then I don’t need right click.
Android virtual devices are communicated with via ports. If you have a zealous firewall
it can block its operation. I needed to add an allowed application to the firewall.
Program operation:
At its basic level the program maps gps locations. We have the following specifications:
1. The map is a 1km grid, it contains no altitude information.
It uses Universal Transverse Mercator geographic coordinates.
2. The user can have a UTM terrain map. Way points from this map can
be entered into the program and displayed along with the gps locations.
3. The map will have a maximum 200km by 200km area.
The display can be zoomed in/out moved left/right, up/down.
The largest display is a 50km by 50 km area. The smallest is 1km by 1km.
4. Gps is a heavy user of battery power so the phone must sleep between gps fixes.
Fixes can be set to automatic. Wake the phone once every 5 minutes for the fix.
5. All the gps locations need to be stored in the phones flash memory.
They are then restored to the application after a phone reboot.
Splash screen:
To do this I use a CountDownTimer.
In onCreate the main.xml has the splash screen image.
We hold this image for 3 seconds then reload the screen with the graphics
map page constructed in mView.
setContentView(R.layout.main);
new CountDownTimer(3000,1000)
{
public void onFinish()
{
setContentView(mView);
}
public void onTick(long arg0){}
}.start();
Map Screen:
The screen is constructed in the MapView class. We pass the screen dimensions to the class
and return the screen with all the data points plotted.
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
screenW =metrics.widthPixels;
screenH = metrics.heightPixels;
mView = new MapView(this,screenW,screenH);
The onDraw method is called every time the screen is redrawn. This happens on startup
and any time I call mView.invalidate(); //redraw screen
public class MapView extends View
{
public MapView(Context context, float sW, float sH)
{
super(context);
setFocusable(true);
d = sW/260;
h = sH;
constructSigns();
}
@Override protected void onDraw(Canvas canvas)
{
drawButtons(canvas);
}
}
The program uses scaling and shifting to generate the map. We can zoom in/out,
shift left/right, shift up/down. This allows the user to center his locations on the
map at the optimum viewing size.
The drawButtons method makes use of Android Path and matrix API’s.
Integral to constructing the map is the conversion of the lat/long
gps fixes(degrees) into meters.
I use the Universal Transverse Mercator geographic coordinate system to do the conversion.
Universal Transverse Mercator:
The system divides the Earth into sixty zones, each a six-degree band of longitude.
The main reason I use UTM is because most bush walkers will have a UTM map of
the area they traverse. They will plot their walk on the paper map and then select the way points in this application.
They then set continuous mode and the program will plot the gps fixes over the selected way point path.
The conversion mathematics comes from the Defense Mapping Agency Technical Manual.
The conversion is done in the UTS class.
public class UTS
{
static long EastingI = 0;
static long NorthingI = 0;
static int ZoneNumber = 0;
static char ZoneDesignator = 'C';
private static double CM = 0;
private final static char ZD[] = {'C','D','E','F','G','H','J','K','L','M',
'N','P','Q','R','S','T','U','V','W','X'};
public static void ConvertGeoUts(double lat, double lon)
{
ZoneNumber = ((int) Math.floor(lon) + 180)/6 + 1;
CM = (ZoneNumber - 1)*6 + 3 - 180;
byte i = 0;
for(i = 0; i < 20; i++)
{
if(lat + 80 < i*8) { break; }
}
ZoneDesignator = ZD[i - 1];
N_GEOtoUTS(lat, lon);
E_GEOtoUTS(lat, lon);
}
}
The methods are all static so that they are held permanently in RAM.
This gives faster operation as I do not need to create an instance each time it’s needed.
UTS.ConvertGeoUts(lat , lon); //this is all thats needed
Map Screen Menu:
The menu is defined in the menu.xml file:
="1.0"="utf-8"
<menu xmlns:android="<a href="http://schemas.android.com/apk/res/android">http://schemas.android.com/apk/res/android</a>" >
<item android:id="@+id/menu_help"
android:icon="@android:drawable/ic_menu_help"
android:title="@string/menu_help" />
<item android:id="@+id/menu_reference"
android:icon="@android:drawable/ic_menu_compass"
android:title="@string/menu_gpsloc" />
<item android:id="@+id/menu_locations"
android:icon="@android:drawable/ic_menu_myplaces"
android:title="View Locations" />
<item android:id="@+id/menu_start"
android:icon="@android:drawable/ic_menu_more"
android:title="Start Continuous" />
<item android:id="@+id/menu_delete"
android:icon="@android:drawable/ic_menu_delete"
android:title="Delete this Walk" />
<item android:id="@+id/menu_cancel"
android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:title="Cancel Continuous" />
</menu>
The menu is implemented by:
@Override
public boolean onCreateOptionsMenu(Menu menu)
{
super.onCreateOptionsMenu(menu);
MenuInflater mi = getMenuInflater();
mi.inflate(R.menu.menu, menu);
return true;
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item)
{
switch(item.getItemId())
{
case R.id.menu_help:
Intent i =new Intent(this,HelpFile.class);
startActivity(i);
return true;
case R.id.menu_reference:
Intent igps =new Intent(this,selectReference.class);
startActivity(igps);
return true;
case R.id.menu_locations:
Intent igl =new Intent(this,ViewLocations.class);
startActivity(igl);
return true;
case R.id.menu_delete:
DeleteWalk();
return true;
case R.id.menu_start:
long alarm = 300000;
if(!continuous){
CG.setContinuous((long) 0, alarm);
continuous = true;
mView.invalidate();
}
return true;
case R.id.menu_cancel:
if(continuous){
CG.cancelContinuous();
continuous = false;
mView.invalidate();
}
return true;
}
return super.onMenuItemSelected(featureId, item);
}
Map Screen Delete:
If the users wishes to delete the entire walk I ask with a dialog if this is their wish:
private void DeleteWalk()
{
mDbHelper = new LocationsDatabase(this);
mDbHelper.open();
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("Do you want to delete all gps and way point locations ")
.setTitle("'Delete this Walk")
.setCancelable(false)
.setPositiveButton("Yes Delete All", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
mDbHelper.DeleteAllLocation();
NumLoc = 0;
Location[0][0]=0; Location[0][1]=0; Location[0][2]=0;
Location[0][3]=0; Location[0][4]=0;
NumWay = 0;
WayPoint[0][0]=0; WayPoint[0][1]=0;
mDbHelper.close();
mView.invalidate();
}
})
.setNegativeButton("No Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
mDbHelper.close();
dialog.cancel(); }
});
final AlertDialog alert = builder.create();
alert.show();
}
Data Restoration:
Applications are stored in flash memory(sometimes called ROM).
This is non volatile and is not lost when the phone is powered down.
When the application is started, the program is loaded into the RAM to allow direct access by the CPU.
All data that is generated by the program is also stored in RAM.
If the program loses focus because of a phone call or user operation, the data remains in RAM.
If the user or the system puts the phone to sleep, the data remains in RAM.
However if the phone is turned off the RAM loses the program and any data generated.
The program remains in flash memory and is reloaded by the android operating system
when the user selects the program.
Any data you need to restore must be first stored in the flash.
Android provides methods for saving and restoring data in the flash.
I use two types of data storage.
Shared Preferences for individual values, start latitude/longitude and Number of gps/waypoints.
The data is stored in onStop whenever the map page loses focus.
protected void onStop()
{
super.onStop();
SharedPreferences settings = getSharedPreferences("gps", 0);
SharedPreferences.Editor editor = settings.edit();
editor.putInt("GpsLocations", NumLoc);
editor.putInt("WayPoints", NumWay);
editor.putFloat("LatRef", (float) MapView.latRef);
editor.putFloat("LonRef", (float) MapView.lonRef);
editor.commit();
}
The data is restored whenever the program is started in onCreate.
SharedPreferences settings = getSharedPreferences("gps",0);
MapView.latRef = settings.getFloat("LatRef", 0);
MapView.lonRef = settings.getFloat("LonRef", 0);
NumLoc = settings.getInt("GpsLocations", 0);
NumWay = settings.getInt("WayPoints", 0);
To store the data from 576 gps locations and 50 way points we need a database.
SQLite database:
The database is enabled in the public class LocationsDatabase
In this class we have a nested class that uses the recommended method
to create a new SQLite database.
private static class DatabaseHelper extends SQLiteOpenHelper
{
DatabaseHelper(Context context)
{
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) { db.execSQL(DATABASE_CREATE);}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion,int newVersion)
{
db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE);
onCreate(db);
}
}
The LocationsDatabase class has these methods which are used to store and access
data from the SQLite database(which is in the flash memory).
public LocationsDatabase open() throws SQLException
public void close() { mDbHelper.close();}
public long AddLocation(int row, String type, double latoreast, double lonornorth,
double altitude, double time, double accuracy)
public Cursor ReadLocation(long rowId) throws SQLException
public boolean DeleteAllLocation()
To access the database we need to create an instance of LocationsDatabase, open
the data base and then store the data. Finally we must close the database.
So to store a way point we have:
mDbHelper = new LocationsDatabase(this);
mDbHelper.open();
mDbHelper.AddLocation(WalkersMapGps.NumWay + WalkersMapGps.NumLoc,"way",
WalkersMapGps.WayPoint[WalkersMapGps.NumWay][0],
WalkersMapGps.WayPoint[WalkersMapGps.NumWay][1],0,0,0);
WalkersMapGps.NumWay += 1;
mDbHelper.close();
There are two types of data stored in the database. Gps locations labeled “gps” and
way points labeled “way” .To restore all the data we read the data base:
Cursor ReadRow =mDbHelper.ReadLocation(i);
startManagingCursor(ReadRow);
String type = ReadRow.getString(ReadRow.getColumnIndexOrThrow
(LocationsDatabase.KEY_TYPE));
if(type.equals("gps"))
{
Location[l][0] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LATOREAST));
Location[l][1] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LONGORNORTH));
Location[l][2] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_ALTITUDE));
Location[l][3] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_TIME));
Location[l][4] = ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_ACCURACY));
l += 1;
}
else
{
WayPoint[w][0] =(int) ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LATOREAST));
WayPoint[w][1] =(int) ReadRow.getDouble(ReadRow.
getColumnIndexOrThrow(LocationsDatabase.KEY_LONGORNORTH));
w += 1;
}
Gps Locations:
The GpsActivity class has all the code necessary to get the gps fixes.
If a fix cannot be obtained within 3 minutes the activity will close.
I use a CountDownTime to close the activity.
The top button(gps on/off) can be selected to redirect the user to the Android
location settings page:
gpsOn = (Button) findViewById(R.id.location);
gpsOn.setOnClickListener(new View.OnClickListener()
{
public void onClick(View v) {
Intent i = new Intent(Settings.
ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(i);
finish();
}
});
The code to obtain gps fixes is the recommended method:
locationManager = (LocationManager) this.getSystemService(Context.
LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER
, 0, 0, locationListener);
LocationListener locationListener = new LocationListener()
{
public void onLocationChanged(Location location)
{
makeUseOfNewLocation(location);
}
public void onStatusChanged(String provider, int status, Bundle extras) {}
public void onProviderEnabled(String provider) {}
public void onProviderDisabled(String provider) {}
};
private void makeUseOfNewLocation(Location loc)
{
Location mLoc = (Location) loc;
if(GetReference)
{
MapView.latRef = mLoc.getLatitude();
MapView.lonRef = mLoc.getLongitude();
GetReference = false;
}
else
{
WalkersMapGps.Location[WalkersMapGps.NumLoc][0] = mLoc.getLatitude();
WalkersMapGps.Location[WalkersMapGps.NumLoc][1] = mLoc.getLongitude();
WalkersMapGps.Location[WalkersMapGps.NumLoc][2] = mLoc.getAltitude();
WalkersMapGps.Location[WalkersMapGps.NumLoc][3] = mLoc.getTime();
WalkersMapGps.Location[WalkersMapGps.NumLoc][4] = mLoc.getAccuracy();
if(WalkersMapGps.NumLoc < 574)
{
mDbHelper.AddLocation(WalkersMapGps.NumLoc + WalkersMapGps.NumWay,"gps",
WalkersMapGps.Location[WalkersMapGps.NumLoc][0],
WalkersMapGps.Location[WalkersMapGps.NumLoc][1],
WalkersMapGps.Location[WalkersMapGps.NumLoc][2],
WalkersMapGps.Location[WalkersMapGps.NumLoc][3],
WalkersMapGps.Location[WalkersMapGps.NumLoc][4]);
WalkersMapGps.NumLoc += 1;
}
else{
Toast.makeText(getApplication(), "Only 576 locations allowed", Toast.LENGTH_SHORT).show();
}
}
cancelGps();
}
Set Reference:
The selectReference class has the code to select the start or reference point for the map.
The user can enter a latitude/longitude: They can enter each separately.
Android does not have an edit text box which allows negative float numbers.
To implement this feature I use try catch blocks. To catch a value out of bounds
I throw an exception. The latitude entry is shown:
final EditText ReferenceLat = (EditText) findViewById(R.id.enter_latitude);
ReferenceLat.setText(String.valueOf(MapView.latRef));
final Button selectLat = (Button) findViewById(R.id.b101);
selectLat.setOnClickListener(new View.OnClickListener()
{
public void onClick(View v) {
try
{
Editable text = ReferenceLat.getText();
double lat = Double.parseDouble(text.toString());
if(lat < -80 || lat > 84) {throw new Exception();}
MapView.latRef = lat;
}
catch(Exception e){
Toast.makeText(getApplication(), "Latitude must between -80.0 and 84.0", Toast.LENGTH_SHORT).show();
}
}
});
Continuous Gps:
The gps is a heavy user of battery power.
I require a gps fix once every 5 minutes.
The first gps fix requires the download of the
Ephemeris (precise satellite orbit) for each satellite.
This may take up to 2 minutes.
Subsequent fixes will only take seconds.
So the phone sleeps for most of the time and it wakes to get
a quick gps fix.
This uses very little battery, extending the period the user
can utilize this application.
Firstly in public class ContinuousGps I set the alarm manager.
public ContinuousGps(Context context) {
mContext = context;
mAlarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
Intent i = new Intent(mContext, OnAlarmReceiver.class);
pi = PendingIntent.getBroadcast(mContext, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
}
public void cancelContinuous(){ mAlarmManager.cancel(pi); }
public void setContinuous(Long taskId, long alarmtime) {
mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000,
alarmtime, pi);
}
Then in public class OnAlarmReceiver I need to setup a BroadcastReceiver.
When this is received we wake the phone and hold it on while we call
a service.
public class OnAlarmReceiver extends BroadcastReceiver
{
@Override
public void onReceive(Context context, Intent intent)
{
WakeIntentService.acquireStaticLock(context);
Intent i = new Intent(context, GpsService.class);
context.startService(i);
}
}
The service started is in the public class GpsService.
The service starts the GpsActivity which gets the gps fix.
public class GpsService extends WakeIntentService {
public GpsService() {
super("GpsService");
}
@Override
void doGpsWork(Intent intent)
{
Intent i = new Intent(this, GpsActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(i);
}
}
The WakeIntentService class is where we setup the PowerManager.WakeLock.
The code used is the recommended method.
public abstract class WakeIntentService extends IntentService
{
abstract void doGpsWork(Intent intent);
public static final String LOCK_NAME_STATIC="com.carl47.walkers";
private static PowerManager.WakeLock lockStatic=null;
public static void acquireStaticLock(Context context) {
getLock(context).acquire();
}
synchronized private static PowerManager.WakeLock getLock(Context context) {
if (lockStatic==null) {
PowerManager mgr=(PowerManager)context.getSystemService(Context.POWER_SERVICE);
lockStatic=mgr.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
LOCK_NAME_STATIC);
lockStatic.setReferenceCounted(true);
}
return(lockStatic);
}
public WakeIntentService(String name) {
super(name);
}
@Override
final protected void onHandleIntent(Intent intent) {
try {
doGpsWork(intent);
}
finally {
getLock(this).release();
}
}
}
Manifest:
All the displayed screens are set to portrait, the map page has
no title bar and uses the full screen.
The manifest contains all the necessary permissions and activities used:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="<a href="http:
package="carl47.com"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="6" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >
<activity
android:name=".WalkersMapGps"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
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=".GpsActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"/>
<activity
android:name=".HelpFile"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:screenOrientation="portrait"/>
<activity
android:name=".GpsSelect"
android:label="@string/app_name"
android:screenOrientation="portrait" />
<activity
android:name=".selectReference"
android:label="@string/app_name"
android:screenOrientation="portrait"/>
<activity
android:name=".ViewLocations"
android:label="@string/app_name"
android:screenOrientation="portrait"/>
<activity
android:name=".WayPoints"
android:label="@string/app_name"
android:screenOrientation="portrait"/>
<receiver android:name=".OnAlarmReceiver" />
<service android:name=".GpsService" />
</application>
</manifest>
Conclusion:
The application works as per the specifications.
It’s published on the Android market. It’s completely free, download it to your Android phone
to see it work as advertised.
I identify with the starfish.
I may be really stupid and have to use visual basic but at least I'm happy.