From bfebf6a4a5ed134f825623f13857aa351ef55f6b Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 22 Sep 2014 09:58:09 +0200 Subject: [PATCH] Add ServerPingWithAlarmManager to smack-android --- build.gradle | 21 +++ settings.gradle | 1 + smack-android-extensions/build.gradle | 19 +++ .../android/ServerPingWithAlarmManager.java | 159 ++++++++++++++++++ smack-android/build.gradle | 25 +-- 5 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 smack-android-extensions/build.gradle create mode 100644 smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java diff --git a/build.gradle b/build.gradle index fdcc6a880..052ae3133 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,10 @@ allprojects { builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date()) oneLineDesc = 'An Open Source XMPP (Jabber) client library' jxmppVersion = "0.3.0" + smackMinAndroidSdk = 8 + androidProjects = [':smack-tcp',':smack-core', ':smack-resolver-minidns', ':smack-sasl-provided', ':smack-extensions', ':smack-experimental'].collect{ project(it) } + androidBootClasspath = getAndroidRuntimeJar() + androidJavadocOffline = getAndroidJavadocOffline() } group = 'org.igniterealtime.smack' sourceCompatibility = 1.7 @@ -306,3 +310,20 @@ def getGitCommit() { assert !gitCommit.isEmpty() gitCommit } + +def getAndroidRuntimeJar() { + def androidHome = new File("$System.env.ANDROID_HOME") + if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set") + def androidJar = new File("$androidHome/platforms/android-$smackMinAndroidSdk/android.jar") + if (androidJar.isFile()) { + return androidJar + } else { + throw new Exception("Can't find android.jar for $smackMinAndroidSdk API. Please install corresponding SDK platform package") + } +} + +def getAndroidJavadocOffline() { + def androidHome = new File("$System.env.ANDROID_HOME") + if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set") + return "$System.env.ANDROID_HOME" + "/docs/reference" +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index f606d75b1..9c4bf1343 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,4 +14,5 @@ include 'smack-core', 'smack-jingle-old', 'smack-bosh', 'smack-android', + 'smack-android-extensions', 'smack-java7' diff --git a/smack-android-extensions/build.gradle b/smack-android-extensions/build.gradle new file mode 100644 index 000000000..484d457aa --- /dev/null +++ b/smack-android-extensions/build.gradle @@ -0,0 +1,19 @@ +description = """\ +Extra Smack extensions for Android.""" + +// Note that the test dependencies (junit, …) are inferred from the +// sourceSet.test of the core subproject +dependencies { + compile project(':smack-extensions') +} + +compileJava { + options.bootClasspath = androidBootClasspath +} + +// See http://stackoverflow.com/a/2823592/194894 +// TODO this doesn't seem to work right now. But on the other hand it +// is not really required, just to avoid a javadoc compiler warning +javadoc { + options.linksOffline "http://developer.android.com/reference", androidJavadocOffline +} \ No newline at end of file diff --git a/smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java b/smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java new file mode 100644 index 000000000..2ac9458fb --- /dev/null +++ b/smack-android-extensions/src/main/java/org/jivesoftware/smackx/ping/android/ServerPingWithAlarmManager.java @@ -0,0 +1,159 @@ +/** + * + * Copyright © 2014 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smackx.ping.android; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.logging.Logger; + +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smackx.ping.PingManager; + +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; + +/** + * Send automatic server pings with the help of {@link AlarmManager}. + *

+ * Smack's {@link PingManager} uses a ScheduledThreadPoolExecutor to schedule the + * automatic server pings, but on Android, those scheduled pings are not reliable. This is because + * the Android device may go into deep sleep where the system will not continue to run this causes + *

+ * That is the reason Android comes with an API to schedule those tasks: AlarmManager. Which this + * class uses to determine every 30 minutes if a server ping is necessary. The interval of 30 + * minutes is the ideal trade-off between reliability and low resource (battery) consumption. + *

+ *

+ * In order to use this class you need to call {@link #onCreate(Context)} once, for example + * in the onCreate() method of your Service holding the XMPPConnection. And to avoid + * leaking any resources, you should call {@link #onDestroy()} when you no longer need any of its + * functionality. + *

+ */ +public class ServerPingWithAlarmManager extends Manager { + + private static final Logger LOGGER = Logger.getLogger(ServerPingWithAlarmManager.class + .getName()); + + private static final String PING_ALARM_ACTION = "org.igniterealtime.smackx.ping.ACTION"; + + private static final Map INSTANCES = Collections + .synchronizedMap(new WeakHashMap()); + + static { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(XMPPConnection connection) { + getInstanceFor(connection); + } + }); + } + + public static synchronized ServerPingWithAlarmManager getInstanceFor(XMPPConnection connection) { + ServerPingWithAlarmManager serverPingWithAlarmManager = INSTANCES.get(connection); + if (serverPingWithAlarmManager == null) { + serverPingWithAlarmManager = new ServerPingWithAlarmManager(connection); + INSTANCES.put(connection, serverPingWithAlarmManager); + } + return serverPingWithAlarmManager; + } + + private boolean mEnabled = true; + + private ServerPingWithAlarmManager(XMPPConnection connection) { + super(connection); + } + + /** + * If enabled, ServerPingWithAlarmManager will call + * {@link PingManager#pingServerIfNecessary()} for the connection of this + * instance every half hour. + * + * @param enabled + */ + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + public boolean isEnabled() { + return mEnabled; + } + + private static final BroadcastReceiver ALARM_BROADCAST_RECEIVER = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + LOGGER.fine("Ping Alarm broadcast received"); + Iterator it = INSTANCES.keySet().iterator(); + while (it.hasNext()) { + XMPPConnection connection = it.next(); + if (ServerPingWithAlarmManager.getInstanceFor(connection).isEnabled()) { + LOGGER.fine("Calling pingServerIfNecessary for connection " + + connection.getConnectionCounter()); + PingManager.getInstanceFor(connection).pingServerIfNecessary(); + } else { + LOGGER.fine("NOT calling pingServerIfNecessary (disabled) on connection " + + connection.getConnectionCounter()); + } + } + } + }; + + private static Context sContext; + private static PendingIntent sPendingIntent; + private static AlarmManager sAlarmManager; + + /** + * Register a pending intent with the AlarmManager to be broadcasted every + * half hour and register the alarm broadcast receiver to receive this + * intent. The receiver will check all known questions if a ping is + * Necessary when invoked by the alarm intent. + * + * @param context + */ + public static void onCreate(Context context) { + sContext = context; + context.registerReceiver(ALARM_BROADCAST_RECEIVER, new IntentFilter(PING_ALARM_ACTION)); + sAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + sPendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(PING_ALARM_ACTION), 0); + sAlarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + AlarmManager.INTERVAL_HALF_HOUR, + AlarmManager.INTERVAL_HALF_HOUR, sPendingIntent); + } + + /** + * Unregister the alarm broadcast receiver and cancel the alarm. + */ + public static void onDestroy() { + sContext.unregisterReceiver(ALARM_BROADCAST_RECEIVER); + sAlarmManager.cancel(sPendingIntent); + } +} diff --git a/smack-android/build.gradle b/smack-android/build.gradle index 31793f3db..2547167d4 100644 --- a/smack-android/build.gradle +++ b/smack-android/build.gradle @@ -5,8 +5,6 @@ Usually you want to add additional dependencies to smack-tcp, smack-extensions and smack-experimental.""" ext { - smackMinAndroidSdk = 8 - androidProjects = [':smack-tcp',':smack-core', ':smack-resolver-minidns', ':smack-sasl-provided', ':smack-extensions', ':smack-experimental'].collect{ project(it) } } // Note that the test dependencies (junit, …) are inferred from the @@ -20,32 +18,15 @@ dependencies { } } -def getAndroidRuntimeJar() { - def androidHome = new File("$System.env.ANDROID_HOME") - if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set") - def androidJar = new File("$androidHome/platforms/android-$smackMinAndroidSdk/android.jar") - if (androidJar.isFile()) { - return androidJar - } else { - throw new Exception("Can't find android.jar for $smackMinAndroidSdk API. Please install corresponding SDK platform package") - } -} - -def getAndroidJavadocOffline() { - def androidHome = new File("$System.env.ANDROID_HOME") - if (!androidHome.isDirectory()) throw new Exception("ANDROID_HOME not found or set") - return "$System.env.ANDROID_HOME" + "/docs/reference" -} - compileJava { - options.bootClasspath = getAndroidRuntimeJar() + options.bootClasspath = androidBootClasspath } // See http://stackoverflow.com/a/2823592/194894 // TODO this doesn't seem to work right now. But on the other hand it // is not really required, just to avoid a javadoc compiler warning javadoc { - options.linksOffline "http://developer.android.com/reference", getAndroidJavadocOffline() + options.linksOffline "http://developer.android.com/reference", androidJavadocOffline } configure (androidProjects) { @@ -53,7 +34,7 @@ configure (androidProjects) { source = compileJava.source classpath = compileJava.classpath destinationDir = new File(buildDir, 'android') - options.bootClasspath = getAndroidRuntimeJar() + options.bootClasspath = androidBootClasspath } }