This codelab is part of the Advanced Android Development training course, developed by the Google Developers Training team. You will get the most value out of this course if you work through the codelabs in sequence.

For complete details about the course, see the Advanced Android Development overview.

Introduction

Users are constantly moving around in the world, usually with their phones in their pockets. Advances in GPS and network technologies have made it possible to create Android apps with precise location awareness, using the location services APIs included in Google Play services.

When an app requests the device location, it impacts network and battery consumption, which impacts device performance. To improve the performance of your app, keep the frequency of location requests as low as possible.

In this practical, you learn how to access the device's last known location. You also learn how to convert a set of geographic coordinates (longitude and latitude) into a street address, and how to perform periodic location updates.

What you should already know

You should be familiar with:

What you'll learn

What you'll do

The WalkMyAndroid app prompts the user to change the device location settings, if necessary, to allow the app to access precise location data. The app shows the device location as latitude and longitude coordinates, then shows the location as a physical address. The completed app does periodic location updates and shows an animation that gives the user a visual cue that the app is tracking the device's location.

You create the WalkMyAndroid app in phases:

Obtaining the location information for a device can be complicated: Devices contain different types of GPS hardware, and a satellite or network connection (cell or Wi-Fi) is not always available. Activating GPS and other hardware components uses power. (For more information about GPS and power use, see the chapter on performance.)

To find the device location efficiently without worrying about which provider or network type to use, use the FusedLocationProviderClient interface. Before you can use FusedLocationProviderClient, you need to set up Google Play services. In this task, you download the starter code and include the required dependencies.

1.1 Download the starter app

  1. Download the starter app for this practical, WalkMyAndroid-Starter.
  2. Open the starter app in Android Studio, rename the app to WalkMyAndroid, and run it.
  3. You might need to update your Android SDK Build-Tools. To do this, use the Android SDK Manager.

The UI has a Get Location button, but tapping it doesn't do anything yet.

1.2 Set up Google Play services

Install the Google Repository and update the Android SDK Manager:

  1. Open Android Studio.
  2. Select Tools > Android > SDK Manager.
  3. Select the SDK Tools tab.
  4. Expand Support Repository, select Google Repository, and click OK.

Now you can include Google Play services packages in your app.

To add Google Play services to your project, add the following line of code to the dependencies section in your app-level build.gradle (Module: app) file:

compile 'com.google.android.gms:play-services-location:XX.X.X'

Replace XX.X.X with the latest version number for Google Play services, for example 11.0.2. Android Studio will let you know if you need to update it. For more information and the latest version number, see Add Google Play Services to Your Project.

To learn how to enable an app configuration known as multidex, see Configure Apps with Over 64K Methods.

Now that Google Play services is installed, you're ready to connect to the LocationServices API.

The LocationServices API uses a fused location provider to manage the underlying technology and provides a straightforward API so that you can specify requirements at a high level, like high accuracy or low power. It also optimizes the device's use of battery power. In this step, you will use it to obtain the device's last known location.

2.1 Set location permission in the manifest

Using the Location API requires permission from the user. Android offers two location permissions:

The permission you choose determines the accuracy of the location returned by the API. For this lesson, use the ACCESS_FINE_LOCATION permission, because you want the most accurate location information possible.

  1. Add the following element to your manifest file, above the <application> element:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

2.2 Request permission at runtime

Starting with Android 6.0 (API level 23), it's not always enough to include a permission statement in the manifest. For "dangerous" permissions, you also have to request permission programmatically, at runtime.

To request location permission at runtime:

  1. Create an OnClickListener for the Get Location button in onCreate() in MainActivity.
  2. Create a method stub called getLocation() that takes no arguments and doesn't return anything. Invoke the getLocation() method from the button's onClick() method.
  3. In the getLocation() method, check for the ACCESS_FINE_LOCATION permission.

For information on runtime permissions, see Requesting Permissions at Run Time.

    private void getLocation() {
        if (ActivityCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]
                            {Manifest.permission.ACCESS_FINE_LOCATION},
                    REQUEST_LOCATION_PERMISSION);
        } else {
            Log.d(TAG, "getLocation: permissions granted");
        }
    } 
  1. In your MainActivity class, define an integer constant REQUEST_LOCATION_PERMISSION. This constant is used to identify the permission request when the results come back in the onRequestPemissionsResult() method. It can be any integer greater than 0.
  2. Override the onRequestPermissionsResult() method. If the permission was granted, call getLocation(). Otherwise, show a Toast saying that the permission was denied.
@Override
public void onRequestPermissionsResult(int requestCode,
 @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode) {
        case REQUEST_LOCATION_PERMISSION:
            // If the permission is granted, get the location,
            // otherwise, show a Toast
            if (grantResults.length > 0
                    && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                getLocation();
            } else {
                Toast.makeText(this,
                        R.string.location_permission_denied,
                        Toast.LENGTH_SHORT).show();
            }
            break;
    }
}
  1. Run the app. Clicking the button requests permission from the user. If permission is granted, you see a log statement in the console.

    After you grant permission, subsequent clicks on the Get Location button have no effect. Because you already granted permission, the app doesn't need to ask for permission again (unless the user manually revokes it in the Settings app), even if you close and restart the app.

2.3 Get the last known location

The getLastLocation() method doesn't actually make a location request. It simply returns the location most recently obtained by the FusedLocationProviderClient class.

If no location has been obtained since the device was restarted, the getLastLocation() method may return null. Usually, the getLastLocation() method returns a Location object that contains a timestamp of when this location was obtained.

To get the last known location:

  1. In strings.xml, add a string resource called location_text. Use location_text to display the latitude, longitude, and timestamp of the last known location.
<string name="location_text">"Latitude: %1$.4f \n Longitude: %2$.4f 
\n Timestamp: %3$tr"</string>
  1. In your MainActivity class, create a member variable of the Location type called mLastLocation.
  2. Find the location TextView by ID (textview_location) in onCreate(). Assign the TextView to a member variable called mLocationTextView.
  3. Create a member variable of the FusedLocationProviderClient type called mFusedLocationClient.
  4. Initialize mFusedLocationClient in onCreate() with the following code:
mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this);

The getLastLocation() method returns a Task that results in a Location object (after the Task's onSuccess() callback method is called, signifying that the Task was successful).

Retrieve the latitude and longitude coordinates of a geographic location from the resulting Location object:

  1. Replace the log statement in the getLocation() method with the following code snippet. The code obtains the device's most recent location and assigns it to mLastLocation.
    mFusedLocationClient.getLastLocation().addOnSuccessListener(
           new OnSuccessListener<Location>() {
    @Override
    public void onSuccess(Location location) {
       if (location != null) {
           mLastLocation = location;
           mLocationTextView.setText(
                   getString(R.string.location_text,
                           mLastLocation.getLatitude(),
                           mLastLocation.getLongitude(),
                           mLastLocation.getTime()));
       } else {
           mLocationTextView.setText(R.string.no_location);
       }
    }
  1. Run the app. You now see the latest location that is stored in the fused location provider.

Testing location on an emulator

If you test the WalkMyAndroid app on an emulator, use a system image that supports Google APIs or Google Play:

  1. In Android Studio, create a new virtual device and select hardware for it.
  2. In the System Image dialog, choose an image that says "Google APIs" or "Google Play" in the Target column.

To update the fused location provider on an emulator:

  1. The emulator appears on your screen with a vertical menu to the right of the virtual device. To access emulator options, click the ... icon at the bottom of this vertical menu.
  2. Click Location.
  3. Enter or change the coordinates in the Longitude and Latitude fields.
  4. Click Send to update the fused location provider.

Clicking Send does not affect the location returned by getLastLocation(), because getLastLocation() uses a local cache that the emulator tools do not update.

When you test the app on an emulator, the getLastLocation() method might return null, because the fused location provider doesn't update the location cache after the device is restarted. If getLastLocation() returns null unexpectedly:

  1. Start the Google Maps app and accept the terms and conditions, if you haven't already.
  2. Use the steps above to update the fused location provider. Google Maps will force the local cache to update.
  3. Go back to your app and click the Get Location button. The app updates with the new location.

Later in this lesson, you learn how to force the fused location to update the cache using periodic updates.

Your app displays the device's most recent location, which usually corresponds to its current location, using latitude and longitude coordinates. Although latitude and longitude are useful for calculating distance or displaying a map position, in many cases the address of the location is more useful. For example, if you want to let your users know where they are or what is close by, a street address is more meaningful than the geographic coordinates of the location.

The process of converting from latitude/longitude to a physical address is called reverse geocoding. The getFromLocation() method provided by the Geocoder class accepts a latitude and longitude and returns a list of addresses. The method is synchronous and requires a network connection. It may take a long time to do its work, so do not call it from the main user interface (UI) thread of your app.

In this task, you subclass an AsyncTask to perform reverse geocoding off the main thread. When the AsyncTask completes its process, it updates the UI in the onPostExecute() method.

Using an AsyncTask means that when your main Activity is destroyed when the device orientation changes, you will not longer be able to update the UI. To handle this, you make the location tracking state persistent in Task 4.5.

3.1 Create an AsyncTask subclass

Recall that an AsyncTask object is used to perform work off the main thread. An AsyncTask object contains one required override method, doInBackground() which is where the work is performed. For this use case, you need another override method, onPostExecute(), which is called on the main thread after doInBackground() finishes. In this step, you set up the boilerplate code for your AsyncTask.

AsyncTask objects are defined by three generic types:

To create a subclass of AsyncTask that you can use for reverse geocoding:

  1. Create a new class called FetchAddressTask that is a subclass of AsyncTask. Parameterize the AsyncTask using the three types described above:
private class FetchAddressTask extends AsyncTask<Location, Void, String> {}
  1. In Android Studio, this class declaration is underlined in red, because you have not implemented the required doInBackground() method. Press Alt + Enter (Option + Enter on a Mac) on the highlighted line and select Implement methods. (Or select Code > Implement methods.)

Notice that the method signature for doInBackground() includes a parameter of the Location type, and returns a String; this comes from parameterized types in the class declaration.

  1. Override the onPostExecute() method by going to the menu and selecting Code > Override Methods and selecting onPostExecute(). Again notice that the passed-in parameter is automatically typed as a String, because this what you put in the FetchAddressTask class declaration.
  2. Create a constructor for the AsyncTask that takes a Context as a parameter and assigns it to a member variable.

Your FetchAddressTask now looks something like this:

    private class FetchAddressTask extends AsyncTask<Location, Void, String> {
       private final String TAG = FetchAddressTask.class.getSimpleName();
       private Context mContext;

       FetchAddressTask(Context applicationContext) {
            mContext = applicationContext;
       }

       @Override
       protected String doInBackground(Location... locations) {
           return null;
       }
       @Override
       protected void onPostExecute(String address) {
           super.onPostExecute(address);
       }
    }

3.2 Convert the location into an address string

In this step, you complete the doInBackground() method so that it converts the passed-in Location object into an address string, if possible. If there is a problem, you show an error message.

  1. Create a Geocoder object. This class handles both geocoding (converting from an address into coordinates) and reverse geocoding:
Geocoder geocoder = new Geocoder(mContext,
       Locale.getDefault());
  1. Obtain a Location object. The passed-in parameter is a Java varargs argument that can contain any number of objects. In this case we only pass in one Location object, so the desired object is the first item in the varargs array:
Location location = params[0];
  1. Create an empty List of Address objects, which will be filled with the address obtained from the Geocoder. Create an empty String to hold the final result, which will be either the address or an error:
List<Address> addresses = null;
String resultMessage = "";
  1. You are now ready to start the geocoding process. Open up a try block and use the following code to attempt to obtain a list of addresses from the Location object. The third parameter specifies the maximum number of addresses that you want to read. In this case you only want a single address:
try {
   addresses = geocoder.getFromLocation(
           location.getLatitude(),
           location.getLongitude(),
           // In this sample, get just a single address
           1);
}
  1. Open a catch block to catch IOException exceptions that are thrown if there is a network error or a problem with the Geocoder service. In this catch block, set the resultMessage to an error message that says "Service not available." Log the error and result message:
catch (IOException ioException) {
   // Catch network or other I/O problems
   resultMessage = mContext
           .getString(R.string.service_not_available);
   Log.e(TAG, resultMessage, ioException);
}
  1. Open another catch block to catch IllegalArgumentException exceptions. Set the resultMessage to a string that says "Invalid coordinates were supplied to the Geocoder," and log the error and result message:
catch (IllegalArgumentException illegalArgumentException) {
   // Catch invalid latitude or longitude values
   resultMessage = mContext
           .getString(R.string.invalid_lat_long_used);
   Log.e(TAG, resultMessage + ". " +
           "Latitude = " + location.getLatitude() +
           ", Longitude = " +
           location.getLongitude(), illegalArgumentException);
}
  1. You need to catch the case where Geocoder is not able to find the address for the given coordinates. In the try block, check the address list and the resultMessage string. If the address list is empty or null and the resultMessage string is empty, then set the resultMessage to "No address found" and log the error:
if (addresses == null || addresses.size() == 0) {
   if (resultMessage.isEmpty()) {
       resultMessage = mContext
               .getString(R.string.no_address_found);
       Log.e(TAG, resultMessage);
   }
}
  1. If the address list is not empty or null, the reverse geocode was successful.

The next step is to read the first address into a string, line by line:

  1. Create an empty ArrayList of Strings.
  2. Iterate over the List of Address objects and read them into the new ArrayList line by line.
  3. Use the TextUtils.join() method to convert the list into a string. Use the \n character to separate each line with the new-line character:

Here is the code:

    else {
       // If an address is found, read it into resultMessage
       Address address = addresses.get(0);
       ArrayList<String> addressParts = new ArrayList<>();

       // Fetch the address lines using getAddressLine,
       // join them, and send them to the thread
       for (int i = 0; i <= address.getMaxAddressLineIndex(); i++) {
           addressParts.add(address.getAddressLine(i));
       }

       resultMessage = TextUtils.join("\n", addressParts);
    }
  1. At the bottom of doInBackground() method, return the resultMessage object.

3.3 Display the result of the FetchAddressTask object

When doInBackground() completes, the resultMessage string is automatically passed into the onPostExecute() method. In this step you update the member variables of MainActivity with new values and display the new data in the TextView using a passed in interface.

  1. Create a new string resource with two replacement variables.
<string name="address_text">"Address: %1$s \n Timestamp: %2$tr"</string>
  1. Create an interface in FetchAddressTask called OnTaskCompleted that has one method, called onTaskCompleted(). This method should take a string as an argument:
interface OnTaskCompleted {
        void onTaskCompleted(String result);
}
  1. Add a parameter for the OnTaskCompleted interface to the FetchAddressTask constructor, and assign it to a member variable:
private OnTaskCompleted mListener;

FetchAddressTask(Context applicationContext, OnTaskCompleted listener) {
    mContext = applicationContext;
    mListener = listener;
}
  1. In the onPostExecute() method, call onTaskCompleted() on the mListener interface, passing in the result string:
@Override
protected void onPostExecute(String address) {
   mListener.onTaskCompleted(address);
   super.onPostExecute(address);
}
  1. Back in the MainActivity, update the activity to implement the FetchAddressTask.OnTaskCompleted interface you created and override the required onTaskCompleted() method.
  2. In this method, updated the TextView with the resulting address and the current time:
@Override
public void onTaskCompleted(String result) {
   // Update the UI
   mLocationTextView.setText(getString(R.string.address_text,
            result, System.currentTimeMillis()));
}
  1. In the getLocation() method, inside the onSuccess() callback, replace the lines that assigns the passed-in location to mLastLocation and sets the TextView with the following line of code. This code creates a new FetchAddressTask and executes it, passing in the Location object. You can also remove the now unused mLastLocation member variable.
 // Start the reverse geocode AsyncTask
new FetchAddressTask(MainActivity.this, 
         MainActivity.this).execute(location);
  1. At the end of the getLocation() method, show loading text while the FetchAddressTask runs:
mLocationTextView.setText(getString(R.string.address_text,
       getString(R.string.loading),
       System.currentTimeMillis()));
  1. Run the app. After briefly loading, the app displays the location address in the TextView.

Up until now, you've used the FusedLocationProviderClient.getLastLocation() method, which relies on other apps having already made location requests. In this task, you learn how to:

4.1 Set up the UI and method stubs

If your app relies heavily on device location, using the getLastLocation() method may not be sufficient, because getLastLocation() relies on a location request from a different app and only returns the last value stored in the provider.

To make location requests in your app, you need to:

The user has no way of knowing that the app is making location requests, except for a tiny icon in the status bar. In this step, you use an animation (included in the starter code) to add a more obvious visual cue that the device's location is being tracked. You also change the button text to show the user whether location tracking is on or off.

To indicate location tracking to the user:

  1. In MainActivity, declare the member variables mAndroidImageView (of type ImageView) and mRotateAnim (of type AnimatorSet).
  2. In the onCreate() method, find the Android ImageView by ID and assign it to mAndroidImageView. Then find the animation included in the starter code by ID and assign it to mRotateAnim. Finally set the Android ImageView as the target for the animation:
mAndroidImageView = (ImageView) findViewById(R.id.imageview_android);

mRotateAnim = (AnimatorSet) AnimatorInflater.loadAnimator
   (this, R.animator.rotate);

mRotateAnim.setTarget(mAndroidImageView);
  1. In the strings.xml file, change the button text to "Start Tracking Location." Do this for for both the portrait and the landscape layouts.
  2. In the strings.xml file, change the TextView text to "Press the button to start tracking your location."
  3. Refactor and rename the getLocation() method to startTrackingLocation().
  4. Create a private method stub called stopTrackingLocation() that takes no arguments and returns void.
  5. Create a boolean member variable called mTrackingLocation. Boolean primitives default to false, so you do not need to initialize mTrackingLocation.
  6. Change the onClick() method for the button's onClickListener:
    @Override
    public void onClick(View v) {
       if (!mTrackingLocation) {
           startTrackingLocation();
       } else {
           stopTrackingLocation();
       }
    }
  1. At the end of the startTrackingLocation() method, start the animation by calling mRotateAnim.start(). Set mTrackingLocation to to true and change the button text to "Stop Tracking Location".
  2. In the stopTrackingLocation() method, check if the you are tracking the location. If you are, stop the animation by calling mRotateAnim.end(), set mTrackingLocation to to false, change the button text back to "Start Tracking Location" and reset the location TextView to show the original hint.
/**
* Method that stops tracking the device. It removes the location
* updates, stops the animation and reset the UI.
*/
private void stopTrackingLocation() {
    if (mTrackingLocation) {
            mTrackingLocation = false;
            mLocationButton.setText(R.string.start_tracking_location);
            mLocationTextView.setText(R.string.textview_hint);
            mRotateAnim.end();
        }
}

4.2 Create the LocationRequest object

The LocationRequest object contains setter methods that determine the frequency and accuracy of location updates. For now, we're only interested in the following parameters:

To create the LocationRequest object:

  1. Create a method called getLocationRequest() that takes no arguments and returns a LocationRequest.
  2. Set the interval, fastest interval, and priority parameters.
private LocationRequest getLocationRequest() {
   LocationRequest locationRequest = new LocationRequest();
   locationRequest.setInterval(10000);
   locationRequest.setFastestInterval(5000);
   locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
   return locationRequest; 
}

4.3 Create the LocationCallback object

When your app requests a location update, the fused location provider invokes the LocationCallback.onLocationResult() callback method. The incoming argument contains a list of Location objects containing the location's latitude and longitude.

To create a LocationCallback object:

  1. At the bottom of onCreate(), create a new LocationCallback object and assign it to a member variable called mLocationCallback.
  2. Override the onLocationResult() method.
mLocationCallback = new LocationCallback() {
    @Override
   public void onLocationResult(LocationResult locationResult) {
   }
};

4.4 Request location updates

You now have the required LocationRequest and LocationCallback objects to request periodic location updates. When your app receives the LocationResultobjects in onLocationResult(), use the FetchAddressTask to reverse geocode the Location object into an address:

  1. To request periodic location updates, replace the call to getLastLocation() in startTrackingLocation() (along with the OnSuccessListener) with the following method call. Pass in the LocationRequest and LocationCallback:
mFusedLocationClient.requestLocationUpdates
       (getLocationRequest(), mLocationCallback,
               null /* Looper */);
  1. In the stopTrackingLocation() method, call removeLocationUpdates() on mFusedLocationClient. Pass in the LocationCallback object.
mFusedLocationClient.removeLocationUpdates(mLocationCallback);
  1. In the onLocationResult() callback, check mTrackingLocation. If mTrackingLocation is true, execute FetchAddressTask(), and use the LocationResult.getLastLocation() method to obtain the most recent Location object.
@Override
public void onLocationResult(LocationResult locationResult) {
   // If tracking is turned on, reverse geocode into an address
   if (mTrackingLocation) {
       new FetchAddressTask(MainActivity.this, MainActivity.this)
                            .execute(locationResult.getLastLocation());
}
  1. In onTaskComplete(), where the UI is updated, wrap the code in an if statement that checks the mTrackingLocation boolean. If the user turns off the location updates while the AsyncTask is running, the results are not displayed to the TextView.
  2. Run the app. Your app tracks your location, updating the location approximately every ten seconds.

Right now, the app continues to request location updates until the user clicks the button, or until the Activity is destroyed. To conserve power, stop location updates when your Activity is not in focus (in the paused state) and resume location updates when the Activity regains focus:

  1. Override the Activity object's onResume() and onPause() methods.
  2. In onResume(), check mTrackingLocation. If mTrackingLocation is true, call startTrackingLocation().
  3. In onPause(), check mTrackingLocation. If mTrackingLocation is true, call stopTrackingLocation() but set mTrackingLocation to true so the app continues tracking the location when it resumes.
  4. Run the app and turn on location tracking. Exiting the app stops the location updates when the activity is not visible.

4.5 Make the tracking state persistent

If you run the app and rotate the device, the app resets to its initial state. The mTrackingLocation boolean is not persistent across configuration changes, and it defaults to false when the Activity is recreated. This means the UI defaults to the initial state.

In this step, you use the saved instance state to make mTrackingLocation persistent so that the app continues to track location when there is a configuration change.

  1. Override the Activity object's onSaveInstanceState() method.
  2. Create a string constant called TRACKING_LOCATION_KEY. You use this constant as a key for the mTrackingLocation boolean.
  3. In onSaveInstanceState(), save the state of the mTrackingLocation boolean by using the putBoolean() method:
@Override
protected void onSaveInstanceState(Bundle outState) {
   outState.putBoolean(TRACKING_LOCATION_KEY, mTrackingLocation);
   super.onSaveInstanceState(outState);
}
  1. In onCreate(), restore the mTrackingLocation variable before you create the LocationCallback instance (because the code checks for the mTrackingLocation boolean before starting the FetchAddressTask):
if (savedInstanceState != null) {
   mTrackingLocation = savedInstanceState.getBoolean(
           TRACKING_LOCATION_KEY);
}
  1. Run the app and start location tracking. Rotate the device. A new FetchAddressTask is triggered, and the device continues to track the location.

WalkMyAndroid-Solution

Challenge: Extend the location TextView to include the distance traveled from the first location obtained. (See the distanceTo() method.)

The related concept documentation is in 7.1: Location services.

Android developer documentation:

This section lists possible homework assignments for students who are working through this codelab as part of a course led by an instructor. It's up to the instructor to do the following:

Instructors can use these suggestions as little or as much as they want, and should feel free to assign any other homework they feel is appropriate.

If you're working through this codelab on your own, feel free to use these homework assignments to test your knowledge.

Build and run an app

In the WalkMyAndroid app, add a second TextView to the app that shows the following:

  1. Accuracy in meters.
  2. Speed in meters per second.

Hint: See the getSpeed() and getAccuracy() documentation.

Answer these questions

Question 1

Which API do you use to request the last known location on the device?

Question 2

Which class do you use for handling geocoding and reverse geocoding?

Question 3

Which method do you use for periodic location updates ?

Submit your app for grading

Guidance for graders

Check that the app has the following features:

To see all the codelabs in the Advanced Android Development training course, visit the Advanced Android Development codelabs landing page.