One of the most interesting aspects for mobile development is interacting with device capabilities such as motion sensors, cameras, and location sensors. While these capabilities are new and fun to many developers, they can also bring a great deal of value to the users of our mobile apps. In this chapter, we will walk through adding location awareness to POIApp
including the following topics:
While working with location services on the Android platform, you will primarily work with an instance of LocationManager
. The process is fairly straightforward as follows:
LocationManager
.LocationManager
to request location change notifications, either ongoing or a single notification.OnLocationChange()
callbacks.Android devices generally provide two different means for determining a location: GPS and Network. When requesting location change notifications, you must specify the provider you wish to receive updates from. The Android platform defines a set of string constants for the following providers:
Provider Name |
Description |
---|---|
|
This provider determines a location using satellites. Depending on conditions, this provider may take a while to return a location fix. This requires the |
|
This provider determines a location based on the availability of a cell tower and Wi-Fi access points. Its results are retrieved by means of a network lookup. |
|
This provider can be used to passively receive location updates when other applications or services request them without actually having to request for the locations yourself. It requires the |
You will notice specific permissions in the provider descriptions that must be set on an app to be used.
App permissions are specified in the AndroidManifest.xml
file. To set the appropriate permissions, perform the following steps:
Properties/AndroidManifest.xml
in the Solution pad. The file will be opened in the manifest editor. There are two tabs at the bottom of the screen, Application and Source, which can be used to toggle between viewing a form for editing the file or the raw XML as follows:Source
View to view the XML as follows:To use an emulator for development, this chapter will require the emulator to be configured with Google APIs so that the address lookup and navigation to map app works.
To install and configure Google APIs, perform the following steps:
The LocationManager
class is a system service that provides access to the location and bearing of a device, if the device supports these services. You do not explicitly create an instance of LocationManager
; instead, you request an instance from a Context
object using the GetSystemService()
method. In most cases, the Context
object is a subtype of Activity
. The following code depicts declaring a reference of a LocationManager
class and requesting an instance:
LocationManager _locMgr; . . . _locMgr = GetSystemService (Context.LocationService) as LocationManager;
The LocationManager
class provides a series of overloaded methods that can be used to request location update notifications. If you simply need a single update, you can call RequestSingleUpdate()
; to receive ongoing updates, call RequestLocationUpdate()
.
Prior to requesting location updates, you must identify the location provider that should be used. In our case, we simply want to use the most accurate provider available at the time. This can be accomplished by specifying the criteria for the desired provider using an instance of Android.Location.Criteria
. The following code example shows how to specify the minimum criteria:
Criteria criteria = new Criteria(); criteria.Accuracy = Accuracy.NoRequirement; criteria.PowerRequirement = Power.NoRequirement;
Now that we have the criteria, we are ready to request updates as follows:
_locMgr.RequestSingleUpdate (criteria, this, null);
You will notice that the second parameter to RequestSingleUpdate()
must be an object that implements ILocationListener
, which defines the following methods:
void OnLocationChanged (Location location);
void OnProviderDisabled (string provider);
void OnProviderEnabled (string provider);
void OnStatusChanged (string provider, Availability status, Bundle extras);
For the most part, we will create blank stubs for all of the methods except OnLocationChanged()
. While writing more sophisticated applications, it will be useful to provide implementations for some of the other methods. For example, you might call RequestLocationUpdate()
to begin receiving updates and then receive a notification via OnProviderEnabled()
that a preferred provider is now available, in which case you would want to stop updates and start them again using the preferred provider.
In POIApp, we have the following two different scenarios for requesting location updates:
POIListActivity
class, we need to calculate the distance to each listed POI. In this scenario, we want to request location change notifications on an ongoing basis and use the most current location to calculate the distance.POIDetailActivity
, we would like to request the current location when adding a new POI. In this scenario, we will want to request a single location change notification.Now that we have some idea of how to add location services to an app, let's add location services to POIListActivity
as follows:
LocationManager
and obtain a reference in OnCreate()
as follows:LocationManager _locMgr; . . . protected override void OnCreate (Bundle bundle) { base.OnCreate (bundle); SetContentView (Resource.Layout.POIList); _locMgr = GetSystemService (Context.LocationService) as LocationManager; . . .
OnResume()
, obtain the best location provider and call RequestLocationUpdates()
to start receiving updates as follows:protected override void OnResume () { base.OnResume (); _adapter.NotifyDataSetChanged (); Criteria criteria = new Criteria (); criteria.Accuracy = Accuracy.NoRequirement; criteria.PowerRequirement = Power.NoRequirement; string provider = _locMgr.GetBestProvider (criteria, true); _locMgr.RequestLocationUpdates(provider, 20000, 100, this); }
RemoveUpdates()
in OnPause()
. This eliminates unnecessary processing of location changes when the POIListActivity
class is not visible, as shown in the following code:protected override void OnPause () { base.OnPause (); _locMgr.RemoveUpdates (this); }
POIListActivity
implements Android.Locations.ILocationListener
and implement stub methods using the Refactor|Implement
interface. Remove any code placed in the stub methods, we will provide logic for OnLocationChange()
.CurrentLocation
property to POILIstViewAdapter
. The POIListActivity
class will use this property to communicate location changes to the adapter:public Location CurrentLocation { get; set; }
OnLocationChanged()
to set CurrentLocation
on POIListViewAdapter
when a location change is received and call NotifyDataSetChange()
to cause the ListView
to be refreshed as follows:public void OnLocationChanged (Location location) { _adapter.CurrentLocation = location; _adapter.NotifyDataSetChanged (); }
GetView()
method on POIListViewAdapter
to calculate the distance between the CurrentLocation
and a POI's location properties and update distanceTextView
with the results. The calculation should only be done if CurrentLocation
is not null
and the Latitude
and Longitude
properties for the POI being added to the ListView
are not null
. If any of these values are null
, simply place ??
in the distance field to indicate it cannot be calculated at this time as follows:if ((CurrentLocation != null) && (poi.Latitude.HasValue) && (poi.Longitude.HasValue)) { Location poiLocation = new Location (""); poiLocation.Latitude = poi.Latitude.Value; poiLocation.Longitude = poi.Longitude.Value; float distance = CurrentLocation.DistanceTo (poiLocation) * 0.000621371F; view.FindViewById<TextView> (Resource.Id.distanceTextView).Text = String.Format ("{0:0,0.00} miles", distance); } else { view.FindViewById<TextView> (Resource.Id.distanceTextView).Text = "??"; }
The steps for adding location services to POIDetailActivity
will be very similar to the previous section, but will be slightly simpler.
Prior to adding the logic, we need to add a few buttons to our app; one for getting our location and one for navigating to the map, which we will cover later in this chapter. We can add these as a row of buttons at the bottom of POIDetail.axml
, as depicted in the following screenshot:
Using ImageButton
allows a drawable
class to be specified.
To add the ImageButton
widgets, perform the following steps:
LinearLayout
instance to the bottom of POIDetail.axml
just below the TableLayout
used to arrange the latitude and longitude content. The orientation should be "horizontal"
.LinearLayout
instance, the orientation should be horizontal, the content should be wrapped (both height and width), and it should be centered horizontally in the parent. The layout gravity can be used to center content horizontally within its parent. The top and bottom padding of 10dp
will provide a good spacing for the buttons.ImageButton
widgets, locationImageButton
and mapImageButton
, within the LinearLayout
instance. Images for these buttons can be found in the drawable
folder in the assets
location.. . . </TableLayout> <LinearLayout p1:orientation="horizontal" p1:layout_width="wrap_content" p1:layout_height="wrap_content" p1:layout_gravity="center_horizontal" p1:minWidth="25px" p1:minHeight="25px" p1:layout_marginTop="10dp" p1:layout_marginBottom="10dp"> <ImageButton p1:src="@drawable/ic_locate" p1:layout_width="wrap_content" p1:layout_height="wrap_content" p1:id="@+id/locationImageButton" /> <ImageButton p1:src="@drawable/ic_map" p1:layout_width="wrap_content" p1:layout_height="wrap_content" p1:id="@+id/mapImageButton" /> </LinearLayout>
Now that we have buttons on the UI, we can add the code to obtain the location as follows:
LocationManager
and obtain a reference in OnCreate()
in the same way we did for POIListView
in the previous section.GetLocationClicked
event handler and hook it up to the ImageButton
as follows:_locationImageButton = FindViewById<ImageButton> (Resource.Id.locationImageButton); _locationImageButton.Click += GetLocationClicked;
RequestSingleUpdate()
in GetLocationClicked()
. The RequestSingleUpdate()
method allows for a Criteria
object to be passed in so that we do not need a separate call to GetBestProvider()
as follows:protected void GetLocationClicked(object sender, EventArgs e) { Criteria criteria = new Criteria(); criteria.Accuracy = Accuracy.NoRequirement; criteria.PowerRequirement = Power.NoRequirement; _locMgr.RequestSingleUpdate (criteria, this, null); }
POIDetailActivity
implements Android.Locations.ILocationListener
and implement stub methods using the Refactor|Implement
interface. Remove any code placed in the stub methods; we will provide a logic for OnLocationChange()
.OnLocationChange()
to update the location fields as follows:public void OnLocationChanged (Location location) { _latEditText.Text = location.Latitude.ToString(); _longEditText.Text = location.Longitude.ToString (); }
Run POIApp
and test adding a new POI and getting the location. While running the app in the emulator, you will notice that apparently nothing happens when the location button is clicked. The app is actually waiting for a callback to OnLocationChanged()
from the location manager; to trigger this callback, you must use the Android Device Monitor.
To trigger OnLocationChanged()
, perform the following steps:
OnLocationChanged()
method to be fired on POIDetailActivity
.Note the other two tabs under Location Controls, that is GPX and KML. These tabs can be used to load a series of location updates from a file and play them back to your app to test more sophisticated scenarios.
Another useful feature provided by the Android platform is called Geocoding. This is the process of obtaining the location in terms of latitude and longitude from a known address. Android also supports Reverse Geocoding, which is what you would expect from the name, obtaining an address from a known location.
The Android.Locations.Geocoder
class is the class used to perform geocoding and reverse geocoding. It's a very straightforward process to use, as shown in the following steps:
Android.Locations.Geocoder
.GetFromLocation()
passing in the location you would like to find the address for.IList<Address>
collection returned. The collection of addresses returned from GetFromLocation()
vary in specifics; meaning, some are specific street addresses, some specify a city, country, and so on. The first address is always the most specific, so we will automatically choose it, using the following code:public void OnLocationChanged (Location location) { _latEditText.Text = location.Latitude.ToString(); _longEditText.Text = location.Longitude.ToString (); Geocoder geocdr = new Geocoder(this); IList<Address> addresses = geocdr.GetFromLocation (location.Latitude, location.Longitude, 5); if (addresses.Any()) { UpdateAddressFields (addresses.First ()); } }
You can see that we chose to call a method to format the address information. The FeatureName
property may contain a title such as Golden Gate Bridge
or Empire State Building
. More times than not, FeatureName
will simply contain the street number. The address contains a list of address lines, which we combine and place in _addrEditText
, as follows:
protected void UpdateAddressFields(Address addr) { if (String.IsNullOrEmpty(_nameEditText.Text)) _nameEditText.Text = addr.FeatureName; If (String.IsNullOrEmpty(_addrEditText.Text)) { for (int i = 0; i < addr.MaxAddressLineIndex; i++) { if (!String.IsNullOrEmpty(_addrEditText.Text)) _addrEditText.Text += System.Environment.NewLine; _addrEditText.Text += addr.GetAddressLine (i); } } }
Now, run POIApp
and test adding a new POI and getting the address for a location.
After using the get location button, you will notice that requests for location information take some amount of time to process; generally a few seconds or more. It would be best to keep users informed that processing is taking place so that they don't continually click on the button. The ProgressDialog
class provides just the solution needed, which is a simple means of displaying a dialog with a spinning progress widget and text description of what process is taking place.
To add a progress dialog, perform the following steps:
ProgressDialog
to POIDetailActivity
as follows:ProgressDialog _progressDialog;
GetLocationClicked()
, call the static method ProgressDialog.Show()
saving the result in the private variable we just created. The Show(),
method accepts an activity, a title, and a message as parameters. This call causes the progress dialog to be presented to the user as follows:_progressDialog = ProgressDialog.Show (this, "", "Obtaining location...");
OnLocationChanged()
, call the Cancel()
method on the progress dialog causing the dialog to be closed as follows:_progressDialog.Cancel ();
Now, run POIApp
and test the new progress dialog.
In the previous section, we solved the problem of keeping the user informed while they wait for location updates, but we unknowingly created another problem. As you may recall from Chapter 1, The Anatomy of an Android App, we mentioned that by default Android destroys and recreates an activity when a configuration change such as a device orientation change occurs.
This is done to take advantage of a feature that Android provides, which allows you to specify different layouts to use based on device orientation. In our app, we have a single folder named layout
, which holds all our layouts. If we wanted POIDetail.axml
to have a different layout when the device is in landscape, we would simply create a new folder in Resources
named layout-land
and create our alternate layout in it using the same name, POIDetail.axml
. When Android destroys and recreates our activity because the device was rotated to landscape, our call to SetContentView()
would cause Android to first look in layout-land
to see if a layout exists.
This is neat, but a fallout of this approach arises when an activity initiates any type of asynchronous processing such as requesting location updates. The problem is when a configuration change takes place and Android destroys the original activity; you no longer want asynchronous callbacks to come in for the original activity.
To observe the issue, run POIApp
, press the get location button, and rotate the screen. In the emulator, press Ctrl + F11 or Ctrl + F12 to rotate the device. If you are in the debug
mode, you should see an exception thrown when you try and cancel the progress dialog; this is due to the fact that Android has already removed the dialog from the view
hierarchy when it destroyed the activity.
Android provides the following ways of dealing with this issue:
AndroidManifest.xml
fileActivity
to save and restore the state appropriately when configuration changes occurTo allow for saving and restoring of a state, Android provides OnSaveInstanceState()
and OnRestoreInstanceState()
. The OnSaveInstanceState()
method is called as a part of the destruction of the activity due to configuration changes and allows you to save processing a state in the Bundle
object that is passed in. The OnRestoreInstanceState()
method is called after OnStart()
when an activity is being reinitialized and receives the same Bundle
object that OnSaveInstanceState()
would have populated.
In our case, we simply want to cancel any pending location changes within OnSaveInsatnceState()
and restart the process in OnRestoreInstanceState()
, if we were indeed waiting for location updates when the configuration change occurred. Unfortunately, many asynchronous scenarios are far more complex than ours and are beyond what can be considered here.
To save and restore a state, perform the following steps:
bool
that will be used to indicate if we are waiting for location updates as follows:bool _obtainingLocation = false;
GetLocationClicked()
to set this variable to true
, and at the bottom of OnLocationChanged()
to set this variable to false
.OnSaveInstanceState()
and save the value of the processing variable and cancel any location updates that might be pending as follows:protected override void OnSaveInstanceState (Bundle outState) { base.OnSaveInstanceState (outState); outState.PutBoolean("obtaininglocation", _obtainingLocation); // if we were waiting on location updates; cancel if (_obtainingLocation) { _locMgr.RemoveUpdates (this); } }
OnRestoreInstanceState()
and restore the value of the processing variable, and if the value is true
, restart the request for location update as follows:protected override void OnRestoreInstanceState (Bundle savedInstanceState) { base.OnRestoreInstanceState (savedInstanceState); _obtainingLocation = savedInstanceState.GetBoolean("obtaininglocation"); // if we were waiting on location updates; restart if (_obtainingLocation) GetLocationClicked (this, new EventArgs ()); }
Now, run POIApp
and test the changes.
By now, you may be wondering if we have a similar problem in POIListActivity
. But we do not because of our decision to turn on and off location updates
in the OnResume()
and OnPause()
methods. These methods are called as part of the normal construction and destruction processes. As a result, there are no scenarios where an asynchronous callback will be called after the activity is destroyed. You will, however, notice that when you change the orientation of the device, the distance labels are changed to ??
due to the activity being reconstructed. This actually provides a good opportunity to demonstrate an alternate way of dealing with this issue; preventing the activity from being destroyed. Android allows for the specification of the configChanges
attribute in the activity
element of the AndroidManifest.xml
file. Specifying configChanges
tells Android that you will take care of reconfiguring the layout, if needed, and the activity
class will not be destroyed. The following code example shows how to specify that orientation and the screen size changes for POIListActivity
will be dealt manually by the app:
<activity android:name="poiapp.POIListActivity" android:configChanges="orientation|screenSize" android:label="POIs" > . . . </activity>
Xamarin.Android provides a more convenient way to specify how to prevent activity destruction using an attribute on the activity
class. The following code example shows the use of the ConfigurationChanges
attribute;
[Activity (Label = "POIs", MainLauncher = true, ConfigurationChanges = (Android.Content.PM.ConfigChanges.Orientation | Android.Content.PM.ConfigChanges.ScreenSize))] public class POIListActivity : Activity, ILocationListener { . . .
After placing the ConfigurationChanges
attribute on POIListActivity
, run POIApp
and observe that once the distances have been calculated, they will not be lost during device orientation changes.
3.16.217.58