1

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();

    }
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
Kushan
  • 5,855
  • 3
  • 31
  • 45
  • ty for the edit Frank – Kushan Feb 09 '17 at 17:55
  • @Frank van Puffelen kindly help with this issue.. have been stuck on this since a few days. The service i have does indeed work the way i want it but i don't know if it is a sure shot way. – Kushan Feb 09 '17 at 18:47
  • _And yes it does need to run constantly_: That may not be possible on devices with API 23 and above due to [Doze Mode](https://developer.android.com/training/monitoring-device-state/doze-standby.html) – Bob Snyder Feb 09 '17 at 23:11
  • Ya it won't remain connected... But that's ok here... No exceptions will be thrown. Firebase should just connect when the connection turns on or if the maintenance window comes though right? And i think this setup will ensure service restart even when the system kills it due to start sticky thus resetting the listener. – Kushan Feb 09 '17 at 23:40
  • I haven't seen a statement of how Doze Mode affects listeners. This post reported the listener was not effective after the device reawakened from Dozing: http://stackoverflow.com/q/39302058/4815718 – Bob Snyder Feb 09 '17 at 23:46

1 Answers1

1

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.

Community
  • 1
  • 1
Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • The issue isn't the actual subscription as you said. It's the unsubscription. Subscribing a topic let's people send push notifications to the user via fcm. My thought was, say a user unsubscribed from say a news page A but still gets notified for the updates from that page, he/she is gonna be pissed off. If i don't keep the listener in a service, any unsubscriptions that happen when the app is killed are lost and they keep getting the push messages. – Kushan Feb 10 '17 at 04:20
  • The client doesn't control the FCM messaging - that's all on your server. The subscription data only needs to be accurate on the server at the time of the message, so the logic on the server can determine which clients to notify. The subscriptions don't have have to be distributed to all the clients to make a difference. – Doug Stevenson Feb 10 '17 at 04:41
  • it defeats the whole purpose of having topics if i have to individually manage the registration tokens and iids anyway. Anyway i am running into another issue where a MeasureBrokerService from gms latches onto my process and leaks memory for no apparent reason – Kushan Feb 10 '17 at 12:54
  • It sounds like your question is actually more about management of FCM topics than your original question. Consider asking a new question with the details of what you're trying to accomplish and the issues you're running into. I suspect that a good solution has nothing to do with trying to keep sockets open indefinitely. – Doug Stevenson Feb 10 '17 at 16:52
  • Ya I'll do that – Kushan Feb 10 '17 at 17:49
  • i have posted another question specifically targeting my issue. Please have a look and help me :) – Kushan Feb 10 '17 at 18:15