Android : Around Me (local provider)

android-256This tutorial is about creating the local provider of places for the “Around me” tutorial. If you have not read the first part, you must read it first : Around Me tutorial. The LocalPlaceProvider consists in loading the near points in the database. Sqlite has no geo feature and this is problematic. I have used the following solution described in this great blog : Query by proximity in Android. To resume, we first start a sql query using a sort and a limit. The result may have small errors so we sort it again in Java using a Comparator and the Android API.

Test before
If you wish to test before, you can install the application with the following link. The whole source code of the project is also available on github.

logo-app
Your app idea
Michenux
Free   
pulsante-google-play-store
pulsante-appbrain
qrcode-app

Prerequisite
If you are not familiar with ContentProvider in Android, i suggest you to read the Android Documentation about :

Let’s start coding !

The comparator

The comparator will be used to sort the list of places by the distance property. As i said before, the sql sort may have small errors so we have to sort it again in Java.

First, create an interface DistanceHolder. A distance holder is just a bean having a distance getter. Any bean implementing this interface will be able to be compared using the DistanceComparator.

public interface DistanceHolder {
    public float getDistance();
}

Add the interface on the Place bean :

public class Place implements DistanceHolder {

Finally, the comparator :

public class DistanceComparator implements Comparator<DistanceHolder> {

    @Override
    public int compare(DistanceHolder dh1, DistanceHolder dh2) {
        float diff = dh1.getDistance() - dh2.getDistance();
        return diff > 0 ? 1 : diff < 0 ? -1 : 0 ;
    }
}

The database

Here’s the sql script to create the T_PLACE table and to populate it with some data. This script must be executed at the first run of the application. I will not detail this part but you can have a look at this tutorial : Android Database Sqlite : Handling creation and upgrade

CREATE TABLE T_PLACE (
    _id INTEGER PRIMARY KEY autoincrement,
    NAME TEXT NOT NULL,
    COUNTRY TEXT NOT NULL,
    URL TEXT NOT NULL,
    LONGITUDE REAL NOT NULL,
    LATITUDE REAL NOT NULL );

INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Statue of Liberty','United States','http://lmichenaud.free.fr/yai/Statue%20of%20Liberty.jpg',-74.044444, 40.689167);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Effeil Tower','France','http://lmichenaud.free.fr/yai/Effeil%20Tower.jpg',2.294694, 48.858093);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Great Sphinx','Egypt','http://lmichenaud.free.fr/yai/Great%20Sphinx.jpg',31.137751, 29.975309);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Taj Mahal','India','http://lmichenaud.free.fr/yai/Taj%20Mahal.jpg',78.0420685, 27.1731476);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Colosseum','Italy','http://lmichenaud.free.fr/yai/Colosseum.jpg',12.492314999999962, 41.890268);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Angkor','Cambodge','http://lmichenaud.free.fr/yai/Angkor.jpg',103.86000000000001, 13.4256);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Stonehenge','United Kingdom','http://lmichenaud.free.fr/yai/Stonehenge.jpg',-1.8261171000000331, 51.1788997);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Acropolis of Athens','Greece','http://lmichenaud.free.fr/yai/Acropolis%20of%20Athens.JPG',23.724831900000027, 37.9704259);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Potala Palace','Tibet','http://lmichenaud.free.fr/yai/Potala%20Palace.jpg',91.11630390000005, 29.6528005);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'The Sultan Ahmed Mosque','Turkey','http://lmichenaud.free.fr/yai/The%20Sultan%20Ahmed%20Mosque.jpg',28.976946999999996, 41.005804);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'The Kaaba','Saudi Arabia','http://lmichenaud.free.fr/yai/The%20Kaaba.jpg',29.907371199999943, 31.1846033);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'St. Basil''s Cathedral','Russia','http://lmichenaud.free.fr/yai/St.%20Basil''s%20Cathedral.jpg',37.62316099999998, 55.75250399999999);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Machu Picchu','Peru','http://lmichenaud.free.fr/yai/Machu%20Picchu.jpg',-72.54594329999998, -13.163721);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Big Ben','United Kingdom','http://lmichenaud.free.fr/yai/Big%20Ben.jpg',-0.12452989999997044, 51.5007396);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Tower of Pisa','Italy','http://lmichenaud.free.fr/yai/Tower%20of%20Pisa.jpg',10.396597100000008, 43.7229516);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Christ the Redeemer','Brazil','http://lmichenaud.free.fr/yai/Christ%20the%20Redeemer.jpg',-43.210841500000015, -22.9514737);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Easter Island','Chile','http://lmichenaud.free.fr/yai/Easter%20Island.jpg',-43.210841500000015, -27.1211919);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Capitol Hill','United States','http://lmichenaud.free.fr/yai/Capitol%20Hill.jpg',-77.00174649999997, 38.8844371);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Niagara Falls','Canada','http://lmichenaud.free.fr/yai/Niagara%20Falls.jpg',-79.08610759999999, 43.0903891);
INSERT INTO T_PLACE(_id, NAME, COUNTRY, URL, LONGITUDE, LATITUDE) VALUES (NULL,'Chichen Itza','Mexique','http://lmichenaud.free.fr/yai/Chichen%20Itza.jpg',-88.56861099999998, 20.6795187);

The ContentProvider

I have created a PlaceContentProvider to query the place table. I will not detail this part because it is not the purpose of this tutorial. You can have a look at the class on my github project : PlaceContentProvider.java.

The PlaceLocalProvider class

This is the main interesting class. How it works :

  1. The AroundMeFragment invokes the method onLocationChanged of the PlaceLocalProvider. At this point, we have to query the PlaceContentProvider with the new user location. The best way to do it is to use the CursorLoader of the Android API as the request will be run in a background thread. For this, we have to call the restartLoader method on the LoaderManager. Notice the PlaceLocalProvider class implements LoaderManager.LoaderCallbacks and is passed as a parameter of the method. The roles of the LoaderCallbacks are to create the CursorLoader and be notified when the load is finished.
  2. The method onCreateLoader is invoked by the LoaderManager. In this method, we create the CursorLoader with the right sql query. The sql sort uses the Manhattan distance formula :
    ORDER BY abs(latitude - (?)) + abs( longitude - (?))

    The results are limited to 20. The ? characters is the current user location (latitude and longitude).

  3. The sql query is executed in a background thread by the LoaderManager. When the treatment is finished, the method onLoadFinished is invoked. In this method, we can read the values in the Cursor parameter and create the list of places. For each place, we compute the distance from the current user location. When the list is done, we can sort it using our DistanceComparator. Finally, we can notify the PlaceLoaderCallback, in our case the AroundMeFragment, that a new list of near places is ready.
public class PlaceLocalProvider implements PlaceProvider, LoaderManager.LoaderCallbacks<Cursor> {

    private Location mCurrentLocation;

    private Fragment mFragment ;

    private PlaceLoaderCallback mCallback;

    private DistanceComparator mDistanceComparator = new DistanceComparator();

    public PlaceLocalProvider( Fragment fragment, PlaceLoaderCallback callback ) {
        this.mFragment = fragment ;
        this.mCallback = callback;
    }

    @Override
    public void onLocationChanged(Location location) {
        mCurrentLocation = location ;
        mFragment.getLoaderManager().restartLoader(1, null, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int arg0, Bundle bundle) {

        String[] projection = {PlaceContentProvider.NAME_COLUMN, PlaceContentProvider.COUNTRY_COLUMN,
                PlaceContentProvider.URL_COLUMN, PlaceContentProvider.LONGITUDE_COLUMN, PlaceContentProvider.LATITUDE_COLUMN};

        StringBuilder sort = new StringBuilder("abs(");
        sort.append(PlaceContentProvider.LATITUDE_COLUMN);
        sort.append(" - ");
        sort.append(this.mCurrentLocation.getLatitude());
        sort.append(") + abs( ");
        sort.append(PlaceContentProvider.LONGITUDE_COLUMN);
        sort.append(" - ");
        sort.append(this.mCurrentLocation.getLongitude());
        sort.append(") LIMIT 20 ");
        return new CursorLoader(this.mFragment.getActivity(), PlaceContentProvider.CONTENT_URI, projection, null, null, sort.toString());
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        List<Place> places = new ArrayList<>();
        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
            Place place = new Place();
            place.setName(CursorUtils.getString(PlaceContentProvider.NAME_COLUMN, cursor));
            place.setCountry(CursorUtils.getString(PlaceContentProvider.COUNTRY_COLUMN, cursor));
            place.setImage(CursorUtils.getString(PlaceContentProvider.URL_COLUMN, cursor));
            Location loc = new Location("database");
            loc.setLatitude(CursorUtils.getDouble(PlaceContentProvider.LATITUDE_COLUMN, cursor));
            loc.setLongitude(CursorUtils.getDouble(PlaceContentProvider.LONGITUDE_COLUMN, cursor));
            place.setLocation(loc);
            place.setDistance(loc.distanceTo(this.mCurrentLocation));
            places.add(place);
        }

        Collections.sort(places, this.mDistanceComparator);
        if ( this.mCallback != null ) {
            this.mCallback.onPlaceLoadFinished(places);
        }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> cursor) {
        if ( this.mCallback != null ) {
            this.mCallback.onPlaceLoadFinished(new ArrayList<Place>());
        }
    }

    @Override
    public void onDestroy() {
    }
}

CursorUtils class is available on my github project : CursorUtils.java

AroundMeFragment

Edit the AroundMeFragment and in the onCreateMethod, initialize the mPlaceProvider with the PlaceLocalProvider:

mPlaceProvider = new PlaceLocalProvider(this, this);

That’s all ! In the next “Around me” tutorial, i will explain how to create a PlaceRemoteProvider to get the near points from a remote service.

Share Button

Comments

One Response to “Android : Around Me (local provider)”

  1. Android : Around me Tutorial | Michenux.net on December 21st, 2013 4:46 pm

    […] Android : Around Me (local provider) […]

Leave a Reply