Home

Android Geofences

Introduction

I'm aware that there already exists a geofence tutorial and example on developer.android.com. However, at the time of writing there are two issues that I have with them. The first being that the tutorial's code doesn't match with the example code, which is linked on the tutorial's page. Though the example is a wonderful resource and shows you what to do and how to do it, trying to understand what you need to do can be difficult figure out. 

Requirements

Since we will be using Google Maps and Location based objects, we will need to have our project reference the Google Play Services. See this tutorial on adding the Google Play Services to your project.

Application

The first thing we need to do is add the necessary entries to the manifest file for both the permissions and pieces of our application.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.paulusworld.geofence"
    android:versionCode="1"
    android:versionName="1.0" >
    
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="21" />

    <uses-permission android:name="android.permission.INTERNET" />
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
	<!--
            Requests address-level location access, which is usually
            necessary for Geofencing
    -->
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
	<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
    
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name="com.paulusworld.geofence.ReceiveTransitionsIntentService" android:exported="false"></service>
        
        <!-- 
        	Required for Google Maps
         -->
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="ANDROID_MAPS_API_KEY_GOES_HERE" />
    </application>

</manifest>

The android.permission.INTERNET and android.permission.ACCESS_NETWORK_STATE are necessary for downloading Google Map data. The android.permission.ACCESS_FINE_LOCATION and com.google.android.providers.gsf.permission.READ_GSERVICES are needed for use with geofences.

We're specifying a service on line 36. By using a service, our app does not need to be running when we enter, exit, or dwell in a geofence.

I've defined some strings in the strings.xml that will be used in the example:

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

    <string name="app_name">Geofence</string>
    <string name="hello_world">Hello world!</string>
    <string name="action_settings">Settings</string>

    <string name="geofence_transition_notification_title">
        %1$s geofence(s) %2$s
    </string>
    <string name="geofence_transition_notification_text">
        Click notification to return to app
    </string>

    <string name="geofence_transition_unknown">Unknown transition</string>
    <string name="geofence_transition_entered">Entered</string>
    <string name="geofence_transition_exited">Exited</string>
    <string name="geofence_transition_dwell">Stop dwelling!</string>
</resources>

We'll use a simple layout that uses a MapFragment:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="com.paulusworld.geofence.MainActivity" >

    <fragment
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="45"
        class="com.google.android.gms.maps.SupportMapFragment" />

</LinearLayout>

Normally we won't be throwing so much of the location related code in the MainActivity, but for simplicity's sake we will do it this time.  

package com.paulusworld.geofence;

import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Color;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.widget.Toast;


import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesClient;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.LocationClient;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationStatusCodes;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.Circle;
import com.google.android.gms.maps.model.CircleOptions;
import com.google.android.gms.maps.model.LatLng;

import java.util.ArrayList;

public class MainActivity extends FragmentActivity
        implements GooglePlayServicesClient.ConnectionCallbacks,
        GooglePlayServicesClient.OnConnectionFailedListener,
        LocationListener,
        LocationClient.OnAddGeofencesResultListener {

    private final static String TAG = "MainActivity";
    /**
     * Google Map object
     */
    private GoogleMap mMap;

    /**
     * Geofence Data
     */

    /**
     * Coordinates for the Geofence.
     */
    private LatLng mGeofenceLatLng = new LatLng(YOUR_LATITUDE, YOUR_LONGITUDE);

    /**
     * Radius of the Geofence in meters.
     */
    private int mRadius = 80;

    /**
     * The Geofence object.
     */
    private Geofence mGeofence;

    /**
     * Entry point for Google's location related APIs.
     */
    private LocationClient mLocationClient;

    /**
     * Used to set the priority and intervals of the location requests.
     */
    private LocationRequest mLocationRequest;

    /**
     * Visuals
     */
    private CircleOptions mCircleOptions;
    private Circle mCircle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /** 
         * We create a new LocationClient which is used as an entry point for Google's location
         * related APIs. The first parameter is the context, the second is
         * GooglePlayServicesClient.ConnectionCallbacks, and the third is
         * GooglePlayServicesClient.OnConnectionFailedListener. Since we implemented both listeners
         * on the MainActivity class, we pass 'this' for the second and third parameters.
         */
        mLocationClient = new LocationClient(this, this, this);

        /**
         * With the LocationRequest, we can set the quality of service. For example, the priority
         * and intervals.
         */
        mLocationRequest = LocationRequest.create();
        mLocationRequest.setPriority(LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY);
        mLocationRequest.setInterval(3600000);
        mLocationRequest.setFastestInterval(60000);
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Connect to the location APIs.
        mLocationClient.connect();
    }

    protected void onStop() {
        // Disconnect from the location APIs.
        mLocationClient.disconnect();
        super.onStop();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
            setUpMapIfNeeded();
        } else {
            GooglePlayServicesUtil.getErrorDialog(
                    GooglePlayServicesUtil.isGooglePlayServicesAvailable(this),
                    this, 0);
        }
    }

    private void setUpMapIfNeeded() {
        // Do a null check to confirm that we have not already instantiated the
        // map.
        if (mMap == null) {
            // Try to obtain the map from the SupportMapFragment.
            mMap = ((SupportMapFragment) getSupportFragmentManager()
                    .findFragmentById(R.id.map)).getMap();

            // Check if we were successful in obtaining the map.
            if (mMap != null) {
                setUpMap();
            }
        }
    }

    /**
     * This is where we can add markers or lines, add listeners or move the
     * camera. In this case, we just add a marker near Africa.
     * <p/>
     * This should only be called once and when we are sure that {@link #mMap}
     * is not null.
     */
    private void setUpMap() {
        // Centers the camera over the building and zooms int far enough to
        // show the floor picker.
        mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(
                new LatLng(mGeofenceLatLng.latitude, mGeofenceLatLng.longitude), 18));
        // Hide labels.
        mMap.setMapType(GoogleMap.MAP_TYPE_HYBRID);
        mMap.setIndoorEnabled(false);
        mMap.setMyLocationEnabled(true);

        // Adding visuals.
        mCircleOptions = new CircleOptions()
                .center(mGeofenceLatLng).radius(mRadius).fillColor(0x40ff0000)
                .strokeColor(Color.TRANSPARENT).strokeWidth(2);
        mCircle = mMap.addCircle(mCircleOptions);

    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Log.v("GEOFENCE", "Connection to LocationClient failed!");
    }

    @Override
    public void onConnected(Bundle arg0) {

        Log.v("GEOFENCE", "Connected to location services.");

        ArrayList<Geofence> geofences = new ArrayList<Geofence>();

        /**
         * The addGeofences function requires that the Geofences be in a List, so there can be
         * multiple geofences. For this example we will only need one.
         */
        mGeofence = new Geofence.Builder()
                .setRequestId("Geofence")
                // There are three types of Transitions. 
                .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_DWELL | Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT)
                // Set the geofence location and radius. 
                .setCircularRegion(mGeofenceLatLng.latitude, mGeofenceLatLng.longitude, mRadius)
                // How long the geofence will remain in place. 
                .setExpirationDuration((1000 * 60) * 60)
                // This is required if you specify GEOFENCE_TRANSITION_DWELL when setting the transition types.
                .setLoiteringDelay(1000)
                .build();

        /**
         * Adding the geofence to the ArrayList, which will be passed as the first parameter
         * to the LocationClient object's addGeofences function.
         */
        geofences.add(mGeofence);

        /**
         * We're creating a PendingIntent that references the ReceiveTransitionsIntentService class
         * in conjunction with the geofences.
         */
        Intent intent = new Intent(this, ReceiveTransitionsIntentService.class);
        PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);

        /**
         * We want this (MainActivity) to handle location updates.(see onLocationChanged function)
         */
        mLocationClient.requestLocationUpdates(mLocationRequest, this);
        /**
         * Adding the Geofences and PendingIntent to the LocationClient and setting this
         * (MainActivity) to handle onAddGeofencesResult. The pending intent, which is the
         * ReceiveTransitionsIntentService, is what gets utilized when one of the transitions
         * that was specified in the geofence is fired.
         */
        mLocationClient.addGeofences(geofences, pendingIntent, this);

    }

    @Override
    public void onDisconnected() {
        Log.v("GEOFENCE", "Disconnected");
    }

    @Override
    public void onLocationChanged(Location location) {
        /**
         * Location data is passed back to this function.
         */
        Toast.makeText(this, "Location Changed: " + location.getLatitude() + ", " + location.getLongitude(), Toast.LENGTH_LONG).show();
    }

    @Override
    public void onAddGeofencesResult(int statusCode, String[] geofenceRequestIds) {

        switch(statusCode) {
            case LocationStatusCodes.SUCCESS:
                Log.v(TAG, "Successfully added Geofence.");
                break;
            case LocationStatusCodes.ERROR:
                Log.v(TAG, "Error adding Geofence.");
                break;
            case LocationStatusCodes.GEOFENCE_TOO_MANY_GEOFENCES:
                Log.v(TAG, "Too many geofences.");
                break;
            case LocationStatusCodes.GEOFENCE_TOO_MANY_PENDING_INTENTS:
                Log.v(TAG, "Too many pending intents.");
                break;
        }
    }
}

The MainActivity class implements the the GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener, LocationListener, and LocatoinClient.OnAddGeofencesResultListener.

The GooglePlayServicesClient.ConnectionCallbacks requires the class to implement the following functions:

The GooglePlayServicesClient.OnConnectionFailedListener requires the class to implement the onConnectionFailed(ConnectionResult result) function.

The LocationListener has four functions that can be implemented, but only one of them is required:

Finally, the LocatoinClient.onAddGeofencesResultListener requires the onAddGeofencesResult function to be implemented. This function is called when the addGeofences(List, PendingIntent, OnAddGeofencesResultListener) operation completes, whether successfully or not.

On lines 51 and 56 you can specify your own latitude, longitude, and radius of the geofence. For the most part, if you've seen my previous blog entries regarding Google Maps, there is a lot of code that is reused. 

On line 91 we're creating a LocationClient object that is saying to "Hey, I want to use Google's location services."

On lines 97 to 100, we're creating a LocationRequest object that sets the various parameters that will be applied for requesting location updates. This LocationRequest object is passed in the LocationClient objects function requestLocationUpdates.

On line 160 is simply adding visuals to the map to make it easier to see where the geofence is.

Line 169 is overriding an abstract function that is defined by implementing the OnConnectionFailedListener.

Line 174 is overriding another abstract function that is defined in GooglePlayServicesClient.ConnectionCallbacks. In this function, we're creating the geofence that we want to monitor. On line 178 we're creating an ArrayList to hold the geofences, even though there is only one. We're doing this because when we call the function that adds the geofences, LocationClient.addGeofences it expects an ArrayList of Geofence objects. Geofence objects are built, not instantiated like most other objects. We do this on line 184 using the Builder class and setting all the parameters through other functions from lines 185 to 193 until finally returning a Geofence object by calling the build() function. The Geofence is added to the ArrayLsit on line 200. On line 206 - 207, we're creating a PendingIntent, which is tied to an IntentService and is fired when the conditions set on the Geofence have been met. In this case, enter, dwell, and exit. In order for the PendingIntents to be handled, we need to add the Geofences to the LocationClient.

Line 224 overrides the abstract function onDisconnect that is defined in GooglePlayServicesClient.ConnectionCallbacks.

Line 229 overrides the abstract function onLocationChanged that is defined in LocationSource.OnLocationChangedListener.

The onAddGeofencesResult function found on line 237 is defined in the LocationClient.OnAddGeofencesResultListener. In this function, we would do any work that needs to be done in the event that a specific statusCode is returned.

package com.paulusworld.geofence;

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.google.android.gms.location.Geofence;
import com.google.android.gms.location.LocationClient;

import java.util.List;

public class ReceiveTransitionsIntentService extends IntentService {

    private final static String TAG = ReceiveTransitionsIntentService.class.getPackage() + "." + ReceiveTransitionsIntentService.class.getSimpleName();

	public ReceiveTransitionsIntentService() {
		super("ReceiveTransitionsIntentService");
        Log.v(TAG, "Service Constructor");
	}

	@Override
	protected void onHandleIntent(Intent intent) {

        if(!LocationClient.hasError(intent)) {
            int transition = LocationClient.getGeofenceTransition(intent);
            Log.v(TAG, "Transition: " + transition);

            // Post a notification
            List<Geofence> geofences = LocationClient.getTriggeringGeofences(intent);
            String[] geofenceIds = new String[geofences.size()];
            for (int index = 0; index < geofences.size() ; index++) {
                geofenceIds[index] = geofences.get(index).getRequestId();
            }
            String ids = TextUtils.join(", ", geofenceIds);
            String transitionType = getTransitionString(transition);

            sendNotification(transitionType, ids);
        } else {
            Log.e(TAG, String.valueOf(LocationClient.getErrorCode(intent)));
        }
	}

    /**
     * Posts a notification in the notification bar when a transition is detected.
     * If the user clicks the notification, control goes to the main Activity.
     * @param transitionType The type of transition that occurred.
     *
     */
    private void sendNotification(String transitionType, String ids) {

        // Create an explicit content Intent that starts the main Activity
        Intent notificationIntent =
                new Intent(getApplicationContext(),MainActivity.class);

        // Construct a task stack
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);

        // Adds the main Activity to the task stack as the parent
        stackBuilder.addParentStack(MainActivity.class);

        // Push the content Intent onto the stack
        stackBuilder.addNextIntent(notificationIntent);

        // Get a PendingIntent containing the entire back stack
        PendingIntent notificationPendingIntent =
                stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

        // Get a notification builder that's compatible with platform versions >= 4
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this);

        // Set the notification contents
        builder.setSmallIcon(R.drawable.ic_launcher)
                .setContentTitle(
                        getString(R.string.geofence_transition_notification_title,
                                transitionType, ids))
                .setContentText(getString(R.string.geofence_transition_notification_text))
                .setContentIntent(notificationPendingIntent);

        // Get an instance of the Notification manager
        NotificationManager mNotificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        // Issue the notification
        mNotificationManager.notify(0, builder.build());
    }

    private String getTransitionString(int transitionType) {
        switch (transitionType) {

            case Geofence.GEOFENCE_TRANSITION_ENTER:
                return getString(R.string.geofence_transition_entered);

            case Geofence.GEOFENCE_TRANSITION_EXIT:
                return getString(R.string.geofence_transition_exited);

            case Geofence.GEOFENCE_TRANSITION_DWELL:
                return getString(R.string.geofence_transition_dwell);

            default:
                return getString(R.string.geofence_transition_unknown);
        }
    }
}

We're using an IntentService because we want to still be able to handle transitions and dwelling Intents even when the application is closed. As long as the geofence has not expired, the LocationClient will continue to fire off Intents, which will be picked up and handled by the IntentService.onHandleIntent

On line 31 we're using the LocationClient to check to see if the Intent contains any errors and if it doesn't, we will proceed to handle the Intent. Once we know that there are no errors, we determine the transition type on line 32.

Line 36 retrieves the geofences that have been triggered. At this point, we have all the information we need to react accordingly. The rest of the function creates a notification reporting the type of transition and which geofence triggered it. 

Android Indoor Maps

Back in May of this year, Google released an update to their Play Services. Up until then, it wasn't possible to use all the features of indoor maps found in Google Maps for Android. Strangely, iOS has had this feature for some time now. One of the those features was being able to manipulate the floor picker. To start using indoor maps properly, you will need to create a new project and configure it to use Google Play Services. The guide can be found here. Since the indoor maps is an added feature to Google Maps, there isn't a lot of additional coding that needs to be done. I've created a simple mobile application that takes advantage of the new Indoor API functions.

The Manifest is pretty self-explanatory and while the layout file is also self explanatory, I would like to explain the spinner found in the layout. Having a spinner, which controls the active floor is redundant, I only added it to demonstrate how to manipulate the map.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.paulusworld.androidindoormaps"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
    <!--
			The following two permissions are not required to use
			Google Maps Android API v2, but are recommended.
    -->
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        
        <meta-data
            android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="THIS_IS_WHERE_YOU_PUT_YOUR_API_KEY" />

        <activity
            android:name=".MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.paulusworld.androidindoormaps.MainActivity" >

    <fragment
        android:id="@+id/map"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_weight="45"
        class="com.google.android.gms.maps.SupportMapFragment" />

    <TextView
        android:id="@+id/textCoordinates"
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="5"
        android:text="@string/btn_load_floors"
        android:textAlignment="center"
        android:textAppearance="?android:attr/textAppearanceSmall"
        android:textIsSelectable="true" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="5"
        android:orientation="horizontal" >

        <Spinner
            android:id="@+id/spinnerFloors"
            android:layout_width="0dp"
            android:layout_height="wrap_content" 
            android:layout_weight="70" />
        
    </LinearLayout>

</LinearLayout>
package com.paulusworld.androidindoormaps;

import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.GoogleMap.OnIndoorStateChangeListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.IndoorBuilding;
import com.google.android.gms.maps.model.IndoorLevel;
import com.google.android.gms.maps.model.LatLng;

import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Spinner;

public class MainActivity extends FragmentActivity {

	private Context mContext;
	private GoogleMap mMap;
	private Spinner mSpinnerFloors;
	private IndoorBuilding mBuilding;
	private ArrayAdapter<String> mBuildingFloors;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mContext = this.getBaseContext();

		mSpinnerFloors = (Spinner) findViewById(R.id.spinnerFloors);
		mSpinnerFloors.setOnItemSelectedListener(new OnItemSelectedListener() {

			@Override
			public void onItemSelected(AdapterView<?> parent, View view,
					int position, long id) {
				try {
					mMap.getFocusedBuilding().getLevels().get(position)
							.activate();
				} catch (NullPointerException npe) {
					// In a production application, we would do something with
					// the exception.
				}

			}

			@Override
			public void onNothingSelected(AdapterView<?> parent) {

			}

		});

	}

	@Override
	protected void onResume() {
		super.onResume();
		setupMapIfNeeded();
	}

	private void setupMapIfNeeded() {
		if (mMap == null) {
			mMap = ((SupportMapFragment) getSupportFragmentManager()
					.findFragmentById(R.id.map)).getMap();
			if (mMap != null) {
				setupMap();
			}
		}
	}

	private void setupMap() {
		// Pick a location that has indoor maps and zoom in far enough to show
		// the floor plans.
		mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(43.040715,
				-87.921467), 18));

		// Add a listener for when the indoor state changes.
		mMap.setOnIndoorStateChangeListener(new OnIndoorStateChangeListener() {

			@Override
			public void onIndoorBuildingFocused() {
				if (mBuilding != mMap.getFocusedBuilding()) {

					mBuilding = mMap.getFocusedBuilding();
					mBuildingFloors = getBuildingFloors(mBuilding);
					mSpinnerFloors.setAdapter(mBuildingFloors);

				}
			}

			@Override
			public void onIndoorLevelActivated(IndoorBuilding building) {

				mSpinnerFloors.setSelection(building.getActiveLevelIndex());

			}

		});

		mMap.setOnCameraChangeListener(new OnCameraChangeListener() {

			private float mCurrentZoom = -1;

			@Override
			public void onCameraChange(CameraPosition position) {
				try {
					if (position.zoom != mCurrentZoom) {

						mCurrentZoom = position.zoom;

						if (mMap.getFocusedBuilding() == null) {

							mBuildingFloors.clear();
							mBuildingFloors.notifyDataSetChanged();

						}
					}
				} catch (NullPointerException npe) {
					// In a production application, we would do something with
					// the exception.
				}
			}
		});
	}

	private ArrayAdapter<String> getBuildingFloors(IndoorBuilding indoorBuilding) {

		ArrayAdapter<String> floors = new ArrayAdapter<String>(mContext,
				android.R.layout.simple_spinner_dropdown_item);

		if (indoorBuilding != null) {
			indoorBuilding.getLevels()
					.get(indoorBuilding.getDefaultLevelIndex()).activate();

			for (IndoorLevel level : indoorBuilding.getLevels()) {
				floors.add(level.getName());
			}
		}

		return floors;
	}
}

Lines 31 - 59 set up the interface. On line 37, we're adding an OnItemSelectedListener to the spinner. The listener has two methods, but we're only using the onItemSelected. The onItemSelected is asking the map if there is a focused building. If there is an active building, get the levels and set the active level of the building to the same level the user selected from the spinner control. 

Line 80 moves the camera to a predefined location and zooms in. Indoor maps are only visible at certain levels. Therefore, to display the floor picker, we're setting the zoom level to 18. 

Line 84 adds a OnIndoorStateChangeListener to the map object. This listener has two methods, onIndoorBuildingFocused and onIndoorLevelActivated. The onIndoorBuildingFocused is looking to see if the building has changed. if it has, then we want to get the floors for the new building and populate them in the spinner. 

Line 100 updates the spinner whenever the user selects a different floor from the floor picker. 

Lines 106 - 130 is a feature in which spinner is cleared if the user zooms out far enough, ultimately losing focus of the building. 

getBuildingFloors which is defined on line 132 is a helper function. The function takes a IndoorBuilding object as a parameter, which is used to build an ArrayAdapter containing the names of the floors in the building. 

Objective-C Classes

If you’ve been programming in one of the more popular object oriented programming languages such as C++ or Java, Objective-C may be a bit confusing at first. With Objective-C, you send objects messages opposed to calling methods. With C++ and Java, calling a method name means that, in most cases, it is bound to a section of code in the target class by the compiler. However, with Objective-C, the target of the message is resolved at runtime.

At the cost of speed and multiple inheritance, Objective-C is able to support dynamic binding by default. This means that messages can go unimplemented or be defined at runtime. However, implementation is still required for the method to be called in the derived object.

Creating a class is typically done using two files. The first being the header file (*.h). Within this file, is where you declare your class with its instance variables and methods/messages. To indicate that you are declaring a class, you use the @interface keyword to begin the class declaration and the @end keyword to end the declaration.

@interface Event : NSObject
{
    // Instance variables
    @public
    NSString* public_var;
    @protected
    NSString* protected_var;
    @private
    NSString* private_var;
}

@property NSInteger* type;
@property NSString* name;
@property NSDate* startTime;
@property NSDate* endTime;
@property NSString* description;

// Class methods
+ (id) newInstance;

// Instance methods
- (id) initWithName:(NSString *)name withType:(NSInteger *)type withStartTime:(NSDate *)start withEndTime:(NSDate *)end withDescription:(NSString *)description;

@end

The second file that is typically used is a ‘.m’ which originally meant “messages.” The .m file is similar to a .c or .cpp file which where you implement your code.

#import "Event.h"

@implementation Event
{
    // Instance variables can also be declared here.
}

// The plus (+) indicates that this is a class message.
+ (id) newInstance
{
    return [[Event alloc] init];
}

- (id) initWithName:(NSString *)name withType:(NSInteger *)type withStartTime:(NSDate *)start withEndTime:(NSDate *)end withDescription:(NSString *)description
{
    self = [super init];
    if(self)
    {
        _name = name;
        _type = type;
        _startTime = start;
        _endTime = end;
        _description = description;
    }
    
    return self;
}
@end

In Objective-C there are two types of keywords, the standard C/C++ keywords, such as int, break, continue, etc. The second set starts with a ‘@‘ (at) symbol. The latter is used to differentiate Objective-C keywords from C/C++ keywords. It’s important to note that the ‘@‘ has multiple meanings based on the context it is used. This symbol is also used for starting strings:

NSString* str = @“Hello World!”

It is also used as literals:

NSArray* array = @[ obj1, obj2, obj3 ];
NSDictionary* dictionary = @{ key1 : obj1, key2 : obj2, key3 : obj3 };
NSNumber* number = @(2 + 2);

Instance Variables

Instance variables can be defined in either the @interface or @implementation as seen in the example code above. When declaring instance variables, the variables are private opposed to properties, which are public. However, you can change the scope by using @public, @private, or @protected.

@interface Event : NSObject
{
	@public
	NSString* name;
	@protected
	NSDate* time;
	@private
	NSString* description;
}
// properties and messages
@end

@implementation Event
{
	@public
	NSString* name;
	@protected
	NSDate* time;
	@private
	NSString* description;
//message declarations
@end

Properties

When declaring properties in an Objective-C class, you are declaring variable as public, giving external classes access to that property. It is possible to limit the access to read only by declaring the property as readonly. Additionally, properties can be declared as: 

  • atomic - By default all properties are atomic, which means they are locked when they are accessed or set. This is not the same as thread safety. This means that if a value is being set, the getter must wait for the value to fully be set before the value can be retrieved.
  • nonatomic - Prevents the property from being locked.
  • readonly - Declaring a property as readonly may be provided with storage semantics such as assign, copy, or retain.
    • assign - Used to set the property’s pointer to the address of the object without retaining it. This is used with scalar types of data (I.e., int, float, char, etc.)
    • copy - the property will maintain a copy of the original value. When specifying a property to keep a copy of the value, it must support NSCopying.
    • retain - Default, system will manage the retain count on its own.
  • getter = getterName: and setter = setterName: - By specifying a getterName and setterName name, you override the getter and setter messages generated when synthesizing properties. If you do not specify getter and setter names, the compiler automatically generates these for you. The getter and setter messages follow a specific naming convention:
    • The getter message is the same property name. If you declare a property of name, you would use the getter as such:
[object name];
    • The setter used to set the property’s value pre-appends the word ‘set’ and capitalizes the first character of the property if it’s not already capitalized. If we have a property of firstName, the setter would be setFirstName.
[object setFirstName:@“Paulus”];
  • strong- By default properties are implicitly declared as having a strong relationship. Meaning that the property will remain in memory until the object is set to nil and it’s not owned by anything else.
  • weak - Used if you don’t want control over the object’s life. The most frequently use cases are when there is a two way relationship between objects. Using a weak reference avoids the possibility of retain cycles.

When a property is declared, an instance variable is created and identified by the property’s name with a pre-appended underscore (_). In the above example, name and age all have an instance variable called _name and _age. However, We defined a different instance variable for address. Instead of _address, the instance variable is called instanceAddress.

@interface Person : NSObject 
@property(copy) NSString* name;
@property(readonly) NSInteger* age;
@property(setter = move:) NSString* address;
// .. messages
@end

@implementation Person
@synthesize name;
@synthesize age;
@synthesize address = instanceAddress;
// .. message
@end

Messages

The syntax for messages are a lot different than methods in C/C++, Java, and PHP. With methods and functions, the object needs to respond or have code to execute. Essentially, you’re jumping to a location in memory to execute code. However, Objective-C objects may choose to not respond or forward the message. This is what makes Objective-C a more dynamic language.

You define messages using labels, argument types, and argument names. If the message does not require parameters, then the message only needs one label is the name of the message:

+ (id) newInstance

Messages with a single parameter are pretty straight forward:

- (void) setName:(NSString *)name;

setName is the name of the message, and name is the parameter you are passing. When dealing with messages that take multiple arguments, the message, in my opinion, becomes difficult to read.

- (id) initWithName:(NSString *)name withType:(NSInteger *)type withStartTime:(NSDate *)start withEndTime:(NSDate *)end withDescription:(NSString *)description

Again, initWithName is the name of the message and also the label for the first parameter. (NSString *) is the type of parameter we’re going to be passing. withType, withStartTime, withEndTime, and withDescription are all labels for the parameters that immediately follow; start, end, and description, respectively.

Categories & Class Extensions

Categories allow you to add messages to an existing class, even if you do not have the original source. For example, if you wanted to add a helper message to the NSObject class, you can.

Categories are typically declared and implemented in two files; a header file (.h) and a messages file (.m). To differentiate the class and category files, a different naming convention is employed by categories. These files are named by taking the name of the class, category, and concatenating them with a plus sign (+):

MyClass+MyCategory.h
MyClass+MyCategory.m
NSObject+MyCategory.h
NSObject+MyCategory.m

To add an existing class to a category you would do the following:

#import "Event.h"

@interface Event (ChildrensEvent)
- (NSInteger) totalHeadCount;
@end

Note the (ChildrensEvent) — This means that you are adding the event class to the ChildrensEvent category. If a category doesn’t exist already, it will be created automatically. In order to use the messages created in the category, you will need to include the header file where ever you wish to use the messages of the category.

#import 
#import "Event.h"
#import "Event+ChildrensEvent.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        
        Event* event = [Event newInstance];
        
        [event setName:@"New Event Name"];
        
        NSLog(@"\n%@", [event name]);
        
        [event setChildCount:2];
        
        NSLog(@"\nNumber of Children: %d", [event totalHeadCount]);
        
    }
    return 0;
}

Categorizing your own classes to add new messages won’t be a problem. When adding existing classes from another framework to a category you may run into name clashes, so it’s advised to add a three letter prefix to the message names. If there is another message with the same name, it’s impossible to know which one will be called.

Categories will not allow you to add instance variables and properties to existing classes. However, you can add instance variables and properties by using class extensions, which are also known as anonymous categories. Unlike categories, where you don’t need the source to add messages, class extensions require that you have the original source. If you add any messages to a class extension, you must implement them.

A useful trick you can do with class extensions is making messages and properties private, meaning that they are only accessible to the class itself. You could only declare a property as readwrite or readonly, which is an all or nothing situation. To get around this, you would declare the property as readonly and create a class extension that has the same property name, but as readwrite:

@interface Event : NSObject
@property (readonly) NSInteger eventId;
@end

#import "Event.h"

@interface Event ()
{
    // Instance variables
}
@property NSInteger eventId;
@property NSInteger childrenThreeToFive;
@property NSInteger childrenSixToEight;

@end

Like classes and categories, class instances are declared and implemented in two different files. The naming convention for class instances is the class name you wish to extend and the name of the class extension concatenated by an underscore (_):

MyClass_MyClassExtension.h
MyClass_MyClassExtension.m
Event_ChildrensEvent.h
Event_ChildrensEvent.m

Protocols

Protocols are used to declare instance and class messages as well as properties that are independent of any specific class, unlike class interfaces. To declare a protocol, use the Objective-C protocol keyword in place of the interface or implementation keywords. Like classes, protocols can inherit from other protocols. The EventDataSource example below shows that the protocol is inheriting from NSObject. If you would like to inherit from another protocol, substitute NSObject with the protocol of your choosing.

@protocol EventDataSource 
- (NSUInteger) totalHeadCount;
- (NSUInteger) numberOfChildren;
- (NSUInteger) numberOfAdults;
@end

When working with data source objects, it’s important to declare the property as weak to avoid strong reference cycles:

@property (weak) id  dataSource;

Using the basic type of id, will allow the class handle any object type as long as it conforms to the protocol. The protocol that we expect the object to conform to is specified between the less than (<) and greater than (>) signs. If you were to try to assign a different object that doesn’t follow the required protocol, the compiler will produce an error.

Protocols can have optional messages which can be specified by using the optional key word:

@protocol EventDataSource 
// The following three messages are required.
- (NSUInteger) totalHeadCount;
- (NSUInteger) numberOfChildren;
- (NSUInteger) numberOfAdults;
@optional
- (void) optionalMessage:(NSString*) message;
// Every thing that follows is optional until the @required keyword is used.
@required
- (void) requiredMessage:(NSString*) message;
// Every thing that follows is now required.
@end

Methods/messages that are marked as optional must be checked to see if an object implements it before calling it:

if([self.dataSource respondsToSelector:@selector(optionalMessage:)]) {
	[self optionalMessage:@"ponies"];
}

Following protocols is very straight forward, which uses the angle brackets. Multiple protocols can be listed within the brackets but are delimited by a coma.

@interface MyClass : NSObject <eventdatasource, anotherprotocol="">
…
@end

Pages