Race condition - two or more listeners clashing?

  • Replies:0
Karol Krajcir
  • Forum posts: 1

Mar 19, 2018, 7:16:09 PM via Website

In short, I am working on an app which is meant to get the current user last known location and show nearby users (within radius of 30km) on Swipe Cards. I am using FireBase and GeoFire to accomplish this. Before I implemented location querying, I was only using

onChildAdded()

listener to fetch all the users from the database - and it worked fine.

However, when I added location querying which uses another set of listeners, I started to get random and unexpected results. For instance duplicates - it would fetch all users twice and then display them on two subsequent cards, i.e. I would swipe user A the first time and then the same user would appear on the following card again. To add to the confusion, this only happens sometimes. I am new to GeoFire, but I am suspecting the listeners are somehow clashing.

Here's my code:

1) in onCreate(), I check for location permissions. Subsequently, I request location updates using FusedLocationProviderClient every two minutes. All working fine and as expected.

2) every two minutes, I receive the current location and this onLocationResult() callback is triggered:

// location callback - get location updates here:
LocationCallback mLocationCallback = new LocationCallback() {
    @Override
    public void onLocationResult(LocationResult locationResult) {
        Log.d("MainActivity", "onLocationResult triggered!");
        for(Location location : locationResult.getLocations()) {
            mCurrentLocation = location;
            Log.d("MainActivity", "Lat: " + mCurrentLocation.getLatitude() + ", Long: " + mCurrentLocation.getLongitude());
            // write current location to geofire:
            mGeofireDB = FirebaseDatabase.getInstance().getReference("Locations");
            GeoFire geofire = new GeoFire(mGeofireDB);
            geofire.setLocation(mCurrentUserID, new GeoLocation(mCurrentLocation.getLatitude(), mCurrentLocation.getLongitude()), new GeoFire.CompletionListener() {
                @Override
                public void onComplete(String key, DatabaseError error) {
                    if (error != null) {
                        Log.d("MainActivity", "There was an error saving the location to GeoFire: " + error);
                    } else {
                        Log.d("MainActivity", "Location saved on server successfully!");
                        // check current user sex:
                        checkSex();

                        // find nearby users of the current user's location:
                        getNearbyUsers();
                       // here's rest of the code which takes the list of cards created by getNearbyUsers() and displays those cards

3) once the location has been written to the database successfully, I call checkSex() method which is self-explanatory and working fine. Then, I am trying to get nearby users with GeoQuery in the function below.

// get all nearby users:
private void getNearbyUsers() {
    Log.d("MainActivity", "getNearbyUsers() triggered!");
    mLocationsDB = FirebaseDatabase.getInstance().getReference().child("Locations");
    GeoFire geoFire = new GeoFire(mLocationsDB);
    GeoQuery geoQuery = geoFire.queryAtLocation(new GeoLocation(mCurrentLocation.getLatitude(), mCurrentLocation.getLongitude()), mCurrentUserProximityRadius);
    geoQuery.removeAllListeners();
    geoQuery.addGeoQueryEventListener(new GeoQueryEventListener() {
        // user has been found within the radius:
        @Override
        public void onKeyEntered(String key, GeoLocation location) {
            Log.d("MainActivity", "User " + key + " just entered the radius. Going to display it as a potential match!");
            nearbyUsersList.add(key);
        }

        @Override
        public void onKeyExited(String key) {
            Log.d("MainActivity", "User " + key + " just exited the radius.");

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                mCardList.removeIf(obj -> obj.getUserID().equals(key));
                mCardArrayAdapter.notifyDataSetChanged();
                Log.d("MainActivity", "User " + key + " removed from the list.");
            } else {
                Log.d("MainActivity", "User should have exited the radius but didn't! TODO support older versions of Android!");
            }
        }

        @Override
        public void onKeyMoved(String key, GeoLocation location) {

        }

        // all users within the radius have been identified:
        @Override
        public void onGeoQueryReady() {
            displayPotentialMatches();
        }

        @Override
        public void onGeoQueryError(DatabaseError error) {

        }
    });
}
// end of getNearbyUsers()

I retrieve all the users within the radius (onKeyEntered()) and add them to the nearbyUsersList. Once all the users have been found and added to the list (yet another listener - onGeoQueryReady()), I finally call displayPotentialMatches() method, where I check if the user from database is in the nearbyUsersList and if yes, I add them to mCardList and notify the adapter about the change:

// retrieve users from database and display them on cards, based on the location and various filters:
private void displayPotentialMatches() {
    Log.d("MainActivity", "displayPotentialMatches() triggered!");
    mUsersDB.addChildEventListener(new ChildEventListener() {
        @Override
        public void onChildAdded(DataSnapshot dataSnapshot, String s) {
            Log.d("MainActivity", "displayPotentialMatches ON CHILD ADDED listener triggered!");
            // check if there is any new potential match and if the current user hasn't swiped with them yet:
            if (dataSnapshot.child("Sex").getValue() != null) {
                if (dataSnapshot.exists()
                        && !dataSnapshot.child("Connections").child("NoMatch").hasChild(mCurrentUserID)
                        && !dataSnapshot.child("Connections").child("YesMatch").hasChild(mCurrentUserID)
                        && !dataSnapshot.getKey().equals(mCurrentUserID)
                        // TODO display users based on current user sex preference:
                        && dataSnapshot.child("Sex").getValue().toString().equals(mCurrentUserOppositeSex)
                        // location check:
                        && nearbyUsersList.contains(dataSnapshot.getKey())
                        ) {
                    String profilePictureURL = "default";
                    if (!dataSnapshot.child("ProfilePictureURL").getValue().equals("default")) {
                        profilePictureURL = dataSnapshot.child("ProfilePictureURL").getValue().toString();
                    }
                    // POPULATE THE CARD WITH THE DATABASE INFO:
                    Log.d("MainActivity", dataSnapshot.getKey() + " passed all the match checks!");
                    Card potentialMatch = new Card(dataSnapshot.getKey(), dataSnapshot.child("Name").getValue().toString(), profilePictureURL);
                    mCardList.add(potentialMatch);
                    mCardArrayAdapter.notifyDataSetChanged();
                }
            }
        }

        @Override
        public void onChildChanged(DataSnapshot dataSnapshot, String s) {
        }

        @Override
        public void onChildRemoved(DataSnapshot dataSnapshot) {
        }

        @Override
        public void onChildMoved(DataSnapshot dataSnapshot, String s) {
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {
        }
    });
} // end of displayPotentialMatches()

This code is followed by piece of code which takes that list and displays those users on cards. This piece of code was tested before I implemented GeoQuerying and was working fine so I am not including it here. It is still within the location result callback.

I am quite sure that if I didn't need to use so many listeners (onKeyEntered, onGeoQueryReady, onChildAdded), it would be working as expected. I would just get all the data from database in onGeoQueryReady listener and once this would be ready, the code which displays users on cards would be executed.

However, since you need to use listener even when getting data from FireBase - onChildAdded (I get it - it's realtime), it results in unexpected results/duplicates. Sometimes it works and sometimes I get duplicates (the same user on two cards in a row) as mentioned above. But notifyDataSetChanged is only called in onChildAdded listener.

What am I missing here? Are the listeners clashing in some way (e.g. one being called and then the other without the first one being finished)? Or is the piece of code which displays the users on cards called without all the listeners being completed and therefore both onGeoQueryReady and onChildAdded triggers it? If this is the case, is there a way to only execute this piece of code once both listeners have finished?

Please let me know if you need anything else, e.g. a screenshot of the log. Any help would be much appreciated.

Be the first to answer