Click here to Skip to main content
15,885,278 members
Articles / Mobile Apps / Android
Article

Implementing Map and Geofence Features in Android* Business Apps

22 Jul 2014CPOL6 min read 21.7K   14  
This case study discusses building map and geolocation features into an Android* business app, which includes overlaying store locations on Google Maps*, using geofence to notify the user when the device enters the store proximities.

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

Abstract

This case study discusses building map and geolocation features into an Android* business app, which includes overlaying store locations on Google Maps*, using geofence to notify the user when the device enters the store proximities.

Table of Contents

  1. Abstract
  2. Overview
  3. Displaying Store Locations on Google Maps
    1. Google Maps Android API v2
    2. Specifying App Settings in the Application Manifest
    3. Adding a Map Fragment
  4. Sending Geofence Notifications
    1. Registering and Unregistering Geofences
    2. Implementing the Location Service Callbacks
    3. Implementing the Intent Service
  5. Summary
  6. References
  7. About the Authors

Overview

In this case study, we will incorporate maps and geolocation functionality into a restaurant business app for Android tablets (Figure 1). The user can access the geolocation functionality from the main menu item "Locations and Geofences" (Figure 2).

Image 1

Figure 1 The Restaurant App Main Screen

Image 2

Figure 2 The Flyout Menu Items

Displaying Store Locations on Google Maps

For a business app, showing the store locations on maps is very graphical and helpful to the user (Figure 3). The Google Maps Android API provides an easy way to incorporate Google Maps into your Android apps.

Google Maps Android API v2

Google Maps Android API v2 is part of the Google Play Services APK. To create an Android app which uses the Google Maps Android API v2 requires setting up the development environment by downloading and configuring the Google Play services SDK, obtaining an API key, and adding the required settings in your app’s AndroidManifest.xml file.

First you need to set up Google Play Services SDK by following http://developer.android.com/google/play-services/setup.html.

Then you register your project and obtain an API key from Google Developers Console https://console.developers.google.com/project. You will need to add the API key in your AndroidManifest.xml file.

Image 3

Figure 3 The Restaurant App Shows Store Locations on Google Maps.

Specifying App Settings in the Application Manifest

To use Google Maps Android API v2, some permissions and features are required to be specified as children of the <manifest> element (Code Example 1). They include the necessary permissions for network connection, external storage, and access to the location. Also OpenGL ES version 2 feature is needed for the Google Maps Android API.

XML
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
<!-- The following two permissions are not required to use
     Google Maps Android API v2, but are recommended. -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION" />

  <uses-feature
    android:glEsVersion="0x00020000"
    android:required="true"/>
Code Example 1. Recommended permissions to specify for an app which uses Google Maps Android API. Include the "ACCESS_MOCK_LOCATION" permission only if you test your app with mock locations **

Also as children of the <application> element, we specify the Google Play Services version and the API key we obtained in <meta-data> elements (Code Example 2).

XML
<meta-data
        android:name="com.google.android.gms.version"
        android:value="@integer/google_play_services_version" />

<meta-data
      android:name="com.google.android.maps.v2.API_KEY"
    android:value="copy your API Key here"/>
Code Example 2. Specify Google Play Services Version and API Key **

Adding a Map Fragment

First in your activity layout xml file, add a MapFragment element (Code Example 3).

XML
<fragment
    android:id="@+id/storelocationmap"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:name="com.google.android.gms.maps.MapFragment"
/>
Code Example 3. Add a MapFragment in the Activity Layout **

In your activity class, you can retrieve the Google Maps MapFragment object and draw the store icons at each store location.

Java
private static final LatLng CHANDLER = new LatLng(33.455,-112.0668);
…
private static final StoreLocation[] ALLRESTURANTLOCATIONS = new StoreLocation[] {
        new StoreLocation(new LatLng(33.455,-112.0668), new String("Phoenix, AZ")),
        new StoreLocation(new LatLng(33.5123,-111.9336), new String("SCOTTSDALE, AZ")),
        new StoreLocation(new LatLng(33.3333,-111.8335), new String("Chandler, AZ")),
        new StoreLocation(new LatLng(33.4296,-111.9436), new String("Tempe, AZ")),
        new StoreLocation(new LatLng(33.4152,-111.8315), new String("Mesa, AZ")),
        new StoreLocation(new LatLng(33.3525,-111.7896), new String("Gilbert, AZ"))
};
…    
      @Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.geolocation_view);
		
		mMap = ((MapFragment) getFragmentManager().findFragmentById(R.id.storelocationmap)).getMap();
		mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(CHANDLER, ZOOM_LEVEL));
		Drawable iconDrawable = getResources().getDrawable(R.drawable.ic_launcher);
		Bitmap iconBmp = ((BitmapDrawable) iconDrawable).getBitmap();
		for(int ix = 0; ix < ALLRESTURANTLOCATIONS.length; ix++) {
			mMap.addMarker(new MarkerOptions()
			    .position(ALLRESTURANTLOCATIONS[ix].mLatLng)
		        .icon(BitmapDescriptorFactory.fromBitmap(iconBmp)));
		}
…
Code Example 4. Draw the Store Icons on Google Maps **

Sending Geofence Notifications

A geofence is a circular area defined by the latitude and longitude coordinates of a point and a radius. An Android app can register geofences with the Android Location Services. The Android app can also specify an expiration duration for a geofence. Whenever a geofence transition happens, for example, when the Android device enters or exits a registered geofence, the Android Location Services will inform the Android app.

In our Restaurant app, we can define a geofence for each store location. When the device enters the proximity of the store, the app sends a notification for example, "You have entered the proximity of your favorite restaurant!" (Figure 4).

Image 4

Figure 4 A geofence is defined as a circular area by a point of interest and a radius

Registering and Unregistering Geofences

In Android SDK, Location Services is also part of Google Play services APK under the "Extras" directory.

To request geofence monitoring, first we need to specify the "ACCESS_FINE_LOCATION" permission in the app’s manifest, which we have already done in the previous sections.

We also need to check the availability of the Google Play Services (the checkGooglePlayServices() method in Code Example 5). After the locationClient().connect() call and the location client has successfully established a connection, the Location Services will call the onConnected(Bundle bundle) function, where the location client can request adding or removing geofences.

Java
public class GeolocationActivity extends Activity implements
        GooglePlayServicesClient.ConnectionCallbacks
…
{
…	
    private LocationClient mLocationClient;
    
…

    static class StoreLocation {
        public LatLng mLatLng;
        public String mId;
        StoreLocation(LatLng latlng, String id) {
            mLatLng = latlng;
            mId = id;
        }
    }

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

        mLocationClient = new LocationClient(this, this, this);

        // Create a new broadcast receiver to receive updates from the listeners and service
        mGeofenceBroadcastReceiver = new ResturantGeofenceReceiver();

        // Create an intent filter for the broadcast receiver
        mIntentFilter = new IntentFilter();

        // Action for broadcast Intents that report successful addition of geofences
        mIntentFilter.addAction(ACTION_GEOFENCES_ADDED);

        // Action for broadcast Intents that report successful removal of geofences
        mIntentFilter.addAction(ACTION_GEOFENCES_REMOVED);

        // Action for broadcast Intents containing various types of geofencing errors
        mIntentFilter.addAction(ACTION_GEOFENCE_ERROR);

        // All Location Services sample apps use this category
        mIntentFilter.addCategory(CATEGORY_LOCATION_SERVICES);

		createGeofences();

		mRegisterGeofenceButton = (Button)findViewById(R.id.geofence_switch);
		mGeofenceState = CAN_START_GEOFENCE;
    
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        // Register the broadcast receiver to receive status updates
        LocalBroadcastManager.getInstance(this).registerReceiver(
            mGeofenceBroadcastReceiver, mIntentFilter);
    }
        
    /**
     * Create a Geofence list
     */
    public void createGeofences() {
        for(int ix=0; ix > ALLRESTURANTLOCATIONS.length; ix++) {
            Geofence fence = new Geofence.Builder()
                .setRequestId(ALLRESTURANTLOCATIONS[ix].mId)
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER)
                .setCircularRegion(
                    ALLRESTURANTLOCATIONS[ix].mLatLng.latitude, ALLRESTURANTLOCATIONS[ix].mLatLng.longitude, GEOFENCERADIUS)
                .setExpirationDuration(Geofence.NEVER_EXPIRE)
                .build();
            mGeofenceList.add(fence);
        }
    }

    // callback function when the mRegisterGeofenceButton is clicked
    public void onRegisterGeofenceButtonClick(View view) {
        if (mGeofenceState == CAN_REGISTER_GEOFENCE) {
            registerGeofences();
            mGeofenceState = GEOFENCE_REGISTERED;
            mGeofenceButton.setText(R.string.unregister_geofence);
            mGeofenceButton.setClickable(true);            
        else {
            unregisterGeofences();
            mGeofenceButton.setText(R.string.register_geofence);
            mGeofenceButton.setClickable(true);
            mGeofenceState = CAN_REGISTER_GEOFENCE;
        }
    }

    private boolean checkGooglePlayServices() {
        int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
        if (result == ConnectionResult.SUCCESS) {
            return true;
        } 
        else {
            Dialog errDialog = GooglePlayServicesUtil.getErrorDialog(
                    result,
                    this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);

            if (errorDialog != null) {
                errorDialog.show();
            }
        }
        return false;
   }


    public void registerGeofences() {
	
        if (!checkGooglePlayServices()) {

            return;
        }
        mRequestType = REQUEST_TYPE.ADD;

        try {
            // Try to add geofences
            requestConnectToLocationClient();
        } catch (UnsupportedOperationException e) {
            // handle the exception
        }
        
    }

    public void unregisterGeofences() {

        if (!checkGooglePlayServices()) {
            return;
        }

        // Record the type of removal
    	  mRequestType = REQUEST_TYPE.REMOVE;

        // Try to make a removal request
        try {
            mCurrentIntent = getRequestPendingIntent());
            requestConnectToLocationClient();

        } catch (UnsupportedOperationException e) {
            // handle the exception
        }
    }

    public void requestConnectToLocationServices () throws UnsupportedOperationException {
        // If a request is not already in progress
        if (!mRequestInProgress) {
            mRequestInProgress = true;

            locationClient().connect();
        } 
        else {
            // Throw an exception and stop the request
            throw new UnsupportedOperationException();
        }
    }


    /**
     * Get a location client and disconnect from Location Services
     */
    private void requestDisconnectToLocationServices() {

        // A request is no longer in progress
        mRequestInProgress = false;

        locationClient().disconnect();
        
        if (mRequestType == REQUEST_TYPE.REMOVE) {
            mCurrentIntent.cancel();
        }

    }

    /**
     * returns A LocationClient object
     */
    private GooglePlayServicesClient locationClient() {
        if (mLocationClient == null) {

            mLocationClient = new LocationClient(this, this, this);
        }
        return mLocationClient;

}

    /*
     Called back from the Location Services when the request to connect the client finishes successfully. At this point, you can
request the current location or start periodic updates
     */
    @Override
    public void onConnected(Bundle bundle) {
        if (mRequestType == REQUEST_TYPE.ADD) {
        // Create a PendingIntent for Location Services to send when a geofence transition occurs
        mGeofencePendingIntent = createRequestPendingIntent();

        // Send a request to add the current geofences
        mLocationClient.addGeofences(mGeofenceList, mGeofencePendingIntent, this);

        } 
        else if (mRequestType == REQUEST_TYPE.REMOVE){

            mLocationClient.removeGeofences(mCurrentIntent, this);        
        } 
}
…
}
Code Example 5. Request Geofence Monitoring by Location Services **

Implementing the Location Service Callbacks

The location service requests are usually non-blocking or asynchronous calls. Actually in Code Example 5 in the previous section, we have already implemented one of these functions: after the locationClient().connect() call and the location client establishes a connection, the Location Services will call the onConnected(Bundle bundle) function. Code Example 6 lists other location callback functions we need to implement.

Java
public class GeolocationActivity extends Activity implements
        OnAddGeofencesResultListener,
        OnRemoveGeofencesResultListener,
        GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener {
…	


    @Override
    public void onDisconnected() {
        mRequestInProgress = false;
        mLocationClient = null;
}

    

    /*
     * Handle the result of adding the geofences
     */
    @Override
    public void onAddGeofencesResult(int statusCode, String[] geofenceRequestIds) {

        // Create a broadcast Intent that notifies other components of success or failure
        Intent broadcastIntent = new Intent();

        // Temp storage for messages
        String msg;

        // If adding the geocodes was successful
        if (LocationStatusCodes.SUCCESS == statusCode) {

            // Create a message containing all the geofence IDs added.
            msg = getString(R.string.add_geofences_result_success,
                    Arrays.toString(geofenceRequestIds));

            // Create an Intent to broadcast to the app
            broadcastIntent.setAction(ACTION_GEOFENCES_ADDED)
                           .addCategory(CATEGORY_LOCATION_SERVICES)
                           .putExtra(EXTRA_GEOFENCE_STATUS, msg);
        // If adding the geofences failed
        } else {
            msg = getString(
                    R.string.add_geofences_result_failure,
                    statusCode,
                    Arrays.toString(geofenceRequestIds)
            );
            broadcastIntent.setAction(ACTION_GEOFENCE_ERROR)
                           .addCategory(CATEGORY_LOCATION_SERVICES)
                           .putExtra(EXTRA_GEOFENCE_STATUS, msg);
        }

        LocalBroadcastManager.getInstance(this)
            .sendBroadcast(broadcastIntent);

        // request to disconnect the location client
        requestDisconnectToLocationServices();
    }

    /*
     * Implementation of OnConnectionFailedListener.onConnectionFailed
     * If a connection or disconnection request fails, report the error
     * connectionResult is passed in from Location Services
     */
    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        mInProgress = false;
        if (connectionResult.hasResolution()) {

            try {
                connectionResult.startResolutionForResult(this,
                    CONNECTION_FAILURE_RESOLUTION_REQUEST);
            } 
            catch (SendIntentException e) {
                // log the error
            }
        } 
        else {
            Intent errorBroadcastIntent = new Intent(ACTION_CONNECTION_ERROR);
            errorBroadcastIntent.addCategory(CATEGORY_LOCATION_SERVICES)
                     .putExtra(EXTRA_CONNECTION_ERROR_CODE,
                                 connectionResult.getErrorCode());
             LocalBroadcastManager.getInstance(this)
                 .sendBroadcast(errorBroadcastIntent);
        }
    }
    
    @Override
    public void onRemoveGeofencesByPendingIntentResult(int statusCode,
            PendingIntent requestIntent) {

        // Create a broadcast Intent that notifies other components of success or failure
        Intent broadcastIntent = new Intent();

        // If removing the geofences was successful
        if (statusCode == LocationStatusCodes.SUCCESS) {

            // Set the action and add the result message
            broadcastIntent.setAction(ACTION_GEOFENCES_REMOVED);
            broadcastIntent.putExtra(EXTRA_GEOFENCE_STATUS,
                    getString(R.string.remove_geofences_intent_success));

        } 
        else {
            // removing the geocodes failed


            // Set the action and add the result message
            broadcastIntent.setAction(ACTION_GEOFENCE_ERROR);
            broadcastIntent.putExtra(EXTRA_GEOFENCE_STATUS,
                    getString(R.string.remove_geofences_intent_failure, 
                        statusCode));
        }
        LocalBroadcastManager.getInstance(this)
                .sendBroadcast(broadcastIntent);

        // request to disconnect the location client
        requestDisconnectToLocationServices();
    }

        
    public class ResturantGeofenceReceiver extends BroadcastReceiver {
  

	  @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            // Intent contains information about errors in adding or removing geofences
            if (TextUtils.equals(action, ACTION_GEOFENCE_ERROR)) {
                // handleGeofenceError(context, intent);
            } 
            else if (TextUtils.equals(action, ACTION_GEOFENCES_ADDED)
                    ||
                    TextUtils.equals(action, ACTION_GEOFENCES_REMOVED)) {
                // handleGeofenceStatus(context, intent);
            } 
            else if (TextUtils.equals(action, ACTION_GEOFENCE_TRANSITION)) {
                // handleGeofenceTransition(context, intent);
            } 
            else {
                // handle error
            }
			
        }
    }


    public PendingIntent getRequestPendingIntent() {
        return createRequestPendingIntent();
    }

    private PendingIntent createRequestPendingIntent() {

        if (mGeofencePendingIntent != null) {

            // Return the existing intent
            return mGeofencePendingIntent;

        // If no PendingIntent exists
        } else {

            // Create an Intent pointing to the IntentService
            Intent intent = new Intent(this,
                ReceiveGeofenceTransitionIntentService.class);

            return PendingIntent.getService(
                    this,
                    0,
                    intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }
    }


    @Override
public void onRemoveGeofencesByRequestIdsResult(int statusCode, 
    String[] geofenceRequestIds) {

        // it should not come here because we only remove geofences by PendingIntent
        // Disconnect the location client
        requestDisconnection();
    }
Code Example 6. IImplement the Location Service Callbacks **

Implementing the Intent Service

Finally, we need to implement the IntentService class to handle the geofence transitions (Code Example 7).

Java
public class ReceiveGeofenceTransitionIntentService extends IntentService {
    /**
     * Sets an identifier for the service
     */
    public ReceiveGeofenceTransitionIntentService() {
        super("ReceiveGeofenceTransitionsIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
    	
        // Create a local broadcast Intent
        Intent broadcastIntent = new Intent();

        // Give it the category for all intents sent by the Intent Service
        broadcastIntent.addCategory(CATEGORY_LOCATION_SERVICES);

   	
        // First check for errors
        if (LocationClient.hasError(intent)) {
            // Get the error code with a static method
            int errorCode = LocationClient.getErrorCode(intent);
        } 
        else {
            // Get the type of transition (entry or exit)
            int transition =
                    LocationClient.getGeofenceTransition(intent);
            
            if ((transition == Geofence.GEOFENCE_TRANSITION_ENTER)  ||
                    (transition == Geofence.GEOFENCE_TRANSITION_EXIT)) {

                // Post a notification
            } 
            else {
                // handle the error
            }
        }
    }
}
Code Example 7. Implement an IntentService Class to Handle Geofence Transitions

Summary

In this article, we have discussed how to incorporate mapping and geofencing features into an Android business app. These features support rich geolocation contents and strong location based services and use cases in your apps.

References

About the Author

Miao Wei is a software engineer in the Intel Software and Services Group. He currently works on the Intel® Atom™ processor scale enabling projects.

*Other names and brands may be claimed as the property of others.
**This sample source code is released under the Intel Sample Source Code License Agreement

Optimization Notice

Intel's compilers may or may not optimize to the same degree for non-Intel microprocessors for optimizations that are not unique to Intel microprocessors. These optimizations include SSE2, SSE3, and SSE3 instruction sets and other optimizations. Intel does not guarantee the availability, functionality, or effectiveness of any optimization on microprocessors not manufactured by Intel.

Microprocessor-dependent optimizations in this product are intended for use with Intel microprocessors. Certain optimizations not specific to Intel microarchitecture are reserved for Intel microprocessors. Please refer to the applicable product User and Reference Guides for more information regarding the specific instruction sets covered by this notice.

Other Related Articles and Resources

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Intel is inside more and more Android devices, and we have tools and resources to make your app development faster and easier.


Comments and Discussions