/*
* Kontalk Android client
* Copyright (C) 2017 Kontalk Devteam <devteam@kontalk.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.kontalk.service.msgcenter;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.XMPPConnection;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.SystemClock;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.util.Preferences;
import org.kontalk.util.SystemUtils;
/**
* An adaptive ping manager using {@link AlarmManager}.
* @author Daniele Ricci
*/
public class AndroidAdaptiveServerPingManager extends AbstractAdaptiveServerPingManager {
private static final Logger LOGGER = Logger.getLogger(AndroidAdaptiveServerPingManager.class.getName());
private static final String PING_ALARM_ACTION = "org.igniterealtime.smackx.ping.ACTION";
private static final Map<XMPPConnection, AndroidAdaptiveServerPingManager> INSTANCES =
new WeakHashMap<>();
private static AlarmManager sAlarmManager;
// we don't use the onConnectionCreated static initializer because we need an Android system context
private static void ensureAlarmManager(Context context) {
sAlarmManager = (AlarmManager) context.getApplicationContext()
.getSystemService(Context.ALARM_SERVICE);
}
public static AndroidAdaptiveServerPingManager getInstanceFor(XMPPConnection connection, Context context) {
synchronized (INSTANCES) {
AndroidAdaptiveServerPingManager serverPingWithAlarmManager = INSTANCES.get(connection);
if (serverPingWithAlarmManager == null) {
serverPingWithAlarmManager = new AndroidAdaptiveServerPingManager(connection, context);
INSTANCES.put(connection, serverPingWithAlarmManager);
}
return serverPingWithAlarmManager;
}
}
private AndroidAdaptiveServerPingManager(XMPPConnection connection, Context context) {
super(connection);
mContext = context;
enable();
onConnectionCompleted();
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
LOGGER.fine("Ping Alarm broadcast received");
if (isEnabled()) {
MessageCenterService.ping(context);
}
}
};
private static final int MIN_ALARM_INTERVAL = 90 * 1000;
private Context mContext;
private PendingIntent mPendingIntent;
private void setupOnConnectionCompleted() {
if (mContext != null) {
// setup first alarm using last value from preference
setupPing(Preferences.getPingAlarmInterval(mContext, AlarmManager.INTERVAL_HALF_HOUR));
// next increase can happen at least at next interval
mNextIncrease = Preferences.getPingAlarmBackoff(mContext, mInterval);
// reset internal variables
mLastSuccess = 0;
mLastSuccessInterval = 0;
}
}
@Override
public void onConnectionCompleted() {
setupOnConnectionCompleted();
mPingStreak = 0;
}
@Override
public void onConnectivityChanged() {
setupOnConnectionCompleted();
}
@Override
public void setEnabled(boolean enabled) {
if (enabled && !isEnabled()) {
enable();
onConnectionCompleted();
}
else if (!enabled && isEnabled()) {
disable();
}
super.setEnabled(enabled);
}
private synchronized void enable() {
if (mPendingIntent == null) {
mContext.registerReceiver(mReceiver, new IntentFilter(PING_ALARM_ACTION));
ensureAlarmManager(mContext);
mPendingIntent = PendingIntent.getBroadcast(mContext, 0, new Intent(PING_ALARM_ACTION), 0);
}
}
private synchronized void disable() {
if (mPendingIntent != null) {
try {
mContext.unregisterReceiver(mReceiver);
}
catch (IllegalArgumentException e) {
// for some strange reason, this can happen once in a while.
// I can't see how it's possible given the protection of
// mPendingIntent != null and the synchronized clause.
// Could it be Android unregistering on its own?
// Whatever, report the exception as non-fatal
ReportingManager.logException(e);
LOGGER.log(Level.WARNING, "Unable to unregister broadcast receiver", e);
}
ensureAlarmManager(mContext);
sAlarmManager.cancel(mPendingIntent);
mPendingIntent = null;
}
}
@Override
protected long getElapsedRealtime() {
return SystemClock.elapsedRealtime();
}
@Override
protected synchronized void setupPing(long intervalMillis) {
if (mPendingIntent != null) {
sAlarmManager.cancel(mPendingIntent);
mInterval = intervalMillis;
// do not go beyond 30 minutes...
if (mInterval > AlarmManager.INTERVAL_HALF_HOUR) {
mInterval = AlarmManager.INTERVAL_HALF_HOUR;
}
// ...or less than 90 seconds
else if (mInterval < MIN_ALARM_INTERVAL) {
mInterval = MIN_ALARM_INTERVAL;
}
// save value to preference for later retrieval
Preferences.setPingAlarmInterval(mContext, mInterval);
// remove difference from last received stanza
long interval = mInterval;
XMPPConnection connection = connection();
if (connection != null) {
long now = System.currentTimeMillis();
long lastStanza = connection.getLastStanzaReceived();
if (lastStanza > 0)
interval -= (now - lastStanza);
}
LOGGER.log(Level.WARNING, "Setting alarm for next ping to " + mInterval + " ms (real " + interval + " ms)");
if (SystemUtils.isOnWifi(mContext)) {
// when on WiFi we can afford an inexact ping (carrier will not destroy our connection)
sAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + interval,
interval, mPendingIntent);
}
else {
// when on mobile network, we need exact ping timings
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
sAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + interval,
mPendingIntent);
} else {
sAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + interval,
mPendingIntent);
}
}
}
}
@Override
protected void setNextIncreaseInterval(long interval) {
super.setNextIncreaseInterval(interval);
Preferences.setPingAlarmBackoff(mContext, mNextIncrease);
}
public static void onConnected() {
synchronized (INSTANCES) {
for (Map.Entry<XMPPConnection, AndroidAdaptiveServerPingManager>
xmppConnectionAndroidAdaptiveServerPingManagerEntry : INSTANCES.entrySet()) {
AndroidAdaptiveServerPingManager instance = xmppConnectionAndroidAdaptiveServerPingManagerEntry
.getValue();
instance.onConnectivityChanged();
}
}
}
/**
* Unregister the alarm broadcast receiver and cancel the alarm.
*/
public static void onDestroy() {
synchronized (INSTANCES) {
for (Map.Entry<XMPPConnection, AndroidAdaptiveServerPingManager>
xmppConnectionAndroidAdaptiveServerPingManagerEntry : INSTANCES.entrySet()) {
AndroidAdaptiveServerPingManager instance = xmppConnectionAndroidAdaptiveServerPingManagerEntry
.getValue();
instance.disable();
}
}
}
}