Location-Based Services on Android Wear - Basic Building Blocks - Android Wearables (2015)

Android Wearables (2015)

Part II. Basic Building Blocks

Chapter 8. Location-Based Services on Android Wear

WHAT’S IN THIS CHAPTER?

· ➤ Introduction to the new location services

· ➤ Accessing your location

· ➤ Showing your street address

· ➤ Detecting activity patterns

WROX.COM CODE DOWNLOADS FOR THIS CHAPTER

The wrox.com code downloads for this chapter are found at www.wrox.com/go/androidwearables on the Download Code tab. The code is in the Chapter 8 download, and the files are named according to the listing numbers noted throughout the chapter.

CHANGING HOW LOCATION WORKS

With the release of the updated Android Wear system in late October 2014, using a global positioning system (GPS) in your Wear apps became technically possible. The Sony SmartWatch 3, the first device that sported the required hardware, was released in late November.

Because the Wear devices are often connected to a master device, the phone, there is always the possibility of accessing the phone’s sensor data. This possibility has forced Google to review how the GPS libraries work on Wear. Previously you would ask the Android system (android.location) for the most suitable location provider according to your specific requirements. In Wear you instead use the newer Google Services location API (gms.location) to access the location services.

Apart from handling location updates in Android Wear, the Google Play location API gives you a handful of other interesting features, including detecting your user’s physical activity, such as walking or running. It also has helper classes for interacting with geographic areas (geofences).

Accessing the Current Location

To access anything location-based in Android Wear, you need to use the FusedLocationProvider class, which selects the most appropriate GPS provider on your device(s). This new API is much simpler in terms of readable code. It’s also more power-efficient, because it considers other apps’ location update requests.

Another feature of FusedLocationProvider is the combination of multiple GPS sensors. Because your Wear device may or may not have a GPS sensor built in, the system needs a way to figure out which sensor is the best option for your needs. In most cases when the Wear device is connected to your phone, it chooses to read updates directly from the phone instead, thereby saving battery on the Wear device. If the phone and Wear device are not connected, the system requests updates directly from the built-in GPS sensor.

Enabling GPS support

Start by requesting permission to use the location services. There are two levels available: ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION. If you want to use the GPS radio you’ll need to add the ACCESS_FINE_LOCATION location. Open the manifest and add the lines shown in Listing 8-1.

LISTING 8-1: Requesting GPS permission

<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.wiley.wrox.myapplication" >

<uses-feature android:name="android.hardware.type.watch" />

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

...

</manifest>

Using the New APIs

Before you can access FusedLocationProvider, you need to establish a connection using GoogleApiClient. Connect to the Wear activity in your onCreate method using the code shown in Listing 8-2.

When you’re working with location through Google Services, the same rules apply as with any other API. Use ConnectionCallbacks to get information about your connection attempt status, and handle failed connection attempts in OnConnectionFailedListener.

LISTING 8-2: Creating GoogleApiClient

package com.wiley.wrox.gpsproject;

import android.app.Activity;

import android.os.Bundle;

import android.support.wearable.view.WatchViewStub;

import android.widget.TextView;

import com.google.android.gms.common.api.GoogleApiClient;

import com.google.android.gms.location.LocationServices;

public class WearActivity extends Activity {

private TextView mTextView;

private GoogleApiClient mApiClient;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_wear);

final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);

stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {

@Override

public void onLayoutInflated(WatchViewStub stub) {

mTextView = (TextView) stub.findViewById(R.id.text);

}

});

mApiClient = new GoogleApiClient.Builder(this)

.addApi(LocationServices.API)

.addConnectionCallbacks(mConnectionListener)

.addOnConnectionFailedListener(mConnectionFailedListener)

.build();

}

@Override

protected void onResume() {

super.onResume();

}

@Override

protected void onPause() {

super.onPause();

}

}

Add the connection callbacks, and then request a connection to Google Services from within the onResume life-cycle method, as shown in Listing 8-3. Don’t forget to also add the disconnect call in onPause.

LISTING 8-3: Adding the callbacks and connecting

package com.wiley.wrox.gpsproject;

import android.app.Activity;

import android.location.Location;

import android.os.Bundle;

import android.support.wearable.view.WatchViewStub;

import android.widget.TextView;

import com.google.android.gms.common.ConnectionResult;

import com.google.android.gms.common.api.GoogleApiClient;

import com.google.android.gms.location.LocationServices;

public class WearActivity extends Activity {

private TextView mTextView;

private GoogleApiClient mApiClient;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_wear);

final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);

stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {

@Override

public void onLayoutInflated(WatchViewStub stub) {

mTextView = (TextView) stub.findViewById(R.id.text);

}

});

mApiClient = new GoogleApiClient.Builder(this)

.addApi(LocationServices.API)

.addApi(Wearable.API)

.addConnectionCallbacks(mConnectionListener)

.addOnConnectionFailedListener(mConnectionFailedListener)

.build();

}

@Override

protected void onResume() {

super.onResume();

mApiClient.connect();

}

@Override

protected void onPause() {

super.onPause();

mApiClient.disconnect();

}

private GoogleApiClient.ConnectionCallbacks mConnectionListener = new

GoogleApiClient.ConnectionCallbacks() {

@Override

public void onConnected(Bundle bundle) {

}

@Override

public void onConnectionSuspended(int i) {

}

};

private GoogleApiClient.OnConnectionFailedListener mConnectionFailedListener =

new GoogleApiClient.OnConnectionFailedListener() {

@Override

public void onConnectionFailed(ConnectionResult connectionResult) {

}

};

}

Determining GPS Availability

Although connecting to the Google Services Client is no problem, suppose the Wear device has no built-in GPS. Listing 8-4 shows how to check for support for GPS.

LISTING 8-4: Detecting if a device has a GPS sensor

private boolean hasGpsSupport(){

return getPackageManager().hasSystemFeature(PackageManager

.FEATURE_LOCATION_GPS);

}

This is, of course, also possible by using the uses-feature element in your application manifest, effectively limiting what devices can install your application.

Requesting the Last Known Location

To request the last known location, you use the FusedLocationApi class. You can find it within LocationServices, as shown in Listing 8-5. Make sure to call this only when the device has connected to Google Services.

LISTING 8-5: Getting the last known location

private GoogleApiClient.ConnectionCallbacks mConnectionListener = new

GoogleApiClient.ConnectionCallbacks() {

@Override

public void onConnected(Bundle bundle) {

Location loc = LocationServices.FusedLocationApi.getLastLocation

(mApiClient);

if (loc != null) {

mTextView.setText(loc.getLatitude() + ", " + loc.getLongitude());

}

}

@Override

public void onConnectionSuspended(int i) {

}

};

This particular call may throw a NullPointerException and cause the app to crash sometimes. It all depends on the current state of the GPS. This is because LocationService still has not received an updated location. There are two ways to deal with this problem. Either you wait with the request for the latest update, or you register a LocationListener for a single location update. The second option may be a bit more secure, because it never delivers a null value. However, it also may never deliver a value at all. In that case you should set an expiration for your request (as described in Table 8.1).

Table 8.1 Location Request Options

OPTION

DESCRIPTION

Priority

Describes the general urgency of your location updates. You can choose from PRIORITY_HIGH_ACCURACY, PRIORITY_BALANCED_POWER_ACCURACY, PRIORITY_LOW_POWER, and PRIORITY_NO_POWER. HIGH is the most accurate, BALANCED is “block”-level accuracy, LOW POWERis “city”-level accuracy, and NO POWER is as good as possible without consuming any power.

Expiration

Sets the expiration of the location. There are two methods for setting the expiration. setExpirationDuration(long) sets the amount of time the request lives, and setExpirationTime(long) sets the exact expiration time since the device booted.

Interval

Sets the desired rate (in milliseconds) for location updates. You should consider this a wish, not a promise. Sometimes you may not receive any updates.

Fastest interval

Sets the limit of how fast your app receives location updates. Unlike the normal interval, this value is exact, meaning your app can receive updates faster than the GPS provides them. This can be handy when you want to receive many updates while still conserving power.

Number of updates

Defines the number of location updates you want your app to receive. When using this setting, you should also be sure to set the expiration time of your request. If you don’t, your request might live forever, consuming valuable battery power.

Smallest displacement

Sets the minimum distance between location updates in meters. The default value is 0.

Requesting Location Updates

Sometimes it’s not enough to have just one location update. You may need more frequent updates on the location. In those cases you use LocationListener. Listing 8-6 shows how to attach a listener and request periodic updates.

LISTING 8-6: Requesting periodic location updates

package com.wiley.wrox.gpsproject;

import android.app.Activity;

import android.content.pm.PackageManager;

import android.location.Location;

import android.os.Bundle;

import android.support.wearable.view.WatchViewStub;

import android.widget.TextView;

import com.google.android.gms.common.ConnectionResult;

import com.google.android.gms.common.api.GoogleApiClient;

import com.google.android.gms.location.LocationListener;

import com.google.android.gms.location.LocationRequest;

import com.google.android.gms.location.LocationServices;

public class WearActivity extends Activity {

private TextView mTextView;

private GoogleApiClient mApiClient;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_wear);

final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);

stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {

@Override

public void onLayoutInflated(WatchViewStub stub) {

mTextView = (TextView) stub.findViewById(R.id.text);

}

});

mApiClient = new GoogleApiClient.Builder(this)

.addApi(LocationServices.API)

.addConnectionCallbacks(mConnectionListener)

.addOnConnectionFailedListener(mConnectionFailedListener)

.build();

}

@Override

protected void onResume() {

super.onResume();

mApiClient.connect();

}

@Override

protected void onPause() {

super.onPause();

LocationServices.FusedLocationApi.removeLocationUpdates(mApiClient,

mLocationListener);

mApiClient.disconnect();

}

private GoogleApiClient.ConnectionCallbacks mConnectionListener = new

GoogleApiClient.ConnectionCallbacks() {

@Override

public void onConnected(Bundle bundle) {

LocationRequest request = LocationRequest.create();

LocationServices.FusedLocationApi.requestLocationUpdates(mApiClient, request,

mLocationListener);

}

@Override

public void onConnectionSuspended(int i) {

}

};

private GoogleApiClient.OnConnectionFailedListener mConnectionFailedListener =

new GoogleApiClient.OnConnectionFailedListener() {

@Override

public void onConnectionFailed(ConnectionResult connectionResult) {

}

};

private LocationListener mLocationListener = new LocationListener() {

@Override

public void onLocationChanged(Location location) {

mTextView.setText(location.getLatitude() + ", " + location.getLongitude());

}

};

}

When either the request for the latest location or the request for periodic location updates returns a location, your app should look something like Figure 8.1.

images

FIGURE 8.1 Displaying GPS coordinates on the device

Being Picky About Location Updates

Requesting a default location update works just fine, but in most cases you may want to be pickier about how you request your updates. If you request updates too seldom, your app may not be as functional as you want. If you request too often, the battery life may be significantly reduced. Neither option is good.

To make more sense of your location updates, you can set multiple options, as shown in Table 8.1.

These options are set through an equal number of setter methods. Each setter returns a new LocationRequest object, meaning you can chain these values as shown in Listing 8-7.

LISTING 8-7: Setting location request options

private GoogleApiClient.ConnectionCallbacks mConnectionListener = new

GoogleApiClient.ConnectionCallbacks() {

@Override

public void onConnected(Bundle bundle) {

LocationRequest request = LocationRequest.create()

.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY)

.setExpirationDuration(2000)

.setFastestInterval(500)

.setInterval(2000)

.setNumUpdates(2)

.setSmallestDisplacement(0.5f);

LocationServices.FusedLocationApi.requestLocationUpdates(mApiClient,

request, mLocationListener);

}

@Override

public void onConnectionSuspended(int i) {

}

};

This request has balanced (“block”-level) accuracy and power consumption. It expires after 2 seconds and delivers location updates twice per second. It delivers a maximum of two location updates, and the user must move at least half a meter before a new location update is delivered.

Showing Your Street Address

While latitude and longitude (and any other values the location may give you) are very handy, in some cases they’re just not good enough. Sometimes you want your locations to be in a human-readable format. That’s when you need to apply reverse geocoding to your location.

Luckily a service called Geocoder exists for this exact purpose. However, it’s not available on all devices, so you must be sure to check if this service is available before you attempt to use it.

Testing Geocoder Availability

Use the helper method isPresent() as shown in Listing 8-8 to test if Geocoder is present.

LISTING 8-8: Testing if Geocoder is present

private LocationListener mLocationListener = new LocationListener() {

@Override

public void onLocationChanged(Location location) {

if(Geocoder.isPresent()){

}

}

};

Getting the Current Address for a Location

Before you can translate the location to an address, you need to create the Geocoder instance. However, because the Geocoder service is running synchronously, you should wrap this in a thread. Listing 8-9 shows you how.

LISTING 8-9: Creating a Geocoder instance

private LocationListener mLocationListener = new LocationListener() {

@Override

public void onLocationChanged(Location location) {

if (Geocoder.isPresent()) {

new AsyncTask<Double, Void, Address>() {

@Override

protected Address doInBackground(Double...doubles) {

Geocoder coder = new Geocoder(WearActivity.this, Locale.getDefault());

return null;

}

}.execute(location.getLatitude(), location.getLongitude());

}

}

};

The Geocoder object returns a list of possible addresses for the location you look up. Create a list of Address objects, and call the method getFromLocation(double, double, int), passing the latitude and longitude and the number of results you’re interested in. We’ll accept only a single result this time, so pass a 1 (see Listing 8-10).

LISTING 8-10: Getting the most accurate address

private LocationListener mLocationListener = new LocationListener() {

@Override

public void onLocationChanged(Location location) {

if (Geocoder.isPresent()) {

new AsyncTask<Double, Void, Address>() {

@Override

protected Address doInBackground(Double...doubles) {

Geocoder coder = new Geocoder(WearActivity.this, Locale.getDefault());

List<Address> addressList = null;

try {

addressList = coder.getFromLocation(doubles[0], doubles[1], 1);

if (addressList != null && addressList.size() > 0) {

return addressList.get(0);

}

} catch (IOException e) {

e.printStackTrace();

}

return null;

}

}.execute(location.getLatitude(), location.getLongitude());

}

}

};

Since we’re doing the reverse geocoding outside of the UI thread, we need to be sure to post the address when we’re back on the UI thread. Luckily AsyncTask has a method just for this called onPostExecute().

If Geocoder is not available on your Wear device, you should use the data APIs to have the phone look up the address.

SUMMARY

In this chapter you explored the “new” Google location services. In particular, you tested them on the new Android Wear platform.

You read the last known location and started reading recurring location updates with LocationListener. You finished the chapter by looking at reverse geocoding and activity recognition. All in all, you covered most of the location services available in Android.

This is the final chapter in Part II of the book. Part III contains a few interesting projects that dive a bit deeper into real-life contexts for wearable apps.

RECOMMENDED READING

1. More in-depth information about how the GPS works, http://en.wikipedia.org/wiki/Global_Positioning_System

2. Android Location APIs can be found at https://developer.android.com/google/play-services/location.html