001/** 002 * 003 * Copyright © 2014-2021 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.jivesoftware.smackx.ping.android; 019 020import java.util.HashSet; 021import java.util.Map; 022import java.util.Set; 023import java.util.WeakHashMap; 024import java.util.logging.Logger; 025 026import org.jivesoftware.smack.ConnectionCreationListener; 027import org.jivesoftware.smack.Manager; 028import org.jivesoftware.smack.XMPPConnection; 029import org.jivesoftware.smack.XMPPConnectionRegistry; 030import org.jivesoftware.smack.util.Async; 031import org.jivesoftware.smackx.ping.PingManager; 032 033import android.app.AlarmManager; 034import android.app.PendingIntent; 035import android.content.BroadcastReceiver; 036import android.content.Context; 037import android.content.Intent; 038import android.content.IntentFilter; 039import android.os.SystemClock; 040 041/** 042 * Send automatic server pings with the help of {@link AlarmManager}. 043 * <p> 044 * Smack's {@link PingManager} uses a <code>ScheduledThreadPoolExecutor</code> to schedule the 045 * automatic server pings, but on Android, those scheduled pings are not reliable. This is because 046 * the Android device may go into deep sleep where the system will not continue to run this causes 047 * <ul> 048 * <li>the system time to not move forward, which means that the time spent in deep sleep is not 049 * counted towards the scheduled delay time</li> 050 * <li>the scheduled Runnable is not run while the system is in deep sleep.</li> 051 * </ul> 052 * <p> 053 * That is the reason Android comes with an API to schedule those tasks: AlarmManager. Which this 054 * class uses to determine every 30 minutes if a server ping is necessary. The interval of 30 055 * minutes is the ideal trade-off between reliability and low resource (battery) consumption. 056 * </p> 057 * <p> 058 * In order to use this class you need to call {@link #onCreate(Context)} <b>once</b>, for example 059 * in the <code>onCreate()</code> method of your Service holding the XMPPConnection. And to avoid 060 * leaking any resources, you should call {@link #onDestroy()} when you no longer need any of its 061 * functionality. 062 * </p> 063 */ 064public final class ServerPingWithAlarmManager extends Manager { 065 066 private static final Logger LOGGER = Logger.getLogger(ServerPingWithAlarmManager.class 067 .getName()); 068 069 private static final String PING_ALARM_ACTION = "org.igniterealtime.smackx.ping.ACTION"; 070 071 private static final Map<XMPPConnection, ServerPingWithAlarmManager> INSTANCES = new WeakHashMap<XMPPConnection, ServerPingWithAlarmManager>(); 072 073 static { 074 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 075 @Override 076 public void connectionCreated(XMPPConnection connection) { 077 getInstanceFor(connection); 078 } 079 }); 080 } 081 082 /** 083 * Get the instance of this manager for the given connection. 084 * 085 * @param connection the connection. 086 * @return the instance of this manager for the given connection. 087 */ 088 public static synchronized ServerPingWithAlarmManager getInstanceFor(XMPPConnection connection) { 089 ServerPingWithAlarmManager serverPingWithAlarmManager = INSTANCES.get(connection); 090 if (serverPingWithAlarmManager == null) { 091 serverPingWithAlarmManager = new ServerPingWithAlarmManager(connection); 092 INSTANCES.put(connection, serverPingWithAlarmManager); 093 } 094 return serverPingWithAlarmManager; 095 } 096 097 private boolean mEnabled = true; 098 099 private ServerPingWithAlarmManager(XMPPConnection connection) { 100 super(connection); 101 } 102 103 /** 104 * If enabled, ServerPingWithAlarmManager will call {@link PingManager#pingServerIfNecessary()} 105 * for the connection of this instance every half hour. 106 * 107 * @param enabled whether or not this manager is should be enabled or not. 108 */ 109 public void setEnabled(boolean enabled) { 110 mEnabled = enabled; 111 } 112 113 /** 114 * Check if this manager is enabled. 115 * 116 * @return <code>true</code> if this manager is enabled, <code>false</code> otherwise. 117 */ 118 public boolean isEnabled() { 119 return mEnabled; 120 } 121 122 private static final BroadcastReceiver ALARM_BROADCAST_RECEIVER = new BroadcastReceiver() { 123 @Override 124 public void onReceive(Context context, Intent intent) { 125 LOGGER.fine("Ping Alarm broadcast received"); 126 Set<Map.Entry<XMPPConnection, ServerPingWithAlarmManager>> managers; 127 synchronized (ServerPingWithAlarmManager.class) { 128 // Make a copy to avoid ConcurrentModificationException when 129 // iterating directly over INSTANCES and the Set is modified 130 // concurrently by creating a new ServerPingWithAlarmManager. 131 managers = new HashSet<>(INSTANCES.entrySet()); 132 } 133 for (Map.Entry<XMPPConnection, ServerPingWithAlarmManager> entry : managers) { 134 XMPPConnection connection = entry.getKey(); 135 if (entry.getValue().isEnabled()) { 136 LOGGER.fine("Calling pingServerIfNecessary for connection " 137 + connection); 138 final PingManager pingManager = PingManager.getInstanceFor(connection); 139 // Android BroadcastReceivers have a timeout of 60 seconds. 140 // The connections reply timeout may be higher, which causes 141 // timeouts of the broadcast receiver and a subsequent ANR 142 // of the App of the broadcast receiver. We therefore need 143 // to call pingServerIfNecessary() in a new thread to avoid 144 // this. It could happen that the device gets back to sleep 145 // until the Thread runs, but that's a risk we are willing 146 // to take into account as it's unlikely. 147 Async.go(new Runnable() { 148 @Override 149 public void run() { 150 pingManager.pingServerIfNecessary(); 151 } 152 }, "PingServerIfNecessary (" + connection.getConnectionCounter() + ')'); 153 } else { 154 LOGGER.fine("NOT calling pingServerIfNecessary (disabled) on connection " 155 + connection.getConnectionCounter()); 156 } 157 } 158 } 159 }; 160 161 private static Context sContext; 162 private static PendingIntent sPendingIntent; 163 private static AlarmManager sAlarmManager; 164 165 /** 166 * Register a pending intent with the AlarmManager to be broadcasted every half hour and 167 * register the alarm broadcast receiver to receive this intent. The receiver will check all 168 * known questions if a ping is Necessary when invoked by the alarm intent. 169 * 170 * @param context an Android context. 171 */ 172 public static void onCreate(Context context) { 173 sContext = context; 174 context.registerReceiver(ALARM_BROADCAST_RECEIVER, new IntentFilter(PING_ALARM_ACTION)); 175 sAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 176 sPendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(PING_ALARM_ACTION), 0); 177 sAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 178 SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, 179 AlarmManager.INTERVAL_HALF_HOUR, sPendingIntent); 180 } 181 182 /** 183 * Unregister the alarm broadcast receiver and cancel the alarm. 184 */ 185 public static void onDestroy() { 186 sContext.unregisterReceiver(ALARM_BROADCAST_RECEIVER); 187 sAlarmManager.cancel(sPendingIntent); 188 } 189}