ちょっと話題の記事

[Ruby][AWS][GCM] Amazon SNS Mobile Push 用のサーバーを Heroku に構築する

2013.12.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今回の記事は先日公開した記事の続編です。AWS SDK for Ruby を使って Amazon SNS に Publish して、Android 端末で Push 通知を受け取りたいと思います!

GCM (Google Cloud Messaging) に登録する

こちらでも簡単に説明していますが、同様に手順を確認しながら進めましょう。まずは Google Developers Console にアクセスし「Create Project」をクリックします。

gcm01

適当な名前で作ります。

gcm02

右のメニューから APIs & auth を選択し、「Google Cloud Messaging for Android」を探し出し、ON にします。

gcm03

次に Credentials を選択し「CREATE NEW KEY」をクリックします。

gcm04

どこで使うキーか聞かれるので「Server key」を選択します。

gcm05

次にアクセスを許可する IP の設定が表示されますが、今回は特に使わないのでそのままで。

gcm06

できあがったら API key をメモしておきます。あと URL にある ProjectID もあとで使うのでメモしておきます。これで GCM の準備完了!

gcm07

Amazon SNS の App を作成する

次に Amazon SNS の出番です。基本的にこちらと同じように App を作成するだけです。まずは「Add a New App」をクリックします。

sns-app01

次に ApplicationName を適当な名前にし、Push Platform を GCM に変更、そして API Key にメモっておいた API Key を入力して「Add New App」をクリックします。

sns-app02

これでおしまいです。PlatformApplicationArn というものが生成されると思うので、メモしておいてください。また次項で Publish 用のサーバーを作りますが、AWS へのアクセスが必要になるのでついでに IAM User を作っておくと良いでしょう。

Publish 用のサーバーを Heroku に構築する

サーバーサイドの実装は以下の記事をベースにしているので、こちらをご覧いただいてから読んでいただければと思います。

[Ruby] Sinatra + PostgreSQL + Unicorn な Web サーバーを Heroku に構築する

まずは gem に aws-sdk を追加します。

vim Gemfile

Gemfile

...
gem 'aws-sdk'
...
bundle install

次に AWS の設定ファイルを突っ込む aws.yml を作成します。

vim aws.yml

aws.yml

development:
  access_key_id: YOUR_ACCESS_KEY_ID
  secret_access_key: YOUR_SECRET_ACCESS_KEY
  region: YOUR_REGION

test:
  access_key_id: YOUR_ACCESS_KEY_ID
  secret_access_key: YOUR_SECRET_ACCESS_KEY
  region: YOUR_REGION

production:
  access_key_id: YOUR_ACCESS_KEY_ID
  secret_access_key: YOUR_SECRET_ACCESS_KEY
  region: YOUR_REGION

次に main.rb を編集します。AWS の設定を読み込むところと Publish 用の API を新たに用意しておきましょう。リクエストパラメータは message とし、好きなメッセージを送れるようにします。platform_application_arn は先ほど SNS の App 作成時にメモした PlatformApplicationArn を入れてください。

vim main.rb

main.rb

require 'sinatra' require 'sinatra/base' require 'active_record' require 'aws-sdk'

ActiveRecord::Base.configurations = YAML.load_file('database.yml') ActiveRecord::Base.establish_connection(ENV['RACK_ENV'])

# AWS の設定ファイルを読み込む AWS.config(YAML.load_file('aws.yml')[ENV['RACK_ENV']])

class User < ActiveRecord::Base end class MainApp < Sinatra::Base get '/' do User.all.to_json end post '/' do user = User.new user.registration_id = params[:registration_id] user.save! status 202 end # Publish API post '/publish' do sns = AWS::SNS.new client = sns.client # とりあえずUserの先頭にPublish response = client.create_platform_endpoint( platform_application_arn: 'YOUR_APP_ARN', token: User.first.registration_id ) endpoint_arn = response[:endpoint_arn] client.publish( target_arn: endpoint_arn, message: params[:message] ) end end [/ruby]

これでOKです。あとは Heroku にデプロイしてサーバーの準備完了です。

git add .
git commit -m 'added amazon sns mobile push'
git push heroku master

Subscribe する Android アプリを作成する

次に Subscribe する Android アプリを作成します。こちらと同じように実装します。Google Play Services のライブラリは最新をインストールしておいてください。まず適当なプロジェクトを作成し AndroidManifest.xml を編集します。

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.samplesubscriber"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />
    
    <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.samplesubscriber.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />

    <uses-permission android:name="com.example.samplesubscriber.permission.C2D_MESSAGE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.samplesubscriber.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <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.samplesubscriber" />
            </intent-filter>
        </receiver>
        <service android:name=".GcmIntentService" />
    </application>

</manifest>

次に IntentServiceBroadcastReceiver を実装していきます。Subscribe したら GCMIntentServiceonHandleIntent() が呼ばれるので、そこで Notification 通知を表示するように実装します。GCMBroadcastReceiverWakefulBroadcastReceiver というクラスを継承していますが、このクラスは WakeLock 中に Service を起動することができる startWakefulService() という便利なメソッドがあるのでこれを使います。

GCMIntentService.java

package com.example.samplesubscriber;

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import com.google.android.gms.gcm.GoogleCloudMessaging;

public class GcmIntentService extends IntentService {

    private static final String TAG = "GcmIntentService";

    public GcmIntentService() {
        super("GcmIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Bundle extras = intent.getExtras();
        GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);
        String messageType = gcm.getMessageType(intent);

        if (!extras.isEmpty()) {
            if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR.equals(messageType)) {
                Log.d(TAG, "messageType: " + messageType + ",body:" + extras.toString());
            } else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED.equals(messageType)) {
                Log.d(TAG, "messageType: " + messageType + ",body:" + extras.toString());
            } else if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)) {
                Log.d(TAG, "messageType: " + messageType + ",body:" + extras.toString());
                // Notificationで通知
                sendNotification(extras.getString("default"));
            }
        }
        GcmBroadcastReceiver.completeWakefulIntent(intent);
    }

    private void sendNotification(String message) {
        NotificationManager manager = 
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

        PendingIntent contentIntent = 
                PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0);

        NotificationCompat.Builder mBuilder = 
                new NotificationCompat.Builder(this)
        .setSmallIcon(R.drawable.ic_launcher)
        .setContentTitle("GCM Notification")
        .setStyle(new NotificationCompat.BigTextStyle().bigText(message))
        .setContentText(message);

        mBuilder.setContentIntent(contentIntent);
        manager.notify(0, mBuilder.build());
    }
}

GCMBroadcastReceiver.java

package com.example.samplesubscriber;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.support.v4.content.WakefulBroadcastReceiver;

public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        ComponentName comp = 
          new ComponentName(context.getPackageName(), GcmIntentService.class.getName());
        startWakefulService(context, (intent.setComponent(comp)));
        setResultCode(Activity.RESULT_OK);
    }
}

ここまでできたら、あとは MainActivity で GCM に登録する処理を実装して終わりです。登録には GoogleCloudMessaging#register() を使います。引数には先ほどメモった GCM の ProjectID (SenderID) を渡し、登録が成功すると registrationId が渡されます。この registrationId を Heroku に構築するサーバーに対してリクエストして、データベースに登録します。

MainActivity.java

package com.example.samplesubscriber;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;

import com.google.android.gms.gcm.GoogleCloudMessaging;

public class MainActivity extends Activity {
    
    /** Logcat出力用タグ. */
    private static final String TAG = MainActivity.class.getSimpleName();

    /** Google Cloud Messagingオブジェクト. */
    private GoogleCloudMessaging mGcm;
    /** コンテキスト. */
    private Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();

        mGcm = GoogleCloudMessaging.getInstance(this);
        registerInBackground();
    }

    private void registerInBackground() {
        new AsyncTask<Void, Void, String>() {
            @Override
            protected String doInBackground(Void... params) {
                String registrationId = "";
                try {
                    // GCM に登録して registrationID を受け取る
                    if (mGcm == null) {
                        mGcm = GoogleCloudMessaging.getInstance(mContext);
                    }
                    // 自分の ProjectID (SenderID) を入れる
                    registrationId = mGcm.register("YOUR_SENDER_ID");
                    Log.d(TAG, "Device registered, registration ID=" + registrationId);
                    // registrationID をサーバーに登録する
                    registerId(registrationId);
                } catch (IOException ex) {
                    Log.e(TAG, "Error :" + ex.getMessage());
                }
                return registrationId;
            }

            @Override
            protected void onPostExecute(String msg) {
            }
        }.execute(null, null, null);
    }

    private void registerId(String registrationId) {

        Log.e(TAG, "POST処理開始");

        // URL
        URI url = null;
        try {
            // 自分の Heroku サーバーの URL を入れる
            url = new URI("http://YOUR_APP_NAME.herokuapp.com/");
        } catch (URISyntaxException e) {
            Log.e(TAG, e.toString());
        }

        // POSTパラメータ付きでPOSTリクエストを構築
        HttpPost request = new HttpPost(url);
        List<NameValuePair> post_params = new ArrayList<NameValuePair>();
        post_params.add(new BasicNameValuePair("registration_id", registrationId));
        try {
            // 送信パラメータのエンコードを指定
            request.setEntity(new UrlEncodedFormEntity(post_params, "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        // POSTリクエストを実行
        DefaultHttpClient httpClient = new DefaultHttpClient();
        try {
            String ret = httpClient.execute(request, new ResponseHandler<String>() {
                @Override
                public String handleResponse(HttpResponse response) throws IOException {
                    Log.d(TAG, "レスポンスコード:" + response.getStatusLine().getStatusCode());
                    // 正常に受信できた場合は200系
                    switch (response.getStatusLine().getStatusCode()) {
                    case HttpStatus.SC_OK:
                    case HttpStatus.SC_CREATED:
                    case HttpStatus.SC_ACCEPTED:
                        Log.d(TAG, "レスポンス取得に成功");
                        // レスポンスデータをエンコード済みの文字列として取得する
                        return EntityUtils.toString(response.getEntity(), "UTF-8");
                    case HttpStatus.SC_NOT_FOUND:
                        Log.e(TAG, "データが存在しない");
                        return null;
                    default:
                        Log.e(TAG, "通信エラー");
                        return null;
                    }
                }
            });
            Log.d(TAG, "Body:" + ret);
        } catch (IOException e) {
            Log.e(TAG, "通信に失敗:" + e.toString());
        } finally {
            // shutdownすると通信できなくなる
            httpClient.getConnectionManager().shutdown();
        }
    }
}

これでクライアントアプリの完成です!

Push 通知を受け取ってみる!

それでは Push 通知を送ってみましょう。Heroku アプリの /publish にリクエストパラメータの message を適当につけて POST すると、クライアントアプリをインストールしている端末に Push 通知が届くはずです!

sns-test

まとめ

ということで Push 通知を受け取るところまで実装してみました。新年の挨拶は年賀状からメールへと変わっていきましたが、その次は自分でサービス作って Push 通知を送る時代が…来る…(来ないw)
ということで2013年もお世話になりました。良いお年を!

参考