1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.example.android.location;
18 
19 import android.annotation.SuppressLint;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.Intent;
25 import android.location.Address;
26 import android.location.Geocoder;
27 import android.location.Location;
28 import android.location.LocationListener;
29 import android.location.LocationManager;
30 import android.os.AsyncTask;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.Message;
35 import android.provider.Settings;
36 import android.support.v4.app.DialogFragment;
37 import android.support.v4.app.FragmentActivity;
38 import android.view.View;
39 import android.widget.Button;
40 import android.widget.TextView;
41 import android.widget.Toast;
42 
43 import java.io.IOException;
44 import java.util.List;
45 import java.util.Locale;
46 
47 public class LocationActivity extends FragmentActivity {
48     private TextView mLatLng;
49     private TextView mAddress;
50     private Button mFineProviderButton;
51     private Button mBothProviderButton;
52     private LocationManager mLocationManager;
53     private Handler mHandler;
54     private boolean mGeocoderAvailable;
55     private boolean mUseFine;
56     private boolean mUseBoth;
57 
58     // Keys for maintaining UI states after rotation.
59     private static final String KEY_FINE = "use_fine";
60     private static final String KEY_BOTH = "use_both";
61     // UI handler codes.
62     private static final int UPDATE_ADDRESS = 1;
63     private static final int UPDATE_LATLNG = 2;
64 
65     private static final int TEN_SECONDS = 10000;
66     private static final int TEN_METERS = 10;
67     private static final int TWO_MINUTES = 1000 * 60 * 2;
68 
69     /**
70      * This sample demonstrates how to incorporate location based services in your app and
71      * process location updates.  The app also shows how to convert lat/long coordinates to
72      * human-readable addresses.
73      */
74     @SuppressLint("NewApi")
75     @Override
onCreate(Bundle savedInstanceState)76     public void onCreate(Bundle savedInstanceState) {
77         super.onCreate(savedInstanceState);
78         setContentView(R.layout.main);
79 
80         // Restore apps state (if exists) after rotation.
81         if (savedInstanceState != null) {
82             mUseFine = savedInstanceState.getBoolean(KEY_FINE);
83             mUseBoth = savedInstanceState.getBoolean(KEY_BOTH);
84         } else {
85             mUseFine = false;
86             mUseBoth = false;
87         }
88         mLatLng = (TextView) findViewById(R.id.latlng);
89         mAddress = (TextView) findViewById(R.id.address);
90         // Receive location updates from the fine location provider (gps) only.
91         mFineProviderButton = (Button) findViewById(R.id.provider_fine);
92         // Receive location updates from both the fine (gps) and coarse (network) location
93         // providers.
94         mBothProviderButton = (Button) findViewById(R.id.provider_both);
95 
96         // The isPresent() helper method is only available on Gingerbread or above.
97         mGeocoderAvailable =
98                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD && Geocoder.isPresent();
99 
100         // Handler for updating text fields on the UI like the lat/long and address.
101         mHandler = new Handler() {
102             public void handleMessage(Message msg) {
103                 switch (msg.what) {
104                     case UPDATE_ADDRESS:
105                         mAddress.setText((String) msg.obj);
106                         break;
107                     case UPDATE_LATLNG:
108                         mLatLng.setText((String) msg.obj);
109                         break;
110                 }
111             }
112         };
113         // Get a reference to the LocationManager object.
114         mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
115     }
116 
117     // Restores UI states after rotation.
118     @Override
onSaveInstanceState(Bundle outState)119     protected void onSaveInstanceState(Bundle outState) {
120         super.onSaveInstanceState(outState);
121         outState.putBoolean(KEY_FINE, mUseFine);
122         outState.putBoolean(KEY_BOTH, mUseBoth);
123     }
124 
125     @Override
onResume()126     protected void onResume() {
127         super.onResume();
128         setup();
129     }
130 
131     @Override
onStart()132     protected void onStart() {
133         super.onStart();
134 
135         // Check if the GPS setting is currently enabled on the device.
136         // This verification should be done during onStart() because the system calls this method
137         // when the user returns to the activity, which ensures the desired location provider is
138         // enabled each time the activity resumes from the stopped state.
139         LocationManager locationManager =
140                 (LocationManager) getSystemService(Context.LOCATION_SERVICE);
141         final boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
142 
143         if (!gpsEnabled) {
144             // Build an alert dialog here that requests that the user enable
145             // the location services, then when the user clicks the "OK" button,
146             // call enableLocationSettings()
147             new EnableGpsDialogFragment().show(getSupportFragmentManager(), "enableGpsDialog");
148         }
149     }
150 
151     // Method to launch Settings
enableLocationSettings()152     private void enableLocationSettings() {
153         Intent settingsIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
154         startActivity(settingsIntent);
155     }
156 
157     // Stop receiving location updates whenever the Activity becomes invisible.
158     @Override
onStop()159     protected void onStop() {
160         super.onStop();
161         mLocationManager.removeUpdates(listener);
162     }
163 
164     // Set up fine and/or coarse location providers depending on whether the fine provider or
165     // both providers button is pressed.
setup()166     private void setup() {
167         Location gpsLocation = null;
168         Location networkLocation = null;
169         mLocationManager.removeUpdates(listener);
170         mLatLng.setText(R.string.unknown);
171         mAddress.setText(R.string.unknown);
172         // Get fine location updates only.
173         if (mUseFine) {
174             mFineProviderButton.setBackgroundResource(R.drawable.button_active);
175             mBothProviderButton.setBackgroundResource(R.drawable.button_inactive);
176             // Request updates from just the fine (gps) provider.
177             gpsLocation = requestUpdatesFromProvider(
178                     LocationManager.GPS_PROVIDER, R.string.not_support_gps);
179             // Update the UI immediately if a location is obtained.
180             if (gpsLocation != null) updateUILocation(gpsLocation);
181         } else if (mUseBoth) {
182             // Get coarse and fine location updates.
183             mFineProviderButton.setBackgroundResource(R.drawable.button_inactive);
184             mBothProviderButton.setBackgroundResource(R.drawable.button_active);
185             // Request updates from both fine (gps) and coarse (network) providers.
186             gpsLocation = requestUpdatesFromProvider(
187                     LocationManager.GPS_PROVIDER, R.string.not_support_gps);
188             networkLocation = requestUpdatesFromProvider(
189                     LocationManager.NETWORK_PROVIDER, R.string.not_support_network);
190 
191             // If both providers return last known locations, compare the two and use the better
192             // one to update the UI.  If only one provider returns a location, use it.
193             if (gpsLocation != null && networkLocation != null) {
194                 updateUILocation(getBetterLocation(gpsLocation, networkLocation));
195             } else if (gpsLocation != null) {
196                 updateUILocation(gpsLocation);
197             } else if (networkLocation != null) {
198                 updateUILocation(networkLocation);
199             }
200         }
201     }
202 
203     /**
204      * Method to register location updates with a desired location provider.  If the requested
205      * provider is not available on the device, the app displays a Toast with a message referenced
206      * by a resource id.
207      *
208      * @param provider Name of the requested provider.
209      * @param errorResId Resource id for the string message to be displayed if the provider does
210      *                   not exist on the device.
211      * @return A previously returned {@link android.location.Location} from the requested provider,
212      *         if exists.
213      */
requestUpdatesFromProvider(final String provider, final int errorResId)214     private Location requestUpdatesFromProvider(final String provider, final int errorResId) {
215         Location location = null;
216         if (mLocationManager.isProviderEnabled(provider)) {
217             mLocationManager.requestLocationUpdates(provider, TEN_SECONDS, TEN_METERS, listener);
218             location = mLocationManager.getLastKnownLocation(provider);
219         } else {
220             Toast.makeText(this, errorResId, Toast.LENGTH_LONG).show();
221         }
222         return location;
223     }
224 
225     // Callback method for the "fine provider" button.
useFineProvider(View v)226     public void useFineProvider(View v) {
227         mUseFine = true;
228         mUseBoth = false;
229         setup();
230     }
231 
232     // Callback method for the "both providers" button.
useCoarseFineProviders(View v)233     public void useCoarseFineProviders(View v) {
234         mUseFine = false;
235         mUseBoth = true;
236         setup();
237     }
238 
doReverseGeocoding(Location location)239     private void doReverseGeocoding(Location location) {
240         // Since the geocoding API is synchronous and may take a while.  You don't want to lock
241         // up the UI thread.  Invoking reverse geocoding in an AsyncTask.
242         (new ReverseGeocodingTask(this)).execute(new Location[] {location});
243     }
244 
updateUILocation(Location location)245     private void updateUILocation(Location location) {
246         // We're sending the update to a handler which then updates the UI with the new
247         // location.
248         Message.obtain(mHandler,
249                 UPDATE_LATLNG,
250                 location.getLatitude() + ", " + location.getLongitude()).sendToTarget();
251 
252         // Bypass reverse-geocoding only if the Geocoder service is available on the device.
253         if (mGeocoderAvailable) doReverseGeocoding(location);
254     }
255 
256     private final LocationListener listener = new LocationListener() {
257 
258         @Override
259         public void onLocationChanged(Location location) {
260             // A new location update is received.  Do something useful with it.  Update the UI with
261             // the location update.
262             updateUILocation(location);
263         }
264 
265         @Override
266         public void onProviderDisabled(String provider) {
267         }
268 
269         @Override
270         public void onProviderEnabled(String provider) {
271         }
272 
273         @Override
274         public void onStatusChanged(String provider, int status, Bundle extras) {
275         }
276     };
277 
278     /** Determines whether one Location reading is better than the current Location fix.
279       * Code taken from
280       * http://developer.android.com/guide/topics/location/obtaining-user-location.html
281       *
282       * @param newLocation  The new Location that you want to evaluate
283       * @param currentBestLocation  The current Location fix, to which you want to compare the new
284       *        one
285       * @return The better Location object based on recency and accuracy.
286       */
getBetterLocation(Location newLocation, Location currentBestLocation)287     protected Location getBetterLocation(Location newLocation, Location currentBestLocation) {
288         if (currentBestLocation == null) {
289             // A new location is always better than no location
290             return newLocation;
291         }
292 
293         // Check whether the new location fix is newer or older
294         long timeDelta = newLocation.getTime() - currentBestLocation.getTime();
295         boolean isSignificantlyNewer = timeDelta > TWO_MINUTES;
296         boolean isSignificantlyOlder = timeDelta < -TWO_MINUTES;
297         boolean isNewer = timeDelta > 0;
298 
299         // If it's been more than two minutes since the current location, use the new location
300         // because the user has likely moved.
301         if (isSignificantlyNewer) {
302             return newLocation;
303         // If the new location is more than two minutes older, it must be worse
304         } else if (isSignificantlyOlder) {
305             return currentBestLocation;
306         }
307 
308         // Check whether the new location fix is more or less accurate
309         int accuracyDelta = (int) (newLocation.getAccuracy() - currentBestLocation.getAccuracy());
310         boolean isLessAccurate = accuracyDelta > 0;
311         boolean isMoreAccurate = accuracyDelta < 0;
312         boolean isSignificantlyLessAccurate = accuracyDelta > 200;
313 
314         // Check if the old and new location are from the same provider
315         boolean isFromSameProvider = isSameProvider(newLocation.getProvider(),
316                 currentBestLocation.getProvider());
317 
318         // Determine location quality using a combination of timeliness and accuracy
319         if (isMoreAccurate) {
320             return newLocation;
321         } else if (isNewer && !isLessAccurate) {
322             return newLocation;
323         } else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
324             return newLocation;
325         }
326         return currentBestLocation;
327     }
328 
329     /** Checks whether two providers are the same */
isSameProvider(String provider1, String provider2)330     private boolean isSameProvider(String provider1, String provider2) {
331         if (provider1 == null) {
332           return provider2 == null;
333         }
334         return provider1.equals(provider2);
335     }
336 
337     // AsyncTask encapsulating the reverse-geocoding API.  Since the geocoder API is blocked,
338     // we do not want to invoke it from the UI thread.
339     private class ReverseGeocodingTask extends AsyncTask<Location, Void, Void> {
340         Context mContext;
341 
ReverseGeocodingTask(Context context)342         public ReverseGeocodingTask(Context context) {
343             super();
344             mContext = context;
345         }
346 
347         @Override
doInBackground(Location... params)348         protected Void doInBackground(Location... params) {
349             Geocoder geocoder = new Geocoder(mContext, Locale.getDefault());
350 
351             Location loc = params[0];
352             List<Address> addresses = null;
353             try {
354                 addresses = geocoder.getFromLocation(loc.getLatitude(), loc.getLongitude(), 1);
355             } catch (IOException e) {
356                 e.printStackTrace();
357                 // Update address field with the exception.
358                 Message.obtain(mHandler, UPDATE_ADDRESS, e.toString()).sendToTarget();
359             }
360             if (addresses != null && addresses.size() > 0) {
361                 Address address = addresses.get(0);
362                 // Format the first line of address (if available), city, and country name.
363                 String addressText = String.format("%s, %s, %s",
364                         address.getMaxAddressLineIndex() > 0 ? address.getAddressLine(0) : "",
365                         address.getLocality(),
366                         address.getCountryName());
367                 // Update address field on UI.
368                 Message.obtain(mHandler, UPDATE_ADDRESS, addressText).sendToTarget();
369             }
370             return null;
371         }
372     }
373 
374     /**
375      * Dialog to prompt users to enable GPS on the device.
376      */
377     private class EnableGpsDialogFragment extends DialogFragment {
378 
379         @Override
onCreateDialog(Bundle savedInstanceState)380         public Dialog onCreateDialog(Bundle savedInstanceState) {
381             return new AlertDialog.Builder(getActivity())
382                     .setTitle(R.string.enable_gps)
383                     .setMessage(R.string.enable_gps_dialog)
384                     .setPositiveButton(R.string.enable_gps, new DialogInterface.OnClickListener() {
385                         @Override
386                         public void onClick(DialogInterface dialog, int which) {
387                             enableLocationSettings();
388                         }
389                     })
390                     .create();
391         }
392     }
393 }
394