package com.idevicesinc.sweetblue;

import static com.idevicesinc.sweetblue.BleManagerState.*;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import android.Manifest;
import android.app.Activity;
import android.app.Application;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import com.idevicesinc.sweetblue.BleDevice.BondListener.BondEvent;
import com.idevicesinc.sweetblue.BleDevice.BondListener.Status;
import com.idevicesinc.sweetblue.BleDevice.BondListener;
import com.idevicesinc.sweetblue.BleDevice.ConnectionFailListener;
import com.idevicesinc.sweetblue.BleManager.DiscoveryListener.DiscoveryEvent;
import com.idevicesinc.sweetblue.BleManager.DiscoveryListener.LifeCycle;
import com.idevicesinc.sweetblue.BleServer.IncomingListener;
import com.idevicesinc.sweetblue.BleManager.ResetListener.ResetEvent;
import com.idevicesinc.sweetblue.BleManager.UhOhListener.UhOh;
import com.idevicesinc.sweetblue.BleManagerConfig.ScanFilter;
import com.idevicesinc.sweetblue.BleManagerConfig.ScanFilter.Please;
import com.idevicesinc.sweetblue.P_ScanManager.DiscoveryEntry;
import com.idevicesinc.sweetblue.PA_StateTracker.E_Intent;
import com.idevicesinc.sweetblue.annotations.Advanced;
import com.idevicesinc.sweetblue.annotations.Experimental;
import com.idevicesinc.sweetblue.annotations.Nullable;
import com.idevicesinc.sweetblue.annotations.Immutable;
import com.idevicesinc.sweetblue.annotations.Nullable.Prevalence;
import com.idevicesinc.sweetblue.backend.historical.Backend_HistoricalDatabase;
import com.idevicesinc.sweetblue.compat.M_Util;
import com.idevicesinc.sweetblue.utils.EpochTime;
import com.idevicesinc.sweetblue.utils.Event;
import com.idevicesinc.sweetblue.utils.ForEach_Breakable;
import com.idevicesinc.sweetblue.utils.ForEach_Void;
import com.idevicesinc.sweetblue.utils.GattDatabase;
import com.idevicesinc.sweetblue.utils.GenericListener_Void;
import com.idevicesinc.sweetblue.utils.HistoricalData;
import com.idevicesinc.sweetblue.utils.Interval;
import com.idevicesinc.sweetblue.utils.Percent;
import com.idevicesinc.sweetblue.utils.State;
import com.idevicesinc.sweetblue.utils.Utils;
import com.idevicesinc.sweetblue.utils.Utils_ScanRecord;
import com.idevicesinc.sweetblue.utils.Utils_String;

/**
 * The entry point to the library. Get a singleton instance using {@link #get(android.content.Context, BleManagerConfig)} or its overloads. Make sure
 * to hook up this manager to lifecycle events for your app as a whole: {@link #onPause()} and {@link #onResume()}.
 * <br><br>
 * Also put the following entries (or something similar) in the root of your AndroidManifest.xml:
 * <br><br>
 * {@code <uses-sdk android:minSdkVersion="18" android:targetSdkVersion="23" />}<br>
 * {@code <uses-permission android:name="android.permission.BLUETOOTH" /> }<br>
 * {@code <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> }<br>
 * {@code <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" /> }<br>
 * {@code <uses-permission android:name="android.permission.WAKE_LOCK" /> } <br>
 * {@code <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> } <br>
 * {@code <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> }<br>
 * <br><br>
 * {@link android.Manifest.permission#WAKE_LOCK} is recommended but optional, needed if {@link BleManagerConfig#manageCpuWakeLock} is enabled to aid with reconnect loops.
 * As of now it's enabled by default.
 * <br><br>
 * {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} (or {@link android.Manifest.permission#ACCESS_FINE_LOCATION})
 * is also strongly recommended but optional. Without it, {@link BleManager#startScan()} and overloads will not properly return results in {@link android.os.Build.VERSION_CODES#M} and above.
 * See {@link #startScan(Interval, BleManagerConfig.ScanFilter, DiscoveryListener)} for more information.
 * <br><br>
 * Now here is a simple example usage:<pre><code>
 * public class MyActivity extends Activity
 * {
 *     {@literal @}Override protected void onCreate(Bundle savedInstanceState)
 *      {
 *          // A ScanFilter decides whether a BleDevice instance will be created
 *          // and passed to the DiscoveryListener implementation below.
 *         final ScanFilter scanFilter = new ScanFilter()
 *         {
 *            {@literal @}Override public Please onEvent(ScanEvent e)
 *             {
 *                 return Please.acknowledgeIf(e.name_normalized().contains("my_device"))
 *                              .thenStopScan();
 *             }
 *         };
 *
 *         // New BleDevice instances are provided through this listener.
 *         // Nested listeners then listen for connection and read results.
 *         final DiscoveryListener discoveryListener = new DiscoveryListener()
 *         {
 *            {@literal @}Override public void onEvent(DiscoveryEvent e)
 *             {
 *                 if( e.was(LifeCycle.DISCOVERED) )
 *                 {
 *                     e.device().connect(new StateListener()
 *                     {
 *                        {@literal @}Override public void onEvent(StateEvent e)
 *                         {
 *                             if( e.didEnter(BleDeviceState.INITIALIZED) )
 *                             {
 *                                 e.device().read(Uuids.BATTERY_LEVEL, new ReadWriteListener()
 *                                 {
 *                                    {@literal @}Override public void onEvent(ReadWriteEvent e)
 *                                     {
 *                                         if( e.wasSuccess() )
 *                                         {
 *                                             Log.i("", "Battery level is " + e.data_byte() + "%");
 *                                         }
 *                                     }
 *                                 });
 *                             }
 *                         }
 *                     });
 *                 }
 *             }
 *         };
 *
 *         // Helps you navigate the treacherous waters of Android M Location requirements for scanning.
 *         BluetoothEnabler.start(this, new DefaultBluetoothEnablerFilter()
 *         {
 *            {@literal @}Override public Please onEvent(BluetoothEnablerEvent e)
 *             {
 *                 if( e.isDone() )
 *                 {
 *                     e.bleManager().startScan(scanFilter, discoveryListener);
 *                 }
 *
 *                 return super.onEvent(e);
 *             }
 *         });
 *    }
 * </code>
 * </pre>
 */
public final class BleManager
{
	/**
	 * Provide an implementation to {@link BleManager#setListener_Discovery(BleManager.DiscoveryListener)} to receive
	 * callbacks when a device is newly discovered, rediscovered, or undiscovered after calling various {@link BleManager#startScan()}
	 * or {@link BleManager#startPeriodicScan(Interval, Interval)} methods. You can also provide this to various
	 * overloads of {@link BleManager#startScan()} and {@link BleManager#startPeriodicScan(Interval, Interval)}.
	 */
	@com.idevicesinc.sweetblue.annotations.Lambda
	public static interface DiscoveryListener extends com.idevicesinc.sweetblue.utils.GenericListener_Void<DiscoveryListener.DiscoveryEvent>
	{
		/**
		 * Enumerates changes in the "discovered" state of a device.
		 * Used at {@link BleManager.DiscoveryListener.DiscoveryEvent#lifeCycle()}.
		 */
		public static enum LifeCycle
		{
			/**
			 * Used when a device is discovered for the first time after
			 * calling {@link BleManager#startScan()} (or its overloads)
			 * or {@link BleManager#startPeriodicScan(Interval, Interval)}.
			 */
			DISCOVERED,

			/**
			 * Used when a device is rediscovered after already being discovered at least once.
			 */
			REDISCOVERED,

			/**
			 * Used when a device is "undiscovered" after being discovered at least once. There is no native equivalent
			 * for this callback. Undiscovery is approximated with a timeout based on the last time we discovered a device, configured
			 * by {@link BleDeviceConfig#undiscoveryKeepAlive}. This option is disabled by default. If set, you should expect that the undiscovery
			 * callback will take some amount of time to receive after an advertising device is turned off or goes out of range or what have you.
			 * It's generally not as fast as other state changes like {@link BleDeviceState#DISCONNECTED} or getting {@link BleDeviceState#DISCOVERED} in the first place.
			 *
			 * @see BleDeviceConfig#minScanTimeNeededForUndiscovery
			 * @see BleDeviceConfig#undiscoveryKeepAlive
			 */
			UNDISCOVERED;
		}

		/**
		 * Struct passed to {@link BleManager.DiscoveryListener#onEvent(BleManager.DiscoveryListener.DiscoveryEvent)}.
		 */
		@com.idevicesinc.sweetblue.annotations.Immutable
		public static class DiscoveryEvent extends com.idevicesinc.sweetblue.utils.Event
		{
			/**
			 * The {@link BleManager} which is currently {@link BleManagerState#SCANNING}.
			 */
			public BleManager manager(){  return device().getManager();  }

			/**
			 * The device in question.
			 */
			public BleDevice device(){  return m_device;  }
			private final BleDevice m_device;

			/**
			 * Convience to return the mac address of {@link #device()}.
			 */
			public String macAddress()  {  return m_device.getMacAddress();  }

			/**
			 * The discovery {@link BleManager.DiscoveryListener.LifeCycle} that the device has undergone.
			 */
			public LifeCycle lifeCycle(){  return m_lifeCycle;  }
			private final LifeCycle m_lifeCycle;

			DiscoveryEvent(final BleDevice device, final LifeCycle lifeCycle)
			{
				m_device = device;
				m_lifeCycle = lifeCycle;
			}

			/**
			 * Forwards {@link BleDevice#getRssi()}.
			 */
			public int rssi()
			{
				return device().getRssi();
			}

			/**
			 * Forwards {@link BleDevice#getRssiPercent()}.
			 */
			public Percent rssi_percent()
			{
				return device().getRssiPercent();
			}

			/**
			 * Convenience method for checking equality of given {@link BleManager.DiscoveryListener.LifeCycle} and {@link #lifeCycle()}.
			 */
			public boolean was(LifeCycle lifeCycle)
			{
				return lifeCycle == lifeCycle();
			}

			@Override public String toString()
			{
				return Utils_String.toString
				(
					this.getClass(),
					"device", device().getName_debug(),
					"lifeCycle", lifeCycle(),
					"rssi", rssi(),
					"rssi_percent", rssi_percent()
				);
			}

			static DiscoveryEvent newEvent(BleDevice device, LifeCycle lifeCycle)
			{
				return new DiscoveryEvent(device, lifeCycle);
			}
		}

		/**
		 * Called when the discovery lifecycle of a device is updated.
		 * <br><br>
		 * TIP: Take a look at {@link BleDevice#getLastDisconnectIntent()}. If it is {@link com.idevicesinc.sweetblue.utils.State.ChangeIntent#UNINTENTIONAL}
		 * then from a user-experience perspective it's most often best to automatically connect without user confirmation.
		 */
		void onEvent(final DiscoveryEvent e);

	}

	/**
	 * Provide an implementation to {@link BleManager#setListener_State(BleManager.StateListener)} to receive callbacks
	 * when the {@link BleManager} undergoes a {@link BleManagerState} change.
	 *
	 * @deprecated - Refactored to {@link ManagerStateListener}.
	 */
	@com.idevicesinc.sweetblue.annotations.Lambda
	public static interface StateListener
	{
		/**
		 * Subclass that adds the manager field.
		 */
		@Immutable
		public static class StateEvent extends State.ChangeEvent<BleManagerState>
		{
			/**
			 * The singleton manager undergoing the state change.
			 */
			public BleManager manager(){  return m_manager;  }
			private final BleManager m_manager;

			StateEvent(final BleManager manager, final int oldStateBits, final int newStateBits, final int intentMask)
			{
				super(oldStateBits, newStateBits, intentMask);

				this.m_manager = manager;
			}

			@Override public String toString()
			{
				return Utils_String.toString
				(
					this.getClass(),
					"entered",			Utils_String.toString(enterMask(), BleManagerState.VALUES()),
					"exited",			Utils_String.toString(exitMask(), BleManagerState.VALUES()),
					"current",			Utils_String.toString(newStateBits(), BleManagerState.VALUES())
				);
			}
		}

		/**
		 * Called when the manager's abstracted {@link BleManagerState} changes.
		 */
		void onEvent(final StateEvent e);
	}

	/**
	 * Provide an implementation to {@link BleManager#setListener_NativeState(BleManager.NativeStateListener)} to receive callbacks
	 * when the {@link BleManager} undergoes a *native* {@link BleManagerState} change. This is similar to {@link BleManager.StateListener}
	 * but reflects what is going on in the actual underlying stack, which may lag slightly behind the
	 * abstracted state reflected by {@link BleManager.StateListener}. Most apps will not find this callback useful.
	 *
	 * @deprecated This will be removed in v3.
	 */
	@Advanced
	@com.idevicesinc.sweetblue.annotations.Lambda
	@Deprecated
	public interface NativeStateListener extends GenericListener_Void<NativeStateListener.NativeStateEvent>
	{
		/**
		 * Class declared here to be make it implicitly imported for overrides.
		 */
		@Advanced
		@Immutable
		class NativeStateEvent extends StateListener.StateEvent
		{
			NativeStateEvent(final BleManager manager, final int oldStateBits, final int newStateBits, final int intentMask)
			{
				super(manager, oldStateBits, newStateBits, intentMask);
			}
		}

		/**
		 * Called when the manager's native bitwise {@link BleManagerState} changes. As many bits as possible are flipped at the same time.
		 */
		@Advanced
		void onEvent(final NativeStateEvent e);
	}

	/**
	 * Provide an implementation to {@link BleManager#setListener_UhOh(BleManager.UhOhListener)}
	 * to receive a callback when an {@link BleManager.UhOhListener.UhOh} occurs.
	 *
	 * @see BleManager.UhOhListener.UhOh
	 */
	@com.idevicesinc.sweetblue.annotations.Lambda
	public static interface UhOhListener extends com.idevicesinc.sweetblue.utils.GenericListener_Void<UhOhListener.UhOhEvent>
	{
		/**
		 * An UhOh is a warning about an exceptional (in the bad sense) and unfixable problem with the underlying stack that
		 * the app can warn its user about. It's kind of like an {@link Exception} but they can be so common
		 * that using {@link Exception} would render this library unusable without a rat's nest of try/catches.
		 * Instead you implement {@link BleManager.UhOhListener} to receive them. Each {@link BleManager.UhOhListener.UhOh} has a {@link BleManager.UhOhListener.UhOh#getRemedy()}
		 * that suggests what might be done about it.
		 *
		 * @see BleManager.UhOhListener
		 * @see BleManager#setListener_UhOh(BleManager.UhOhListener)
		 */
		public static enum UhOh
		{
			/**
			 * A {@link BleTask#BOND} operation timed out. This can happen a lot with the Galaxy Tab 4, and doing {@link BleManager#reset()} seems to fix it.
			 * SweetBlue does as much as it can to work around the issue that causes bond timeouts, but some might still slip through.
			 */
			BOND_TIMED_OUT,

			/**
			 * A {@link BleDevice#read(java.util.UUID, BleDevice.ReadWriteListener)}
			 * took longer than timeout set by {@link BleDeviceConfig#taskTimeoutRequestFilter}.
			 * You will also get a {@link BleDevice.ReadWriteListener.ReadWriteEvent} with {@link BleDevice.ReadWriteListener.Status#TIMED_OUT}
			 * but a timeout is a sort of fringe case that should not regularly happen.
			 */
			READ_TIMED_OUT,

			/**
			 * A {@link BleDevice#read(java.util.UUID, BleDevice.ReadWriteListener)} returned with a <code>null</code>
			 * characteristic value. The <code>null</code> value will end up as an empty array in {@link BleDevice.ReadWriteListener.ReadWriteEvent#data}
			 * so app-land doesn't have to do any special <code>null</code> handling.
			 */
			READ_RETURNED_NULL,

			/**
			 * Similar to {@link #READ_TIMED_OUT} but for {@link BleDevice#write(java.util.UUID, byte[])}.
			 */
			WRITE_TIMED_OUT,

			/**
			 * Similar to {@link #WRITE_TIMED_OUT}, only used to signify when testing a new MTU size, and it times out. This usually means the android device
			 * has a bug where it says the MTU size has changed, but can't write the MTU size amount (OnePlus2, Moto X Pure). In this case, SweetBlue will disconnect
			 * the device, as no other reads/writes will work once this happens.
			 */
			WRITE_MTU_TEST_TIMED_OUT,


			/**
			 * When the underlying stack meets a race condition where {@link android.bluetooth.BluetoothAdapter#getState()} does not
			 * match the value provided through {@link android.bluetooth.BluetoothAdapter#ACTION_STATE_CHANGED} with {@link android.bluetooth.BluetoothAdapter#EXTRA_STATE}.
			 *
			 */
			INCONSISTENT_NATIVE_BLE_STATE,

			/**
			 * A {@link BleDevice} went from {@link BleDeviceState#BONDING} to {@link BleDeviceState#UNBONDED}.
			 * UPDATE: This can happen under normal circumstances, so not listing it as an uh oh for now.
			 */
//			WENT_FROM_BONDING_TO_UNBONDED,

			/**
			 * A {@link android.bluetooth.BluetoothGatt#discoverServices()} operation returned two duplicate services. Not the same instance
			 * necessarily but the same UUID.
			 */
			DUPLICATE_SERVICE_FOUND,

			/**
			 * A {@link android.bluetooth.BluetoothGatt#discoverServices()} operation returned a service instance that we already received before
			 * after disconnecting and reconnecting.
			 */
			OLD_DUPLICATE_SERVICE_FOUND,

			/**
			 * {@link android.bluetooth.BluetoothAdapter#startLeScan(BluetoothAdapter.LeScanCallback)} failed for an unknown reason. The library is now using
			 * {@link android.bluetooth.BluetoothAdapter#startDiscovery()} instead.
			 *
			 * @see BleManagerConfig#revertToClassicDiscoveryIfNeeded
			 */
			START_BLE_SCAN_FAILED__USING_CLASSIC,

			/**
			 * {@link android.bluetooth.BluetoothGatt#getConnectionState(BluetoothDevice)} says we're connected but we never tried to connect in the first place.
			 * My theory is that this can happen on some phones when you quickly restart the app and the stack doesn't have
			 * a chance to disconnect from the device entirely.
			 */
			CONNECTED_WITHOUT_EVER_CONNECTING,

			/**
			 * Similar in concept to {@link BleManager.UhOhListener.UhOh#RANDOM_EXCEPTION} but used when {@link android.os.DeadObjectException} is thrown.
			 */
			DEAD_OBJECT_EXCEPTION,

			/**
			 * The underlying native BLE stack enjoys surprising you with random exceptions. Every time a new one is discovered
			 * it is wrapped in a try/catch and this {@link BleManager.UhOhListener.UhOh} is dispatched.
			 */
			RANDOM_EXCEPTION,

			/**
			 * Occasionally, when trying to get the native GattService, android will throw a ConcurrentModificationException. This can happen
			 * when trying to perform any read or write. Usually, you simply have to just try again.
			 */
			CONCURRENT_EXCEPTION,

			/**
			 * {@link android.bluetooth.BluetoothAdapter#startLeScan(BluetoothAdapter.LeScanCallback)} failed and {@link BleManagerConfig#revertToClassicDiscoveryIfNeeded} is <code>false</code>.
			 *
			 * @see BleManagerConfig#revertToClassicDiscoveryIfNeeded
			 */
			START_BLE_SCAN_FAILED,

			/**
			 * {@link android.bluetooth.BluetoothAdapter#startLeScan(BluetoothAdapter.LeScanCallback)} failed and {@link BleManagerConfig#revertToClassicDiscoveryIfNeeded} is <code>true</code>
			 * so we try {@link android.bluetooth.BluetoothAdapter#startDiscovery()} but that also fails...fun!
			 */
			CLASSIC_DISCOVERY_FAILED,

			/**
			 * {@link android.bluetooth.BluetoothGatt#discoverServices()} failed right off the bat and returned false.
			 */
			SERVICE_DISCOVERY_IMMEDIATELY_FAILED,

			/**
			 * {@link android.bluetooth.BluetoothAdapter#disable()}, through {@link BleManager#turnOff()}, is failing to complete.
			 * We always end up back at {@link android.bluetooth.BluetoothAdapter#STATE_ON}.
			 */
			CANNOT_DISABLE_BLUETOOTH,

			/**
			 * {@link android.bluetooth.BluetoothAdapter#enable()}, through {@link BleManager#turnOn()}, is failing to complete.
			 * We always end up back at {@link android.bluetooth.BluetoothAdapter#STATE_OFF}. Opposite problem of {@link #CANNOT_DISABLE_BLUETOOTH}
			 */
			CANNOT_ENABLE_BLUETOOTH,

			/**
			 * This can be thrown when the underlying state from {@link BluetoothManager#getConnectionState(BluetoothDevice, int)} does not match
			 * the apparent condition of the device (for instance, you perform a scan, then try to connect to a device, but it reports as being connected...in this case, it cannot
			 * be connected, AND advertising). It seems the values from this method are cached, so sometimes this cache gets "stuck" in the connected state. In this case, it may
			 * be best to clear cache of the Bluetooth app (Sometimes called Bluetooth Cache).
			 */
			INCONSISTENT_NATIVE_DEVICE_STATE,

			/**
			 * Just a blanket case for when the library has to completely shrug its shoulders.
			 */
			UNKNOWN_BLE_ERROR;

			/**
			 * Returns the {@link BleManager.UhOhListener.Remedy} for this {@link BleManager.UhOhListener.UhOh}.
			 */
			public Remedy getRemedy()
			{
				if( this.ordinal() >= CANNOT_DISABLE_BLUETOOTH.ordinal() )
				{
					return Remedy.RESTART_PHONE;
				}
				else if( this.ordinal() >= START_BLE_SCAN_FAILED.ordinal() )
				{
					return Remedy.RESET_BLE;
				}
				else
				{
					return Remedy.WAIT_AND_SEE;
				}
			}
		}

		/**
		 * The suggested remedy for each {@link BleManager.UhOhListener.UhOh}. This can be used as a proxy for the severity
		 * of the issue.
		 */
		public static enum Remedy
		{
			/**
			 * Nothing you can really do, hopefully the library can soldier on.
			 */
			WAIT_AND_SEE,

			/**
			 * Calling {@link BleManager#reset()} is probably in order.
			 *
			 * @see BleManager#reset()
			 */
			RESET_BLE,

			/**
			 * Might want to notify your user that a phone restart is in order.
			 */
			RESTART_PHONE;
		}

		/**
		 * Struct passed to {@link BleManager.UhOhListener#onEvent(BleManager.UhOhListener.UhOhEvent)}.
		 */
		@com.idevicesinc.sweetblue.annotations.Immutable
		public static class UhOhEvent extends com.idevicesinc.sweetblue.utils.Event
		{
			/**
			 * The manager associated with the {@link BleManager.UhOhListener.UhOhEvent}
			 */
			public BleManager manager(){  return m_manager;  }
			private final BleManager m_manager;

			/**
			 * Returns the type of {@link BleManager.UhOhListener.UhOh} that occurred.
			 */
			public UhOh uhOh(){  return m_uhOh;  }
			private final UhOh m_uhOh;

			/**
			 * Forwards {@link BleManager.UhOhListener.UhOh#getRemedy()}.
			 */
			public Remedy remedy(){  return uhOh().getRemedy();  };

			UhOhEvent(BleManager manager, UhOh uhoh)
			{
				m_manager = manager;
				m_uhOh = uhoh;
			}

			@Override public String toString()
			{
				return Utils_String.toString
				(
					this.getClass(),
					"uhOh",			uhOh(),
					"remedy",		remedy()
				);
			}
		}

		/**
		 * Run for the hills.
		 */
		void onEvent(final UhOhEvent e);
	}

	/**
	 * Provide an implementation to {@link BleManager#reset(BleManager.ResetListener)}
	 * to be notified when a reset operation is complete.
	 *
	 * @see BleManager#reset(BleManager.ResetListener)
	 */
	@com.idevicesinc.sweetblue.annotations.Lambda
	public static interface ResetListener
	{
		/**
		 * Enumeration of the progress of the reset.
		 * More entries may be added in the future.
		 */
		public static enum Progress
		{
			/**
			 * The reset has completed successfully.
			 */
			COMPLETED;
		}

		/**
		 * Struct passed to {@link BleManager.ResetListener#onEvent(BleManager.ResetListener.ResetEvent)}.
		 */
		@Immutable
		public static class ResetEvent extends Event
		{
			/**
			 * The {@link BleManager} the reset was applied to.
			 */
			public BleManager manager(){  return m_manager;  }
			private final BleManager m_manager;

			/**
			 * The progress of the reset.
			 */
			public Progress progress(){  return m_progress;  }
			private final Progress m_progress;

			ResetEvent(BleManager manager, Progress progress)
			{
				m_manager = manager;
				m_progress = progress;
			}

			@Override public String toString()
			{
				return Utils_String.toString
				(
					this.getClass(),
					"progress",		progress()
				);
			}
		}

		/**
		 * The reset event, for now only fired when the reset is completed. Hopefully the bluetooth stack is OK now.
		 */
		void onEvent(final ResetEvent e);
	}

	/**
	 * Mostly only for SweetBlue library developers. Provide an implementation to
	 * {@link BleManager#setListener_Assert(BleManager.AssertListener)} to be notified whenever
	 * an assertion fails through {@link BleManager#ASSERT(boolean, String)}.
	 */
	@Advanced
	@com.idevicesinc.sweetblue.annotations.Lambda
	public static interface AssertListener
	{
		/**
		 * Struct passed to {@link BleManager.AssertListener#onEvent(BleManager.AssertListener.AssertEvent)}.
		 */
		@Immutable
		public static class AssertEvent extends Event
		{
			/**
			 * The {@link BleManager} instance for your application.
			 */
			public BleManager manager(){  return m_manager;  }
			private final BleManager m_manager;

			/**
			 * Message associated with the assert, or an empty string.
			 */
			public String message(){  return m_message;  }
			private final String m_message;

			/**
			 * Stack trace leading up to the assert.
			 */
			public StackTraceElement[] stackTrace(){  return m_stackTrace;  }
			private final StackTraceElement[] m_stackTrace;

			AssertEvent(BleManager manager, String message, StackTraceElement[] stackTrace)
			{
				m_manager = manager;
				m_message = message;
				m_stackTrace = stackTrace;
			}

			@Override public String toString()
			{
				return Utils_String.toString
				(
					this.getClass(),
					"message",			message(),
					"stackTrace",		stackTrace()
				);
			}
		}

		/**
		 * Provides additional info about the circumstances surrounding the assert.
		 */
		void onEvent(final AssertEvent e);
	}


	/**
	 * Create the singleton instance or retrieve the already-created singleton instance with default configuration options set.
	 * If you call this after you call {@link #get(android.content.Context, BleManagerConfig)} (for example in another
	 * {@link android.app.Activity}), the {@link BleManagerConfig} originally passed in will be used.
	 * Otherwise, if a new instance is to be created, this calls {@link #get(android.content.Context, BleManagerConfig)} with a {@link BleManagerConfig}
	 * instance created using the default constructor {@link BleManagerConfig#BleManagerConfig()}.
	 */
	public static BleManager get(Context context)
	{
		if( s_instance == null )
		{
			return get(context, new BleManagerConfig());
		}
		else
		{
			verifySingleton(context);

			return s_instance;
		}
	}

	/**
	 * Create the singleton instance or retrieve the already-created singleton instance with custom configuration options set.
	 * If you call this more than once (for example from a different {@link android.app.Activity}
	 * with different {@link BleManagerConfig} options set then the newer options overwrite the older options.
	 */
	public static BleManager get(Context context, BleManagerConfig config)
	{
		if( s_instance == null )
		{
			s_instance = new BleManager(context, config);

			return s_instance;
		}
		else
		{
			verifySingleton(context);

			s_instance.setConfig(config);

			return s_instance;
		}
	}

	private static void verifySingleton(Context context)
	{
		//--- DRK > Not confident how this method behaves with complex applications, multiple activities, services, widgets, etc.
		//---		Don't want to throw Errors needlessly, so commenting out for now.
//		if( s_instance != null && s_instance.getApplicationContext() != context.getApplicationContext() )
//		{
//			//--- DRK > Not sure how/if this could happen, but I never underestimate Android.
//			throw new InstantiationError("There can only be one instance of "+BleManager.class.getSimpleName() + " created per application.");
//		}
	}

	private final static long UPDATE_LOOP_WARNING_DELAY = 10000;

	private final Context m_context;
	private UpdateRunnable m_updateRunnable;
	private final P_ScanFilterManager m_filterMngr;
	final P_BluetoothCrashResolver m_crashResolver;
	private			P_Logger m_logger;
			  BleManagerConfig m_config;
		final P_DeviceManager m_deviceMngr;
		final P_DeviceManager m_deviceMngr_cache;
	final P_BleManager_Listeners m_listeners;
	final P_BleStateTracker m_stateTracker;
	final P_NativeBleStateTracker m_nativeStateTracker;
	private P_PostManager m_postManager;
	private P_ScanManager m_scanManager;
	private final P_TaskQueue m_taskQueue;
	private 	P_UhOhThrottler m_uhOhThrottler;
				P_WakeLockManager m_wakeLockMngr;

			BleDevice.HistoricalDataLoadListener m_historicalDataLoadListener;
			DiscoveryListener m_discoveryListener;
	private P_WrappingResetListener m_resetListeners;
	private AssertListener m_assertionListener;
			DeviceStateListener m_defaultDeviceStateListener;
			BleDevice.ConnectionFailListener m_defaultConnectionFailListener;
			BleServer.ConnectionFailListener m_defaultConnectionFailListener_server;
			BleDevice.BondListener m_defaultBondListener;
			ReadWriteListener m_defaultReadWriteListener;
			NotificationListener m_defaultNotificationListener;
	final P_DiskOptionsManager m_diskOptionsMngr;

	private double m_timeForegrounded = 0.0;
	private long m_timeTurnedOn = 0;
	private long m_lastTaskExecution;
	private long m_currentTick;
	private boolean m_isForegrounded = false;
	private boolean m_ready = false;
	private boolean m_unitTestCheckDone = false;
    private long m_lastUpdateLoopWarning = 0;

    BleServer.StateListener m_defaultServerStateListener;
	BleServer.OutgoingListener m_defaultServerOutgoingListener;
	IncomingListener m_defaultServerIncomingListener;
	BleServer.ServiceAddListener m_serviceAddListener;
	BleServer.AdvertisingListener m_advertisingListener;
//    final P_ServerManager m_serverMngr;

	final Backend_HistoricalDatabase m_historicalDatabase;

	BleServer m_server = null;

	static BleManager s_instance = null;

	/**
	 * Field for app to associate any data it wants with the singleton instance of this class
	 * instead of having to subclass or manage associative hash maps or something.
	 * The library does not touch or interact with this data in any way.
	 *
	 * @see BleDevice#appData
	 * @see BleServer#appData
	 */
	public Object appData;


	private BleManager(Context context, BleManagerConfig config)
	{
		m_context = context.getApplicationContext();

		m_currentTick = System.currentTimeMillis();

		addLifecycleCallbacks();
		m_config = config.clone();
		initLogger(this);
		m_scanManager = new P_ScanManager(this);
		m_historicalDatabase = PU_HistoricalData.newDatabase(context, this);
		m_diskOptionsMngr = new P_DiskOptionsManager(m_context);
		m_filterMngr = new P_ScanFilterManager(this, m_config.defaultScanFilter);
		if (m_config.nativeManagerLayer.isManagerNull())
		{
			m_config.nativeManagerLayer.resetManager(m_context);
		}
		BleManagerState nativeState = BleManagerState.get(m_config.nativeManagerLayer.getState());

		if (m_timeTurnedOn == 0 && nativeState.overlaps(BluetoothAdapter.STATE_ON)) {
			m_timeTurnedOn = System.currentTimeMillis();
		}

		m_stateTracker = new P_BleStateTracker(this);
		m_stateTracker.append(nativeState, E_Intent.UNINTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
		m_nativeStateTracker = new P_NativeBleStateTracker(this);
		m_nativeStateTracker.append(nativeState, E_Intent.UNINTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
		m_taskQueue = new P_TaskQueue(this);
		m_crashResolver = new P_BluetoothCrashResolver(m_context);
		m_deviceMngr = new P_DeviceManager(this);
//		m_serverMngr = new P_ServerManager(this);
		m_deviceMngr_cache = new P_DeviceManager(this);
		m_listeners = new P_BleManager_Listeners(this);

		m_lastTaskExecution = System.currentTimeMillis();

		initConfigDependentMembers();

		m_postManager.postToUpdateThread(new Runnable()
		{
			@Override public void run()
			{
				if (!m_config.unitTest)
				{
					m_logger.printBuildInfo();
				}
			}
		});
	}

	/**
	 * Updates the config options for this instance after calling {@link #get(android.content.Context)} or {@link #get(android.content.Context, BleManagerConfig)}.
	 * Providing a <code>null</code> value will set everything back to default values.
	 */
	public final void setConfig(@Nullable(Prevalence.RARE) BleManagerConfig config_nullable)
	{
		m_config = config_nullable != null ? config_nullable.clone() : new BleManagerConfig();
		initLogger(this);
		initConfigDependentMembers();
	}

	public final BleManagerConfig getConfigClone()
	{
		return m_config.clone();
	}

	private void initLogger(BleManager mgr)
	{
		m_logger = new P_Logger(mgr, m_config.debugThreadNames, m_config.uuidNameMaps, m_config.loggingEnabled, m_config.logger);
	}

	private void initConfigDependentMembers()
	{
		// Check if one of the classes from the unit test module can be loaded. If so, we're running in a
		// unit test. Otherwise, we aren't
		try
		{
			Class.forName("com.idevicesinc.sweetblue.UnitTestLogger");
			m_config.unitTest = true;
		}
		catch (ClassNotFoundException e)
		{
			m_config.unitTest = false;
		}

		m_listeners.updatePollRate(m_config.defaultStatePollRate);

		m_filterMngr.updateFilter(m_config.defaultScanFilter);

		if (m_config.nativeManagerLayer instanceof P_AndroidBluetoothManager)
		{
			((P_AndroidBluetoothManager) m_config.nativeManagerLayer).setBleManager(this);
		}
		if (m_config.nativeManagerLayer.isManagerNull())
		{
			m_config.nativeManagerLayer.resetManager(m_context);
		}

		boolean startUpdate = true;

		if (m_updateRunnable != null)
		{
			m_updateRunnable.m_shutdown = false;
			m_postManager.removeUpdateCallbacks(m_updateRunnable);
		}
		else
		{
			if (Interval.isEnabled(m_config.autoUpdateRate))
			{
				m_updateRunnable = new UpdateRunnable(m_config.autoUpdateRate.millis());
			}
			else
			{
				startUpdate = false;
				m_updateRunnable = new UpdateRunnable();
			}
		}

		if (m_config.scanMode != null)
		{
			m_config.scanApi = BleScanApi.fromBleScanMode(m_config.scanMode);
			if (m_config.scanMode.isLollipopScanMode())
			{
				m_config.scanPower = BleScanPower.fromBleScanMode(m_config.scanMode);
			}
			m_config.scanMode = null;
		}

		m_uhOhThrottler = new P_UhOhThrottler(this, Interval.secs(m_config.uhOhCallbackThrottle));

		if( m_wakeLockMngr == null )
		{
			m_wakeLockMngr = new P_WakeLockManager(this, m_config.manageCpuWakeLock);
		}
		else if( m_wakeLockMngr != null && m_config.manageCpuWakeLock == false )
		{
			m_wakeLockMngr.clear();
			m_wakeLockMngr = new P_WakeLockManager(this, m_config.manageCpuWakeLock);
		}

		if( m_config.defaultDiscoveryListener != null )
		{
			this.setListener_Discovery(m_config.defaultDiscoveryListener);
		}

		initPostManager();

		if( startUpdate )
		{
			m_postManager.postToUpdateThreadDelayed(m_updateRunnable, m_config.autoUpdateRate.millis());
		}
	}

	private void initPostManager()
	{
		P_SweetHandler update;
		P_SweetHandler ui;
		if (m_config.runOnMainThread)
		{
			update = new P_SweetUIHandler(this);
			ui = update;
		}
		else
		{
			ui = new P_SweetUIHandler(this);
			update = new P_SweetBlueThread();
			update.post(new Runnable()
			{
				@Override public void run()
				{
					m_logger.setUpdateThread(android.os.Process.myTid());
				}
			});
		}
		ui.post(new Runnable()
		{
			@Override public void run()
			{
				m_logger.setMainThread(android.os.Process.myTid());
			}
		});
		m_postManager = new P_PostManager(this, ui, update);
	}

	/**
	 * Returns whether the manager is in any of the provided states.
	 */
	public final boolean isAny(BleManagerState ... states)
	{
		for( int i = 0; i < states.length; i++ )
		{
			if( is(states[i]) )  return true;
		}

		return false;
	}

	/**
	 * Returns whether the manager is in all of the provided states.
	 *
	 * @see #isAny(BleManagerState...)
	 */
	public final boolean isAll(BleManagerState... states)
	{
		for (int i = 0; i < states.length; i++)
		{
			if( !is(states[i]) )  return false;
		}

		return true;
	}

	/**
	 * Returns whether the manager is in the provided state.
	 *
	 * @see #isAny(BleManagerState...)
	 */
	public final boolean is(final BleManagerState state)
	{
		return state.overlaps(getStateMask());
	}

	/**
	 * Returns <code>true</code> if there is partial bitwise overlap between the provided value and {@link #getStateMask()}.
	 *
	 * @see #isAll(int)
	 */
	public final boolean isAny(final int mask_BleManagerState)
	{
		return (getStateMask() & mask_BleManagerState) != 0x0;
	}

	/**
	 * Returns <code>true</code> if there is complete bitwise overlap between the provided value and {@link #getStateMask()}.
	 *
	 * @see #isAny(int)
	 */
	public final boolean isAll(final int mask_BleManagerState)
	{
		return (getStateMask() & mask_BleManagerState) == mask_BleManagerState;
	}

	/**
	 * See similar comment for {@link BleDevice#getTimeInState(BleDeviceState)}.
	 *
	 * @see BleDevice#getTimeInState(BleDeviceState)
	 */
	public final Interval getTimeInState(BleManagerState state)
	{
		return Interval.millis(m_stateTracker.getTimeInState(state.ordinal()));
	}

	/**
	 * See similar comment for {@link BleDevice#getTimeInState(BleDeviceState)}.
	 *
	 * @see BleDevice#getTimeInState(BleDeviceState)
	 */
	public final Interval getTimeInNativeState(BleManagerState state)
	{
		return Interval.millis(m_nativeStateTracker.getTimeInState(state.ordinal()));
	}

	/**
	 * Checks the underlying stack to see if BLE is supported on the phone.
	 */
	public final boolean isBleSupported()
	{
		PackageManager pm = m_context.getPackageManager();
		boolean hasBLE = pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);

		return hasBLE;
	}

	/**
	 * Checks to see if the device is running an Android OS which supports
	 * advertising.
	 */
	public final boolean isAdvertisingSupportedByAndroidVersion()
	{
		return Utils.isLollipop();
	}

	/**
	 * Checks to see if the device supports advertising.
	 */
	public final boolean isAdvertisingSupportedByChipset()
	{
		if( isAdvertisingSupportedByAndroidVersion() )
		{
			return managerLayer().isMultipleAdvertisementSupported();
		}
		else
		{
			return false;
		}
	}

	/**
	 * Checks to see if the device supports advertising BLE services.
	 */
	public final boolean isAdvertisingSupported()
	{
		return isAdvertisingSupportedByAndroidVersion() && isAdvertisingSupportedByChipset();
	}

	/**
	 * Disables BLE if manager is {@link BleManagerState#ON}. This disconnects all current
	 * connections, stops scanning, and forgets all discovered devices.
	 */
	public final void turnOff()
	{
		turnOff_private(false);
	}

	/**
	 * Returns the native manager.
	 */
	@Advanced
	public final BluetoothManager getNative()
	{
		return managerLayer().getNativeManager();
	}

	/**
	 * Returns the native bluetooth adapter.
	 */
	@Advanced
	public final BluetoothAdapter getNativeAdapter()
	{
		return managerLayer().getNativeAdaptor();
	}

	final P_NativeManagerLayer managerLayer()
	{
		return m_config.nativeManagerLayer;
	}

	/**
	 * Sets a default backup {@link BleNode.HistoricalDataLoadListener} that will be invoked
	 * for all historical data loads to memory for all uuids for all devices.
	 */
	public final void setListener_HistoricalDataLoad(@Nullable(Prevalence.NORMAL) final BleNode.HistoricalDataLoadListener listener_nullable)
	{
		m_historicalDataLoadListener = listener_nullable;
	}

	/**
	 * Set a listener here to be notified whenever we encounter an {@link UhOh}.
	 */
	public final void setListener_UhOh(@Nullable(Prevalence.NORMAL) UhOhListener listener_nullable)
	{
		m_uhOhThrottler.setListener(listener_nullable);
	}

	/**
	 * Set a listener here to be notified whenever {@link #ASSERT(boolean)} fails.
	 * Mostly for use by internal library developers.
	 */
	public final void setListener_Assert(@Nullable(Prevalence.NORMAL) AssertListener listener_nullable)
	{
		m_assertionListener = listener_nullable;
	}

	/**
	 * Set a listener here to be notified whenever a {@link BleDevice} is discovered, rediscovered, or undiscovered.
	 */
	public final void setListener_Discovery(@Nullable(Prevalence.NORMAL) DiscoveryListener listener_nullable)
	{
		m_discoveryListener = listener_nullable;
	}

	/**
	 * Returns the discovery listener set with {@link #setListener_Discovery(BleManager.DiscoveryListener)} or
	 * {@link BleManagerConfig#defaultDiscoveryListener}, or <code>null</code> if not set.
	 */
	public final DiscoveryListener getListener_Discovery()
	{
		return m_discoveryListener;
	}

	/**
	 * Set a listener here to be notified whenever this manager's {@link BleManagerState} changes.
	 *
	 * @deprecated - This will be removed in version 3. This class has been refactored to {@link ManagerStateListener}.
	 */
	public final void setListener_State(@Nullable(Prevalence.NORMAL) final StateListener listener_nullable)
	{
		if (listener_nullable == null)
		{
			m_stateTracker.setListener(null);
		}
		else
		{
			m_stateTracker.setListener(new ManagerStateListener()
			{
				@Override public void onEvent(StateListener.StateEvent e)
				{
					if (listener_nullable != null)
					{
						listener_nullable.onEvent(e);
					}
				}
			});
		}
	}

	/**
	 * Set a listener here to be notified whenever this manager's {@link BleManagerState} changes.
	 */
	public final void setListener_State(@Nullable(Prevalence.NORMAL) ManagerStateListener listener_nullable)
	{
		m_stateTracker.setListener(listener_nullable);
	}

	/**
	 * Convenience method to listen for all changes in {@link BleDeviceState} for all devices.
	 * The listener provided will get called in addition to and after the listener, if any, provided
	 * to {@link BleDevice#setListener_State(BleDevice.StateListener)}.
	 *
	 * @see BleDevice#setListener_State(BleDevice.StateListener)
	 *
	 * @deprecated - This will be removed in version 3. It has been refactored to {@link DeviceStateListener}.
	 */
	public final void setListener_DeviceState(@Nullable(Prevalence.NORMAL) final BleDevice.StateListener listener_nullable)
	{
		if (listener_nullable == null)
		{
			m_defaultDeviceStateListener = null;
		}
		else
		{
			m_defaultDeviceStateListener = new DeviceStateListener()
			{
				@Override public void onEvent(BleDevice.StateListener.StateEvent e)
				{
					if (listener_nullable != null)
					{
						listener_nullable.onEvent(e);
					}
				}
			};
		}
	}

	/**
	 * Convenience method to listen for all changes in {@link BleDeviceState} for all devices.
	 * The listener provided will get called in addition to and after the listener, if any, provided
	 * to {@link BleDevice#setListener_State(BleDevice.StateListener)}.
	 *
	 * @see BleDevice#setListener_State(BleDevice.StateListener)
	 */
	public final void setListener_DeviceState(@Nullable(Prevalence.NORMAL) DeviceStateListener listener_nullable)
	{
		m_defaultDeviceStateListener = listener_nullable;
	}

	/**
	 * Convenience method to handle server connection fail events at the manager level. The listener provided
	 * will only get called if the server whose connection failed doesn't have a listener provided to
	 * {@link BleServer#setListener_ConnectionFail(BleServer.ConnectionFailListener)}. This is unlike the behavior
	 * behind (for example) {@link #setListener_ServerState(BleServer.StateListener)} because
	 * {@link BleServer.ConnectionFailListener#onEvent(BleServer.ConnectionFailListener.ConnectionFailEvent)} requires a return value.
	 *
	 * @see BleServer#setListener_ConnectionFail(BleServer.ConnectionFailListener)
	 */
	public final void setListener_ConnectionFail_Server(@Nullable(Prevalence.NORMAL) BleServer.ConnectionFailListener listener_nullable)
	{
		m_defaultConnectionFailListener_server = listener_nullable;
	}

	/**
	 * Convenience method to handle server request events at the manager level. The listener provided
	 * will only get called if the server receiving a request doesn't have a listener provided to
	 * {@link BleServer#setListener_Incoming(BleServer.IncomingListener)} . This is unlike the behavior (for example)
	 * behind {@link #setListener_Outgoing(BleServer.OutgoingListener)} because
	 * {@link BleServer.IncomingListener#onEvent(BleServer.IncomingListener.IncomingEvent)} requires a return value.
	 *
	 * @see BleServer#setListener_Incoming(IncomingListener)
	 */
	public final void setListener_Incoming(@Nullable(Prevalence.NORMAL) BleServer.IncomingListener listener_nullable)
	{
		m_defaultServerIncomingListener = listener_nullable;
	}

	/**
	 * Convenience method to listen for all service addition events for all servers.
	 * The listener provided will get called in addition to and after the listener, if any, provided
	 * to {@link BleServer#setListener_ServiceAdd(BleServer.ServiceAddListener)}.
	 *
	 * @see BleServer#setListener_ServiceAdd(BleServer.ServiceAddListener)
	 */
	public final void setListener_ServiceAdd(@Nullable(Prevalence.NORMAL) BleServer.ServiceAddListener listener_nullable)
	{
		m_serviceAddListener = listener_nullable;
	}

	/**
	 * Convenience method to listen for all changes in {@link BleServerState} for all servers.
	 * The listener provided will get called in addition to and after the listener, if any, provided
	 * to {@link BleServer#setListener_State(BleServer.StateListener)}.
	 *
	 * @see BleServer#setListener_State(BleServer.StateListener)
	 */
	public final void setListener_ServerState(@Nullable(Prevalence.NORMAL) BleServer.StateListener listener_nullable)
	{
		m_defaultServerStateListener = listener_nullable;
	}

	/**
	 * Convenience method to listen for completion of all outgoing messages from
	 * {@link BleServer} instances. The listener provided will get called in addition to and after the listener, if any, provided
	 * to {@link BleServer#setListener_Outgoing(BleServer.OutgoingListener)}.
	 *
	 * @see BleServer#setListener_Outgoing(BleServer.OutgoingListener)
	 */
	public final void setListener_Outgoing(@Nullable(Prevalence.NORMAL) BleServer.OutgoingListener listener_nullable)
	{
		m_defaultServerOutgoingListener = listener_nullable;
	}

	/**
	 * Convenience method to handle connection fail events at the manager level. The listener provided
	 * will only get called if the device whose connection failed doesn't have a listener provided to
	 * {@link BleDevice#setListener_ConnectionFail(ConnectionFailListener)}. This is unlike the behavior
	 * behind {@link #setListener_DeviceState(BleDevice.StateListener)} because
	 * {@link BleDevice.ConnectionFailListener#onEvent(BleDevice.ConnectionFailListener.ConnectionFailEvent)} requires a return value.
	 *
	 * @see BleDevice#setListener_ConnectionFail(BleDevice.ConnectionFailListener)
	 */
	public final void setListener_ConnectionFail(@Nullable(Prevalence.NORMAL) BleDevice.ConnectionFailListener listener_nullable)
	{
		m_defaultConnectionFailListener = listener_nullable;
	}

	/**
	 * Convenience method to set a default back up listener for all {@link BondEvent}s across all {@link BleDevice} instances.
	 */
	public final void setListener_Bond(@Nullable(Prevalence.NORMAL) BleDevice.BondListener listener_nullable)
	{
		m_defaultBondListener = listener_nullable;
	}

	/**
	 * Sets a default backup {@link ReadWriteListener} that will be called for all {@link BleDevice} instances.
	 * <br><br>
	 * TIP: Place some analytics code in the listener here.
	 *
	 * @deprecated - This will be removed in version 3. Use {@link ReadWriteListener} instead (it was refactored to be in it's own class file, rather than an inner class).
	 */
	public final void setListener_ReadWrite(@Nullable(Prevalence.NORMAL) final com.idevicesinc.sweetblue.BleDevice.ReadWriteListener listener_nullable)
	{
		if (listener_nullable != null)
		{
			m_defaultReadWriteListener = new ReadWriteListener()
			{
				@Override public void onEvent(BleDevice.ReadWriteListener.ReadWriteEvent e)
				{
					if (listener_nullable != null)
					{
						listener_nullable.onEvent(e);
					}
				}
			};
		}
		else
		{
			m_defaultReadWriteListener = null;
		}
	}

	/**
	 * Sets a default backup {@link ReadWriteListener} that will be called for all {@link BleDevice} instances.
	 * <br><br>
	 * TIP: Place some analytics code in the listener here.
	 */
	public final void setListener_Read_Write(@Nullable(Prevalence.NORMAL) ReadWriteListener listener_nullable)
	{
		m_defaultReadWriteListener = listener_nullable;
	}

	public final void setListener_Notification(@Nullable(Prevalence.NORMAL) NotificationListener listener_nullable)
	{
		m_defaultNotificationListener = listener_nullable;
	}

	/**
	 * Set a listener here to be notified whenever this manager's native {@link BleManagerState} changes.
	 *
	 * @deprecated {@link NativeStateListener} is being removed in v3. There is no alternative. Just use SweetBlue's internal state management (which
	 * is keyed from the native state, and callbacks).
	 */
	@Deprecated
	public final void setListener_NativeState(NativeStateListener listener)
	{
		m_nativeStateTracker.setListener(listener);
	}

	/**
	 * Set a listener here to be notified of the result of starting to advertise.
	 */
	public final void setListener_Advertising(BleServer.AdvertisingListener listener)
	{
		m_advertisingListener = listener;
	}

	/**
	 * Manually starts a periodic scan. This is the post-constructor runtime equivalent to setting
	 * {@link BleManagerConfig#autoScanActiveTime} and {@link BleManagerConfig#autoScanPauseInterval}, so see
	 * their comments for more detail. Calling this forever-after overrides the options you set
	 * in {@link BleManagerConfig}.
	 *
	 * @see BleManagerConfig#autoScanActiveTime
	 * @see BleManagerConfig#autoScanPauseInterval
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void startPeriodicScan(Interval scanActiveTime, Interval scanPauseTime)
	{
		startPeriodicScan(scanActiveTime, scanPauseTime, (ScanFilter) null, (DiscoveryListener) null);
	}

	/**
	 * Same as {@link #startPeriodicScan(Interval, Interval)} but calls {@link #setListener_Discovery(BleManager.DiscoveryListener)} for you too.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void startPeriodicScan(Interval scanActiveTime, Interval scanPauseTime, DiscoveryListener discoveryListener)
	{
		startPeriodicScan(scanActiveTime, scanPauseTime, (ScanFilter) null, discoveryListener);
	}

	/**
	 * Same as {@link #startPeriodicScan(Interval, Interval)} but adds a filter too.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void startPeriodicScan(Interval scanActiveTime, Interval scanPauseTime, BleManagerConfig.ScanFilter filter)
	{
		startPeriodicScan(scanActiveTime, scanPauseTime, filter, (DiscoveryListener) null);
	}

	/**
	 * Same as {@link #startPeriodicScan(Interval, Interval)} but calls {@link #setListener_Discovery(BleManager.DiscoveryListener)} for you too and adds a filter.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void startPeriodicScan(Interval scanActiveTime, Interval scanPauseTime, BleManagerConfig.ScanFilter filter, DiscoveryListener discoveryListener)
	{
		showScanWarningIfNeeded();

		if( discoveryListener != null )
		{
			setListener_Discovery(discoveryListener);
		}

		m_filterMngr.add(filter);

		m_config.autoScanActiveTime = scanActiveTime;
		m_config.autoScanPauseInterval = scanPauseTime;

		if( doAutoScan() )
		{
			startScan_private(new ScanOptions().scanPeriodically(m_config.autoScanActiveTime, m_config.autoScanPauseInterval));
		}
	}

	/**
	 * Same as {@link #stopPeriodicScan()} but will also unregister any {@link BleManagerConfig.ScanFilter} provided
	 * through {@link #startPeriodicScan(Interval, Interval, BleManagerConfig.ScanFilter)} or other overloads.
	 */
	public final void stopPeriodicScan(final ScanFilter filter)
	{
		m_filterMngr.remove(filter);

		stopPeriodicScan();
	}

	/**
	 * Stops a periodic scan previously started either explicitly with {@link #startPeriodicScan(Interval, Interval)} or through
	 * the {@link BleManagerConfig#autoScanActiveTime} and {@link BleManagerConfig#autoScanPauseInterval} config options.
	 */
	public final void stopPeriodicScan()
	{
		m_config.autoScanActiveTime = Interval.DISABLED;

		if( false == m_scanManager.isInfiniteScan() )
		{
			this.stopScan();
		}
	}

	/**
	 * Starts a scan that will continue indefinitely until {@link #stopScan()} is called.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan()
	{
		return startScan(Interval.INFINITE);
	}

	/**
	 * Calls {@link #startScan(Interval, BleManagerConfig.ScanFilter)} with {@link Interval#INFINITE}.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan(ScanFilter filter)
	{
		return startScan(Interval.INFINITE, filter, (DiscoveryListener) null);
	}

	/**
	 * Same as {@link #startScan()} but also calls {@link #setListener_Discovery(BleManager.DiscoveryListener)} for you.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan(DiscoveryListener discoveryListener)
	{
		return startScan(Interval.INFINITE, (ScanFilter) null, discoveryListener);
	}

	/**
	 * Overload of {@link #startScan(Interval, BleManagerConfig.ScanFilter, BleManager.DiscoveryListener)}
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan(Interval scanTime, ScanFilter filter)
	{
		return startScan(scanTime, filter, (DiscoveryListener) null);
	}

	/**
	 * Overload of {@link #startScan(Interval, BleManagerConfig.ScanFilter, BleManager.DiscoveryListener)}
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan(Interval scanTime, DiscoveryListener discoveryListener)
	{
		return startScan(scanTime, (ScanFilter) null, discoveryListener);
	}

	/**
	 * Same as {@link #startScan()} but also calls {@link #setListener_Discovery(BleManager.DiscoveryListener)} for you.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan(ScanFilter filter, DiscoveryListener discoveryListener)
	{
		return startScan(Interval.INFINITE, filter, discoveryListener);
	}

	/**
	 * Starts a scan that will generally last for the given time (roughly).
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan(Interval scanTime)
	{
		return startScan(scanTime, (ScanFilter) null, (DiscoveryListener) null);
	}

	/**
	 * Same as {@link #startScan(Interval)} but also calls {@link #setListener_Discovery(BleManager.DiscoveryListener)} for you.
	 * <br><br>
	 * WARNING: For {@link android.os.Build.VERSION_CODES#M} and up, in order for this method to return scan events
	 * through {@link ScanFilter} you must have {@link android.Manifest.permission#ACCESS_COARSE_LOCATION} or {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
	 * in your AndroidManifest.xml, AND enabled at runtime (see {@link #isLocationEnabledForScanning_byRuntimePermissions()} and {@link #turnOnLocationWithIntent_forPermissions(Activity, int)}),
	 * AND location services should be enabled (see {@link #isLocationEnabledForScanning_byOsServices()} and {@link #isLocationEnabledForScanning_byOsServices()}).
	 * <br><br>
	 * The assumed reason why location must be enabled is that an app might scan for bluetooth devices like iBeacons with known physical locations and unique advertisement packets.
	 * Knowing the physical locations, the app could report back that you're definitely within ~50 ft. of a given longitude and latitude. With multiple beacons involved and/or fine-tuned RSSI-based
	 * distance calculations the location could get pretty accurate. For example a department store app could sprinkle a few dozen beacons throughout its store and
	 * if you had their app running they would know exactly where you are. Not an everyday concern, and it makes BLE even more annoying to implement on Android,
	 * but Google is understandably erring on the side of privacy and security for its users.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 *
	 * @return <code>true</code> if scan started, <code>false></code> otherwise - usually this means this manager is not {@link BleManagerState#ON}.
	 */
	public final boolean startScan(Interval scanTime, ScanFilter filter, DiscoveryListener discoveryListener)
	{
		showScanWarningIfNeeded();

		return startScan_private(new ScanOptions().scanFor(scanTime).withScanFilter(filter).withDiscoveryListener(discoveryListener));
	}

	public final boolean startScan(ScanOptions options)
	{
		showScanWarningIfNeeded();

		return startScan_private(options);
	}

	private void showScanWarningIfNeeded()
	{
		if( false == isLocationEnabledForScanning() )
		{
			final String ENABLED = "enabled";
			final String DISABLED = "disabled";

			final boolean reasonA = isLocationEnabledForScanning_byManifestPermissions();
			final boolean reasonB = isLocationEnabledForScanning_byRuntimePermissions();
			final boolean reasonC = isLocationEnabledForScanning_byOsServices();
			final String enabledA = reasonA ? ENABLED : DISABLED;
			final String enabledB = reasonB ? ENABLED : DISABLED;
			final String enabledC = reasonC ? ENABLED : DISABLED;

			Log.w
					(
							BleManager.class.getSimpleName(),

							"As of Android M, in order for low energy scan results to return you must have the following:\n" +
									"(A) " + Manifest.permission.ACCESS_COARSE_LOCATION + " or " + Manifest.permission.ACCESS_FINE_LOCATION + " in your AndroidManifest.xml.\n" +
									"(B) Runtime permissions for aforementioned location permissions as described at https://developer.android.com/training/permissions/requesting.html.\n" +
									"(C) Location services enabled, the same as if you go to OS settings App and enable Location.\n" +
									"It looks like (A) is " + enabledA + ", (B) is " + enabledB + ", and (C) is " + enabledC + ".\n" +
									"Various methods like BleManager.isLocationEnabledForScanning*() overloads and BleManager.turnOnLocationWithIntent*() overloads can help with this painful process.\n" +
									"Good luck!"
					);
		}
	}

//
//    final boolean startScan_private(Interval scanTime, ScanFilter filter, DiscoveryListener discoveryListener, final boolean isPoll)
//    {
	final boolean startScan_private(ScanOptions options)
	{
		if (m_taskQueue.isInQueue(P_Task_Scan.class, this))
		{
			getLogger().w("A startScan method was called when there's already a scan task in the queue!");
			return false;
		}

		if( false == isBluetoothEnabled() )
		{
			m_logger.e(BleManager.class.getSimpleName() + " is not " + ON + "! Please use the turnOn() method first.");

			return false;
		}

		final P_Task_Scan scanTask = m_taskQueue.get(P_Task_Scan.class, this);

		if( scanTask != null )
		{
			scanTask.resetTimeout(options.m_scanTime.secs());
		}
		else
		{
			ASSERT(!m_taskQueue.isCurrentOrInQueue(P_Task_Scan.class, this));

			m_stateTracker.append(BleManagerState.STARTING_SCAN, E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE);

			m_scanManager.resetTimeNotScanning();
			options.m_scanTime = options.m_scanTime.secs() < 0.0 ? Interval.INFINITE : options.m_scanTime;
			m_scanManager.setInfiniteScan(options.m_scanTime.equals(Interval.INFINITE), options.m_forceIndefinite);

			if( options.m_discoveryListener != null )
			{
				setListener_Discovery(options.m_discoveryListener);
			}


			if (options.m_scanFilter != null)
			{
				m_filterMngr.add(options.m_scanFilter);
			}

			if (options.m_isPeriodic)
			{
				m_config.autoScanActiveTime = options.m_scanTime;
				m_config.autoScanPauseInterval = options.m_pauseTime;
			}

            PE_TaskPriority pri = options.m_isPriorityScan ? PE_TaskPriority.CRITICAL : null;

			boolean startScan = true;

			if (options.m_isPeriodic)
			{
				if (!doAutoScan() )
				{
					startScan = false;
				}
			}

			if (startScan)
			{
				m_taskQueue.add(new P_Task_Scan(this, m_listeners.getScanTaskListener(), options.m_scanTime.secs(), options.m_isPeriodic, pri));
			}
		}

		return true;
	}

	/**
	 * Requires the {@link android.Manifest.permission#WAKE_LOCK} permission. Gives you access to the internal
	 * wake lock as a convenience and eventually calls {@link android.os.PowerManager.WakeLock#acquire()}.
	 *
	 * @see BleManagerConfig#manageCpuWakeLock
	 */
	@Advanced
	public final void pushWakeLock()
	{
		m_wakeLockMngr.push();
	}

	/**
	 * Opposite of {@link #pushWakeLock()}, eventually calls {@link android.os.PowerManager.WakeLock#release()}.
	 */
	@Advanced
	public final void popWakeLock()
	{
		m_wakeLockMngr.pop();
	}

	/**
	 * Fires a callback to {@link BleManager.AssertListener} if condition is false. Will post a {@link android.util.Log#ERROR}-level
	 * message with a stack trace to the console as well if {@link BleManagerConfig#loggingEnabled} is true.
	 */
	@Advanced
	public final boolean ASSERT(boolean condition)
	{
		return ASSERT(condition, "");
	}

	/**
	 * Same as {@link #ASSERT(boolean)} but with an added message.
	 */
	@Advanced
	public final boolean ASSERT(boolean condition, String message)
	{
		if( !condition )
		{
			Exception dummyException = null;
			message = message != null ? message : "";

			if( m_config.loggingEnabled || m_assertionListener != null )
			{
				dummyException = new Exception();
			}

			if( m_config.loggingEnabled )
			{
				Log.e(BleManager.class.getSimpleName(), "ASSERTION FAILED " + message, dummyException);
			}

			if( m_assertionListener != null )
			{
				final AssertListener.AssertEvent event = new AssertListener.AssertEvent(this, message, dummyException.getStackTrace());

				m_assertionListener.onEvent(event);
			}

			return false;
		}

		return true;
	}

	/**
	 * Returns the abstracted bitwise state mask representation of {@link BleManagerState} for this device.
	 *
	 * @see BleManagerState
	 */
	public final int getStateMask()
	{
		return m_stateTracker.getState();
	}

	/**
	 * Returns the native bitwise state mask representation of {@link BleManagerState} for this device.
	 * Similar to calling {@link android.bluetooth.BluetoothAdapter#getState()}
	 *
	 * @see BleManagerState
	 */
	@Advanced
	public final int getNativeStateMask()
	{
		return m_nativeStateTracker.getState();
	}

	/**
	 * Enables BLE if manager is currently {@link BleManagerState#OFF} or {@link BleManagerState#TURNING_OFF}, otherwise does nothing.
	 * For a convenient way to ask your user first see {@link #turnOnWithIntent(android.app.Activity, int)}.
	 */
	public final void turnOn()
	{
		if( isAny(TURNING_ON, ON) )  return;

		if( is(OFF) )
		{
			m_stateTracker.update(E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE, TURNING_ON, true, OFF, false);
		}

		m_taskQueue.add(new P_Task_TurnBleOn(this, /*implicit=*/false));
		if (m_timeTurnedOn == 0) {
			m_timeTurnedOn = System.currentTimeMillis();
		}
	}

	final void forceOn()
	{
		m_stateTracker.update(E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE, ON, true, OFF, false);
	}

	final void forceOff()
	{
		m_stateTracker.update(E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE, ON, false, OFF, true);
	}

	/**
	 * This is essentially a big red reset button for the Bluetooth stack. Use it ruthlessly
	 * when the stack seems to be acting up, like when you can't connect to a device that you should be
	 * able to connect to. It's similar to calling {@link #turnOff()} then {@link #turnOn()},
	 * but also does other things like removing all bonds (similar to {@link #unbondAll()}) and
	 * other "special sauce" such that you should use this method instead of trying to reset the
	 * stack manually with component calls.
	 * <br><br>
	 * It's good app etiquette to first prompt the user to get permission to reset because
	 * it will affect Bluetooth system-wide and in other apps.
	 *
	 *  @see BleManagerState#RESETTING
	 */
	public final void reset()
	{
		reset(null);
	}

	/**
	 * Same as {@link #reset()} but with a convenience callback for when the reset is
	 * completed and the native BLE stack is (should be) back to normal.
	 *
	 * @see BleManagerState#RESETTING
	 */
	public final void reset(ResetListener listener)
	{
		reset_private(false, listener);
	}

	/**
	 * Similar to {@link BleManager#reset()}, only this also calls the factoryReset method hidden in {@link BluetoothAdapter} after turning
	 * off BLE, and running the crash resolver. It's not clear what this method does, hence why this is marked as being experimental.
	 *
	 * @see #reset()
	 */
	@Experimental
	public final void nukeBle()
	{
		nukeBle(null);
	}

	/**
	 * Similar to {@link BleManager#reset(ResetListener)}, only this also calls the factoryReset method hidden in {@link BluetoothAdapter} after turning
	 * off BLE, and running the crash resolver. It's not clear what this method does, hence why this is marked as being experimental.
	 *
	 * @see #reset(ResetListener)
	 */
	@Experimental
	public final void nukeBle(ResetListener resetListener)
	{
		reset_private(true, resetListener);
	}

	/**
	 * Removes bonds for all devices that are {@link BleDeviceState#BONDED}.
	 * Essentially a convenience method for calling {@link BleDevice#unbond()},
	 * on each device individually.
	 */
	public final void unbondAll()
	{
		m_deviceMngr.unbondAll(null, Status.CANCELLED_FROM_UNBOND);
	}

	/**
	 * Disconnects all devices that are {@link BleDeviceState#CONNECTED}.
	 * Essentially a convenience method for calling {@link BleDevice#disconnect()},
	 * on each device individually.
	 */
	public final void disconnectAll()
	{
		m_deviceMngr.disconnectAll();
	}

	/**
	 * Same as {@link #disconnectAll()} but drills down to {@link BleDevice#disconnect_remote()} instead.
	 */
	public final void disconnectAll_remote()
	{
		m_deviceMngr.disconnectAll_remote();
	}

	/**
	 * Undiscovers all devices that are {@link BleDeviceState#DISCOVERED}.
	 * Essentially a convenience method for calling {@link BleDevice#undiscover()},
	 * on each device individually.
	 */
	public final void undiscoverAll()
	{
		m_deviceMngr.undiscoverAll();
	}

	/**
	 * If {@link #isLocationEnabledForScanning_byOsServices()} returns <code>false</code>, you can use this method to allow the user to enable location services.
	 * <br><br>
	 * NOTE: If {@link #isLocationEnabledForScanning_byOsServices()} returns <code>false</code> but all other overloads of {@link #isLocationEnabledForScanning()} return <code>true</code> then
	 * SweetBlue will fall back to classic discovery through {@link BluetoothAdapter#startDiscovery()} when you call {@link #startScan()} or overloads, so you may not have to use this.
	 *
	 * @see #isLocationEnabledForScanning_byOsServices()
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void turnOnLocationWithIntent_forOsServices(final Activity callingActivity, int requestCode)
	{
		final Intent enableLocationIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);

		callingActivity.startActivityForResult(enableLocationIntent, requestCode);
	}

	/**
	 * Overload of {@link #turnOnLocationWithIntent_forOsServices(Activity, int)} if you don't care about result.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void turnOnLocationWithIntent_forOsServices(final Activity callingActivity)
	{
		final Intent enableLocationIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);

		callingActivity.startActivity(enableLocationIntent);

		if( false == Utils.isMarshmallow() )
		{
			m_logger.w("You may use this method but since the phone is at " + Build.VERSION.SDK_INT + " and the requirement is "+Build.VERSION_CODES.M+", it is not necessary for scanning.");
		}
	}

	private static final String LOCATION_PERMISSION_NAMESPACE = "location_permission_namespace";
	private static final String LOCATION_PERMISSION_KEY = "location_permission_key";

	/**
	 * Returns <code>true</code> if {@link #turnOnLocationWithIntent_forPermissions(Activity, int)} will pop a system dialog, <code>false</code> if it will bring
	 * you to the OS's Application Settings. The <code>true</code> case happens if the app has never shown a request Location Permissions dialog or has shown a request Location Permission dialog and the user has yet to select "Never ask again". This method is used to weed out the false
	 * negative from {@link Activity#shouldShowRequestPermissionRationale(String)} when the Location Permission has never been requested. Make sure to use this in conjunction with {@link #isLocationEnabledForScanning_byRuntimePermissions()}
	 * which will tell you if permissions are already enabled.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final boolean willLocationPermissionSystemDialogBeShown(Activity callingActivity)
	{
		if( Utils.isMarshmallow() )
		{
			SharedPreferences preferences = callingActivity.getSharedPreferences(LOCATION_PERMISSION_NAMESPACE, Context.MODE_PRIVATE);
			boolean hasNeverAskAgainBeenSelected = !M_Util.shouldShowRequestPermissionRationale(callingActivity);//Call only returns true if Location permission has been previously denied. Returns false if "Never ask again" has been selected
			boolean hasLocationPermissionSystemDialogShownOnce = preferences.getBoolean(LOCATION_PERMISSION_KEY, false);

			return (!hasLocationPermissionSystemDialogShownOnce) || (hasLocationPermissionSystemDialogShownOnce && !hasNeverAskAgainBeenSelected);
		}
		else
		{
			return false;
		}
	}

	/**
	 * If {@link #isLocationEnabledForScanning_byOsServices()} returns <code>false</code>, you can use this method to allow the user to enable location
	 * through an OS intent. The result of the request (i.e. what the user chose) is passed back through {@link Activity#onRequestPermissionsResult(int, String[], int[])}
	 * with the requestCode provided as the second parameter to this method. If the user selected "Never ask again" the function will open up the app settings screen where the
	 * user can navigate to enable the permissions.
	 *
	 * @see #isLocationEnabledForScanning_byRuntimePermissions()
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void turnOnLocationWithIntent_forPermissions(final Activity callingActivity, int requestCode)
	{
		if( Utils.isMarshmallow() )
		{
			if( false == isLocationEnabledForScanning_byRuntimePermissions() && false == willLocationPermissionSystemDialogBeShown(callingActivity))
			{
				final Intent intent = new Intent();
				intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
				final Uri uri = Uri.fromParts("package", callingActivity.getPackageName(), null);
				intent.setData(uri);
				callingActivity.startActivityForResult(intent, requestCode);
			}
			else
			{
				final SharedPreferences.Editor editor = callingActivity.getSharedPreferences(LOCATION_PERMISSION_NAMESPACE, Context.MODE_PRIVATE).edit();
				editor.putBoolean(LOCATION_PERMISSION_KEY, true).commit();
				M_Util.requestPermissions(callingActivity, requestCode);
			}
		}
		else
		{
			m_logger.w("BleManager.turnOnLocationWithIntent_forPermissions() is only applicable for API levels 23 and above so this method does nothing.");
		}
	}

	/**
	 * Tells you whether a call to {@link #startScan()} (or overloads), will succeed or not. Basically a convenience for checking if both
	 * {@link #isLocationEnabledForScanning()} and {@link #is(BleManagerState)} with {@link BleManagerState#SCANNING} return <code>true</code>.
	 */
	public final boolean isScanningReady()
	{
		return isLocationEnabledForScanning() && is(ON);
	}

	/**
	 * Convenience method which reports <code>true</code> if the {@link BleManager} is in any of the following states: <br></br>
	 * {@link BleManagerState#SCANNING}, {@link BleManagerState#SCANNING_PAUSED}, {@link BleManagerState#BOOST_SCANNING}, or {@link BleManagerState#STARTING_SCAN}
	 */
	public final boolean isScanning()
	{
		return isAny(SCANNING, SCANNING_PAUSED, BOOST_SCANNING, STARTING_SCAN);
	}

	/**
	 * Returns <code>true</code> if location is enabled to a degree that allows scanning on {@link android.os.Build.VERSION_CODES#M} and above.
	 * If this returns <code>false</code> it means you're on Android M and you either (A) do not have {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
	 * (or {@link android.Manifest.permission#ACCESS_FINE_LOCATION} in your AndroidManifest.xml, see {@link #isLocationEnabledForScanning_byManifestPermissions()}), or (B)
	 * runtime permissions for aformentioned location permissions are off (see {@link #isLocationEnabledForScanning_byRuntimePermissions()} and
	 * https://developer.android.com/training/permissions/index.html), or (C) location services on the phone are disabled (see {@link #isLocationEnabledForScanning_byOsServices()}).
	 * <br><br>
	 * If this returns <code>true</code> then you are good to go for calling {@link #startScan()}.
	 *
	 * @see #startScan(Interval, BleManagerConfig.ScanFilter, DiscoveryListener)
	 *
	 * @see #turnOnLocationWithIntent_forPermissions(Activity, int)
	 * @see #turnOnLocationWithIntent_forOsServices(Activity)
	 * @see #turnOnLocationWithIntent_forOsServices(Activity, int)
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final boolean isLocationEnabledForScanning()
	{
		return managerLayer().isLocationEnabledForScanning();
	}

	/**
	 * Returns <code>true</code> if you're either pre-Android-M, or app has permission for either {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
	 * or {@link android.Manifest.permission#ACCESS_FINE_LOCATION} in your AndroidManifest.xml, <code>false</code> otherwise.
	 *
	 * @see #startScan(Interval, BleManagerConfig.ScanFilter, DiscoveryListener)
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final boolean isLocationEnabledForScanning_byManifestPermissions()
	{
		return Utils.isLocationEnabledForScanning_byManifestPermissions(getApplicationContext());
	}

	/**
	 * Returns <code>true</code> if you're either pre-Android-M, or app has runtime permissions enabled by checking
	 * <a href="https://developer.android.com/reference/android/support/v4/content/ContextCompat.html#checkSelfPermission(android.content.Context, java.lang.String)"</a>	 *
	 * See more information at https://developer.android.com/training/permissions/index.html.
	 *
	 * @see #startScan(Interval, BleManagerConfig.ScanFilter, DiscoveryListener)
	 *
	 * @see #turnOnLocationWithIntent_forPermissions(Activity, int)
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final boolean isLocationEnabledForScanning_byRuntimePermissions()
	{
		return managerLayer().isLocationEnabledForScanning_byRuntimePermissions();
	}

	/**
	 * Returns <code>true</code> if you're either pre-Android-M, or location services are enabled, the same is if you go to the Android Settings app
	 * and manually toggle Location ON/OFF.
	 * <br><br>
	 * NOTE: If this returns <code>false</code> but all other overloads of {@link #isLocationEnabledForScanning()} return <code>true</code> then
	 * SweetBlue will fall back to classic discovery through {@link BluetoothAdapter#startDiscovery()} when you call {@link #startScan()} or overloads.
	 *
	 * @see #startScan(Interval, BleManagerConfig.ScanFilter, DiscoveryListener)
	 *
	 * @see #turnOnLocationWithIntent_forOsServices(Activity)
	 * @see #turnOnLocationWithIntent_forOsServices(Activity, int)
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final boolean isLocationEnabledForScanning_byOsServices()
	{
		return managerLayer().isLocationEnabledForScanning_byOsServices();
	}

	/**
	 * Convenience method to request your user to enable ble in a "standard" way
	 * with an {@link android.content.Intent} instead of using {@link #turnOn()} directly.
	 * Result will be posted as normal to {@link android.app.Activity#onActivityResult(int, int, Intent)}.
	 * If current state is {@link BleManagerState#ON} or {@link BleManagerState#TURNING_ON}
	 * this method early outs and does nothing.
	 *
	 * @see com.idevicesinc.sweetblue.utils.BluetoothEnabler
	 */
	public final void turnOnWithIntent(Activity callingActivity, int requestCode)
	{
		if( isAny(ON, TURNING_ON) )  return;

		final Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
		callingActivity.startActivityForResult(enableBtIntent, requestCode);
	}

	/**
	 * Opposite of {@link #onPause()}, to be called from your override of {@link android.app.Activity#onResume()} for each {@link android.app.Activity}
	 * in your application. See comment for {@link #onPause()} for a similar explanation for why you should call this method.
	 */
	public final void onResume()
	{
		m_isForegrounded = true;
		m_timeForegrounded = 0.0;

		m_scanManager.onResume();
	}

	/**
	 * It's generally recommended to call this in your override of {@link android.app.Activity#onPause()} for each {@link android.app.Activity}
	 * in your application. This doesn't do much for now, just a little bookkeeping and stops scan automatically if
	 * {@link BleManagerConfig#stopScanOnPause} is <code>true</code>. Strictly speaking you don't *have* to call this method,
	 * but another good reason is for future-proofing. Later releases of this library may do other more important things
	 * in this method so it's good to have it being called just in case.
	 */
	public final void onPause()
	{
		m_isForegrounded = false;
		m_timeForegrounded = 0.0;
		m_scanManager.onPause();
	}

	/**
	 * Call this from your app's {@link android.app.Activity#onDestroy()} method.
	 * NOTE: Apparently no good way to know when app as a whole is being destroyed
	 * and not individual Activities, so keeping this package-private for now.
	 */
	final void onDestroy()
	{
		m_wakeLockMngr.clear();
		m_listeners.onDestroy();
	}

	/**
	 * Disconnects all devices, shuts down the BleManager, and it's backing thread, and unregisters any receivers that may be in use.
	 * This also clears out it's static instance. This is meant to be called upon application exit. However, to use it again,
	 * just call {@link BleManager#get(Context)}, or {@link BleManager#get(Context, BleManagerConfig)} again.
	 */
	public final void shutdown()
	{
		m_logger.e("Received shutdown call, shutting down BleManager...");
		disconnectAll();
		clearQueue_blocking();
		m_uhOhThrottler.shutdown();
		m_updateRunnable.m_shutdown = true;
		m_postManager.removeUpdateCallbacks(m_updateRunnable);
		m_postManager.quit();
		m_wakeLockMngr.clear();
		m_listeners.onDestroy();
		s_instance = null;
	}

	private void clearQueue_blocking()
	{
		m_taskQueue.clearQueueOfAll_blocking();
	}

	/**
	 * Returns the {@link android.app.Application} provided to the constructor.
	 */
	public final Context getApplicationContext()
	{
		return (Application) m_context;
	}

	/**
	 * Convenience that will call both {@link #stopPeriodicScan()} and {@link #stopScan()} for you.
	 */
	public final void stopAllScanning()
	{
		this.stopPeriodicScan();
		this.stopScan();
	}

	/**
	 * Stops a scan previously started by {@link #startScan()} or its various overloads.
	 * This will also stop the actual scan operation itself that may be ongoing due to
	 * {@link #startPeriodicScan(Interval, Interval)} or defined by {@link BleManagerConfig#autoScanActiveTime},
	 * but scanning in general will still continue periodically until you call {@link #stopPeriodicScan()}.
	 */
	public final void stopScan()
	{
		m_scanManager.setInfiniteScan(false, false);

		stopScan_private(E_Intent.INTENTIONAL);
	}

	/**
	 * Same as {@link #stopScan()} but also unregisters any filter supplied to various overloads of
	 * {@link #startScan()} or {@link #startPeriodicScan(Interval, Interval)} that take an {@link BleManagerConfig.ScanFilter}.
	 * Calling {@link #stopScan()} alone will keep any previously registered filters active.
	 */
	public final void stopScan(ScanFilter filter)
	{
		m_filterMngr.remove(filter);

		stopScan();
	}

	final void stopScan_private(E_Intent intent)
	{
		m_scanManager.resetTimeNotScanning();

		if( !m_taskQueue.succeed(P_Task_Scan.class, this) )
		{
			// I don't think this is needed, if we're clearing it from the queue below.
//			P_Task_Scan scanTask = m_taskQueue.get(P_Task_Scan.class, BleManager.this);
//			if (scanTask != null)
//				scanTask.succeed();
			m_logger.i("Clearing queue of any scan tasks...");
			m_taskQueue.clearQueueOf(P_Task_Scan.class, BleManager.this);
		}

		m_stateTracker.remove(BleManagerState.STARTING_SCAN, intent, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
		m_stateTracker.remove(BleManagerState.SCANNING, intent, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
		m_stateTracker.remove(BleManagerState.SCANNING_PAUSED, intent, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
		m_stateTracker.remove(BleManagerState.BOOST_SCANNING, intent, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
	}

	/**
	 * Gets a known {@link BleDeviceState#DISCOVERED} device by MAC address, or {@link BleDevice#NULL} if there is no such device.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice(final String macAddress)
	{
		final String macAddress_normalized = normalizeMacAddress(macAddress);

		final BleDevice device = m_deviceMngr.get(macAddress_normalized);

		if( device != null )  return device;

		return BleDevice.NULL;
	}

	/**
	 * Shortcut for checking if {@link #getDevice(String)} returns {@link BleDevice#NULL}.
	 */
	public final boolean hasDevice(final String macAddress)
	{
		return !getDevice(macAddress).isNull();
	}

	/**
	 * Calls {@link #hasDevice(String)}.
	 */
	public final boolean hasDevice(final BleDevice device)
	{
		return hasDevice(device.getMacAddress());
	}

	/**
	 * Might not be useful to outside world. Used for sanity/early-out checks internally. Keeping private for now.
	 * Does referential equality check.
	 */
	private final boolean hasDevice_private(BleDevice device)
	{
		return m_deviceMngr.has(device);
	}

	/**
	 * Returns the first device that is in the given state, or {@link BleDevice#NULL} if no match is found.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice(BleDeviceState state)
	{
		for( int i = 0; i < m_deviceMngr.getCount(); i++ )
		{
			BleDevice device = m_deviceMngr.get(i);

			if( device.is(state) )
			{
				return device;
			}
		}

		return BleDevice.NULL;
	}

	/**
	 * Returns true if we have a device in the given state.
	 */
	public final boolean hasDevice(BleDeviceState state)
	{
		return !getDevice(state).isNull();
	}

	/**
	 * Forwards {@link #getDeviceAt(int)} with index of 0.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice()
	{
		return hasDevices() ? getDeviceAt(0) : BleDevice.NULL;
	}

	/**
	 * Returns the first device that matches the query, or {@link BleDevice#NULL} if no match is found.
	 * See {@link BleDevice#is(Object...)} for the query format.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice(Object ... query)
	{
		for( int i = 0; i < m_deviceMngr.getCount(); i++ )
		{
			BleDevice device = m_deviceMngr.get(i);

			if( device.is(query) )
			{
				return device;
			}
		}

		return BleDevice.NULL;
	}

	/**
	 * Returns true if we have a device that matches the given query.
	 * See {@link BleDevice#is(Object...)} for the query format.
	 */
	public final boolean hasDevice(Object ... query)
	{
		return !getDevice(query).isNull();
	}

	/**
	 * Returns the first device which returns <code>true</code> for {@link BleDevice#isAny(int)}, or {@link BleDevice#NULL} if no such device is found.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice(final int mask_BleDeviceState)
	{
		return m_deviceMngr.getDevice(mask_BleDeviceState);
	}

	/**
	 * Returns <code>true</code> if there is any {@link BleDevice} for which {@link BleDevice#isAny(int)} with the given mask returns <code>true</code>.
	 */
	public final boolean hasDevice(final int mask_BleDeviceState)
	{
		return !getDevice(mask_BleDeviceState).isNull();
	}

	/**
	 * Offers a more "functional" means of iterating through the internal list of devices instead of
	 * using {@link #getDevices()} or {@link #getDevices_List()}.
	 */
	public final void getDevices(final ForEach_Void<BleDevice> forEach)
	{
		m_deviceMngr.forEach(forEach);
	}

	/**
	 * Same as {@link #getDevices(ForEach_Void)} but will only return devices
	 * in the given state provided.
	 */
	public final void getDevices(final ForEach_Void<BleDevice> forEach, final BleDeviceState state)
	{
		m_deviceMngr.forEach(forEach, state, true);
	}

	/**
	 * Overload of {@link #getDevices(ForEach_Void)}
	 * if you need to break out of the iteration at any point.
	 */
	public final void getDevices(final ForEach_Breakable<BleDevice> forEach)
	{
		m_deviceMngr.forEach(forEach);
	}

	/**
	 * Overload of {@link #getDevices(ForEach_Void, BleDeviceState)}
	 * if you need to break out of the iteration at any point.
	 */
	public final void getDevices(final ForEach_Breakable<BleDevice> forEach, final BleDeviceState state)
	{
		m_deviceMngr.forEach(forEach, state, true);
	}

	/**
	 * Returns the mac addresses of all devices that we know about from both current and previous
	 * app sessions.
	 */
	public final @Nullable(Prevalence.NEVER) Iterator<String> getDevices_previouslyConnected()
	{
		return m_diskOptionsMngr.getPreviouslyConnectedDevices();
	}


	/**
	 * Convenience method to return a {@link Set} of currently bonded devices. This simply calls
	 * {@link BluetoothAdapter#getBondedDevices()}, and wraps all bonded devices into separate
	 * {@link BleDevice} classes.
	 *
	 * NOTE: If the Bluetooth radio is turned off, some android devices return <code>null</code>. In this case,
	 * SweetBlue will just return an empty list.
     */
	public final Set<BleDevice> getDevices_bonded()
	{
		Set<BluetoothDevice> native_bonded_devices = managerLayer().getBondedDevices();
		// The native system can return null from the above call if the bluetooth radio is
		// turned off, so if that's the case, just return an empty Set.
		if (native_bonded_devices == null)
			return new HashSet<>(0);

		Set<BleDevice> bonded_devices = new HashSet<>(native_bonded_devices.size());
		BleDevice device;
		for (BluetoothDevice d : native_bonded_devices)
		{
			device = getDevice(d.getAddress());
			if (device.isNull())
			{
				device = newDevice(d.getAddress());
			}
			bonded_devices.add(device);
		}
		return bonded_devices;
	}

	/**
	 * Returns all the devices managed by this class. This generally includes all devices that are either.
	 * {@link BleDeviceState#ADVERTISING} or {@link BleDeviceState#CONNECTED}.
	 */
	public final @Nullable(Prevalence.NEVER) BleDeviceIterator getDevices()
	{
		return new BleDeviceIterator(getDevices_List());
	}

	/**
	 * Same as {@link #getDevices()}, but with the devices sorted using {@link BleManagerConfig#defaultListComparator}, which
	 * by default sorts by {@link BleDevice#getName_debug()}.
	 */
	public final @Nullable(Prevalence.NEVER) BleDeviceIterator getDevices_sorted()
	{
		return new BleDeviceIterator(getDevices_List_sorted());
	}

	/**
	 * Overload of {@link #getDevices()} that returns a {@link java.util.List} for you.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List()
	{
		return (List<BleDevice>) m_deviceMngr.getList().clone();
	}

	/**
	 * Same as {@link #getDevices_List()}, but sorts the list using {@link BleManagerConfig#defaultListComparator}.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List_sorted()
	{
		return (List<BleDevice>) m_deviceMngr.getList_sorted().clone();
	}

	/**
	 * Returns the total number of devices this manager is...managing.
	 * This includes all devices that are {@link BleDeviceState#DISCOVERED}.
	 */
	public final int getDeviceCount()
	{
		return m_deviceMngr.getCount();
	}

	/**
	 * Returns the number of devices that are in the current state.
	 */
	public final int getDeviceCount(BleDeviceState state)
	{
		return m_deviceMngr.getCount(state);
	}

	/**
	 * Returns the number of devices that match the given query.
	 * See {@link BleDevice#is(Object...)} for the query format.
	 */
	public final int getDeviceCount(Object ... query)
	{
		return m_deviceMngr.getCount(query);
	}

	/**
	 * Accessor into the underlying array used to store {@link BleDevice} instances.
	 * Combine with {@link #getDeviceCount()} to iterate, or you may want to use the
	 * {@link java.util.Iterator} returned from {@link #getDevices()} and its various overloads instead.
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final @Nullable(Prevalence.NEVER) BleDevice getDeviceAt(final int index)
	{
		return m_deviceMngr.get(index);
	}

	/**
	 * Returns the index of this device in the internal list, or -1 if it's not found.
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final int getDeviceIndex(final BleDevice device)
	{
		for( int i = 0; i < getDeviceCount(); i++ )
		{
			final BleDevice ith = getDeviceAt(i);

			if( ith.equals(device) )
			{
				return i;
			}
		}

		return -1;
	}

	/**
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice_previous(final BleDevice device)
	{
		return m_deviceMngr.getDevice_offset(device, -1);
	}

	/**
	 * Same as {@link #getDevice_next(BleDevice, BleDeviceState)} but just returns the next device in the internal list
	 * with no state checking.
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice_next(final BleDevice device)
	{
		return m_deviceMngr.getDevice_offset(device, 1);
	}

	/**
	 * Returns the first device previous to the provided one in the internal list that is in the given state. For various fringe cases like
	 * this manager not having any devices, this method returns {@link BleDevice#NULL}. This method wraps
	 * around so that if the provided device is at index 0, the returned device will be the last device this manager holds.
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice_previous(final BleDevice device, final BleDeviceState state)
	{
		return m_deviceMngr.getDevice_offset(device, -1, state, true);
	}

	/**
	 * Same as {@link #getDevice_previous(BleDevice, BleDeviceState)} but returns the next device in the internal list
	 * with no state checking.
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice_next(final BleDevice device, final BleDeviceState state)
	{
		return m_deviceMngr.getDevice_offset(device, 1, state, true);
	}

	/**
	 * Same as {@link #getDevice_previous(BleDevice, BleDeviceState)} but allows you to pass a query.
	 * See {@link BleDevice#is(Object...)} for the query format.
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice_previous(final BleDevice device, final Object ... query)
	{
		return m_deviceMngr.getDevice_offset(device, -1, query);
	}

	/**
	 * Same as {@link #getDevice_next(BleDevice, BleDeviceState)} but allows you to pass a query.
	 * See {@link BleDevice#is(Object...)} for the query format.
	 *
	 * @deprecated This is going to be removed in version 3. If this is something you use a lot, please let us know at
	 * [email protected]
	 */
	@Deprecated
	public final @Nullable(Prevalence.NEVER) BleDevice getDevice_next(final BleDevice device, final Object ... query)
	{
		return m_deviceMngr.getDevice_offset(device, 1, query);
	}

	/**
	 * Returns whether we have any devices. For example if you have never called {@link #startScan()}
	 * or {@link #newDevice(String)} (or overloads) then this will return false.
	 */
	public final boolean hasDevices()
	{
		return m_deviceMngr.getCount() > 0;
	}

	/**
	 * Same as {@link #getDevice(BleDeviceState)} except returns all matching devices.
	 */
	public final @Nullable(Prevalence.NEVER) BleDeviceIterator getDevices(final BleDeviceState state)
	{
		return new BleDeviceIterator(getDevices_List(), state, true);
	}

	/**
	 * Overload of {@link #getDevices(BleDeviceState)} that returns a {@link java.util.List} for you.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List(final BleDeviceState state)
	{
		return m_deviceMngr.getDevices_List(false, state);
	}

	/**
	 * Same as {@link #getDevices_List(BleDeviceState)} except the list is sorted using {@link BleManagerConfig#defaultListComparator}.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List_sorted(final BleDeviceState state)
	{
		return m_deviceMngr.getDevices_List(true, state);
	}

	/**
	 * Same as {@link #getDevice(Object...)} except returns all matching devices.
	 * See {@link BleDevice#is(Object...)} for the query format.
	 */
	public final @Nullable(Prevalence.NEVER) BleDeviceIterator getDevices(final Object ... query)
	{
		return new BleDeviceIterator(getDevices_List(), query);
	}

	/**
	 * Overload of {@link #getDevices(Object...)} that returns a {@link java.util.List} for you.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List(final Object ... query)
	{
		return m_deviceMngr.getDevices_List(false, query);
	}

	/**
	 * Same as {@link #getDevices_List(Object...)} except the list is sorted using {@link BleManagerConfig#defaultListComparator}.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List_sorted(final Object ... query)
	{
		return m_deviceMngr.getDevices_List(true, query);
	}

	/**
	 * Same as {@link #getDevices()} except filters using {@link BleDevice#isAny(int)}.
	 */
	public final @Nullable(Prevalence.NEVER) BleDeviceIterator getDevices(final int mask_BleDeviceState)
	{
		return new BleDeviceIterator(getDevices_List(), mask_BleDeviceState);
	}

	/**
	 * Overload of {@link #getDevices(int)} that returns a {@link java.util.List} for you.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List(final int mask_BleDeviceState)
	{
		return m_deviceMngr.getDevices_List(false, mask_BleDeviceState);
	}

	/**
	 * Same as {@link #getDevices_List(int)} except the list is sorted using {@link BleManagerConfig#defaultListComparator}.
	 */
	public final @Nullable(Prevalence.NEVER) List<BleDevice> getDevices_List_sorted(final int mask_BleDeviceState)
	{
		return m_deviceMngr.getDevices_List(true, mask_BleDeviceState);
	}

	/**
	 * Removes the given {@link BleDevice} from SweetBlue's internal device cache list. You should never have to call this
	 * yourself (and probably shouldn't), but it's here for flexibility.
     */
	@Advanced
	public final void removeDeviceFromCache(BleDevice device)
	{
		m_deviceMngr.remove(device, m_deviceMngr_cache);
	}

	/**
	 * Removes all {@link BleDevice}s from SweetBlue's internal device cache list. You should never have to call this
	 * yourself (and probably shouldn't), but it's here for flexibility.
	 */
	@Advanced
	public final void removeAllDevicesFromCache()
	{
		m_deviceMngr.removeAll(m_deviceMngr_cache);
	}

	/**
	 * Returns a new {@link HistoricalData} instance using
	 * {@link BleDeviceConfig#historicalDataFactory} if available.
	 */
	public final HistoricalData newHistoricalData(final byte[] data, final EpochTime epochTime)
	{
		final BleDeviceConfig.HistoricalDataFactory factory = m_config.historicalDataFactory;

		if( m_config.historicalDataFactory != null )
		{
			return m_config.historicalDataFactory.newHistoricalData(data, epochTime);
		}
		else
		{
			return new HistoricalData(data, epochTime);
		}
	}

	/**
	 * Same as {@link #newHistoricalData(byte[], EpochTime)} but tries to use
	 * {@link BleDevice#newHistoricalData(byte[], EpochTime)} if we have a device
	 * matching the given mac address.
	 */
	public final HistoricalData newHistoricalData(final byte[] data, final EpochTime epochTime, final String macAddress)
	{
		final BleDevice device = getDevice(macAddress);

		if( device.isNull() )
		{
			return newHistoricalData(data, epochTime);
		}
		else
		{
			return device.newHistoricalData(data, epochTime);
		}
	}

	/**
	 * Overload of {@link #getServer(BleServer.IncomingListener)} without any initial set-up parameters.
	 */
	public final BleServer getServer()
	{
		return getServer((IncomingListener) null);
	}

	/**
	 * Returns a {@link BleServer} instance. which for now at least is a singleton.
	 */
	public final BleServer getServer(final IncomingListener incomingListener)
	{
		m_server = m_server != null ? m_server : new BleServer(this, /*isNull=*/false);

//		bleServer.setConfig(config);
		m_server.setListener_Incoming(incomingListener);

		return m_server;
	}

	/**
	 * Overload of {@link #getServer(GattDatabase, BleServer.ServiceAddListener)}, with no {@link com.idevicesinc.sweetblue.BleServer.ServiceAddListener} set.
	 */
	public final BleServer getServer(final GattDatabase gattDatabase)
	{
		return getServer(gattDatabase, null);
	}

	/**
	 * Overload of {@link BleManager#getServer(IncomingListener, GattDatabase, BleServer.ServiceAddListener)}, with no {@link IncomingListener} set.
	 */
	public final BleServer getServer(final GattDatabase gattDatabase, BleServer.ServiceAddListener addServiceListener)
	{
		return getServer(null, gattDatabase, addServiceListener);
	}

	/**
	 * Returns a {@link BleServer} instance. This is now the preferred method to retrieve the server instance.
	 */
	public final BleServer getServer(final IncomingListener incomingListener, final GattDatabase gattDatabase, final BleServer.ServiceAddListener addServiceListener)
	{
		if (m_server == null)
		{
			m_server = new BleServer(this, /*isNull*/false);
			if (gattDatabase != null)
			{
				for (BluetoothGattService service : gattDatabase.getServiceList())
				{
					m_server.addService(service, addServiceListener);
				}
			}
		}
		m_server.setListener_Incoming(incomingListener);
		return m_server;
	}

	/**
	 * Same as {@link #newDevice(String, String, BleDeviceConfig)} but uses an empty string for the name
	 * and passes a <code>null</code> {@link BleDeviceConfig}, which results in inherited options from {@link BleManagerConfig}.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice newDevice(String macAddress)
	{
		return newDevice(macAddress, null, null);
	}

	/**
	 * Same as {@link #newDevice(String)} but allows a custom name also.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice newDevice(final String macAddress, final String name)
	{
		return newDevice(macAddress, name, null);
	}

	/**
	 * Same as {@link #newDevice(String)} but passes a {@link BleDeviceConfig} to be used as well.
	 */

	public final @Nullable(Prevalence.NEVER) BleDevice newDevice(final String macAddress, final BleDeviceConfig config)
	{
		return newDevice(macAddress, null, config);
	}

	/**
	 * Creates a new {@link BleDevice} or returns an existing one if the macAddress matches.
	 * {@link BleManager.DiscoveryListener#onEvent(DiscoveryEvent)} will be called if a new device
	 * is created.
	 * <br><br>
	 * NOTE: You should always do a {@link BleDevice#isNull()} check on this method's return value just in case. Android
	 * documentation says that underlying stack will always return a valid {@link android.bluetooth.BluetoothDevice}
	 * instance (which is required to create a valid {@link BleDevice} instance), but you really never know.
	 */
	public final @Nullable(Prevalence.NEVER) BleDevice newDevice(final String macAddress, final String name, final BleDeviceConfig config)
	{
		final String macAddress_normalized = normalizeMacAddress(macAddress);

		final BleDevice existingDevice = this.getDevice(macAddress_normalized);

		if( !existingDevice.isNull() )
		{
			if( config != null )
			{
				existingDevice.setConfig(config);
			}

			if( name != null )
			{
				existingDevice.setName(name);
			}

			return existingDevice;
		}

		final P_NativeDeviceLayer device_native = newNativeDevice(macAddress_normalized);

		if( device_native == null ) //--- DRK > API says this should never happen...not trusting it!
		{
			return BleDevice.NULL;
		}

		final String name_normalized = Utils_String.normalizeDeviceName(name);

		final BleDevice newDevice = newDevice_private(device_native, name_normalized, name != null ? name : "", BleDeviceOrigin.EXPLICIT, config);

		if( name != null )
		{
			newDevice.setName(name);
		}

		onDiscovered_wrapItUp(newDevice, device_native, /*newlyDiscovered=*/true, /*scanRecord=*/null, 0, BleDeviceOrigin.EXPLICIT, /*scanEvent=*/null);

		return newDevice;
	}

	final P_NativeDeviceLayer newNativeDevice(final String macAddress)
	{
		BluetoothDevice nativeDevice = managerLayer().getRemoteDevice(macAddress);
		P_NativeDeviceLayer layer = m_config.newDeviceLayer(BleDevice.NULL);
		layer.setNativeDevice(nativeDevice);
		return layer;
	}

	/**
	 * Forcefully undiscovers a device, disconnecting it first if needed and removing it from this manager's internal list.
	 * {@link BleManager.DiscoveryListener#onEvent(DiscoveryEvent)} with {@link LifeCycle#UNDISCOVERED} will be called.
	 * No clear use case has been thought of but the method is here just in case anyway.
	 *
	 * @return	<code>true</code> if the device was undiscovered, <code>false</code> if device is already {@link BleDeviceState#UNDISCOVERED} or manager
	 * 			doesn't contain an instance, checked referentially, not through {@link BleDevice#equals(BleDevice)} (i.e. by mac address).
	 */
	public final boolean undiscover(final BleDevice device)
	{
		if( device == null )							return false;
		if( device.isNull() )							return false;
		if( !hasDevice(device) )						return false;
		if( device.is(BleDeviceState.UNDISCOVERED) )	return false;

		if( device.isAny(BleDeviceState.CONNECTED, BleDeviceState.CONNECTING, BleDeviceState.CONNECTING_OVERALL) )
			device.disconnectAndUndiscover();
		else
			m_deviceMngr.undiscoverAndRemove(device, m_discoveryListener, m_deviceMngr_cache, E_Intent.INTENTIONAL);

		return true;
	}

	/**
	 * This method will clear the task queue of all tasks.
	 * NOTE: This can really mess things up, especially if you're currently trying to connect to a device. Only use this if you absolutely have to!
	 */
	@Advanced
	public final void clearQueue()
	{
		m_taskQueue.clearQueueOfAll();
	}

	/**
	 * Convenience forwarding of {@link #clearSharedPreferences(String)}.
	 *
	 * @see #clearSharedPreferences(String)
	 */
	public final void clearSharedPreferences(final BleDevice device)
	{
		clearSharedPreferences(device.getMacAddress());
	}

	/**
	 * Clears all data currently being held in {@link android.content.SharedPreferences} for a particular device.
	 *
	 * @see BleDeviceConfig#manageLastDisconnectOnDisk
	 * @see BleDeviceConfig#tryBondingWhileDisconnected_manageOnDisk
	 * @see BleDeviceConfig#saveNameChangesToDisk
	 * @see #clearSharedPreferences()
	 */
	public final void clearSharedPreferences(final String macAddress)
	{
		final String macAddress_normalized = normalizeMacAddress(macAddress);

		m_diskOptionsMngr.clear(macAddress_normalized);
	}

	/**
	 * Clears all data currently being held in {@link android.content.SharedPreferences} for all devices.
	 *
	 * @see BleDeviceConfig#manageLastDisconnectOnDisk
	 * @see BleDeviceConfig#tryBondingWhileDisconnected_manageOnDisk
	 * @see BleDeviceConfig#saveNameChangesToDisk
	 * @see #clearSharedPreferences(String)
	 */
	public final void clearSharedPreferences()
	{
		m_diskOptionsMngr.clear();
	}

	//--- DRK > Smooshing together a bunch of package-private accessors here.
	final P_BleStateTracker			getStateTracker(){				return m_stateTracker;									}
	final P_NativeBleStateTracker	getNativeStateTracker(){		return m_nativeStateTracker;							}
	final P_BluetoothCrashResolver	getCrashResolver(){				return m_crashResolver;									}
	final P_TaskQueue				getTaskQueue(){					return m_taskQueue;										}
	final P_Logger					getLogger(){					return m_logger;										}
	final long 						timeTurnedOn(){					return m_timeTurnedOn;									}
	final double 					timeForegrounded(){				return m_timeForegrounded;								}
	final boolean 					isBluetoothEnabled(){			return managerLayer().isBluetoothEnabled();				}
	final P_ScanManager 			getScanManager(){				return m_scanManager;									}
	final long 						getUpdateRate(){				return m_updateRunnable.getUpdateRate();				}
	final P_PostManager 			getPostManager(){				return m_postManager;									}


	private void turnOff_private(final boolean removeAllBonds)
	{
		if( isAny(TURNING_OFF, OFF) )  return;

		if( is(ON) )
		{
			m_stateTracker.update(E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE, TURNING_OFF, true, ON, false);
		}

		m_deviceMngr.disconnectAllForTurnOff(PE_TaskPriority.CRITICAL);

		if( removeAllBonds )
		{
			m_deviceMngr.unbondAll(PE_TaskPriority.CRITICAL, BondListener.Status.CANCELLED_FROM_BLE_TURNING_OFF);
		}

		if( m_server != null )
		{
			m_server.disconnect_internal(BleServer.ServiceAddListener.Status.CANCELLED_FROM_BLE_TURNING_OFF, BleServer.ConnectionFailListener.Status.CANCELLED_FROM_BLE_TURNING_OFF, State.ChangeIntent.INTENTIONAL);
		}

		final P_Task_TurnBleOff task = new P_Task_TurnBleOff(this, /*implicit=*/false, new PA_Task.I_StateListener()
		{
			@Override public void onStateChange(PA_Task taskClass, PE_TaskState state)
			{
				if( state == PE_TaskState.EXECUTING )
				{
					if( is(RESETTING) )
					{
						m_nativeStateTracker.append(RESETTING, E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
					}

					m_deviceMngr.undiscoverAllForTurnOff(m_deviceMngr_cache, E_Intent.INTENTIONAL);
				}
			}
		});

		m_taskQueue.add(task);
	}

	String getDeviceName(P_NativeDeviceLayer device, byte[] scanRecord) throws Exception
	{
		final String nameFromDevice;
		final String nameFromRecord;
		nameFromDevice = device.getName();
		nameFromRecord = Utils_ScanRecord.parseName(scanRecord);
		if (isDeviceThatReturnsShortName())
		{
			if (!TextUtils.isEmpty(nameFromRecord))
			{
				return nameFromRecord;
			}
			else
			{
				m_logger.w("Unable to get complete name from scan record! Defaulting to the short name given from BluetoothDevice.");
			}
		}
		return TextUtils.isEmpty(nameFromDevice) ? nameFromRecord : nameFromDevice;
	}

	private boolean isDeviceThatReturnsShortName()
	{
		//--- > RB  Right now, this is the only device we're aware of that returns the short name from BluetoothDevice.getName(). This may grow in the future.
		if (Build.MANUFACTURER.equalsIgnoreCase("amobile") && Build.PRODUCT.equalsIgnoreCase("full_amobile2601_wp_l") && Build.MODEL.equalsIgnoreCase("iot-500"))
		{
			return true;
		}
		return false;
	}

	final synchronized void onDiscoveredFromNativeStack(List<DiscoveryEntry> entries)
	{
		//--- DRK > Protects against fringe case where scan task is executing and app calls turnOff().
		//---		Here the scan task will be interrupted but still potentially has enough time to
		//---		discover another device or two. We're checking the enum state as opposed to the native
		//---		integer state because in this case the "turn off ble" task hasn't started yet and thus
		//---		hasn't called down into native code and thus the native state hasn't changed.
		if( false == is(ON) )  return;

		//--- DRK > Not sure if queued up messages to library's thread can sneak in a device discovery event
		//---		after user called stopScan(), so just a check to prevent unexpected callbacks to the user.
		if( false == is(SCANNING) )  return;

		final List<DiscoveryEntry> list = new ArrayList<>();

		for (DiscoveryEntry entry : entries)
		{

			final String macAddress = entry.device().getAddress();
			BleDevice device_sweetblue = m_deviceMngr.get(macAddress);

			if (device_sweetblue != null)
			{
				if (!device_sweetblue.layerManager().getDeviceLayer().equals(entry.device()))
				{
					ASSERT(false, "Discovered device " + entry.device().getName() + " " + macAddress + " already in list but with new native device instance.");
				}
			}

			final Please please;
			final boolean newlyDiscovered;
			final ScanFilter.ScanEvent scanEvent_nullable;

			if (device_sweetblue == null)
			{
				final String rawDeviceName;

				try
				{
					rawDeviceName = getDeviceName(entry.device(), entry.record());
				}

				//--- DRK > Can occasionally catch a DeadObjectException or NullPointerException here...nothing we can do about it.
				catch (Exception e)
				{
					m_logger.e(e.getStackTrace().toString());

					//--- DRK > Can't actually catch the DeadObjectException itself.
					if (e instanceof DeadObjectException)
					{
						uhOh(UhOh.DEAD_OBJECT_EXCEPTION);
					}
					else
					{
						uhOh(UhOh.RANDOM_EXCEPTION);
					}

					continue;
				}

				final String normalizedDeviceName = Utils_String.normalizeDeviceName(rawDeviceName);

				final boolean hitDisk = BleDeviceConfig.boolOrDefault(m_config.manageLastDisconnectOnDisk);
				final State.ChangeIntent lastDisconnectIntent = m_diskOptionsMngr.loadLastDisconnect(macAddress, hitDisk);
				scanEvent_nullable = m_filterMngr.makeEvent() ? ScanFilter.ScanEvent.fromScanRecord(entry.device().getNativeDevice(), rawDeviceName, normalizedDeviceName, entry.rssi(), lastDisconnectIntent, entry.record()) : null;

				please = m_filterMngr.allow(m_logger, scanEvent_nullable);

				if (please != null && false == please.ack()) continue;

				final String name_native = rawDeviceName;

				final BleDeviceConfig config_nullable = please != null ? please.getConfig() : null;
				device_sweetblue = newDevice_private(entry.device(), normalizedDeviceName, name_native, BleDeviceOrigin.FROM_DISCOVERY, config_nullable);
				newlyDiscovered = true;
			}
			else
			{
				scanEvent_nullable = null;
				newlyDiscovered = false;
			}

			entry.m_newlyDiscovered = newlyDiscovered;
			entry.m_bleDevice = device_sweetblue;
			entry.m_origin = BleDeviceOrigin.FROM_DISCOVERY;
			entry.m_scanEvent = scanEvent_nullable;
			list.add(entry);
		}

		onDiscovered_wrapItUp(list);
	}

	private void reset_private(boolean nuclear, ResetListener listener)
	{
		if( listener != null )
		{
			if( m_resetListeners != null )
			{
				m_resetListeners.addListener(listener);
			}
			else
			{
				m_resetListeners = new P_WrappingResetListener(listener, m_postManager.getUIHandler(), m_config.postCallbacksToMainThread);
			}
		}

		if( is(BleManagerState.RESETTING) )
		{
			return;
		}

		m_stateTracker.append(RESETTING, E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE);

		if( m_config.enableCrashResolverForReset )
		{
			m_taskQueue.add(new P_Task_CrashResolver(BleManager.this, m_crashResolver, /*partOfReset=*/true));
		}

		turnOff_private(/*removeAllBonds=*/true);

		if (nuclear)
		{
			P_Task_FactoryReset reset = new P_Task_FactoryReset(this, null);
			m_taskQueue.add(reset);
		}

		m_taskQueue.add(new P_Task_TurnBleOn(this, /*implicit=*/false, new PA_Task.I_StateListener()
		{
			@Override
			public void onStateChange(PA_Task taskClass, PE_TaskState state)
			{
				if (state.isEndingState())
				{
					ResetListener nukeListeners = m_resetListeners;
					m_resetListeners = null;
					m_nativeStateTracker.remove(RESETTING, E_Intent.UNINTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
					m_stateTracker.remove(RESETTING, E_Intent.UNINTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE);

					if (nukeListeners != null)
					{
						ResetEvent event = new ResetEvent(BleManager.this, ResetListener.Progress.COMPLETED);
						nukeListeners.onEvent(event);
					}
				}
			}
		}));
	}

	private BleDevice newDevice_private(final P_NativeDeviceLayer device_native, final String name_normalized, final String name_native, final BleDeviceOrigin origin, final BleDeviceConfig config_nullable)
	{
		// TODO: for now always true...should these be behind a config option?
		final boolean hitCache = true;

		final BleDevice device_cached;

		if( hitCache )
		{
			device_cached = m_deviceMngr_cache.get(device_native.getAddress());

			if( device_cached != null )
			{
				m_deviceMngr_cache.remove(device_cached, null);
				device_cached.setConfig(config_nullable);
			}
		}
		else
		{
			device_cached = null;
		}

		final BleDevice device = device_cached != null ? device_cached : new BleDevice(BleManager.this, device_native, name_normalized, name_native, origin, config_nullable, /*isNull=*/false);

		m_deviceMngr.add(device);

		return device;
	}

	final void onDiscovered_fromRogueAutoConnect(final BleDevice device, final boolean newlyDiscovered, final List<UUID> services_nullable, final byte[] scanRecord_nullable, final int rssi)
	{
		if( !m_deviceMngr.has(device) ) // DRK > as of now checked upstream also, so just being anal
		{
			m_deviceMngr.add(device);
		}

		onDiscovered_wrapItUp(device, device.layerManager().getDeviceLayer(), newlyDiscovered, scanRecord_nullable, rssi, BleDeviceOrigin.FROM_DISCOVERY, /*scanEvent=*/null);
	}

	void postEvent(final GenericListener_Void listener, final Event event)
	{
		if (listener != null)
		{
			if (listener instanceof PA_CallbackWrapper)
			{
				m_postManager.runOrPostToUpdateThread(new Runnable()
				{
					@Override
					public void run()
					{
						if (listener != null)
						{
							listener.onEvent(event);
						}
					}
				});
			}
			else
			{
				m_postManager.postCallback(new Runnable()
				{
					@Override
					public void run()
					{
						if (listener != null)
						{
							listener.onEvent(event);
						}
					}
				});
			}
		}
	}

	<T extends Event> void postEvents(final GenericListener_Void listener, final List<T> events)
	{
		if (listener != null)
		{
			if (listener instanceof PA_CallbackWrapper)
			{
				m_postManager.runOrPostToUpdateThread(new Runnable()
				{
					@Override
					public void run()
					{
						if (listener != null)
						{
							for (Event e : events)
							{
								listener.onEvent(e);
							}
						}
					}
				});
			}
			else
			{
				m_postManager.postCallback(new Runnable()
				{
					@Override
					public void run()
					{
						if (listener != null)
						{
							for (Event e : events)
							{
								listener.onEvent(e);
							}
						}
					}
				});
			}
		}
	}

    private void onDiscovered_wrapItUp(final BleDevice device, final P_NativeDeviceLayer device_native, final boolean newlyDiscovered, final byte[] scanRecord_nullable, final int rssi, final BleDeviceOrigin origin, ScanFilter.ScanEvent scanEvent_nullable)
    {
    	if( newlyDiscovered )
    	{
    		device.onNewlyDiscovered(device_native, scanEvent_nullable, rssi, scanRecord_nullable, origin);

    		if( m_discoveryListener != null )
    		{
				final DiscoveryEvent event = new DiscoveryEvent(device, LifeCycle.DISCOVERED);
				postEvent(m_discoveryListener, event);
    		}
    	}
    	else
    	{
    		device.onRediscovered(device_native, scanEvent_nullable, rssi, scanRecord_nullable, BleDeviceOrigin.FROM_DISCOVERY);

    		if( m_discoveryListener != null )
    		{
				final DiscoveryEvent event = new DiscoveryEvent(device, LifeCycle.REDISCOVERED);
				postEvent(m_discoveryListener, event);
    		}
    	}
    }

	private void onDiscovered_wrapItUp(List<DiscoveryEntry> entries)
	{
		final List<DiscoveryEvent> events = new ArrayList<>(entries.size());

		for (DiscoveryEntry e : entries)
		{
			if (e.m_newlyDiscovered)
			{
				e.m_bleDevice.onNewlyDiscovered(e.device(), e.m_scanEvent, e.rssi(), e.record(), e.m_origin);
				final DiscoveryEvent event = DiscoveryEvent.newEvent(e.m_bleDevice, LifeCycle.DISCOVERED);
				events.add(event);
			}
			else
			{
				e.m_bleDevice.onRediscovered(e.device(), e.m_scanEvent, e.rssi(), e.record(), e.m_origin);
				final DiscoveryEvent event = DiscoveryEvent.newEvent(e.m_bleDevice, LifeCycle.REDISCOVERED);
				events.add(event);
			}
		}
		if (m_discoveryListener != null)
		{
			postEvents(m_discoveryListener, events);
		}
	}

	final void clearScanningRelatedMembers(final E_Intent intent)
	{
//		m_filterMngr.clear();

		m_scanManager.resetTimeNotScanning();

		m_stateTracker.remove(BleManagerState.SCANNING, intent, BleStatuses.GATT_STATUS_NOT_APPLICABLE);
	}

	final void tryPurgingStaleDevices(final double scanTime)
	{
		m_deviceMngr.purgeStaleDevices(scanTime, m_deviceMngr_cache, m_discoveryListener);
	}

	final boolean ready() {
		if (!m_ready)
		{
			if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
			{
				m_ready = is(ON);
			}
			else
			{
				m_ready = is(ON) && isLocationEnabledForScanning_byRuntimePermissions() && isLocationEnabledForScanning_byOsServices();
			}
		}
		if (m_ready && !is(BLE_SCAN_READY))
		{
			setBleScanReady();
		}
		return m_ready;
	}

	final void checkIdleStatus()
	{
		if (is(IDLE))
		{
			// To ensure we get back up to speed as soon as possible, we'll remove the callbacks, set the new update rate
			// then post the runnable again. This avoids waiting for the idle time before the speed bumps back up.
			getLogger().i("Update loop is no longer in the IDLE state.");
			getPostManager().removeUpdateCallbacks(m_updateRunnable);
			m_updateRunnable.setUpdateRate(m_config.autoUpdateRate.millis());
			m_stateTracker.update(E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE, IDLE, false);
			getPostManager().postToUpdateThread(m_updateRunnable);
		}
	}

	/**
	 * This method is made public in case you want to tie the library in to an update loop
	 * from another codebase. Generally you should leave {@link BleManagerConfig#autoUpdateRate}
	 * alone and let the library handle the calling of this method.
	 *
	 * @deprecated - This is marked as deprecated because it will no longer be exposed in v3 (there will be a new way
	 * to have SweetBlue run on a thread of your choosing).
	 */
	@Advanced
	@Deprecated
	public final void update(final double timeStep_seconds, final long currentTime)
	{
		m_currentTick = currentTime;

		m_listeners.update(timeStep_seconds);

		m_uhOhThrottler.update(timeStep_seconds);

		if (m_taskQueue.update(timeStep_seconds, currentTime))
		{
			m_lastTaskExecution = currentTime;
			checkIdleStatus();
		}

		if( m_isForegrounded )
		{
			m_timeForegrounded += timeStep_seconds;
		}
		else
		{
			m_timeForegrounded = 0.0;
		}

		m_deviceMngr.update(timeStep_seconds);

		if ( m_timeTurnedOn == 0 && is(ON) )
		{
			m_timeTurnedOn = currentTime;
		}

		if( m_scanManager.update(timeStep_seconds, currentTime) == false )
		{
			if (Interval.isEnabled(m_config.minTimeToIdle))
			{
				if (!is(IDLE) && m_lastTaskExecution + m_config.minTimeToIdle.millis() < currentTime)
				{
					m_updateRunnable.setUpdateRate(m_config.idleUpdateRate.millis());
					m_stateTracker.update(E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE, IDLE, true);
					getLogger().i("Update loop has entered IDLE state.");
				}
			}
		}

		if( m_config.updateLoopCallback != null )
		{
			m_config.updateLoopCallback.onUpdate(timeStep_seconds);
		}

		if (!is(IDLE) && m_config.autoUpdateRate.millis() < (System.currentTimeMillis() - m_currentTick) && (m_lastUpdateLoopWarning + UPDATE_LOOP_WARNING_DELAY <= m_currentTick))
		{
            m_lastUpdateLoopWarning = m_currentTick;
			getLogger().w("BleManager", String.format("Update loop took longer to run than the current interval of %dms", m_config.autoUpdateRate.millis()));
		}
	}

	/**
	 * Returns this manager's knowledge of the app's foreground state.
	 */
	public final boolean isForegrounded()
	{
		return m_isForegrounded;
	}

	final long currentTime()
	{
		return m_currentTick;
	}

	final boolean doAutoScan()
	{
		return is(ON) && Interval.isEnabled(m_config.autoScanActiveTime) && (m_config.autoScanDuringOta || !m_deviceMngr.hasDevice(BleDeviceState.PERFORMING_OTA));
	}

	final void setBleScanReady()
	{
		m_stateTracker.update(E_Intent.INTENTIONAL, BleStatuses.GATT_STATUS_NOT_APPLICABLE, BLE_SCAN_READY, true);
	}

	final void uhOh(UhOh reason)
	{
//		if( reason == UhOh.UNKNOWN_CONNECTION_ERROR )
//		{
//			m_connectionFailTracker = 0;
//		}

		m_uhOhThrottler.uhOh(reason);
	}

	@Override public final String toString()
	{
		return m_stateTracker.toString();
	}

	final String normalizeMacAddress(final String macAddress)
	{
		final String macAddress_normalized = Utils_String.normalizeMacAddress(macAddress);

		if( macAddress == macAddress_normalized )
		{
			return macAddress;
		}
		else if( macAddress.equals(macAddress_normalized) )
		{
			return macAddress;
		}
		else
		{
			getLogger().w("Given mac address " + macAddress + " has been auto-normalized to " + macAddress_normalized);

			return macAddress_normalized;
		}
	}

	private Application.ActivityLifecycleCallbacks newLifecycleCallbacks()
	{
		return new Application.ActivityLifecycleCallbacks()
		{
			@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState){}
			@Override public void onActivityStarted(Activity activity){}
			@Override public void onActivityStopped(Activity activity){}
			@Override public void onActivitySaveInstanceState(Activity activity, Bundle outState){}
			@Override public void onActivityDestroyed(Activity activity){}

			@Override public void onActivityPaused(Activity activity)
			{
				if( m_config.autoPauseResumeDetection == true )
				{
					BleManager.this.onPause();
				}
			}

			@Override public void onActivityResumed(Activity activity)
			{
				if( m_config.autoPauseResumeDetection == true )
				{
					BleManager.this.onResume();
				}
			}
		};
	}

	private void addLifecycleCallbacks()
	{
		if( getApplicationContext() instanceof Application )
		{
			final Application application = (Application) getApplicationContext();
			final Application.ActivityLifecycleCallbacks callbacks = newLifecycleCallbacks();

			application.registerActivityLifecycleCallbacks(callbacks);
		}
		else
		{
			//--- DRK > Not sure if this is practically possible but nothing we can do here I suppose.
		}
	}


	private final class UpdateRunnable implements Runnable
	{

		private Long m_lastAutoUpdateTime;
		private long m_autoUpdateRate = -1;
		private boolean m_shutdown = false;


		public UpdateRunnable(long updateRate)
		{
			m_autoUpdateRate = updateRate;
		}

		public UpdateRunnable()
		{
		}

		public void setUpdateRate(long rate)
		{
			m_autoUpdateRate = rate;
		}

		public long getUpdateRate()
		{
			return m_autoUpdateRate;
		}

		@Override public void run()
		{
			long currentTime = System.currentTimeMillis();
			if (m_lastAutoUpdateTime == null)
			{
				m_lastAutoUpdateTime = currentTime;
			}
			double timeStep = ((double) currentTime - m_lastAutoUpdateTime)/1000.0;

			timeStep = timeStep <= 0.0 ? .00001 : timeStep;
			//--- RB > Not sure why this was put here. If the tick is over a second, we still want to know that, otherwise tasks will end up running longer
			// 			than expected.
//			timeStep = timeStep > 1.0 ? 1.0 : timeStep;


			update(timeStep, currentTime);

			m_lastAutoUpdateTime = currentTime;

			if (!m_shutdown)
			{
				m_postManager.postToUpdateThreadDelayed(this, m_autoUpdateRate);
			}
		}
	}
}