com.paywith.ibeacon.service.IBeaconService.java Source code

Java tutorial

Introduction

Here is the source code for com.paywith.ibeacon.service.IBeaconService.java

Source

/**
 * Radius Networks, Inc.
 * http://www.radiusnetworks.com
 *
 * @author David G. Young
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.paywith.ibeacon.service;

import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.PowerManager;
import android.provider.ContactsContract;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.util.Log;
import android.view.WindowManager;

import com.paywith.ibeacon.R;
import com.paywith.bluetooth.BluetoothCrashResolver;
import com.paywith.ibeacon.IBeacon;
import com.paywith.ibeacon.IBeaconManager;
import com.paywith.ibeacon.Region;
//import com.paywith.ibeacon.BuildConfig;

import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * @author dyoung
 */

@TargetApi(5)
public class IBeaconService extends Service {
    public static final String TAG = "IBeaconService";

    private Map<Region, RangeState> rangedRegionState = new HashMap<Region, RangeState>();
    private Map<Region, MonitorState> monitoredRegionState = new HashMap<Region, MonitorState>();
    private BluetoothAdapter bluetoothAdapter;
    private boolean scanning;
    private boolean scanningPaused;
    private Date lastIBeaconDetectionTime = new Date();
    private HashSet<IBeacon> trackedBeacons;
    int trackedBeaconsPacketCount;
    private Handler handler = new Handler();
    private int bindCount = 0;
    private BluetoothCrashResolver bluetoothCrashResolver;
    private boolean scanCyclerStarted = false;
    private boolean scanningEnabled = false;

    private static final String PREFERENCES = "com.paywith.paywith";
    private Context context;
    private SharedPreferences settings;// = context.getSharedPreferences(PREFERENCES, 0);
    private SharedPreferences.Editor editor;

    private IBeacon lastBeaconFound = null;
    private IBeacon lastBeaconFound2 = null;

    private Integer old_locid = 0;
    private Integer old_locid2 = 0;

    private Integer lastlocationid = 0;
    private String lastaction = "";

    private Integer noPaywithBeaconCount = 0;

    /*
     * The scan period is how long we wait between restarting the BLE advertisement scans
     * Each time we restart we only see the unique advertisements once (e.g. unique iBeacons)
     * So if we want updates, we have to restart.  iOS gets updates once per second, so ideally we
     * would restart scanning that often to get the same update rate.  The trouble is that when you 
     * restart scanning, it is not instantaneous, and you lose any iBeacon packets that were in the 
     * air during the restart.  So the more frequently you restart, the more packets you lose.  The
     * frequency is therefore a tradeoff.  Testing with 14 iBeacons, transmitting once per second,
     * here are the counts I got for various values of the SCAN_PERIOD:
     * 
     * Scan period     Avg iBeacons      % missed
     *    1s               6                 57
     *    2s               10                29
     *    3s               12                14
     *    5s               14                0
     *    
     * Also, because iBeacons transmit once per second, the scan period should not be an even multiple
     * of seconds, because then it may always miss a beacon that is synchronized with when it is stopping
     * scanning.
     * 
     */

    private long scanPeriod = IBeaconManager.DEFAULT_FOREGROUND_SCAN_PERIOD;
    private long betweenScanPeriod = IBeaconManager.DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD;

    private List<IBeacon> simulatedScanData = null;

    // don kelley addition for running service in background and talking to paywith app:

    private BroadcastReceiver mReceiver;

    /**
     * Class used for the client Binder.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with IPC.
     */
    public class IBeaconBinder extends Binder {
        public IBeaconService getService() {
            Log.i(TAG, "getService of IBeaconBinder called");
            // Return this instance of LocalService so clients can call public methods
            return IBeaconService.this;
        }
    }

    /**
     * Command to the service to display a message
     */
    public static final int MSG_START_RANGING = 2;
    public static final int MSG_STOP_RANGING = 3;
    public static final int MSG_START_MONITORING = 4;
    public static final int MSG_STOP_MONITORING = 5;
    public static final int MSG_SET_SCAN_PERIODS = 6;
    public static final int MSG_SET_DO_LOCK = 7;
    public static final int MSG_SET_DO_UNLOCK = 8;
    public static final int MSG_START_SCANNING = 9;
    public static final int MSG_STOP_SCANNING = 10;

    Map<String, String> apiresult = null;
    public PayWithAPI apiInstance;// = new PayWithAPI(this.getBaseContext());

    static class IncomingHandler extends Handler {
        private final WeakReference<IBeaconService> mService;

        IncomingHandler(IBeaconService service) {
            mService = new WeakReference<IBeaconService>(service);
        }

        @Override
        public void handleMessage(Message msg) {
            IBeaconService service = mService.get();
            StartRMData startRMData = (StartRMData) msg.obj;

            if (service != null) {
                switch (msg.what) {
                case MSG_START_RANGING:
                    Log.i(TAG, "start ranging received");
                    service.startRangingBeaconsInRegion(startRMData.getRegionData(),
                            new com.paywith.ibeacon.service.Callback(startRMData.getCallbackPackageName()));
                    service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod());
                    break;
                case MSG_STOP_RANGING:
                    Log.i(TAG, "stop ranging received");
                    service.stopRangingBeaconsInRegion(startRMData.getRegionData());
                    service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod());
                    break;
                case MSG_START_MONITORING:
                    Log.i(TAG, "start monitoring received");
                    service.startMonitoringBeaconsInRegion(startRMData.getRegionData(),
                            new com.paywith.ibeacon.service.Callback(startRMData.getCallbackPackageName()));
                    service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod());
                    break;
                case MSG_STOP_MONITORING:
                    Log.i(TAG, "stop monitoring received");
                    service.stopMonitoringBeaconsInRegion(startRMData.getRegionData());
                    service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod());
                    break;
                case MSG_SET_SCAN_PERIODS:
                    Log.i(TAG, "set scan intervals received");
                    service.setScanPeriods(startRMData.getScanPeriod(), startRMData.getBetweenScanPeriod());
                    break;
                case MSG_SET_DO_LOCK:
                    Log.i(TAG, "do locks");
                    getLock(service.getBaseContext());
                    break;
                case MSG_SET_DO_UNLOCK:
                    Log.i(TAG, "do locks");
                    PowerManager.WakeLock lock = getLock(service.getBaseContext());

                    if (lock.isHeld())
                        lock.release();
                    break;
                case MSG_START_SCANNING:
                    Log.i(TAG, "start scanning received");
                    service.startScanning(
                            new com.paywith.ibeacon.service.Callback(startRMData.getCallbackPackageName()));
                    break;
                case MSG_STOP_SCANNING:
                    Log.i(TAG, "stop scanning received");
                    service.stopScanning(
                            new com.paywith.ibeacon.service.Callback(startRMData.getCallbackPackageName()));
                    break;
                default:
                    super.handleMessage(msg);
                }
            }
        }
    }

    /**
     * Target we publish for clients to send messages to IncomingHandler.
     */
    final Messenger mMessenger = new Messenger(new IncomingHandler(this));

    /**
     * When binding to the service, we return an interface to our messenger
     * for sending messages to the service.
     */
    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "binding");
        bindCount++;
        return mMessenger.getBinder();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.i(TAG, "unbinding");
        bindCount--;
        return false;
    }

    @Override
    public void onCreate() {
        //Log.i(TAG, "iBeaconService version "+ BuildConfig.VERSION_NAME+" is starting up");
        getBluetoothAdapter();
        bluetoothCrashResolver = new BluetoothCrashResolver(this);
        bluetoothCrashResolver.start();

        setupApiConnection();

        // Look for simulated scan data
        try {
            Class klass = Class.forName("com.paywith.ibeacon.SimulatedScanData");
            java.lang.reflect.Field f = klass.getField("iBeacons");
            this.simulatedScanData = (List<IBeacon>) f.get(null);
        } catch (ClassNotFoundException e) {
            if (IBeaconManager.debug)
                Log.d(TAG, "No com.paywith.ibeacon.SimulatedScanData class exists.");
        } catch (Exception e) {
            Log.e(TAG,
                    "Cannot get simulated Scan data.  Make sure your com.paywith.ibeacon.SimulatedScanData class defines a field with the signature 'public static List<IBeacon> iBeacons'",
                    e);
        }

        IntentFilter theFilter = new IntentFilter();
        theFilter.addAction(Intent.ACTION_SCREEN_OFF);
        theFilter.addAction(Intent.ACTION_SCREEN_ON);
        this.context = this.getBaseContext();
        settings = context.getSharedPreferences(PREFERENCES, 0);
        editor = settings.edit();
    }

    private void setupApiConnection() {
        // this needs to happen not only during onCreate but also when restarting a beacon searching routine after logging in
        apiInstance = new PayWithAPI(this.getBaseContext());
        //PayWithAPI apiInstance = new PayWithAPI(this.getBaseContext());
        apiInstance.setCurrentLaunchUrl("");
    }

    @Override
    @TargetApi(18)
    public void onDestroy() {
        Log.e("service", "onDestroy()");
        if (android.os.Build.VERSION.SDK_INT < 18) {
            Log.w(TAG, "Not supported prior to API 18.");
            return;
        }
        setNextLaunchUrl(null);
        bluetoothCrashResolver.stop();
        Log.i(TAG, "onDestroy called.  stopping scanning");
        handler.removeCallbacksAndMessages(null);
        scanLeDevice(false);
        if (bluetoothAdapter != null) {
            bluetoothAdapter.stopLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback());
            lastScanEndTime = new Date().getTime();
        }
        // remove any remaining system notifications that this service has generated
        NotificationManager notificationManager = (NotificationManager) getSystemService(
                Context.NOTIFICATION_SERVICE);
        notificationManager.cancelAll();

    }

    private int ongoing_notification_id = 1;

    /* 
     * Returns true if the service is running, but all bound clients have indicated they are in the background
     */
    private boolean isInBackground() {
        if (IBeaconManager.debug)
            Log.d(TAG, "bound client count:" + bindCount);
        return bindCount == 0;
    }

    public Boolean getUserLoggedIn() {
        // Don Kelley, Aug 2014
        return settings.getBoolean("userLoggedIn", false);
    }

    public void setNextLaunchUrl(String mLaunchURL) {
        // Don Kelley, August 2014
        editor.putString("nextLaunchUrl", mLaunchURL);
        editor.commit();
    }

    public void setNextLaunchName(String mMerchantName) {
        // Don Kelley, August 2014
        editor.putString("MerchantName", mMerchantName);
        editor.commit();
    }

    /**
     * methods for clients
     */

    public void startScanning(Callback callback) {
        setupApiConnection();
        if (getUserLoggedIn()) {
            enableScanning();
        }
    }

    public void stopScanning(Callback callback) {
        disableScanning();
    }

    public void startRangingBeaconsInRegion(Region region, Callback callback) {
        //Log.e("Service","startRangingBeaconsInRegion()");
        synchronized (rangedRegionState) {
            if (rangedRegionState.containsKey(region)) {
                Log.i(TAG, "Already ranging that region -- will replace existing region.");
                rangedRegionState.remove(region); // need to remove it, otherwise the old object will be retained because they are .equal
            }
            rangedRegionState.put(region, new RangeState(callback));
        }
        if (IBeaconManager.debug)
            Log.d(TAG, "Currently ranging " + rangedRegionState.size() + " regions.");
        if (!scanningEnabled && getUserLoggedIn()) {
            enableScanning();
        }
    }

    public void stopRangingBeaconsInRegion(Region region) {
        synchronized (rangedRegionState) {
            rangedRegionState.remove(region);
        }
        if (IBeaconManager.debug)
            Log.d(TAG, "Currently ranging " + rangedRegionState.size() + " regions.");

        if (scanningEnabled && rangedRegionState.size() == 0 && monitoredRegionState.size() == 0) {
            disableScanning();
        }
    }

    // make background service persistent (auto restarts if killed off)???
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("Service", "onStartCommand");
        return android.app.Service.START_STICKY;
    }

    public void startMonitoringBeaconsInRegion(Region region, Callback callback) {
        if (IBeaconManager.debug)
            Log.d(TAG, "startMonitoring called");

        synchronized (monitoredRegionState) {
            if (monitoredRegionState.containsKey(region)) {
                Log.i(TAG, "Already monitoring that region -- will replace existing region monitor.");
                monitoredRegionState.remove(region); // need to remove it, otherwise the old object will be retained because they are .equal
            }
            monitoredRegionState.put(region, new MonitorState(callback));
        }
        if (IBeaconManager.debug)
            Log.d(TAG, "Currently monitoring " + monitoredRegionState.size() + " regions.");
        if (!scanningEnabled) {
            enableScanning();
        }
    }

    public void stopMonitoringBeaconsInRegion(Region region) {
        if (IBeaconManager.debug)
            Log.d(TAG, "stopMonitoring called");
        synchronized (monitoredRegionState) {
            monitoredRegionState.remove(region);
        }
        if (IBeaconManager.debug)
            Log.d(TAG, "Currently monitoring " + monitoredRegionState.size() + " regions.");
        if (scanningEnabled && rangedRegionState.size() == 0 && monitoredRegionState.size() == 0) {
            disableScanning();
        }
    }

    public void setScanPeriods(long scanPeriod, long betweenScanPeriod) {
        this.scanPeriod = scanPeriod;
        this.betweenScanPeriod = betweenScanPeriod;
        long now = new Date().getTime();
        if (nextScanStartTime > now) {
            // We are waiting to start scanning.  We may need to adjust the next start time
            // only do an adjustment if we need to make it happen sooner.  Otherwise, it will
            // take effect on the next cycle.
            long proposedNextScanStartTime = (lastScanEndTime + betweenScanPeriod);
            if (proposedNextScanStartTime < nextScanStartTime) {
                nextScanStartTime = proposedNextScanStartTime;
                Log.i(TAG, "Adjusted nextScanStartTime to be " + new Date(nextScanStartTime));
            }
        }
        if (scanStopTime > now) {
            // we are waiting to stop scanning.  We may need to adjust the stop time
            // only do an adjustment if we need to make it happen sooner.  Otherwise, it will
            // take effect on the next cycle.
            long proposedScanStopTime = (lastScanStartTime + scanPeriod);
            if (proposedScanStopTime < scanStopTime) {
                scanStopTime = proposedScanStopTime;
                Log.i(TAG, "Adjusted scanStopTime to be " + new Date(scanStopTime));
            }
        }
    }

    private long lastScanStartTime = 0l;
    private long lastScanEndTime = 0l;
    private long nextScanStartTime = 0l;
    private long scanStopTime = 0l;

    public void enableScanning() {
        scanningEnabled = true;
        /*
        get lock
                PowerManager.WakeLock lock=getLock(this.getApplicationContext());
            
                if (!lock.isHeld())
        lock.acquire();
        */
        if (!scanCyclerStarted)
            scanLeDevice(true);
    }

    public void disableScanning() {
        scanningEnabled = false;
    }

    @TargetApi(18)
    private void scanLeDevice(final Boolean enable) {
        //Log.e("Service","scanLeDevice()");
        scanCyclerStarted = true;
        if (android.os.Build.VERSION.SDK_INT < 18) {
            Log.w(TAG, "Not supported prior to API 18.");
            return;
        }
        if (getBluetoothAdapter() == null) {
            Log.e(TAG, "No bluetooth adapter.  iBeaconService cannot scan.");
            Log.w(TAG, "exiting");
            return;
        }
        if (enable) {
            long millisecondsUntilStart = nextScanStartTime - (new Date().getTime());
            if (millisecondsUntilStart > 0) {
                if (IBeaconManager.debug)
                    Log.d(TAG, "Waiting to start next bluetooth scan for another " + millisecondsUntilStart
                            + " milliseconds");
                // Don't actually wait until the next scan time -- only wait up to 1 second.  this
                // allows us to start scanning sooner if a consumer enters the foreground and expects
                // results more quickly
                // NOTE: Don Kelley - changed back to NOT use the 1 second override.  I hate that...
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        //Log.e("Service","calling scanLeDevice(true)");
                        scanLeDevice(true);
                    }
                    //}, millisecondsUntilStart > 1000 ? 1000 : millisecondsUntilStart); // 1 second max override
                }, millisecondsUntilStart);
                return;
            }

            trackedBeacons = new HashSet<IBeacon>();
            trackedBeaconsPacketCount = 0;
            if (scanning == false || scanningPaused == true) {
                scanning = true;
                scanningPaused = false;
                try {
                    if (getBluetoothAdapter() != null) {
                        if (getBluetoothAdapter().isEnabled()) {
                            if (bluetoothCrashResolver.isRecoveryInProgress()) {
                                Log.e(TAG, "Skipping scan because crash recovery is in progress.");
                            } else {
                                if (scanningEnabled) {
                                    getBluetoothAdapter()
                                            .startLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback());
                                } else {
                                    if (IBeaconManager.debug)
                                        Log.d(TAG, "Scanning unnecessary - no monitoring or ranging active.");
                                }
                            }
                            lastScanStartTime = new Date().getTime();
                        } else {
                            Log.e(TAG, "Bluetooth is disabled.  Cannot scan for iBeacons.");
                            //scheduleScanStop();
                            disableScanning();
                        }
                    }
                } catch (Exception e) {
                    Log.e("Service",
                            "Exception starting bluetooth scan.  Perhaps bluetooth is disabled or unavailable?");
                }
            } else {
                if (IBeaconManager.debug)
                    Log.d(TAG, "We are already scanning");
            }
            scanStopTime = (new Date().getTime() + scanPeriod);
            scheduleScanStop();

            if (IBeaconManager.debug)
                Log.d(TAG, "Scan started");
        } else {
            if (IBeaconManager.debug)
                Log.d(TAG, "disabling scan");
            scanning = false;
            if (getBluetoothAdapter() != null) {
                getBluetoothAdapter().stopLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback());
                lastScanEndTime = new Date().getTime();
            }
        }
    }

    private void scheduleScanStop() {
        // Stops scanning after a pre-defined scan period.
        long millisecondsUntilStop = scanStopTime - (new Date().getTime());
        if (millisecondsUntilStop > 0) {
            if (IBeaconManager.debug)
                Log.d(TAG, "Waiting to stop scan for another " + millisecondsUntilStop + " milliseconds");
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    scheduleScanStop();
                }
            }, millisecondsUntilStop > 1000 ? 1000 : millisecondsUntilStop);
        } else {
            finishScanCycle();
        }
    }

    @TargetApi(18)
    private void finishScanCycle() {
        if (android.os.Build.VERSION.SDK_INT < 18) {
            Log.w(TAG, "Not supported prior to API 18.");
            return;
        }
        if (IBeaconManager.debug)
            Log.d(TAG, "Done with scan cycle");
        processExpiredMonitors();
        if (scanning == true) {
            processRangeData();
            // If we want to use simulated scanning data, do it here.  This is used for testing in an emulator
            if (simulatedScanData != null) {
                // if simulatedScanData is provided, it will be seen every scan cycle.  *in addition* to anything actually seen in the air
                // it will not be used if we are not in debug mode
                Log.w(TAG,
                        "Simulated scan data is deprecated and will be removed in a future release. Please use the new BeaconSimulator interface instead.");

                if (0 != (getApplicationInfo().flags &= ApplicationInfo.FLAG_DEBUGGABLE)) {
                    for (IBeacon iBeacon : simulatedScanData) {
                        processIBeaconFromScan(iBeacon);
                    }
                } else {
                    Log.w(TAG,
                            "Simulated scan data provided, but ignored because we are not running in debug mode.  Please remove simulated scan data for production.");
                }
            }

            if (getBluetoothAdapter() != null) {
                if (getBluetoothAdapter().isEnabled()) {
                    getBluetoothAdapter().stopLeScan((BluetoothAdapter.LeScanCallback) getLeScanCallback());
                    lastScanEndTime = new Date().getTime();
                } else {
                    Log.w(TAG, "Bluetooth is disabled.  Cannot scan for iBeacons.");
                }
            }

            if (!anyRangingOrMonitoringRegionsActive()) {
                if (IBeaconManager.debug)
                    Log.d(TAG, "Not starting scan because no monitoring or ranging regions are defined.");
                scanCyclerStarted = false;
            } else {
                if (IBeaconManager.debug)
                    Log.d(TAG, "Restarting scan.  Unique beacons seen last cycle: " + trackedBeacons.size()
                            + " Total iBeacon advertisement packets seen: " + trackedBeaconsPacketCount);

                scanningPaused = true;
                nextScanStartTime = (new Date().getTime() + betweenScanPeriod);
                if (scanningEnabled) {
                    scanLeDevice(true);
                } else {
                    if (IBeaconManager.debug)
                        Log.d(TAG, "Scanning disabled.  No ranging or monitoring regions are active.");
                    scanCyclerStarted = false;
                }
            }
        }
    }

    private Object leScanCallback;

    @TargetApi(18)
    private Object getLeScanCallback() {
        if (leScanCallback == null) {
            leScanCallback = new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
                    //Log.e("bt","got record");
                    // DEBUG NOTE FOR BLUETOOTH PROBLEM (Don Kelley August 2014):
                    // IF THIS NEVER HAPPENS (at least around a real bt broadcasting device), SOMETHING IS WRONG.
                    if (IBeaconManager.debug)
                        Log.d(TAG, "got record");
                    new ScanProcessor().execute(new ScanData(device, rssi, scanRecord));
                }
            };
        } else {
            //Log.e("bt","NOT got record");
            //Log.e("bt",leScanCallback.toString());
        }

        return leScanCallback;
    }

    private class ScanData {
        public ScanData(BluetoothDevice device, int rssi, byte[] scanRecord) {
            this.device = device;
            this.rssi = rssi;
            this.scanRecord = scanRecord;
        }

        @SuppressWarnings("unused")
        public BluetoothDevice device;
        public int rssi;
        public byte[] scanRecord;
    }

    private void processRangeData() {
        // Don Kelley August 2014 (based loosely on original method from Radius)
        // This is the method that does most of the custom paywith magical logic.

        // always check if user logged in before continuing
        if (!getUserLoggedIn()) {
            Log.e("processRangeData() ", "user logged out");
            disableScanning();
            return;
        }
        Iterator<Region> regionIterator = rangedRegionState.keySet().iterator();
        while (regionIterator.hasNext()) {
            Region region = regionIterator.next();
            RangeState rangeState = rangedRegionState.get(region);
            if (IBeaconManager.debug)
                Log.d(TAG, "Calling ranging callback");
            //rangeState.getCallback().call(IBeaconService.this, "rangingData", new RangingData(rangeState.finalizeIBeacons(), region));

            //Log.e("iterator",trackedBeacons);
            IBeacon nearest_beacon = findNearest(trackedBeacons);
            Boolean beacon_changed = false;

            if (nearest_beacon == null) {
                //Log.e("nearest_beacon() result","no beacon found");
                // if no paywith beacon nearby, remove any of our notifications.
                // Need this to only happen after several iterations of no beacon found.... 
                noPaywithBeaconCount = noPaywithBeaconCount + 1; // inc the counter and remove notification if higher than 10 cycles

                if (noPaywithBeaconCount > 3) {
                    // clear all paywith notifications and launch url... nothing has been found for 10 cycles
                    setNextLaunchUrl(null);
                    NotificationManager notificationManager = (NotificationManager) getSystemService(
                            Context.NOTIFICATION_SERVICE);
                    notificationManager.cancelAll();
                    old_locid = 0;
                    old_locid2 = 0;
                    lastBeaconFound = null;
                    lastBeaconFound2 = null;
                    lastaction = "cancelAll";
                }
                return;
            }
            noPaywithBeaconCount = 0; // always reset the sequential nobeaconsfound counter once a nearby paywith beacon is found

            Integer m1 = nearest_beacon.getMinor();
            Integer M1 = nearest_beacon.getMajor();
            Integer m2 = 0; // just some safe default settings
            Integer M2 = 0;
            Integer m3 = 0;
            Integer M3 = 0;
            if (lastBeaconFound != null) {
                m2 = lastBeaconFound.getMinor();
                M2 = lastBeaconFound.getMajor();
            }

            if (lastBeaconFound2 != null) {
                m3 = lastBeaconFound2.getMinor();
                M3 = lastBeaconFound2.getMajor();
            }

            //Log.e("service",m1.toString() + " = " + m2.toString() + " = " + m3.toString() + ", " + M1.toString() + " = " + M2.toString() + " = " + M3.toString());
            lastBeaconFound2 = lastBeaconFound;
            lastBeaconFound = nearest_beacon;

            //Log.e("matching?",m1.toString() + " = " + m2.toString() + ", " + M1.toString() + " = " + M2.toString());
            if (!m1.equals(m2) || !M1.equals(M2) || !m2.equals(m3) || !M2.equals(M3)) {
                // simple debouncer queue:
                // We only get past this point if beacon found is same as last beacon found and also the one before that.
                //return;
                // skip this... now that we're reducing the frequency of beacon checks, this will take way too long to happen.
            }

            Integer thisminor = nearest_beacon.getMinor();
            String muuid = nearest_beacon.getProximityUuid();
            Integer mmajor = nearest_beacon.getMajor();
            Integer mminor = nearest_beacon.getMinor();
            Double distance = nearest_beacon.getAccuracy();

            String last_updated_at = null; // need to do the date formatting below.
            String current_datetime = null;
            String beaconurl;
            String beaconname;
            Integer location_id;
            // also need to store results of this api beacon call in sharedprefs and check
            // if data for a found beacon exists there and use that data before making a
            // possibly unneccessary api callback about it.

            // note: I need to do something like this still (from iOS app):

            String format = "yyyy-MM-dd HH:mm:ss Z";
            SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.US);
            //System.out.format("%30s %s\n", format, sdf.format(new Date(0)));
            //sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
            current_datetime = sdf.format(new Date(0)); // this will be used for beacon database getUpdatedAt/setUpdatedAt string dates        

            // Set a Beacon Shell object into UserHelper with last updated at of right now
            // so that we do not query for this Beacon again until +24hours since we do not want to overwhelm device
            /*NSDateFormatter *dateFormatter = [[NSDateFormatter alloc]init];
            [dateFormatter setDateFormat:DATE_FORMAT];
            NSString *last_updated_at = [dateFormatter stringFromDate:[NSDate date]];*/
            // the call should include last_updated_at, @"last_updated_at

            // experimenting with storing in my new sqlite db and looking for existing records there before hitting api

            DatabaseHandler db = new DatabaseHandler(this);

            /**
             * CRUD Operations
             * */

            //Log.e("Select:", "Looking for beacon in db..");
            BeaconStore foundBeacon = db.getBeacon(muuid, mmajor, mminor);

            // Read all beacons (just a database demo example - not sure we'll ever need it in our app)
            /*Log.e("Reading: ", "Reading all beacons.."); 
            List<BeaconStore> beacons = db.getAllBeacons();       
                 
            for (BeaconStore cn : beacons) {
            String log = "Id: "+cn.getID()+" ,uuid: " + cn.getUuid() + " ,major: " + cn.getMajor() + " ,minor: " + cn.getMinor();
                // Writing Contacts to log
            Log.e("Beacon: ", log);
            }*/

            // beacon isn't in our phone database so poll api about it
            if (foundBeacon == null) {
                Log.e("Service", "Making API Call");
                //if (distance < 1) {
                // distance handling is done in nearest_beacon() now...
                //Log.e("IBeaconService","*** mminor=" + mminor.toString() + ", distance =" + distance.toString());
                Map<String, String> beaconApiData = runAppAPI("beaconInfoRequest", muuid, mmajor.toString(),
                        mminor.toString());

                //Log.e("Insert: ", "Inserting beacon from api results.."); 
                //String mername = beaconApiData.get("name");
                //Integer locid = Integer.parseInt(beaconApiData.get("location_id"));
                //String url = beaconApiData.get("url");  

                beaconurl = beaconApiData.get("url");
                beaconname = beaconApiData.get("name");
                location_id = Integer.parseInt(beaconApiData.get("location_id"));

                // Insert Beacon
                db.addBeacon(new BeaconStore(muuid, mmajor, mminor, current_datetime, beaconname, location_id,
                        beaconurl));

            } else {

                //Log.e("Service","!*!*! NOT Making API Call - beacon already in our sql db!");

                beaconurl = foundBeacon.getUrl();
                beaconname = foundBeacon.getMerchantName();
                location_id = foundBeacon.getLocationId();
            }

            // Check if database current_datetime for this beacon is older than 1 hour and older than 24 hours.
            // different logic may result based on those values.
            // for example, if greater than 24 hours old, we should still poll the api for updated info about this beacon.
            // 

            //Log.e("ibeaconservice","about to send data to app");
            //Log.e("beaconurl"," " + beaconurl);
            if (beaconurl != null && beaconurl != "signinfailure") {
                // force open app at payment page:
                //Log.e("service","beaconurl = " + beaconurl);
                String previousNotificationUrl = apiInstance.getCurrentLaunchUrl();
                // only generate notification if it's different from the last one
                String notetext = "Pay with your phone";
                notetext = notetext + " at " + beaconname;

                //Log.e("service","lastaction=" + lastaction + ", lastlocationid=" + lastlocationid.toString() + ", location_id=" + location_id.toString());
                //Log.e("locid before N/1/2",location_id+"/"+old_locid+"/"+old_locid2);

                // if location_id has changed from last two sent by a notification in this background service:
                beacon_changed = (location_id.equals(old_locid) && old_locid.equals(old_locid2));
                //Log.e("beacon_changed",beacon_changed.toString());
                //old_locid = location_id;
                PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
                KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
                boolean isScreenOn = powerManager.isScreenOn();
                boolean keyboardLocked = km.inKeyguardRestrictedInputMode();

                if (isForeground("com.paywith.paywith") && isScreenOn && !keyboardLocked) {
                    // only if the app is running main on your phone AND phone is currently unlocked

                    // only do this if:
                    // the last thing this service did was NOT send a location id
                    // OR the last thing this service did was NOT send this same location change

                    // also add the 3 level debouncer when in foreground to locations list 
                    //Log.e("matching?",m1.toString() + " = " + m2.toString() + ", " + M1.toString() + " = " + M2.toString());
                    if (!m1.equals(m2) || !M1.equals(M2) || !m2.equals(m3) || !M2.equals(M3)) {
                        // simple debouncer queue:
                        // We only get past this point if beacon found is same as last beacon found and also the one before that.
                        return;
                        // skip this... now that we're reducing the frequency of beacon checks, this will take way too long to happen.
                    }
                    //if (!lastaction.equals("moveLocationToTop") || 
                    //      lastaction.equals("moveLocationToTop") && !lastlocationid.equals(location_id)) {

                    //Log.e("service","sending new location to locationlist:" + location_id.toString());
                    // tell app to move this location to top of location list if possible
                    moveLocationToTop(location_id.toString());
                    //}
                    //generateNotification("TEST",notetext, beaconurl, location_id.toString());// this generates system notification

                    lastaction = "moveLocationToTop";
                    scanPeriod = IBeaconManager.DEFAULT_FOREGROUND_SCAN_PERIOD;
                    betweenScanPeriod = IBeaconManager.DEFAULT_FOREGROUND_BETWEEN_SCAN_PERIOD;
                    //setScanPeriods(scanPeriod,betweenScanPeriod);
                    //Log.e("sending intent","locations list order");
                } else if (!lastaction.equals("generateNotification")
                        || lastaction.equals("generateNotification") && !lastlocationid.equals(location_id)) { // add hour timer here also                       
                    // && beacon_changed
                    // only if app is running in background or not running at all.

                    // only do this if:
                    // the last thing this service did was NOT send a location id
                    // OR the last thing this service did was NOT send this same location change
                    // ALSO only notify if this location hasn't been notified within last hour.  (still necessary?  not sure...)
                    // (trying without the hour thing for now).

                    // simple debouncer targetted at notifications:
                    if (!m1.equals(m2) || !M1.equals(M2) || !m2.equals(m3) || !M2.equals(M3)) {
                        //if (m1.equals(m2) && M1.equals(M2) || m1.equals(m3) && M1.equals(M3)) {
                        // logic:  if this beacon was notified within the past 3 cycles, don't notify again.
                        return;
                    }

                    // generate notification.
                    generateNotification("Pay Here", notetext, beaconurl, location_id.toString());// this generates system notification
                    setNextLaunchUrl(beaconurl);
                    setNextLaunchName(beaconname);
                    lastaction = "generateNotification";
                    //Log.e("sending intent","*** notification of new location");
                    scanPeriod = IBeaconManager.DEFAULT_BACKGROUND_SCAN_PERIOD;
                    betweenScanPeriod = IBeaconManager.DEFAULT_BACKGROUND_BETWEEN_SCAN_PERIOD;
                    //setScanPeriods(scanPeriod,betweenScanPeriod);
                }
                lastlocationid = location_id;
                //}
                // update location_id queue history
                old_locid2 = old_locid;
                old_locid = location_id;
            }
        }

    }

    private void processExpiredMonitors() {
        Iterator<Region> monitoredRegionIterator = monitoredRegionState.keySet().iterator();
        while (monitoredRegionIterator.hasNext()) {
            Region region = monitoredRegionIterator.next();
            MonitorState state = monitoredRegionState.get(region);
            if (state.isNewlyOutside()) {
                if (IBeaconManager.debug)
                    Log.d(TAG, "found a monitor that expired: " + region);
                state.getCallback().call(IBeaconService.this, "monitoringData",
                        new MonitoringData(state.isInside(), region));
            }
        }
    }

    private void processIBeaconFromScan(IBeacon iBeacon) {
        //if (IBeaconManager.debug) Log.d(TAG,"iBeacon detected multiple times in scan cycle :" + iBeacon.getProximityUuid() + " "+ iBeacon.getMajor() + " " + iBeacon.getMinor());
        //Log.e("ibeacon?","test");

        // check for uuid = 5e6e6190-0b83-0132-9a3d-12313d1ca6ab (paywith beacon uuid) or skip:
        // note: estimote default uuid is B9407F30-F5F8-466E-AFF9-25556B57FE6D
        // note: radius default uuid is 52414449-5553-4E45-5457-4F524B53434F
        // note: IOD default uuid is 
        //Log.w("check if paywithbeacon",iBeacon.getProximityUuid().toString().toUpperCase());
        if (!iBeacon.getProximityUuid().toString().toUpperCase().equals("B9407F30-F5F8-466E-AFF9-25556B57FE6D")
                && !iBeacon.getProximityUuid().toString().toUpperCase()
                        .equals("5E6E6190-0B83-0132-9A3D-12313D1CA6AB")) {
            // not our beacon?  don't include it!
            //Log.w("check result","not paywith");
            return;
        }

        lastIBeaconDetectionTime = new Date();
        trackedBeaconsPacketCount++;
        if (trackedBeacons.contains(iBeacon)) {
            if (IBeaconManager.debug)
                Log.d(TAG, "iBeacon detected multiple times in scan cycle :" + iBeacon.getProximityUuid() + " "
                        + iBeacon.getMajor() + " " + iBeacon.getMinor());
        }
        trackedBeacons.add(iBeacon);
        if (IBeaconManager.debug)
            Log.d(TAG, "iBeacon detected :" + iBeacon.getProximityUuid() + " " + iBeacon.getMajor() + " "
                    + iBeacon.getMinor());

        List<Region> matchedRegions = null;
        synchronized (monitoredRegionState) {
            matchedRegions = matchingRegions(iBeacon, monitoredRegionState.keySet());
        }
        Iterator<Region> matchedRegionIterator = matchedRegions.iterator();
        while (matchedRegionIterator.hasNext()) {
            //Log.d(TAG, "looping through region iterator");
            Region region = matchedRegionIterator.next();
            MonitorState state = monitoredRegionState.get(region);
            if (state.markInside()) {
                state.getCallback().call(IBeaconService.this, "monitoringData",
                        new MonitoringData(state.isInside(), region));
            }
        }

        if (IBeaconManager.debug)
            Log.d(TAG, "looking for ranging region matches for this ibeacon");
        synchronized (rangedRegionState) {
            matchedRegions = matchingRegions(iBeacon, rangedRegionState.keySet());
        }
        matchedRegionIterator = matchedRegions.iterator();
        while (matchedRegionIterator.hasNext()) {
            Region region = matchedRegionIterator.next();
            if (IBeaconManager.debug)
                Log.d(TAG, "matches ranging region: " + region);
            RangeState rangeState = rangedRegionState.get(region);
            synchronized (rangeState) {
                rangeState.addIBeacon(iBeacon);
            }
        }
    }

    private class ScanProcessor extends AsyncTask<ScanData, Void, Void> {

        @Override
        protected Void doInBackground(ScanData... params) {
            if (IBeaconManager.debug)
                Log.d(TAG, "ScanProcessor");
            ScanData scanData = params[0];

            IBeacon iBeacon = IBeacon.fromScanData(scanData.scanRecord, scanData.rssi, scanData.device);

            if (iBeacon != null) {
                processIBeaconFromScan(iBeacon);
            }

            bluetoothCrashResolver.notifyScannedDevice(scanData.device,
                    (BluetoothAdapter.LeScanCallback) getLeScanCallback());
            return null;
        }

        @Override
        protected void onPostExecute(Void result) {
        }

        @Override
        protected void onPreExecute() {
        }

        @Override
        protected void onProgressUpdate(Void... values) {
        }
    }

    private List<Region> matchingRegions(IBeacon iBeacon, Collection<Region> regions) {
        List<Region> matched = new ArrayList<Region>();
        Iterator<Region> regionIterator = regions.iterator();
        while (regionIterator.hasNext()) {
            Region region = regionIterator.next();
            if (region.matchesIBeacon(iBeacon)) {
                matched.add(region);
            } else {
                if (IBeaconManager.debug)
                    Log.d(TAG, "This region does not match: " + region);
            }

        }

        return matched;
    }

    private void launchApp(String murl) {
        // call this to launch the main paywith app
        Intent intent = new Intent("android.intent.category.LAUNCHER");
        intent.setClassName("com.paywith.paywith", "com.paywith.paywith.MainActivity");
        //intent.setClassName("com.paywith.paywith", "com.paywith.paywith.BeaconToAppActivity");
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra("OpenUrl", murl);
        startActivity(intent);
    }

    private Map<String, String> runAppAPI(String sent_action, String muuid, String mmajor, String mminor) {
        //Log.e("IBeaconService","runAppAPI() sent_action=" + sent_action);
        // call this to access the main paywith app API callbacks.
        //call like this:
        //Log.e("IBeaconService","runAppAPI");
        //   runAppAPI("beaconInfoRequest", muuid, mmajor, mminor);
        if (sent_action.equals("beaconInfoRequest")) {
            //Log.e("IBeaconService apiInstance created","sent_action = beaconInfoResult");
            apiresult = apiInstance.getBeacon(muuid, mmajor, mminor);
            if (apiresult == null) {
                //Log.e("IBeaconService:runAppAPI","apiresult == null");
                return null;
            }

            String beaconname = apiresult.get("name");
            if (beaconname == "") {
                // need beaconname for this to be of any use
                return null;
            }

        }

        return apiresult;
    }

    protected void generateNotification(String title, String merchanttext, String murl, String location_id) {

        String ns = Context.NOTIFICATION_SERVICE;
        NotificationManager mNotificationManager = (NotificationManager) getSystemService(ns);

        Builder mNotifyBuilder = new NotificationCompat.Builder(this);

        //int icon = R.drawable.ic_launcher;
        //CharSequence tickerText = "PayWith";
        //long when = System.currentTimeMillis();

        //@SuppressWarnings("deprecation")
        //Notification notification = new Notification(icon,
        //        tickerText, when);

        mNotifyBuilder.setContentText(merchanttext).setContentTitle(title).setSmallIcon(R.drawable.ic_launcher);//logo

        //notification.flags |= Notification.FLAG_AUTO_CANCEL;
        Context context = getApplicationContext();
        Intent notificationIntent = new Intent("android.intent.category.LAUNCHER");
        //CharSequence contentTitle = title;
        //CharSequence contentText = merchanttext;

        notificationIntent.putExtra("OpenUrl", murl);
        notificationIntent.putExtra("MerchantName", merchanttext);
        notificationIntent.setClassName("com.paywith.paywith", "com.paywith.paywith.MainActivity");

        //PendingIntent contentIntent = PendingIntent
        //         .getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT);

        //notification.setLatestEventInfo(context, contentTitle,
        //        contentText, contentIntent);

        //mNotificationManager.notify(ongoing_notification_id, notification);

        PendingIntent contentIntent = PendingIntent.getActivity(context, 0, notificationIntent,
                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT);
        // PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT

        // this gives us flexibility to add vibration and sound etc:
        Notification note = mNotifyBuilder.build();

        // THIS line is what makes the notification tappable to launch app and generate mCard payment:
        note.setLatestEventInfo(context, title, merchanttext, contentIntent);

        // make phone vibrate and make sound on notification:
        note.defaults |= Notification.DEFAULT_VIBRATE;
        note.defaults |= Notification.DEFAULT_SOUND;

        notificationIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
        //Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP

        note.flags |= Notification.FLAG_AUTO_CANCEL;
        // Because the ID remains unchanged, the existing notification is updated.
        mNotificationManager.notify(ongoing_notification_id, note);
        //Log.e("notification","sent");
    }

    protected void moveLocationToTop(String location_id) {
        Intent intent = new Intent("android.intent.category.LAUNCHER");
        intent.setClassName("com.paywith.paywith", "com.paywith.paywith.MainActivity");
        intent.putExtra("location_id", location_id);
        //intent.setClassName("com.paywith.paywith", "com.paywith.paywith.BeaconToAppActivity");
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(intent);
    }

    /*
     Returns false if no ranging or monitoring regions have beeen requested.  This is useful in determining if we should scan at all.
     */
    public boolean anyRangingOrMonitoringRegionsActive() {
        return (rangedRegionState.size() + monitoredRegionState.size()) > 0;
    }

    @TargetApi(18)
    private BluetoothAdapter getBluetoothAdapter() {
        if (android.os.Build.VERSION.SDK_INT < 18) {
            Log.w(TAG, "Not supported prior to API 18.");
            return null;
        }
        if (bluetoothAdapter == null) {
            // Initializes Bluetooth adapter.
            final BluetoothManager bluetoothManager = (BluetoothManager) this.getApplicationContext()
                    .getSystemService(Context.BLUETOOTH_SERVICE);
            bluetoothAdapter = bluetoothManager.getAdapter();
        }
        return bluetoothAdapter;
    }

    //TA changes
    static final String NAME = IBeaconService.class.getName();
    private static volatile PowerManager.WakeLock lockStatic = null;

    synchronized private static PowerManager.WakeLock getLock(Context context) {
        if (lockStatic == null) {
            PowerManager mgr = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

            lockStatic = mgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, NAME);
            lockStatic.setReferenceCounted(true);
        }

        return (lockStatic);
    }

    public boolean isForeground(String myPackage) {
        ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
        List<ActivityManager.RunningTaskInfo> runningTaskInfo = manager.getRunningTasks(1);

        ComponentName componentInfo = runningTaskInfo.get(0).topActivity;
        if (componentInfo.getPackageName().equals(myPackage)) {
            return true;
        }
        return false;
    }

    protected synchronized IBeacon findNearest(Collection<IBeacon> beacons2) {

        Collection<IBeacon> beacons = trackedBeacons;
        // must convert linkedhashmap to map for looping through data
        //Map<String, Beacon> beaconsMap = beacons; //beacons
        //Iterator<Entry<String, Beacon>> beaconIt = beacons.entrySet().iterator();
        // nearestDistance is initially set higher than any beacon would reasonably be
        Double nearestDistance = IBeaconManager.alertRangeNear;//200.0;
        // nearestBeacon will be set to the Beacon object whenever it's found to be nearest
        IBeacon nearestBeacon = null;// = new IBeacon();
        //Collection<IBeacon> beaconsToCheck;
        //synchronized Collection<IBeacon> beaconsToCheck() {

        //}

        /*public synchronized Collection<IBeacon> finalizeIBeacons() {
            ArrayList<IBeacon> iBeacons = new ArrayList<IBeacon>();
            Map<IBeacon,RangedIBeacon> newRangedIBeacons = new HashMap<IBeacon,RangedIBeacon>();
            
            synchronized (rangedIBeacons) {
           for (IBeacon iBeacon : rangedIBeacons.keySet()) {
                  
                  
           }
            }
        }*/
        synchronized (beacons) {

            for (Iterator<IBeacon> iterator = beacons.iterator(); iterator.hasNext();) {
                //Log.e("findnearest","looping through beacons");
                IBeacon aBeacon = (IBeacon) iterator.next();
                Integer minor = aBeacon.getMinor();
                Double aBeaconDistance = aBeacon.getAccuracy();
                //Log.d("findNearest", minor.toString() + " :: " + aBeaconDistance.toString());
                //Long aBeaconTimeScannedEpoch = aBeacon.getTimeScannedEpoch();
                if (aBeaconDistance < nearestDistance) {
                    // this beacon is closest in list so far... (first beacon in list always at first followed by anything closer)
                    nearestBeacon = aBeacon;
                    nearestDistance = aBeaconDistance;
                }
            }

        }

        return nearestBeacon;
    }
}