Java tutorial
/** * Copyright (c) 2015, 2016 IBM Corporation. All rights reserved. * <p/> * 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 * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * 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. */ package com.ibm.mf.geofence; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.location.Location; import android.location.LocationManager; import android.os.AsyncTask; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.Status; import com.google.android.gms.location.Geofence; import com.google.android.gms.location.GeofencingRequest; import com.google.android.gms.location.LocationServices; import com.ibm.mf.geofence.rest.HttpMethod; import com.ibm.mf.geofence.rest.HttpRequest; import com.ibm.mf.geofence.rest.HttpRequestCallback; import com.ibm.mf.geofence.rest.HttpRequestError; import com.ibm.mf.geofence.rest.HttpService; import com.ibm.mf.geofence.rest.JSONPayloadRequest; import com.ibm.pisdk.geofencing.BuildConfig; import org.apache.log4j.Logger; import org.json.JSONObject; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** * Provides an API to load geofences from, and send entry/exit to, the server. */ public class MFGeofencingManager { /** * Logger for this class. */ private static final Logger log = LoggingConfiguration.getLogger(MFGeofencingManager.class.getSimpleName()); static final String INTENT_ID = "IBMGeofencingService"; /** * Part of a request path pointing to the geofence connector. */ static final String GEOFENCE_CONNECTOR_PATH = "events"; /** * Part of a request path pointing to the pi conifg connector. */ static final String CONFIG_CONNECTOR_PATH = "geofences"; /** * Mode indicating this geofence manager is executed by the user-provided app. */ static final int MODE_APP = 1; /** * Mode indicating this geofence manager is executed by the geofence transition service. */ static final int MODE_GEOFENCE_EVENT = 2; /** * Mode indicating this geofence manager is executed by the significant location change service. */ static final int MODE_MONITORING_REQUEST = 3; /** * Mode indicating this geofence manager is executed by the reboot handler service. */ static final int MODE_REBOOT = 4; /** * The restful service which connects to and communicates with the Adaptive Experience server. */ final HttpService mHttpService; /** * The Google API client. */ GoogleApiClient mGoogleApiClient; /** * The Android application context. */ Context mContext; /** * Handles the geofences that are currently monitored. */ final int mMaxDistance; /** * Pending intent used to register a set of geofences. */ private PendingIntent mPendingIntent; /** * Provides uniquely identifying information for the device. */ private final String mDeviceDescriptor; /** * The settings of the application. */ Settings mSettings; /** * The execution mode for this gefoence manager; * one of {@link #MODE_APP}, {@link #MODE_GEOFENCE_EVENT}, {@link #MODE_MONITORING_REQUEST} or {@link #MODE_REBOOT}. */ final int mMode; /** * A callback that immplements the Google API connection callback interfaces. */ GoogleLocationAPICallback mGoogleAPICallback; /** * The minimum delay between two synchronizations with the server. */ int mIntervalBetweenDowloads = 24; /** * Initialize this service. * @param context the Android application context. * @param baseURL base URL of the server. * @param username username. * @param password password. * @param maxDistance distance threshold for sigificant location changes. * Defines the bounding box for the monitored geofences: square box with a {@code maxDistance} side centered on the current location. */ public MFGeofencingManager(Context context, String baseURL, String username, String password, int maxDistance) { this(null, MODE_APP, context, baseURL, username, password, maxDistance); } /** * Initialize this service. * @param context the Android application context. * @param baseURL base URL of the server. * @param username username. * @param password password. * @param maxDistance distance threshold for sigificant location changes. * Defines the bounding box for the monitored geofences: square box with a {@code maxDistance} side centered on the current location. */ MFGeofencingManager(Settings settings, int mode, Context context, String baseURL, String username, String password, int maxDistance) { log.debug("mf-geofence version " + BuildConfig.VERSION_NAME); this.mMode = mode; this.mMaxDistance = maxDistance; this.mHttpService = new HttpService(baseURL, username, password); this.mContext = context; this.mSettings = (settings != null) ? settings : new Settings(context); log.debug("MFGeofencingManager() settings = " + this.mSettings); this.mIntervalBetweenDowloads = this.mSettings.getInt(ServiceConfig.SERVER_SYNC_MIN_DELAY_HOURS, 24); this.mDeviceDescriptor = retrieveDeviceDescriptor(); int n = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); log.debug("google play service availability = " + getGoogleAvailabilityAsText(n)); if (this.mMode == MODE_APP) { //initSettingsData(); updateSettings(); } connectGoogleAPI(); } /** * Connect the Google API client. The synchronous / asynchronous mode depends on the {@link #mMode} of this geofence manager. */ private void connectGoogleAPI() { if ((mContext != null) && (mMode != MODE_GEOFENCE_EVENT)) { mGoogleAPICallback = new GoogleLocationAPICallback(this); mGoogleApiClient = new GoogleApiClient.Builder(mContext).addApi(LocationServices.API) .addConnectionCallbacks(mGoogleAPICallback).addOnConnectionFailedListener(mGoogleAPICallback) .build(); log.debug("initGms() connecting to google play services ..."); if ((mMode == MODE_MONITORING_REQUEST) || (mMode == MODE_REBOOT)) { try { // can't run blockingConnect() on the UI thread ConnectionResult result = new AsyncTask<Void, Void, ConnectionResult>() { @Override protected ConnectionResult doInBackground(Void... params) { return mGoogleApiClient.blockingConnect(60_000L, TimeUnit.MILLISECONDS); } }.execute().get(); log.debug(String.format("google api connection %s, result=%s", (result.isSuccess() ? "success" : "error"), result)); } catch (Exception e) { log.error("error while attempting connection to google api", e); } } else if (mMode == MODE_APP) { mGoogleApiClient.connect(); } } } /** * Send a notification to the backend as an HTTP request. * @param fences the geofences for which to send a notification. * @param type the type of geofence notification: either {@link MFGeofenceEvent.Type#ENTER ENTER} or {@link MFGeofenceEvent.Type##EXIT EXIT}. */ void postGeofenceEvent(final List<PersistentGeofence> fences, final MFGeofenceEvent.Type type) { HttpRequestCallback<String> callback = new HttpRequestCallback<String>() { @Override public void onSuccess(String result) { log.debug("sucessfully notified connector for geofences " + fences); } @Override public void onError(HttpRequestError error) { log.error("error notifying connector for geofences " + fences + " : " + error.toString()); } }; JSONObject payload = GeofencingJSONUtils.toJSONGeofenceEvent(fences, type, mDeviceDescriptor, BuildConfig.VERSION_NAME); HttpRequest<String> request = new HttpRequest<String>(callback, HttpMethod.POST, payload.toString()) { @Override protected String resultFromResponse(byte[] source) throws Exception { return new String(source, "UTF-8"); } }; String path = String.format(Locale.US, "%s", GEOFENCE_CONNECTOR_PATH); request.setPath(path); request.setBasicAuthRequired(true); mHttpService.executeRequest(request); } /** * Add the specified geofences to the monitored geofences. * @param geofences the geofences to add. */ void monitorGeofences(List<PersistentGeofence> geofences) { if (!geofences.isEmpty()) { log.debug("monitorGeofences(" + geofences + ")"); List<Geofence> list = new ArrayList<>(geofences.size()); List<Geofence> noTriggerList = new ArrayList<>(geofences.size()); Location last = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); for (PersistentGeofence geofence : geofences) { Geofence fence = new Geofence.Builder().setRequestId(geofence.getCode()) .setCircularRegion(geofence.getLatitude(), geofence.getLongitude(), (float) geofence.getRadius()) .setExpirationDuration(Geofence.NEVER_EXPIRE).setNotificationResponsiveness(10_000) .setLoiteringDelay(300000) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) .build(); Location location = new Location(LocationManager.NETWORK_PROVIDER); location.setLatitude(geofence.getLatitude()); location.setLongitude(geofence.getLongitude()); if ((mMode != MODE_REBOOT) || (last == null) || (location.distanceTo(last) > geofence.getRadius())) { list.add(fence); } else { // if already in geofence, do not trigger upon registration. noTriggerList.add(fence); } } registerFencesForMonitoring(list, GeofencingRequest.INITIAL_TRIGGER_ENTER); registerFencesForMonitoring(noTriggerList, 0); } } private void registerFencesForMonitoring(List<Geofence> fences, int initialTrigger) { if (!fences.isEmpty()) { GeofencingRequest request = new GeofencingRequest.Builder().setInitialTrigger(initialTrigger) .addGeofences(fences).build(); PendingIntent pi = getPendingIntent(INTENT_ID); LocationServices.GeofencingApi.addGeofences(mGoogleApiClient, request, pi) .setResultCallback(new ResultCallback<Status>() { @Override public void onResult(Status status) { log.debug("add geofence request status " + status); } }); } } /** * Remove the specified geofences from the monitored geofences. * @param geofences the geofences to remove. */ void unmonitorGeofences(List<PersistentGeofence> geofences) { log.debug("unmonitorGeofences(" + geofences + ")"); if (!geofences.isEmpty()) { List<String> uuidsToRemove = new ArrayList<>(geofences.size()); for (PersistentGeofence g : geofences) { uuidsToRemove.add(g.getCode()); } LocationServices.GeofencingApi.removeGeofences(mGoogleApiClient, uuidsToRemove); } } /** * Get the minimum delay in hours between two synchronizations with the server. * When not already set, the default value is 24 hours. * @return the minimum number of hours between two server synchronizations. */ public int getIntervalBetweenDowloads() { return mIntervalBetweenDowloads; } /** * Set the minimum delay in hours between two synchronizations with the server. * If (the specified value is less than 1, then this method has no effect. * @param intervalBetweenDowloads the minimum number of hours between two server synchronizations. */ public void setIntervalBetweenDowloads(int intervalBetweenDowloads) { if (intervalBetweenDowloads >= 0) { this.mIntervalBetweenDowloads = intervalBetweenDowloads; updateSettings(); } } /** * Load a set of geofences from a reosurce file. * @param resource the path to the resource to load the geofences from. */ public void loadGeofencesFromResource(final String resource) { AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { private GeofenceList geofenceList; private HttpRequestError error; @Override protected Void doInBackground(Void... params) { ZipInputStream zis = null; try { InputStream is = getClass().getClassLoader().getResourceAsStream(resource); zis = new ZipInputStream(is); ZipEntry entry; Map<String, PersistentGeofence> allGeofences = new HashMap<>(); while ((entry = zis.getNextEntry()) != null) { byte[] bytes = GeofencingUtils.loadBytes(zis); if (bytes != null) { int fileSize = bytes.length; JSONObject json = new JSONObject(new String(bytes, "UTF-8")); bytes = null; // the byte[] may be large, we make sure it can be GC-ed ASAP GeofenceList list = GeofencingJSONUtils.parseGeofences(json); List<PersistentGeofence> geofences = list.getGeofences(); if ((geofences != null) && !geofences.isEmpty()) { PersistentGeofence.saveInTx(geofences); log.debug(String.format(Locale.US, "loaded %,d geofences from resource '[%s]/%s' (%,d bytes)", geofences.size(), resource, entry.getName(), fileSize)); } for (PersistentGeofence pg : list.getGeofences()) { allGeofences.put(pg.getCode(), pg); } } else { log.debug(String.format("the zip entry [%s]/%s is empty", resource, entry.getName())); } } geofenceList = new GeofenceList(new ArrayList<>(allGeofences.values())); log.debug(String.format(Locale.US, "loaded %,d geofences from resource '[%s]'", allGeofences.size(), resource)); } catch (Exception e) { error = new HttpRequestError(-1, e, String.format("error loading resource '%s'", resource)); } finally { try { zis.close(); } catch (Exception e) { log.error(String.format("error closing zip input stream for resource %s", resource), e); if (error == null) { error = new HttpRequestError(-1, e, String.format("error loading resource '%s'", resource)); } } } return null; } @Override protected void onPostExecute(Void aVoid) { if (error != null) { log.error(String.format("error loading resource %s : %s", resource, error)); } else { try { Intent broadcastIntent = new Intent(MFGeofenceEvent.ACTION_GEOFENCE_EVENT); broadcastIntent.setPackage(mContext.getPackageName()); MFGeofenceEvent.toIntent(broadcastIntent, MFGeofenceEvent.Type.SERVER_SYNC, geofenceList.getGeofences(), null); mContext.sendBroadcast(broadcastIntent); } catch (Exception e) { log.error("error sending broadcast event", e); } } } }; task.execute(); } /** * Get the path to the log file. * @return the full path to the log file on the file system. */ public static String getLogFilePath() { return LoggingConfiguration.getLogFile(); } /** * Get a pending intent for the specified callback. * @param geofenceCallbackUuid the uuid of an internally mapped callback. * @return a <code>PendingIntent</code> instance. */ private PendingIntent getPendingIntent(String geofenceCallbackUuid) { if (mPendingIntent == null) { Intent intent = new Intent(mContext, GeofenceTransitionsService.class); //intent.setPackage(context.getPackageName()); ServiceConfig config = new ServiceConfig().fromGeofencingManager(this); config.populateFromSettings(mSettings); config.toIntent(intent); intent.putExtra(INTENT_ID, geofenceCallbackUuid); intent.setClass(mContext, GeofenceTransitionsService.class); mPendingIntent = PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); } return mPendingIntent; } /** * Load geofences from the local database if they are present, or from the server if not. */ void loadGeofences() { if (mHttpService.getServerURL() != null) { log.debug("loadGeofences() loading geofences from the server"); loadGeofencesFromServer(); } else { log.debug("loadGeofences() found geofences in local database"); setInitialLocation(); } } /** * Query the geofences from the server, based on the current anchor. */ private void loadGeofencesFromServer() { if (PersistentGeofence.count(PersistentGeofence.class) <= 0) { loadGeofencesFromServer(-1L); } else { long now = System.currentTimeMillis(); long lastTimeStamp = mSettings.getLong(ServiceConfig.SERVER_SYNC_LOCAL_TIMESTAMP, -1L); if ((lastTimeStamp < 0L) || (now - lastTimeStamp >= mIntervalBetweenDowloads * 3600L * 1000L)) { loadGeofencesFromServer(-1L); } } } /** * Query the geofences from the server, based on the current last sync date. */ private void loadGeofencesFromServer(long lastSyncTimestamp) { HttpRequestCallback<JSONObject> cb = new HttpRequestCallback<JSONObject>() { @Override public void onSuccess(JSONObject result) { try { GeofenceList list = GeofencingJSONUtils.parseGeofences(result); List<PersistentGeofence> geofences = list.getGeofences(); if (!geofences.isEmpty()) { PersistentGeofence.saveInTx(geofences); setInitialLocation(); } if (!geofences.isEmpty() || !list.getDeletedGeofenceCodes().isEmpty()) { Intent broadcastIntent = new Intent(MFGeofenceEvent.ACTION_GEOFENCE_EVENT); broadcastIntent.setPackage(mContext.getPackageName()); MFGeofenceEvent.toIntent(broadcastIntent, MFGeofenceEvent.Type.SERVER_SYNC, geofences, list.getDeletedGeofenceCodes()); mContext.sendBroadcast(broadcastIntent); } log.debug("loadGeofences() got " + list.getGeofences().size() + " geofences"); } catch (Exception e) { HttpRequestError error = new HttpRequestError(-1, e, "error while parsing JSON"); log.debug(error.toString()); } } @Override public void onError(HttpRequestError error) { log.debug(error.toString()); } }; JSONPayloadRequest request = new JSONPayloadRequest(cb, HttpMethod.GET, null); request.setPath(String.format("%s", CONFIG_CONNECTOR_PATH)); if (lastSyncTimestamp >= 0L) { request.addParameter("updatedAfter", Long.toString(lastSyncTimestamp)); } mHttpService.executeRequest(request); } /** * Set the initial location upon starting the app and trigger the registration of geofences, if any, around this location. */ private void setInitialLocation() { Runnable r = new Runnable() { @Override public void run() { Location last = LocationServices.FusedLocationApi.getLastLocation(mGoogleApiClient); log.debug("setInitialLocation() last location = " + last); if (last != null) { new LocationUpdateReceiver(MFGeofencingManager.this).onLocationChanged(last, true); } } }; new Thread(r).start(); } /** * Converts a Google play services availability code into a displayable string. Used for debugging and tracing purposes. * @param availabilityCode the google api connection result to convert. * @return a readable string. */ private String getGoogleAvailabilityAsText(int availabilityCode) { switch (availabilityCode) { case ConnectionResult.SUCCESS: return "SUCCESS"; case ConnectionResult.SERVICE_MISSING: return "SERVICE_MISSING"; case ConnectionResult.SERVICE_UPDATING: return "SERVICE_UPDATING"; case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED: return "SERVICE_VERSION_UPDATE_REQUIRED"; case ConnectionResult.SERVICE_DISABLED: return "SERVICE_DISABLED"; case ConnectionResult.SERVICE_INVALID: return "SERVICE_INVALID"; default: return "undefined"; } } private void updateSettings() { mSettings.putString(ServiceConfig.SERVER_URL, mHttpService.getServerURL()) .putString(ServiceConfig.USERNAME, mHttpService.getUsername()) .putString(ServiceConfig.PASSWORD, mHttpService.getPassword()) .putInt(ServiceConfig.MAX_DISTANCE, mMaxDistance) .putInt(ServiceConfig.SERVER_SYNC_MIN_DELAY_HOURS, mIntervalBetweenDowloads).commit(); } /** * Retrieve the last used device descritpor, if any. If none exists, one is created from the {@link GeofencingDeviceInfo} API. * @return the device descriptor. */ String retrieveDeviceDescriptor() { String result = mSettings.getString(GeofencingDeviceInfo.DESCRIPTOR_KEY, null); if (result == null) { SharedPreferences prefs = mContext.getSharedPreferences(GeofencingDeviceInfo.SHARED_PREF, Context.MODE_PRIVATE); result = prefs.getString(GeofencingDeviceInfo.DESCRIPTOR_KEY, null); if (result == null) { GeofencingDeviceInfo info = new GeofencingDeviceInfo(mContext); result = info.getDescriptor(); prefs.edit().putString(GeofencingDeviceInfo.DESCRIPTOR_KEY, result).apply(); } mSettings.putString(GeofencingDeviceInfo.DESCRIPTOR_KEY, result).commit(); } return result; } }