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