001/** 002 * 003 * Copyright © 2014 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 * 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 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 public static synchronized ServerPingWithAlarmManager getInstanceFor(XMPPConnection connection) { 083 ServerPingWithAlarmManager serverPingWithAlarmManager = INSTANCES.get(connection); 084 if (serverPingWithAlarmManager == null) { 085 serverPingWithAlarmManager = new ServerPingWithAlarmManager(connection); 086 INSTANCES.put(connection, serverPingWithAlarmManager); 087 } 088 return serverPingWithAlarmManager; 089 } 090 091 private boolean mEnabled = true; 092 093 private ServerPingWithAlarmManager(XMPPConnection connection) { 094 super(connection); 095 } 096 097 /** 098 * If enabled, ServerPingWithAlarmManager will call 099 * {@link PingManager#pingServerIfNecessary()} for the connection of this 100 * 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.getConnectionCounter()); 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 157 * half hour and register the alarm broadcast receiver to receive this 158 * intent. The receiver will check all known questions if a ping is 159 * Necessary when invoked by the alarm intent. 160 * 161 * @param context 162 */ 163 public static void onCreate(Context context) { 164 sContext = context; 165 context.registerReceiver(ALARM_BROADCAST_RECEIVER, new IntentFilter(PING_ALARM_ACTION)); 166 sAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 167 sPendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(PING_ALARM_ACTION), 0); 168 sAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 169 SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, 170 AlarmManager.INTERVAL_HALF_HOUR, sPendingIntent); 171 } 172 173 /** 174 * Unregister the alarm broadcast receiver and cancel the alarm. 175 */ 176 public static void onDestroy() { 177 sContext.unregisterReceiver(ALARM_BROADCAST_RECEIVER); 178 sAlarmManager.cancel(sPendingIntent); 179 } 180}