2016年1月21日木曜日

[Android] In-app Billing v3を使ってライセンス購入を実装するLicenseManagerをアップデートしました

憂鬱なムックを助ける友の会: [Android] In-app Billing v3を使ってライセンス購入を実装する で紹介していたLicenseManagerクラスですが、onCreateでnewして、onResumeでisLicensePurchased()メソッドにて購入状態を判定するという作りにしていましたが、onCreateからonResumeが走るまでにIn-app Billing APIが結果を返しきる保証は何もないので、判定を誤るという可能性がありました。

そこで、そのバグ(そう、これはバグです!すみませんっ!!)を修正しましたのでご案内します(そして、コミットログ、ミスりました。。。恥ずかしい。。)

https://github.com/shinchit/LicenseManager

使い方はほぼ変わりません。ただ、newした段階でIn-app Billing APIが結果を返し済みであることを確認してから先の処理に進むように改修していますので、newの直後にisLicensePurchased()メソッドを使っても問題ありません。

技術的には、非同期処理の順番、完了待ちを保証するために、JDeferredというライブラリを利用しています。
そのため、appレベルのbuild.gradleのdependenciesに

 compile 'org.jdeferred:jdeferred-android-aar:1.2.4'

といった記述が必要です(バージョンは適宜調整してください。動作確認をとっているのは1.2.4です)。

JDeferredの細かい使い方については、公式、または次のページがわかりやすいです。

Java - Androidの非同期処理を綺麗に書けるjdeferredが便利! - Qiita

LicenseManagerでは、以下の記述でnewの中でIn-app Billing APIへの非同期リクエストが帰ってくるのを待ち合わせています。

// ライセンス購入セットアップ開始
AndroidDeferredManager dm = new AndroidDeferredManager();
try {
dm.when(() -> {
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()) {
Log.d(TAG, "セットアップ失敗 結果: " + result);
return;
}
// オブジェクトが生成されていない
if (mHelper == null) return;
Log.d(TAG, "セットアップ成功。 購入情報照会へ");
mHelper.queryInventoryAsync(mGotInventoryListener);
}
});
}).done(result -> {
// do nothing
}).fail(tr -> {
tr.printStackTrace();
}).waitSafely();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ちなみに、ここではJavaのλ式(ラムダ式)を利用しています。が、別に使わなくても問題ないです^^
ただ、見てお分かりの通り、λ式を使うと型の迂遠な明示が省略できるので楽に読み書きでき、スッキリして見えます。

また、GitHub上のLicenseManagerでは、アプリのライセンスキーを暗号化せずにそのまま渡していますが、実際のリリースapkを作る際にはそれはオススメできません。ライセンスキーは暗号化して保持し、これを復号化してLisenceManagerに渡すようにするべきです。とはいえ、暗号化のseedはapk内に持つため、その気になれば簡単に復号化もできてしまうのですが。。。万全を期すなら、seedを外部のサーバ(Webサーバなど)において、SSLでそれを取得してというのが良いのでしょうが、、、そこまでやる必要もないかと(というか、それをやると実装によってはオフラインでアプリを使用できないシーンが出てくるし)。
この辺の、暗号化・復号化の話はまた別エントリーで取り上げます。

[Android] Android Studioでaarライブラリを取り込む方法

Android Studioでaarで提供されているライブラリをプロジェクトに取り込むには次のようにします。

まず、プロジェクトレベルのbuild.gradleに次のようにflatDirの記述を追加します。

allprojects {
repositories {
jcenter()
// ここを追加。下記の設定ではプロジェクトのapp/libs/にaarを設置する設定になる。
flatDir {
dirs 'libs'
}
}
}
view raw build.gradle hosted with ❤ by GitHub
flatDirのdirsのパスは任意のパスを指定できますが、ここでは簡便に使えるプロジェクトのapp/libs/を指定しています。

次に、appレベルのbuild.gradleに次のように取り込むaarファイルの情報を追記します。


dependencies {
// HogeHoge.aarを取り込む記述
compile(name:'HogeHoge', ext:'aar')
}
view raw build.gradle hosted with ❤ by GitHub
ここまで行ったら、プロジェクトのapp/libs/ディレクトリに目的のaarファイル(例ではHogeHoge.aar)を設置します。

あとは、Android StudioでSyncProject(下図の赤枠部)を実行すれば無事にaarがプロジェクトに取り込まれて幸せになれます。

Sync Project with Gradle Filesボタン


参考:
 AndroidStudio - Android Studio でaarをローカルから読み込む方法 - Qiita

2015年11月27日金曜日

[Capybara] [Turnip] 複数のエディションのテストで分岐がある場合の対応方法

Capybara + Turnipを使ってE2E(End-to-End)テストをする時に、テスト対象のサービスや製品に複数のエディションがあり、画面遷移は殆ど共通なのですが、あるエディションにだけに他のエディションにはない画面(遷移)がある場合のノウハウ。

まぁ、愚直にやるなら、各エディション分featureファイルを作れば良いのですが、DRYじゃないし、タイピングも怠いです。RSpecからテストを実行する時にパラメータを与えて分岐できたら幸せじゃないだろうか?とも考えたのですが、そのような方法は私は見つけられず、パラメータを覚えておかなければいけないのも考えてみると怠いです。

というわけで、featureファイルに全部テストデータを突っ込みつつ、分岐もすれば比較的怠くなくいけた感がありましたので方法を書いておきます。

まずは、サンプル。

機能: ○○テスト
シナリオアウトライン: 決済ウィザードを立ち上げ、カートの決済を行う
前提: 決済ウィザードにアクセスする
もし: 決済ウィザードが表示されている
ならば: AAAする
かつ: BBBする
かつ: CCCする
かつ: <DDDする>
かつ: EEEする
例:
|DDDする |
|何もしない|
|何もしない|
|DDDする |
|何もしない|
view raw sample.feature hosted with ❤ by GitHub
step "何もしない" do
end
step "DDDする" do
click_on "カートに入れる" # DDDするの中身は適当です。この例では、例えば画面の「カートに入れる」ボタンを押します。
end
view raw sample_steps.rb hosted with ❤ by GitHub
やっていることは至ってシンプルです。Capybara + Turnipでは<>でシナリオ内に変数を埋め込むことができます(例で言うと<DDDする>が変数です)。この変数にfeatureファイルの最下部に記載した「例:」の各値を当てはめて逐次実行することをCapybara + Turnipは行ってくれます。例では、5行の定義がありますが、最初の一行はヘッダで変数名と同じにする必要があります。残り4行がデータになり、Capybara + Turnipは各行から値を取り出しては変数にその値を代入し、代入した結果と一致するstepを実行します。

例では上から順に「何もしない」、「何もしない」、「DDDする」、「何もしない」と定義していますので、1番目、2番目、4番目は<DDDする>というstepが"何もしない"になります。そして、3番目だけが、"DDDする"というstepになります。

stepsファイルを見ると、二つのstepだけ定義しています。"何もしない"と"DDDする"の二つです(もちろん略しているだけで、他にも"AAAする"、"BBBする"などが定義されていなければいけません)。step "何もしない"は見てのとおり何もしません。step "DDDする"は何か仕事をしています。

この様に、ステップ名全体を変数化し、"何もしない"といった様な何の処理もしない空のstepを用意することで、一部のエディションにだけ存在する画面に分岐してテストすることが一つのfeatureファイルで可能になります。殆どが同じ処理のシナリオをfeatureを分けるなどをして書く必要はないですし、テストデータ(例:の表組のデータのことです)もfeatureファイルに閉じ込めておくことができます(外部から入力を与える必要がない)。

ちなみに、例:の表組を使うためには、featureファイルにて「シナリオ」ではなく、「シナリオアウトライン」を使う必要があることに注意してください。

2015年11月7日土曜日

[Capybara] [RSpec] [Poltergeist] [Turnip] E2Eテストを自動化する手順

WebシステムのE2E(End-to-End)テストの自動化を、Capybara + RSpec + Poltergeist + Turnipで行ってみました。新規に自動化のスクリプトを作っていく手順を備忘で書いておきます。環境はMacOSX EI Capitanです。

# ディレクトリ名は適当に決めてください
$ mkdir test
$ cd test
$ bundle init
view raw make_test.sh hosted with ❤ by GitHub
Gemfileを次の内容に更新します。

source "https://rubygems.org"
gem 'turnip'
gem 'capybara'
gem 'poltergeist'
view raw Gemfile hosted with ❤ by GitHub
そして、bundle installを実行します。
$ bundle install
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies...
Using mime-types 2.6.2
Using mini_portile 0.6.2
Using nokogiri 1.6.6.2
Using rack 1.6.4
Using rack-test 0.6.3
Using xpath 2.0.0
Installing capybara 2.5.0
Installing cliver 0.3.2
Installing diff-lcs 1.2.5
Installing multi_json 1.11.2
Installing gherkin 2.12.2
Installing websocket-extensions 0.1.2
Installing websocket-driver 0.6.3
Installing poltergeist 1.7.0
Installing rspec-support 3.3.0
Installing rspec-core 3.3.2
Installing rspec-expectations 3.3.1
Installing rspec-mocks 3.3.2
Installing rspec 3.3.0
Installing turnip 1.3.1
Using bundler 1.8.4
Bundle complete! 3 Gemfile dependencies, 21 gems now installed.
Bundled gems are installed into ./vendor/bundle.
view raw make_test1.5.sh hosted with ❤ by GitHub
次に、RSpecとTurnipの設定ファイルを用意します。

$ rspec --init
$ echo '-r turnip/rspec' >> .rspec
$ echo '--format d' >> .rspec
view raw make_test2.sh hosted with ❤ by GitHub
spec/turnip_helper.rbを作ります。

require 'turnip/capybara'
require 'capybara/poltergeist'
Capybara.default_driver = :poltergeist
Dir.glob("spec/**/*steps.rb") { |f| load f, true }
ここまででテストシナリオを作成する環境の準備が整います。

では、テストシナリオを書いていきます。
Turnipで日本語が使えるようになっていますので、日本語で書いていきます。
簡単な例として、Googleにアクセスして、キーワードTurnipで検索し、その結果をsnapshotに取るというシナリオを作ってみます。

シナリオはfeatureファイルに記述します。
spec/features/google_search.featureを次の内容で作りました。

# encoding: utf-8
# language: ja
機能: Googleの検索テスト
シナリオ: Googleで検索を実行してスナップショットを取る
前提 Googleにアクセスする
もし Googleのトップページを表示する
ならば Googleと表示されている
かつ 検索キーワードにTurnipを入力する
かつ Google 検索ボタンをクリックする
ならば カブと表示されている
シナリオに呼応するstepファイルを記述します。ファイルの命名規則に注意してください。spec/turnip_helper.rbのDir.glob()で指定したパスにマッチするように作ります。
spec/steps/google_search_steps.rb

# encoding: utf-8
step "Googleにアクセスする" do
Capybara.app_host = "http://www.google.com"
end
step "Googleのトップページを表示する" do
visit '/'
end
step "検索キーワードにTurnipを入力する" do
fill_in "q", with: "Turnip"
end
step "Google 検索ボタンをクリックする" do
click_on "Google 検索"
end
step %(:textと表示されている) do |text|
expect(page).to have_content(text)
page.save_screenshot(
File.expand_path("./snapshot/snap-#{text}.png"), full:true
)
end
テストが出来上がったので実行します。実行前にスナップショットを保存するディレクトリを mkdir snapshot で作っておきます。

$ rspec
view raw make_test3.sh hosted with ❤ by GitHub
無事にテストが通れば次のようになります。
Googleの検索テスト
Googleで検索を実行してスナップショットを取る
前提Googleにアクセスする -> もしGoogleのトップページを表示する -> ならばGoogleと表示されている -> かつ検索キーワードにTurnipを入力する -> かつGoogle 検索ボタンをクリックする -> ならばカブと表示されている
Finished in 4.73 seconds (files took 1.02 seconds to load)
1 example, 0 failures
view raw make_test4.sh hosted with ❤ by GitHub
スナップショットも作成されています。
$ ls -la snapshot/
total 1488
drwxr-xr-x 4 shinchi staff 136 11 7 19:47 .
drwxr-xr-x 11 shinchi staff 374 11 7 19:47 ..
-rw-r--r-- 1 shinchi staff 50456 11 7 19:47 snap-Google.png
-rw-r--r-- 1 shinchi staff 705831 11 7 19:47 snap-カブ.png
view raw make_test5.sh hosted with ❤ by GitHub

2015年11月4日水曜日

[Android] Recent Apps(アプリ履歴)にActivityを表示させない方法

非常にニッチなニーズなのですが、諸般の事情で実装する必要があったので備忘で書いておきます。

Recent Apps(アプリ履歴、タスク一覧ともいうのかな?)に対象のActivityを非表示にするには、対象Activityがフォアグラウンドになければ


にある通り、AndroidManifest.xmlにて対象Activityの<activity>に

android:excludeFromRecents="true"

を指定してあげれば非表示になります。

問題は、対象Activityがフォアグラウンドにある場合で、その場合は、下記のようにexcludeFromRecents以外に色々と設定してあげる必要があります。
(TargetActivityはご自身のActivity名に読み変えてください)。

<activity
android:name=".TargetActivity"
android:label="@string/title_activity_target"
android:excludeFromRecents="true"
android:noHistory="true"
android:alwaysRetainTaskState="false"
android:stateNotNeeded="true"
android:clearTaskOnLaunch="true"
android:finishOnTaskLaunch="true" />

正直、各設定の詳細な意味は深く理解していません(汗
おまじないに近いです。

次の記事を参考にしました。

Issue 53313 - android - Foreground service killed when receiving broadcast after acitivty swiped away in task list - Android Open Source Project - Issue Tracker - Google Project Hosting

2015/11/7追記:

もっと簡単で確実な方法がありました。onPause()でsetTheme(android.R.style.Theme_NoDisplay)すれば良いです。

protected void onPause() {
    setTheme(android.R.style.Theme_NoDisplay);
    super.onPause();
}



2015年7月20日月曜日

[MovableType] [Data API] [Perl] 画像タイプのカスタムフィールドを持つエントリーをData APIを使って投稿する


MovableTypeで画像タイプのカスタムフィールドを持つエントリー(記事)をData APIを使って投稿する方法について(しかもPerlで)

これを実現するには、
1.画像をData APIを使ってアップロードし、アイテム(Asset)に登録する
2.エントリーをData APIを使って投稿する際に、1で登録したアイテムを紐付けて登録する
という手順が必要です。

サンプルを見るのが一番わかりやすいと思いますので、次にコードを提示します。

#!/usr/bin/perl -w
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Request::Common;
use JSON;
use URI::Escape;
use Data::Dumper;
use Encode;
my $api_url = 'http://localhost/cgi-bin/mt/mt-data-api.cgi';
my $blog_id = 1;
my $res;
my $json;
my $token;
my $lwp = LWP::UserAgent->new;
$lwp->timeout(20);
# authentication
$res = $lwp->post(
"$api_url/v1/authentication",{
username => '[MTのログインアカウント名を指定]',
password => '[指定したアカウントのパスワードを指定]',
clientId => 'test', # 任意の文字列を指定する
}
);
$json = from_json($res->content);
$token = $json->{accessToken};
if(! $token){
print 'error authentication';
return;
}
# 画像を投稿(アップロード)する
my $img_ep = "$api_url/v2/sites/$blog_id/assets/upload"; # 画像(厳密にはAsset)をアップロードするエントリーポイント
my $request = POST($img_ep, Content_Type => 'form-data', 'X-MT-Authorization' => "MTAuth accessToken=$token", Content =>
{
path => '',
file => ['/Users/takahiro/Desktop/IMG_1498.jpg'], # サンプルとしてデスクトップに置いたIMG_1498.jpgという画像を投稿(アップロード)する
autoRenameIfExists => 'true',
}
);
$res = $lwp->request($request);
$json = from_json($res->content);
# check status
if (! &check_status($json)) { exit; }
# 記事を投稿する(画像タイプのカスタムフィールドを持ち、そのカスタムフィールドには先に投稿した画像を紐付ける)
my $ep = "$api_url/v2/sites/$blog_id/entries"; # 記事を投稿するエントリーポイント
$request = POST($ep, 'X-MT-Authorization' => "MTAuth accessToken=$token");
my $params = {
entry => encode_json({
title => 'title - test(asset: ' . $json->{id} . ')',
status => 'Publish',
customFields => [
{
basename => 'custimg',
value => sprintf(qq{<form mt:asset-id='%d' class='mt-enclosure mt-enclosure-%s' style='display: inline'><a href="%s"><img src="%s"/ width="500"></a></form>}, $json->{id}, $json->{class}, $json->{url}, $json->{url}),
},
],
})
};
$request->content(join('&', map{$_.'='.$params->{$_}} keys %$params));
$res = $lwp->request($request);
$json = from_json($res->content);
# check_status
if (! &check_status($json)) { exit; }
# Data APIからのレスポンスをチェックして、不正であれば詳細メッセージ等を表示する
sub check_status {
my $json = shift;
if(! $json->{id}){
my $message = $json->{ error }->{ message };
$message =~ s/\\x{([0-9a-z]+)}/chr(hex($1))/ge;
print $json->{error}->{code};
print ':';
print $message;
return 0;
}
return 1;
}
view raw gistfile1.pl hosted with ❤ by GitHub
(私的)ハマりポイントは次の通り。

1.画像をアップロードする場合にはX-MT-Authorizationでトークンを渡すだけでなく、Content-Type: multiple/form-dataも渡す必要がある。boundaryとか自力でやろうとするとこれは大変なので、HTTP::Request::CommonのPOSTメソッドを使いました。POSTメソッドにて、POST(url, Content_Type => 'form-data', ... );とすることでHTTP::Request::Commonがboundaryも含めたヘッダをよしなに処理してくれます。POSTメソッドへの引数がContent-Typeではなく、Content_Typeになっていることに要注意です。

2. 画像の実体の指定もHTTP::Request::CommonのPOSTメソッドを利用すれば、Contentパラメータ内で['ファイルのパス']とすることで処理してくれます。自力でファイルを開いたり(binmode指定して頑張ったりもして)、IO::Fileオブジェクト作って渡してみたり、色々やってみましたが、これらの方法はうまくいきません。

3.画像タイプのアセットを記事に紐付けて、編集画面にも埋め込んで表示するには、サンプルの通り、
 sprintf(qq{<form mt:asset-id='%d' class='mt-enclosure mt-enclosure-%s' style='display: inline'><a href="%s"><img src="%s"/ width="500"></a></form>}, $json->{id}, $json->{class}, $json->{url}, $json->{url})
というように、formタグの中にimgやaタグを組み込んだ値を指定します。style='display: inline;'というように';'を含んだ値を渡すとJSONモジュールがエラー吐いて死にますので、外します。formの他の属性はおまじないだと思ってください。

参考:
 MovableType Data API v2
 HTTP::Request::Common
 実装メモ:Movable Type 6の便利なData APIを利用して画像を出力する : アークウェブのブログ
 

2015年7月19日日曜日

[ImageMagick] 縦長画像のみ90度回転して表示される問題を解決する

昨今のデジカメやiPhoneなどで撮影した画像をWebにアップすると縦長画像が90度回転して表示されてしまう問題に遭遇しました。画像のExif情報が悪さしている模様です。

全てが縦長の画像であれば対処は単純なのですが、横長と混在している状態だとどうしたらいいの?ってなります。が、ご安心。ImageMagickに素敵なオプションがありました。
auto-orientです。次のように使うと、縦長の画像だけ判定して90度回転してくれます。

# auto-orientを使えばよしなに処理してくれます。
$ ls *.jpg | xargs -I{} convert {} -auto-orient -verbose conved/{}
# conved/ディレクトリに*.jpgで縦長画像は90度回転、横長画像はそのままの画像が生成されます。
view raw gistfile1.sh hosted with ❤ by GitHub