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