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