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