content: 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 library, 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())));
              }
              else {
                  // In case the user has changed the definition, update it.
                  Region existingRegion = existingRangedRegions.get(existingRangedRegions.indexOf(newRangedRegion));
                  if (newRangedRegion.hasSameIdentifiers(existingRegion)) {
                      mRangedRegionState.remove(existingRegion);
                      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();
      }
  
  }