androidandroid-8.0-oreoibeacon-androidandroid-ibeacon

Foreground Service restarts on didExitRegion


I'm building an app to detect iBeacons on Android. The basic functionality is to store the advertised data by the beacon onto the device and upload it to a server. For this purpose, I'm using Android Beacon Library.

To run it in the background on Android O I'm using foreground services. The problem is when the App is in the background for more than 30 mins, after detecting a beacon, when the user exits the beacon region and didExitRegion is called, somehow, the service is automatically killed and it restarts thus no data is uploaded to the server. Also after restarting, on a second didExitRegion call, it stops completely and randomly sometime in the future, it restarts but to do the same loop all over again.

Sequence of events as they happen when app comes in regions after being inactive for around 30min

First Restart after didExit (Image)
Here you can see switching from region 11 to 9. Midway app closed and retriggered instantaneously, however no push was sent

Exit after second didExit (Image)
Next up: when exiting from this region now, app again stops in the background. But this time does not get re-triggered immediately. This is the exact sequence taking place always.

Code Snippets
BeaconScanner.java

        @Override
        public void didExitRegion(Region region) {
            Log.d(TAG, "Exited A Region");
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                notificationHelper.notify(2, notificationHelper.getNotification("BeaconScanner", "Exit Major #"+previousMajor, false));
            else
                utils.dispatchNotification("Exit Major #"+previousMajor, 1);

            Log.e(TAG, "DidSend "+didSend+" has Data "+userData.hasPendingData());
            if(didSend && userData.hasPendingData()) {
                JSONObject data = userData.getJsonFromUser();
                Log.d(TAG, "Timestamp = "+System.currentTimeMillis());
                userData.addTimestamp(""+System.currentTimeMillis());
                userData.requestDataSync(data);
                userData.clearBeaconData();
                Log.d(TAG, data.toString());
                didSend = !didSend;
            }
            previousMajor = -1000;
            lastBeacon = resetBeacon;
        }

User.java

JSONObject getJsonFromUser() {
    Log.d(TAG, "Timestamp as in getJsonFromUser "+timestamp);
    JSONObject json = new JSONObject();
    try {
        json.put("email", email);
        json.put("name", name);
        JSONArray beaconArray = new JSONArray();
        for (Beacon beacon : beaconData){
            beaconArray.put(new JSONObject()
                    .put("major", beacon.getId2().toInt())
                    .put("minor", beacon.getId3().toInt())
                    .put("uuid", beacon.getId1().toString())
            );
        }
        json.put("data", beaconArray);
        Log.d(TAG, timestamp);
        json.put("timestamp", ""+System.currentTimeMillis());
        return json;

    } catch (Exception e){
        Log.e(TAG, e.getMessage());
        Crashlytics.log(e.getLocalizedMessage());
    }
    return json;
}

void requestDataSync(final JSONObject json){
    User.syncing = true;
    Crashlytics.log(1, "User.java", "Requesting Auth Token");
    FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
    user.getIdToken(true).addOnCompleteListener(new OnCompleteListener<GetTokenResult>() {
        @Override
        public void onComplete(@NonNull Task<GetTokenResult> task) {
            if(task.isSuccessful()){
                final Task<GetTokenResult> t = task;
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            trustAllHosts();
                            URL url = new URL("https://indpulse.com/generatetoken");
                            HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
                            connection.setHostnameVerifier(DO_NOT_VERIFY);
                            connection.setRequestProperty("Authorization", ""+t.getResult().getToken());
                            connection.setRequestMethod("GET");
                            Log.d(TAG, t.getResult().getToken());
                            connection.setDoOutput(true);
                            connection.connect();
                            BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
                            StringBuilder response = new StringBuilder();
                            String packet;
                            while((packet = br.readLine()) != null){
                                response.append(packet);
                            }
                            JSONObject responseObject = new JSONObject(response.toString());
                            String idToken = responseObject.getString("token");
                            Crashlytics.log(1, "User.java", "Auth Token Acquired");
                            sendData(json, idToken);
                        } catch (MalformedURLException e){
                            Log.e(TAG, "Malformed URL "+e.getLocalizedMessage());
                            Crashlytics.log(e.getLocalizedMessage());
                        } catch (IOException e){
                            Log.e(TAG, "IOExeption "+e.getLocalizedMessage());
                            Crashlytics.log(e.getLocalizedMessage());
                        } catch (JSONException e) {
                            Log.d(TAG,"Json error");
                            Crashlytics.log(e.getLocalizedMessage());
                        }
                    }
                });
                thread.start();
            }
        }
    });

void sendData(JSONObject json, final String idToken){
    final String sJson = json.toString();
    System.out.println(sJson);
    Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                URL url = new URL("https://indpulse.com/android");
                HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
                conn.setRequestMethod("POST");
                conn.setRequestProperty("Authorization", "Bearer "+idToken);
                conn.setDoOutput(true);
                conn.setDoInput(true);
                conn.connect();

                DataOutputStream os = new DataOutputStream(conn.getOutputStream());
                os.writeBytes(sJson);
                os.flush();
                os.close();

                Log.i(TAG, String.valueOf(conn.getResponseCode()));
                Log.i(TAG , conn.getResponseMessage());


                conn.disconnect();
            } catch (Exception e){
                Log.e("BeaconScanner", e.getLocalizedMessage());
                Crashlytics.log(e.getLocalizedMessage());
            }

            User.syncing = false;
        }
    });

    thread.start();
}

EDIT 1

One thing to note is that the beacons have an overlapping region, i.e a beacon scanner will detect 2 beacons in the region. So the nearest beacon is decided by the greatest value of the timeAverageRssi, the bug specifically crops up, after 30 minutes of inactivity there are region switches i.e beacon 1 was the nearest and then beacon 2 becomes the nearest beacon


Solution

  • I suspect a bug with using foreground services with the Android Beacon Library version 2.15 on Android 8+. While I have not reproduced this myself, the theory is that Android 8+ blocks the usage of an Intent Service used to deliver monitoring callbacks from the beacon scanning service.

    I built a proposed fix in library version 2.15.1 beta 1 based on a similar report issue here. Please try this fix and see if it solves your problem.