Kushan
Kushan

Reputation: 5984

What is Firebase Event Listener's back-off policy?

I have a project where a user can have multiple logins across multiple devices.

Now the user can subscribe to a particular topic on any device and the need is that the rest of the device logins should also do the same. Similar case is when one device unsubscribes, the rest should also follow suite.

In order to do this, I have made a Node under each user where all the subscriptions are maintained in the firebase database. I have a START_STICKY service which attaches a Firebase listener to this location and subs/unsubs from the topics when the changes occur. The code for the service is attached under the description.

In regular usage from observation, the service that i have does re-spawn due to the start sticky in case the system kills it. It will also explicitly respawn in case the user tampers with it using the developer options. The only cases which will cause it to completely cease are :

  1. signout
  2. data cleared
  3. force stop

My questions are

  1. how badly will keeping the listener attached affect the battery life. AFAIK Firebase has an exponential backoff when the web socket disconnects to prevent constant battery drain

  2. Can the firebase listener just give up reconnecting if the connection is off for quite some time? If so, when is the backoff limit reached.

  3. Is there a better way to ensure that a topic is subscribed and unsubscribed across multiple devices?

  4. Is the service a good way to do this? can the following service be optimised? And yes it does need to run constantly.

Code

public class SubscriptionListenerService extends Service {

    DatabaseReference userNodeSubscriptionRef;

    ChildEventListener subscribedTopicsListener;

    SharedPreferences sessionPref,subscribedTopicsPreference;

    SharedPreferences.Editor subscribedtopicsprefeditor;

    String userid;

    boolean stoppedInternally = false;

    SharedPreferences.OnSharedPreferenceChangeListener sessionPrefChangeListener;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //do not need a binder over here
        return null;
    }

    @Override
    public void onCreate(){
        super.onCreate();

        Log.d("FragmentCreate","onCreate called inside service");

        sessionPref = getSharedPreferences("SessionPref",0);

        subscribedTopicsPreference=getSharedPreferences("subscribedTopicsPreference",0);

        subscribedtopicsprefeditor=subscribedTopicsPreference.edit();

        userid = sessionPref.getString("userid",null);

        sessionPrefChangeListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                Log.d("FragmentCreate","The shared preference changed "+key);
                stoppedInternally=true;
                sessionPref.unregisterOnSharedPreferenceChangeListener(this);
                if(userNodeSubscriptionRef!=null && subscribedTopicsListener!=null){
                    userNodeSubscriptionRef.removeEventListener(subscribedTopicsListener);
                }
                stopSelf();
            }
        };

        sessionPref.registerOnSharedPreferenceChangeListener(sessionPrefChangeListener);

        subscribedTopicsListener = new ChildEventListener() {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String s) {
                if(!(dataSnapshot.getValue() instanceof Boolean)){
                    Log.d("FragmentCreate","Please test subscriptions with a boolean value");
                }else {
                    if ((Boolean) dataSnapshot.getValue()) {
                        //here we subscribe to the topic as the topic has a true value
                        Log.d("FragmentCreate", "Subscribing to topic " + dataSnapshot.getKey());
                        subscribedtopicsprefeditor.putBoolean(dataSnapshot.getKey(), true);
                        FirebaseMessaging.getInstance().subscribeToTopic(dataSnapshot.getKey());
                    } else {
                        //here we unsubscribed from the topic as the topic has a false value
                        Log.d("FragmentCreate", "Unsubscribing from topic " + dataSnapshot.getKey());
                        subscribedtopicsprefeditor.remove(dataSnapshot.getKey());
                        FirebaseMessaging.getInstance().unsubscribeFromTopic(dataSnapshot.getKey());
                    }

                    subscribedtopicsprefeditor.commit();
                }
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String s) {
                //either an unsubscription will trigger this, or a re-subscription after an unsubscription

                if(!(dataSnapshot.getValue() instanceof Boolean)){
                    Log.d("FragmentCreate","Please test subscriptions with a boolean value");
                }else{

                    if((Boolean)dataSnapshot.getValue()){
                        Log.d("FragmentCreate","Subscribing to topic "+dataSnapshot.getKey());
                        subscribedtopicsprefeditor.putBoolean(dataSnapshot.getKey(),true);
                        FirebaseMessaging.getInstance().subscribeToTopic(dataSnapshot.getKey());
                    }else{
                        Log.d("FragmentCreate","Unsubscribing from topic "+dataSnapshot.getKey());
                        subscribedtopicsprefeditor.remove(dataSnapshot.getKey());
                        FirebaseMessaging.getInstance().unsubscribeFromTopic(dataSnapshot.getKey());
                    }

                    subscribedtopicsprefeditor.commit();
                }

            }

            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {
                //Log.d("FragmentCreate","Unubscribing from topic "+dataSnapshot.getKey());
                //FirebaseMessaging.getInstance().unsubscribeFromTopic(dataSnapshot.getKey());
            }

            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String s) {
                //do nothing, this won't happen --- rather this isnt important
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                Log.d("FragmentCreate","Failed to listen to subscriptions node");
            }
        };

        if(userid!=null){

            Log.d("FragmentCreate","Found user id in service "+userid);

            userNodeSubscriptionRef = FirebaseDatabase.getInstance().getReference().child("Users").child(userid).child("subscriptions");

            userNodeSubscriptionRef.addChildEventListener(subscribedTopicsListener);

            userNodeSubscriptionRef.keepSynced(true);

        }else{
            Log.d("FragmentCreate","Couldn't find user id");
            stoppedInternally=true;
            stopSelf();
        }

    }

    @Override
    public int onStartCommand(Intent intent,int flags,int startId){
        //don't need anything done over here
        //The intent can have the following extras

        //If the intent was started by the alarm manager ..... it will contain android.intent.extra.ALARM_COUNT
        //If the intent was sent by the broadcast receiver listening for boot/update ... it will contain wakelockid
        //If it was started from within the app .... it will contain no extras in the intent

        //The following will not throw an exception if the intent does not have an wakelockid in extra
        //As per android doc... the following method releases the wakelock if any specified inside the extra and returns true
        //If no wakelockid is specified, it will return false;

        if(intent!=null){
            if(BootEventReceiver.completeWakefulIntent(intent)){
                Log.d("FragmentCreate","Wakelock released");
            }else{
                Log.d("FragmentCreate","Wakelock not acquired in the first place");
            }
        }else{
            Log.d("FragmentCreate","Intent started by regular app usage");
        }

        return START_STICKY;
    }

    @Override
    public void onDestroy(){

        if(userNodeSubscriptionRef!=null){
            userNodeSubscriptionRef.keepSynced(false);
        }

        userNodeSubscriptionRef = null;

        subscribedTopicsListener = null;

        sessionPref = null;
        subscribedTopicsPreference = null;

        subscribedtopicsprefeditor = null;

        userid = null;

        sessionPrefChangeListener = null;

        if(stoppedInternally){
            Log.d("FragmentCreate","Service getting stopped due to no userid or due to logout or data clearance...do not restart auto.. it will launch when user logs in or signs up");
        }else{

            Log.d("FragmentCreate","Service getting killed by user explicitly from running services or by force stop ... attempt restart");

            //well basically restart the service using an alarm manager ... restart after one minute

            AlarmManager alarmManager = (AlarmManager) this.getSystemService(ALARM_SERVICE);

            Intent restartServiceIntent = new Intent(this,SubscriptionListenerService.class);
            restartServiceIntent.setPackage(this.getPackageName());

            //context , uniqueid to identify the intent , actual intent , type of pending intent
            PendingIntent pendingIntentToBeFired = PendingIntent.getService(this,1,restartServiceIntent,PendingIntent.FLAG_ONE_SHOT);

            if(Build.VERSION.SDK_INT>=23){
                alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()+600000,pendingIntentToBeFired);
            }else{
                alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()+600000,pendingIntentToBeFired);
            }
        }

        super.onDestroy();

    }
}

Upvotes: 1

Views: 684

Answers (1)

Doug Stevenson
Doug Stevenson

Reputation: 317477

A service is not really necessary for what you're trying to do. There's no advantage to having a service, except that it may keep your app's process alive longer than it would without the service started. If you don't need to actually take advantage of the special properties of a Service, there's no point in using one (or you haven't really made a compelling case why it does need to be started all the time). Just register the listener when the app process starts, and let it go until the app process is killed for whatever reason. I highly doubt that your users will be upset about not having subscription updates if the app just isn't running (they certainly aren't using it!).

The power drain on an open socket that does no I/O is minimal. Also, an open socket will not necessarily keep the device's cell radio on at full power, either. So if the listen location isn't generating new values, then your listener is never invoked, and there is no network I/O. If the value being listened to is changing a lot, you might want reconsider just how necessary it is to keep the user's device busy with those updates.

The listener itself isn't "polling" or "retrying". The entire Firebase socket connection is doing this. The listener has no clue what's going on behind the scenes. It's either receiving updates, or not. It doesn't know or care about the state of the underlying websocket. The fact that a location is of interest to the client is actually managed on the server - that is what's ultimately responsible for noticing a change and propagating that to listening clients.

Upvotes: 1

Related Questions