Skip to content

Commit

Permalink
Merge pull request #405 from AltBeacon/region-state-persistence-impro…
Browse files Browse the repository at this point in the history
…vements

Region state persistence improvements
  • Loading branch information
davidgyoung authored Jul 25, 2016
2 parents 6c63184 + 98c0e7f commit a997f7d
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 48 deletions.
43 changes: 43 additions & 0 deletions src/main/java/org/altbeacon/beacon/BeaconManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
import org.altbeacon.beacon.logging.LogManager;
import org.altbeacon.beacon.logging.Loggers;
import org.altbeacon.beacon.service.BeaconService;
import org.altbeacon.beacon.service.MonitoringStatus;
import org.altbeacon.beacon.service.RangeState;
import org.altbeacon.beacon.service.RangedBeacon;
import org.altbeacon.beacon.service.RegionMonitoringState;
import org.altbeacon.beacon.service.RunningAverageRssiFilter;
import org.altbeacon.beacon.service.StartRMData;
import org.altbeacon.beacon.service.scanner.NonBeaconLeScanCallback;
Expand Down Expand Up @@ -539,6 +541,44 @@ public void removeAllMonitorNotifiers(){
}
}

/**
* Turns off saving the state of monitored regions to persistent storage so it is retained
* over app restarts. Defaults to enabled. When enabled, there will not be an "extra" region
* entry event when the app starts up and a beacon for a monitored region was previously visible
* within the past 15 minutes. Note that there is a limit to 50 monitored regions that may be
* perisisted. If more than 50 regions are monitored, state is not persisted for any.
*
* @param enabled
*/
public void setRegionStatePeristenceEnabled(boolean enabled) {
if (enabled) {
MonitoringStatus.getInstanceForApplication(mContext).startStatusPreservation();
}
else {
MonitoringStatus.getInstanceForApplication(mContext).stopStatusPreservation();
}
}

/**
* Requests the current in/out state on the specified region. If the region is being monitored,
* this will cause an asynchronous callback on the `MonitorNotifier`'s `didDetermineStateForRegion`
* method. If it is not a monitored region, it will be ignored.
* @param region
*/
public void requestStateForRegion(Region region) {
MonitoringStatus status = MonitoringStatus.getInstanceForApplication(mContext);
RegionMonitoringState stateObj = status.stateOf(region);
int state = MonitorNotifier.OUTSIDE;
if (stateObj != null && stateObj.getInside()) {
state = MonitorNotifier.INSIDE;
}
synchronized (monitorNotifiers) {
for (MonitorNotifier notifier: monitorNotifiers) {
notifier.didDetermineStateForRegion(state, region);
}
}
}

/**
* Tells the <code>BeaconService</code> to start looking for beacons that match the passed
* <code>Region</code> object, and providing updates on the estimated mDistance every seconds while
Expand Down Expand Up @@ -623,11 +663,14 @@ public void startMonitoringBeaconsInRegion(Region region) throws RemoteException
if (serviceMessenger == null) {
throw new RemoteException("The BeaconManager is not bound to the service. Call beaconManager.bind(BeaconConsumer consumer) and wait for a callback to onBeaconServiceConnect()");
}
LogManager.d(TAG, "Starting monitoring region "+region+" with uniqueID: "+region.getUniqueId());
Message msg = Message.obtain(null, BeaconService.MSG_START_MONITORING, 0, 0);
StartRMData obj = new StartRMData(region, callbackPackageName(), this.getScanPeriod(), this.getBetweenScanPeriod(), this.mBackgroundMode);
msg.obj = obj;
serviceMessenger.send(msg);
synchronized (monitoredRegions) {
// If we are already tracking the state of this region, send a callback about it
this.requestStateForRegion(region);
monitoredRegions.add(region);
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/org/altbeacon/beacon/Region.java
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,31 @@ public boolean equals(Object other) {
return false;
}

public boolean hasSameIdentifiers(Region region) {
if (region.mIdentifiers.size() == this.mIdentifiers.size()) {
for (int i = 0 ; i < region.mIdentifiers.size(); i++) {

if (region.getIdentifier(i) == null && this.getIdentifier(i) != null) {
return false;
}
else if (region.getIdentifier(i) != null && this.getIdentifier(i) == null) {
return false;
}
else if (!(region.getIdentifier(i) == null && this.getIdentifier(i) == null)) {
if (!region.getIdentifier(i).equals(this.getIdentifier(i))) {
return false;
}
}
}
}
else {
return false;
}
return true;
}



public String toString() {
StringBuilder sb = new StringBuilder();
int i = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ public void onDestroy() {
LogManager.i(TAG, "onDestroy called. stopping scanning");
handler.removeCallbacksAndMessages(null);
mCycledScanner.stop();
monitoringStatus.stopStatusPreservationOnProcessDestruction();
monitoringStatus.stopStatusPreservation();
}

@Override
Expand Down
156 changes: 120 additions & 36 deletions src/main/java/org/altbeacon/beacon/service/MonitoringStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand All @@ -23,11 +24,12 @@

public class MonitoringStatus {
private static 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 final Map<Region, RegionMonitoringState> mRegionsStatesMap
= new HashMap<Region, RegionMonitoringState>();
private Map<Region, RegionMonitoringState> mRegionsStatesMap;

private Context mContext;

Expand All @@ -46,30 +48,47 @@ public static MonitoringStatus getInstanceForApplication(Context context) {

public MonitoringStatus(Context context) {
this.mContext = context;
restoreMonitoringStatus();
}

public synchronized void addRegion(Region region) {
if (mRegionsStatesMap.containsKey(region)) return;
mRegionsStatesMap.put(region, new RegionMonitoringState(new Callback(mContext.getPackageName())));
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;
}
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;
}
}
}
}
getRegionsStateMap().put(region, new RegionMonitoringState(new Callback(mContext.getPackageName())));
saveMonitoringStatusIfOn();
}

public synchronized void removeRegion(Region region) {
mRegionsStatesMap.remove(region);
getRegionsStateMap().remove(region);
saveMonitoringStatusIfOn();
}

public synchronized Set<Region> regions() {
return mRegionsStatesMap.keySet();
return getRegionsStateMap().keySet();
}

public synchronized int regionsCount() {
return regions().size();
}

public synchronized RegionMonitoringState stateOf(Region region) {
return mRegionsStatesMap.get(region);
return getRegionsStateMap().get(region);
}

public synchronized void updateNewlyOutside() {
Expand All @@ -78,27 +97,59 @@ public synchronized void updateNewlyOutside() {
while (monitoredRegionIterator.hasNext()) {
Region region = monitoredRegionIterator.next();
RegionMonitoringState state = stateOf(region);
if (state.isNewlyOutside()) {
if (state.markOutsideIfExpired()) {
needsMonitoringStateSaving = true;
LogManager.d(TAG, "found a monitor that expired: %s", region);
state.getCallback().call(mContext, "monitoringData", new MonitoringData(state.isInside(), region));
state.getCallback().call(mContext, "monitoringData", new MonitoringData(state.getInside(), region));
}
}
if (needsMonitoringStateSaving) saveMonitoringStatusIfOn();
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 = mRegionsStatesMap.get(region);
RegionMonitoringState state = getRegionsStateMap().get(region);
if (state != null && state.markInside()) {
needsMonitoringStateSaving = true;
state.getCallback().call(mContext, "monitoringData",
new MonitoringData(state.isInside(), region));
new MonitoringData(state.getInside(), region));
}
}
if (needsMonitoringStateSaving) saveMonitoringStatusIfOn();
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) {
Expand All @@ -113,41 +164,61 @@ private List<Region> regionsMatchingTo(Beacon beacon) {
return matched;
}

private void saveMonitoringStatusIfOn() {
protected void saveMonitoringStatusIfOn() {
if(!mStatePreservationIsOn) return;
LogManager.d(TAG, "saveMonitoringStatusIfOn()");
FileOutputStream outputStream = null;
ObjectOutputStream objectOutputStream = null;
try {
outputStream = mContext.openFileOutput(STATUS_PRESERVATION_FILE_NAME, MODE_PRIVATE);
objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(mRegionsStatesMap);
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) {
} 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) {
if (objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (IOException ignored) {
}
}
}
}
}

private void restoreMonitoringStatus() {
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));
}
mRegionsStatesMap.putAll(obj);

} catch (IOException | ClassNotFoundException | ClassCastException e) {
Expand All @@ -170,13 +241,26 @@ private void restoreMonitoringStatus() {
}
}

public synchronized void stopStatusPreservationOnProcessDestruction() {
/**
* 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 synchronized void clear() {
mContext.deleteFile(STATUS_PRESERVATION_FILE_NAME);
mRegionsStatesMap.clear();
getRegionsStateMap().clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ public boolean markInside() {
}
return false;
}
public boolean isNewlyOutside() { //FIXME oh my god, it changes state of object :O

public boolean markOutsideIfExpired() {
if (inside) {
if (lastSeenTime > 0 && SystemClock.elapsedRealtime() - lastSeenTime > BeaconManager.getRegionExitPeriod()) {
inside = false;
Expand All @@ -67,12 +68,8 @@ public boolean isNewlyOutside() { //FIXME oh my god, it changes state of object
}
return false;
}
public boolean isInside() { //FIXME it also can change state through isNewlyOutside()
if (inside) {
if (!isNewlyOutside()) {
return true;
}
}
return false;

public boolean getInside() {
return inside;
}
}
Loading

0 comments on commit a997f7d

Please sign in to comment.