Chapter 26. Upgrading SQLite – Adding Locations and Maps

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:

  • Update our database code, which will result in the users of our app getting an updated database
  • Add code to the Photo class to handle location information about where in the world a photo was taken
  • Upgrade the code in CaptureFragment and ViewFragment to give the functionality we are aiming for

Adding locations and maps to Where it's Snap

We 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:

  • To capture and store the location each time a photo is taken
  • When the SHOW MAP button is clicked on, we launch a map screen of that location
  • Change the Photo class and database structures to store GPS locations
  • Modify DataManager and helper methods to store and retrieve the extra bit of data
  • Add the Google Maps functionality to the SHOW MAP button

Initially, 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.

Tip

If you were adding features to an unreleased app, you could simply update the database structure, the Photo class, and so on. We are inventing this problem for ourselves just for the sake of learning about this real-world situation.

Updating the database

We need to update the database, not delete the existing one and create a new one.

We have two scenarios to deal with:

  • The existing users of our app need an update (not replacement) to their existing database so that they can keep all their current data
  • New users installing our app for the first time need the new database

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

Adding member variables to represent location data

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
*/…

Updating the database version

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";
…

Adding code in onUpgrade to upgrade the database for existing users

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

} 

Updating the database creation code in onCreate for new users

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

Updating the addPhoto method to handle GPS coordinates

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.

Updating the Photo 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;
}
…
…

The updated Photo class is ready for use.

Updating CaptureFragment

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.65.247