From 2d191d2b82e8f30bce47d93b8d4aecdd3de9576c Mon Sep 17 00:00:00 2001 From: Asser Date: Sun, 19 Feb 2017 18:32:03 +1100 Subject: [PATCH] T0X.04-Solution-Geofencing --- app/src/main/AndroidManifest.xml | 2 + .../shushme/GeofenceBroadcastReceiver.java | 40 +++++ .../example/android/shushme/Geofencing.java | 168 ++++++++++++++++++ .../example/android/shushme/MainActivity.java | 53 +++--- 4 files changed, 233 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java create mode 100644 app/src/main/java/com/example/android/shushme/Geofencing.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 44d2738d..c5fbcc1a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,8 @@ android:authorities="com.example.android.shushme" android:exported="false"/> + + diff --git a/app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java b/app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java new file mode 100644 index 00000000..c02d01dc --- /dev/null +++ b/app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java @@ -0,0 +1,40 @@ +package com.example.android.shushme; + +/* +* Copyright (C) 2017 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class GeofenceBroadcastReceiver extends BroadcastReceiver { + + public static final String TAG = GeofenceBroadcastReceiver.class.getSimpleName(); + + /*** + * Handles the Broadcast message sent when the Geofence Transition is triggered + * Careful here though, this is running on the main thread so make sure you start an AsyncTask for + * anything that takes longer than say 10 second to run + * + * @param context + * @param intent + */ + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "onReceive called"); + } +} diff --git a/app/src/main/java/com/example/android/shushme/Geofencing.java b/app/src/main/java/com/example/android/shushme/Geofencing.java new file mode 100644 index 00000000..bd8a2fa1 --- /dev/null +++ b/app/src/main/java/com/example/android/shushme/Geofencing.java @@ -0,0 +1,168 @@ +package com.example.android.shushme; + +/* +* Copyright (C) 2017 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.places.Place; +import com.google.android.gms.location.places.PlaceBuffer; + +import java.util.ArrayList; +import java.util.List; + +public class Geofencing implements ResultCallback { + + // Constants + public static final String TAG = Geofencing.class.getSimpleName(); + private static final float GEOFENCE_RADIUS = 50; // 50 meters + private static final long GEOFENCE_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours + + private List mGeofenceList; + private PendingIntent mGeofencePendingIntent; + private GoogleApiClient mGoogleApiClient; + private Context mContext; + + public Geofencing(Context context, GoogleApiClient client) { + mContext = context; + mGoogleApiClient = client; + mGeofencePendingIntent = null; + mGeofenceList = new ArrayList<>(); + } + + /*** + * Registers the list of Geofences specified in mGeofenceList with Google Place Services + * Uses {@code #mGoogleApiClient} to connect to Google Place Services + * Uses {@link #getGeofencingRequest} to get the list of Geofences to be registered + * Uses {@link #getGeofencePendingIntent} to get the pending intent to launch the IntentService + * when the Geofence is triggered + * Triggers {@link #onResult} when the geofences have been registered successfully + */ + public void registerAllGeofences() { + // Check that the API client is connected and that the list has Geofences in it + if (mGoogleApiClient == null || !mGoogleApiClient.isConnected() || + mGeofenceList == null || mGeofenceList.size() == 0) { + return; + } + try { + LocationServices.GeofencingApi.addGeofences( + mGoogleApiClient, + getGeofencingRequest(), + getGeofencePendingIntent() + ).setResultCallback(this); + } catch (SecurityException securityException) { + // Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission. + Log.e(TAG, securityException.getMessage()); + } + } + + /*** + * Unregisters all the Geofences created by this app from Google Place Services + * Uses {@code #mGoogleApiClient} to connect to Google Place Services + * Uses {@link #getGeofencePendingIntent} to get the pending intent passed when + * registering the Geofences in the first place + * Triggers {@link #onResult} when the geofences have been unregistered successfully + */ + public void unRegisterAllGeofences() { + if (mGoogleApiClient == null || !mGoogleApiClient.isConnected()) { + return; + } + try { + LocationServices.GeofencingApi.removeGeofences( + mGoogleApiClient, + // This is the same pending intent that was used in registerGeofences + getGeofencePendingIntent() + ).setResultCallback(this); + } catch (SecurityException securityException) { + // Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission. + Log.e(TAG, securityException.getMessage()); + } + } + + + /*** + * Updates the local ArrayList of Geofences using data from the passed in list + * Uses the Place ID defined by the API as the Geofence object Id + * + * @param places the PlaceBuffer result of the getPlaceById call + */ + public void updateGeofencesList(PlaceBuffer places) { + mGeofenceList = new ArrayList<>(); + if (places == null || places.getCount() == 0) return; + for (Place place : places) { + // Read the place information from the DB cursor + String placeUID = place.getId(); + double placeLat = place.getLatLng().latitude; + double placeLng = place.getLatLng().longitude; + // Build a Geofence object + Geofence geofence = new Geofence.Builder() + .setRequestId(placeUID) + .setExpirationDuration(GEOFENCE_TIMEOUT) + .setCircularRegion(placeLat, placeLng, GEOFENCE_RADIUS) + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) + .build(); + // Add it to the list + mGeofenceList.add(geofence); + } + } + + /*** + * Creates a GeofencingRequest object using the mGeofenceList ArrayList of Geofences + * Used by {@code #registerGeofences} + * + * @return the GeofencingRequest object + */ + private GeofencingRequest getGeofencingRequest() { + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); + builder.addGeofences(mGeofenceList); + return builder.build(); + } + + /*** + * Creates a PendingIntent object using the GeofenceTransitionsIntentService class + * Used by {@code #registerGeofences} + * + * @return the PendingIntent object + */ + private PendingIntent getGeofencePendingIntent() { + // Reuse the PendingIntent if we already have it. + if (mGeofencePendingIntent != null) { + return mGeofencePendingIntent; + } + Intent intent = new Intent(mContext, GeofenceBroadcastReceiver.class); + mGeofencePendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent. + FLAG_UPDATE_CURRENT); + return mGeofencePendingIntent; + } + + @Override + public void onResult(@NonNull Result result) { + Log.e(TAG, String.format("Error adding/removing geofence : %s", + result.getStatus().toString())); + } + +} diff --git a/app/src/main/java/com/example/android/shushme/MainActivity.java b/app/src/main/java/com/example/android/shushme/MainActivity.java index b3ccdefd..768eb799 100644 --- a/app/src/main/java/com/example/android/shushme/MainActivity.java +++ b/app/src/main/java/com/example/android/shushme/MainActivity.java @@ -18,6 +18,7 @@ import android.content.ContentValues; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; @@ -31,6 +32,8 @@ import android.util.Log; import android.view.View; import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.Switch; import android.widget.Toast; import com.example.android.shushme.provider.PlaceContract; @@ -63,7 +66,9 @@ public class MainActivity extends AppCompatActivity implements // Member variables private PlaceListAdapter mAdapter; private RecyclerView mRecyclerView; + private boolean mIsEnabled; private GoogleApiClient mClient; + private Geofencing mGeofencing; /** * Called when the activity is starting @@ -81,11 +86,22 @@ protected void onCreate(Bundle savedInstanceState) { mAdapter = new PlaceListAdapter(this, null); mRecyclerView.setAdapter(mAdapter); - // TODO (9) Create a boolean SharedPreference to store the state of the "Enable Geofences" switch - // and initialize the switch based on the value of that SharedPreference + // Initialize the switch state and Handle enable/disable switch change + Switch onOffSwitch = (Switch) findViewById(R.id.enable_switch); + mIsEnabled = getPreferences(MODE_PRIVATE).getBoolean(getString(R.string.setting_enabled), false); + onOffSwitch.setChecked(mIsEnabled); + onOffSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); + editor.putBoolean(getString(R.string.setting_enabled), isChecked); + mIsEnabled = isChecked; + editor.commit(); + if (isChecked) mGeofencing.registerAllGeofences(); + else mGeofencing.unRegisterAllGeofences(); + } - // TODO (10) Handle the switch's change event and Register/Unregister geofences based on the value of isChecked - // as well as set a private boolean mIsEnabled to the current switch's state + }); // Build up the LocationServices API client // Uses the addApi method to request the LocationServices API @@ -98,31 +114,7 @@ protected void onCreate(Bundle savedInstanceState) { .enableAutoManage(this, this) .build(); - // TODO (1) Create a Geofencing class with a Context and GoogleApiClient constructor that - // initializes a private member ArrayList of Geofences called mGeofenceList - - // TODO (2) Inside Geofencing, implement a public method called updateGeofencesList that - // given a PlaceBuffer will create a Geofence object for each Place using Geofence.Builder - // and add that Geofence to mGeofenceList - - // TODO (3) Inside Geofencing, implement a private helper method called getGeofencingRequest that - // uses GeofencingRequest.Builder to return a GeofencingRequest object from the Geofence list - - // TODO (4) Create a GeofenceBroadcastReceiver class that extends BroadcastReceiver and override - // onReceive() to simply log a message when called. Don't forget to add a receiver tag in the Manifest - - // TODO (5) Inside Geofencing, implement a private helper method called getGeofencePendingIntent that - // returns a PendingIntent for the GeofenceBroadcastReceiver class - - // TODO (6) Inside Geofencing, implement a public method called registerAllGeofences that - // registers the GeofencingRequest by calling LocationServices.GeofencingApi.addGeofences - // using the helper functions getGeofencingRequest() and getGeofencePendingIntent() - - // TODO (7) Inside Geofencing, implement a public method called unRegisterAllGeofences that - // unregisters all geofences by calling LocationServices.GeofencingApi.removeGeofences - // using the helper function getGeofencePendingIntent() - - // TODO (8) Create a new instance of Geofencing using "this" as the context and mClient as the client + mGeofencing = new Geofencing(this, mClient); } @@ -177,7 +169,8 @@ public void refreshPlacesData() { @Override public void onResult(@NonNull PlaceBuffer places) { mAdapter.swapPlaces(places); - // TODO (11) Call updateGeofenceList and registerAllGeofences if mIsEnabled is true + mGeofencing.updateGeofencesList(places); + if (mIsEnabled) mGeofencing.registerAllGeofences(); } }); }