7.1: Using the device location

Contents:

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:

  • Creating, building, and running apps in Android Studio.
  • The Activity lifecycle.
  • Making data persistent across configuration changes.
  • Requesting permissions at runtime.
  • Using an AsyncTask to do background work.

What you will LEARN

You will learn how to:

  • Get the last known location of the device.
  • Obtain a physical address from a set of coordinates (reverse geocoding).
  • Perform periodic location updates.

What you will DO

  • Create the WalkMyAndroid app and have it display the device's last known location as latitude and longitude coordinates.
  • Reverse geocode the latitude and longitude coordinates into a physical address.
  • Extend the app to receive periodic location updates.

App overview

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:

  • In tasks 1 and 2, you implement a button that gets the most recent location for the device and displays the coordinates and timestamp of the location in a TextView.
  • In task 3, you turn the coordinates into a physical address, through a process called reverse geocoding .
  • In task 4, you learn how to trigger periodic updates of the location.

     First step of the WalkMyAndroid app  Second step of the WalkMyAndroid app  Final step of the WalkMyAndroid app

Task 1. Set up location services

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.

Note: If your app references more than 65K methods, the app may fail to compile. To mitigate this problem, compile your app using only the Google Play services APIs that your app uses. To learn how to selectively compile APIs into your executable, see Set Up Google Play Services in the developer documentation. 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.

Task 2. Get the last known location

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.

Dangerous permissions cover areas where the app wants data or resources that involve the user's private information, or could potentially affect the user's stored data or the operation of other apps.

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.

    • If the permission has not been granted, request it.
    • If the permission has been granted, display a message in the logs. (The code below shows a TAG variable, which you declare later, in Task 3.1.)

    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");
         }
     }
    
  4. 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.
  5. 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;
         }
     }
    
  6. 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>
    
    Note: If you aren't familiar with string replacement and formatting, see Formatting and Styling and the Formatter documentation.
  2. In your MainActivity class, create a member variable of the Location type called mLastLocation.

  3. Find the location TextView by ID (textview_location) in onCreate(). Assign the TextView to a member variable called mLocationTextView.
  4. Create a member variable of the FusedLocationProviderClient type called mFusedLocationClient.
  5. 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:

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

    • If the returned location is not null, set the TextView to show the coordinates and time stamp of the Location object.
    • If the location returned is null, the fused location provider has not obtained a location since the device was restarted. Display a message in the TextView that says that the location is not available.
      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);
        }
      }
      
  7. 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.

Task 3. Get the location as an address

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:

  • Use the Params type to pass parameters into the doInBackground() method. For this app, the passed-in parameter is the Location object.
  • Use the Progress type to mark progress in the onProgressUpdate() method. For this app, you are not interested in the Progress type, because reverse geocoding is typically quick.
  • Use the Results type to publish results in the onPostExecute() method. For this app, the published result is the returned address String.

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> {}
    
  2. 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.

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

  4. 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());
    
  2. 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];
    
  3. 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 = "";
    
  4. 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);
     }
    
  5. 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);
     }
    
  6. 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);
     }
    
  7. 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);
        }
     }
    
  8. 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:

    • Create an empty ArrayList of Strings.
    • Iterate over the List of Address objects and read them into the new ArrayList line by line.
    • 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);
     }
    
  9. 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>
    
  2. 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);
     }
    
  3. 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;
     }
    
  4. 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);
     }
    
  5. Back in the MainActivity, update the activity to implement the FetchAddressTask.OnTaskCompleted interface you created and override the required onTaskCompleted() method.
  6. 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()));
     }
    
  7. 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);
    
  8. 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()));
    
  9. Run the app. After briefly loading, the app displays the location address in the TextView.

Task 4. Receive location updates

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:

  • Track the device location using periodic location requests.
  • Make the tracking state persistent.
  • Show an animation to give a visual cue that the device location is being tracked.
  • Check the device location settings. You need to know whether location services are turned on, and whether they are set to the accuracy that your app needs.

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:

  • Create a LocationRequest object that contains the requirements for your location requests. The requirements include update frequency, accuracy, and so on. You do this step in 4.2 Create the LocationRequest object, below.
  • Create a LocationCallback object and override its onLocationResult() method. The onLocationResult() method is where your app receives location updates. You do this step in 4.3 Create the LocationCallback object.
  • Call requestLocationUpdates() on the FusedLocationProviderClient. Pass in the LocationRequest and the LocationCallback. You do this step in 4.4 Request location updates.

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);
    
  3. In the strings.xml file:

    • Change the button text to "Start Tracking Location." Do this for for both the portrait and the landscape layouts.
    • Change the TextView text to "Press the button to start tracking your location."
  4. Refactor and rename the getLocation() method to startTrackingLocation().

  5. Create a private method stub called stopTrackingLocation() that takes no arguments and returns void.
  6. Create a boolean member variable called mTrackingLocation. Boolean primitives default to false, so you do not need to initialize mTrackingLocation.
  7. Change the onClick() method for the button's onClickListener:

    • If mTrackingLocation is false, call startTrackingLocation().
    • If mTrackingLocation is true, call stopTrackingLocation().
      @Override
      public void onClick(View v) {
        if (!mTrackingLocation) {
            startTrackingLocation();
        } else {
            stopTrackingLocation();
        }
      }
      
  8. 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".
  9. 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:

  • Interval: The setInterval() method defines the desired update interval in milliseconds. For this app, use 10 seconds (10000 milliseconds).
  • Fastest interval: The fused location provider attempts to make location requests more efficient by batching requests from different apps. This means that you may receive updates faster than what you set in setInterval(), which can cause problems if your UI is not ready for updates. To limit the rate of location updates, use the setFastestInterval() method. In this app, use 5 seconds (5000 milliseconds)
  • Priority: Use this parameter with one of the priority constants to specify a balance between power consumption and accuracy. (Greater accuracy requires greater power consumption.) For this app, use the PRIORITY_HIGH_ACCURACY constant to prioritize accuracy.

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 */);
    
  2. In the stopTrackingLocation() method, call removeLocationUpdates() on mFusedLocationClient. Pass in the LocationCallback object.
     mFusedLocationClient.removeLocationUpdates(mLocationCallback);
    
  3. 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());
     }
    
  4. 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.
  5. Run the app. Your app tracks your location, updating the location approximately every ten seconds.
Testing the location-update functionality on an emulator can be tough: the UI will say "Loading" until you send a new location, and seeing the timing of the set interval is impossible. You can use a GPX file to simulate different locations over time. For testing, you can use the places_gps_data.gpx GPX file, which contains several locations:
  1. Download the places_gps_data.gpx file.
  2. Open your emulator, click the ... icon at the bottom of this vertical settings menu, and select the Location tab.
  3. Click Load GPX/KML and select the downloaded file.
  4. Change the duration of each item to 10 seconds, and click the play button. If you start tracking when the GPX file is playing, you see a changing address displayed in the UI.

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);
     }
    
  4. 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);
     }
    
  5. Run the app and start location tracking. Rotate the device. A new FetchAddressTask is triggered, and the device continues to track the location.

Solution code

WalkMyAndroid-Solution

Coding challenge

Note: All coding challenges are optional.

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

Summary

  • Location information is available through the FusedLocationProviderClient.
  • Using location services requires location permissions.
  • Location permissions are categorized as "dangerous permissions," so you must include them in the manifest and request them at runtime.
  • Use the getLastLocation() method to obtain the device's last known location from the FusedLocationProviderClient.
  • The process of converting a set of coordinates (longitude and latitude) into a physical address is called reverse geocoding . Reverse geocoding is available through the Geocoder class' getFromLocation() method.
  • The getFromLocation() method is synchronous and may take a while to complete, so you should not use it on the main thread.
  • Use a LocationRequest to specify the accuracy and frequency requirements of your location updates.
  • Provided that the device settings are appropriate, use the FusedLocationProviderClient to request periodic location updates with requestLocationUpdates().
  • Stop the location updates with the FusedLocationProviderClient removeLocationUpdates() method.

The related concept documentation is in 7.1 C: Location services.

Learn more

Android developer documentation:

results matching ""

    No results matching ""