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 * 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 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 {@link PingManager#pingServerIfNecessary()} 099 * for the connection of this instance every half hour. 100 * 101 * @param enabled 102 */ 103 public void setEnabled(boolean enabled) { 104 mEnabled = enabled; 105 } 106 107 public boolean isEnabled() { 108 return mEnabled; 109 } 110 111 private static final BroadcastReceiver ALARM_BROADCAST_RECEIVER = new BroadcastReceiver() { 112 @Override 113 public void onReceive(Context context, Intent intent) { 114 LOGGER.fine("Ping Alarm broadcast received"); 115 Set<Entry<XMPPConnection, ServerPingWithAlarmManager>> managers; 116 synchronized (ServerPingWithAlarmManager.class) { 117 // Make a copy to avoid ConcurrentModificationException when 118 // iterating directly over INSTANCES and the Set is modified 119 // concurrently by creating a new ServerPingWithAlarmManager. 120 managers = new HashSet<>(INSTANCES.entrySet()); 121 } 122 for (Entry<XMPPConnection, ServerPingWithAlarmManager> entry : managers) { 123 XMPPConnection connection = entry.getKey(); 124 if (entry.getValue().isEnabled()) { 125 LOGGER.fine("Calling pingServerIfNecessary for connection " 126 + connection); 127 final PingManager pingManager = PingManager.getInstanceFor(connection); 128 // Android BroadcastReceivers have a timeout of 60 seconds. 129 // The connections reply timeout may be higher, which causes 130 // timeouts of the broadcast receiver and a subsequent ANR 131 // of the App of the broadcast receiver. We therefore need 132 // to call pingServerIfNecessary() in a new thread to avoid 133 // this. It could happen that the device gets back to sleep 134 // until the Thread runs, but that's a risk we are willing 135 // to take into account as it's unlikely. 136 Async.go(new Runnable() { 137 @Override 138 public void run() { 139 pingManager.pingServerIfNecessary(); 140 } 141 }, "PingServerIfNecessary (" + connection.getConnectionCounter() + ')'); 142 } else { 143 LOGGER.fine("NOT calling pingServerIfNecessary (disabled) on connection " 144 + connection.getConnectionCounter()); 145 } 146 } 147 } 148 }; 149 150 private static Context sContext; 151 private static PendingIntent sPendingIntent; 152 private static AlarmManager sAlarmManager; 153 154 /** 155 * Register a pending intent with the AlarmManager to be broadcasted every half hour and 156 * register the alarm broadcast receiver to receive this intent. The receiver will check all 157 * known questions if a ping is Necessary when invoked by the alarm intent. 158 * 159 * @param context 160 */ 161 public static void onCreate(Context context) { 162 sContext = context; 163 context.registerReceiver(ALARM_BROADCAST_RECEIVER, new IntentFilter(PING_ALARM_ACTION)); 164 sAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 165 sPendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(PING_ALARM_ACTION), 0); 166 sAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, 167 SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, 168 AlarmManager.INTERVAL_HALF_HOUR, sPendingIntent); 169 } 170 171 /** 172 * Unregister the alarm broadcast receiver and cancel the alarm. 173 */ 174 public static void onDestroy() { 175 sContext.unregisterReceiver(ALARM_BROADCAST_RECEIVER); 176 sAlarmManager.cancel(sPendingIntent); 177 } 178}