2012年10月21日日曜日

Androidを開発して気づいた5つの誤解

チームEGGの曽川です。

「ICT ERA + ABC 2012 東北」で「Androidを開発して気づいた5つの誤解」と言うタイトルでLT発表をしてきましたので資料を公開します。

資料はこちらです。

これまでチームEGGとしては「よるにゃん」、「待ち受けトラッパー」をリリースしてきましたが、どうすればいいのか手探り状態でした。
そういった、「どうやってユーザをファンするの?」にしたり「どうやってダウンロード数伸ばせばいいかな?」と言うところに焦点を当てています。
ぜひご覧くださればと思います。

2012年10月17日水曜日

GoogleAnalyticsを利用したエラー報告

チームEGGの曽川です。

アプリの強制終了の報告はGoogle Playのデベロッパーコンソールでも確認できますが、ユーザに送ってもらわなければなりません。
この1クッションの手順が結構面倒なため、デベロッパーコンソールにはなかなか送ってもらえません。
そのため、GoogleAnalyticsを使用して自動的に送ってもらうようなサンプルを紹介します。
サンプルコードは以下のとおりです。
Androidのバージョン、端末名、エラー内容を送信します。

/**
 * 原文のスタックトレースを送信します。
 * 
 * @author yoshihide-sogawa
 * 
 */
public class AnalyticsRetracableUncaughtExceptionHandler implements UncaughtExceptionHandler {

    /** {@link Context} */
    private Context mApplicationContext;

    /** Androidのデフォルトの{@link UncaughtExceptionHandler} */
    private UncaughtExceptionHandler mExceptionHandler;

    /** 改行の代用文字(Analyticsで改行文字が使用できないため) */
    private static final String BR = "=";

    /**
     * コンストラクタ
     * 
     * @param applicationContext
     *            {@link Context}
     */
    public AnalyticsRetracableUncaughtExceptionHandler(final Context applicationContext) {
        mApplicationContext = applicationContext;
        mExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void uncaughtException(final Thread thread, final Throwable ex) {

        // 非同期でアナリティクストラックを実行
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(final Void... voids) {
                try {
                    // トラックイベントを生成
                    doTrackEvent(ex);
                } catch (Exception e) {
                    // エラーがあってもクラッシュを防止
                } finally {
                    // 強制終了
                    mExceptionHandler.uncaughtException(thread, ex);
                }
                return null;
            }
        }.execute();

    }

    /**
     * トラックイベントを実行します。
     * 
     * @param ex
     *            {@link Throwable}
     */
    private void doTrackEvent(final Throwable ex) {

        final StringBuilder actionBuilder = new StringBuilder();
        // バージョン情報
        actionBuilder.append("versionCode:");
        actionBuilder.append(AppUtils.getVersionCode(mApplicationContext));

        final StringBuilder errorMessageBuilder = new StringBuilder();
        // OS
        errorMessageBuilder.append("os:");
        errorMessageBuilder.append(Build.VERSION.SDK_INT);
        errorMessageBuilder.append(BR);
        // モデル
        errorMessageBuilder.append("model:");
        errorMessageBuilder.append(Build.MODEL);
        errorMessageBuilder.append(BR);
        errorMessageBuilder.append(Log.getStackTraceString(ex));

        // アナリティクスに送信
        final GoogleAnalyticsTracker tracker = GoogleAnalyticsTracker.getInstance();
        tracker.startNewSession("UA-XXXXXXXX-X", mApplicationContext);
        tracker.trackEvent("error", actionBuilder.toString(), errorMessageBuilder.toString(), 1);
    }
}

あとは、アプリケーションクラス等で以下の記述をすると準備完了です。
Thread.setDefaultUncaughtExceptionHandler(new AnalyticsRetracableUncaughtExceptionHandler(this));

ちなみにGoogleAnalyticsのV2からは、どういうエラーでアプリが強制終了したかを自動的に送信してくれます。
詳細はV2の記事を参考にしてください。

2012年10月10日水曜日

「Android での Google Play 課金 API 続・ことはじめ (実践編)」をまとめました。

チームEGGの曽川です。
今回は「「Android での Google Play 課金 API 続・ことはじめ (実践編)」をまとめました。
動画はこちらです。


今日はIn-App-Billingの遅延の話し。
In-App-Billinの質問は多く来ている
業績を上げている会社は多い

問題点
反映が遅くてクレームの処理に追われる事もある
日本だけではなく、韓国、アメリカ、ヨーロッパでも起こっている。

今回はホワイトボードで紹介する
推奨している作り方とは?

構成
アプリには一般的に以下のものが含まれる
Activity
Service
BroadcastReceiver


GooglePlay側
アプリのBroadcastReceiverに対してIN_APP_NOTIFYを通知


サンプルプログラムの例
1.GooglePlayがBroadcastReceiverに対してIN_APP_NOTIFYを送信
2.ServiceでIN_APP_NOTIFYを受信
3.GooglePlayに対してgetPurchaseInformationを送信
4.BroadcastReceiverでPURCHASE_STATE_CHANGEDを受信
5.Serviceに処理を渡す
6.Service内で後にゴニョゴニョ処理を行う
7.結果がActivityに反映される

どこで間違いが起こるか?
1.GooglePlayからIN_APP_NOTIFYが飛んで来る時
この処理が遅い?届いてない?と問い合わせが多く寄せられる
しかしGoogleの分析ではほぼ速攻で送っている
2.BroadcastReceiverからServiceに処理を渡すとき
3.ServiceからActivityにデータを渡すとき

メモリ不足によるダメージ
GooglePlayの画面が前面に出ているときはメモリを使っている
そのため以下の事が起こることが多い
1.BroadcastReceiverまでは届いたが、Serviceに渡すときにメモリ不足でServiceが起動しない
2.Serviceまでは届いたが、画面に反映させたりDBに保存する際にアプリが終了した
⇒GooglePlayはCONFIRM_NOTIFICATIONが送られるまで再配信する仕様
ただし、運が良ければ数秒、悪ければ数分後に配信されるため1回でも起こるとダメージが大きい
⇒ユーザはその間待ち続ける
再送は3秒後、30秒後、2分後という風に広がっていく(間隔は保障されていない)

一番最初の配信を逃さない。
BroadcastReceiverで受け取った瞬間にSQLite,txt,SharedPreferenceに保存するなどする
⇒サンプルコードでは記述されていない
Broadcastから書くのか良いのかServiceで書くのが良いのかまだ分析できていない
受け取ってから10ms以内書き込む
これを行う事で安定したという報告がある

データの保存後
まずは速攻でCONFIRM_NOTIFICATIONSで処理を返す
Serviceでいけるならばその場で処理を続ける
次回アプリを起動した際にデータが残っていれば処理の続きを行う

データ保存のありがちな間違い
1.PURCHASE_STATE_CHANGEDで受け取ったデータをメモリ上にのみ保持する
2.図の2や3の処理を行う
3.途中でシャットダウンされる
4.次回のIN_APP_NOTIFYまでまたなければならない

Google側のCONFIRM_NOTIFICATIONSの計測
問題がないアプリは一瞬で返ってくる
問題のあるアプリは1分程度などかかる場合がある

アプリの検証
Activityが終了している状態
Serviceが一回走ったけど終了させられた場合
⇒動くか?
BroadcastReceiver単体で動作するか?

今回と08/28分の質問に答える
Q.GooglePlayカードについて
A.まだアメリカのみ
Q.In-App-BillingとSubscriptionのorderIdが一致しない
A.それぞれでキーとして扱うものが違う
SubscriptionはpurchaseTokenを使う
orderIdは次の月になれば別のものになったり、月の途中で変更されたりする(仕様)
一方In-App-BillingはorderIdを使う
Q.Subscriptionの払い戻しとキャンセル
A.GooglePlayのAPIは一度販売すると有効期限が来るまでは基本キャンセルは無いという仕様
払い戻しをした場合のコンテンツの提供の停止はアプリ側で行う
払い戻しの通知がアプリに届いた場合、サーバ側のAPIで判明した場合というロジックを明示的に作成する
Google側で検討する
Q.長期間支払っているユーザに割引などどう対応するか?
A.Subscriptionの値段の変更はできないが今後できるようになる
現状、GooglePlayのAPIではできない
Q.定期購読モデルを止める時
A.GooglePlayから規約上のペナルティはない。
一般常識に照らし合わせてユーザのクレームが起こらないようにする
ユーザにキャンセルの方法を案内する
・GoogleWalletからキャンセル
・GooglePlayからキャンセル
Q.定期購読の猶予時間はどう計算されるのか
A.最初の購入日時を照らし合わせている
Q.GooglePlayの他のアプリマーケットよりも低い傾向にあるが何か施策は無いか?
A.フリーミアム
無料でダウンロードさせて個別課金が多い
ユーザをどう集めるかが肝になってきている
iOSとAndroidで一人当たりの売り上げが変わらないところもあるしAndroidが低いといわれる事もある
・アプリのクオリティ
・ユーザ層の違い
・課金がうまくいかない
アプリ内課金の方が長期にわたって収益が上がりやすい
正式版をアプリ内課金で行っている方が収益が上がる
⇒端末のメモリを圧迫しなくてよい
AndroidのGooglePlayは全ユーザにクレジットを要求していないから?
⇒課金のハードルは低い