package org.altbeacon.beacon.service; import android.content.Context; import android.util.Log; import org.altbeacon.beacon.BeaconManager; import org.altbeacon.beacon.BeaconParser; 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.FileNotFoundException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import static android.content.Context.MODE_PRIVATE; /** * Stores the full state of scanning for the libary, including all settings so it can be ressurrected easily * for running from a scheduled job * * Created by dyoung on 3/26/17. * @hide */ public class ScanState implements Serializable { private static final String TAG = ScanState.class.getSimpleName(); private static final String STATUS_PRESERVATION_FILE_NAME = "android-beacon-library-scan-state"; private static final String TEMP_STATUS_PRESERVATION_FILE_NAME = "android-beacon-library-scan-state-temp"; public static int MIN_SCAN_JOB_INTERVAL_MILLIS = 300000; // 5 minutes private Map<Region, RangeState> mRangedRegionState = new HashMap<Region, RangeState>(); private transient MonitoringStatus mMonitoringStatus; private Set<BeaconParser> mBeaconParsers = new HashSet<BeaconParser>(); private ExtraDataBeaconTracker mExtraBeaconDataTracker = new ExtraDataBeaconTracker(); private long mForegroundBetweenScanPeriod; private long mBackgroundBetweenScanPeriod; private long mForegroundScanPeriod; private long mBackgroundScanPeriod; private boolean mBackgroundMode; private long mLastScanStartTimeMillis = 0l; private transient Context mContext; public Boolean getBackgroundMode() { return mBackgroundMode; } public void setBackgroundMode(Boolean backgroundMode) { mBackgroundMode = backgroundMode; } public Long getBackgroundBetweenScanPeriod() { return mBackgroundBetweenScanPeriod; } public void setBackgroundBetweenScanPeriod(Long backgroundBetweenScanPeriod) { mBackgroundBetweenScanPeriod = backgroundBetweenScanPeriod; } public Long getBackgroundScanPeriod() { return mBackgroundScanPeriod; } public void setBackgroundScanPeriod(Long backgroundScanPeriod) { mBackgroundScanPeriod = backgroundScanPeriod; } public Long getForegroundBetweenScanPeriod() { return mForegroundBetweenScanPeriod; } public void setForegroundBetweenScanPeriod(Long foregroundBetweenScanPeriod) { mForegroundBetweenScanPeriod = foregroundBetweenScanPeriod; } public Long getForegroundScanPeriod() { return mForegroundScanPeriod; } public void setForegroundScanPeriod(Long foregroundScanPeriod) { mForegroundScanPeriod = foregroundScanPeriod; } public ScanState(Context context) { mContext = context; } public MonitoringStatus getMonitoringStatus() { return mMonitoringStatus; } public void setMonitoringStatus(MonitoringStatus monitoringStatus) { mMonitoringStatus = monitoringStatus; } public Map<Region, RangeState> getRangedRegionState() { return mRangedRegionState; } public void setRangedRegionState(Map<Region, RangeState> rangedRegionState) { mRangedRegionState = rangedRegionState; } public ExtraDataBeaconTracker getExtraBeaconDataTracker() { return mExtraBeaconDataTracker; } public void setExtraBeaconDataTracker(ExtraDataBeaconTracker extraDataBeaconTracker) { mExtraBeaconDataTracker = extraDataBeaconTracker; } public Set<BeaconParser> getBeaconParsers() { return mBeaconParsers; } public void setBeaconParsers(Set<BeaconParser> beaconParsers) { mBeaconParsers = beaconParsers; } public long getLastScanStartTimeMillis() { return mLastScanStartTimeMillis; } public void setLastScanStartTimeMillis(long time) { mLastScanStartTimeMillis = time; } public static ScanState restore(Context context) { ScanState scanState = null; synchronized (ScanState.class) { FileInputStream inputStream = null; ObjectInputStream objectInputStream = null; try { inputStream = context.openFileInput(STATUS_PRESERVATION_FILE_NAME); objectInputStream = new ObjectInputStream(inputStream); scanState = (ScanState) objectInputStream.readObject(); scanState.mContext = context; } catch (FileNotFoundException fnfe) { LogManager.w(TAG, "Serialized ScanState does not exist. This may be normal on first run."); } catch (IOException | ClassNotFoundException | ClassCastException e) { if (e instanceof InvalidClassException) { LogManager.d(TAG, "Serialized ScanState has wrong class. Just ignoring saved state..."); } else { LogManager.e(TAG, "Deserialization exception"); Log.e(TAG, "error: ", e); } } finally { if (null != inputStream) { try { inputStream.close(); } catch (IOException ignored) { } } if (objectInputStream != null) { try { objectInputStream.close(); } catch (IOException ignored) { } } } if (scanState == null) { scanState = new ScanState(context); } if (scanState.mExtraBeaconDataTracker == null) { scanState.mExtraBeaconDataTracker = new ExtraDataBeaconTracker(); } scanState.mMonitoringStatus = MonitoringStatus.getInstanceForApplication(context); LogManager.d(TAG, "Scan state restore regions: monitored="+scanState.getMonitoringStatus().regions().size()+" ranged="+scanState.getRangedRegionState().keySet().size()); return scanState; } } public void save() { synchronized (ScanState.class) { // TODO: need to limit how big this object is somehow. // Impose limits on ranged and monitored regions? FileOutputStream outputStream = null; ObjectOutputStream objectOutputStream = null; try { outputStream = mContext.openFileOutput(TEMP_STATUS_PRESERVATION_FILE_NAME, MODE_PRIVATE); objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(this); } catch (IOException e) { LogManager.e(TAG, "Error while saving scan status to file: ", e.getMessage()); } finally { if (null != outputStream) { try { outputStream.close(); } catch (IOException ignored) { } } if (objectOutputStream != null) { try { objectOutputStream.close(); } catch (IOException ignored) { } } } File file = new File(mContext.getFilesDir(), STATUS_PRESERVATION_FILE_NAME); File tempFile = new File(mContext.getFilesDir(), TEMP_STATUS_PRESERVATION_FILE_NAME); LogManager.d(TAG, "Temp file is "+tempFile.getAbsolutePath()); LogManager.d(TAG, "Perm file is "+file.getAbsolutePath()); if (!file.delete()) { LogManager.e(TAG, "Error while saving scan status to file: Cannot delete existing file."); } if (!tempFile.renameTo(file)) { LogManager.e(TAG, "Error while saving scan status to file: Cannot rename temp file."); } mMonitoringStatus.saveMonitoringStatusIfOn(); } } public int getScanJobIntervalMillis() { long cyclePeriodMillis; if (getBackgroundMode()) { cyclePeriodMillis = getBackgroundScanPeriod()+getBackgroundBetweenScanPeriod(); } else { cyclePeriodMillis = getForegroundScanPeriod()+getForegroundBetweenScanPeriod(); } int scanJobIntervalMillis = MIN_SCAN_JOB_INTERVAL_MILLIS; if (cyclePeriodMillis > MIN_SCAN_JOB_INTERVAL_MILLIS) { scanJobIntervalMillis = (int) cyclePeriodMillis; } return scanJobIntervalMillis; } public int getScanJobRuntimeMillis() { long scanPeriodMillis; LogManager.d(TAG, "ScanState says background mode for ScanJob is "+getBackgroundMode()); if (getBackgroundMode()) { scanPeriodMillis = getBackgroundScanPeriod(); } else { scanPeriodMillis = getForegroundScanPeriod(); } if (!getBackgroundMode()) { // if we are in the foreground, we keep the scan job going for the minimum interval if (scanPeriodMillis < MIN_SCAN_JOB_INTERVAL_MILLIS) { return MIN_SCAN_JOB_INTERVAL_MILLIS; } } return (int) scanPeriodMillis; } public void applyChanges(BeaconManager beaconManager) { mBeaconParsers = new HashSet<>(beaconManager.getBeaconParsers()); mForegroundScanPeriod = beaconManager.getForegroundScanPeriod(); mForegroundBetweenScanPeriod = beaconManager.getForegroundBetweenScanPeriod(); mBackgroundScanPeriod = beaconManager.getBackgroundScanPeriod(); mBackgroundBetweenScanPeriod = beaconManager.getBackgroundBetweenScanPeriod(); mBackgroundMode = beaconManager.getBackgroundMode(); ArrayList<Region> existingMonitoredRegions = new ArrayList<>(mMonitoringStatus.regions()); ArrayList<Region> existingRangedRegions = new ArrayList<>(mRangedRegionState.keySet()); ArrayList<Region> newMonitoredRegions = new ArrayList<>(beaconManager.getMonitoredRegions()); ArrayList<Region> newRangedRegions = new ArrayList<>(beaconManager.getRangedRegions()); LogManager.d(TAG, "ranged regions: old="+existingRangedRegions.size()+" new="+newRangedRegions.size()); LogManager.d(TAG, "monitored regions: old="+existingMonitoredRegions.size()+" new="+newMonitoredRegions.size()); for (Region newRangedRegion: newRangedRegions) { if (!existingRangedRegions.contains(newRangedRegion)) { LogManager.d(TAG, "Starting ranging region: "+newRangedRegion); mRangedRegionState.put(newRangedRegion, new RangeState(new Callback(mContext.getPackageName()))); } } for (Region existingRangedRegion: existingRangedRegions) { if (!newRangedRegions.contains(existingRangedRegion)) { LogManager.d(TAG, "Stopping ranging region: "+existingRangedRegion); mRangedRegionState.remove(existingRangedRegion); } } LogManager.d(TAG, "Updated state with "+newRangedRegions.size()+" ranging regions and "+newMonitoredRegions.size()+" monitoring regions."); this.save(); } }