Reputation: 5984
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 :
My questions are
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
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.
Is there a better way to ensure that a topic is subscribed and unsubscribed across multiple devices?
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
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