久しぶりのエントリー。ゆーすけべーさんのおっぱいスクリプトに触発されて、自分も少し生活を楽にするスクリプトを書いてみました。
KinoPadという娘のために一時間くらい作った即席アプリが題材です。公開以来ぼちぼち他の言語にも対応してほしいとのご要望を頂いてきました。ローカライズ自体は言語につき数分で出来ることがわかり、ゴミアプリを量産する後ろめたさを感じながらも18言語分作ってしまいました。しかし、これを手作業でやっていてはメンテナンスがとても大変です。そこで極限まで手間を減らせるよう色々と工夫しましたので地味ですがTipsとして公開します。ゆうすけべーさんのような高尚な動機ではなくてすみません。
プロジェクトをテンプレート化する
まず、18個にもなると、一つずつEclipseのプロジェクトを作って、編集して、ビルドしてという作業は想像しただけでも絶望感を味わえます。そこで、一つ雛形となるプロジェクトファイルを用意して、スクリプトで動的にプロジェクトを生成することにしました。テンプレートとして差し替えられるようにするのは次のアイテムです。
- パッケージ名
- アプリケーションID
- アイコン
パッケージ名とアプリケーションIDを書き換える
プロジェクトのテンプレートが出来たら後はそれをコピーして派生バージョン用に書き換えます。パッケージ名とアプリケーションIDはAndroidManifest.xmlとjavaファイルに書かれていますので、コピーと置換はbashだと下記のような感じになります。
cp -r ${TEMPLATE_DIR} ${PROJECT_DIR} find ${PROJECT_DIR} -type f -name "*.xml" -or -name "*.java" | xargs sed -i "s/org\.exilis\.kinopad/org.exilis.kinopad${LANG}/g"
プログラムの挙動自体はこのパッケージ名を判別して変えるようにするとよいでしょう。パッケージ名はgetPackageName() で取得できます。
getApplication().getPackageName()
アイコンの生成はImageMagick
アイコンも18個を手作業で作るとなると大仕事です。クオリティにこだわらなければ簡単な合成のみでそれらしいものは作成可能です。KinoPadの場合は、ベースとなる画像を一枚用意して、後はそれに国旗の画像を重ねただけのものを使用しています。合成にはImageMagickを使います。
convert assets/icon512.png assets/flags/${LANG}.png -gravity southeast -composite assets/icon512_${LANG}.png
ついでに解像度に合わせた縮小画像を生成するためのコマンドは下記のとおりです。
convert -geometry 36x assets/icon512_${LANG}.png ${PROJECT_DIR}/res/drawable-ldpi/ic_launcher.png convert -geometry 48x assets/icon512_${LANG}.png ${PROJECT_DIR}/res/drawable-mdpi/ic_launcher.png convert -geometry 72x assets/icon512_${LANG}.png ${PROJECT_DIR}/res/drawable-hdpi/ic_launcher.png convert -geometry 96x assets/icon512_${LANG}.png ${PROJECT_DIR}/res/drawable-xhdpi/ic_launcher.png
プロジェクトのビルドとパッケージ作成
これもコマンド一発です。いちいちEclipseを起動する必要もありません。
ant -f ${PROJECT_DIR}/build.xml release
ついでに接続されている実機にもインストールしたければ、
${ANDROID_SDK_ROOT}/platform-tools/adb install -r
でOKです。
Androidマーケットへの自動アップロード
これがキモです。普通は下記の作業をWebブラウザでやることになります。
- http://market.android.com/publish にアクセス&ログイン (入力&1クリック)
- 該当アプリのエントリを開く (1~2クリック)
- 「APK ファイル」のタブを開く (1クリック)
- ファイルをアップロードする (3~クリック)
- アクティベート、保存 (3~クリック)
少なく見積もっても一つアップロードする度に10クリック+テキスト入力が必要になり、アップロードやページロードの待ち時間を考慮すると1分でやるにはかなりの反射神経が要求されます。18アプリともなると単純計算で約20分、180クリックが必要となりやってられません。自動化しましょう。
アプローチとしては二通りあります。一つはおっぱいスクリプトのように生のHTTPリクエストでやりとりする方法、もう一つはWebブラウザの操作を自動化するツールを活用する方法です。
前者は、APIとしてHTTP経由のインターフェースが定義されている場合や、込み入った認証が必要ない場合、あるいはページがJavaScriptやCanvasにあまり依存しておらず比較的簡単にパースできる場合に高速で有用です。
一方後者はそれ以外のケースでも無難に動くという点で適用範囲の広い方法です。今回は、後者の方法をWatirというRubyのモジュールを使用して実装することにしました。目標は
update_apk.rb <google_id> <google_pass> <full_path_to_apk>
というコマンドを実装することです。
APKファイルのバージョンを取得する
パラメーターとして渡されるのはAPKファイルのパスだけですので、アップロードする際に必要になるアプリケーションIDやバージョン文字列はAPKから取得します。これらは、自動化する際のURLを生成するためにあると便利です。APKファイルといってもタダのZIPなので展開してAndroidManifest.xmlを読めば簡単に抽出出来ると思ったのですが、パッケージングするときにバイナリーにエンコードされてしまっていることが分かりました。この件についてはKlabの方の記事が詳しいです。でググったらRuby用のパーサーがありましたのでこれを使わせていただくことにしました。パーサー部を関数化するだけの単純な変更を加えました。
https://github.com/exilis/apk-updater
Watirでアップロードする
これは、FirebugなりでDOMを見ながら実際にワンステップずつwatirのコマンドに書き換えていくことになります。以下のような感じになりました。
上記のパーサーと合わせてgithubに載せてあります。
require 'rubygems' require 'watir-webdriver' require 'watir-webdriver/wait' require File.dirname(__FILE__) + "/axml2xml" require "rexml/document" unless ARGV.length == 3 puts "Usage: ruby update_apk.rb <google_id> <google_pass> <full_path_to_apk>\n" exit end userid = ARGV[0] passwd = ARGV[1] apk_fullpath = ARGV[2] # extract package name and version code out of the APK begin xmldoc = REXML::Document.new(load_android_manifest_xml(apk_fullpath)) version_code = xmldoc.elements['manifest'].attributes['versioncode'].to_i(16) package_name = xmldoc.elements['manifest'].attributes['package'] rescue puts "Error: failed to load APK file\n" exit end # login b = Watir::Browser.new b.goto 'https://market.android.com/publish/' b.text_field(:id => 'Email').set userid b.text_field(:id => 'Passwd').set passwd b.button(:id => 'signIn').click # upload & save Watir::Wait.until { b.text.include? "All Android Market listings" } b.goto 'https://market.android.com/publish/Home#AppEditorPlace:p=' + package_name b.div(:id => 'gwt-debug-multiple_apk-apk_list_tab').when_present.click b.button(:id => 'gwt-debug-apk_list-upload_button').when_present.click b.file_field(:name, "Filedata").when_present.set(apk_fullpath) b.button(:id => 'gwt-debug-app_editor-apk-upload-upload_button').when_present.click b.button(:id => 'gwt-debug-bundle_upload-save_button').when_present.click b.execute_script("window.confirm = function() {return true}") b.a(:id, "gwt-debug-apk_list-activate_link-#{version_code}").when_present.click b.button(:id => 'gwt-debug-multiple_apk-save_button').when_present.click
まとめ
以上の処理をバリエーションごとにループで回すシェルスクリプトなりを書けば、元のテンプレートとなるプロジェクトを一つ修正するだけで、ビルドからAndroid Marketへのアップロードまでをコマンド一発で済ませることができます。GNU Parallel等で並列化するともっと素敵です。KinoPadの場合は実質一つのバージョンをメンテナンスするだけの労力で18のバージョンを管理できるようになりました。
開発に関係の無い日常の業務でも自動化はとても有効です。時間の節約だけでなく、ヒューマンエラーのリスクを減らすことができます。私は他の業種のことは知りませんが、コンピューターを使って仕事をしている現場のほとんどでExcelのマクロ以上の自動化は出来ていないのではないかと思います。Salesforceなどワークフロー全体を管理するシステムにしっかり投資していれば話は別ですが、私が経験してきたIT企業の現場ですらエンジニアがいるにもかかわらず自動化可能な「作業」を手作業でこなしている場面が多々見られました。何でも二回以上する可能性のあることは自動化すると生産性は確実に上がります。
今後の展望
ところで、蛇足ですがBingの画像検索APIにあるAdultフラグは全然使い物になりません。KinoPadもおっぱいスクレイパーと同じく内部でBingの画像検索APIを使用していますが、子供向けということでこのフラグを有効にしても想像力豊で過激な画像がたくさん降ってきます。仕方なく不適切な検索結果が表示された時はワンタッチで通報できる機能を実装していますが、現状キーワードベースなのでキリがないというか根本的な解決にはつながりません。ちゃんとやるなら画像単位で通報できるようにして、サーバー側で画像解析&クラスタリング、いけない画像コーパスを構築して検索結果を表示する前に逐次評価ってことになると思いますが、KinoPadにそんなに手間をかけるつもりはないので今のところ放置です。
ただ考えようによっては「いけない画像だけをはじける=いけない画像だけを集められる」ということなので、同等の技術はおっぱいスクリプトを新たな次元に押し上げる足がかりになるに違いありません。つまりおっぱいスクリプトが収集したビッグおっぱいデータから有益なおっぱいだけを抽出するということです。おっぱい星人&ビッグデータ屋さんのみなさん、いかがでしょう?








