All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.codename1.location.GeofenceManager Maven / Gradle / Ivy

There is a newer version: 7.0.167
Show newest version
/*
 * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Codename One designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *  
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 * 
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 * 
 * Please contact Codename One through http://www.codenameone.com/ if you 
 * need additional information or have any questions.
 */
package com.codename1.location;

import com.codename1.io.Log;
import com.codename1.io.Storage;
import com.codename1.ui.Display;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A utility class to simplify Geofencing in Codename One.  Using this class to manage
 * an app's Geofences works around the 20-region limit on iOS and 100-region limit on Android, so that 
 * your app can monitor an unlimited number of Geofences simulataneously.
 * 
 * 

How it Works

*

GeofenceManager maintains a "bubble" region around the current device location. You may register * as many regions as you like to be monitored with GeofenceManager, but it will only register the regions * that intersect the current "bubble" region. When you exit the bubble region, the GeofenceManager will clear all of the * previously registered regions, create a new bubble, and then register only those regions that intersect this * new bubble.

* *

GeofenceManager uses {@link Storage} to maintain its own active list of regions.

* *

Limitations

*

GeofenceManager will only register 19 regions at a time, so if more than 19 regions intersect the current "bubble" * region, some of them won't make the cut. You can set the radius of the "bubble" region using {@link #setBubbleRadius(int) } * to increase or decrease the "bubble" region area, so that no regions are left behind.

* *

Although you can set any positive radius value you like, a typical Android or iOS device has a minimum effective * radius of about 100m.

* *

Note: If your app uses GeofenceManager, you shouldn't also add your own Geofences manually using {@link LocationManager#addGeoFencing(java.lang.Class, com.codename1.location.Geofence) } * as your manual regions may conflict.

* *

Usage

* * {@code * GeofenceManager mgr = GeofenceManager.getInstance(); * mgr.setListenerClass(MyGeofenceListener.class); * mgr.add(geofence1, geofence2, geofence3); * mgr.update(10000); * } * * And the MyGeofenceListener class should be an instance of Geofence. * *

Reloading Geofences Upon Exiting Bubble

* *

While there is no absolute limit on the number of regions that you can register in GeofenceManager * simulataneously, since it is actually storing the list of Geofences in Storage, there is a practical limit. E.g. * It probably wouldn't perform well if you stored several thousand at a time. If you want to monitor large quantifies * of regions (thousands, or millions), you can simply respond to the {@link GeofenceListener#onExit(java.lang.String) } event * for the "bubble" region, and "reload" the GeofenceManager with new regions related to the device's current location. * You might load the new locations from a web-service, for example. Use the {@link #isBubble(java.lang.String)} method * to check if the {@literal id} parameter is for the bubble region, and act accordintly.

* * * @author shannah */ public class GeofenceManager implements Iterable { //private GeoStreamerAsyncDataSource dataSource; private static final String STORAGE_KEY = "$AsyncGeoStreamer.geofences$"; private static final String ACTIVE_FENCES_KEY = "$AsyncGeoStreamer.activegeofences$"; private static final String CURRENT_ACTIVE_KEY = "$AsyncGeoStreamer.currentActive"; private static final String EXPIRATIONS_KEY = "$AsyncGeoStreamer.expirations"; private static final String LISTENER_CLASS_KEY = "$AsyncGeoStreamer.listenerClass"; private static final String BUBBLE_GEOFENCE_ID = "$AsyncGeoStreamer.bubble"; private static int MAX_ACTIVE_GEOFENCES=19; /** * The radius of the bubble region (in metres) */ private int bubbleRadius = 1000; /** * The bubble region expiraton time (duration). Default -1 means no expiration. */ private long bubbleExpiration = -1L; /** * The Class that should be instantiated to handle Geofence events. */ private Class listenerClass; /** * Maintains list of currently registered geofence IDs. Only the ones * that are actually currently registered with the OS. */ private List activeKeys; private Map expiryTimes; /** * Default timeout for getting location. */ private static final int defaultTimeout = 10000; /** * Map of all currently registered fences. */ private Map fences; private Map activeFences; private static GeofenceManager instance; private synchronized Map getExpiryTimes(boolean reload) { if (reload || expiryTimes == null) { try { expiryTimes = (Map) Storage.getInstance().readObject(EXPIRATIONS_KEY); } catch (Throwable t) {} if (expiryTimes == null) { expiryTimes = new HashMap(); } } return expiryTimes; } private synchronized void updateExpiryTimes(Geofence... geofences) { Map times = getExpiryTimes(false); long now = System.currentTimeMillis(); for (Geofence g : geofences) { if (g.getExpiration() <= 0) { times.put(g.getId(), -1L); } else { times.put(g.getId(), now + g.getExpiration()); } } Storage.getInstance().writeObject(EXPIRATIONS_KEY, times); } private synchronized void purgeExpired() { long now = System.currentTimeMillis(); Map times = getExpiryTimes(false); List expired = new ArrayList(); Map fences = getFences(false); List activeKeys = getActiveKeys(false); Map activeFences = getActiveFences(false); boolean saveFences = false; boolean saveActive = false; boolean saveActiveFences = false; for (Map.Entry time : times.entrySet()) { if (time.getValue() > 0L && time.getValue() < now) { times.remove(time.getKey()); if (!saveFences && fences.containsKey(time.getKey())) { saveFences = true; } fences.remove(time.getKey()); while (activeKeys.remove(time.getKey())) { saveActive = true; } if (activeFences.containsKey(time.getKey())) { activeFences.remove(time.getKey()); saveActiveFences = true; } } } Storage.getInstance().writeObject(EXPIRATIONS_KEY, times); if (saveFences) { saveFences(); } if (saveActive) { saveActiveKeys(); } if (saveActiveFences) { saveActiveFences(); } } /** * Obtains reference to the singleton GeofenceManager * @return */ public static GeofenceManager getInstance() { if (instance == null) { instance = new GeofenceManager(); } return instance; } /** * Gets the radius of the "bubble" region, in metres. * @return the bubbleRadius */ public int getBubbleRadius() { return bubbleRadius; } /** * Sets the radius of the "bubble" regin, in metres. Default value is {@literal 1000}. * @param bubbleRadius the bubbleRadius to set */ public void setBubbleRadius(int bubbleRadius) { this.bubbleRadius = bubbleRadius; } /** * Gets the expiration duration (in milliseconds) of the bubble region. * @return the bubbleExpiration */ public long getBubbleExpiration() { return bubbleExpiration; } /** * Sets the expiration duration (in milliseconds) of the bubble region. Default is {@literal -1} * meaning "No expiration". * @param bubbleExpiration the bubbleExpiration to set */ public void setBubbleExpiration(long bubbleExpiration) { this.bubbleExpiration = bubbleExpiration; } /** * The Listener class that is registered to receive Geofence events. This is * used internally by {@link GeofenceManager}. It is only public because * {@link GeofenceListener} classes must be public. * @deprecated For internal use only. */ public static class Listener implements GeofenceListener, LocationListener { @Override public void onExit(String id) { GeofenceManager.getInstance().onExit(id); } @Override public void onEntered(String id) { GeofenceManager.getInstance().onEntered(id); } @Override public void locationUpdated(Location location) { GeofenceManager.getInstance().locationUpdated(location); } @Override public void providerStateChanged(int newState) { //GeofenceManager.getInstance().providerStateChanged(newState); } } private GeofenceManager() { if ("and".equals(Display.getInstance().getPlatformName())) { MAX_ACTIVE_GEOFENCES = 99; } // On simulator we need to force refresh because the // actual geofence timers aren't persisted update(defaultTimeout, true); } /** * Checks if the given ID is for the "bubble" region. * @param id An ID to check. * @return True if {@literal id} is for the "bubble" region. */ public boolean isBubble(String id) { return BUBBLE_GEOFENCE_ID.equals(id); } /** * Gets the currently registered Listener class. * @return */ public synchronized Class getListenerClass() { if (listenerClass == null) { String className = (String)Storage.getInstance().readObject(LISTENER_CLASS_KEY); if (className != null) { try { listenerClass = (Class)Class.forName(className); } catch (Throwable t){ Log.e(t); } } } return listenerClass; } /** * Sets the GeofenceListener class that should receive Geofence events. * @param c */ public synchronized void setListenerClass(Class c) { listenerClass = (Class)c; if (c == null) { Storage.getInstance().deleteStorageFile(LISTENER_CLASS_KEY); } else { Storage.getInstance().writeObject(LISTENER_CLASS_KEY, c.getName()); } } /** * * @return */ private GeofenceListener getListener() { Class c = getListenerClass(); if (c != null) { try { return (GeofenceListener)c.newInstance(); } catch (Throwable t) { Log.e(t); } } return null; } /** * Adds a set of regions to be monitored by GeofenceManager. * @param geofence */ public synchronized void add(Geofence... geofence) { Map fences = getFences(false); for (Geofence f : geofence) { fences.put(f.getId(), f); } saveFences(); updateExpiryTimes(geofence); } /** * Adds a set of regions to be monitored by GeofenceManager. * @param geofences */ public synchronized void add(Collection geofences) { add(geofences.toArray(new Geofence[geofences.size()])); } private synchronized List getActiveKeys(boolean reload) { if (reload || activeKeys == null) { activeKeys = (List)Storage.getInstance().readObject(CURRENT_ACTIVE_KEY); if (activeKeys == null) { activeKeys = new ArrayList(); } } return activeKeys; } public synchronized boolean isCurrentlyActive(String id) { return getActiveKeys(false).contains(id); } private synchronized void saveActiveKeys() { Storage.getInstance().writeObject(CURRENT_ACTIVE_KEY, getActiveKeys(false)); } /** * Removes a set of regions (by ID) so that they will no longer be monitored. * @param ids */ public synchronized void remove(String... ids) { Map fences = getFences(false); for (String i : ids) { fences.remove(i); } saveFences(); } public synchronized void remove(Collection ids) { remove(ids.toArray(new String[ids.size()])); } /** * Removes all current regions. */ public synchronized void clear() { Map fences = getFences(false); fences.clear(); saveFences(); } /** * Checks the number of regions that are currently being monitored. * @return */ public synchronized int size() { return getFences(false).size(); } /** * Returns the Geofences as a Map. * @return */ public synchronized Map asMap() { return getFences(false); } /** * Returns the Geofences as a list. * @return */ public synchronized List asList() { return new ArrayList(getFences(false).values()); } /** * Returns all Geofences sorted by distance from the current location. * @return */ public synchronized List asSortedList() { List l = asList(); Location curr = LocationManager.getLocationManager().getLastKnownLocation(); if (curr != null) { Collections.sort(l, Geofence.createDistanceComparator(curr)); } return l; } /** * Reloads geofences from storage. */ public synchronized void refresh() { getFences(true); } private Geofence fromMap(Map m) { double lng = (Double)m.get("lng"); double lat = (Double)m.get("lat"); String id = (String)m.get("id"); int radius = (Integer)m.get("radius"); Long expiration = (Long)m.get("expiration"); if (expiration == null) { expiration = -1L; } Location l = new Location(); l.setLatitude(lat); l.setLongitude(lng); return new Geofence(id, l, radius, expiration); } private Map toMap(Geofence g) { double lng = g.getLoc().getLongitude(); double lat = g.getLoc().getLatitude(); int radius = g.getRadius(); String id = g.getId(); HashMap out = new HashMap(); out.put("lng", lng); out.put("lat", lat); out.put("radius", radius); out.put("id", id); out.put("expiration", g.getExpiration()); return out; } private synchronized Map getActiveFences(boolean reload) { if (reload || activeFences == null) { activeFences = new HashMap(); Map tmp = (Map)Storage.getInstance().readObject(ACTIVE_FENCES_KEY); if (tmp != null) { for (Map.Entry e : tmp.entrySet()) { activeFences.put(e.getKey(), fromMap(e.getValue())); } } } return activeFences; } private synchronized void saveActiveFences() { if (activeFences != null) { Map out = new HashMap(); for (Map.Entry f : activeFences.entrySet()) { out.put(f.getValue().getId(), toMap(f.getValue())); } Storage.getInstance().writeObject(ACTIVE_FENCES_KEY, out); } } private synchronized Map getFences(boolean reload) { if (reload || fences == null) { fences = new HashMap(); Map tmp = (Map)Storage.getInstance().readObject(STORAGE_KEY); if (tmp != null) { for (Map.Entry e : tmp.entrySet()) { fences.put(e.getKey(), fromMap(e.getValue())); } } } return fences; } private synchronized void saveFences() { if (fences != null) { Map out = new HashMap(); for (Map.Entry f : fences.entrySet()) { out.put(f.getValue().getId(), toMap(f.getValue())); } Storage.getInstance().writeObject(STORAGE_KEY, out); } } private boolean isWithinRadius(Location l1, Location l2, int radius) { return l1.getDistanceTo(l2) <= radius; } // Reference to the last bubble set. Geofence lastBubble; /** * Updates the active Geofences that are being monitored on the OS. This should be called * after making changes to the set of Geofences you wish to monitor. * @param timeout Timeout (in milliseconds) * */ public synchronized void update(int timeout) { update(timeout, false); } /** * Updates the active Geofences that are being monitored on the OS. This should be called * after making changes to the set of Geofences you wish to monitor. * @param timeout Timeout (in milliseconds) * @param forceRefresh If true, then this will force removal and re-addition of all geofences. */ public synchronized void update(int timeout, boolean forceRefresh) { Location here = LocationManager.getLocationManager().getCurrentLocationSync(timeout); if (here == null) { LocationManager.getLocationManager().setBackgroundLocationListener(Listener.class); return; } LocationManager.getLocationManager().setBackgroundLocationListener(null); List activeIds = new ArrayList(getActiveKeys(false)); List activeKeys = getActiveKeys(false); for (String id : activeIds) { Geofence g = getFences(false).get(id); Geofence cg = getActiveFences(false).get(id); if (!forceRefresh && g != null) { if (!isWithinRadius(g.getLoc(), here, getBubbleRadius() + g.getRadius())) { LocationManager.getLocationManager().removeGeoFencing(id); removeAll(activeKeys, id); activeFences.remove(id); } } else { LocationManager.getLocationManager().removeGeoFencing(id); removeAll(activeKeys, id); activeFences.remove(id); } } for (Geofence g : asSortedList()) { if (isWithinRadius(g.getLoc(), here, getBubbleRadius() + g.getRadius())) { if (activeKeys.size() >= MAX_ACTIVE_GEOFENCES) { // only allowed 20 at a time break; } Geofence ag = getActiveFences(false).get(g.getId()); if (forceRefresh || !activeKeys.contains(g.getId()) || !g.equals(ag)) { if (!activeKeys.contains(g.getId())) { activeKeys.add(g.getId()); } activeFences.put(g.getId(), g); LocationManager.getLocationManager().addGeoFencing(Listener.class, g); } } } saveActiveKeys(); saveActiveFences(); Location hereCopy = new Location(); hereCopy.setLatitude(here.getLatitude()); hereCopy.setLongitude(here.getLongitude()); Geofence bubble = new Geofence(BUBBLE_GEOFENCE_ID, hereCopy, getBubbleRadius(), getBubbleExpiration()); if (lastBubble == null || lastBubble.getLoc().getDistanceTo(bubble.getLoc()) > Math.min(100, bubbleRadius+1)) { lastBubble = bubble; LocationManager.getLocationManager().addGeoFencing(Listener.class, bubble); } purgeExpired(); } private void onExit(String id) { GeofenceListener l = getListener(); if (l != null) { l.onExit(id); } if (BUBBLE_GEOFENCE_ID.equals(id)) { // We are exiting our bubble update(defaultTimeout); } } private void removeAll(List l, Object o) { while (l.remove(o)); } private void onEntered(String id) { GeofenceListener l = getListener(); if (l != null) { l.onEntered(id); } } /** * Iterates over all geofences that are being monitored. * @return */ @Override public Iterator iterator() { return getFences(false).values().iterator(); } private void locationUpdated(Location location) { update(defaultTimeout); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy