安卓开发文档翻译:实现谷歌云消息客户端,Implementing GCM Client
一个谷歌云消息(Google Cloud Messaging (GCM))客户端,指的是,运行 于某个安卓设备上的一个启用了谷歌云消息功能的应用程序。 我们建议,在编写客户端代码的过程中,使用 谷歌 云消息应用编程接口 。谷歌 云消息的前一个版本中提供的客户端辅助库,仍然可以使用,但是,已经 被更高效的 谷歌 云消息应用编程接口 取代了。
一个完整的谷歌云消息系统中,需要有一个客户端和一个服务器。 欲知更多关于实现服务器端功能的信息,则阅读 实现谷歌 云消息服务器 。
后续小节中,将说明在编写谷歌云消息的客户端应用程序的过程中所需要的步骤。妳的客户端应用程序可以做得有多复杂就多复杂,但是,从最基本的角度来讲,作为一个谷歌云消息的客户端应用程序,必须包含以下代码:注册(这个过程中将获得一个注册编号);以及一个广播接收器,用来接收谷歌云消息系统发来的消息。
要编写客户端程序,则使用 谷歌云消息应用编程接口 。 要想使用这个应用编程接口,妳必须先设置好妳的项目以使用谷歌玩服务软件开发工具包(Google Play services SDK),详情 在 设置谷歌 玩服务软件开发工具包 中说明。
警告: 当妳向项目中加入玩服务库(Play Services library)时, 要确保是以 带资源 的方式加入的, 详情 在 设置谷歌 玩服务软件开发工具包 中说明。关键 点是,妳必须 引用 这个库——如果 妳只是简单地向月食(Eclipse)项目中加入一个 .jar 文件的话则不起作用。 妳必须按照说明来引用一个库,否则的话,妳的应用程序就无法访问到那个库的资源,于是就会运行不正常。
向妳的应用程序的清单文件中加入以下内容:
•. com.google.android.c2dm.permission.RECEIVE 权限 ,这样的话,这个安卓应用程序才能够完成注册,以及接收消息。
•. android.permission.INTERNET 权限 , 这样的话,这个安卓应用程序才能够将注册编号发送给第三方服务器。
•. android.permission.GET_ACCOUNTS 权限 ,因为,谷歌 云消息功能要求设备上存在一个谷歌账号 ( 仅当该设备的安卓系统版本低于安卓 4.0.4 时才是必要的 )
•. android.permission.WAKE_LOCK 权限 ,这样的话,这个应用程序就可以在接收到消息时避免设备进入休眠状态。可选—— 仅当该应用程序需要阻止设备进行休眠状态时才使用。
•. 应用程序 的软件 包 名( applicationPackage ) + ".permission.C2D_MESSAGE" 权限 , 以阻止其它安卓应用程序注册及接收该安卓应用程序的消息。此处 所说的权限名字必须严格符合这个模式——否则的话,这个安卓应用程序本身也将接收不到消息。
•. 一个针对 com.google.android.c2dm.intent.RECEIVE 的接收器, 其分类(category)设置为应用程序 的软件包名( applicationPackage ) 。 这个接收器应当 带有 com.google.android.c2dm.SEND 权限 ,这样的话,只有谷歌云消息框架才能够向它发送消息。如果 妳的应用程序使用了一个 意图服务 ( 不是必须的,但这是一个常见模式 ) ,则, 这个接收器应当是保持唤醒状态 的广播接收器 ( WakefulBroadcastReceiver )的一个实例。 WakefulBroadcastReceiver 会为妳的应用程序处理好创建及管理一个 部分唤醒 锁 的任务。
•. 一个 服务 ( 一般是一个 意图服务 ) ,前面所说的 WakefulBroadcastReceiver 会将处理谷歌 云 消息的工作交给该服务,并且确保 这个过程中该设备不会进入休眠状态。使用 意图服务 是可选的—— 妳可以选择就在一个普通的 广播接收器 里处理这些消息,但是 呢,真的, 大部分应用程序都会使用 意图服务 。
•. 如果谷歌 云消息功能对于妳的安卓应用程序是不可或缺的,那么, 要确保在清单文件中设置了 android:minSdkVersion="8" 或更高版本。 这样,能够确保, 该安卓应用程序不会被安装到一个它无法正常运行的环境中去。
以下是某个支持谷歌云消息的示例清单文件中的一些片断:
<manifest package = "com.example.gcm" ... >
<uses-sdk android:minSdkVersion = "8" android:targetSdkVersion = "17" />
<uses-permission android:name = "android.permission.INTERNET" />
<uses-permission android:name = "android.permission.GET_ACCOUNTS" />
<uses-permission android:name = "android.permission.WAKE_LOCK" />
<uses-permission android:name = "com.google.android.c2dm.permission.RECEIVE" />
<permission android:name = "com.example.gcm.permission.C2D_MESSAGE"
android:protectionLevel = "signature" />
<uses-permission android:name = "com.example.gcm.permission.C2D_MESSAGE" />
<application ... >
<receiver
android:name = ".GcmBroadcastReceiver"
android:permission = "com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name = "com.google.android.c2dm.intent.RECEIVE" />
<category android:name = "com.example.gcm" />
</intent-filter>
</receiver>
<service android:name = ".GcmIntentService" />
</application>
</manifest>
最后 ,就是写程序了。 在这一小节中,我们会逐渐写出一个示例客户端程序,以展示如何使用 谷歌 云消息 应用编程接口。 这个示例中,包含:一个主活动(activity) ( DemoActivity ) ; 一个 WakefulBroadcastReceiver ( GcmBroadcastReceiver ) ;和一个 意图服务 ( GcmIntentService ) 。 妳可以在 开源网站 找到这个示例程序的完整代码。
注意以下事情:
•. 这个示例程序展示 了注册及上行(设备 到云端 )消息。 上行消息,只 有 那些连接 到 云连接服务器 ( 可扩展消息及状态协议( XMPP ) )的应用程序才能使用; 超文本传输协议的服务器不支持上行消息。
•. 谷歌 云消息 的注册用应用编程接口已经替换掉了旧的注册过程, 而后者是基于已经废弃的客户端辅助库的。尽管 旧的注册过程仍然有效,但是,我们建议妳使用新的, 谷歌 云消息 注册应用编程接口 ,而无论妳使用的是哪种底层服务器。
在 设置谷歌 玩服务软件开发工具包 中有说明,那些依赖 玩服务软件开发工具包的应用程序,在使用谷歌玩服务功能之前,应当检查设备上是否存在一个相兼容的谷歌玩服务安卓应用程序包。 在示例程序中,有两处进行了这个检查:在主活动的 onCreate() 方法 和 onResume() 方法中。 onCreate() 中的检查,确保了,在检查失败的情况下,无法使用该应用程序。 onResume() 中的检查,确保了,当用户以其它手段返回到这个正在运行的应用程序时(例如通过返回按钮),仍然会进行检查。 如果设备上没有相兼容的谷歌玩服务安卓应用程序包的话,则, 妳可以在自己的应用程序中调用 GooglePlayServicesUtil.getErrorDialog() ,以让用户从谷歌玩商店中下载该安卓应用程序包或者 在设备的系统设置中启用它。例如:
private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000 ;
...
@Override
public void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . main );
mDisplay = ( TextView ) findViewById ( R . id . display );
context = getApplicationContext ();
// 检查设备 上是否存在玩服务安卓应用程序包。
if ( checkPlayServices ()) {
// 如果检查成功, 则继续进行正常的处理。
// 否则 ,提示用户弄到有效的玩服务安卓应用程序包。
...
}
}
// 在这里也需要检查玩服务安卓应用程序包。
@Override
protected void onResume () {
super . onResume ();
checkPlayServices ();
}
/**
* 检查设备 ,以确保,它拥有谷歌玩服务安卓应用程序包。如果没有 的话,
* 则显示一个对话框, 以让用户从谷歌玩商店中下载该安卓应用程序包或者在设备的系统设置中启用它。
*/
private boolean checkPlayServices () {
int resultCode = GooglePlayServicesUtil . isGooglePlayServicesAvailable ( this );
if ( resultCode != ConnectionResult . SUCCESS ) {
if ( GooglePlayServicesUtil . isUserRecoverableError ( resultCode )) {
GooglePlayServicesUtil . getErrorDialog ( resultCode , this ,
PLAY_SERVICES_RESOLUTION_REQUEST ). show ();
} else {
Log . i ( TAG , "This device is not supported." );
finish ();
}
return false ;
}
return true ;
}
一个安卓应用程序,在接收消息之前,需要注册到谷歌云消息服务器。 在应用程序进行注册的过程中,它会收到一个注册编号, 这个注册编号可以储存起来以供日后使用 (注意 ,注册编号应当保密 ) 。 以下代码片断取自示例程序的 onCreate() 方法,其中,会检查,该应用程序是否已经注册到谷歌云消息系统及自己的服务器:
/**
* 示例应用程序 的主界面。
*/
public class DemoActivity extends Activity {
public static final String EXTRA_MESSAGE = "message" ;
public static final String PROPERTY_REG_ID = "registration_id" ;
private static final String PROPERTY_APP_VERSION = "appVersion" ;
private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000 ;
/**
* 替换 成妳自己的发送者编号。 即为妳在应用编程接口控制台中看到的项目编号,详情见
* “入门”( "Getting Started" )。
*/
String SENDER_ID = "Your-Sender-ID" ;
/**
* 用来输出日志 的标记。
*/
static final String TAG = "GCMDemo" ;
TextView mDisplay ;
GoogleCloudMessaging gcm ;
AtomicInteger msgId = new AtomicInteger ();
SharedPreferences prefs ;
Context context ;
String regid ;
@Override
public void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . main );
mDisplay = ( TextView ) findViewById ( R . id . display );
context = getApplicationContext ();
// 检查设备 上是否有玩服务安卓应用程序包。如果检查成功,
// 则开始进行谷歌云消息注册。
if ( checkPlayServices ()) {
gcm = GoogleCloudMessaging . getInstance ( this );
regid = getRegistrationId ( context );
if ( regid . isEmpty ()) {
registerInBackground ();
}
} else {
Log . i ( TAG , "No valid Google Play Services APK found." );
}
}
...
}
该应用程序会调用 getRegistrationId() 以检查是否在共享选项中储存有一个注册编号:
/**
* 获取当前用于谷歌 云消息服务的注册编号。
* <p>
* 如果结果 为空,则该应用程序需要进行注册。
*
* @return 注册编号 ,或者,如果不存在注册编号则返回空字符串。
*/
private String getRegistrationId ( Context context ) {
final SharedPreferences prefs = getGCMPreferences ( context );
String registrationId = prefs . getString ( PROPERTY_REG_ID , "" );
if ( registrationId . isEmpty ()) {
Log . i ( TAG , "Registration not found." );
return "" ;
}
// 检查 该应用程序是否被升级了;如果是升级了,则必须清空已有的注册编号,
// 因为 ,已有的注册编号并不确保能与新版本的应用程序配套使用。
int registeredVersion = prefs . getInt ( PROPERTY_APP_VERSION , Integer . MIN_VALUE );
int currentVersion = getAppVersion ( context );
if ( registeredVersion != currentVersion ) {
Log . i ( TAG , "App version changed." );
return "" ;
}
return registrationId ;
}
...
/**
* @return 本应用程序的共享选项( {@code SharedPreferences} )。
*/
private SharedPreferences getGCMPreferences ( Context context ) {
// 这个示例程序会将注册编号存储在共享选项中,
// 但是, 妳可以 在自己的应用程序中 按照自己的方式来存储注册编号。
return getSharedPreferences ( DemoActivity . class . getSimpleName (),
Context . MODE_PRIVATE );
}
如果注册编号 不存在,或者该应用程序被升级了,则 getRegistrationId() 会返回一个空字符串,以表明该应用程序需要获取到一个新的注册编号。 getRegistrationId() 调用 以下方法来检查该应用程序的版本号:
/**
* @return 从软件包管理器({@code PackageManager})中获取到的本应用程序的版本号。
*/
private static int getAppVersion ( Context context ) {
try {
PackageInfo packageInfo = context . getPackageManager ()
. getPackageInfo ( context . getPackageName (), 0 );
return packageInfo . versionCode ;
} catch ( NameNotFoundException e ) {
// 理论 上不会发生
throw new RuntimeException ( "Could not get package name: " + e );
}
}
如果 不存在有效的注册编号,则, DemoActivity 会调用以下的 registerInBackground() 方法 来进行注册。注意 ,因为谷歌 云消息的方法 register() 和 unregister() 都是阻塞的,所以 ,需要 在一个后台线程中调用它们。 在这个示例程序中,是使用 异步任务 来进行这个操作的:
/**
* 异步 地将本应用程序注册到谷歌云消息服务器。
* <p>
* 在应用程序的共享选项中存储注册编号和应用程序版本号。
*/
private void registerInBackground () {
new AsyncTask () {
@Override
protected String doInBackground ( Void ... params ) {
String msg = "" ;
try {
if ( gcm == null ) {
gcm = GoogleCloudMessaging . getInstance ( context );
}
regid = gcm . register ( SENDER_ID );
msg = "Device registered, registration ID=" + regid ;
// 妳应当通过超文本传输协议将注册编号发送给妳自己的服务器,
// 这样,它就可以通过谷歌云消息/ 超文本传输协议或云连接服务器
// 来向妳的应用程序发送消息。如果妳的应用程序中包含认证功能的话,
// 向妳的服务器所发送的这个请求应当被认证。
sendRegistrationIdToBackend ();
// 在这个示例中:我们不需要发送注册编号,因为设备 会向一个回显服务器
// 发送上行消息, 该服务器会向消息中的‘来源’('from')地址发送一 条 回显消息。
// 将注册编号持久存储——日后就不用再注册了。
storeRegistrationId ( context , regid );
} catch ( IOException ex ) {
msg = "Error :" + ex . getMessage ();
// 如果 发生了错误,则,不要一味地立即重试注册。
// 让用户点击某个按钮来进行重试, 或,按照指数回退算法
// (exponential back-off)来重试。
}
return msg ;
}
@Override
protected void onPostExecute ( String msg ) {
mDisplay . append ( msg + "\n" );
}
}. execute ( null , null , null );
...
}
当妳收到注册编号之后,将它发送给妳的服务器:
/**
* 妳应当通过超文本传输协议将注册编号发送给妳自己的服务器,
* 这样,它就可以通过谷歌云消息/ 超文本传输协议或云连接服务器
* 来向妳的应用程序发送消息。
* 在这个示例中:我们不需要发送注册编号,因为设备 会向一个回显服务器
* 发送上行消息, 该服务器会向消息中的‘来源’('from')地址发送一 条 回显消息 。
*/
private void sendRegistrationIdToBackend () {
// 自行实现 这一部分。
}
注册完成之后 ,该应用程序调用 storeRegistrationId() 来将注册编号储存在共享选项中, 以便日后使用。 这只是储存注册编号的一种方式。 在妳的应用程序中,可随意使用其它方式:
/**
* 在应用程序的共享选项({@code SharedPreferences})中储存注册编号和应用程序版本号 。
*
* @param context 应用程序 的上下文。
* @param regId 注册编号
*/
private void storeRegistrationId ( Context context , String regId ) {
final SharedPreferences prefs = getGCMPreferences ( context );
int appVersion = getAppVersion ( context );
Log . i ( TAG , "Saving regId on app version " + appVersion );
SharedPreferences . Editor editor = prefs . edit ();
editor . putString ( PROPERTY_REG_ID , regId );
editor . putInt ( PROPERTY_APP_VERSION , appVersion );
editor . commit ();
}
当用户点击应用程序的 发送 按钮, 该应用程序就使用 谷歌 云消息 应用编程接口来发送一条上行消息。 要接收到上行消息的话,妳的服务器应当连接到云连接服务器。 妳可以使用 实现 一个基于可扩展消息及状态协议的应用程序服务器 中的某个示例服务器来连接到云连接服务器。
public void onClick ( final View view ) {
if ( view == findViewById ( R . id . send )) {
new AsyncTask () {
@Override
protected String doInBackground ( Void ... params ) {
String msg = "" ;
try {
Bundle data = new Bundle ();
data . putString ( "my_message" , "Hello World" );
data . putString ( "my_action" ,
"com.google.android.gcm.demo.app.ECHO_NOW" );
String id = Integer . toString ( msgId . incrementAndGet ());
gcm . send ( SENDER_ID + "@gcm.googleapis.com" , id , data );
msg = "Sent message" ;
} catch ( IOException ex ) {
msg = "Error :" + ex . getMessage ();
}
return msg ;
}
@Override
protected void onPostExecute ( String msg ) {
mDisplay . append ( msg + "\n" );
}
}. execute ( null , null , null );
} else if ( view == findViewById ( R . id . clear )) {
mDisplay . setText ( "" );
}
}
正如前面 在 步骤2 中所说的, 这个示例程序中包含了一个针对 com.google.android.c2dm.intent.RECEIVE 的 WakefulBroadcastReceiver 。谷歌 云消息系统是使用广播接收器的机制来传递消息的。 当妳在 onClick() 中调用 gcm.send() 的时候,它会触发对应的广播接收 器 的 onReceive() 方法 ,那个方法 中会确保这条谷歌云消息被正确处理。
WakefulBroadcastReceiver 是一种特殊的广播接收器, 它会为妳的应用程序创建及管理好一个 部分唤醒 锁 。 它会将该条谷歌云消息的处理事务交给一个 服务 ( 一般是一个 意图服务 ) 来做,并且确保 在传递过程中设备不会进入休眠状态。如果 妳在将事务交给服务的过程中不持有一个唤醒锁的话,则, 就相当于允许在处理完成这个任务之前就让设备进入休眠状态了。结果 就是, 妳的应用程序可能无法立即完成对该谷歌云消息的处理,直到日后的某个时候才有机会完成工作,这种效果可能不是妳想要的。
注意 : 使用 WakefulBroadcastReceiver 并不是必须的。如果 妳的应用程序相对简单,不需要用到服务的话,那么 , 妳可以在一个普通的 广播接收 器 中解释该谷歌云消息并且当场处理掉。 当妳在妳的广播接收器的 onReceive() 方法中接收到谷歌云消息传递过来的意图时,接下来怎么处理就是自便了。
以下代码片断,使用 startWakefulService() 方法来启动 GcmIntentService 。 这个方法与 startService() 类似,区别 就是, WakefulBroadcastReceiver 会在启动服务的时候持有一个唤醒锁。 startWakefulService() 中传递的那个意图,会包含一个额外参数,其中标识了该唤醒锁:
public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
@Override
public void onReceive ( Context context , Intent intent ) {
// 显式指定,要由 GcmIntentService处理 这个意图。
ComponentName comp = new ComponentName ( context . getPackageName (),
GcmIntentService . class . getName ());
// 启动 该服务,在启动过程中保持设备处于唤醒状态。
startWakefulService ( context , ( intent . setComponent ( comp )));
setResultCode ( Activity . RESULT_OK );
}
}
以下展示的意图服务对该条谷歌云消息进行实际处理。 当服务完成工作之后,会调用 GcmBroadcastReceiver.completeWakefulIntent() 来释放唤醒锁。 在 completeWakefulIntent() 方法 的参数中,应当将当初由 WakefulBroadcastReceiver 传递过 来 的那个意图原样传递回去。
这个代码片断会依据消息的类型来处理该条谷歌云消息,并且将结果发送到通知栏中。但是,在妳自己的应用程序中,究竟怎么处理该条谷歌云消息,这是完全取决于妳的——其中的可能性是无穷的。例如,这条消息可能就是一个信号(ping),告诉妳的应用程序应当与某个服务器进行同步以获取到新的内容,或者,可能是一条应当显示在界面上的聊天消息。
public class GcmIntentService extends IntentService {
public static final int NOTIFICATION_ID = 1 ;
private NotificationManager mNotificationManager ;
NotificationCompat . Builder builder ;
public GcmIntentService () {
super ( "GcmIntentService" );
}
@Override
protected void onHandleIntent ( Intent intent ) {
Bundle extras = intent . getExtras ();
GoogleCloudMessaging gcm = GoogleCloudMessaging . getInstance ( this );
// getMessageType() 中的那个意图参数,应当就是
// 妳在BroadcastReceiver 中收到的那个意图。
String messageType = gcm . getMessageType ( intent );
if (! extras . isEmpty ()) { // 有解包的作用
/*
* 依据消息类型 来过滤消息。因为谷歌 云消息系统可能
* 会在日后进行扩展以引入新的消息类型,所以,应当无视 掉任何妳不感兴趣
* 或者无法识别 的消息类型。
*/
if ( GoogleCloudMessaging .
MESSAGE_TYPE_SEND_ERROR . equals ( messageType )) {
sendNotification ( "Send error: " + extras . toString ());
} else if ( GoogleCloudMessaging .
MESSAGE_TYPE_DELETED . equals ( messageType )) {
sendNotification ( "Deleted messages on server: " +
extras . toString ());
// 如果 这是一条常规的谷歌云消息,则进行处理。
} else if ( GoogleCloudMessaging .
MESSAGE_TYPE_MESSAGE . equals ( messageType )) {
// 这个循环,代表着这个服务在做某些处理。
for ( int i = 0 ; i < 5 ; i ++) {
Log . i ( TAG , "Working... " + ( i + 1 )
+ "/5 @ " + SystemClock . elapsedRealtime ());
try {
Thread . sleep ( 5000 );
} catch ( InterruptedException e ) {
}
}
Log . i ( TAG , "Completed work @ " + SystemClock . elapsedRealtime ());
// 发送通知栏通知,告知收到了消息。
sendNotification ( "Received: " + extras . toString ());
Log . i ( TAG , "Received: " + extras . toString ());
}
}
// 释放 由WakefulBroadcastReceiver 所提供的唤醒锁。
GcmBroadcastReceiver . completeWakefulIntent ( intent );
}
// 将消息放置到一个通知栏通知中,并且发送出去。
// 这只是一个简单的示例,展示了对于谷歌云消息可做什么处理。
private void sendNotification ( String msg ) {
mNotificationManager = ( NotificationManager )
this . getSystemService ( Context . NOTIFICATION_SERVICE );
PendingIntent contentIntent = PendingIntent . getActivity ( this , 0 ,
new Intent ( this , DemoActivity . class ), 0 );
NotificationCompat . Builder mBuilder =
new NotificationCompat . Builder ( this )
. setSmallIcon ( R . drawable . ic_stat_gcm )
. setContentTitle ( "GCM Notification" )
. setStyle ( new NotificationCompat . BigTextStyle ()
. bigText ( msg ))
. setContentText ( msg );
mBuilder . setContentIntent ( contentIntent );
mNotificationManager . notify ( NOTIFICATION_ID , mBuilder . build ());
}
}
按照以下步骤来运行这个示例:
1. 按照 入门 中的步骤获取妳的发送者编号和应用编程接口密钥。
2. 按照 本文档中的说明来实现妳的客户端应用程序。 妳可在 开源网站 下载到客户端应用程序的完整代码。
3. 运行 实现 一个基于可扩展消息及状态协议的应用程序服务器 中提供 的 某个示例服务器( 爪哇(Java)或派森(Python) )。无论 妳选择使用哪个示例服务器,都不要忘了, 要编辑它的源代码,以写上妳自己的发送者编号和应用编程接口密钥。
按照以下方法来查看妳的谷歌云消息应用程序的统计信息及任何错误消息:
2.使用妳的开发者账号登录。
妳会看到一个包含了妳的所有应用程序的列表的页面。
3.点击妳想查看其谷歌云消息统计信息的应用程序旁边的“统计信息”("statistics")链接。
现在妳看到统计信息页面了。
4.点击下拉菜单,选择妳想查看的某个谷歌云消息指标。
HxLauncher: Launch Android applications by voice commands