Now that we know how GPS and Google Maps work together, we will look at another approach to using them. To achieve this, we will need to make minor modifications throughout the app, but we will see how to do this and still allow the existing users to keep their data.
In this chapter, we will:
Photo
class to handle location information about where in the world a photo was takenCaptureFragment
and ViewFragment
to give the functionality we are aiming forWe want to add a feature to our app where the user can click on the SHOW MAP button while viewing a photo, and the app will then show a map with the location of where the photo was taken in the world.
We have a good insight into this already from the previous chapter, when we viewed a specific GPS location on Google Maps.
The functionality we need to add and the issues we need to overcome are as follows:
Photo
class and database structures to store GPS locationsDataManager
and helper methods to store and retrieve the extra bit of dataInitially, in the context of simply developing an app, this might seem straightforward. Now, imagine that our app is already published. We have a million users with lots of photos already stored in their database. If we simply update our app with new helper methods and a new database structure that holds GPS locations, then all the users' existing data will be lost.
Fortunately, SQLite and the supporting Android API are designed around this problem. What we will do now is we will step through and update the Where it's Snap database, Photo
class, and helper methods and we will add the map functionality as well.
We need to update the database, not delete the existing one and create a new one.
We have two scenarios to deal with:
To cater for the first scenario, the SQL keyword we need is ALTER
. Also, we need to put our code and SQL into the onUpgrade
method of the CustomSQLiteOpenHelper
class in our DataManager
class.
How will we trigger the upgrade in devices that already have the app installed? All we need to do is increment the version of the database. In our case, this is held in the DB_VERSION
int. Each time an instance of the CustomSQLiteOpenHelper
class is instantiated, it checks the version held in the DB_VERSION
int against the actual version of the current database on the device. The point at which this check occurs is when we call the super class constructor from our constructor, which is highlighted in the next code snippet:
public CustomSQLiteOpenHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
This code is already part of our app. All we need to do to get started is increment the version.
Now that we know what we need to do, we can go ahead and update the database structure (for new and existing users), increment the database version, and add code in onUpgrade
(for the existing users).
First, add two more public static final String
variables to our DataManager
class so that they represent our new fields, as highlighted in the next code:
public static final String TABLE_ROW_ID = "_id"; public static final String TABLE_ROW_TITLE = "image_title"; public static final String TABLE_ROW_URI = "image_uri"; // New with version 2 public static final String TABLE_ROW_LOCATION_LAT = "gps_location_lat"; public static final String TABLE_ROW_LOCATION_LONG = "gps_location_long"; /* Next we have a private static final strings for each row/table that we need to refer to just inside this class */…
The next step will cause the onUpgrade
method to be called for any users whose current database is at version 1. Edit the value of DB_VERSION
in the DataManager
class, as highlighted in the following code:
/*
Next we have a private static final strings for
each row/table that we need to refer to just
inside this class
*/
private static final String DB_NAME = "wis_db";
private static final int DB_VERSION = 2;
private static final String TABLE_PHOTOS = "wis_table_photos";
private static final String TABLE_TAGS = "wis_table_tags";
private static final String TABLE_ROW_TAG1 = "tag1";
private static final String TABLE_ROW_TAG2 = "tag2";
private static final String TABLE_ROW_TAG3 = "tag3";
…
Now, we can add code to execute the altered SQL in the onUpgrade
method. In the onUpgrade
method, we add code that uses ALTER
in two separate
queries, uses our two new Strings
, and adds in the columns to the structure. Remember that this code only runs when the database on the device of the user has a version number lower than that held in DB_VERSION
. In the last step, we incremented DB_VERSION
, so this next method will run once for all existing users.
Add the code that we just discussed to the onUpgrade
method:
// This method only runs when we increment DB_VERSION @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Update for version 2 String addLongColumn = "ALTER TABLE " + TABLE_PHOTOS + " ADD " + TABLE_ROW_LOCATION_LONG + " real;"; db.execSQL(addLongColumn); String addLatColumn = "ALTER TABLE " + TABLE_PHOTOS + " ADD " + TABLE_ROW_LOCATION_LAT + " real;"; db.execSQL(addLatColumn); }
Of course, some users will be completely new to our app and may have never used it when the database was at version 1. When they install our app for the first time, onUpgrade
won't run, so we need to alter the code that creates the database to add the extra fields there as well.
We simply modify our create table
statement in the overridden onCreate
method of our SQLiteOpenHelper
class and add in the two new real
columns.
Amend the code in onCreate
that we just discussed to be the same as this next code:
// This method only runs the first time the database is created @Override public void onCreate(SQLiteDatabase db) { // Create a table for photos and all their details String newTableQueryString = "create table " + TABLE_PHOTOS + " (" + TABLE_ROW_ID + " integer primary key autoincrement not null," + TABLE_ROW_TITLE + " text not null," + TABLE_ROW_URI + " text not null," + TABLE_ROW_LOCATION_LAT + " real," + TABLE_ROW_LOCATION_LONG + " real," + TABLE_ROW_TAG1 + " text not null," + TABLE_ROW_TAG2 + " text not null," + TABLE_ROW_TAG3 + " text not null" + ");"; db.execSQL(newTableQueryString); // Create a separate table for tags newTableQueryString = "create table " + TABLE_TAGS + " (" + TABLE_ROW_ID + " integer primary key autoincrement not null," + TABLE_ROW_TAG + " text not null" + ");"; db.execSQL(newTableQueryString); }
Now, we need to make the following minor changes to our helper method that stores a photo to make sure that it handles the two extra columns.
Amend the addPhoto
helper method as shown in the following code:
// Here are all our helper methods public void addPhoto(Photo photo){ // Add all the details to the photos table String query = "INSERT INTO " + TABLE_PHOTOS + " (" + TABLE_ROW_TITLE + ", " + TABLE_ROW_URI + ", " + TABLE_ROW_LOCATION_LAT + ", " + TABLE_ROW_LOCATION_LONG + ", " + TABLE_ROW_TAG1 + ", " + TABLE_ROW_TAG2 + ", " + TABLE_ROW_TAG3 + ") " + "VALUES (" + "'" + photo.getTitle() + "'" + ", " + "'" + photo.getStorageLocation() + "'" + ", " + photo.getGpsLocation().getLatitude() + ", " + photo.getGpsLocation().getLongitude() + ", " + "'" + photo.getTag1() + "'" + ", " + "'" + photo.getTag2() + "'" + ", " + "'" + photo.getTag3() + "'" + ");"; Log.i("addPhoto()", query); db.execSQL(query); // Add any NEW tags to the tags table … …
That's all the changes that are needed for the DataManager
class.
We need to add one new member as well as a getter and setter method into the Photo
class. The new member is of the type Location
that stores both the latitude and longitude. Add the highlighted part of the next code:
private String mTitle; private Uri mStorageLocation; private Location mGpsLocation; private String mTag1; private String mTag2; private String mTag3; public Location getGpsLocation() { return mGpsLocation; } public void setGpsLocation(Location mGpsLocation) { this.mGpsLocation = mGpsLocation; } public String getTitle() { return mTitle; } … …
This will be a breeze because we have already seen most of the code that we need in the previous chapter.
Make CaptureFragment
implement LocationListener
and add the highlighted member variables:
public class CaptureFragment extends Fragment implements LocationListener { private static final int CAMERA_REQUEST = 123; private ImageView mImageView; // The filepath for the photo String mCurrentPhotoPath; // Where the captured image is stored private Uri mImageUri = Uri.EMPTY; // A reference to our database private DataManager mDataManager; // For the Location private Location mLocation = new Location(""); private LocationManager mLocationManager; private String mProvider;
In the onCreate
method, let's get the location in the usual way. Add the highlighted code to find it:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDataManager = new DataManager(getActivity().getApplicationContext()); // Initialize mLocationManager mLocationManager = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE); Criteria criteria = new Criteria(); mProvider = mLocationManager.getBestProvider(criteria, false); }
Now, we can update the class that listens for clicks on the SAVE button in the onCreateView
method to use our new setter method in the Photo
class, as shown in the following code:
// Listen for clicks on the save button btnSave.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if(mImageUri != null){ if(!mImageUri.equals(Uri.EMPTY)) { // We have a photo to save Photo photo = new Photo(); photo.setTitle(mEditTextTitle.getText().toString()); photo.setStorageLocation(mImageUri); // Set the current GPS location photo.setGpsLocation(mLocation); // What is in the tags String tag1 = mEditTextTag1.getText().toString(); String tag2 = mEditTextTag2.getText().toString(); String tag3 = mEditTextTag3.getText().toString(); // Assign the strings to the Photo object photo.setTag1(tag1); photo.setTag2(tag2); photo.setTag3(tag3); // Send the new object to our DataManager mDataManager.addPhoto(photo); Toast.makeText(getActivity(), "Saved", Toast.LENGTH_LONG).show(); }else { // No image Toast.makeText(getActivity(), "No image to save", Toast.LENGTH_LONG).show(); } }else { // No image Toast.makeText(getActivity(), "No image to save", Toast.LENGTH_LONG).show(); } } });
And of course, we must override the necessary methods of the LocationListener
interface. We don't use all these next three methods, but we must still override them as this is required by the interface. Add the following methods to the CaptureFragment
class:
@Override public void onLocationChanged(Location location) { // Update the location if it changed mLocation = location; } @Override public void onStatusChanged(String provider, int status, Bundle extras) { } @Override public void onProviderEnabled(String provider) { } @Override public void onProviderDisabled(String provider) { }
Just as we did in the Where in the World mini app, we must start and stop the updates in the onResume
and onPause
methods. If we don't call requestLocationUpdates
, then we will never get any updates, and if we don't call removeUpdates
, the device will go on communicating and receiving updates even when our Activity has ended:
// Start updates when app starts/resumes @Override public void onResume() { super.onResume(); mLocationManager.requestLocationUpdates(mProvider, 500, 1, this); } // pause the location manager when app is paused/stopped @Override public void onPause() { super.onPause(); mLocationManager.removeUpdates(this); }
That's the CaptureFragment
class ready to go.
3.147.65.247