package org.altbeacon.beacon.service; import android.content.Context; import org.altbeacon.beacon.Beacon; import org.altbeacon.beacon.MonitorNotifier; import org.altbeacon.beacon.Region; import org.altbeacon.beacon.logging.LogManager; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import static android.content.Context.MODE_PRIVATE; public class MonitoringStatus { private static volatile MonitoringStatus sInstance; private static final int MAX_REGIONS_FOR_STATUS_PRESERVATION = 50; private static final int MAX_STATUS_PRESERVATION_FILE_AGE_TO_RESTORE_SECS = 60 * 15; private static final String TAG = MonitoringStatus.class.getSimpleName(); public static final String STATUS_PRESERVATION_FILE_NAME = "org.altbeacon.beacon.service.monitoring_status_state"; private Map<Region, RegionMonitoringState> mRegionsStatesMap; private Context mContext; private boolean mStatePreservationIsOn = true; /** * Private lock object for singleton initialization protecting against denial-of-service attack. */ private static final Object SINGLETON_LOCK = new Object(); public static MonitoringStatus getInstanceForApplication(Context context) { /* * Follow double check pattern from Effective Java v2 Item 71. * * Bloch recommends using the local variable for this for performance reasons: * * > What this variable does is ensure that `field` is read only once in the common case * > where it's already initialized. While not strictly necessary, this may improve * > performance and is more elegant by the standards applied to low-level concurrent * > programming. On my machine, [this] is about 25 percent faster than the obvious * > version without a local variable. * * Joshua Bloch. Effective Java, Second Edition. Addison-Wesley, 2008. pages 283-284 */ MonitoringStatus instance = sInstance; if (instance == null) { synchronized (SINGLETON_LOCK) { instance = sInstance; if (instance == null) { sInstance = instance = new MonitoringStatus(context.getApplicationContext()); } } } return instance; } public MonitoringStatus(Context context) { this.mContext = context; } public synchronized void addRegion(Region region, Callback callback) { addLocalRegion(region, callback); saveMonitoringStatusIfOn(); } public synchronized void removeRegion(Region region) { removeLocalRegion(region); saveMonitoringStatusIfOn(); } public synchronized Set<Region> regions() { return getRegionsStateMap().keySet(); } public synchronized int regionsCount() { return regions().size(); } public synchronized RegionMonitoringState stateOf(Region region) { return getRegionsStateMap().get(region); } public synchronized void updateNewlyOutside() { Iterator<Region> monitoredRegionIterator = regions().iterator(); boolean needsMonitoringStateSaving = false; while (monitoredRegionIterator.hasNext()) { Region region = monitoredRegionIterator.next(); RegionMonitoringState state = stateOf(region); if (state.markOutsideIfExpired()) { needsMonitoringStateSaving = true; LogManager.d(TAG, "found a monitor that expired: %s", region); state.getCallback().call(mContext, "monitoringData", new MonitoringData(state.getInside(), region).toBundle()); } } if (needsMonitoringStateSaving) { saveMonitoringStatusIfOn(); } else { updateMonitoringStatusTime(System.currentTimeMillis()); } } public synchronized void updateNewlyInsideInRegionsContaining(Beacon beacon) { List<Region> matchingRegions = regionsMatchingTo(beacon); boolean needsMonitoringStateSaving = false; for(Region region : matchingRegions) { RegionMonitoringState state = getRegionsStateMap().get(region); if (state != null && state.markInside()) { needsMonitoringStateSaving = true; state.getCallback().call(mContext, "monitoringData", new MonitoringData(state.getInside(), region).toBundle()); } } if (needsMonitoringStateSaving) { saveMonitoringStatusIfOn(); } else { updateMonitoringStatusTime(System.currentTimeMillis()); } } private Map<Region, RegionMonitoringState> getRegionsStateMap() { if (mRegionsStatesMap == null) { restoreOrInitializeMonitoringStatus(); } return mRegionsStatesMap; } private void restoreOrInitializeMonitoringStatus() { long millisSinceLastMonitor = System.currentTimeMillis() - getLastMonitoringStatusUpdateTime(); mRegionsStatesMap = new HashMap<Region, RegionMonitoringState>(); if (!mStatePreservationIsOn) { LogManager.d(TAG, "Not restoring monitoring state because persistence is disabled"); } else if (millisSinceLastMonitor > MAX_STATUS_PRESERVATION_FILE_AGE_TO_RESTORE_SECS * 1000) { LogManager.d(TAG, "Not restoring monitoring state because it was recorded too many milliseconds ago: "+millisSinceLastMonitor); } else { restoreMonitoringStatus(); LogManager.d(TAG, "Done restoring monitoring status"); } } private List<Region> regionsMatchingTo(Beacon beacon) { List<Region> matched = new ArrayList<Region>(); for (Region region : regions()) { if (region.matchesBeacon(beacon)) { matched.add(region); } else { LogManager.d(TAG, "This region (%s) does not match beacon: %s", region, beacon); } } return matched; } protected void saveMonitoringStatusIfOn() { if(!mStatePreservationIsOn) return; LogManager.d(TAG, "saveMonitoringStatusIfOn()"); if (getRegionsStateMap().size() > MAX_REGIONS_FOR_STATUS_PRESERVATION) { LogManager.w(TAG, "Too many regions being monitored. Will not persist region state"); mContext.deleteFile(STATUS_PRESERVATION_FILE_NAME); } else { FileOutputStream outputStream = null; ObjectOutputStream objectOutputStream = null; try { outputStream = mContext.openFileOutput(STATUS_PRESERVATION_FILE_NAME, MODE_PRIVATE); objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(getRegionsStateMap()); } catch (IOException e) { LogManager.e(TAG, "Error while saving monitored region states to file. %s ", e.getMessage()); } finally { if (null != outputStream) { try { outputStream.close(); } catch (IOException ignored) { } } if (objectOutputStream != null) { try { objectOutputStream.close(); } catch (IOException ignored) { } } } } } protected void updateMonitoringStatusTime(long time) { File file = mContext.getFileStreamPath(STATUS_PRESERVATION_FILE_NAME); file.setLastModified(time); } protected long getLastMonitoringStatusUpdateTime() { File file = mContext.getFileStreamPath(STATUS_PRESERVATION_FILE_NAME); return file.lastModified(); } protected void restoreMonitoringStatus() { FileInputStream inputStream = null; ObjectInputStream objectInputStream = null; try { inputStream = mContext.openFileInput(STATUS_PRESERVATION_FILE_NAME); objectInputStream = new ObjectInputStream(inputStream); Map<Region, RegionMonitoringState> obj = (Map<Region, RegionMonitoringState>) objectInputStream.readObject(); LogManager.d(TAG, "Restored region monitoring state for "+obj.size()+" regions."); for (Region region : obj.keySet()) { LogManager.d(TAG, "Region "+region+" uniqueId: "+region.getUniqueId()+" state: "+obj.get(region)); } // RegionMonitoringState objects only get serialized to the status preservation file when they are first inside, // therefore, their {@link RegionMonitoringState#lastSeenTime will be when they were first "inside". // Mark all beacons that were inside again so they don't trigger as a new exit - enter. for (RegionMonitoringState regionMonitoringState : obj.values()) { if (regionMonitoringState.getInside()) { regionMonitoringState.markInside(); } } mRegionsStatesMap.putAll(obj); } catch (IOException | ClassNotFoundException | ClassCastException e) { if (e instanceof InvalidClassException) { LogManager.d(TAG, "Serialized Monitoring State has wrong class. Just ignoring saved state..." ); } else LogManager.e(TAG, "Deserialization exception, message: %s", e.getMessage()); } finally { if (null != inputStream) { try { inputStream.close(); } catch (IOException ignored) { } } if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException ignored) { } } } } /** * Client applications should not call directly. Call BeaconManager#setRegionStatePeristenceEnabled */ public synchronized void stopStatusPreservation() { mContext.deleteFile(STATUS_PRESERVATION_FILE_NAME); this.mStatePreservationIsOn = false; } /** * Client applications should not call directly. Call BeaconManager#setRegionStatePeristenceEnabled */ public synchronized void startStatusPreservation() { if (!this.mStatePreservationIsOn) { this.mStatePreservationIsOn = true; saveMonitoringStatusIfOn(); } } public boolean isStatePreservationOn() { return mStatePreservationIsOn; } public synchronized void clear() { mContext.deleteFile(STATUS_PRESERVATION_FILE_NAME); getRegionsStateMap().clear(); } public void updateLocalState(Region region, Integer state) { RegionMonitoringState internalState = getRegionsStateMap().get(region); if (internalState == null) { internalState = addLocalRegion(region); } if (state != null) { if (state == MonitorNotifier.OUTSIDE) { internalState.markOutside(); } if (state == MonitorNotifier.INSIDE) { internalState.markInside(); } } } public void removeLocalRegion(Region region) { getRegionsStateMap().remove(region); } public RegionMonitoringState addLocalRegion(Region region){ Callback dummyCallback = new Callback(null); return addLocalRegion(region, dummyCallback); } private RegionMonitoringState addLocalRegion(Region region, Callback callback){ if (getRegionsStateMap().containsKey(region)) { // if the region definition hasn't changed, becasue if it has, we need to clear state // otherwise a region with the same uniqueId can never be changed for (Region existingRegion : getRegionsStateMap().keySet()) { if (existingRegion.equals(region)) { if (existingRegion.hasSameIdentifiers(region)) { return getRegionsStateMap().get(existingRegion); } else { LogManager.d(TAG, "Replacing region with unique identifier "+region.getUniqueId()); LogManager.d(TAG, "Old definition: "+existingRegion); LogManager.d(TAG, "New definition: "+region); LogManager.d(TAG, "clearing state"); getRegionsStateMap().remove(region); break; } } } } RegionMonitoringState monitoringState = new RegionMonitoringState(callback); getRegionsStateMap().put(region, monitoringState); return monitoringState; } }