现在的位置: 首页 > 移动开发> 正文
Android云推送之GCM
2013年09月28日 移动开发 暂无评论 ⁄ 被围观 7,713+

本文展示了怎样来利用谷歌的云推送系统Google Cloud Messaging (GCM)来推送消息到Android手机,目前支持两大类消息,一类是“send-to-sync”,即发送一个触发指令,格式为一个自定义的字符串,例如 "syncTime"表示同步时间,那么Android客户端收到此消息的话,需要启动一个后台线程,同设定的Server进行交互,获取最新的时间;还有一类是“message with payload”,即发送一条消息,消息中可包含正文内容,比如message.addData("new_time", curTime),则可以通过键值"new_time"获取到具体的时间。

Pushing data has a number of benefits such as increased battery life and decreased server load. GCM is free to use and has no quotas, so it is a perfect candidate to handle the heavy lifting required for data updates.

There are two types of messages you can push with GCM: Send-to-sync and message with payload. A send-to-sync is basically a tickle, telling your app to sync with the server while a message with payload lets you send data in the push message (up to 4kb). Here’s a simple, full stack example that will help highlight how to get each of these types of push messages integrated with your app.

The app will take either a push message with a payload that updates the time that is saved on the device or a send to sync message that triggers the app to sync the time with the server. This was created using the “Generate App Engine Backend” feature of Android Studio. The Android developers blog has a tutorial on this. If you already have a backend setup, the GCM setup instructions for an existing server can be found here.

The source for this example is up on GitHub.

Create a new endpoint

Wizard generated files Message.java and MessageEndpoint.java in PushDontPoll-AppEngine were deleted, along with the messageendpoint package in PushDontPoll-endpoints. In lieu of this, DateEndpoint.java was created in PushDontPoll-AppEngine. This class makes an endpoint at /_ah/api/dateendpoint/v1/currentdate that provides the current time in JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    @JsonAutoDetect
    public static class CurrentDate {
 
        @JsonProperty
        private long date = new Date().getTime();
 
        public long getDate() {
            return date;
        }
    }
 
    @ApiMethod(name = "currentDate")
    public CurrentDate getDate() {
        return new CurrentDate();
    }
    @JsonAutoDetect
    public static class CurrentDate {

        @JsonProperty
        private long date = new Date().getTime();

        public long getDate() {
            return date;
        }
    }

    @ApiMethod(name = "currentDate")
    public CurrentDate getDate() {
        return new CurrentDate();
    }

It also provides methods to send a message with payload and send to sync message via GCM. This was ripped out the deleted MessageEndpoint.java , so it only sends messages to a maximum of 10 devices. They are both built using GCM’s Message.Builder().

The message with payload adds the time to the data parameter. Key/value pairs added here will be automatically added as extras in the the intent bundle when parsing the message.

1
2
3
4
    String newTime = String.valueOf(new Date().getTime());
    Message message = new Message.Builder()
            .addData("new_time", newTime)
            .build();
    String newTime = String.valueOf(new Date().getTime());
    Message message = new Message.Builder()
            .addData("new_time", newTime)
            .build();

The send-to-sync message uses a collapse key so that only one message will be delivered if multiple tickles are queued up. You should not use more than four distinct keys since that is the maximum number that the GCM server can store. You can read more about this parameter here.

1
2
3
    Message message = new Message.Builder()
            .collapseKey("syncTime")
            .build();
    Message message = new Message.Builder()
            .collapseKey("syncTime")
            .build();

Add send-to-sync and payload message buttons

The index.html in the PushDontPoll-AppEngine project was modified to add buttons that allow us to send the two different types of push messages. Most of the wizard-generated code was removed for simplicity. The code to show the buttons is:

1
2
3
4
5
6
7
  <div>
      <button id="sendTickleButton">Send Tickle</button>
  </div>
 
  <div>
      <button id="sendUpdateButton">Send Update</button>
  </div>
  <div>
      <button id="sendTickleButton">Send Tickle</button>
  </div>

  <div>
      <button id="sendUpdateButton">Send Update</button>
  </div>

The functions that they call are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  <script type="text/javascript">
    $("#sendTickleButton").click(sendTickle);
    $("#successArea").hide();
  </script>
  <script type="text/javascript">
    $("#sendUpdateButton").click(sendUpdate);
    $("#successArea").hide();
  </script>
 
  function sendTickle() {
    gapi.client.dateendpoint
    .sendTickle()
    .execute(handleMessageResponse);
  }
 
  function sendUpdate() {
    gapi.client.dateendpoint
    .sendUpdate()
    .execute(handleMessageResponse);
  }
  <script type="text/javascript">
    $("#sendTickleButton").click(sendTickle);
    $("#successArea").hide();
  </script>
  <script type="text/javascript">
    $("#sendUpdateButton").click(sendUpdate);
    $("#successArea").hide();
  </script>

  function sendTickle() {
    gapi.client.dateendpoint
    .sendTickle()
    .execute(handleMessageResponse);
  }

  function sendUpdate() {
    gapi.client.dateendpoint
    .sendUpdate()
    .execute(handleMessageResponse);
  }

These call the sendTickle() and sendUpdate() methods in DateEndpoint.java. Make sure you re-generate the client libraries after changing the PushDontPoll-AppEngine code (Tools -> Google Cloud Endpoints -> Generate Client Libraries).

Add code to parse GCM message

The wizard-generated files RegisterActivity.java and GCMIntentService.java were moved to the main project so they could use the Constants class. The added application code to show the saved time was added to the MainActivityclass in order to keep things separated and easy to understand. The RegisterActivity is accessible via an action item.

Parsing the GCM message is done in the onMessage() method in the GCMIntentService class. This class is already fleshed out for you thanks to the wizard.

Parse send-to-sync

The send-to-sync message will have a collapse_key, so we can request a sync if that matches the sync type we’re expecting:

1
2
3
4
5
6
7
8
9
    if ("syncTime".equals(intent.getStringExtra("collapse_key"))) {
        Account[] accounts = AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE);
        if (accounts.length > 0) {
            Bundle bundle = new Bundle(2);
            bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
            bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
            ContentResolver.requestSync(accounts[0], Constants.AUTHORITY, bundle);
        }
        [snip]
    if ("syncTime".equals(intent.getStringExtra("collapse_key"))) {
        Account[] accounts = AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE);
        if (accounts.length > 0) {
            Bundle bundle = new Bundle(2);
            bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
            bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
            ContentResolver.requestSync(accounts[0], Constants.AUTHORITY, bundle);
        }
        [snip]

This uses a sync adapter with a stubbed authenticator and content provider – see here for more info. This can easily be modified to send a network request to sync using your method of choice (Volley, Retrofit, etc) if a sync adapter is not desirable. All of the syncing code is in the com.doubleencore.android.pushdontpoll.sync package, along with the necessary xml files in res/xml/ and in the AndroidManifest.xml.

In the onPerformSync() of the SyncAdapter, we can make a network request to the new endpoint that was created to get the current time:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    Dateendpoint.Builder endpointBuilder = new Dateendpoint.Builder(
            AndroidHttp.newCompatibleTransport(),
            new JacksonFactory(),
            new HttpRequestInitializer() {
                @Override
                public void initialize(HttpRequest httpRequest) throws IOException {
                }
            });
 
    Dateendpoint endpoint = CloudEndpointUtils.updateBuilder(endpointBuilder).build();
    try {
        CurrentDate date = endpoint.currentDate().execute();
        long time = date.getDate();
        [snip]
    Dateendpoint.Builder endpointBuilder = new Dateendpoint.Builder(
            AndroidHttp.newCompatibleTransport(),
            new JacksonFactory(),
            new HttpRequestInitializer() {
                @Override
                public void initialize(HttpRequest httpRequest) throws IOException {
                }
            });

    Dateendpoint endpoint = CloudEndpointUtils.updateBuilder(endpointBuilder).build();
    try {
        CurrentDate date = endpoint.currentDate().execute();
        long time = date.getDate();
        [snip]

Parse message with payload

The message with payload will have new_time as a key in the intent extras. This key coincides with the one we added when building the message. You can read more about the data parameter here.

1
2
3
4
    if (intent.hasExtra("new_time")) {
        // Extract the data from the message
        long time = Long.parseLong(intent.getStringExtra("new_time"));
        [snip]
    if (intent.hasExtra("new_time")) {
        // Extract the data from the message
        long time = Long.parseLong(intent.getStringExtra("new_time"));
        [snip]

In both cases, the time is then saved to shared preferences, and a local broadcast is sent to update the UI. Again, this can be done using your preferred method (event bus, observable cursor, etc).

1
2
3
4
5
    SharedPreferences.Editor editor = getSharedPreferences(Constants.PREFS_NAME, 0).edit();
    editor.putLong(Constants.PREFS_KEY_TIME, time).apply();
 
    Intent updateIntent = new Intent(Constants.UPDATE_UI_INTENT_ACTION);
    LocalBroadcastManager.getInstance(this).sendBroadcast(updateIntent);
    SharedPreferences.Editor editor = getSharedPreferences(Constants.PREFS_NAME, 0).edit();
    editor.putLong(Constants.PREFS_KEY_TIME, time).apply();

    Intent updateIntent = new Intent(Constants.UPDATE_UI_INTENT_ACTION);
    LocalBroadcastManager.getInstance(this).sendBroadcast(updateIntent);

Running the server

In order to use GCM, you must have an API key. You can enter this when running the wizard or by replacing it in DateEndpoint.java in the PushDontPoll-AppEngine project. The server can either be deployed as an App Engine app or run locally using the dev server.

Local instance

To run it as a local instance, you need to change:

1
2
    //PushDontPoll-endpoints Project - CloudEndpointUtils.java
    protected static final boolean LOCAL_ANDROID_RUN = true;
    //PushDontPoll-endpoints Project - CloudEndpointUtils.java
    protected static final boolean LOCAL_ANDROID_RUN = true;

Then you can just run the appengine:devserver maven goal, and it will be running on localhost. The server will now work with a Google API emulator.

If you want to access this local instance on a device (it must be on the same network as the local instance,) you’ll also need to change:

1
2
    //PushDontPoll-endpoints Project - CloudEndpointUtils.java
    protected static final String LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID = "http://[local_ip_address]:8080";
    //PushDontPoll-endpoints Project - CloudEndpointUtils.java
    protected static final String LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID = "http://[local_ip_address]:8080";

…and download the App Engine SDK from here, and run:

    appengine-java-sdk/bin/dev_appserver.sh --address 0.0.0.0 [project_directory]/PushDontPoll-Appegine/target/PushDontPoll-AppEngine-1.0

App Engine app

If you didn’t follow along in the blog and are just trying to get the sample code running, you need to create a Google Cloud Platform project and obtain the Project Number and Project ID. You can do this by following the Preliminary Setup section of this blog post.

Set the PROJECT_NUMBER variable in GCMIntentService.java to your project number and the <application> tag in appengine-web.xml to your project ID, and regenerate the client libraries.

You may also want to change the packages (com.doubleencore.android) and domains (doubleencore.com) to your domain.

Deploy the backend server by running the appengine:update maven goal. You can then access your server athttp://<project-id>.appspot.com.

Running the Android app

You can now deploy the PushDontPoll app to your device or emulator. Once deployed, click on the GCM Register action item to register your device with the server. Once the success message appears, press back and navigate to your server on your computer. Pressing the “Send Tickle” button will push a message that signals the device to sync with the server. The device will hit the date endpoint for the current time, and the time should refresh on the device. Pressing the “Send Update” button will push a message that has the new time in the payload data. This should instantly update the time shown on the device without needing to make an additional trip to the server.

Hopefully, this simple example app will assist you in getting a push framework up and running for your app. I highly recommend reading through the GCM documentation to get a thorough understanding, as there are many features not discussed in this article. You will need to handle things, such as request spikes with send-to-sync messages, more gracefully than this sample does as well. Good luck and happy pushing!

文章节选:http://www.doubleencore.com/2013/09/push-dont-poll-how-to-use-gcm-to-update-app-data/

给我留言

留言无头像?


×
腾讯微博