2018年8月12日日曜日

GooglePlay IAB(subscription)のレシート検証処理の実装 (Node.js)

AndroidのIn-App-Billing API v3でアプリ内課金(subscription)を実装したアプリ+サーバを作ったのですが、アプリの実装は簡単だった反面サーバを作るのに若干難産したので、そのことをネタに備忘録的なものを書いてみます。なお、サーバ言語はNode.js + TypeScriptです。

【①クライアントの実装】
GooglePlay Billing Libraryを使って実装します。
https://developer.android.com/google/play/billing/billing_library_overview
半日ぐらいで簡単に作れました。ライブラリ自体の使い方が(v3だけあって)かなり洗練されていることに加え、ググれば十分に情報が出てくるので、初心者でも簡単に実装できるかと思います。
つまり、クライアントの実装ネタ自体は十分にあるので本記事では割愛します。

【②サーバ実装】
subscription IABでは、購入時に発行されるreceiptをサーバへ送信し、サーバでGooglePlayへのverification(REST API)を行い、有効な状態ならコンテンツ閲覧などの機能を開放するといった形で実装します。

実装時に注意すべき点としては、APIの実行上限が20万件/日に制限されている(引き上げは出来るけど恐らく有料になる)ので、verificationが完了した状態のreceiptデータをRedisやDB等にキャッシュしておく必要がある(要するに無闇にポンポンGoogleへリクエストを投げてはいけない)事ぐらいでしょうか。

【③verification API】
GooglePlay developer APIのPurchases.proudcts:getを用いて検証します。
https://developer.android.com/google/play/developer-api?hl=ja
https://developers.google.com/android-publisher/api-ref/purchases/products/get

Requires AuthorizationなAPIなので、予めGooglePlay developer consoleでサービスアカウントを作成して、JWT認証できるようにしておく必要があります。

APIは普通にHTTP clientで書いても良いかと思いますが、様々なサーバ言語向けライブラリが用意されていて、少なくともauth系のAPIはライブラリを使用することが(Googleから)推奨されています。
https://developers.google.com/android-publisher/libraries

上記ページには、現時点でJavaとPhytonしかありませんが、ページを辿っていくとNode.jsのライブラリもありました。ただし、Alpha版(この時点で嵐の予感)。
https://github.com/google/google-api-nodejs-client

とりあえず、上記ライブラリでJWT認証+purchases APIを実装してみた。ところが、JWT認証は上手くいったのですが、purchases APIが全然上手く動かない。「Invalid Value」というエラーで失敗するのですが、何処がinvalidなのやら...。

StackOverflow等で調べてもv2以前の情報しか載っていなくて、一応v3の実装自体は入っているけどexampleとか一切無い状態。
https://github.com/google/google-api-nodejs-client/tree/master/src/apis/androidpublisher
やる気が感じられない...せめてどうやって実装するか切り口となる説明ぐらいREADME.mdに書いて欲しい。なので、JWT認証はライブラリで行いつつnode-rest-clientで直にpurchases APIを実行するのが、現時点で最も無難な選択肢だろうと判断しました。

以下、上記の方式で実装したコードを晒します。

【④必要なパッケージ】
npm install --save googleapis
npm install --save node-rest-client
npm install --save xml2js
※xml2jsを入れないとnode-rest-clientのコンストラクタでモジュール不整合が起きる

【⑤実装】
const RestClient = require('node-rest-client');
const { google } = require('googleapis');
const authClient = new google.auth.JWT({
    email: サービスアカウントのEメール,
    key: サービスアカウントのプライベートキー,
    scopes: ['https://www.googleapis.com/auth/androidpublisher']
});

const restClient = new RestClient.Client();

(略)

authClient.authorize((error, tokens) => {
    if (error) {
        略(認証エラー時の処理)
        return;
    }
    const url = 
        "https://www.googleapis.com/androidpublisher/v3/applications/"
        + receipt.packageName + "/purchases/subscriptions/"
        + receipt.productId + "/tokens/" + receipt.purchaseToken
        + "?access_token=" + tokens.access_token;
    restClient.get(url, {
        headers: {
            'Content-Type': 'application/json'
        }
    }, (data, response) => {
        if (200 != response.statusCode) {
            略(APIエラー時の処理)
        } else {
            receiptExpire = data.expiryTimeMillis;
            略(成功時の処理)
        }
    });
});

2018年4月22日日曜日

東方VGS Lite 1.0.2 (不具合対策版)

不具合対策版の東方VGS Lite 1.0.2 を先程(AM2:30頃)リリースしました。
曲の再生が途中で止まってしまうという連絡を受けて調査していたのですが、結構難航しました。

デベロッパーコンソールで確認した限り、クラッシュレポートが挙がってきているので、アプリ側のバグであることは間違い無いのですが...
色々と厄介そうな感じです。

まず、「発生元不明」ってのが厄介そうですね。これは恐らく、stack traceを吐かずに強制したものと推測できるので、大方ActivityManager辺りにkillされたのだろうと思います。まぁ、それ自体はAndroidならよくある事です。

最も厄介なのは、私が再現環境(Android5.0以降の実機)を持っていないことです。(私物のスマホはiPhoneなんですよねぇ)

何とか知り合いからNexus6を借りることができたので、それで実機確認してみたところ、それらしき現象を確認。再現できればもう勝ったも同然だと思っていたのですが、そこからがまた長かった。

どういう修正をしたのかは、アプリのアップデート内容に無駄に事細かに書いておきましたが、(1)の修正が恐らく決め手です。これで治ってくれる・・・はず。
無駄に事細かに書いておいた修正内容
(500字制限なので実はこれでも結構削った)

2018年4月7日土曜日

東方VGS Liteを公開

GooglePlayで公開しました。
https://play.google.com/store/apps/details?id=com.suzukiplan.tohovgs2
開発着手から約2週間でリリースという超短期間で完成させたのですが、旧東方VGSのソースコードは1行も使っていなくて、完全フルスクラッチで作り直しました。言語はKotlinで。

音源モジュール部分はC言語(下記)ですが。
https://github.com/suzukiplan/vgs-bgm-decoder

言語の構成比率としては下図のような感じです。

ここまで短期間(実働では3人日ぐらい)で作れたのは、Kotlinの言語としての良し悪しというよりは、Android Studio(IntelliJ)の生産効率がメチャクチャ高いからですね。単純に私の開発速度が尋常じゃなく速いのもあるのですが、それでもiPhoneアプリで同じものを作ろうとしたら最低でも3倍(10人日)ぐらい掛かります。

iOS版の東方VGS Liteは多分作らないです。

東方VGS Lite (3)


東方VGSをオーバーホールするこの機会に、一部機種でバックグラウンド再生時に音飛びするあの問題の解消を試みました。

まず、この問題が発生する原因を簡単に解説します。

VGSでは、波形メモリ音源エミュレータが短い間隔で波形データ(PCM)を生成し、それをOSの音源ドライバへ逐次的に書き込むことで音を再生していて、音飛びはこの波形データ生成処理が遅延することで発生します。そして、アプリがバックグラウンド状態に入るとOSがアプリのCPUプライオリティを引き下げることが原因で波形データ生成処理が遅延します。

これを回避するため、AndroidにはWakeLockという仕組みがあります。東方VGSでは、バックグラウンドに入る時にPARTIAL_WAKE_LOCKというCPUプライオリティを維持するWakeLockを取得することで、CPUプライオリティを引き下げられないように要求していますが、それが一部機種で効いていないことが原因で「バックグラウンドにした時に音飛びする」という問題が発生していました。

それ効かない原因としては、WakeLockの権限の取り方が誤っている(アプリ側の問題)か、OSがアプリのWakeLock要求を無視している(OS側の問題)が考えられますが、WakeLock#acquire自体は成功しているので、後者が原因だろうと思っています。

ここまでが既に分かっていたことです。旧東方VGSでは、このアプローチで問題解決を試み続けていた訳です(最近は全くノータッチだったけど)。ただ、疑わしいと思いつつ、結構大変だった為に試せなかった方式があって、それは音源ドライバのAPIをOpenSL/ESからAudioTrackに変更するというものです。

そして今回、この変更を試みたのですが、かなりアッサリとバックグラウンド再生時の音飛び問題が解消されました。

オーバーヘッド的には、OpenSL/ESよりもAudioTrackの方が重いです。

ただ、OpenSL/ESの場合、バッファリングと再生の処理を全てネイティブスレッド(Cスレッド)で動かしているので、JavaスレッドのCPU使用率が上がらないため、OSが「CPU使ってないならWakeLock要らないよね?」的な感じでWakeLockを無効化させていたのではないかなと。対してAudioTrackの場合、バッファリングはJNI経由でJavaスレッド、再生はJavaスレッドのみで実行されるので、JavaスレッドのCPU使用率が上がるため、音飛びがしなくなったと。

ひとまず、一番大きな問題を解消できたので、後は細かい所を片付ければリリースできそう。
(今日中にはリリースできるかも)

2018年4月5日木曜日

東方VGS Lite (2)

ランドスケープ用のデザインを雑に作ってみました。
VGSだと割と面倒だし、どうせバックグラウンド利用が大半だからと Portrait 専用にしてましたが、ネイティブUIだと楽ですね。

画面構成要素としては、LandscapeもPortraitも全く同じで、
・上半分のヘッダー、ピアノ、コントローラの領域を PlayContainer
・リストのタブ(TabLayout)とリスト(ViewPager)を ListContainer
みたいな形で定義しておき、Landscapeの時はそれらを左右に並べるだけという感じ。
(5分で作れました)

2018年4月4日水曜日

Android版 東方VGS Lite(開発中)

東方VGSのAndroid版が新しいAndroidで動かなくなったという報告を頂いたので調べた所、VGSではなくネイティブUIで作り直してしまった方が手っ取り早そうだったので、作り直し中。
旧来の東方VGSはそのまま残しておき、東方VGS Lite(別アプリ)として公開予定です。
完成度は7割前後なのですが、ここから先が長い...
でも、4月中にはGooglePlayで公開できるかと思います。

2018年3月14日水曜日

サーバサイドのお勉強

スマホのソシャゲで使えそうなアカウントサーバを作ってみました。
https://github.com/suzukiplan/simple-account-server
https://github.com/suzukiplan/simple-ap-server

ただし、飽くまでも試作です。
実用的かは未知数。
現在の本職はクライアント畑の人間なのですが、サーバのことも把握しておこうかなと思いまして。要するに単なる興味本位です。

まずは、アーキテクチャ設計。

アカウントサーバというと、Web時代のソシャゲであれば、メアド(ID)とパスワードで作るのが一般的ですが、スマホ向けということで、POSTすれば自動生成され、アプリ側では preference / userDefaults にそれを記憶して使うのだろうと思います。この辺は実際にスマホのソシャゲを幾つか遊んでみた限り、だいたいそんな感じだろうと想像で作成。
初回起動時(アカウント生成)

自動生成されたIDとtoken(パスワードに相当するもの)を用いてログインするイメージは下図のような感じ。
ログイン

DBですが、アカウント情報というとRDB一択だろうとは思いつつ、スキーマ定義が面倒臭いというただそれだけの理由で MongoDB(NoSQL DBMS)にしてみました。MongoDBというと、どちらかというとログ用途というイメージなのですが。

どうしてもHiRDBなどのRDBじゃないとマズイのであれば、置き換えは割と簡単にできます(実際、試しにMariaDBに置き換えてみたのですが、コードの書き換えよりもスキーマ定義等の下準備の方が面倒だった)。なので、まぁこのまま良いかなと。

DBがMongoDBだから速いので、キャッシュとか要らないのではないだろうかと思ったのですが、実測してみた限りRedisの方が最大10倍ぐらい早かったので、Redisでキャッシュ(Read cache)も作ってみました。なお、Write cacheは現時点では実装してません。アカウントの情報更新量が多いとかで必要であれば、RabbitMQを使う感じでしょうか。

最後に言語。
言語の定番は PHP かな?と思いつつ, TypeScript + Node.js で。
理由としては、
・豊富なパッケージ(使おうとしたミドルのパッケージが全てnpmにあった)
・情報を見つけやすい(≒情報が多い?)
あたり。

サーバサイドの言語事情はよく分かりません。Goとかも興味ありましたが、習得するのも面倒だしNode.jsなら少し知っているから楽なので。実のところ、TypeScriptにする必要すら無い(=ダイレクトJSで良い)んじゃないかなと思ったのですが、modelの定義にclassが使えるのが便利だし、tscも色々良い感じになっていてコンパイル手間も無いに等しい状態だったから、ダイレクトJSはやめておきました。

ひとまずこんな感じで、ユーザ登録、ログイン、ユーザ情報参照、ユーザ情報更新といったアカウントサーバの基本的な仕組みを作ってみました・・・が、コレ、実用に耐えられるシロモノなんですかね?正直良くわからないので、誰かコレでソシャゲ作ってサービスインしてみて下さい。(責任は取らない)

合理的ではないものを作りたい

ここ最近、実機版の東方VGSの開発が忙しくて、東方VGSの曲追加が滞っています。 東方VGS(実機版)のデザインを作りながら検討中。基本レトロUIベースですがシークバーはモダンに倣おうかな…とか pic.twitter.com/YOYprlDsYD — SUZUKI PLAN (...