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}