/** * * Copyright © 2014-2021 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.ping.android; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Logger; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.util.Async; import org.jivesoftware.smackx.ping.PingManager; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.SystemClock; /** * Send automatic server pings with the help of {@link AlarmManager}. *

* Smack's {@link PingManager} uses a ScheduledThreadPoolExecutor to schedule the * automatic server pings, but on Android, those scheduled pings are not reliable. This is because * the Android device may go into deep sleep where the system will not continue to run this causes *

*

* That is the reason Android comes with an API to schedule those tasks: AlarmManager. Which this * class uses to determine every 30 minutes if a server ping is necessary. The interval of 30 * minutes is the ideal trade-off between reliability and low resource (battery) consumption. *

*

* In order to use this class you need to call {@link #onCreate(Context)} once, for example * in the onCreate() method of your Service holding the XMPPConnection. And to avoid * leaking any resources, you should call {@link #onDestroy()} when you no longer need any of its * functionality. *

*/ public final class ServerPingWithAlarmManager extends Manager { private static final Logger LOGGER = Logger.getLogger(ServerPingWithAlarmManager.class .getName()); private static final String PING_ALARM_ACTION = "org.igniterealtime.smackx.ping.ACTION"; private static final Map INSTANCES = new WeakHashMap(); static { XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { @Override public void connectionCreated(XMPPConnection connection) { getInstanceFor(connection); } }); } public static synchronized ServerPingWithAlarmManager getInstanceFor(XMPPConnection connection) { ServerPingWithAlarmManager serverPingWithAlarmManager = INSTANCES.get(connection); if (serverPingWithAlarmManager == null) { serverPingWithAlarmManager = new ServerPingWithAlarmManager(connection); INSTANCES.put(connection, serverPingWithAlarmManager); } return serverPingWithAlarmManager; } private boolean mEnabled = true; private ServerPingWithAlarmManager(XMPPConnection connection) { super(connection); } /** * If enabled, ServerPingWithAlarmManager will call {@link PingManager#pingServerIfNecessary()} * for the connection of this instance every half hour. * * @param enabled whether or not this manager is should be enabled or not. */ public void setEnabled(boolean enabled) { mEnabled = enabled; } public boolean isEnabled() { return mEnabled; } private static final BroadcastReceiver ALARM_BROADCAST_RECEIVER = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { LOGGER.fine("Ping Alarm broadcast received"); Set> managers; synchronized (ServerPingWithAlarmManager.class) { // Make a copy to avoid ConcurrentModificationException when // iterating directly over INSTANCES and the Set is modified // concurrently by creating a new ServerPingWithAlarmManager. managers = new HashSet<>(INSTANCES.entrySet()); } for (Map.Entry entry : managers) { XMPPConnection connection = entry.getKey(); if (entry.getValue().isEnabled()) { LOGGER.fine("Calling pingServerIfNecessary for connection " + connection); final PingManager pingManager = PingManager.getInstanceFor(connection); // Android BroadcastReceivers have a timeout of 60 seconds. // The connections reply timeout may be higher, which causes // timeouts of the broadcast receiver and a subsequent ANR // of the App of the broadcast receiver. We therefore need // to call pingServerIfNecessary() in a new thread to avoid // this. It could happen that the device gets back to sleep // until the Thread runs, but that's a risk we are willing // to take into account as it's unlikely. Async.go(new Runnable() { @Override public void run() { pingManager.pingServerIfNecessary(); } }, "PingServerIfNecessary (" + connection.getConnectionCounter() + ')'); } else { LOGGER.fine("NOT calling pingServerIfNecessary (disabled) on connection " + connection.getConnectionCounter()); } } } }; private static Context sContext; private static PendingIntent sPendingIntent; private static AlarmManager sAlarmManager; /** * Register a pending intent with the AlarmManager to be broadcasted every half hour and * register the alarm broadcast receiver to receive this intent. The receiver will check all * known questions if a ping is Necessary when invoked by the alarm intent. * * @param context an Android context. */ public static void onCreate(Context context) { sContext = context; context.registerReceiver(ALARM_BROADCAST_RECEIVER, new IntentFilter(PING_ALARM_ACTION)); sAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); sPendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(PING_ALARM_ACTION), 0); sAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, AlarmManager.INTERVAL_HALF_HOUR, sPendingIntent); } /** * Unregister the alarm broadcast receiver and cancel the alarm. */ public static void onDestroy() { sContext.unregisterReceiver(ALARM_BROADCAST_RECEIVER); sAlarmManager.cancel(sPendingIntent); } }