Home > GAE

GAE Archive

Android1.6でも動く「オレオレC2DM」の実装

前のエントリ「Android1.6でもAndroid Marketなくても動くC2DMを作ってみた」の実装篇です。

C2DMの構成

前回のおさらいですがホンモノのC2DM(Cloud To Device Messaging)の構成はこんな感じ。

  1. 端末をC2DMサーバ(@Google)に登録する
  2. お返しにregistration IDをもらえる(人にバレちゃだめ)
  3. 自分で作ったapp server(GAEで作れる)にuserアカウントとregistration IDを対応付けて保存する
  4. browserからapp serverにuser account情報と送信したデータをPOST
  5. app serverはuser accountからregistration IDを求めて、データと併せてC2DMサーバへPOST
  6. C2DMサーバはregistration IDと対応付けていたデバイスに向けてデータをPUSH
オレオレC2DMの構成

独自実装の「オレオレC2DM」の構成はこんな感じ。

  1. 端末をC2DMサーバ(GAE)に登録する
  2. お返しにtokenをもらえる(人にバレちゃだめ)
  3. browserからapp server(GAE)にuser account情報と送信したデータを送信
  4. app server(GAE)はuser accountと対応付けていたデバイスに向けてデータをPUSH
もうちょっと詳しく


(A) サーバはGAE(python)で作ってAndroidアプリのServiceにChannel APIでPUSHします。
(B) AndroidアプリのService側はPUSHを受け取りIntentでアプリを起動します。
(C) ブラウザはブックマークレットにして色々なブラウザに対応できるようにします。

では、(A)から順にソース付きでご紹介します。
コンセプトは誰でも作れるぐらい簡単に!

(A) サーバアプリ

サーバには2つのサービスを作ります。
1つめはクライアント(Androidアプリ)からユーザをサーバに登録するサービス(/client)。
2つめはクライアントへデータ(URI)をPUSHするサービスです(/send)。

1. ユーザを登録するサービス(/client)

端末を登録と言ってもユーザ名をサーバ側で登録して、それに対応するChannel APIのtokenを返すだけです。

class ClientHandler(webapp.RequestHandler):
    def get(self):
        id = self.request.get('id')
        if id:
            token = channel.create_channel(id)#ユーザーのチャンネルを作る
            template_values = { 'token': token }
            path = os.path.join(os.path.dirname(__file__), 'c2dm.html')
            self.response.out.write(template.render(path, template_values))
            return
        self.error(400) #Bad Request

リクエストパラメータにGoogleアカウントの名前部分だけ受け取ります。
Channel APIのtokenはc2dm.htmlというHTMLのテンプレートの中に埋め込みます。

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Ore Ore C2DM</title>
<script src="/_ah/channel/jsapi" language="javascript" type="text/javascript"></script>
</head>
<body>
<script type="text/javascript">
  var channel = new goog.appengine.Channel("{{ token }}");
  var socket = channel.open();
  socket.onmessage = function(msg) {
      android.send(msg.data);
  };
</script>
</body>
</html>

tokenはこのHTMLの中のJavaScript部分に渡しています。
これでJavaScriptがChannel APIのPUSHを受け取ってくれます。
受け取った値をandroid.send(msg.data)でAndroid側に渡しています。
この処理については(B)でご紹介します。

このPUSHを受け取るJSはbodyタグの中に書かないと動きません。
#ドキュメントに書いているのに読まずにハマったのは僕ですが何か?

このtokenは他の人にバレると、個別に操作されてしまう可能性があるそうなので、
一応HTTPSでアクセスして暗号化しておくと吉。
そのためのapp.yamlは以下の通り。

application: ore-c2dm
version: 1
runtime: python
api_version: 1
 
handlers:
- url: /client
  script: main.py
  secure: always
 
- url: /send
  script: main.py
2. クライアントへデータ(URI)をPUSHするサービス(/send)

ブラウザのブックマークレットを通して送られて来たデータ(URI)をAndroidアプリ(の中のJavaScript)へPUSHする部分です。

class SendHandler(webapp.RequestHandler):
    def get(self):
        usr = users.get_current_user()
        if not usr:
            self.redirect(users.create_login_url(self.request.uri))
            return
 
        id = usr.nickname()
        uri = self.request.get('uri')
        channel.send_message(id, uri)#ユーザへデータをpush
        self.response.headers['Content-Type'] = 'text/html; charset=utf8'
        self.response.out.write("""<html><head><title>Ore-C2DM</title><head><body onLoad='window.close();'></body></html>""")

まずユーザがGoogleアカウントでログイン中かをチェックします。
ログイン中であればそのアカウントの名前(user.nickname())を使ってChannel APIでデータをPUSHします。
このシステムが異常に簡単になっているのはこの部分のおかげです。
ここのユーザ名は上の/clientサービスで登録したユーザ名に一致している必要があります。
こうする事でPUSHできる人がアカウントの本人だけに限定できます。

これでサーバの実装が完了しました。
python部分だけで43行。短い!

Java実装

サーバをJavaで実装されている方がいますので、
JavaでChannel APIを使いたい方は参考にどうぞ。
実装構成はホボ同じです。

(B) Androidアプリ

Androidアプリは2つのコンポーネントで構築します。
1つめはサーバにユーザを登録するServiceを起動するActivity
2つめはそのService

1. Activity

まず画面構成はこんな感じ。

EditTextはユーザ名を入力する領域です。
STARTボタンはServiceを起動するボタンです。
STOPボタンはServiceを終了するボタンです。
さっそく実装をどうぞ。

	@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        ((Button)findViewById(R.id.start_btn)).setOnClickListener(this);
        ((Button)findViewById(R.id.stop_btn)).setOnClickListener(this);
        mName = (EditText)findViewById(R.id.name_et);
        String nickname = getNickname(this);
        if (nickname != null && nickname.length() > 0) {
        	mName.setText(nickname);
        }
    }
 
    public static String getNickname(Context context) {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
        String nickname = sp.getString(NICKNAME, null);
        return nickname;
    }

まずonCreateでコンポーネントの動作を決定します。
一度入力された文字はSharedPreferencesで保存します。
この値をServiceが読み取るので単なる保存だけの意味ではありません。

ボタン動作の実装は以下。

public void onClick(View v) {
    switch (v.getId()) {
    case R.id.start_btn:
      startService();
      break;
    case R.id.stop_btn:
      stopService();
      break;
  }
}
 
private void startService() {
  String nickname = mName.getText().toString();
  if (nickname != null && nickname.length() > 0) {
    saveNickname(nickname);
    mName.setEnabled(false);
    Intent service = new Intent();
    service.setClass(this, OreC2DMService.class);
    startService(service);
    showStatusLive(nickname);
  }
}
 
private void stopService() {
  Intent service = new Intent();
  service.setClass(this, OreC2DMService.class);
  stopService(service);
  showStatusDead();
  mName.setEnabled(true);
}

単純にServiceをstartしたりstopしたりしているだけの簡単入門レベルですね。

では次にService側です。

2. Service

コチラも簡単。GAEの/clientへアクセスしChannel APIのPUSHを受け取れるJSを含んだHTMLをWebViewで保持するだけです。

@Override
public void onCreate() {
  super.onCreate();
  LogI("Service.onCreate");
  mWebView = new WebView(this);
  mWebView.getSettings().setJavaScriptEnabled(true);// turn on js
  mWebView.addJavascriptInterface(new JS(), "android");
  String nickname = OreC2DMActivity.getNickname(this);
  if (nickname != null && nickname.length() > 0) {
    mWebView.loadUrl(URI + "?id=" + nickname);
  } else {
    showNotification("Ore-C2DM::Invalid Nickname");
  }
}

mWebView.loadUrl(…)がまさにその部分ですね。
PUSHを受け取ったJSからAndroidへ値を渡す部分が

mWebView.addJavascriptInterface(new JS(), "android");

になります。JavaScriptとAndroidをブリッジする処理です。
このメソッドの第一引数がAndroidの処理で、JavaScript内ではandroidというオブジェクトに見えています。
GAEの/clientサービスで受け取るHTML内にandroidというオブジェクトを使ったJavaScriptが含まれていたことを確認して下さい。
Android側の処理となるJSクラスは以下の通りです。

public class JS {
  public void send(String str) {
    LogD(str);
    String s = URLDecoder.decode(str);
    Intent it = new Intent(Intent.ACTION_VIEW, Uri.parse(s));
    it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(it);
  }
}

JavaScript内でanddroid.send(msg.data);を実行すると、JS.send(…)メソッドが実行されます。
ここではIntentでアプリを起動しているだけです。
ブラウザから渡されるURIに応じてAndroid内のアプリが起動します。

アプリ

アプリはコチラ。
OreC2DM0.2.apk
#個人で遊ぶ用です。

ソースコード的にはAndroid1.6以上で動きます。
動作確認はAndroid2.2だけです(汗

ソース

全てのソースを公開します。どうぞお楽しみ下さい。
OreC2DM.zip

(C) ブックマークレット

簡単に説明するとブックマークでJavaScriptを動かすのがブックマークレットです。
現在見ているサイトURIをGAEサーバへ送信(GETリクエスト)するだけです。
GETなので送信できるURIの長さにブラウザ依存が発生しますが、大体のサイトは大丈夫と思います。
ブックマークレットは以下をブックマークバーにドラッグして下さい。
俺C2DM
ソースは本エントリのHTMLのソースをご覧下さいw

おわりに

どうでしょうか?とても簡単にC2DMが実装できたと思います。
みんなでPUSHを使った面白いアプリ考えませんか?^^

おまけ1

ユーザをGAEの/clientに登録する部分でユーザ名がネットワークにそのまま流れてしまいます。
それが気になるようでしたらPOSTでデータを投げるのが良いでしょう。もちろんSSLで。

おまけ2

2つ以上の端末で同じユーザ名をGAEに登録するとどうなるんだろう…未テストですw

おまけ3

GAEのChannel APIはインタフェースはそのままで、
後々HTML5のWeb Socketに置き換わるそうなので、今から着手しておいても損しないですよ^^

おまけ4

Channel APIはJavaScriptと連携したロングポーリング(Comet式)かと思っていたのですが、
Google Talkの仕組みを使っているのだとか(参考)。つまりXMPPってこと?
@kazunori_279 さん情報ありがとうアザーッス!

ホーム > GAE

Author
Search
Feeds
Meta

Return to page top