RadiusNetworks / android-ibeacon-service

An Android library providing APIs to interact with iBeacons
http://developer.radiusnetworks.com/ibeacon/android/index.html
681 stars 244 forks source link

Sequent 'didExitRegion()' and 'didEnterRegion()' event occurrence #44

Closed kingmbc closed 5 years ago

kingmbc commented 10 years ago

Hi. Actually, I'm developing a Data Crawler on Android for sensors, networks, bluetooth, etc for gathering user data on my research. And I added iBeacon service lib into my project for capturing users' enter and leave events in a target region.

However, I encountered the problem. Although I am in the range of Beacon (i.e., enterRegion), didExitRegion() and didEnterRegion() event occured while crawling the data. So, event is follow. {Enter-- (while crawling) -- Exit -> (after few seconds) Enter .... -> (while crawling) -> Exit -> (after few seconds) Enter}

So currently, I solved the problem by ignoring duplicate sequent Leave-Enter events. But this is not a best solution but just a heuristic.

As a result of debugging, I think that the multiple use of BluetoothAdapter make a this problem. How to solve this issue in iBeacon lib?

Thank you.

davidgyoung commented 10 years ago

Are you saying you are scanning for Bluetooth devices in code external to the Android iBeacon Library? If so, can you temporarily disable this and repeat your test to see if you still experience the enter/exit/enter symptom? I would be surprised if this were true, as scanning should be independent.

Maybe obvious, but enter/exit/enter sequences can also happen when you are on the edge of beacon range or with an intermittently transmitting beacon.

kingmbc commented 10 years ago

Note that both my android phone and Beacon device are not moving and very close (about 30 cm) in a range of Beacon. As you said, I think the scanning should be independent. However, I had same symptom in two cases.

Case 1) There are two apps using a function of scanning Bluetooth devices. One is sample app based on iBeacon lib and another is my android crawler app. While crawling data on my android app, iBeacon sample app throw out the exit/enter sypthom.

Case 2) One app including iBeacon service code and my crawler code. While crawling data on my crawler code, didExitRegion() and didEnterRegion() are concurently made.

If not crawling (i.e., scanning Bluetooth device) on my crawler code, that problem is not made.

kingmbc commented 10 years ago

And I wonder what the mechanism of making Exit event is in your library. Is it a polling method by monitoring Beacon signal strength with time interval that we defined and by checking whether the signal is below the threshold?

Although I try to find that part of codes in your lib, it is not easy.

Thank you.

kingmbc commented 10 years ago

Actually, I found the MonitorSate class that perhaps check the event of inside and outside. And INSIDE_EXPIRATION_MILLIS is set to 10000l (i.e., 10 sec)

However, while crwaling, the time diff between lasSeenTime and currTime by invoking (New Date()).getTime() is bigger than INSIDE_EXPIRATION_MILLS. (When I tested, the time diff was 15384, 13064, 15882.)

I think the Beacon Lib was affected by my crawling code (there are many Thread.sleep routines.) But it is still strange as two are different threads.

kingmbc commented 10 years ago

So, I delayed the INSIDE_EXPIRATION_MILLS to 60000l.

After it, the symptom disappeared.

davidgyoung commented 10 years ago

Glad to hear you found a workaround, though it is a bit disturbing to hear that you believe to have code that affects BLE scanning in across apps. Can you please describe or share a code snippet shjowing what your crawler code does? Does it start and stop BLE scanning? If so how often? Does it enable or disable Bluetooth? Does initiate a Bluetooth discovery process (different from just scanning)?

kingmbc commented 10 years ago

public class BluetoothLogger extends Logger { /* * @brief The identifier for the tag part in the logcat / private static final String TAG = BluetoothLogger.class.getSimpleName();

/**
 * @brief Refer to global information about an application environment in Android 
 */
private Context mContext = null;
/**
 * @brief The xml formatted data to write the context log 
 */
private ContextDataFormat ctxData = null;

/**
 * @brief The bluetooth adapter to access to the bluetooth system 
 */
private BluetoothAdapter bluetoothAdapter = null;
private BluetoothBroadcastReceiver mBRBluetooth = null;
private HashMap<String, BluetoothScanResult> mDevices = new HashMap<String, BluetoothScanResult>();

/**
 * @brief Singleton instance of the BluetoothLogger class
 */
private static BluetoothLogger btUsageLogger = null;

private String ownMacAddress = "";

/**
 * @return whether the bluetooth system is enabled and not discovering
 */
private boolean isBluetoothReady() {
    return bluetoothAdapter.isEnabled() && !bluetoothAdapter.isDiscovering();
}

/**
 * @brief Constructor of BluetoothLogger class
 * 
 * @param c Reference for Android's global information 
 */
public BluetoothLogger(Context c) {
    /**
     * @impl intialize the member variable and invoke registerEventListener()
     * @code
     * set the Context variable
     * make a bluetooth broadcast receiver
     * get a defalut adapter of the bluetooth broadcast receiver 
     * IF the bluetoothAdapter is null
     *  leave the error log
     * ELSE IF      
     *  invoke registerEventListener()
     *  start the activity for letting the user turn on the bluetooth device
     * ENDIF
     * @endcode
     */
    mContext = c;
    mBRBluetooth = new BluetoothBroadcastReceiver();
    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (bluetoothAdapter == null) {
        Log.e(TAG, "device does not support Bluetooth");
    } else {
        registerEventListener();
        if (!bluetoothAdapter.isEnabled()) {
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            enableBtIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(enableBtIntent);

        }
    }
}

/**
 * @brief Get instance of BluetoothLogger class
 * 
 * @param c Reference for Android's global information 
 * 
 * @return Instance of BluetoothLogger class
 * 
 */
public static synchronized BluetoothLogger getInstance(Context c) {
    /**
     * @impl if the singleton instance is null, then make a new instance
     * @code
     * IF the instance is null
     *  make a new instance of BluetoothLogger
     * ENDIF
     * RETURN instance of BluetoothLogger
     * @endcode
     */
    if(btUsageLogger == null) {
        btUsageLogger = new BluetoothLogger(c);
    }
    return btUsageLogger;
}

/**
 * @brief Register Bluetooth status change event
 * such as when the Bluetooth device is found, when the Bluetooth discover is finished,
 * and when the Bluetooth state is changed 
 * 
 * @see android.content.Context
 */
@Override
public void registerEventListener(){
    /**
     * @impl register to Bluetooth state changed event.
     * 
     * @code
     * create an IntentFilter.
     * add ACTION_FOUND action to the filter.
     * add ACTION_DISCOVERY_FINISHED action to the filter.
     * add ACTION_STATE_CHANGED action to the filter.
     * make mContext connect the filter and mBRBluetooth.
     * @endcode
     */
    IntentFilter filter = new IntentFilter();
    filter.addAction(BluetoothDevice.ACTION_FOUND);
    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
    filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
    mContext.registerReceiver(mBRBluetooth, filter);
}

/**
 * @brief Unregister the Bluetooth change event listener.
 */
@Override
public void removeEventListener(){
    /**
     * @impl unregister to Bluetooth state changed event.
     * 
     * @code
     * unregister the receiver of the mBRBluetooth
     * set btUsageLogger null
     * @endcode
     */
    if(mBRBluetooth != null){
        mContext.unregisterReceiver(mBRBluetooth);
        mBRBluetooth = null;
    }
    btUsageLogger = null;
}

/**
 * @brief The number of the xml element for the bluetooth log 
 */
private static final int NUMBER_OF_ELEMEMENT_BLUETOOTH = 5;
/**
 * @brief Compose the data for xml format based on the bluetooth information
 * 
 * @return the xml data format
 */
public synchronized ContextDataFormat getContextFormat() {
    /**
     * @impl fill the contents for the xml based on the Bluetooth information 
     * 
     * @code
     * make the data format for the xml
     * fill the Bluetooth data such as the near bluetooth signal, the time, and the count
     * RETURN the data format filled
     * @endcode
     */
    ctxData = new ContextDataFormat("Bluetooth", NUMBER_OF_ELEMEMENT_BLUETOOTH);
    String[] slotId = new String[NUMBER_OF_ELEMEMENT_BLUETOOTH];
    String[] slotValue = new String[NUMBER_OF_ELEMEMENT_BLUETOOTH];

    slotId[0] = "OwnMacAddr";       slotValue[0] = ownMacAddress;
    slotId[1] = "NearbyMacAddr";    slotValue[1] = "";      
    slotId[2] = "Name";             slotValue[2] = "";
    slotId[3] = "Strength";         slotValue[3] = "";
    slotId[4] = "Time";             slotValue[4] = "";

    List<BluetoothScanResult> results = new ArrayList<BluetoothScanResult>(mDevices.values());      
    Collections.sort(results, new BluetoothSignalComparator());

    for(BluetoothScanResult bsr: results) {
        slotValue[1] += bsr.deviceMacAddr + "#";        //Nearby Device Mac Address         
        slotValue[2] += bsr.deviceName + "#";           //Nearby Device Name
        slotValue[3] += bsr.signalStrength + "#";       //Nearby Device BT Strength
        slotValue[4] += bsr.logTime + "#";              //Logging Time
    }       

    ctxData.setSlotId(slotId);
    ctxData.setSlotValue(slotValue);

    mDevices.clear();

    return ctxData;
}

/**
 * @brief Make the xml based on the composed context data
 *  
 * @param xmlGen the xml generator 
 */
@Override
public void startLogging(XMLGenerator xmlGen) {
    /**
     * @impl make the context data format and generate the xml 
     * @code
     * make the context data format
     * generate the xml by using the xml generator and the context data 
     * @endcode
     */
    ctxData = getContextFormat();
    super.writeTextLog(xmlGen, ctxData);        
    Log.e(TAG, "Bluetooth-EndLogging!");
}

 /**
 * Start device discovery with the BluetoothAdapter
 */
public void doDiscovery() {
    if(bluetoothAdapter == null)
        return;

    // If we're already discovering, stop it
    if (bluetoothAdapter.isDiscovering()) {
        bluetoothAdapter.cancelDiscovery();
        Log.e(TAG, "bluetoothApdator is cenceled");
    }

    // Request discover from BluetoothAdapter
    bluetoothAdapter.startDiscovery();
    try{
        Thread.sleep(ESMConstant.BLUETOOTH_SCANNING_TIME_INTERVAL);
    }catch(InterruptedException e){
        e.printStackTrace();
    }
}

/**
 * @brief Inner class in BluetoothLogger to receive the Bluetooth change event from Android system
 */
class BluetoothBroadcastReceiver extends BroadcastReceiver{
    /**
     * @brief the event handler when the Bluetooth state change event is received
     * @param context the Context instance
     * @param intent the intent having the event information
     */
    public void onReceive(Context context, Intent intent) {
        /**
         * @impl if ACTION_BATTERY_CHANGED is received, then call the correspond method 
         * 
         * @code
         * IF action is ACTION_FOUND
         *  get the elapsed time
         *  write the log based on the time and the address
         * ELSE IF action is ACTION_STATE_CHANGED
         *  start the discovery if the bluetooth is ready
         * ELSE IF action is ACTION_DISCOVERY_FINISHED
         *  sleep the thread for 100sec
         *  start the discovery if the bluetooth is ready
         * ENDIF
         * @endcode
         */
        String action = intent.getAction();

        if (BluetoothDevice.ACTION_FOUND.equals(action)) {      
            ownMacAddress = BluetoothAdapter.getDefaultAdapter().getAddress();

            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            BluetoothScanResult bsr = new BluetoothScanResult();
            bsr.deviceMacAddr = device.getAddress();
            bsr.deviceName = device.getName();
            bsr.signalStrength = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI,Short.MIN_VALUE);
            bsr.logTime = LogUtil.getCurrentTime();;

            mDevices.put(bsr.deviceMacAddr, bsr);
        }
        else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {

// if (isBluetoothReady()) // bluetoothAdapter.startDiscovery(); } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {

        }
    }
};
kingmbc commented 10 years ago

I just copy and paste the code although a display is not good to see.

Anyway, in my crawler, I invoked doDiscovery() method in the above code first. Then, BluetoothBroadcastReceiver can receive the event (i.e., BluetoothDevice.ACTION_FOUND). By using the information of nearby Bluetooth device found, I send it to my server.

As you said, I think, I don't disable BluetoothAdaptor and just continuously do discovery.. So after finishing the bluetooth scanning, I try to disable the BluetoothAdapter.

davidgyoung commented 10 years ago

OK, I believe the issue is the use of the Bluetooth discovery process and not just regular scanning. The code does this:

bluetoothAdapter.startDiscovery();

I have looked at the BlueDroid source code for this and have confirmed that this operation is global to the phone. It will disrupt other scans going on in other applications (or within the same app).

If you want this to be compatible with the Android iBeacon Library, you would need to rewrite it to use startLEScan instead of startDiscovery.

Your workaround is OK, but understand that you may be missing iBeacon detections when the discovery process is active.

kingmbc commented 10 years ago

Ok. While trying, I can understand the usage of startDiscovery and startLEScan and its different point.

As a result of my experience, startLEScan can only scan LE device (i.e., Android 4.3 later devices or Beacon). In case of startDiscovery, all the device supporting Bluetooth can be discovered.

Thank David for your kind explanation.