2011年7月6日水曜日

Android In-app Billing Overview

In-app Billing Architecture


  • 請求リクエストと請求レスポンスのやりとりを非同期メッセージループを使って、アプリと Android Market サーバ間で行う
  • 実際にはアプリは Android Market サーバと直接やりとりすることはない
  • アプリは請求リクエストを Android Market application に interprocess communication (IPC) を介して送り、Android Market application から購入レスポンスを非同期の broadcast intent として受け取る
  • コンテンツを配送したり、トランザクションを検証するためのプライベートなリモートサーバを使うアプリ内課金の実装もあるが、in-app billing の実装にリモートサーバは必須ではない
  • メディアファイルや写真等をユーザーのデーバイスに配送する必要があるデジタルコンテンツを売る場合、リモートサーバーは役に立つ
  • ユーザーのトランザクション履歴を保存したり、署名検証などのアプリ内課金用セキュリティタスクを実行するためにリモートサーバを使うこともある
  • セキュリティに関連した全てのタスクをアプリ内で処理することも可能だが、セキュリティアタックへの脆弱性を減らすためにこれらのタスクをリモートサーバで行うことが推奨される


典型的な in-app billing の実装は3つのコンポーネントに依存している

  • Service
    (サンプルアプリでは BillingService という名前)
    アプリからの購入メッセージを処理し、Android Market の in-app billing service に請求リクエストを送る

  • BroadcastReceiver
    (サンプルアプリでは BillingReceiver という名前)
    Android Market application からの全ての非同期請求レスポンスを受け取る

  • セキュリティコンポーネント
    (サンプルアプリでは Security という名前)
    署名検証やワンタイムトークンの生成などセキュリティに関連したタスクを実行する
    in-app billing のセキュリティに関するより詳しい情報は Security controls を参照する


必要に応じて in-app billing をサポートする以下のコンポーネントを組み込む

  • response Handler
    (サンプルアプリでは ResponseHandler という名前)
    購入の通知、エラー、その他ステータスメッセージのアプリケーション固有の処理を提供する

  • observer
    (サンプルアプリでは PurchaseObserver という名前)
    アプリへのコールバック送信に責任をもち、これによりアプリは購入情報やステータスなどで、ユーザーのインタフェースをアップデートする


これらのコンポーネントに加えて
・ユーザーの購入情報を保存する方法
・ユーザーが購入アイテムを選択できるユーザーインタフェース
を提供しなければならない

アプリは checkout 用のユーザーインタフェースを用意する必要はない(Android Market application が提供する)
ユーザーの checkout プロセスが完了したら、アプリが再開される



from http://developer.android.com/guide/market/billing/billing_overview.html

---

In-app Billing Messages

  • ユーザーが購入を開始したら、アプリは Android Market の in-app billing service (MarketBillingService という名前) に単一の IPC メソッド呼び出しと使って請求メッセージを送る。
  • Android Market application は全ての請求リクエストに対して同期にレスポンスを返し、ステータス通知と他の情報をアプリに提供する
  • Android Market アプリはいくつかの請求リクエストに対して非同期でもレスポンスを返し、エラーメッセージと詳細なトランザクション情報をアプリに提供する

In-app billing requests

  • アプリは MarketBillingService インタフェースで公開されている single IPC method (sendBillingRequest()) を呼び出すことで請求リクエストを送る
  • このインタフェースは Android Interface Definition Language ファイル (IMarketBillingService.aidl) で定義されている
  • この AIDL ファイルは in-app billing のサンプルアプリと一緒にダウンロードできる

  • sendBillingRequest() メソッドは引数として Bundle パラメータを一つ持つ
  • アプリが渡すこの Bundle には、リクエストのパラメータを指定するための、いくつかの key-value ペアが含まれていなければならない
  • リクエストで送る Bundle keys の詳細は In-app Billing Service Interface を参照する

  • 一番重要な key は各リクエスト Bundle で必須な BILLING_REQUEST key
  • この key で作成する請求リクエストのタイプを指定する

Android Market の in-app billing service は次の 5 タイプの請求リクエストをサポートしている:

  • CHECK_BILLING_SUPPORTED
    Android Market application が in-app billing をサポートしているかどうか確かめるリクエスト
    アプリケーションの開始時にこのリクエストを送って in-app billing が利用できるときのみ UI を有効にするべき

    その他の必須な key-value pairs
    ・API_VERSION : 1
    ・PACKAGE_NAME : package name

    同期レスポンス Bundle のキー
    ・RESPONSE_CODE

    RESPONSE_CODE の可能な値
    ・RESULT_OK
    ・RESULT_BILLING_UNAVAILABLE
    ・RESULT_ERROR
    ・RESULT_DEVELOPER_ERROR

  • REQUEST_PURCHASE
    Android Market application に購入メッセージを送るリクエスト
    in-app billing の基盤
    ユーザーがアプリのアイテムを購入したいことを示したときにこのリクエストを送る
    Android Market は checkout のユーザーインタフェースを表示して購入のトランザクションを処理する

    その他の必須な key-value pairs
    ・API_VERSION : 1
    ・PACKAGE_NAME : package name
    ・ITEM_ID : 有効な製品(アイテム)の識別子

    同期レスポンス Bundle のキー
    ・RESPONSE_CODE
    ・PURCHASE_INTENT
    ・REQUEST_ID

    RESPONSE_CODE の可能な値
    ・RESULT_OK
    ・RESULT_ERROR
    ・RESULT_DEVELOPER_ERROR

    非同期レスポンス
    ・IN_APP_NOTIFY(broadcast intent)
       ・a notification ID(データ)

  • GET_PURCHASE_INFORMATION
    購入状態の変化の詳細を取得するためのリクエスト
    リクエストした購入の支払いが成功した場合やユーザーが checkout 中にトランザクションをキャンセルした場合に購入の状態が変わる
    事前の購入がリファンドされたときにも購入状態が変わる
    購入状態が変更になったとき Android Market はアプリに通知するので、取得したいトランザクション情報がある場合だけこのリクエストを送ればいい

    その他の必須な key-value pairs
    ・API_VERSION : 1
    ・PACKAGE_NAME : package name
    ・NONCE : 有効な long
    ・NOTIFY_IDS : 有効な long 値の配列

    同期レスポンス Bundle のキー
    ・RESPONSE_CODE
    ・REQUEST_ID

    RESPONSE_CODE の可能な値
    ・RESULT_OK
    ・RESULT_ERROR
    ・RESULT_DEVELOPER_ERROR

    非同期レスポンス
    ・PURCHASE_STATE_CHANGED(broadcast intent)

  • CONFIRM_NOTIFICATIONS
    購入状態変更の詳細をアプリが取得したこと知らせるためのリクエスト
    Android Market はアプリが購入状態変更の通知を受け取ったことを確認するまで通知を送る

    その他の必須な key-value pairs
    ・API_VERSION : 1
    ・PACKAGE_NAME : package name
    ・NOTIFY_IDS : 有効な long 値の配列

    同期レスポンス Bundle のキー
    ・RESPONSE_CODE
    ・REQUEST_ID

    RESPONSE_CODE の可能な値
    ・RESULT_OK
    ・RESULT_ERROR
    ・RESULT_DEVELOPER_ERROR

  • RESTORE_TRANSACTIONS
    購入を管理する(managed purchases)ためのユーザーのトランザクションを取得するリクエスト
    ユーザーのトランザクション状態を取得する必要があるときだけこのリクエストを送る
    通常はアプリケーションがデバイスに最初にインストールされたときか、再インストールされたとき

    その他の必須な key-value pairs
    ・API_VERSION : 1
    ・PACKAGE_NAME : package name
    ・NONCE : 有効な long

    同期レスポンス Bundle のキー
    ・RESPONSE_CODE
    ・REQUEST_ID

    RESPONSE_CODE の可能な値
    ・RESULT_OK
    ・RESULT_ERROR
    ・RESULT_DEVELOPER_ERROR

    非同期レスポンス
    ・RESPONSE_CODE(broadcast intent)
    ・PURCHASE_STATE_CHANGED(broadcast intent)



---

In-app Billing Responses

Android Market application からの in-app billing リクエストに対するレスポンスには、同期・非同期両方のレスポンスがある
同期的なレスポンスは Bundle で、次の 3 つのキーの値をもつ

  • RESPONSE_CODE
    リクエストのステータス情報とエラー情報を提供するキー
  • PURCHASE_INTENT
    checkout activit を起動するための PendingIntent を提供するキー
  • REQUEST_ID
    リクエストと非同期のレスポンスを一致させるためのリクエスト識別子を提供するキー


いくつかのキーは全てのリクエストで使えるわけではない
より詳しい情報は、Messaging sequence を参照する

非同期レスポンスはそれそれが broadcast intent の形式で送られる

  • com.android.vending.billing.RESPONSE_CODE
    Android Market server の response code を含む
    in-app billing リクエストを作成した後に送られる
    レスポンスコードによって、Android Market に billing request が正しく送れたか、もしくはエラーがリクエスト中に起こったかを判別できる
    このレスポンスは購入状態の変化(リファンドや購入の情報)をレポートするものではない
    レスポンスコードの詳細は Server Response Codes for In-app Billing を参照する

    Extras
    request_id : long : request ID を表す
    response_code : int : Android Market server のレスポンスコードを表す

    response_code extras に含まれるレスポンスコード

    • RESULT_OK
      値 : 0
      リクエストが正しくサーバーに送れたことを意味する
      このコードが CHECK_BILLING_SUPPORTED のレスポンスとして返された場合は、billing がサポートされていることを意味する

    • RESULT_USER_CANCELED
      値 : 1
      checkout ページでユーザーがアイテムを購入しようとせずに戻るボタンを押したことを意味する

    • RESULT_SERVICE_UNAVAILABLE
      値 : 2
      ネットワークコネクションがダウンしていることを意味する

    • RESULT_BILLING_UNAVAILABLE
      値 : 3
      指定した API_VERSION が Android Market application に認識されなかった、もしくはユーザーが in-app billing に不適格(例えば、ユーザーが in-app purchase が禁止されている国にいるなど)なことにより in-app billing が使えないことを意味する

    • RESULT_ITEM_UNAVAILABLE
      値 : 4
      Android Market がリクエストされたアイテムをアプリケーションの product list に見つけられなかったことを意味する。REQUEST_PURCHASE で product ID をミススペルしている場合、もしくはアイテムがアプリケーションの product list にパブリッシュされていない場合におこる

    • RESULT_DEVELOPER_ERROR
      値 : 5
      アプリケーションが in-app billing リクエストをしようとしたが、アプリケーションが com.android.vending.BILLING パーミッションを manifest で宣言していないことを意味する。アプリケーションが適切に署名されていない、もしくは変なリクエスト(例えば、request type を識別するための Bundle keys がないリクエストなど)を送ったことも意味しうる

    • RESULT_ERROR
      値 : 6
      予測できないサーバーエラーを意味する。自分で自分のアイテムを購入しようとした場合、このエラーが起こる


  • com.android.vending.billing.IN_APP_NOTIFY
    購入状態(購入に成功した、キャンセルされた、リファンドされた)が変わったことを意味するレスポンス
    このレスポンスには 1つ以上の notification ID が含まれる
    ID は特定のサーバサイドメッセージとひもづいており、各メッセージは1つ上のトランザクションに関する情報を含む
    IN_APP_NOTIFY broadcast intent を受け取った後、アプリケーションは受け取った ID でGET_PURCHASE_INFOMATION リクエストを送ってメッセージの詳細を受け取るべき

    Extras
    notification_id : String : notification ID を表す

  • com.android.vending.billing.PURCHASE_STATE_CHANGED
    1つ以上のトランザクションに関する詳細状態を含むレスポンス
    トランザクション情報は JSON string に含まれる
    JSON string は署名され、JSON string と一緒に signature が送られる (暗号化はされていない)
    あなたの in-app billing メッセージのセキュリティを確保するために、この JSON string の signature を使ってアプリケーションを検証する

    Extras
    inapp_signed_data : String : signed された JSON string
    inapp_signature : String : signature を表す String

    JSON string の例

    { "nonce" : 1836535032137741465,
    "orders" :
    { "notificationId" : "android.test.purchased",
    "orderId" : "transactionId.android.test.purchased",
    "packageName" : "com.example.dungeons",
    "productId" : "android.test.purchased",
    "developerPayload" : "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ",
    "purchaseTime" : 1290114783411,
    "purchaseState" : 0 }
    }

    JSON string のフィールドの情報は In-app Billing Broadcast Intents を参照する


    • nonce
      1回だけ使われる数字。自分のアプリケーションで nonce を生成して GET_PURCHASE_INFOMATION と一緒に送る。Android Market は JSON string の一部として nonce を送り返す。これによりメッセージを識別できる。

    • notificationId
      IN_APP_NOTIFY broadcast intent と一緒に送られるユニークな識別子。各 notificationId は Android Market server に受信されるのを待っている特定のメッセージと対応する。notificationIdGET_PURCHASE_INFORMATION メッセージと一緒に送り返すことで、アプリケーションがメッセージを受信したかどうかを Android Market が判断できる。

    • orderId
      トランザクション用のユニークなオーダー識別子。Google Checkout Order ID とひもづいている。

    • packageName
      購入が発生したアプリケーションのパッケージ名

    • productId
      アイテムの product 識別子。各アイテムは Android Market publisher site でアプリケーションの product list として指定された product ID をもたなければならない。

    • purchaseTime
      product が購入された時間。epoch (Jan 1, 1970) からの経過ミリ秒数

    • purchaseState
      オーダーの購入状態。可能は値は 0 (purchased), 1 (canceled), 2 (refunded) のいずれか。

    • developerPayload
      開発者が指定した文字列。オーダーの付加情報を含む。REQUEST_PURCHASE リクエストを発行するときにこのフィールドを指定することができる。

0 件のコメント:

コメントを投稿

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,''):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

Blogger Syntax Highliter

Version: {V}

http://www.dreamprojections.com/syntaxhighlighter

©2004-2007 Alex Gorbatchev.

'},ClipboardSwf:null,Version:'1.5.1'}};dp.SyntaxHighlighter=dp.sh;dp.sh.Toolbar.Commands={ExpandSource:{label:'+ expand source',check:function(highlighter){return highlighter.collapse;},func:function(sender,highlighter) {sender.parentNode.removeChild(sender);highlighter.div.className=highlighter.div.className.replace('collapsed','');}},ViewSource:{label:'view plain',func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/'+code+'');wnd.document.close();}},CopyToClipboard:{label:'copy to clipboard',check:function(){return window.clipboardData!=null||dp.sh.ClipboardSwf!=null;},func:function(sender,highlighter) {var code=dp.sh.Utils.FixForBlogger(highlighter.originalCode).replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');if(window.clipboardData) {window.clipboardData.setData('text',code);} else if(dp.sh.ClipboardSwf!=null) {var flashcopier=highlighter.flashCopier;if(flashcopier==null) {flashcopier=document.createElement('div');highlighter.flashCopier=flashcopier;highlighter.div.appendChild(flashcopier);} flashcopier.innerHTML='';} alert('The code is in your clipboard now');}},PrintSource:{label:'print',func:function(sender,highlighter) {var iframe=document.createElement('IFRAME');var doc=null;iframe.style.cssText='position:absolute;width:0px;height:0px;left:-500px;top:-500px;';document.body.appendChild(iframe);doc=iframe.contentWindow.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write('

'+highlighter.div.innerHTML+'

');doc.close();iframe.contentWindow.focus();iframe.contentWindow.print();alert('Printing...');document.body.removeChild(iframe);}},About:{label:'?',func:function(highlighter) {var wnd=window.open('','_blank','dialog,width=300,height=150,scrollbars=0');var doc=wnd.document;dp.sh.Utils.CopyStyles(doc,window.document);doc.write(dp.sh.Strings.AboutDialog.replace('{V}',dp.sh.Version));doc.close();wnd.focus();}}};dp.sh.Toolbar.Create=function(highlighter) {var div=document.createElement('DIV');div.className='tools';for(var name in dp.sh.Toolbar.Commands) {var cmd=dp.sh.Toolbar.Commands[name];if(cmd.check!=null&&!cmd.check(highlighter)) continue;div.innerHTML+=''+cmd.label+'';} return div;} dp.sh.Toolbar.Command=function(name,sender) {var n=sender;while(n!=null&&n.className.indexOf('dp-highlighter')==-1) n=n.parentNode;if(n!=null) dp.sh.Toolbar.Commands[name].func(sender,n.highlighter);} dp.sh.Utils.CopyStyles=function(destDoc,sourceDoc) {var links=sourceDoc.getElementsByTagName('link');for(var i=0;i');} dp.sh.Utils.FixForBlogger=function(str) {return(dp.sh.isBloggerMode==true)?str.replace(/
|<br\s*\/?>/gi,'\n'):str;} dp.sh.RegexLib={MultiLineCComments:new RegExp('/\\*[\\s\\S]*?\\*/','gm'),SingleLineCComments:new RegExp('//.*$','gm'),SingleLinePerlComments:new RegExp('#.*$','gm'),DoubleQuotedString:new RegExp('"(?:\\.|(\\\\\\")|[^\\""\\n])*"','g'),SingleQuotedString:new RegExp("'(?:\\.|(\\\\\\')|[^\\''\\n])*'",'g')};dp.sh.Match=function(value,index,css) {this.value=value;this.index=index;this.length=value.length;this.css=css;} dp.sh.Highlighter=function() {this.noGutter=false;this.addControls=true;this.collapse=false;this.tabsToSpaces=true;this.wrapColumn=80;this.showColumns=true;} dp.sh.Highlighter.SortCallback=function(m1,m2) {if(m1.indexm2.index) return 1;else {if(m1.lengthm2.length) return 1;} return 0;} dp.sh.Highlighter.prototype.CreateElement=function(name) {var result=document.createElement(name);result.highlighter=this;return result;} dp.sh.Highlighter.prototype.GetMatches=function(regex,css) {var index=0;var match=null;while((match=regex.exec(this.code))!=null) this.matches[this.matches.length]=new dp.sh.Match(match[0],match.index,css);} dp.sh.Highlighter.prototype.AddBit=function(str,css) {if(str==null||str.length==0) return;var span=this.CreateElement('SPAN');str=str.replace(/ /g,' ');str=str.replace(/');if(css!=null) {if((/br/gi).test(str)) {var lines=str.split(' 
');for(var i=0;ic.index)&&(match.index/gi,'\n');var lines=html.split('\n');if(this.addControls==true) this.bar.appendChild(dp.sh.Toolbar.Create(this));if(this.showColumns) {var div=this.CreateElement('div');var columns=this.CreateElement('div');var showEvery=10;var i=1;while(i<=150) {if(i%showEvery==0) {div.innerHTML+=i;i+=(i+'').length;} else {div.innerHTML+='·';i++;}} columns.className='columns';columns.appendChild(div);this.bar.appendChild(columns);} for(var i=0,lineIndex=this.firstLine;i0;i++) {if(Trim(lines[i]).length==0) continue;var matches=regex.exec(lines[i]);if(matches!=null&&matches.length>0) min=Math.min(matches[0].length,min);} if(min>0) for(var i=0;i

ページビューの合計