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
+ *
+ * - the system time to not move forward, which means that the time spent in deep sleep is not
+ * counted towards the scheduled delay time
+ * - the scheduled Runnable is not run while the system is in deep sleep.
+ *
+ * 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
}
}