原文地址:http://dalelane.co.uk/blog/?p=1599
概述
如何在Android移动应用程序中使用MQTT接收推送通知
背景
我之前曾经写过一篇文章MQTT as a technology for doing push notifications to mobile。当时我写那篇文章时,我提供了一个Android工程的例子。但是,那是我第一次做Android开发,虽然它是一个好的Java MQTT示例,但它是一个糟糕的Android示例-我当时不懂Android平台是如何工作的。
hackday app用来从网站推送更新消息到你的手机(pushing updates from websites to your phone)并且学到了如何是它正常工作。好吧,如果不能正常工作,至少也要比之前好一点。
但是Google仍然把人们导航到我的旧的、可能并没有帮助的例子。所以是时候分享一些更有用的东西了。
我已经把示例实现的完整源码(full source for a sample implementation)放到了下方,(请注意,我正在使用来自ibm.com的Java J2EE客户端库)。希望其中的意见是足够清楚的,但这里有几个要点。
Services vs Activities
我们先从简单的开始。我第一次尝试中最明显的一个问题是没有意识到活动和服务之间的区别。
当你编写Android GUI时,您可以extend Activity。手机上一次只能运行一个activity-用户正在使用的app。当你切换到其他app或者关闭当前app时,当前的activity就会被销毁掉。如果app运行时你旋转一下屏幕,当前activity会被销毁并开启一个新的活动来实现新的方向的GUI。
重点是-activities是短暂活动。为了使它们美观,不要让他们做任何长期运行的繁重任务。
不要把任何MQTT的东西放入你的activity中,因为那样它会很快被系统杀掉,并且不会有什么提示。将它们写在后台线程中同样也会很快被系统杀掉。
我第一个MQTT例子(将所有任务都写到了一个activity中)是完全不可靠的。我通过创建一个MQTTClient对象来构造一个TCP/IP长连接,然后如果我把手机方向旋转一些,我的MQTTClient 对象就会被垃圾系统回收了。
相反,我现在使用一个Service。Services旨在实现长期运行的操作-多个service可以同时运行。如果你的GUI activity销毁并被垃圾系统回收了,service仍然能够在后台保持运行。
一个MQTTClient 对象能够在一个service对象中存活,并保持一个TCP/IP长连接
Make it sticky(让service有粘性)
services意味着长期运行,但不会永远持续下去。 如果手机内存不足 - 通常是因为用户正在前台使用需要大量内存的应用程序(因此被视为高优先级)。
这种情况经常发生-并不像出现一次蓝月亮那样不寻常。我的经验告诉我,你的service像这样的情况每天都在发生。有几次,我的服务可能会幸免于难,然后就会毫无警告的被杀死了。
通过在你的service启动时返回START_STICKY常量,你可以告诉Android系统,如果它不得不杀掉你的服务来释放宝贵的资源时,你希望如果资源再次可用时再次重启你的服务。
A persistent connection(一个长连接)
这里有一些你能免费得到的东西,但是我想它足够有趣值得注释一下。
如果你创建一个MQTTClient对象,其中一件事是设置一个TCP/IP长连接。 如果接收到了消息,则会回调publishArrived方法。
当用户没有使用手机并关闭屏幕时,Android开始关闭电源(可能翻译成开启省电模式更合理)以节省电池的使用寿命。 我关心的是关闭手机可能会破坏我的连接,或停止正在等待传入消息的代码。
虽然我没有找到一个很好的例子说明这一点,但似乎即使您的手机已经睡着了,如果在连接上收到了数据,你的代码将被唤醒并调用publishArrived放法。 当socket上的任何数据到达时,socket阻塞的MQTT客户端库代码将会复原。
Keeping the connection alive – in a bad way(使连接保活-通过一种不好的方式)
好吧,你几乎可以免费得到它了。
当连接到MQTT服务器时,其中一个参数是一个keepAlive时间段 - 一个客户端和服务器之间关于服务器期望听到客户端连接的频率的协议。
如果服务器在keepAlive时间段期间没有听到客户端连接,它就会假设客户端已经消失并关闭连接。
MQTTClient对象启动一个后台线程,该后台线程负责频繁地向服务器发送ping消息,以保持连接的存活。
我上面提到了,当用户没有使用手机并关闭屏幕时,Android开始关闭电源(可能翻译成开启省电模式更合理)以节省电池的使用寿命,包括关闭CPU。 如果CPU停止了,则该后台线程也会停止。
没有后台线程,在keepAlive时间段到期后,服务器关闭连接。 如果你使用的是RSMB,则会在服务器看到下面的日志。
20110130 234952.683 CWNAN0024I 1200 second keepalive timeout for client 12964204954459774d56d6, ending connection
应对这种情况最简单的方法是使用Wake Lock(唤醒锁)机制。 这是一种防止手机关机(或者该理解为进入省电模式)的应用程序。 通过使用PARTIAL_WAKE_LOCK,即使用户按下电源按钮(锁屏),也可以保持CPU运行。
如果你在进行MQTT连接之前采取了PARTIAL_WAKE_LOCK机制,并保留它,则在运行应用程序时,CPU不会停止,标准的MQTTClient后台线程将永远不会停止,并且会长时间保持网络连接。
但是...呃...这也意味着你已经阻止了用户关掉(锁屏)他们的手机! 这有点激进,因为它对手机的电池寿命有很大的影响。 所以我宁愿避免这种情况。
Testing what happens when you turn your phone off – a gotcha(测试当您关闭(锁屏)手机时会发生什么?)
我早些时候尝试在Android中使用一个MQTT服务时,没有使用Wake Lock(唤醒锁)机制,但它仍然工作正常。
我并没有意识到,我有几个应用程序安装在我的手机上,而它们中部分持续保持Wake Lock(唤醒锁)机制。 即使我的应用程序没有阻止手机的CPU关闭,其他应用程序却做了。 我得到了这个“好处”,所以没有意识到我存在一个问题。
adb shell dumpsys命令吐露出了关于已连接手机的一些东西,包括当前的唤醒锁的列表。 (在输出中搜索'mLocks.size')。 你必须这样做几次才能看到发生了什么 - 一些应用程序将不断的完全合法地采取部分唤醒锁机制,而他们会做一些关键的事情来保证他们不被打断。
问题在于那些在启动时会发出唤醒锁,并永远保留它们 - 始终位于dumpsys输出中的mLocks表中的应用程序。
在卸载了这些app后,我的测试结果变得十分不同,它告诉我我需要采取些措施来使连接保活。
Keeping the connection alive – in a good way(使连接保活-一个很好的方式)
最好的方法可能是以Android的工作方式为中心来编写MQTT客户端库。但是,说实话,我太懒了 - 所以我使用现有的Java J2EE客户端库,但除此之外,我实现了我自己的keepAlive方法,假设客户端库的一个不够好(in the assumption that the client library’s one wont be good enough)。
AlarmManager提供了一种方法,要求Android在指定时间内唤醒睡眠的设备,以便在代码中运行特定的方法。
在当前的连接活动期限到期之前,我预设了一些唤醒手机的事务,它有足够长时间发送一个MQTT ping,之后手机又可以自由返回睡眠状态。 如果手机屏幕关闭,这不会唤醒手机屏幕 - 它只是启动CPU,以便我的保持活动代码可以运行。
请注意,MQTT规范说服务器应该向客户端再提供一半的保持活动间隔的“宽限期”(“grace period”)(例如,如果保持活动为20分钟,那么服务器在最后听到客户端连接终止之后的30分钟内不会切断客户端)。 这意味着我不需要担心在keepAlive间隔到达之前获取我的ping - 在规范中有足够的余地,我可以在最后一次与服务器的交互的几秒中之后安排一个ping操作来保活。
Don’t rely on connectionLost(不要依赖)
MQTTClient库有一个connectionLost回调,如果你的连接断开了,就会调用它来告诉你的代码。 如果你设置了一个长时间的保活间隔,则客户端会等待很长时间,然后才会考虑听不到服务器连接意味着连接断开了。
这是一个有用的回调,但本身响应并不灵敏,特别是对于连接状态频繁变化的手机(例如,移动数据覆盖和丢失连接,在WiFi和移动数据连接之间切换, 等等。)
Android提供通知系统,通知你的应用程序的网络状态更改。 当收到这些通知时,我认为我的连接不再可靠,并重新连接。
如果你在使用RSMB,你能在服务端看到如下日志信息.
20110129 230652.613 CWNAN0033I Connection attempt to listener 1883 received from client 12963424184129774d56d6 on address 93.97.32.135:61055 20110129 230703.834 CWNAN0033I Connection attempt to listener 1883 received from client 12963424184129774d56d6 on address 82.132.248.199:58529 20110129 230703.834 CWNAN0034I Duplicate connection attempt received for client identifier "12963424184129774d56d6", ending oldest connectionRespect the user’s requests(尊重用户的请求)
Android为用户提供了一种方法来禁止应用程序使用后台数据。 但是这并不是强制性的。
用户可以取消“Background data”选项,以指示应用程序不应在后台使用网络,并为应用程序提供一个API来检查该值。
但应用程序可以自由忽略这一设置。
这不是一件好事 - 用户应该能够决定自己的手机如何使用。
我在做任何事情之前先检查这个值,并设置一个监听器,以防在我的服务运行时值被更改。
Create a unique client ID(创建一个唯一的clientID)
MQTT规范要求每个客户端都有唯一的客户端ID。
我正在使用手机中的唯一设备ID(unique device ID from the phone)来创建。 令人讨厌的是,这不适用于虚拟机,所以我使用的时间戳是1970年1月1日以来以毫秒为单位的时间戳。
请注意,规范还定义了客户端ID的最大允许长度,因此我必要时将其截断。
Assuming that you will spend time without a connection(假设你的时间将在无连接状态下)
这更多的是一般的方法,而不是一些特定的代码。 但是,该服务必须假定它将经常断开连接,并且应该在可能的情况下处理重新连接,而不需要用户干预。
我已经看到有些应用程序(我在看你,Facebook!),如果他们无法连接到他们的服务器,只是陷入无休止的重试循环 - 不断尝试和失败。
在我个人笔记中,我有比SIM卡更多的手机。 而且我注意到安装了这样的应用程序,当没有SIM卡时,电话的电池寿命会更糟,部分是因为这些应用程序不断在没有网络连接的状态下尝试与服务器进行通信。
相反,检测网络何时不可用,并停止一切。 不要轮询,不要ping,不要继续尝试或检查,只要求在重新连接时会收到通知,停止任何保活的定时器任务,并且进入睡眠状态。 做好所有备份并在连接再次可用时迅速运行起来。
还有一个可以考虑的问题就是你的服务器端盖如何发送消息。当你公布一个消息时,你可以设置使信息"保留"。这意味着如果客户端不可用时,服务器将为它安全地存储消息,并在客户端重新连接时传递它们。
这可能不适用于所有情况,但是它确实是我经常用到的,而且它似乎更适合于经常断开网络连接的手机,客户端不必错过自身在网络黑点时服务器发布的消息。
Reminding the user that you’re running(提醒用户你正在运行)
在桌面窗口环境中,用户可以知道系统正在运行什么,因为他们可以看到他们已经打开的窗口。
在一个一次只能运行一个app的移动系统中,很难感觉到后台在运行什么。对于在后台运行特殊任务并会在任务完成后自动停止的service,这样是可以的。但是对于如果没有人为操作来停止就会永久运行的service,用户应该被提醒,该service正在后台运行。
在我的示例中,我在状态栏中使用持续的通知,每当service运行时,通知将一直保持在这里。 由此来提醒用户该service正在使用他们手机的一些资源。
你可能认为这样做可能会令人讨厌,所以也许这应该是可选的 - 如果用户愿意在没有通知提醒的情况下运行service,则可以让用户隐藏通知。 但即使如此,我会默认显示通知。
Keeping the received data safe(保证接收数据的安全)
可能service运行的大部分时间里,并没有一个activity UI来显示最新的数据。activity UI可能会在之后的某个时间启动,并希望得到自身运行前已经获取到的消息。
这意味着该服务可能应该存储它收到的消息。 这将取决于特定的应用程序。
处理非常少的数据的应用程序-例如如果应用程序和/或电话重新启动时,不需要更新和通知数据是持久的,则可以将该数据存储在service的变量中。 这就是我在下面的示例中所做的 - 将它存储在本地哈希表中。
处理大量数据和/或即使应用程序/手机重新启动仍需要持久数据的应用程序需要将数据存储到某些安全的地方。这里有一些选择variety of storage options available-选择一个合适你数据类型/频率/大小等的存储方式。
Choosing a keep-alive value(选择一个保活值)
我在上面提到了KeepAlive参数-指定过久客户端应该与服务器联系一次来使连接保活。这是个棘手的问题。ping太频繁了,你会不必要的浪费电池寿命。如果ping太稀少,你可能不会意识到你已经失去了连接直到下一次失败的ping操作。你应该在你的应用程序处理数据的时间敏感度与电池寿命可接受的影响之间来权衡一个值。
但也可能存在网络特殊的关注点? 移动网络运营商在垃圾回收发生前会持续空闲连接多长时间? 例如,如果你的用户的网络运营商在20分钟后回收空闲连接,则可能没有保活为40分钟的连接存在。
我需要更多的尝试,但在此期间,我一直在使用20分钟的保活值,这似乎足够可靠了。
Passing config values(传递配置值)
有一个小点,但值得考虑的是如何给service所需的连接参数,如服务器主机名。 有很多方法可以做到这一点。
由于我不希望用户每次启动服务时都必须输入这些参数,所以我还需要保留这些设置。 service可以访问,activity UI可以配置并且可以持续存储少量配置信息的最简单方式是使用SharedPreferences - 下面为示例。
Rebroadcasting received messages for interested UIs(将收到的信息重新广播给感兴趣的UI)
我之前概述了在service中实现的后端,网络长连接与在Activity中实现的UI之间的基本划分,它们之间有多种方式可以相互通信。
对于这个例子,我使用Broadcasts。 它本质上是手机本地的订阅/发布机制。 每当service从网络接收到MQTT消息时,它将在本地手机上重新广播。 如果一个Activity UI正在运行,它将通过创建BroadcastReceiver来订阅这些消息。 当activity不活动时,它可以取消注册这些侦听器。
这意味着service不需要跟踪Activity UI是否正在运行 - 这样就实现了应用程序的后端和前端之间的耦合。
My sample Service – the code(我的service例子-源码)
说完所有这一切的情况 - 这里是代码。 我的博客不能很好地显示代码,所以你可以将其复制并粘贴到你喜欢的IDE中并启用Java格式化来阅读。
我现在把代码贴出来希望它对你有帮助。如果你使用它,希望得到一个感谢的评论或者转载。
package dalelane.android.mqtt; import java.lang.ref.WeakReference; import java.util.Calendar; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Color; import android.net.ConnectivityManager; import android.os.Binder; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.provider.Settings; import android.provider.Settings.Secure; import android.util.Log; import com.ibm.mqtt.IMqttClient; import com.ibm.mqtt.MqttClient; import com.ibm.mqtt.MqttException; import com.ibm.mqtt.MqttNotConnectedException; import com.ibm.mqtt.MqttPersistence; import com.ibm.mqtt.MqttPersistenceException; import com.ibm.mqtt.MqttSimpleCallback; /* * An example of how to implement an MQTT client in Android, able to receive * push notifications from an MQTT message broker server. * * Dale Lane ([email protected]) * 28 Jan 2011 */ public class MQTTService extends Service implements MqttSimpleCallback { /************************************************************************/ /* CONSTANTS */ /************************************************************************/ // something unique to identify your app - used for stuff like accessing // application preferences public static final String APP_ID = "com.dalelane.mqtt"; // constants used to notify the Activity UI of received messages public static final String MQTT_MSG_RECEIVED_INTENT = "com.dalelane.mqtt.MSGRECVD"; public static final String MQTT_MSG_RECEIVED_TOPIC = "com.dalelane.mqtt.MSGRECVD_TOPIC"; public static final String MQTT_MSG_RECEIVED_MSG = "com.dalelane.mqtt.MSGRECVD_MSGBODY"; // constants used to tell the Activity UI the connection status public static final String MQTT_STATUS_INTENT = "com.dalelane.mqtt.STATUS"; public static final String MQTT_STATUS_MSG = "com.dalelane.mqtt.STATUS_MSG"; // constant used internally to schedule the next ping event public static final String MQTT_PING_ACTION = "com.dalelane.mqtt.PING"; // constants used by status bar notifications public static final int MQTT_NOTIFICATION_ONGOING = 1; public static final int MQTT_NOTIFICATION_UPDATE = 2; // constants used to define MQTT connection status public enum MQTTConnectionStatus { INITIAL, // initial status CONNECTING, // attempting to connect CONNECTED, // connected NOTCONNECTED_WAITINGFORINTERNET, // can't connect because the phone // does not have Internet access NOTCONNECTED_USERDISCONNECT, // user has explicitly requested // disconnection NOTCONNECTED_DATADISABLED, // can't connect because the user // has disabled data access NOTCONNECTED_UNKNOWNREASON // failed to connect for some reason } // MQTT constants public static final int MAX_MQTT_CLIENTID_LENGTH = 22; /************************************************************************/ /* VARIABLES used to maintain state */ /************************************************************************/ // status of MQTT client connection private MQTTConnectionStatus connectionStatus = MQTTConnectionStatus.INITIAL; /************************************************************************/ /* VARIABLES used to configure MQTT connection */ /************************************************************************/ // taken from preferences // host name of the server we're receiving push notifications from private String brokerHostName = ""; // taken from preferences // topic we want to receive messages about // can include wildcards - e.g. '#' matches anything private String topicName = ""; // defaults - this sample uses very basic defaults for it's interactions // with message brokers private int brokerPortNumber = 1883; private MqttPersistence usePersistence = null; private boolean cleanStart = false; private int[] qualitiesOfService = { 0 } ; // how often should the app ping the server to keep the connection alive? // // too frequently - and you waste battery life // too infrequently - and you wont notice if you lose your connection // until the next unsuccessfull attempt to ping // // it's a trade-off between how time-sensitive the data is that your // app is handling, vs the acceptable impact on battery life // // it is perhaps also worth bearing in mind the network's support for // long running, idle connections. Ideally, to keep a connection open // you want to use a keep alive value that is less than the period of // time after which a network operator will kill an idle connection private short keepAliveSeconds = 20 * 60; // This is how the Android client app will identify itself to the // message broker. // It has to be unique to the broker - two clients are not permitted to // connect to the same broker using the same client ID. private String mqttClientId = null; /************************************************************************/ /* VARIABLES - other local variables */ /************************************************************************/ // connection to the message broker private IMqttClient mqttClient = null; // receiver that notifies the Service when the phone gets data connection private NetworkConnectionIntentReceiver netConnReceiver; // receiver that notifies the Service when the user changes data use preferences private BackgroundDataChangeIntentReceiver dataEnabledReceiver; // receiver that wakes the Service up when it's time to ping the server private PingSender pingSender; /************************************************************************/ /* METHODS - core Service lifecycle methods */ /************************************************************************/ // see http://developer.android.com/guide/topics/fundamentals.html#lcycles @Override public void onCreate() { super.onCreate(); // reset status variable to initial state connectionStatus = MQTTConnectionStatus.INITIAL; // create a binder that will let the Activity UI send // commands to the Service mBinder = new LocalBinder(this); // get the broker settings out of app preferences // this is not the only way to do this - for example, you could use // the Intent that starts the Service to pass on configuration values SharedPreferences settings = getSharedPreferences(APP_ID, MODE_PRIVATE); brokerHostName = settings.getString("broker", ""); topicName = settings.getString("topic", ""); // register to be notified whenever the user changes their preferences // relating to background data use - so that we can respect the current // preference dataEnabledReceiver = new BackgroundDataChangeIntentReceiver(); registerReceiver(dataEnabledReceiver, new IntentFilter(ConnectivityManager.ACTION_BACKGROUND_DATA_SETTING_CHANGED)); // define the connection to the broker defineConnectionToBroker(brokerHostName); } @Override public void onStart(final Intent intent, final int startId) { // This is the old onStart method that will be called on the pre-2.0 // platform. On 2.0 or later we override onStartCommand() so this // method will not be called. new Thread(new Runnable() { @Override public void run() { handleStart(intent, startId); } }, "MQTTservice").start(); } @Override public int onStartCommand(final Intent intent, int flags, final int startId) { new Thread(new Runnable() { @Override public void run() { handleStart(intent, startId); } }, "MQTTservice").start(); // return START_NOT_STICKY - we want this Service to be left running // unless explicitly stopped, and it's process is killed, we want it to // be restarted return START_STICKY; } synchronized void handleStart(Intent intent, int startId) { // before we start - check for a couple of reasons why we should stop if (mqttClient == null) { // we were unable to define the MQTT client connection, so we stop // immediately - there is nothing that we can do stopSelf(); return; } ConnectivityManager cm = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE); if (cm.getBackgroundDataSetting() == false) // respect the user's request not to use data! { // user has disabled background data connectionStatus = MQTTConnectionStatus.NOTCONNECTED_DATADISABLED; // update the app to show that the connection has been disabled broadcastServiceStatus("Not connected - background data disabled"); // we have a listener running that will notify us when this // preference changes, and will call handleStart again when it // is - letting us pick up where we leave off now return; } // the Activity UI has started the MQTT service - this may be starting // the Service new for the first time, or after the Service has been // running for some time (multiple calls to startService don't start // multiple Services, but it does call this method multiple times) // if we have been running already, we re-send any stored data rebroadcastStatus(); rebroadcastReceivedMessages(); // if the Service was already running and we're already connected - we // don't need to do anything if (isAlreadyConnected() == false) { // set the status to show we're trying to connect connectionStatus = MQTTConnectionStatus.CONNECTING; // we are creating a background service that will run forever until // the user explicity stops it. so - in case they start needing // to save battery life - we should ensure that they don't forget // we're running, by leaving an ongoing notification in the status // bar while we are running NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); Notification notification = new Notification(R.drawable.icon, "MQTT", System.currentTimeMillis()); notification.flags |= Notification.FLAG_ONGOING_EVENT; notification.flags |= Notification.FLAG_NO_CLEAR; Intent notificationIntent = new Intent(this, MQTTNotifier.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); notification.setLatestEventInfo(this, "MQTT", "MQTT Service is running", contentIntent); nm.notify(MQTT_NOTIFICATION_ONGOING, notification); // before we attempt to connect - we check if the phone has a // working data connection if (isOnline()) { // we think we have an Internet connection, so try to connect // to the message broker if (connectToBroker()) { // we subscribe to a topic - registering to receive push // notifications with a particular key // in a 'real' app, you might want to subscribe to multiple // topics - I'm just subscribing to one as an example // note that this topicName could include a wildcard, so // even just with one subscription, we could receive // messages for multiple topics subscribeToTopic(topicName); } } else { // we can't do anything now because we don't have a working // data connection connectionStatus = MQTTConnectionStatus.NOTCONNECTED_WAITINGFORINTERNET; // inform the app that we are not connected broadcastServiceStatus("Waiting for network connection"); } } // changes to the phone's network - such as bouncing between WiFi // and mobile data networks - can break the MQTT connection // the MQTT connectionLost can be a bit slow to notice, so we use // Android's inbuilt notification system to be informed of // network changes - so we can reconnect immediately, without // haing to wait for the MQTT timeout if (netConnReceiver == null) { netConnReceiver = new NetworkConnectionIntentReceiver(); registerReceiver(netConnReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); } // creates the intents that are used to wake up the phone when it is // time to ping the server if (pingSender == null) { pingSender = new PingSender(); registerReceiver(pingSender, new IntentFilter(MQTT_PING_ACTION)); } } @Override public void onDestroy() { super.onDestroy(); // disconnect immediately disconnectFromBroker(); // inform the app that the app has successfully disconnected broadcastServiceStatus("Disconnected"); // try not to leak the listener if (dataEnabledReceiver != null) { unregisterReceiver(dataEnabledReceiver); dataEnabledReceiver = null; } if (mBinder != null) { mBinder.close(); mBinder = null; } } /************************************************************************/ /* METHODS - broadcasts and notifications */ /************************************************************************/ // methods used to notify the Activity UI of something that has happened // so that it can be updated to reflect status and the data received // from the server private void broadcastServiceStatus(String statusDescription) { // inform the app (for times when the Activity UI is running / // active) of the current MQTT connection status so that it // can update the UI accordingly Intent broadcastIntent = new Intent(); broadcastIntent.setAction(MQTT_STATUS_INTENT); broadcastIntent.putExtra(MQTT_STATUS_MSG, statusDescription); sendBroadcast(broadcastIntent); } private void broadcastReceivedMessage(String topic, String message) { // pass a message received from the MQTT server on to the Activity UI // (for times when it is running / active) so that it can be displayed // in the app GUI Intent broadcastIntent = new Intent(); broadcastIntent.setAction(MQTT_MSG_RECEIVED_INTENT); broadcastIntent.putExtra(MQTT_MSG_RECEIVED_TOPIC, topic); broadcastIntent.putExtra(MQTT_MSG_RECEIVED_MSG, message); sendBroadcast(broadcastIntent); } // methods used to notify the user of what has happened for times when // the app Activity UI isn't running private void notifyUser(String alert, String title, String body) { NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); Notification notification = new Notification(R.drawable.icon, alert, System.currentTimeMillis()); notification.defaults |= Notification.DEFAULT_LIGHTS; notification.defaults |= Notification.DEFAULT_SOUND; notification.defaults |= Notification.DEFAULT_VIBRATE; notification.flags |= Notification.FLAG_AUTO_CANCEL; notification.ledARGB = Color.MAGENTA; Intent notificationIntent = new Intent(this, MQTTNotifier.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); notification.setLatestEventInfo(this, title, body, contentIntent); nm.notify(MQTT_NOTIFICATION_UPDATE, notification); } /************************************************************************/ /* METHODS - binding that allows access from the Actitivy */ /************************************************************************/ // trying to do local binding while minimizing leaks - code thanks to // Geoff Bruckner - which I found at // http://groups.google.com/group/cw-android/browse_thread/thread/d026cfa71e48039b/c3b41c728fedd0e7?show_docid=c3b41c728fedd0e7 private LocalBinder mBinder; @Override public IBinder onBind(Intent intent) { return mBinder; } public class LocalBinder extends Binder { private WeakReferencemService; public LocalBinder(S service) { mService = new WeakReference(service); } public S getService() { return mService.get(); } public void close() { mService = null; } } // // public methods that can be used by Activities that bind to the Service // public MQTTConnectionStatus getConnectionStatus() { return connectionStatus; } public void rebroadcastStatus() { String status = ""; switch (connectionStatus) { case INITIAL: status = "Please wait"; break; case CONNECTING: status = "Connecting..."; break; case CONNECTED: status = "Connected"; break; case NOTCONNECTED_UNKNOWNREASON: status = "Not connected - waiting for network connection"; break; case NOTCONNECTED_USERDISCONNECT: status = "Disconnected"; break; case NOTCONNECTED_DATADISABLED: status = "Not connected - background data disabled"; break; case NOTCONNECTED_WAITINGFORINTERNET: status = "Unable to connect"; break; } // // inform the app that the Service has successfully connected broadcastServiceStatus(status); } public void disconnect() { disconnectFromBroker(); // set status connectionStatus = MQTTConnectionStatus.NOTCONNECTED_USERDISCONNECT; // inform the app that the app has successfully disconnected broadcastServiceStatus("Disconnected"); } /************************************************************************/ /* METHODS - MQTT methods inherited from MQTT classes */ /************************************************************************/ /* * callback - method called when we no longer have a connection to the * message broker server */ public void connectionLost() throws Exception { // we protect against the phone switching off while we're doing this // by requesting a wake lock - we request the minimum possible wake // lock - just enough to keep the CPU running until we've finished PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MQTT"); wl.acquire(); // // have we lost our data connection? // if (isOnline() == false) { connectionStatus = MQTTConnectionStatus.NOTCONNECTED_WAITINGFORINTERNET; // inform the app that we are not connected any more broadcastServiceStatus("Connection lost - no network connection"); // // inform the user (for times when the Activity UI isn't running) // that we are no longer able to receive messages notifyUser("Connection lost - no network connection", "MQTT", "Connection lost - no network connection"); // // wait until the phone has a network connection again, when we // the network connection receiver will fire, and attempt another // connection to the broker } else { // // we are still online // the most likely reason for this connectionLost is that we've // switched from wifi to cell, or vice versa // so we try to reconnect immediately // connectionStatus = MQTTConnectionStatus.NOTCONNECTED_UNKNOWNREASON; // inform the app that we are not connected any more, and are // attempting to reconnect broadcastServiceStatus("Connection lost - reconnecting..."); // try to reconnect if (connectToBroker()) { subscribeToTopic(topicName); } } // we're finished - if the phone is switched off, it's okay for the CPU // to sleep now wl.release(); } /* * callback - called when we receive a message from the server */ public void publishArrived(String topic, byte[] payloadbytes, int qos, boolean retained) { // we protect against the phone switching off while we're doing this // by requesting a wake lock - we request the minimum possible wake // lock - just enough to keep the CPU running until we've finished PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MQTT"); wl.acquire(); // // I'm assuming that all messages I receive are being sent as strings // this is not an MQTT thing - just me making as assumption about what // data I will be receiving - your app doesn't have to send/receive // strings - anything that can be sent as bytes is valid String messageBody = new String(payloadbytes); // // for times when the app's Activity UI is not running, the Service // will need to safely store the data that it receives if (addReceivedMessageToStore(topic, messageBody)) { // this is a new message - a value we haven't seen before // // inform the app (for times when the Activity UI is running) of the // received message so the app UI can be updated with the new data broadcastReceivedMessage(topic, messageBody); // // inform the user (for times when the Activity UI isn't running) // that there is new data available notifyUser("New data received", topic, messageBody); } // receiving this message will have kept the connection alive for us, so // we take advantage of this to postpone the next scheduled ping scheduleNextPing(); // we're finished - if the phone is switched off, it's okay for the CPU // to sleep now wl.release(); } /************************************************************************/ /* METHODS - wrappers for some of the MQTT methods that we use */ /************************************************************************/ /* * Create a client connection object that defines our connection to a * message broker server */ private void defineConnectionToBroker(String brokerHostName) { String mqttConnSpec = "tcp://" + brokerHostName + "@" + brokerPortNumber; try { // define the connection to the broker mqttClient = MqttClient.createMqttClient(mqttConnSpec, usePersistence); // register this client app has being able to receive messages mqttClient.registerSimpleHandler(this); } catch (MqttException e) { // something went wrong! mqttClient = null; connectionStatus = MQTTConnectionStatus.NOTCONNECTED_UNKNOWNREASON; // // inform the app that we failed to connect so that it can update // the UI accordingly broadcastServiceStatus("Invalid connection parameters"); // // inform the user (for times when the Activity UI isn't running) // that we failed to connect notifyUser("Unable to connect", "MQTT", "Unable to connect"); } } /* * (Re-)connect to the message broker */ private boolean connectToBroker() { try { // try to connect mqttClient.connect(generateClientId(), cleanStart, keepAliveSeconds); // // inform the app that the app has successfully connected broadcastServiceStatus("Connected"); // we are connected connectionStatus = MQTTConnectionStatus.CONNECTED; // we need to wake up the phone's CPU frequently enough so that the // keep alive messages can be sent // we schedule the first one of these now scheduleNextPing(); return true; } catch (MqttException e) { // something went wrong! connectionStatus = MQTTConnectionStatus.NOTCONNECTED_UNKNOWNREASON; // // inform the app that we failed to connect so that it can update // the UI accordingly broadcastServiceStatus("Unable to connect"); // // inform the user (for times when the Activity UI isn't running) // that we failed to connect notifyUser("Unable to connect", "MQTT", "Unable to connect - will retry later"); // if something has failed, we wait for one keep-alive period before // trying again // in a real implementation, you would probably want to keep count // of how many times you attempt this, and stop trying after a // certain number, or length of time - rather than keep trying // forever. // a failure is often an intermittent network issue, however, so // some limited retry is a good idea scheduleNextPing(); return false; } } /* * Send a request to the message broker to be sent messages published with * the specified topic name. Wildcards are allowed. */ private void subscribeToTopic(String topicName) { boolean subscribed = false; if (isAlreadyConnected() == false) { // quick sanity check - don't try and subscribe if we // don't have a connection Log.e("mqtt", "Unable to subscribe as we are not connected"); } else { try { String[] topics = { topicName }; mqttClient.subscribe(topics, qualitiesOfService); subscribed = true; } catch (MqttNotConnectedException e) { Log.e("mqtt", "subscribe failed - MQTT not connected", e); } catch (IllegalArgumentException e) { Log.e("mqtt", "subscribe failed - illegal argument", e); } catch (MqttException e) { Log.e("mqtt", "subscribe failed - MQTT exception", e); } } if (subscribed == false) { // // inform the app of the failure to subscribe so that the UI can // display an error broadcastServiceStatus("Unable to subscribe"); // // inform the user (for times when the Activity UI isn't running) notifyUser("Unable to subscribe", "MQTT", "Unable to subscribe"); } } /* * Terminates a connection to the message broker. */ private void disconnectFromBroker() { // if we've been waiting for an Internet connection, this can be // cancelled - we don't need to be told when we're connected now try { if (netConnReceiver != null) { unregisterReceiver(netConnReceiver); netConnReceiver = null; } if (pingSender != null) { unregisterReceiver(pingSender); pingSender = null; } } catch (Exception eee) { // probably because we hadn't registered it Log.e("mqtt", "unregister failed", eee); } try { if (mqttClient != null) { mqttClient.disconnect(); } } catch (MqttPersistenceException e) { Log.e("mqtt", "disconnect failed - persistence exception", e); } finally { mqttClient = null; } // we can now remove the ongoing notification that warns users that // there was a long-running ongoing service running NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.cancelAll(); } /* * Checks if the MQTT client thinks it has an active connection */ private boolean isAlreadyConnected() { return ((mqttClient != null) && (mqttClient.isConnected() == true)); } private class BackgroundDataChangeIntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context ctx, Intent intent) { // we protect against the phone switching off while we're doing this // by requesting a wake lock - we request the minimum possible wake // lock - just enough to keep the CPU running until we've finished PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MQTT"); wl.acquire(); ConnectivityManager cm = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE); if (cm.getBackgroundDataSetting()) { // user has allowed background data - we start again - picking // up where we left off in handleStart before defineConnectionToBroker(brokerHostName); handleStart(intent, 0); } else { // user has disabled background data connectionStatus = MQTTConnectionStatus.NOTCONNECTED_DATADISABLED; // update the app to show that the connection has been disabled broadcastServiceStatus("Not connected - background data disabled"); // disconnect from the broker disconnectFromBroker(); } // we're finished - if the phone is switched off, it's okay for the CPU // to sleep now wl.release(); } } /* * Called in response to a change in network connection - after losing a * connection to the server, this allows us to wait until we have a usable * data connection again */ private class NetworkConnectionIntentReceiver extends BroadcastReceiver { @Override public void onReceive(Context ctx, Intent intent) { // we protect against the phone switching off while we're doing this // by requesting a wake lock - we request the minimum possible wake // lock - just enough to keep the CPU running until we've finished PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MQTT"); wl.acquire(); if (isOnline()) { // we have an internet connection - have another try at connecting if (connectToBroker()) { // we subscribe to a topic - registering to receive push // notifications with a particular key subscribeToTopic(topicName); } } // we're finished - if the phone is switched off, it's okay for the CPU // to sleep now wl.release(); } } /* * Schedule the next time that you want the phone to wake up and ping the * message broker server */ private void scheduleNextPing() { // When the phone is off, the CPU may be stopped. This means that our // code may stop running. // When connecting to the message broker, we specify a 'keep alive' // period - a period after which, if the client has not contacted // the server, even if just with a ping, the connection is considered // broken. // To make sure the CPU is woken at least once during each keep alive // period, we schedule a wake up to manually ping the server // thereby keeping the long-running connection open // Normally when using this Java MQTT client library, this ping would be // handled for us. // Note that this may be called multiple times before the next scheduled // ping has fired. This is good - the previously scheduled one will be // cancelled in favour of this one. // This means if something else happens during the keep alive period, // (e.g. we receive an MQTT message), then we start a new keep alive // period, postponing the next ping. PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, new Intent(MQTT_PING_ACTION), PendingIntent.FLAG_UPDATE_CURRENT); // in case it takes us a little while to do this, we try and do it // shortly before the keep alive period expires // it means we're pinging slightly more frequently than necessary Calendar wakeUpTime = Calendar.getInstance(); wakeUpTime.add(Calendar.SECOND, keepAliveSeconds); AlarmManager aMgr = (AlarmManager) getSystemService(ALARM_SERVICE); aMgr.set(AlarmManager.RTC_WAKEUP, wakeUpTime.getTimeInMillis(), pendingIntent); } /* * Used to implement a keep-alive protocol at this Service level - it sends * a PING message to the server, then schedules another ping after an * interval defined by keepAliveSeconds */ public class PingSender extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { // Note that we don't need a wake lock for this method (even though // it's important that the phone doesn't switch off while we're // doing this). // According to the docs, "Alarm Manager holds a CPU wake lock as // long as the alarm receiver's onReceive() method is executing. // This guarantees that the phone will not sleep until you have // finished handling the broadcast." // This is good enough for our needs. try { mqttClient.ping(); } catch (MqttException e) { // if something goes wrong, it should result in connectionLost // being called, so we will handle it there Log.e("mqtt", "ping failed - MQTT exception", e); // assume the client connection is broken - trash it try { mqttClient.disconnect(); } catch (MqttPersistenceException e1) { Log.e("mqtt", "disconnect failed - persistence exception", e1); } // reconnect if (connectToBroker()) { subscribeToTopic(topicName); } } // start the next keep alive period scheduleNextPing(); } } /************************************************************************/ /* APP SPECIFIC - stuff that would vary for different uses of MQTT */ /************************************************************************/ // apps that handle very small amounts of data - e.g. updates and // notifications that don't need to be persisted if the app / phone // is restarted etc. may find it acceptable to store this data in a // variable in the Service // that's what I'm doing in this sample: storing it in a local hashtable // if you are handling larger amounts of data, and/or need the data to // be persisted even if the app and/or phone is restarted, then // you need to store the data somewhere safely // see http://developer.android.com/guide/topics/data/data-storage.html // for your storage options - the best choice depends on your needs // stored internally private HashtabledataCache = new Hashtable (); private boolean addReceivedMessageToStore(String key, String value) { String previousValue = null; if (value.length() == 0) { previousValue = dataCache.remove(key); } else { previousValue = dataCache.put(key, value); } // is this a new value? or am I receiving something I already knew? // we return true if this is something new return ((previousValue == null) || (previousValue.equals(value) == false)); } // provide a public interface, so Activities that bind to the Service can // request access to previously received messages public void rebroadcastReceivedMessages() { Enumeration e = dataCache.keys(); while(e.hasMoreElements()) { String nextKey = e.nextElement(); String nextValue = dataCache.get(nextKey); broadcastReceivedMessage(nextKey, nextValue); } } /************************************************************************/ /* METHODS - internal utility methods */ /************************************************************************/ private String generateClientId() { // generate a unique client id if we haven't done so before, otherwise // re-use the one we already have if (mqttClientId == null) { // generate a unique client ID - I'm basing this on a combination of // the phone device id and the current timestamp String timestamp = "" + (new Date()).getTime(); String android_id = Settings.System.getString(getContentResolver(), Secure.ANDROID_ID); mqttClientId = timestamp + android_id; // truncate - MQTT spec doesn't allow client ids longer than 23 chars if (mqttClientId.length() > MAX_MQTT_CLIENTID_LENGTH) { mqttClientId = mqttClientId.substring(0, MAX_MQTT_CLIENTID_LENGTH); } } return mqttClientId; } private boolean isOnline() { ConnectivityManager cm = (ConnectivityManager)getSystemService(CONNECTIVITY_SERVICE); if(cm.getActiveNetworkInfo() != null && cm.getActiveNetworkInfo().isAvailable() && cm.getActiveNetworkInfo().isConnected()) { return true; } return false; } }
Using the Service(使用service)
最好,一些关于从你的activity使用该示例service的一些建议。
首先,您需要确保您已设置所需的权限:
开启 MQTT client,并将代理名称和标题保存到SharedPreferences。
SharedPreferences settings = getSharedPreferences(MQTTService.APP_ID, 0); SharedPreferences.Editor editor = settings.edit(); editor.putString("broker", preferenceBrokerHost); editor.putString("topic", preferenceBrokerUser); editor.commit();然后设置用于接收service发送广播的监听器,service会发送两种广播,MQTT消息和当前连接状态消息。
private StatusUpdateReceiver statusUpdateIntentReceiver; private MQTTMessageReceiver messageIntentReceiver; @Override public void onCreate(Bundle savedInstanceState) { ... statusUpdateIntentReceiver = new StatusUpdateReceiver(); IntentFilter intentSFilter = new IntentFilter(MQTTService.MQTT_STATUS_INTENT); registerReceiver(statusUpdateIntentReceiver, intentSFilter); messageIntentReceiver = new MQTTMessageReceiver(); IntentFilter intentCFilter = new IntentFilter(MQTTService.MQTT_MSG_RECEIVED_INTENT); registerReceiver(messageIntentReceiver, intentCFilter); ... } public class StatusUpdateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Bundle notificationData = intent.getExtras(); String newStatus = notificationData.getString(MQTTService.MQTT_STATUS_MSG); ... } } public class MQTTMessageReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Bundle notificationData = intent.getExtras(); String newTopic = notificationData.getString(MQTTService.MQTT_MSG_RECEIVED_TOPIC); String newData = notificationData.getString(MQTTService.MQTT_MSG_RECEIVED_MSG); ... } } @Override protected void onDestroy() { ... unregisterReceiver(statusUpdateIntentReceiver); unregisterReceiver(messageIntentReceiver); ... }
现在,您可以开始运行MQTT服务了。
Intent svc = new Intent(this, MQTTService.class); startService(svc);同样,断开连接并停止:
Intent svc = new Intent(this, MQTTService.class); stopService(svc);您可能还想清除由service创建的通知:
@Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); if (hasFocus) { NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mNotificationManager.cancel(MQTTService.MQTT_NOTIFICATION_UPDATE); } }最后,您可能希望直接从您的activity中调用service中的方法。 它们大多是私有的,但你可以在那里放一些公共的方法。
例如,我之前在“保持收到的数据安全”中提到,当activity启动时,它可能希望从service中检索它在运行时收到的所有推送数据。
我已经通过在service中添加本地绑定来提供对此功能的支持,可以使用这样的方式进行调用:
bindService(new Intent(this, MQTTService.class), new ServiceConnection() { @SuppressWarnings("unchecked") @Override public void onServiceConnected(ComponentName className, final IBinder service) { MQTTService mqttService = ((LocalBinder)service).getService(); mqttService.rebroadcastReceivedMessages(); unbindService(this); } @Override public void onServiceDisconnected(ComponentName name) {} }, 0);