2013年6月21日金曜日

tomcatでwebsocket

Tomcat7.0.27からwebsocketが使えるようになりました。

早速使ってみましょう。

今回はtomcat7.0.41を使用しています。

手始めにチャットを作ってみます。

①WebSocketServlet
 tomcatと言えばHttpServletを継承してサーブレットを作成しますが、
 websocketは「WebSocketServlet」を継承してサーブレットを作ります。

 public class ChatServlet extends WebSocketServlet {

②クライアントからの接続要求
 接続が来たらcreateWebSocketInboundが呼ばれます。

 @Override
 protected StreamInbound createWebSocketInbound(String subProtocol, HttpServletRequest request) {
  return new EchoMessageInbound(byteBufSize,charBufSize);
 }

③上記で生成するEchoMessageInboundクラスを定義します
 private static final class EchoMessageInbound extends MessageInbound {

  private String name = null;

  public EchoMessageInbound(int byteBufferMaxSize, int charBufferMaxSize) {

   super();

   setByteBufferMaxSize(byteBufferMaxSize);

   setCharBufferMaxSize(charBufferMaxSize);

  }

  @Override

  protected void onOpen(WsOutbound outbound) {

   super.onOpen(outbound);

   peers.add(this);

  }

  @Override

  protected void onClose(int status) {

   super.onClose(status);

   peers.remove(this);

  }

  @Override

  protected void onBinaryMessage(ByteBuffer message) throws IOException {

   System.out.println("onBinaryMessage");

   getWsOutbound().writeBinaryMessage(message);

  }

  @Override

  protected void onTextMessage(CharBuffer message) throws IOException {

   System.out.println("onTextMessage[" + message + "]");

   if( message.toString().indexOf("joined") > 0 ) {

    name = message.toString().split(" ")[0];

   }

   for( EchoMessageInbound u: peers ) {

    u.getWsOutbound().writeTextMessage(CharBuffer.wrap(""+message.toString()));

   }

  }

 }


これだけです。

④あとはクライアント側

websocket.js
※これはJavaDayTokyoでデモで使用されたファイルです。
/*

 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.

 *

 * Copyright (c) 2013 Oracle and/or its affiliates. All rights reserved.

 *

 * The contents of this file are subject to the terms of either the GNU

 * General Public License Version 2 only ("GPL") or the Common Development

 * and Distribution License("CDDL") (collectively, the "License").  You

 * may not use this file except in compliance with the License.  You can

 * obtain a copy of the License at

 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html

 * or packager/legal/LICENSE.txt.  See the License for the specific

 * language governing permissions and limitations under the License.

 *

 * When distributing the software, include this License Header Notice in each

 * file and include the License file at packager/legal/LICENSE.txt.

 *

 * GPL Classpath Exception:

 * Oracle designates this particular file as subject to the "Classpath"

 * exception as provided by Oracle in the GPL Version 2 section of the License

 * file that accompanied this code.

 *

 * Modifications:

 * If applicable, add the following below the License Header, with the fields

 * enclosed by brackets [] replaced by your own identifying information:

 * "Portions Copyright [year] [name of copyright owner]"

 *

 * Contributor(s):

 * If you wish your version of this file to be governed by only the CDDL or

 * only the GPL Version 2, indicate your decision by adding "[Contributor]

 * elects to include this software in this distribution under the [CDDL or GPL

 * Version 2] license."  If you don't indicate a single choice of license, a

 * recipient has the option to distribute your version of this file under

 * either the CDDL, the GPL Version 2 or to extend the choice of license to

 * its licensees as provided above.  However, if you add GPL Version 2 code

 * and therefore, elected the GPL Version 2 license, then the option applies

 * only if the new code is made subject to such option by the copyright

 * holder.

 */



var wsUri = "ws://" + document.location.host + "/Chat/websocket";

var websocket = new WebSocket(wsUri);



var username;

websocket.onopen = function(evt) { onOpen(evt) };

websocket.onmessage = function(evt) { onMessage(evt) };

websocket.onerror = function(evt) { onError(evt) };

var output = document.getElementById("output");



function join() {

    username = textField.value;

    websocket.send(username + " joined");

}



function send_message() {

    websocket.send(username + ": " + textField.value);

}



function onOpen() {

    writeToScreen("Connected to " + wsUri);

}



function onMessage(evt) {

    console.log("onMessage");

    writeToScreen("RECEIVED: " + evt.data);

    if (evt.data.indexOf("joined") != -1) {

        userField.innerHTML += evt.data.substring(0, evt.data.indexOf(" joined")) + "\n";

    } else {

        chatlogField.innerHTML += evt.data + "\n";

    }

}



function onError(evt) {

    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);

}



function writeToScreen(message) {

    output.innerHTML += message + "<br>";

}


⑤最後にhtml index.html
※これはJavaDayTokyoでデモで使用されたファイルを参考にしています。
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>WebSocket Chat</title>
    </head>
    <body>
        <h1>Chat!</h1>
        <div style="text-align: center;">
            <form action="">

                <table>
                    <tr>
                        <td>
                            Users<br/>
                            <textarea readonly="true" rows="6" cols="20" id="userField"></textarea>
                        </td>
                        <td>
                            Chat Log<br/>
                            <textarea readonly="true" rows="6" cols="50" id="chatlogField"></textarea>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2">
                            <input id="textField" name="name" value="Duke" type="text"><br>
                            <input onclick="join();" value="Join" type="button">
                            <input onclick="send_message();" value="Chat" type="button">
                        </td>
                    </tr>
                </table>

            </form>
        </div>
        <div id="output"></div>
        <script language="javascript" type="text/javascript" src="websocket.js">

        </script>

    </body>
</html>


⑤完成です

これだけではwebsocketの威力は感じにくいかもしれません。
このチャットアプリを少し応用して複数の人が1つのcanvasに絵が書けるアプリを作ってみました。
とても簡単に作る事ができ、websocketの威力を味わえた気がします。


2013年6月7日金曜日

アンドロイドでアプリ内課金(v3)のテスト環境




アプリ内課金のテストを行うまでの手順を簡単にまとめます。

①googleアカウントを作成

  普通のgoogleアカウントです。


②googleウォレットの登録

  クレジットカード情報等を登録しましょう。


③販売・配布事業者登録

  Developer Console ( https://play.google.com/apps/publish )
  話を進めていき登録手数料$25支払う
  ※これをしないとアプリが公開できません


④google checkoutの登録

  google checkoutの登録(②のウォレット登録していれば入力が楽)
  ※これをしないと有料アプリやアプリ内課金ができません。


⑤Developer Consoleへ新規のアプリを登録する

  apkのアップロードを行います。
  ※特にこのアップロードしたapkを使わないとアプリ内課金のテストができないという事ではないので
    形式的なものです。未完成のapkをアップロードするのでも全然かまいません。
  ※apkを上げても「公開」されるわけではありません。


⑥Developer Console-アプリ内アイテム-新しいサービスを追加

サービスタイプは以下の3つ
1.管理対象の商品
2.管理対象外の商品
3.定期購入

1.はアカウント単位で1回購入できるアイテム(電子書籍とか)。appleでいうNon-consumable
2.は同一アカウントで何回も購入できるアイテム(回復系アイテムとか)。appleでいうConsumable
3.これはappleのAuto-renewable subscriptionsかな?

今回は何度も購入できるアイテム「2」を選択しました。
サービスID⇒アイテムに一意のIDを振ります。appleのproductID相当です。
後は、タイトルと説明と値段を書く。


⑦上記で作成したアイテムのステータスを「有効」にする



⑧テストアカウント作成

  テストアカウントとはアイテムを購入しても実際に課金される事は無いアカウントです。

  左のメニューバーから「設定」をクリック。
  [アカウントの詳細]ページで少し下にスクロールすると
  [テスト用のアクセス権があるGmailアカウント]という項目があるので
  テスト用のgoogleアカウントを設定する。

  複数のテストアカウントを登録する場合は , で区切ります
  例 test201306071014@gmail.com,test201306071015@gmail.com


⑨アイテム取得のサンプルアプリ

  実装はいろんなサイトで紹介されていますのでそちらを参照下さい。
  例えば http://y-anz-m.blogspot.jp/2013/02/android-api-version3.html

  まずはアイテム情報を取得するプログラムから作成し、それが成功したら
  購入の処理を追加すると良いと思います。

  appleの課金と違って消耗型は明示的に消耗した事をgoogle pleyに連絡しなければならない。
  consumePurchaseしないで再度購入しようとするとエラーになります

  [参考]
  http://catsdallas.blogspot.jp/2012/12/3-in-app-billing-version-3.html


【様々なエラーを体験したので記録しておきます】


(■エラー1)aidlのexception
05-28 11:42:53.068: W/Parcel(709): **** enforceInterface() expected 'com.android.vending.billing.IInAppBillingService' but read 'jp.co.arise.smp.rd002_inapppurchase.purchase.IInAppBillingService'
05-28 11:42:53.068: W/System.err(9859): java.lang.SecurityException: Binder invocation to an incorrect interface
05-28 11:42:53.076: W/System.err(9859):  at android.os.Parcel.readException(Parcel.java:1425)
05-28 11:42:53.076: W/System.err(9859):  at android.os.Parcel.readException(Parcel.java:1379)
05-28 11:42:53.076: W/System.err(9859):  at jp.co.arise.smp.rd002_inapppurchase.purchase.IInAppBillingService$Stub$Proxy.getSkuDetails(IInAppBillingService.java:251)
05-28 11:42:53.076: W/System.err(9859):  at jp.co.arise.smp.rd002_inapppurchase.MainActivity$2.run(MainActivity.java:62)
         ・
         ・
         ・
  ⇒プロジェクトに com.android.vending.billing というパッケージを作って IInAppBillingService.aidl を
    格納しました。勝手にパッケージ名を変えたら上記の様に怒られます。

(■エラー2)「このバージョンのアプリは、Google Playを通じたお支払いはご利用になれません。
    詳しくはヘルプセンターをご覧ください。」
  ⇒署名されたapkファイルを作り(⑤でアップロードしたAPKファイルを作成するときに
     使ったキーで署名しましょう)

    adb.exeを使ってインストールする。
    例:
        (a) [プロジェクト右クリック]-[android tools]-[export signed application package...]で
            apkを作成。
            例 C:\home\RD002_InAppPurchase.apk を作成
        (b) コマンドプロンプトを開く
        (c) cd c:\developAndroid\sdk\platform-tools (SDKの場所は環境により異なります)
        (d) adb install C:\home\RD002_InAppPurchase.apk
            少しすると Successs とでればOK 自動起動しないので実機のアプリ一覧から起動する。
        ※アンインストール adb uninstall jp.co.arise.smp.rd002_inapppurchase
           (マニフェストに書かれているパッケージ)

(■エラー3)「このIDでは課金できません的なエラー」
  ⇒テストアカウントではないから。
     ⑧で登録したテストアカウントがandroid端末に設定されていますか?

(■エラー4)サービスにバインドできない。(以下の部分がfalseになる)
boolean b = bindService(
new Intent("com.android.vending.billing.InAppBillingService.BIND"),
            mServiceConn, Context.BIND_AUTO_CREATE);

  ⇒サービスにバインドできないケースはTabActivityの子Activity内でbindServiceした場合に
    発生するようです。
    解決方法は bindService()を getApplicationContext().bindService() にすればうまくいきます。
boolean b = getApplicationContext().bindService(
new Intent("com.android.vending.billing.InAppBillingService.BIND"),
            mServiceConn, Context.BIND_AUTO_CREATE);


(■エラー5)startIntentSenderForResult()でonActivityResult()がコールバックされない
  ⇒tabactivityを使って startChildActivity() を呼んだ時に onActivetyResultがコールバック
    されない動きになるようです。例えばTab2SubActivityに課金処理を記述した場合

    【コールバックされない呼び方】
Intent intentTicket = new Intent(getParent(), Tab2SubActivity.class);
TabGroupActivity parentActivity = (TabGroupActivity)getParent();
parentActivity.startChildActivity("Tab2SubActivity", intentTicket);

    【コールバックされた呼び方】
Intent intentTicket = new Intent(getParent(), Tab2SubActivity.class);
TabGroupActivity parentActivity = (TabGroupActivity)getParent();
parentActivity.startActivity(intentTicket);

  こういうのは当たり前なのですかね?Activityのメカニズムとか知識が乏しくてすみません・・・


成功するとこんな感じのポップアップが表示されます。

-------------------------
| アプリ内課金チケット               ¥170
| 1枚(test)                                    
|                                                
| これはテスト用の注文です。課金は発生しません。
| お支払い方法を追加して購入を完了するには、[続行]
| をタップしてください。                        
|                                                
| ▲Google play                   [   続行   ]  
-------------------------


■1.続行を押してみる。

■2.「クレジット」か「キャリア課金」の選択を求められる。

■3.とりあえずキャリア課金を選択してみた。

■4.NTT DOCOMOの課金に関する利用規約が表示された。

■5.同意した。

■6.Googleアカウントに保存する請求先住所を入力して下さいと。。。

■7.恐る恐る入力。

すると違う画面が出てきた。
-------------------------
| アプリ内課金チケット               ¥170
| 1枚(test)
| NTT DOCOMO利用料金と一緒に支払い
|                          
| これはテスト用の注文です。課金は発生しません。
| [購入]をタップすると、Googleウォレット利用規約に
| 同意したことになります。          
|                                  
| ▲Google play                   [   購入   ]  
-------------------------

■8.SPモードのパスワードを聞いてきた。

■9.「お支払いが完了しました」という画面が・・・本当に大丈夫だよね。


以上です。


課金周りは実装もそうですが環境で大変な思いをする事があるかと思います。
少しでも助けになればと思い今回はお恥ずかしながらエラーもさらけ出しました。
役に立てば良いなと思います。ありがとうございました。