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との差分を記述していって、場合によってはデータメンテナンスも行ないます。

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

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

リーダブルコードを読んでみて気になったこと

オライリー(O'Reilly)から出版されたリーダブルコードについてです。 ほとんどいいがかりみたいですが、読んで気になったところをメモしておきます。

これから文句みたいなことを書きますが、この本はいろいろな発見があって、Web専業といった特定の業務に特化していて、いろいろなシステムに触れる機会が少ないような方には特にお勧めです。

最近Androidアプリを作ってJavaばかり触っているので、特にこの観点から気になった点をまとめました。

「関数の戻り値にretvalを使うな」について

型のないいわゆるLLと呼ばれるようなスクリプト系言語を扱う場合には、tmp/retvalのような名称は使うべきではないと強く思います。

しかし型のある言語の場合には、メソッドの先頭で戻り値のオブジェクトを格納する変数を用意しておき、最後にreturn ret;とするのは悪い習慣だとは思いません。

問題なのはメソッドの戻り値になるオブジェクトを格納する変数を、メソッドの途中に宣言をする事です。 読み手にゴールを提示せずに書き始め、そのゴールを途中にそっと忍び込ませるような真似は、決してするべきではないと思います。

C言語のように全ての変数をメソッド/スコープの先頭で宣言するべきとは思いませんが、処理の全体に関連する変数、定数の宣言はメソッドの先頭で行ない、それが処理全体の主人公であるという見通しを読者に伝えるべきです。

  public String findUniqueNameFromIndex(int index) {
     String ret = "";
     ...
     return ret;
  }

すべてのメソッド定義でこのような形式に沿った記述をしていれば、その処理がやや冗長になっても途中でターゲットを見失わずに済みます。

処理の途中で使用する変数は、そのスコープにもっとも近い場所で宣言をすることで、読者にそのスコープに関連する事を強く印象づける事ができると考えています。

大切な事は、全ての記述を特定の形式に収める事で、読者の興味、関心を他に移さずにコードに集中させる事です。

Syntactic Sugarであるswitch文を排していない

読んだ限りではswitch文について言及している部分はありませんでした。 まぁこれは趣味な部分も含んでいるので、こうでなきゃいけないというコンセンサスは得られないと思います。

ただ、switch文は単一スコープの中に記述する形式になっているので、一度あるcase文の中で宣言した変数は、他の個所で使う事はできません。break文を忘れて、意図せず変数に代入が行なわれて気がつかずに特定の処理でバグが入るといった可能性があります。

一般にswitch文はif-else文で置き換えが可能で、スコープが明確に分かれているif-else文を使うのが個人的にはお勧めです。少なくともbreak文を抜かすような真似はif-else文ではできません。

まぁswitch文は便利ですし、組み込み系のプログラミングで8bitの入力を処理するのに255個のcase文があるコードを見た事もあるので、環境によって流儀はあると思いますが、あえてswitch文は止めようといっておきます。

まとめ

リーダブルコードは読者を意識したコーディングを行なうための、とても良いガイドになっていると思います。

昔から特定の言語を対象にした、「べき・べからず本」というのはありましたが、どうもTipsが命令的で柔軟性にやや欠けるような印象がありました。

リーダブルコードのテーマは昔から出版されている本と比較して、とても斬新というわけではありません。

しかし、押し付けようとせずに、プロ・アマ問わずに誰でも読める記述になっている事はお勧めのポイントとして、とてもとても高く評価しています。

この本は誰が読んでも参考になりますが、できれば10代で、この本を読んで、最初から読み手を意識したコーディングをする方が増える事を願っています。

最後に、深夜に書き始めたせいか、いつもどおり、そしていつもにも増して上から目線になってしまいました。 ごめんなさい。

おまけ:自分が書くコードはリーダブルか (小学生読書感想文調)

私は、ずっとこういう事をテーマにしているはずなのに、いまだに最初からリファクタリングが不要なコードを書く事ができていません。

これは小説家が何枚も原稿用紙を破るように、当たり前の事なんだと思ってきました。 でも、リーダブルコードを読んで、プロの人達はこんな事はしなくても経験を積めばリーダブルで高可用性なコードが最初から書けて、こんな素晴しい本まで書けるようになるんだと知りました。

私は以前に同じようなコードを書いた事がなければ、いろいろ設計らしき事をしてから作り始めても、一つのメソッドが100行くらいになってしまう事は、たまに起ります。

1つのスコープでそんなに長いコードを書いているので、tmpという変数名を使ってしまうと、後でtmp2のような変数名を使わなくてはいけなくなるので、結果として説明的な変数を使うようになります。

それから、処理の内容をできるだけ分割して、意味のある単位で外部のメソッドとして括り出して、その処理に責任を負うのに相応しいクラスに処理を移します。

そんな作業を何回も繰り返して、やっとそれなりに適当な粒度のメソッドに分割されたコードを作る事ができた、と思う事ができるようになります。

そして、いくつかの責務を負っているクラスを分割したくなります。 この衝動はどうしても抑える事ができないのですが、よくよく考えずにやってしまって、最終的にgit checkout -fをしなければいけなくなる時があります。

きっとプログラミングが仕事の人達は、最初から見通しをつけて空のメソッドをどんどん書いて、後から、その中に処理を書いていく事ができるんだと思います。何年も仕事をしていて、いまだにこんな事をしているのは自分だけに違いないと思うと情けないけれど、負けずに頑張っていきたいと思います。

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

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

Pythonスクリプトの出力をリダイレクトしたらエラーになった

このブログサイトに登録してあるキーワードを出力するスクリプトをgdataライブラリを使って、pythonで書いているのですが、出力結果をファイルにキャッシュしておこうとしてラップしているbashスクリプトに../list_labels.py | tee labels.txt を追加したところ、次のようなエラーが発生しました。

  File ".../python/list_labels.py", line 133, in run
    self.list_labels()
  File ".../python/list_labels.py", line 125, in list_labels
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-7: ordinal not in range(128)

問題になったのは次のようなコードの中で、print labelをしているところです。

問題になった該当コード周辺

...
    l = []
    for label in set(labels):
      l.append(label)
      pass
    for label in sorted(l):
      print label
      pass
    pass

Googleでとりあえず検索してみた結果

いくつかの方法がありましたが、sys.setdefaultencodingにencodingを記述するといった方法がありました。

import sysをしてからdir(sys)でみると、少なくとも手元のpython 2.7.3にはそんなメソッドは定義されていません。

次にヒットしたのはunicodeに文字列を変換するものですが、どちらかというとスクリプト中に埋め込んだutf-8文字列を画面に出力するための方法を解説しているもので、リダイレクトの時に問題が発生している事については触れられていませんでした。

pythonのバージョンに依るのかもしれませんが、この他の方法もいまいち決定的なものはありませんでした。

unicodeに変換するのではなく、リダイレクトの時の正しいencodeを指定する

解決策は次のようにencode()関数を追加して'utf-8'に変換しました。

変更後の該当コード個所の抜粋

    l = []
    for label in set(labels):
      l.append(label)
      pass
    for label in sorted(l):
      print label.encode('utf-8')
      pass
    pass

リダイレクトの時にLANGなどをみて標準的なlocaleに変化してくれれば良いのですが、リダイレクトの時にencodeが設定されないというのは、かなり悩ましい挙動です。

暗黙の挙動だから不定になるのは当然という意見もありそうですが、リダイレクトの時に挙動が切り替わるのは、やっぱり謎仕様です。rawデータとして何の処理もせずにbyte列を掃き出すなら、まだ分かるんですけどね。

ともかく無事に終わったので部屋の整理をして今日は休もうと思います。

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つだけなので、良いソリューションだと思います。

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

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

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

Ubuntu 12.04 LTSマルチディスプレイ環境下での不具合?

数年前に新品パネルに交換した事もあって、いまだに17インチディスプレイ(Nanao FlexScan L567)を使っています。 これを2枚並べてマルチディスプレイ環境にしているのですが、拡大表示時の課題などもあってディスプレイを仮想的に接続せずにDISPLAY環境変数でいうところの:0.0と:0.1として使っています。

このディスプレイが接続されているマシンはUbuntu 12.04 LTS専用機で、Androidアプリの開発やらメール書きやらのメインマシンとして使っています。

タイトルに?をつけてスポーツ新聞の見出しみたいですが、今回はいままで気がついたものの、コード上でまだ原因が特定できていない、おそらくマルチディスプレイ環境が原因と思われる症状をメモしておきます。

環境について

OSよりもWindowManagerの影響は大きいと思われて、その他のGNOME, KDEなどでは現象が発生しないかもしれません。

  • OS: Ubuntu 12.04 LTS x86_64版
  • WindowManager: xfwm4 (デスクトップ環境はXFCE)

考慮点

ここでまとめた内容は、まだコード上でマルチディスプレイが原因と特定できていない現象を含んでいます。 そのため現象としては正しいが、原因はマルチディスプレイ環境にないものも含まれている可能性があるのでご注意下さい。

なおアプリケーションをDIPSLAY=:0.0設定下で稼働した場合には不具合は起っていません。

其の壱:Android AVD(qemu)が終了しない

EclipseからAVD(qemuによるandroid emulator)を起動して、デバッグが終ったところでウィンドウを閉じて終了しようとしても、AVDプロセスが終了しません。

KILLHUPシグナルは受け付けないので、この場合の対処法はkill -9で該当プロセスを強制終了させるのみです。

其の弐:Firefoxのプルダウンメニューが開かない

住所欄の都道府県などはFormのプルダウンメニューで選択可能になっている場合があります。 このプルダウンをクリックしても選択肢が表示されません。下矢印キーで選択はできるので実用上は致命的とまではいえません。

其の参:PiTiViビデオエディタで動画のExport処理が完了しない

最終的に完成した動画をExportするわけですが、この処理が永遠に終わりません。

この他にも動画関連の不具合はあって、ffmpegやmpeg2enc辺りが怪しいんですが、追求していません。

其の肆:gtk-recordmydesktopが撮影を終了することができなくなる

やっぱり動画周りの不具合ですが、これは通知エリアを利用するアプリケーションで、DISPLAY=:0.0に通知ウィンドウがある場合に、DISPLAY=:0.1で動かしたgtk-recordmydesktopが隠れたまま、操作不可になってしまう事が原因です。

この場合、gtk-recordmydesktopを端末から起動していてC-cで終了する事はできますが、recordmydesktopプロセスは残ってしまいます。手動でKILLHUPを送信すれば停止しますが、注意が必要です。

当然、gtk-recordmydesktopを使わず端末からrecordmydesktopを直接動かせば何の問題もありません。

まとめ

これからKDE,GNOMEで動かしてみて、どんなになるか確認してみます。 とはいえ作業中なので環境全体をシャットダウンして試すことはすぐにはできないんですよね。

最近は1920x1200を越えるディスプレイも簡単に購入できて、27インチで2560x1440ともなれば、1280x1024の2枚(2560x1024)よりも下に広くなります。でもこのFlexScan L567の発色は良くて満足しています。 Mac/Windows用に安い1920x1080なIPS液晶ディスプレイもあるんですが、表面処理が荒くてオブラートみたいな薄い膜が一枚あるみたいなのが残念です。

IPSが高級品だった時代は過ぎさってしまったので、次に購入する時は実物も眺めつつ決めたいと思います。

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修飾子のように、適切な記述ができていない部分があるのではないかなと思います。

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