ラベル android の投稿を表示しています。 すべての投稿を表示
ラベル android の投稿を表示しています。 すべての投稿を表示

2012/07/04

SK17iをICSにして撮影した画像のファイルサイズが増えている件について

ExifPMというAndroidアプリを作成している事もあって、Xperia Mini Pro (SK17i) で撮影した写真データのExifデータをチェックしていると、ICSへのアップデート前後でファイルサイズが違っている事に気がつきました。

Exifデータを比較してみると、画素データのサンプリング方法がYCbCr420からYCbCr422へと変更されていた事が原因でした。

元々500万画素のカメラなのに、生成される写真データは700[KB]から900[KB]程度のファイルサイズだったので、 カメラアプリの不具合かなぁと思っていたのですが、これはデータをYCbCr420で処理していた事が原因でした。

色データの扱いは対象の特性によって異っていて、ディスプレイへの出力はRGBで考えて、印刷をする時にはマゼンダなどの中間色を使ったCMYKが使われます。JPEGの場合にはYCbCrやYCbCrと呼ばれるような、輝度と色差のデータを使う事になります。

基本的にはYCbCr420は縦横4ピクセルで色差情報を共有するので、横並びの2ピクセルで色差情報を共有するYCbCr422と比較してデータ量が落ちるため、画質が落ちるといわれています。

データ量が少ない分だけ劣化するのは事実ですが、まぁそんなに単純な話しでもないので、 サンプリング方法の詳しい説明はGoogleでいくつかのサイトの記述を確認されるのがお勧めです。

問題はICSにアップデートしたSK17iではカメラの画質が上がっているのだろうか、という疑問に対する答えがあるのかどうかです。

結論からいえば情報不足でよく分からないという事になるのですが、とりあえずまとめたところをメモにしておきます。

撮影した画像の見た目の違い

残念ながら同一条件で、同一被写体を撮影した画像がないので、正確な比較はできません。

撮影する時間帯はずれているので照明の条件は同じになりませんが、似たようなアングルで撮影した画像をみてみると、古い画像は絹のような質感が感じられて何か靄のようなもので覆われている印象があります。それと比較するとICS以降に撮影した画像は鮮明に感じられます。

画像をみてICSで撮影された画像かどうか区別できるものもあれば、難しいと感じるものもあって、単純に画質が向上したというのは難しいかなと感じています。

レンズの汚れや撮影環境の条件をちゃんと揃えないと、見た目の判断だけでは何ともいえないところです。

画像ファイルサイズの違い

対象によってファイルサイズは変化しますが、室内・屋外で撮影した手元の画像を眺めると、だいたいファイルサイズは次のようになっています。

  • Android 2.3: 500[KB]〜900[KB]
  • Android 4.0: 1.2[MB]〜1.8[MB]

画質の違いは別にして、ファイルサイズは確実に増えています。

APIからみた画質の変化について

自作アプリを作った時にカメラAPIから渡されたデータは、YUV420SPという形式でした。 ちょっと調べたところ、このAPIに変化はなさそうです。

Androidのカメラアプリを作って気になったのは、カメラからのRAWデータにアクセスする方法がないところでもありました。APIとしては準備されているんですが、あれが有効なデータを返すデバイスを寡聞にして聞いた事がありません。

使えたとして、大抵のデバイスでは、ほぼ確実にヒープ領域が確保できないでしょうから、処理を始める前にアプリが落ちるんでしょうけれど。

カメラ固有のAPIからデータを取得してYCbCr422データを作らない限りは、YCbCr422のデータをYUV420SPをYCbCr422に変換してもデータサイズが大きいだけで画質の向上は見込めない事になります。

現状では満足していますが、画質が向上したのかという疑問にははっきりとした答えが出せないでいます。

さいごに

SK17iを入手して比較的すぐにICSにアップデートしてしまったので、カメラアプリの挙動の違いが本当に正しいのか、設定の違いなんじゃないのか、という点について良くわかっていません。

体験として嘘は書いてませんが、十分に調査したとはいえないので、ひょっとすると勘違いが含まれているかもしれません。その際はご指摘頂ければ幸いです。

ただICSにアップデートした事自体は公開していません。Xperiaのドコモ端末の中でRAM 512MB未満はICSへのアップデートが不適とされて行なわれない事になりました。

理由がRAMサイズにあるという事ですが、それで何が問題なのかという点はよく分かっていません。

ただしばらくSK17iを使っていて、落ち着いて使っている時は良いのですが、ソフトウェアキーボードを急いで操作する時には、ひっかかるように感じる時があります。

その反面、ハードウェアキーボードを使ってメールやtwitterを使う場面で、何か問題が起こった事はないので、細かい違いを気にするのであれば、ICSへのアップデートは避けた方がいいのかもしれません。

それを除けば、いまのところICSだから問題だという現象には遭遇していないところです。 カメラの件は裏の仕組みがどうなっているか分からないので、良いのかどうか分かりませんが、基本的に新しいものが好きなのでICSにして満足しています。

2012/06/29

バージョンアップを続けるAndroidアプリでのDBスキーマの更新方法

さて、AndroidアプリケーションにはSQLite3への接続を管理するためのクラスとしてandroid.database.sqlite.SQLiteOpenHelperクラスがあります。

通常はこのクラスを継承して、子クラスを自分のアプリケーションで定義するわけですが、今回はスキーマをバージョンアップするためのonUpgradeメソッドの書き方についてです。

よくあるサンプルは直前のバージョンと自分のバージョンを比較して、バージョンアップ用のコードを定義していますが、アプリを使うユーザーが次のようなシナリオに遭遇する場合を想定しているでしょうか。

  • Version 1.0のアプリをダウンロードして使う
  • Version 2.0がリリースされるが、ユーザーは更新を行なわなかった
  • Version 3.0がリリースされ、ユーザーはアプリを更新した

アプリは毎回のバージョンアップで、ALTER TABLE ... ADD COLUMN ...を行なっているとします。

SQLiteOpenHelperクラスを紹介するWebサイトはいろいろあったのですが、こういう使い方を説明しているところはなかったのでまとめる事にしました。

まぁ良く考えれば分かる事ですが、いまのところの自分のベストプラクティスをメモしておきます。

解決するべき課題

前提として毎回ユーザーはアプリを更新しない、 Version 1.0からVersion 20.0へアップグレードしても、アプリケーションの内部データは一貫性を保ちたい、という要求があるとします。

これ自体は日本的というか、こういうのは諦めて、onUpgradeでDROP TABLE 〜 CREATE TABLEを行なってデータを初期化してしまうのも、まま、見ることです。

ここでは敢えて、この課題を解決する方法を目指します。

定石1:バージョン毎にスキーマやデータを変更するメソッドを定義する

DBのバージョンは1から始まりますが、この時のDB定義はonCreate(SQLiteDatabase db)メソッドの中でdbオブジェクトにSQLを発行して定義します。

バージョン2からは例えば、次のようなメソッドを用意してあげます。 これは直前のバージョン1からのアップグレードだけを前提にしています。

ExifPMで使用しているversion 2、3用メソッドの例

    private void updatedb2(SQLiteDatabase db) {
        Log.d(this.toString(), "upgrading db 1 to 2");
        StringBuilder sql = new StringBuilder();
        ...
        db.execSQL(sql.toString());
    }
    private void updatedb3(SQLiteDatabase db) {
        ...
    }

定石2:onCreate、onUpgradeメソッドに一度記述した内容は消してはいけない

もちろんこれは内容を変更しないという意味ではありません。

定石1で作成したバージョン毎のメソッドを追記する操作だけを許可するというルールです。

ExifPMで使用しているonUpgradeメソッドの全体


    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if(oldVersion < 2 && 2 <= newVersion) {
            updatedb2(db);
        }
        if(oldVersion < 3 && 3 <= newVersion) {
            updatedb3(db);
        }
    }

よくみる例はここで、if (oldVersion == 1 && newVersion == 2) {...}みたいにしていますが、(oldVersion, newVersion) = (1,3)の組み合せもあるわけで、まぁそれは破綻するわけです。

ちなみにonCreateメソッドの方は初期化時に1回しか呼ばれないためもっと簡単です。

    @Override
    public void onCreate(SQLiteDatabase db) {
         ...
         updatedb2(db);
         updatedb3(db);
    }

念のためにコンストラクタの書き方

SQLiteOpenHelperのサブクラスとしてSQLDBHelperというクラスを定義していて、この中で親クラスのコンストラクタを呼び出し、DBバージョンを指定しています。

コンスタクタの例


    public SQLDBHelper(Context context) {
        super(context, "appdb", null, 3);
    }

実際にはバージョン番号はアプリケーション全体で定数を管理しているクラスの中でfinal static intで定義されていますが、こんな感じでSQLDBHelperを使う側はDBバージョンを意識する必要がなくなっています。

さいごに

この方法でDBバージョン Nに対して、N-1との差分を記述していって、場合によってはデータメンテナンスも行ないます。

まぁ初期化した方が楽だったりもするんですが、ユーザーデータをできるだけ開発側の理由で、消したくないという思いもあるので、こういう方法を採用しています。

あんまりこういう事が検索に引っかからないのは、もっと良い方法があるからなのか、かなり気になっています。 なにか理由があるのかなぁ…。

2012/06/27

CursorLoaderとAsyncTaskLaoderを使ったアプリのソースコードを公開してみた

最初は「Android 3.0以前でのFragmentとLoaderによるプログラミングのすゝめ」というタイトルにしたのですが、説明的すぎたので止めました。

さて、本屋さんでは、あんざい ゆきさんの本などを除いて、初心者向けのAndroid本のほとんどで、FragmentやLoaderについての記述がある本をみることはありません。

初心者向け本が備えるべき要件をここで書くつもりはないので、何が初心者向けなのかは議論の余地があると思いますが、網羅的な本を読むよりは、多少偏ってもアプリケーションを一つ作って、その周辺知識を増やしていく方が学習方法としては効果的じゃないかなと考えています。

その点では良い土台が必要なんじゃないかなと思うわけですが、そのうちAndroid 2.3を対象にしたアプリケーションでもFragmentやLoaderを使ったプログラミングを推奨する「やり直し本」が出てくるんじゃないかなと期待しているんですが、どうするべきなんでしょうね。

Android 1.6の頃はThreadを生成して、Handlerオブジェクトに処理を実行させるコードを書いた事もあったかもしれませんが、アプリケーションを見通し良く開発していくためには、新しい方法を取り入れて進んでいくしかないと思っています。

互換ライブラリを使ったアプリケーションの開発

CursorLoaderはContentProviderに対するwrapperのように使えば良いのですが、そういう説明をみなかったので実際にアプリケーションを作ってみました。

アプリケーションのベースは以前作成した「日本の郵便番号検索 Free」で、郵便番号の3桁+4桁の数字のみを入力するようにして、内部のコードはシンプルに保っています。

作成したアプリケーションのスクリーンショット

内部構造や使う仕組みは一部は流用していますが、ほぼ新規に作成しています。 ログの出力用メソッドに対する工夫など、役に立ちそうな部分は極力反映したつもりです。

このアプリケーション「Yamaneko」のソースコードはApache License 2.0でGithubにて公開してます。

アプリケーションの導入や内部構造について

使うためのEclipseの操作方法はスクリーンショットを交えて説明しているので、内部構造の説明を含めてgithubのwikiを参照して下さい。

データフロー概要

実際にアプリケーションを作って感じたこと

「動けばいい」というのではなくて、内部構造をシンプルに保ちつつ見通しのよいコードを書くために、普通はフレームワークを開発します。

Android 3.0やCompatibility Packageで提供されている新しいクラスは、そういう新しい(抽象度は低めかもしれませんが)フレームワークに相当する処理で、開発元が公式に提供しているものとなります。

その点では独自になにか仕組みを作るよりは、馴染むまでに時間がかかるかもしれませんが、できるだけ使った方が良いスタイルを強制できる事になります。

初心者向けの雛型アプリケーション

GNU公式のGNU Helloアプリのソースコードを雛型に開発をしようとして、その完成度の高さのあまりに挫折した人も多いのではないでしょうか。

まぁ、GNU Helloはほとんどネタですけれど、Androidでも似たようなアプリケーションが必要なのかもしれません。

本屋さんでAndroidの初心者向け開発本を眺めた印象では、初心者向けの良い雛型アプリケーションを提供する本がないなぁという印象を受けました。

内部構造を知るためにAndroidのframeworks/base/coreディレクトリにあるソースコードを見るのは普通だと思いますが、アプリケーションを開発するための土台としては、何か参考になるか聞かれて「GNU Helloみてみたら」みたいな定番があった方が便利じゃないでしょうか。

ApiDemoアプリも良いんですけれど、単品の機能だけなので、連携が分からないところがネックかなと思っています。

非同期処理のための他の方法との比較

メインUIやContentProvider, Serviceプロセスをブロックしないためには、HandlerやAsyncTaskクラスを使えば、AsyncTaskLoaderを使う必要は必ずしもありません。

仕組なくともProgressViewでアップデートするためにはAsyncTaskLoaderは適切ではなくて、AsyncTaskクラスの方が便利でしょう。

今回、CusrorLoaderとAsyncTaskLoaderを使った印象は、自前でAsyncTaskを管理してContentProviderの内部からタスクを起動するよりも、AsyncTaskLoaderでネットワークアクセスのみを扱い、ListViewへの出力はCursorLoaderと平行に起動できるのは内部構造としては見通しが良く、管理もしやすかったです。

外部のWebサービスから結果を取得して表示するアプリケーションでは、今回の構造がいまのところベストかなと思います。

表示するデータをContentProviderに管理させる構成について

直感的には自分のアプリケーションで表示するデータをDBに入れるだけではなくて、そのためにContentProviderのサブクラスを作成して、AndroidManifest.xmlに登録するというのは冗長に感じます。

とはいえ、twitterのタイムラインのように、ListViewやGridViewに表示する項目数が想定できないような場面では、データベースを中継してCursorオブジェクトでListViewやGridViewに表示する方法がベストです。

多少面倒でも、SimpleCursorAdapterとCursorLoaderを組み合せるのは良い方法だと思います。 パフォーマンスが懸念される場面でListAdapterを使うというのは、いまのところ納得していません。

また、副次効果としてSQLiteDatabaseオブジェクトをContentProviderクラスだけが扱うことになるのは良い構造だと思います。

さいごに

自分のコードが最善だとは到底思えないので、アプリケーションのコードを公開するのは気が重いです。

Androidアプリのソースコードは、あまりみないので、公開しようかなと思ったのですが、 Apache License 2.0やGPLv3などのライセンス別にまとめるサイトがあってもいいかもしれません。

いろいろ叩かれるとしても、それを乗り越えられるなら、コードを隠しても良い事はほとんどないですからね。

次に勤める会社があるなら、その会社との契約に縛られない限りは、 まとまった処理単位での動きが確認できるアプリケーションのコードはこれからも公開していきます。

2012/06/23

Xperia Mini Pro (SK17i)をIce Cream Sandwich (ICS) 4.0.4にしてみた

HVGA(320x480)サイズの開発機として海外から購入したXperia Mini Pro (SK17i)のOSをIce Cream Sandwich (ICS) 4.0.4に変更しました。

これで手元に実機環境は画面サイズがHVGA, WVGA, WXGA, OSが2.3, 3.2, 4.0と、だいたいメジャー所が揃った事になります。

お金があれば未使用中古品のGalaxy Nexusも欲しいんですけどね。 SC-04Dはちゃんと技適の通った端末ですし、b-mobileの各種SIMカードも使えるはずですし。

twitterやメールの読み書きぐらいはHVGA端末でまったく問題ないですが、PDFビューアーとして使うなら一辺1024ピクセル以上は欲しいです。960x640なiPod Touchの画面は3.5"あって綺麗ですが、PDFファイルを見ようという気にはなりませんから。

さて、今回はICSにアップデートして気がついた点を書いていきます。Xperia固有の事もありますが、ICSの特徴と区別していません。

【2012年6月30日追記】
ここでの対象はソニーが配布しているオフィシャルなICSについてです。 ROMを書き換える方法の方が対応が早いので、検索エンジンにもよくヒットするとは思いますけれど…。
携帯端末についていえば、ROMを書き換えるような方法は一般にお勧めできる方法ではありません。

【2012/6/30追記】アップデートの方法について

設定画面のアップデートの確認をしてもICSへのアップグレードはできません。事前にユーティリティをインストールしたWindowsか、Macに接続する必要があります。

更新情報は自動的に通知されるので、パソコンを使えば特に問題なくICSへのアップグレードできるはずです。

内部ストレージの増加

正確に画面ダンプを持っているわけではないのですが、以前のブログを書いた時には空き容量が170MB程度でした。

ICSを導入した後は、内部ストレージ全体は420MBほどあって、使用領域が200MB、空き領域が220MB程度になっています。

スクリーンショットが標準機能に

電源ボタン長押しでメニューにスクリーンショットを取るためのボタンが表示されましたが、このボタンはなくなっています。 ICSからは標準機能として電源ボタンとボリュームのマイナスボタンの同時長押しでできるようになっています。

撮影したスクリーンショットはSDカード上のPitures/ScreenshotsフォルダにPNGファイルとして保存されています。

標準カメラ機能

購入直後は撮影した画像がサムネイルみたいで残念な感じだったのですが、ICSにアップデートしてからはちゃんと撮影できるようになっています。

開発者向けオプションの充実

ICSは標準で開発者向けオプションがいろいろ提供されているはず…、だと思っていたので不思議でもなんでもなかったのですが、Acer Iconia Tab A100では、この項目が極端に少ないといった情報があって、本当だとすればXperiaの項目はちゃんと揃っている方だと思います。

「アクティビティを保持しない」のオプションを使えば、検索結果をクリックして地図を表示するActivityに移動してから戻ってきたらActivityが再起動して画面がクリアされている、といったメモリが余っている開発機では発生しない現象の確認もできます。

Activityのライフサイクルをちゃんと理解するにも役立ちますし、個人的にはこのオプションが気に入っています。

まとめ

ICSにしたから特に動きが機敏になったという印象はありません。 ランチャーも引き続き使え、使い勝手も特に変化はありません。

とはいえ、タブを使っているアプリケーションではアイコンが消えてしまったり、いろいろ細かい点ではUIが違っています。

ICSになって劣っている点があるとは思えなくて、個人的にはユーザーは積極的にICSにアップデートして、開発者も追随するべきだと思います。これはアプリ開発者としての意見です。

不満がなければアップデートするなというのは、システム屋からの意見として、まっとうだと思いますけどね。

これでICSでの動作確認も実機でできるようになりました。 だからといって特に新しい機能「だけ」を使ったアプリを作るわけじゃないですけどね。

何か参考になるかと思って、本屋さんで「Android SDK 4対応」とか意味不明な売り文句の本を眺めてきたのですが、 フラグメントについてはまったく触れられていませんでした。互換パッケージについても同様です。

まぁ現状のAndroidは安定期とはとてもいえなくて、活発に変化が起こっているおもしろい時期でもあるし、変化に追従しなければいけない時期でもあります。この時期はいろいろな事がおこって楽しいですね。過去に正しかったものが、時間を経て決してベストとは呼べなくなったり、本当に大変ですけれど。

Android In-app Billingのサンプルを自前アプリに組み込んでみた

AndroidのIn-app Billingは、無料アプリケーションの中で課金アイテムを販売するための仕組みです。 Application Licensingが有料アプリケーションのみを対象としているのと対照的です。

無料と有料とを別々に販売する方法は、アプリ内広告のためのライブラリを省くなどなど、アプリケーションサイズを小さくすることなどはできますが、その反面、コードのメンテナンスやテストはいろいろと面倒だったりします。

無料アプリをそのままにして、有料アプリを頻繁にバージョンアップさせる方法もありますが、 あまり良いインタフェースではなさそうに思えたので、今回は単一のアプリケーションイメージを無料で配布して、 その中で一部機能のロックを解除するツールを販売する方法にしてみました。

このIn-app Billingのサンプルアプリケーションを動かすのも、Androidプログラミング自体が始めての場合には いろいろと面倒そうですが、手順はいろいろ出回っていて、そもそもDev Guideの手順が一番充実していたりするので、 今回はこのサンプルのコードを自分のアプリケーションに組み込んだ時のログを残す事にしました。

Dungeonsアプリについて

Android Dev. Guideに従って作業を進めると、Google PlayにAPKファイルをアップロードするところまでは簡単に進むと思います。

アップロードしてからすぐに課金アイテムの作成などを行なう事ができるようになりますが、 アプリケーションから正しく処理ができるようになるまでは30分ほどかかりました。

うまく動かない場合には、アイテムを増やしたりするよりも、しばらく時間を置くのが良さそうです。

非署名アプリからの購入テスト

一度署名アプリをGoogle Playに登録して、課金アイテムを登録すると、emacsからアプリを起動した時のように署名をしていないapkから起動したアプリも正常に動き、"android.test.purchased"の購入などのテストができるようになります。

作業の進め方 (方針)

Dungeonsアプリを下敷に、自分のアプリケーションからアイテムの購入ができるようにする方法を考えます。

今回はDungeonsをライブラリに変更せずに、自作アプリケーション配下のパッケージに導入します。

ManagedアイテムとUnManagedアイテム

一回購入すると購入者情報と紐付いて、アプリケーションをアンインストールしようが購入履歴がGoogleに保存されて、再インストールしたアプリにも引き継がれる、それがManagedアイテムです。

よくよくアプリ課金で問題になるコインや武器といった繰り返し購入可能なアイテムはUnmanagedと呼ばれていますが、今回は対象としては考えません。

課金(Billing)機能の組み込みについて

基本的にはDungeonsアプリのDungeons.javaファイルを除いて、全てのファイルを自分のアプリケーションにコピーします。

Eclipse上のPackage Explorerでは次の画像のように、自分のアプリケーションのパッケージの中にbillingサブパッケージを追加して全てのクラスをコピーしてきました。

Eclipse上のPackage Explorerの様子

この他にsrcフォルダに "com.android.vending.billing" パッケージを作成し、IMarketBillingService.aidl ファイルをコピーしておきます。

また"com.example.dungeons.util"パッケージのBase64.javaとBase64DecoderException.javaもコピーしてきます。

参照関係はEclipseのエラーを確認しながら修正していきます。

Dungeonsアプリとの差分について

参照関係を解決しても、Dungeonsアプリ固有のUIを操作している部分は削除する事になります。

パッケージ名やコメント内の重要ではない部分を省いた差分は、概ね次のようになります。

BillingService.java

-    class RequestPurchase extends BillingRequest {
+    public class RequestPurchase extends BillingRequest {

-    class RestoreTransactions extends BillingRequest {
+    public class RestoreTransactions extends BillingRequest {

Security.javaの差分 (セキュリティ上の理由から省略)

-            String base64EncodedPublicKey = "...";
+            String base64EncodedPublicKey = "...";

RequestPurchaseをpublicに変更したのはパッケージnet.yadiary.android.exifpc.billingの外部からアクセスする必要があるからです。

Dungeonsでは単一のパッケージの中に含まれるので意識しない部分ですが、それを除いてもかなり使い周せるように考えられて作られていると思います。

自分のアプリからbillingパッケージにコピーしたクラスを使う

いよいよメインの作業に入っていきますが、Dungeons.javaを参考にしていきます。

購入アイテムのリストを作成する

クラス変数としてCATALOGのエントリを作成します。 今回のアプリでは画面に表示する名称は別途管理するので、このCatalogEntryの第二引数は実際には使っていません。

mBillingServiceインスタンスは外部から操作する必要があるのでアクセスできるよう、getterのみ定義しています。

MainActivityのライセンス関連変数


        /*
	 * ライセンス用設定
	 */
	private static final String TAG = "ExifPM";
	private static final String DB_INITIALIZED = "db_initialized";

	private Handler mHandler;
	private BillingService mBillingService;
	public BillingService getBillingService() {
		return mBillingService;
	}
	private ExifPMPurchaseObserver mExifPMPurchaseObserver;

	/**
	 * カタログ情報はここにまとめ、各Fragmentが参照する
	 */
	public static final CatalogEntry[] CATALOG = new CatalogEntry[] {
			// primary selling tools
			new CatalogEntry("exifpm_purchases_item", R.string.billing_item_hiddenads, CatalogEntry.Managed.MANAGED),
			// debug items
			new CatalogEntry("android.test.purchased", R.string.billing_item_hiddenads, CatalogEntry.Managed.MANAGED),
			new CatalogEntry("android.test.canceled", R.string.billing_item_hiddenads, CatalogEntry.Managed.MANAGED),
			new CatalogEntry("android.test.refunded", R.string.billing_item_hiddenads, CatalogEntry.Managed.MANAGED),
	// please add other stuffs under this line.
	};
	private PurchaseDatabase mPurchaseDatabase;
onCreateメソッドでのライセンス関連設定

onCreateメソッドでのライセンス関連設定


                mHandler = new Handler();
		mExifPMPurchaseObserver = new ExifPMPurchaseObserver(mHandler);
		mBillingService = new BillingService();
		mBillingService.setContext(this);
		ResponseHandler.register(mExifPMPurchaseObserver);
		if (mBillingService.checkBillingSupported(null) == false) {
                        // 必要に応じてエラーメッセージを表示する
			Toast.makeText(this, R.string.billing_toast_notsupported, Toast.LENGTH_LONG).show();
		}

最後のToast文は課金がサポートされていない事を通知するためのメッセージです。 Emulatorなどで実行すると、ここでメッセージが表示されるはずです。

onDestroyでの後始末

課金サービスに限らずServiceに定義したインスタンスはunbindしないと、バックグラウンドで稼働してバッテリーを消費します。 定義されているだけではServiceは起動しませんが、BillingServiceのようにアクセスされ、起動したServiceは、いわゆるlong-runningプロセスとしてシステムが管理します。 GCが動くとか妄想は止めて、必ず停止するようにしましょう。

onDestroyメソッド全体


        @Override
	protected void onDestroy() {
		mBillingService.unbind();
		super.onDestroy();
	}
PurchaseObserverサブクラスの作成

Activityクラス内にPurchaseObserverのサブクラスを定義します。

ここでは変数として宣言されていた、mExifPMPurchaseObserverを作成していきます。 参考までに作成されたExifPMPurchaseObserver全体は次のようになっています。

ExifPMPurchaseObserverクラス全体 (一部省略)


	private class ExifPMPurchaseObserver extends PurchaseObserver {
		public ExifPMPurchaseObserver(Handler handler) {
			super(MainFragmentActivity.this, handler);
		}
		@Override
		public void onBillingSupported(boolean supported, String type) {
			if (type == null || type.equals(Consts.ITEM_TYPE_INAPP)) {
				if (supported) {
					restoreDatabase();
				}
			} else if (type.equals(Consts.ITEM_TYPE_SUBSCRIPTION)) {
				// This type is not essential of this application
			} else {
				// not supported state, do nohing.
			}
		}
		/**
		 * キャンセル、リファンド通知はこのメソッドのpurchaseStateで判断する
		 */
		@Override
		public void onPurchaseStateChange(PurchaseState purchaseState, String itemId, int quantity, long purchaseTime, String developerPayload) {
			if (purchaseState == PurchaseState.PURCHASED) {
				if (itemId.equals(CATALOG[0].sku) || itemId.equals(CATALOG[1].sku)) {
					if (adView != null) {
						adView.setVisibility(View.GONE);
					}
				}
			} else if (purchaseState == PurchaseState.CANCELED) {
			        // do nothing
			} else {
				// refunded state
				if (itemId.equals(CATALOG[0].sku) || itemId.equals(CATALOG[2].sku) || itemId.equals(CATALOG[3].sku)) {
					AppConfig.isFreeEdition = false;
				}
			}
		}

		@Override
		public void onRequestPurchaseResponse(RequestPurchase request, ResponseCode responseCode) {
			if (responseCode == ResponseCode.RESULT_OK) {
				if (Consts.DEBUG) {
					Log.i(TAG, "purchase was successfully sent to server");
				}
			} else if (responseCode == ResponseCode.RESULT_USER_CANCELED) {
				if (Consts.DEBUG) {
					Log.i(TAG, "user canceled purchase");
				}
			} else {
				if (Consts.DEBUG) {
					Log.i(TAG, "purchase failed");
				}
			}
		}

		@Override
		public void onRestoreTransactionsResponse(RestoreTransactions request, ResponseCode responseCode) {
			AppConfig.sendMessage("called with ResponseCode=" + responseCode);
			if (responseCode == ResponseCode.RESULT_OK) {
				if (Consts.DEBUG) {
					Log.d(TAG, "completed RestoreTransactions request");
				}
				// Update the shared preferences so that we don't perform
				// a RestoreTransactions again.
				SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
				SharedPreferences.Editor edit = prefs.edit();
				edit.putBoolean(DB_INITIALIZED, true);
				edit.commit();
			} else {
				if (Consts.DEBUG) {
					Log.d(TAG, "RestoreTransactions error: " + responseCode);
				}
			}
		}
	}

ここではコンストラクタを除くと4つのメソッドが定義されています。 全てのメソッドの基本的な構造はDungeonsクラスから、そのまま引き継いでいます。

前半のonBillingSupportedとonPurchaseStateChangeは

後半のonRequestPurchaseResponseとonRestoreTransactionsResponseはアプリケーションの動きに応じて、例えばリセットされたアプリケーション起動時にステータスを回復したタイミングで「購入情報を更新中です」みたいなメッセージを表示する事ができます。

restoreDatabaseの動き

バッサリ省略したrestoreDatabaseメソッドは、キャッシュクリアされたアプリを起動した場合などに、購入済み情報を取得するためのメソッドです。

実際の取得処理はmBillingServiceのrestoreTransactionメソッドを呼び出します。

実際の購入処理

このActivityの管理下にあるFragmentから実際の購入処理を呼び出す事になります。

ボタンなどをクリックした時にActivityのBillingServiceインスタンスに対して、 requestPurchaseメッセージを送信します。

この処理は簡単なので省略します。

まとめ

課金処理を追加する事自体でアプリケーションコードは、ほとんど増えませんし、パーミッションも明示的なcom.android.vending.BILLING 1つだけなので、良いソリューションだと思います。

反面、課金処理は簡単ですが、ゲームなんかでコインを購入させるのは、どうかなぁと思います。

単純に時間短縮のためにコインを購入オプションがあって、時間さえかければ先に進めるようなものは良いのですが、ゲームバランスが極端に悪いものは遊んでいてあまりおもしろいとは思いません。

じゃぁなんで課金機能を追加したのかと言われれば、日本の法律では寄付の受付は禁止ですから、アプリケーションを気に入ってもらったり、このブログが参考になったりした場合に、代りにアイテムを購入して頂ければ幸いです。

2012/06/22

HVGA,タブレット両対応の郵便番号アプリをViewPagerとFragmentで作りかえてみ た

以前作成した郵便番号アプリはSlidingDrawerを使って同一レイアウトXMLに全てのViewを記述しつつ、 入力部分の描画を切り替えるようにしていました。 タブレットではSlidingDrawerを使わずにViewを配置する事で、画面サイズの違いに対応していました。

今回はViewPagerを使う事で、SlidingDrawerのツメ部分の描画が省略されるなど、若干シンプルになりました。 タブレットではViewPagerを使わずにレイアウトXMLを記述しています。 まったくの同一コードという分けにはいかないので、MainActivity側でViewPagerインスタンスが入手できない事を検出して、Fragmentへのアクセス方法を切り分ける事で、同一コードで対応しました。

今回はViewPagerならではの部分についてまとめていきます。

ViewPagerを使用した画面イメージ Tablet用画面イメージ

対応するバージョン、使用したAPI等々

今回のアプリケーションはAndroid 1.6以降、Android 4.0までをターゲットにしています。 検証のために次のような機器を使用しています。

  • Acer Liquid MT (2.3 800x480)
  • Iconia Tab A500 (3.2 1280x800)
  • Sony Xperia Mini Pro (4.0 320x480)

内部ではFragmentを使うために、android.support.v4パッケージを使用しています。 参考までにActivityクラスのimport文は、次のようになっています。

Activityクラスのimport文抜粋

import net.yadiary.android.actionbarcompat.ActionBarActivity;
...
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v4.widget.CursorAdapter;
...

ViewPagerがある場合とない場合の切り替え方法

レイアウトXMLにViewPagerの記述があれば、findViewById(R.id.pager)のようなメソッドでインスタンスが取得できるはずです。

課題はFragmentのインスタンスにどのようにアクセスするのかという事です。 通常はFragmentManagerインスタンスを経由して、Fragmentにアクセスしますが、 ViewPagerが設定するFragment Tag名は外部からは(一応)分かりません。

Activity(実際にはFragmentActivityをベースにしたActionBarActivityの子クラス)中のコードは次のようになっています。

onCreateメソッドの抜粋

	protected void onCreate(Bundle state) {
		super.onCreate(state);
		setContentView(R.layout.main);
		viewPager = (ViewPager) findViewById(R.id.pager);
		if (viewPager != null) {
			myAdapter = new MyAdapter(getSupportFragmentManager());
			viewPager.setAdapter(myAdapter);
		}

viewPagerの中に配置するFragmentはMyAdapterクラスのgetItem(int position)メソッドの中でインスタンスを生成しています。 ここら辺はオフィシャルのFragmentPagerAdapterリファレンスを参照してください。

FragmentにアクセスするためのサポートメソッドをActivity中に追加しています。 ViewPagerを使う場合は、アダプターのinstantiateItem(viewPager, position)を使用しています。

FragmentはレイアウトXMLで記述したのでR.id経由で指定していますが、 ViewPagerのインスタンスがnullの場合に、必要な場面は想像できませんが、動的にFragmentを定義する事もできます。

Activityクラスに追加したFragment取得用サポートメソッド

	public Fragment getFragment(int position) {
		Fragment ret = null;
		if (viewPager != null) {
			ret = (Fragment) myAdapter.instantiateItem(viewPager, position);
		} else {
			FragmentManager fm = getSupportFragmentManager();
			if (position == PAGER_PAGE_INPUT) {
				ret = fm.findFragmentById(R.id.fragmentInput);
			} else {
				ret = fm.findFragmentById(R.id.fragmentListView);
			}
		}
		return ret;
	}
レイアウトXML中でのViewPager, Fragmentの記述方法

android.support.v4パッケージを使っている事で、レイアウトXMLの具体的な書き方は、Android 3.0以降に対応したものとは少し変わっています。

ここら辺の書き方で困る場合もありそうなので、該当個所の抜粋だけ載せておきます。

android.support.v4のViewPager, FragmentレイアウトXML記述例

...
    <android.support.v4.view.ViewPager
        android:id="@+id/pager"
        android:layout_width="match_parent"
        android:layout_height="0px"
        android:layout_weight="0.9" >
    </android.support.v4.view.ViewPager>

...

        <fragment
            android:id="@+id/fragmentInput"
            android:name="net.yadiary.android.jpostal.InputFragment"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
        </fragment>

...

要素名をパッケージで指定したり、大文字が小文字だったり、知っていれば何でもないんですけどね。

まとめ

ViewPagerを使う事で画面はシンプルになりましたが、ActionBarを使っているので20〜30ピクセルほどは以前よりも画面を占有するようになりました。

とはいえ、Fragmentに分けた事で内部構造は分割統治が可能になりシンプルになりました。

以前のコードはサポートクラスに処理を切り出したりはしていましたが、ステータス管理の面からは巨大なActivityクラスでした。 android.support.v4パッケージとFragmentを使う事で、互換性を維持しつつ、よりレイアウトXMLを中心としたアプリケーション開発ができるでしょう。

正直なところFragmentを始める前は、解説書を読んでもどういう風に処理を分けたらいいのか、イメージを持つ事が難しかったです。

まずは新規で簡単なアプリを作って慣れてから、古いアプリのActivityをFragment対応にする場合には、レイアウトXMLに対応する新しいFragmentクラスを作って、Activityの処理をFragmentクラスに移すようにするべきでしょう。

いろいろなデバイスに対応するのは面倒ですが、参考になれば幸いです。

2012/06/20

ROM 512MBと表記されたAndroid端末の空きストレージ

expansys香港から今年に入って、Acer Liquid MT ()Sony Ericsson Xperia Mini Pro (SK17i)の2台を購入しました。

どちらもRAM 512MB, ROM 512MBとスペックには記載されていますが、 導入できるアプリケーションの量には、けっこうな違いがあります。

今回はいわゆるキャリア携帯のお話ではなく、SIMフリーなAndroid端末のお話しです。 キャリア携帯ではプリインストールアプリが多く含まれているとは思いますが、 今回はそういう日本国内の事情は含まれていません。

両製品の印象

購入したのはどちらも1,2000円程度(に値下がりした後)で、2012年2月にLiquidMTを購入し、6月にXperia Liquid MTを購入しています。 どちらの製品も発売当初にいろいろなサイトで取り上げられているので、スペックや印象は他のサイトを参考にしてください。

どちらの製品も液晶の品質や画面サイズについてみれば、同時期のより上位の製品に見劣りするかもしれません。 しかしCPU, GPUのパフォーマンスは他の製品と同等で、アプリケーションの動き自体はキビキビしています。

内蔵ディスク領域の空き容量

アプリケーションをいろいろいれてみようとしたのですが、LiquidMTとXperia Mini Proでは基本的な空き領域に差があります。

正確な数字ではありませんが、設定画面のStorage SettingsからInternal storageをみると、だいたい次のような数字だったと記憶しています。

  • Acer Liquid MT: 空き70MB前後
  • Xperia Mini Pro: 空き領域200MB前後

ちなみにRAMについては、LiquidMTが常時200MB以上の空き領域があるのに対して、 Xperia Miniは170MB程度となっています。 Androidのアプリケーション1つが消費するメモリ量は多くても30MB前後でしょうから、どちらも足りないというほどではありません。

常駐プロセスを必要とするアプリケーションをいくつも立ち上げるなら別ですが、 メール、スケジュール、Twitter/Facebook, ときどきWeb閲覧というぐらいの自分の使い方の範囲では どちらも問題なく使えています。

Andoird端末が使うStorage領域とSDカードの関係

新規でインストールするアプリケーションは全部SDカード上に入れておきたいのですが、 アプリケーション開発者が意図的にSDカードを使うように作成しておく必要があります。 そういった設定がされていないアプリケーションはInternal Storageを消費する事になります。

また、いろいろ保証もないのでroot化を考えない事にすると、プレインストールされているアプリケーションは削除できないROM部分に配置されています。

プレインストールアプリのアップデートもまたInternal Storageに格納される事になるので、空き領域のサイズは意外と重要です。

データについていえば、タブレットや一部(HoneyComb以降など)の端末を除けば、SDカードは/mnt/sdcardなどの領域にマウントされて、内蔵カメラアプリで撮影した画像などは自動的にSDカード上のDCIM/Cameraなどに保存されるので、 これについては問題になる事はないでしょう。

結局のところ、アプリケーションは導入も更新もInternal Storage領域を使うものがあるので、 この部分の空きが致命的に少ないLiquidMTに導入できるアプリケーションの量や種類は限られてしまいます。

Acer LiquidMTを4ヶ月使ってみて

LiquidMTでもDocs To Goのアップデートやゲームなどのアプリをむやみに入れなければ、問題なく使えています。 とはいえGMailなどのプレインストールアプリが多い分、Internal Storageを消費していて、空きは22MBほどになっています。

また、主にTwitter/Facebook端末として使ってきましたが、短文とはいえソフトウェアキーボードはフィードバックが少なくて使いづらいというのが結論です。

今回は、この方法を推し進めてハードウェアキーボードのあるXperia Mini Proを購入してみました。

いまのところはちょっとしたID/パスワード入力なんかでも便利さを実感しているところです。 まだ目新しくてテンションが高いだけかもしれませんけれど。

さいごに

製品スペックや紹介サイトの記事をみていても、プレインストールアプリの数や量などは使ってみないとはっきり分からないところがあります。

とはいえ現在のフラッグシップはInternal Storageが16GB, 32GBは当たり前で、むしろSDカードが使えない端末もあります。 これから1,2年の内にSDカードを使わないような端末が普及しても不思議ではない状況で、 内蔵ストレージの枯渇問題は一過性のものなのかもしれません。

またroot化を前提にしている方には、あまり関係のない話題なのかなとも思います。

これから求められるアプリケーションのUIや使い方はどんな事になるのか、iPhoneはiCloudとの連携が中心になるでしょうし、とりあえずAndroidはGoogle Driveを前提にしてドキュメント管理や、Audio/Movieが混在したコンテツの活用について考えた方がよさそうです。

いままでは効率化が叫ばれてきましたが、そろそろAndroid版富豪的プログラミングスタイルが流行るのかもしれません。

2012/06/18

ActionBarCompatをライブラリ化して自分のアプリに組み込んでみる

[2012年6月29日追記] Google IO 2012でActionBarの互換ライブラリがリリースされる予定だとの発表がありました。 この内容を利用する前に、公式の互換ライブラリがリリースされていないかご確認下さい。

自作アプリケーションからActionBarを使おうとした場合には、API Level 11(Android 3.0)以降でないと対応していません。 不特定多数に配布するアプリケーションの作成する際には、普通はAndroid 1.6か2.1以降、どんなに悪くても2.3以降の対応として、まだAPI Level 11以降のみをターゲットにしたアプリケーションを作成する機会は少ないのではないでしょうか。

Android 3.0以前に対応したActionBarの実装について検索をすると、SDK以下のsamples/android-15/ActionBarCompat/にAPI Level 4(Android 1.6)以降に対応したActionBar互換アプリケーションがある事がわかります。

最初はioschedアプリもActionBarのような動きをするので参考にしようとしたのですが、面倒になったので改めて探してctionBarCompatアプリに辿りついたのでした。

このアプリケーションを全面的にコピーする方法は試されているようでしたが、 便利そうだったので、これからいくつかのプロジェクトで使う機会もあるだろうと思ったので、ライブラリとして自分のアプリケーションから参照を追加するようにしました。

作業環境

基本的にはv4サポートライブラリを使って、Android 2.3端末のAcer Liquid MTの実機で確認していきます。

Android 3.2(Honeycomb)環境としては、Acer Iconia Tab A500を使って確認します。

Android 4.0(ICE)環境は、Xperia Mini Proを入手しようとしていますが、いまのところはエミュレーターで行ないます。

ActionBarCompatをライブラリ化する流れ

作業ステップはおおまかに次のようになります。

  • ActionBarCompatサンプルから、自分のworkspace以下に新しいプロジェクトを作成
  • プロジェクトのパッケージ名を変更
  • srcフォルダのパッケージ名をプロジェクトのパッケージ名に変更
  • MainActivityなどの不要なファイルを削除
  • (android.support.v4.app.Fragmentを使う場合のみ) ActionBarActivityクラスをFragmentActivityのサブクラスに
  • プロジェクトのプロパティからAndroid欄の"is library"にチェックをつけライブラリプロジェクトに変更
  • 自作のアプリケーションのプロパティからライブラリに追加
  • 自作アプリケーションのActivityをActionBarActivityのサブクラスに変更
  • 自作アプリケーションに見栄えに合せたActionBarの背景色などの変更

The ActionBar Result Image

ActionBarCompatサンプルから、自分のworkspace以下に新しいプロジェクトを作成

新規プロジェクトの作成する時にはオプションの中からCreate project from existing sampleを選択すると、サンプルプロジェクトのコピーをデフォルトのworkspace以下に作成する事ができます。

ActionBarCompatサンプルはAPI Level 15 (Android 4.0.3)を選択すると表示されます。

プロジェクトのパッケージ名を変更

このままではパッケージ名がcom.exampleeから始まってしまうので、自分のドメインに変更してしまいます。

この作業は、まずプロジェクトフォルダを右クリックして、Android Toolsの中にある"Rename Application Package"メニューを選択して行ないます。

この時に、次のステップにあるsrcフォルダの中にあるパッケージ名を先に変更してしまうとEclipseが競合を解決できなくなるので注意してください。

srcフォルダのパッケージ名をプロジェクトのパッケージ名に変更

おなじみのsrcフォルダ直下にあるパッケージフォルダを右クリックして"Refactor"から"Rename"を選択します。

この時入力するパッケージ名は先ほど行なったApplication Packageの名前と同じにしておきます。

MainActivityなどの不要なファイルを削除

この時点でMainActivityを選択して実行すると、サンプルを動かすことができるはずです。 問題なくビルドできているプロジェクトをライブラリプロジェクトにしていきます。

resフォルダの中にあるlayout/main.xml, menu/main.xmlは不要なので削除しておくか、自作アプリケーションのプロジェクトに移動するなどして、ActionBarCompat以下には配置しないようにします。

また2つのmain.xmlとMainActivity.javaを削除すると、values/strings.xmlの内容もほとんど不要になります。

values/strings.xmlからapp_nameを残して、他のエントリを削除します。

(android.support.v4.app.Fragmentを使う場合のみ) ActionBarActivityクラスをFragmentActivityのサブクラスに

互換パッケージのFragmentを使う場合には、ActivityのサブクラスからはFragmentManagerのインスタンスにアクセスできないので、FragmentActivityを使用します。

この場合には、ActionBarActivityクラスの親クラスをActivityからandroid.support.v4.app.FragmentActivityに変更します。

あるいはActionBarActivityクラスファイルをコピーして、ActionBarFragmentActivityのようなクラス名にした上で、FragmentActivityの子クラスとしてもいいかもしれません。

将来的にActivityとFragmentActivityを使い分ける事があるのなら、こちらの方がお勧めです。

ActionBarActivity変更個所の抜粋

public abstract class ActionBarActivity extends FragmentActivity {
    final ActionBarHelper mActionBarHelper = ActionBarHelper.createInstance(this);
プロジェクトのプロパティからAndroid欄の"is Library"にチェックをつけライブラリプロジェクトに変更

ここまで作業を進めて、エラーがなければライブラリにして、他のプロジェクトから参照できるようにします。

is Libraryをセットした画面キャプチャ

自作のアプリケーションのプロパティからライブラリに追加

ここまできて、自作アプリケーションのプロジェクトフォルダに移動します。

プロパティから参照するライブラリにActionBarCompatを指定します。

Libraryを追加した画面キャプチャ

自作アプリケーションのActivityをActionBarActivityのサブクラスに変更

extends Activityextends FragmentActivityと書かれているところを、extends ActionBarActivityに変更します。

ActionBarCompatのvalues/menu/main.xmlを参考にして、通常のmenuリソースを作成して、ActionBarActivityのサブクラスでonCreateOptionsMenu(Menu)メソッドでメニューを構成します。

ここでndroid:showAsAction="always"が指定されたメニューはActionBarにアイコンが表示されます。 "never"を指定すれば、別途メニュー(Android 3.0以降ならContext Menu)に文字とアイコン付きで表示されます。

Menuの作成方法については、これぐらいの点を気にすれば、他は特に変わった点はありません。

自作アプリケーションに見栄えに合せたActionBarの背景色などの変更

メニューの作成方法は通常通りですが、この時点ではまだ、ActionBarらしくはみえません。 ここからさらに必要な作業をまとめると次のような項目があります。

  • 自作アプリケーションのAndroidManifest.xmlのMainActivityに ユニークなStyle名を設定
  • 自作アプリケーションのresフォルダに、values/styles.xmlを作成
  • ActionBarCompatプロジェクトからvalues-v11, values-v13フォルダを自作アプリケーションのresフォルダにコピー

カスタマイズの方法について

ここではタイトルバーの色と文字色を変更するまでの流れをまとめておきます。

テーマの有効化

まずはAndroidManifest.xmlに、ActionBarCompatのAppThemeとは違う名前のテーマを設定します。

@style/JPAppThemeを指定した例

<activity
  android:name=".MainActivity"
  android:label="@string/app_name"
  android:theme="@style/JPAppTheme" >
  <intent-filter>
      <action android:name="android.intent.action.MAIN" />
      <category android:name="android.intent.category.LAUNCHER" />
  </intent-filter>
</activity>

この変更をすると、全体が白色ベースのThemeに変更され、ActionBarも太く、それらしく表示されるはずです。

ActionBarの背景色と文字色を変更する

変更には、全体をコピーするのではなく、ActionBarCompatで定義されている@style/AppThemeをparentとして差分を指定して、必要な個所だけを変更するようにします。

まずタイトル文字色を指定するリソースをvalues/colors.xmlに定義します。

values/colors.xml全体

<resources>
    <color name="jpactionbar_title_color">#efefef</color>
</resources>

次にAndroid 1.6〜2.3のアプリケーション用に背景色と文字色を変更します。

values/styles.xmlファイル全体

<resources>
    <style name="JPAppTheme" parent="@style/AppTheme">
        <item name="android:windowTitleBackgroundStyle">@style/JPActionBarCompat</item>
        <item name="actionbarCompatTitleStyle">@style/JPActionBarCompatTitle</item>
        <item name="android:textColor">@color/jpactionbar_title_color</item>
    </style>
    <style name="JPActionBarCompat" parent="@style/ActionBarCompat">
        <item name="android:background">#283255</item>
        <item name="android:textColor">@color/jpactionbar_title_color</item>
    </style>
    <style name="JPActionBarCompatTitle" parent="style/ActionBarCompatTitleBase">
        <item name="android:textColor">@color/jpactionbar_title_color</item>
    </style>
</resources>

ActionBarCompatのサンプルでは、parentにTheme.Lightを指定したいたので、色については白色地に黒色が基本になっています。

AndroidManifest.xmlで指定したThemeについては、parentを@style/AppThemeから継承するか、他のandroid:styleのThemeから継承するかはケース毎に違うと思います。

android:styleのThemeをparentに指定する場合には、ActionBarCompatのstyles.xmlを参考にしてください。

続いてvalues-v11/styles.xmlを編集します。

values-v11/styles.xmlファイル全体

<resources>
    <style name="JPAppTheme" parent="@style/AppTheme">
        <item name="android:actionBarStyle">@style/JPActionBar</item>
        <item name="android:textColor">@color/jpactionbar_title_color</item>
    </style>
    <style name="JPActionBar" parent="@style/ActionBar">
        <item name="android:background">#283255</item>
        <item name="android:textColor">@color/jpactionbar_title_color</item>
        <item name="android:titleTextStyle">@style/ActionBarTitle</item>
        <item name="android:icon">@drawable/icon</item>
    </style>
</resources>

こちらもvalues/styles.xmlと同じです。文字色を変更する指定が追加されています。

最後にvalues-v13/styles.xmlを編集します。 values-v11との差分だけを定義するので、内容は1項目だけです。

values-v11/styles.xmlファイル全体

<resources>
    <style name="JPActionBarTitle" parent="@style/ActionBarTitle">
        <item name="android:textColor">@color/jpactionbar_title_color</item>
    </style>
</resources>

これぐらいを指定すると、だいたいどのバージョンの端末でも正しく表示されると思いますが、 API Versionによってボタンの配色など、違うところがあるので、TextViewやButton用にStyleを定義するといった事は必要だと思います。

さいごに

細かい際を全て吸収するのは難しいですが、ActionBar自体はAndroid 2.3とAndroid 3.2の端末で同じようにみえています。

ViewPagerと組み合せて使っていますが、v4サポートのFragmentと組み合せて、 できるだけ快適な操作性を提供していきたいと思っています。

これまで郵便番号検索と、Exif情報を編集するExifPMアプリを作ってきましたが、郵便番号の方はFragment対応を進めていて、ActionBarを組み込む予定です。

2012/06/15

ADT18のproguard-project.txtで困ったところ

JPEG画像のExif位置情報の削除と編集が可能なツールExifPMを作成したのですが、 その署名アプリケーションを作成する時にproguard設定で、いくつかトラブルに遭遇しました。

ここで、その内容をまとめて今後の参考のためのメモを残しておく事にします。

システムの構成

この記事は次のシステム上での挙動について書かれています。 Windowsなど、他のシステム上では当てはまらない可能性もあるのでご注意ください。

  • OS: Ubuntu 12.04 LTS 64bit版
  • CPU: AMD Phenom(tm) II X4 905e Processor
  • Memory: 16GB
  • Android開発環境:NVidia Tegra Android Developer Pack 1.0r8 (最新版"Android SDK r19, ADT 18"更新済み)

症状

Eclipse上で正常に署名済みAPKファイルを作成したと思ったものの、デバイスにインストールしてみたら起動時にClassNotFoundExceptionが発生し、異常終了する問題が発生しました。 症状は次の通りです。

  • EclipseのLogCat上ではActivityクラスやContentProviderクラスが見つからないと表示される
  • 過去に正常に動いていた時の署名済みAPKファイルと比較して、100KB以上サイズが小さくなっている

署名アプリケーションはdebug時と比較すると、Proguardを使用する事によって、1MB以上あったAPKファイルのサイズが350KB程度に圧縮されています。 トラブルが発生したAPKファイルでは200〜300KB程度になっている事も症状の一つでした。

Clean...直後には正常にAPKファイルが生成されたのに、繰り返すとサイズの小さなAPKファイルが生成されてしまいます。

-dontwarnによってアプリケーションパッケージ以下から出る警告を全て無視しているため、 気がつかないうちに一部クラスが欠落したAPKファイルを作り出してしまっているのでしょう。

ADT 18でのproguard-project.txtファイルの取り扱い

これまでは各アプリケーション共通の設定とアプリケーション固有の設定を混ぜたproguard.cfgファイルを準備していましたが、ADT17からはシステム用設定とアプリケーション用設定を分けて管理する事になります。

システム全体の設定の中ではデフォルトで-optimizationsが無効化されているので、 必要な場合には有効にする必要があります。

proguardはClass.forNameでインスタンス化したり、Reflection APIを通じてクラスにアクセスする必要がある場合には、難読化によって指定するべきメソッド名や変数名が変化してしまいますから、少なくともpublicな部分は名称が変更されないようにする必要があります。

例えばAndroidManifest.xmlにはActivityやServiceのクラス名を書きますから、少なくとも ここに記載されているクラス名は難読化の対象から外す必要があります。

システム全体のproguard-project.txtフィルには、Activityとそのサブクラス名を難読化の対象から外すような設定が含まれています。

Tips

問題が発生した時に見直す点をまとめていきます。

署名APKファイルを作成する前の儀式

EclipseのProjectメニューの中で、Clean...を選択します。 Build Automaticallyにチェックを入れていない場合には、Build Projectを選択しておきます。

少なくともADT 18.0.0.v201203301601-306762を使っている現状では、同じ設定でも、Clean...を選択せずにAPKファイルを出力した場合、サイズが小さなり、正しく動かないAPKファイルが生成されています。

これについてはprojectの設定がどこか正しくない可能性が高いと思っていますが、 原因が追求できていないため、Google Playに揚げる署名APKファイルの生成時にはClean...を毎回選択してから作業を行なっています。

staticフィルドからnon-static enumを使用している場合

クラス内部でenumを定義した時に、次のようなコードを作成したところ、proguardがバグっぽい動きをしました。

修正前:問題のあったenum定義

...
public enum Orientation {
	PORTRAIT, LANDSCAPE
}
public static CommonConfig.Orientation orientation;
...

このコードはproguard以前では問題なく動いていましたが、次のように修正してからは問題なく動くようになりました。

修正後

...
public static enum Orientation {
	PORTRAIT, LANDSCAPE
}

このstaticなフィールドとして定義したい内部enum定義であれば、staticという定義は適切だと思います。 むしろ以前のコードで問題なく動いているところに少し違和感を持っています。

課金サービス(com.android.vending.BILLING)を使用している場合

システム全体のproguard-project.txtではライセンスサービス用のインタフェースは含まれていますが、 課金サービス用の設定は含まれていませんでした。

ひょっとしたら必要ないのかもしれませんが、基本的にgenディレクトリ以下に出力されるクラスは全てproguard-project.txtの中で対象から外すように設定するべきだと思います。

次のような設定を追加しました。

proguard-project.txtに追加したBilling用設定


-keep public class com.android.vending.billing.*
Google Maps APIを使用している場合のproguard-project.txt設定

mapsについてはproguardの対象にする必要がないと思っていたのですが、 手元では次のように設定しないと動きませんでした。

google maps関連のproguard-project.txt該当個所

-keep public class com.google.android.maps.**
AdMob用設定の追加

AdMob用に次のような設定を追加しています。

AdMob用proguard-project.txtの該当個所


-keep public class com.google.ads.** {
    public protected *;
}
-keep public class com.google.gson.** {
    public protected *;
}

現状のproguard-project.txtファイル全体

参考までに、現在使っているproguard-project.txtの内容を全てそのまま載せておきます。

現行proguard-project.txtの全体

# Add any project specific keep options here:

-keep public class com.google.android.maps.**
-keep class net.yadiary.android.exifpc.beans.*
-keep class net.yadiary.android.exifpc.provider.*

-keep public class com.google.ads.** {
    public protected *;
}
-keep public class com.google.gson.** {
    public protected *;
}

-keep public class com.android.vending.billing.*

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
-keepclassmembers class net.yadiary.android.exifpc.InfoFragment.DemoJavaScriptInterface {
   public *;
}

-dontwarn net.yadiary.android.exifpc.**

さいごに

いくつかシステム全体でContentProviderのサブクラスを指定しているのに、なぜか手元でも明示的にContentProviderのサブクラスを指定しないとうまく動かなかったりしています。

おそらくenumのstatic修飾子のように、適切な記述ができていない部分があるのではないかなと思います。

そのため、ここに書かれている内容はベストとは限りませんが、現状で正しく動く設定であるのも確かなので、そこら辺をふまえて参考にしてください。

2012/05/30

署名Androidアプリケーション作成時のWarning

Google PlayにAndroidアプリケーションを載せる前に、アプリケーションに署名を行ないますが、 Export Signed Application Package...を選択した時に、同一パッケージ内で can't find referenced classのワーニングが大量に発生して、パッケージを作成する事ができませんでした。

いろいろ調べると、proguard設定に-dontwarn設定を追加すれば良いと書かれていますが、 不必要に全てのワーニングを抑制するのが嫌だったのでproguardのサイトを眺めた時のメモです。

ワーニングメッセージの例

...
Proguard returned with error code 1. See console
Warning: com.example.android.MenuFragment$1: can't find referenced class com.example.android.TempActivity
Warning: com.example.android.ToolsFragment: can't find referenced class com.example.android.ImportAsyncFragment
Warning: com.example.android.ToolsImportFragment$1: can't find referenced class com.example.android.ImportAsyncFragment
...

EclipseのADTバージョンによるproguard設定の指定方法の違い

Eclipse 3.6ベースのADTを使っていた時には、project.propertiesにproguard.cfgファイルを指定していました。

...
target=android-4
proguard.config=proguard.cfg...
...

この時のproguard.cfgは必要な設定全体を含んでいましたが、 Android 3.7ベースのADT 18.0.0では、project.propertiesに次のような行が含まれています。

...
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
...

コメントの一部なのでまぎらわしいですが、proguard.configで始まる一行をコメントアウトして、システム全体で共通の設定ファイルを使うように変更されています。

自分のアプリケーション用の設定は、AndroidManifest.xmlなどと同じフォルダにproguard-project.txtを作成し、 差分だけを記述するように変更されています。

Proguard トラブルシューティング記事のまとめ

proguardサイトのトップページから"Manual"をクリックすると、左のメニューが変化して"Troubleshooting"ページにアクセスする事ができるようになります。

can't find referenced classで検索すると、3つの項目が並んでいました。 Eclipseを使ってAndroidアプリを開発している場合には、1,2はなさそうです。

選択肢1. 必要なjarファイルが指定されていない場合

指定漏れのあるjarファイルがあれば、-injarsオプションなどで適宜追加するように書かれています。 コマンドラインでビルドする場合には注意が必要でしょうが、Eclipse内でビルドが正常に完了して、Eclipseからproguardを起動していれば必要ないはずです。

選択肢2. アプリケーションの稼働に不要なクラスの参照関係が解決できない場合

ワーニングに指定されているクラスへの参照は必要ないから、解決できなくても動くよ、そんな場合にはfilter outの指定をするように書かれています。

これは1.との合わせ技で、-injarsで指定するjarファイルの後ろに無視する条件を指定する方法で、これもEclipseを使っていれば本来必要ないはずです。

選択肢3. filter outが気に入らなければ-ignorewarningsか-dontwarnを使えばいいよ

ほとんどのEclipseでAndroidアプリを開発している場合にあてはまるのが、このオプションを指定する場合だと思います。

StackOverflowなんかをみていても、-dontwarnを指定すればいいんじゃない?、というアドバイスが多かったです。

Troubleshootingページをみると、"-dontwarn java.awt.**" みたいな指定ができるので、 自分が作成した同一パッケージ内でのクラスの参照関係が解決できなくても無視するように次のような設定をしています。

現状のproguard-project.txt全体

-keepclassmembers class com.example.android.InfoFragment.DemoJavaScriptInterface {
   public *;
}
-dontwarn com.example.android.*

WebViewを使ってJavaScript用にオブジェクトを設定している場合には、そのクラスを指定するように書かれているので、その下に-dontwarn行を追加しました。

さいごに

署名済みAPKファイルは無事に生成できるようになりました。

別のprogurad.cfgの挙動が変化した分けじゃないんですが、ADTをアップグレードして空のプロジェクトを生成した時にproguard.cfgがなくなったりすると、そのまま昔のproguard.cfgをproguard-project.txちょっとした混乱が起きそうです。

Googleで解決策がみつかるのは良いんですが、そのまま信用して簡単な策を取ると、いろいろ問題がありそうですね。 まぁ趣味の範囲なら、なんでも良いんですけれど。

いろいろな事がボーダーレスになって、プロとアマチュアの境界があいまいになりつつあるのが、少し怖いです。 怖いのはアマチュアに侵食されるんじゃなくて、プロのはずがアマチュアに落ちる事なんですけどね。

2012/05/24

Fragmentを使ったAndroidアプリの作成

いままでの知識と新しい情報

Androidアプリを作っていましたが、画面の切り替えにTabを使おうとしたところで、 Deprecatedクラスを避けるためにFragmentを使うことにしました。

minSdkを低い値にしているとはいえ、TabHost, TabWidgetを使うために標準ライブラリに代替クラスは準備されずに、 Support Librariesを使わなくてはいけないというのは少しおかしな印象を持ちます。 いずれにしても、良い機会なのでFragmentについての資料を読み漁っているところです。

いろいろ新しい仕組みが整いつつあるので、これまでProviderによるNetwork MVCパターンを使ってきた非同期更新の仕組みをLoaderに切り替えたり、ProgressBarで逃げてきたAsyncTaskを管理する処理でViewを持たないFragmentを使うなど、実装をやり直す余地はありそうです。

ただ、いままでの方法は、これはこれで問題もないので、とりあえずはTab表示したいActivityと使い回しをしたいActivityをFragmentに置き換えたり、バックグランドで処理をするのが面倒だったためAsyncTask + ProgressBarで逃げていたところをViewなしFragmentにするところから始めようかなと考えています。

まぁ、ここにきて時間が少しある事もあって、あんざいゆきさんの著書やYoutube講演/資料なんかを中心にAndroid 3.x以降の変更点を調べています。

いままでPlatform 2.3.xまでの知識で、1画面1Activityなアプリを作成してきて満足していたので、Fragmentは面倒そうに感じて避けてきましたが、いまは積極的に使うべきだと思っています。

標準的な枠組みの中で、これまで各自が個別に対応していた処理が行なえるのは、チームで開発する場合などに威力を発揮するはずです。

Fragmentの良いところ

Fragmentを使う事の利点は、明示的にViewを生成して返すonCreateViewメソッドが準備されている事でしょうか。 小さな事ですが、全体の流れやメソッドに一貫性が生まれつつあるように感じています。

とはいえ、単純にActivityをFragmentに変換しただけでは、いろいろ細かいところで適当ではない処理を行なっているので、使いまわせる部品となるようにBundleオブジェクトなどでインタフェースを考えてやらないといけないところがいくつかあります。

これからFragmentPagerAdapterを使ったり、FragmentActivityとFragmentの呼び出し順などライフサイクルをちゃんと理解しないといけないので、しばらくは忙しくできそうです。

2012/05/21

Androidアプリケーション開発で感じたこと

最近は仕事を探していることもあってAndroidでのアプリケーション開発を学ぶ事に時間を費しています。 Eclipseも数年前に比べれば非常に成熟していて、パッケージのアップデートによる相互依存性を解決できずに起動できなくなるような状態には、ほぼならずに安定かつ快適な開発環境を提供してくれています。

Androidアプリケーションの開発に必要なハードルはパソコンと、アプリケーションをGoogle Playで公開するために開発者登録をするクレジットカードくらいで、アプリケーションを開発したいと思えば学生でも気軽に始められるような状況です。

そして開発に必要な情報もまたインターネットで入手する事ができ、StackOverflowなどから情報を引き出す事ができます。

とはいえ、Android 1.6の頃からの情報と3.0以降の情報が錯綜していて、古い情報や制作手法を解説しているページが多い点も気になりました。

情報の入手性と確からしさ

多くの開発者がその経験をまとめた記事をブログにまとめています。 そして、私も含めて、その情報のほとんどは、ある特定のテーマを解説する断片的なものです。

一部にはリファレンスの内容を網羅的に翻訳したり、その上で解説を加えているものもありますが、例外的です。

多くは、(当然のことですが)、特定の操作や目的に対する解決策を1つの記事としてまめています。 例えば、作成した画像ファイルをギャラリーアプリなどに反映させるために、「MediaStoreサービスにデータをinsert()しましょう」といった記事が作成されます。

しかし、同時に管理されている画像ファイルの属性を変更した場合に、アプリでその変更が反映されない事に気がつき、「MediaStoreからレコードを削除して、再度insert()しましょう」といった内容もまた記載されています。

これらは作者の体験として正しいものですし、実際に正しく動きます。 しかし目的を達成するためにはMediaStoreに対して、update()/notifyChange()メソッドを使用しても良いはずです。 システムに対する負荷は、より軽くなるでしょう。

情報としては間違っていないけれど、ベストではない、常に、そういう気がまえで検索エンジンの結果をみる事る事が必要だと、つくづく感じました。

情報の鮮度

GridViewやListViewのパフォーマンスチューニングのために、Googleで検索したいくつかの記事を参考にしましたが、 CursorAdapterの派生クラスを作成したり、O'ReillyのプログラミングAndroidにあるA Network MVCについて触れているものは多くはありませんでした。

本格的なアプリケーションを作成するためには、CursorとCursorAdapterを使い、効率良くViewを描画する事が必要だと感じています。静的な項目をリストすることしかできないために、ArrayListオブジェクトを作ってListViewに渡すようなコードは書きたくありません。

Programming AndroidはSafari Onlineでも読めますし、A Network MVCの項目はお勧めです。 サーチエンジンではフレームワークの解説ばかりで、A Network MVCのCursorとAsyncTaskを使用した非同期更新のパターンを知る事は難しいかもしれません。

開発環境の進化

チューニングのための手法はいろいろ進化していて、Android Platform 3.0以上を対象にEclipseでプロジェクトを作成すれば、ネットワークアクセスしている処理をUIスレッドに記述するだけでANRを避けるためコンパイルに失敗します。

現実にはPlatform 2.3.xを対象にしないHoneycomb以降のみを対象としたアプリを開発する機会は、よほど限定された環境を対象にしない限りは、ないと思います。ネットワークアクセスを行なうアプリケーションを開発していれば、試してみる事をお勧めします。何かAsyncTaskを使っていなかったり、その使い方がまずいところを指摘してくれるかもしれません。

SQLite3とORマッピングオブジェクト

スキーマを記述しない事がスマートだという風潮もありますが、インクリメントアップデートするように構成するなどの工夫をすれば世代管理などの運用はそれほど面倒ではありません。

これを避けるためにORマッピングを使う例もありますが、JavaのReflection APIを使う事でJITの対象から外れ、パフォーマンスダウンを引き起す事は良く知られています。時々使用するメソッドをReflection APIを使って汎用的に実装する事は良いかもしれませんが、ループの中で使う事は避けるべきで、あまり突き詰めずに、ほどほどにするのが正解です。

これまでの経験ではテーブル定義をJavaBeans的にGetter/Setterアクセスするクラスを作成して、適切なコンストラクタとinsert,update(save),restoreぐらいのDBアクセス用のメソッドを個別に実装してあげれば十分だと思っています。 参照のみであれば直接CursorオブジェクトをCursorAdapterでレンダリングに使い、書き換えが不要なレコードを表現するbeanの使用は避けましょう。

DBへのアクセスは局所的に抑え込むため、ContentProviderと、これらのプレースホルダ的なクラス以外から、直接DBアクセスをするような処理はまずい場合が多いと思います。

またSQLite3に関連する処理の説明はいろいろありますが、サンプル的なものが多く、スキーマの更新のような運用性 を考慮していない記述はお勧めできません。

機会があれば、SQLite3の使い方は別にまとめておきたいと思います。

最適化手法の変化

JITが有効になったPlatform 2.2以降と、それ以前では対応に違いがでてきます。 例えばインタフェース型の変数にオブジェクトを格納し、メソッドをコールすると2倍遅い場合があると言われていますが、それはJITを搭載していないプラットフォームでの話しです。JIT搭載環境では6%ぐらいだという記述があります。

最近ではPlatform 2.2以降を対象としても、問題とならないケースもあるでしょう。 反対に1.6以降で対応しなければいけないケースもあり、ハードウェアのスペックもより厳しい事が想定されます。

様々なプラットフォームに合わせて、その最適化手法の優先順位が変わってくる事は知っておくべきです。

適切なスタティックメソッドの実装やクラス内部からのgetter/setterコールを防ぐ事は必要だと思います。 しかしパフォーマンスのために、瑣末なコーディングルールにこだわるよりも、アプリケーションの内部構造とUXの向上にもっと注力しましょう。

この記事で取り上げた品々

2012/05/20

Apache Commons Imaging (a.k.a. Sanselan)によるExifヘッダを編集する Androidアプリの作成

Apache CommonsのCommons Sanselanは 画像処理を行なうためのPure-Java実装ですが、2009年以降は安定版がリリースされていません。

しかしSVNのリポジトリをみると、Sanselan(org.apache.commons.sanselan)からImaging(org.apache.commons.imaging)へと名前とパッケージ名をかえて、開発が進んでいるようです。

AndroidではSanselanでも問題ないようですが、別件でTiffファイルのヘッダがうまくパースできなかったので、Commons Imagingを使う事にしました。Sanselanを使ったアプリケーションはいくつかありますが、Commons Imagingを使ったサンプルはあまりないようです。

今回はSanselanとImagingを比較しながら、使い方のメモを残していきます。

SanselanによるExif日付情報の書き換え

Sanselan自体にはExifの日時情報を書き換えるような適当なサンプルがないので、Googleで探すとGPSdingsのSanselanExifWriter.java、openstreetmapのExifGPSTagger.javaなどのコードをみる事ができます。

ちなみに、これらのコードはGPLv2, GPLv3で配布されています。

ExifGPSTagger.java でのSanselanを使ったコード例 (一部変更)

  Double[] timeStamp = {
    new Double(calendar.get(Calendar.HOUR_OF_DAY)),
    new Double(calendar.get(Calendar.MINUTE)), 
    new Double(calendar.get(Calendar.SECOND))
  };
  TiffOutputField gpsTimeStamp = TiffOutputField.create(
    GPSTagConstants.GPS_TAG_GPS_TIME_STAMP, outputSet.byteOrder, timeStamp);

SanselanではTiffOutputFieldオブジェクトを作っていきますが、Commons Imagingと比べると編集対象によって値の指定方法が異なる点はすこし扱いずらいと感じました。

Commons ImagingによるExif日付情報の書き換え

SVNからコードをcheckoutすると、パッケージ名が org.apache.commons.imaging に変更されています。 これはスナップショットですが、今回は2012/05/19にチェックアウトしたコードを使っていきます。

内部構造も変更が進んでいるので、単純にパッケージ名を変更するだけでは前述のコードは動きません。 先ほどと同じ内容のコードは次のようになります。

  TiffOutputDirectory gpsDirectory = outputSet.getGPSDirectory();
  gpsDirectory.removeField(GpsTagConstants.GPS_TAG_GPS_TIME_STAMP);
  gpsDirectory.add(GpsTagConstants.GPS_TAG_GPS_TIME_STAMP,
      RationalNumberUtilities.getRationalNumber(new Double(calendar.get(Calendar.HOUR_OF_DAY))),
      RationalNumberUtilities.getRationalNumber(new Double(calendar.get(Calendar.MINUTE))),
      RationalNumberUtilities.getRationalNumber(new Double(calendar.get(Calendar.SECOND))));

SanselanではTiffOutputFieldを作成して追加していましたが、Commons Imagingでは直接TiffOutputDirectoryのaddメソッドを使うように変更されています。

また org.apache.commons.imaging.common パッケージの RationalNumberUtilities を使い、Double型をRationalNumber型に変換するところもポイントです。

全体としては操作方法に統一性があるのでAPIドキュメントとfind, grepがあれば、必要な機能の使い方は推測できると思います。

AndroidでCommons Imagingを使う時の考慮点

パッケージのサイズが大きいので、SanselanのExif編集機能だけをAndroid用にまとめたコードsanselandroidとしてgoogle codeでホストされています。

スナップショットをビルドしてできるライブラリJARファイルは672KBなので、Sanselan-0.97と比較して200KB近く大きくなっています。 そこでsanselandoroidにならってEclipse上でライブラリプロジェクトを作成し、Androidアプリから参照するようにしました。

まだJARファイルのサイズは388KBほどあって、sanselandroidほど小さく(248KB)なっていませんが、うまくまとまればGithubにでも公開したいと思います。需要があるのが分かれば、現状のまま上げてもいいんですけどね。 とりあえずはSanselandroidで十分だと思います。

さいごに

AndroidにはExifInterfaceクラスがあって、そこそこの操作はできますが、書き換えについてはヘッダを壊してしまう場合があるように思えます。

実際の操作はandroidに組み込まれているjheadライブラリを使っているはずです。 少ないファイルを処理している範囲では、体感的なスピードはSanselanでも大差はないと感じています。

Android 2.2のソースコードではjheadのバージョンが2.87です。 そこからの変更点をみていくと、いくつか重要なfixが行なわれているようにみえるのが気になるところです。

2012/04/25

Androidで外部JarがAPKファイルに取り込まれなくなった時の対処法

ひさしぶりに自宅でAndroidアプリを開発しようとして、開発環境をNVIDIAが提供している最新のTegra Android Development Pack 1.0r6に変更したところ、いくつかのアプリケーションで実行時にjava.lang.NoClassDefFoundError:エラーを出して実行できなくなっていました。

どうやらAndroid SDK Tools r17以降(正確にはADT 17.0.0以降)では、外部Jarファイルの扱いについて変更が入った模様です。

Android SDK r17からの変更点について

r17からのJarファイルの取り扱いについて、どのように問題を解決したかは、FoxykeepがHow to fix the “NoClassDefFoundError” with ADT 17にまとめています。

エラー内容

今回の問題は実行時にならないと分からず、Eclipseの中では無事に整合性が取れてコンパイルが通ってしまう事が特徴です。

アプリケーションを実行するとLogCatには次のようなエラーが記録されていました。

04-25 09:51:55.080: E/AndroidRuntime(23064): FATAL EXCEPTION: main
04-25 09:51:55.080: E/AndroidRuntime(23064): java.lang.NoClassDefFoundError: com.google.ads.AdView
04-25 09:51:55.080: E/AndroidRuntime(23064): 	at net.yadiary.android.jpostal.JPostalSearchActivity.onResume(JPostalSearchActivity.java:204)

AdMobは別のディレクトリに最新版を持っていて、複数のアプリケーションからそれを参照する構成になっていました。

解決方法

外部Jarの参照を止めて、次のステップで修正を行ないます。

  • Java Build Pathから外部Jarを参照している項目を全て削除する
  • Refactorでlibディレクトリをlibsに変更する
  • 外部Jarで参照していたJarを作成したlibsにコピーする

これでEclipse上はJava Build Pathにプロジェクト内部のlibsフォルダにあるJarファイルが登録されているはずです。

この状態でアプリケーションを実行したところ、無事にアプリケーションが動き出しました。

さいごに

ADT 17.0.0のリリースノートをみると、libsディレクトリを使う事は変更点としてまとまっています。

ADT 17.0.0リリースノートからの抜粋

Added feature to automatically setup JAR dependencies. Any .jar files in the /libs folder are added to the build configuration (similar to how the Ant build system works). Also, .jar files needed by library projects are also automatically added to projects that depend on those library projects. 

つまり、必要なJarファイルはlibsディレクトリにコピーさえしてくれれば、ADTが勝手に組み込むよ、というわけです

まさか依存関係にあるJarファイルをAPKに組み込まなくなるとは思わなかったので、解決までに時間を取ってしまいました。

2012/04/01

AndroidでJSONICライブラリを使ってみる

このブログを作成するシステムはgdataライブラリを利用して、いくつかの手製スクリプトで構成されています。

Subversionで履歴を管理していたのですが、実際にはほとんど使えないデータだったので履歴を切り離してGitに移行してしまいました。 まぁcommitしてきたログがあるので移行しても良かったのですが、気分的にはすっきりしていい感じです。

さて、新しい仕事を始めてからは.Net FrameworkやらAndroidやらiPhoneなんかのプログラミングを始めていますが、今回はJSONICというJavaでJSONデータを扱うライブラリについてです。

AndroidでのJSONデータの扱い

さて、Androidではorg.jsonパッケージがついてきて最低限のJSONデータを取り扱う環境があります。

送信用にJavaのデータ構造をJSONにする際には、特に問題なく扱えると思います。 反対に外部から受け取ったデータを扱う際にはローレベル過ぎて扱いが少しだけ面倒です。

今回はorg.jsonパッケージの上にライブラリを構築するのではなく、JSONICというJava言語用のJSONライブラリを利用する事にしました。

作成した成果物はjsonic4androidとしてGithubで公開しています。

JSONICを扱うメリット

基本的には無理に使う必要はありません。

自作の郵便番号検索システムから受け取ったJSONデータをJavaBeansとして扱うために使用しています。

この部分をorg.jsonパッケージを利用して取り出そうとして、面倒だったのは次のような点です。

  • UTF-8にエンコードするため、BufferedReaderを使い全データを変換する必要があった
  • 配列の中に各データのレコードを持っているため、要素名を指定してアクセスする処理が冗長で、JavaBeansにマッピングする必要があった
  • org.jsonパッケージを利用する自作変換クラスが安定して動作しなかった (これは自分の技量の問題です)

JSONのデータ構造は単純ですが項目数が多いため、各要素にアクセスするコードは繰り返しが多く、JavaBeansに変換してから扱うのがベストに思えたのですが、その変換コードを書くのがさらに面倒だったというのが実情です。

エントリ数が10以下で配列の中にさらにJSONデータ構造を持つような場合でなければ、org.jsonパッケージをそのまま使うのがお勧めです。

Android端末向けのプログラミングではライブラリを利用する事も作業量を減らす上では大切ですが、アプリ全体のパッケージサイズの圧縮が必要だったり、インタフェースを使わないなどのいくつかのプログラミング上のテクニックが存在します。

そういった事との兼ね合いで常にベストな選択をしてください。

パフォーマンス

jsonicでBufferedInputStreamからJavaBeansオブジェクト(JPostalBeanインスタンス)を生成した場合と、自前でInputStreamを開いてUTF-8に変換した文字列からJavaBeansオブジェクトを返すメソッドを作成して変換時間を計測して比較してみました。

jsonicを使ったコード


is = urlconn.getInputStream();
bis = new BufferedInputStream(is);
long beginTime = new Date().getTime();
ret = JSON.decode(bis, JPostalBean.class);
long endTime = new Date().getTime();

自前の変換メソッドを使ったコード (reqTextの生成時間は計測していません)


is = urlconn.getInputStream();
br = new BufferedReader(new java.io.InputStreamReader(is,"UTF-8"));
StringBuilder reqText = new StringBuilder();
...
long beginTime = new Date().getTime();
ret = parseJsonStream(reqText.toString());
long endTime = new Date().getTime();

合計で170件のデータを含むデータを処理しましたが、jsonicを利用した場合は約10%ほど高速でした。 自前ライブラリの場合には、BufferedReaderを使いUTF-8文字列を取得するところは省いていますが、それを含めると2倍近く処理速度の差が確認できました。

jsonicを使ったから特に処理が遅くなるという事ではなさそうなので、全体のバランスの中で選択すれば良いのかなと思います。

パッケージサイズの増加量

郵便番号検索アプリで作成したAPKファイルで比較するために、jsonicに依存するコードをコメントアウトしてjsonic4androidライブラリプロジェクトへのリンクを削除したAPKファイルを作成してみました。

$ ls -l JPostalSearch.*.apk
-rw-r--r-- 1 yasu yasu 469461 2012-04-01 10:38 /home/yasu/JPostalSearch.16e.without_jsonic.apk

jsonic4androidを含む、通常のAPKファイルは次のようになります。

$ ls -l JPostalSearch.*.apk
-rw-r--r-- 1 yasu yasu 499148 2012-04-01 10:45 /home/yasu/JPostalSearch.16e.apk

およそ30KB程度の増加量になり、配布について大きな障害ではなくなります。

jsonic4androidが対応するSDK(API)バージョン

作成しているアプリはAndroid 1.6 から(minSdkVersion="4")対応するようにエミュレータで動作を確認しています。

実機で確認しているのはAndroid 2.3.3 (SdkVersion="8")、及び、Android 3.2 (SdkVersion="13") です。

変更内容は次のセクションを参照してください。

jsonic4android - Eclipseプロジェクト形式での配布

今回扱ったJSONICのバージョンは1.2.10です。当初は1.2.9でしたがバージョンアップしたため、Githubのjsonic4androidも追随しました。

Githubに登録してあるのはEclipseに組み込む事を前提としたインポート可能なパッケージ形式です。

GithubのプロジェクトページのWikiにアクセスすると、画像で使い方が並んでいます。 gitでcloneしてから「Existing Projects into Workspace」を選ぶだけで、Eclipseの中で利用する事ができます。

jsonic4androidとして変更点

Eclipseパッケージとして組み込めるような形式になっている他の変更は次のような点です。

SDKのバージョンでいうと4、いわゆるAndroid 1.6以降で扱えるように、主にString::isEmptyメソッドを使わないように修正しています。

jsonicとproguardとの関係

リリースする際には署名をしますが、ここで通常はProguardを使う事になります。

開発環境ではうまく動くのに、署名したAPKファイルを配布すると問題になる場合がある原因の一つにはProguardがあります。

Proguardでの課題はテンプレートプログラミングを行なうためのGenericsとの相性です。

Proguardはパッケージ名やメソッドなどを変更してしまいますが、Java Reflection APIを扱うJSONICからはメソッドの名称が変更されてしまうと変数にアクセスする事ができなくなってしまいます。

鉤括弧でクラスを指定するGenericsの形式を使う場合には、明示的にproguard.cfgでそのオブジェクトを対象外にする必要があります。

今回はJPostalBeanクラスをjsonicのdecodeメソッドで処理しているため、proguard.cfgに次のような記述を追加しました。

proguard.cfgに追加したJSONICで処理するJavaBeans用の記述

-keep class net.yadiary.android.jpostal.beans.JPostalBean {
  <init>(...);
  *;
}

さいごに

JSONはクライアントとサーバーが1対1に対応するような軽量Webサービスを利用する場合の選択肢としては最有力だと思います。

SOAPでなければいけない場合は、ステートフルなセッションを行ない、メッセージボディを途中で分割して別々のサーバーに送信するSOPA対応Proxyを使用しなければいけない、限られた環境に限定されるでしょう。

Androidのようなスマートフォンで外部とのメッセージをやりとりする場合には自然に使う事になるJSONですが、アクセスするためにJSONICのようなデコーダとJavaBeansのデータ構造を利用しないと、JSONのデータ構造が変化する度にコードを追加、修正する手間が発生する事になります。

選択肢は沢山あるので、うまくライブラリを利用して見通しの良いコードを書くようにしたいものです。