google-code-prettify

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

2015-06-23

Android4.0以前と4.1以降では、親のViewをクリックした時に子のViewがどう見えるのかが異なる

Android4.0以前と4.1以降では、親のViewをクリックした時に子のViewがどう見えるのかが異なるので注意が必要。

以下のようなViewがあったとする。



Clickできる箇所は、外側の白い部分(下図のA)と、上の水色の部分(下図のB)。残りはnon clickable。すべてのViewのbackgroundDrawableは、stateful(pressedステートだと色が暗くなる)とする。



Android 4.1以降では、子にもpressedステートが伝わる


Android4.1以降でAをクリックした場合は、Aの領域すべてが暗くなる(子にもpressedステートが伝わる)。ただしclickableなBには伝わらない。



Android4.1以降でBをクリックした場合は、Bの領域が暗くなる(Bの中にあるnon clickableな子にもpressedステートが伝わっている)。



Android 4.0以前では、子にpressedステートが伝わらない


一方、Android4.0でAをクリックした場合。子にpressedステートが伝わっていないので、子の色が変わらない。



Android4.0でBをクリックした場合。同様にBの中にある子の色は変わっていない。



なぜ動作が違うか


おそらく関係しているのはView.javaのこのcommit

変更後は、ACTION_DOWNでsetPressed()が呼ばれるようになった。setPressed()が呼ばれるとdispatchSetPressed()が呼ばれ、そこから子のViewのsetPressed()が呼ばれる。




2015-06-10

Activityの再生成にまつわる処理を書くベストな方法

AndroidでActivityの再生成にまつわる問題をちゃんと解決しようとすると、非常に煩雑なコードになる。

その煩雑さを解消するクラスを作った。今の所、この方法がベストな方法だと思う。
Retain.java

以下、Activityの再生成に関する典型的な対処法とその問題点。

Activityの再生成のパターンは2つある


Activityは、以下の場合に再生成される。
  1. Configuration changeの時(画面の回転など)。プロセスは生きたまま。
  2. バックグラウンドにいる間にOSによってプロセスごと殺された後、アプリに戻ってきた時。
再生成後の新しいinstanceにデータを渡したいときにどう渡すかが問題となる。

方法1. onSaveInstanceStateでbundleに入れる

  • processが死んでいた場合でも、データが残っている。
  • Bundleに入る型しか保持できない(Integer, Boolean, String, Serializableなど)。
    例えば、通信処理スレッドを再生成後のinstanceに引き渡す、ということは直接はできない。
  • onSaveInstanceState()が呼ばれた後には、もうbundleに入れることができない。

方法2. staticな領域に覚えておく

  • processが死んでいない場合は、データは残っている。
  • processが死んでいた場合は、データは残っていない。
  • Activityが2つ存在するときに、間違って別のActivityにデータを渡さないようにしなくてはならない。

方法3. retain用のFragmentを使う

setRetainInstance(true)なFragmentを用意して、そのretain用Fragmentにデータを覚えさせておく方法。 http://developer.android.com/guide/topics/resources/runtime-changes.html
  • processが死んでいない場合は、データは残っている。
  • processが死んでいた場合は、データは残っていない。
  • Activityに紐付いたデータ(ThreadにおけるThread Local Storageのように)なので、別のActivityに間違ってデータを渡してしまうことはない。
  • Activityがいなくなると、自動でFragmentも消してくれる。
方法3の問題点
  • あくまでActivityに結びついた情報なので、Activityに含まれるFragmentごとに別々のretain用fragmentをもたせる、と言ったことは非常にしにくい。
  • retain用Fragmentを削除するタイミングが難しい。下手なタイミングでFragmentTransactionをすると、IllegalStateExceptionが出る。
  • FragmentActivity, Fragmentを使わなくてはならない。
  • コードが読みにくい

解決方法

  • processが死んだ場合でも、できるだけ情報を残したい、
  • でも、bundleに入る情報以外も保持したい
  • 3の方法は嫌だ
となると結局1と2のハイブリッドな方法で行くしか無い。 1,2の問題点を解消しつつ、1も2も統一的な方法で扱えるためのクラスを作った。 Retain.java

使い方

private Retain<somedata> retain; // SomeDataは、ParcelableまたはSerializableであること

@Override
protected void onCreate(Bundle savedInstanceState) {
    retain = Retain.forSerializable()
    someData = retain.onCreate(savedInstanceState);
    if (someData == null) {
        someData = new SomeData()
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    retain.onSaveInstanceState(outState, someData);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    retain.onDestroy(this);
}

これだけで、someDataの情報はできるだけ保持されるし、画面回転等にも対処できる。

2015-05-20

AndroidのcompileSdkVersionとかbuildToolsVersionとか

build.gradleに指定する、compileSdkVersionやtargetSdkVersionなどの説明。

compileSdkVersion

Javaのソースをコンパイルするときに使うSDKのバージョン。新しいSDKではAPIやandroid.R.id.xxxの定義が追加されていたりする。

例えば、ViewのrequestUnfufferedDispatchはAPIレベル21で追加されているので、このAPIを使う場合にはcompileSdkVersionは21以上にする必要がある。

buildToolsVersion

ビルドに使うツールチェイン(aapt/dxなど)のバージョン。上位互換なので、最新バージョンを追っていってもあまり問題はない。

SDKバージョンと連動していて、あるバージョンのSDKがリリースされると、一緒に対応する.0.0のバージョンのbuildToolsがリリースされる。例えばAPI 20がリリースされたときには、20.0.0のbuildToolsがリリースされた。

compileSdkVersionの値以上にする必要がある。

minSdkVersion

このバージョン未満の端末には、アプリをインストールできなくなる。アプリの動作には影響はない。

targetSdkVersion

targetという意味が分かりにくいが、どのバージョンの端末を念頭に置いて開発されたかを表す。この値を変えると、アプリの見た目や挙動が変わるので注意が必要。

例えば、targetSdkVersionが19(Kitkat)の場合、
  • 端末バージョンが19以下の場合、特に影響はない。
  • 端末バージョンが20(Lollipop)以上の場合には、19互換モードでアプリが動く。アプリの見た目は19の見た目になり、20で新たに導入された挙動はOFFになる。

targetSdkVersionで挙動が変わる例:
  • 19以上の端末でtargetSdkVersionを19以上にすると、AlarmManagerのset()でセットしたアラームのなるタイミングは若干不正確になる(バッテリ消費を抑えるため)
  • 14以上の端末でtargetSdkVersionを14以上にすると、hardware accelerationがデフォルトでONになる。
  • 10以上の端末でtargetSdkVersionを10以上にすると、UIスレッドでネットワークアクセスできなくなる。
公式ドキュメントのtargetSdkVersionには、テストした端末のバージョンを書けと書いてあるが、まさにその通り。20の端末でしかテストしていないのにtargetSdkVersionを21にすると、21の端末で動かない可能性がある。




2014-05-05

dx.jarをfastdx.jarに入れ替えて、Androidのビルドを50%くらい速くする

Androidのアプリのビルドは非常に遅い。原因の一つはGradleが重いことなのだが、実はDEXとPRE-DEXのフェーズも無駄な処理を行なっているため、非常に時間がかかる。
そこで、DEXとPRE-DEXを行なっているdx.jarの高速版のfastdx.jarを作成した。全体のビルド時間は、私の環境では50%くらい高速化された。

fastdx.jarのダウンロードはこちら

オリジナルのdx.jarが遅い原因は、大きく2つある。

  1. 無駄に.classから.dexの作成を行なっている(キャッシュしていない)
  2. 複数のdexのマージの処理に無駄がある

無駄に.classから.dexの作成を行なっている(キャッシュしていない)


オリジナルのdx.jarでは、.jarに含まれていた.classが一つでも変更された場合、.jarに含まれていた全.classファイルを再度.dexにコンバートし直している。
例えば、.jarの中身が以下の3つの.classを含んでいたとする。

a.class
b.class
c.class

a.classが変更された場合、dx.jarはa.dexのみならず、b.dex, c.dexも再度生成する。このコンバートに結構時間がかかる。

fastdx.jarでは、コンバート済みの.dexをキャッシュすることで、無駄に.dexへのコンバートが発生しないようにした。

複数のdexのマージの処理に無駄がある


複数のdexをマージする際、例えばa.dexからd.dexまでをマージする際、オリジナルのdx.jarは以下のようにdexをひとつずつマージする。
a.dex + b.dex = ab.dex
ab.dex + c.dex = abc.dex
abc.dex + d.dex = abdc.dex
dexファイルをいちいち生成、再読み込み、を行うため、非常に時間がかかる。

fastdx.jarでは、マージを一気に行う。
a.dex + b.dex + c.dex + d.dex = abcd.dex

このため無駄なdexの生成と読み込みが無くなり、高速化されている。



2014-04-24

AndroidのGradleでのビルドが遅いのをちょっと速くする

Gradleを使ってAndroidアプリのビルドをしていると、あまりのビルドの遅さについついGradleに移行したことを後悔してしまいそうになる。
遅い原因を調べて、2つほどビルドを速くするポイントを発見した。
  • 実はGradleデーモンが使われていない問題
  • 実はlibrary projectはReleaseとDebugの両方がビルドされている問題
以下、IntelliJ 13.1.2での症状&対処法だが、Android Studioでも同じ方法で速くなると思われる

前者は、Android Studio 0.6.0で直ったようです。


実はGradleデーモンが使われていない問題


.gradle/gradle.propertiesに
org.gradle.daemon=true
と書いておけばGradleデーモンが使われるので立ち上がりが速くなる、というのはあちこちに書いてあるが、実はGradleデーモンが使われていない可能性がある。

その理由は2つ。
  • IntelliJはGradleデーモンがいない場合にはGradleデーモンを起動するが、アイドルタイムアウトが60秒(固定値)。そのため、次にビルドするときにはGradleデーモンが既に死んでいる。
  • 手動でGradleデーモンを立ち上げたとしても、IntelliJが起動しようとするGradleとあまりに引数が違うと、新たに別のGradleデーモンが(timeout60秒で)起動する。
Gradleデーモンの引数やアイドルタイムアウトは、ps all wwで確認できる。
0  1000 10170     1  20   0 2124512 1254752 futex_ Sl pts/3    11:15 /usr/lib/jvm/java-6-oracle/bin/java -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8 -cp /home/sakurai/.gradle/wrapper/dists/gradle-1.11-all/7qd8qq8te5j4f5q9aaei3gh3lj/gradle-1.11/lib/gradle-launcher-1.11.jar org.gradle.launcher.daemon.bootstrap.GradleDaemon 1.11 /home/sakurai/.gradle/daemon 10800000 7e2c5ad5-a731-455d-a37b-699b5c251ea6 -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m -Dfile.encoding=UTF-8
IntelliJでのビルド直後にps all wwで、timeoutの値をチェックしてみると良い。数値が60000の場合には、60秒で死ぬデーモンが使われている。タイムアウトが長いデーモンを使うには、手動でGradleを立ち上げるしか無い。

また、Gradleの引数が一致していない場合には、IntelliJのSettings->Gradle->Gradle VM Optionsの値を、手動で起動したGradleと合わせておく必要がある。私の場合には、以下を設定してある。
-XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Xmx1024m

実はlibrary projectはReleaseとDebugの両方がビルドされている問題


Build VariantsをDebugにしていると、library projectはReleaseとDebugの両方がビルドされている(library以外は関係ない)。

現状、Debugビルドをしていても、必ずReleaseビルドのlibraryが使われてしまうというバグがある。
Gradle plugin does not propagate debug/release to dependencies

このバグの副作用として、アプリをDebugビルドした場合に、library projectはReleaseとDebugの両方がビルドされてしまうようだ。

Gradleの出力例: (DebugもReleaseもビルドされている)
:ActionBarSherlock:preReleaseBuild
:ActionBarSherlock:checkReleaseManifest
...
:ActionBarSherlock:preBuild
:ActionBarSherlock:preDebugBuild

どうせReleaseビルドしか使われないのだから、最初からlibraryはReleaseビルドしかしないようにしてしまえばよい。

IntelliJのウィンドウの左下のBuild Variantをクリックし、libraryプロジェクトのBuild Variantは全てReleaseに変えておく。

2013-11-19

ある特定の文字列をTAGにすると、Log.xが出力されない

Androidのandroid.util.Logで、ある特定の文字列をTAGにした場合には、ログが出力されない。

ログが出力されないTAGは、以下の通り。
  • HTC_RIL
  • RILで始まる文字列
  • AT
  • GSM
  • STK
  • CDMA
  • PHONE
  • SMS
電話番号を扱うアプリを作っていてTAGを"PHONE"にしていたところ、ログに何も出力されずに悩んでしまった。

参考:
logd_write.c

2013-06-18

Androidアプリのテストに役立つサービス

Androidアプリのテストに役立つサービス。

Samsung Remote Test Lab

サムスンの端末をリモートで操作でき、アプリのインストールやテストが行える。サムスンの機種がいろいろそろっている。無料。

Apkudo

apkをアップロードすると、多数(250くらい)の端末上でのmonkeyテストを自動で行なって結果を表示してくれるサービス。無料。日本の端末はあまり用意されていない。

Remote TestKit

端末をリモートで操作でき、アプリのインストールやテストが行える。日本の端末が豊富に用意されているので、日本向けのアプリを出す際には便利なサービス。
以前とある端末でのみ落ちるバグがあり、その原因を調べるときに同じ作者のリモート・スマホ・レンタルにお世話になった。

Linux版のクライアントが無いため私は試せていないが、デバッガでリモートデバッグができたり、adb shellが動作したりもするらしい。
1時間945円ほど。

2013-06-07

onCreateの引数savedInstanceStateは、Fragmentが再生成したかどうかの判断には(厳密には)使えない

Fragmentの再生成が起こるときには、onCreate(Bundle savedInstanceState)のsavedInstanceStateが必ず!=nullになる。

…と思っていたのだが、そうはならない条件があることがわかった。
つまり、Fragmentが再生成されたにも関わらず、savedInstanceState=nullとなる場合がある。

再生成されたのにsavedInstanceState=nullになるのは、以下の2つの条件を満たした場合。
  1. FragmentがUIを持たない。つまり、onCreateViewでnullを返している
  2. onSaveInstanceState(Bundle outState)で、outStateに何もputしていない

要は、Bundleに誰も何もセットしていない場合には、再生成時のsavedInstanceStateはnullになる(空のBundleが渡されてくるわけではない)。

なお、Viewは自らの状態をBundleに保存しているので、FragmentがViewを持つ場合には通常Bundleは空にはならない。そのため、savedInstanceStateがnullかどうかによって、再生成かどうかを判断できる。

2013-04-18

Fragmentでloaderを使っている場合は、setRetainInstance(true)してはいけない

FragmentでsetRetainInstance(true)としていると、loaderのonLoadFinished()が呼ばれないことがある。
そのため、loaderを使っている場合は、setRetainInstance(true)としてはいけない。

自作アプリでloaderとsetRetainInstance(true)を混ぜて使っていて、なぜかonLoadFinished()が呼ばれないことがあるのでググってみたら、Dianne Hackborn(Googleの人)もloaderとsetRetainInstance()を混ぜて使うなと言っていた。

以下の方法で、「onLoadFinished()が呼ばれない場合」を再現できる(GalaxyNexus + JellyBeanで確認)。

ソースコード

Googleによるサンプルコードに従った書き方をしている。onActivityCreated()でinitLoader()をし、onLoaderReset()でswapCursor(null)している。

public class MyFragment extends ListFragment implements LoaderManager.LoaderCallbacks {

    private MyAdapter adapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        adapter = new MyAdapter(getActivity());
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        setListAdapter(adapter);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        setListAdapter(null);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), ...);
    }

    @Override
    public void onLoadFinished(Loader loader, Cursor data) {
        adapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader loader) {
        adapter.swapCursor(null);
    }
}

手順

  1. 携帯を横(landscape)にしておく。この状態ではListFragmentにCursorの中身が表示されている。
  2. 電源ボタンを押して、スリープに入れる。
  3. 携帯を縦に持ちかえる。電源ボタンを押してスリープを解除し、ロックも解除する。すると、ListFragmentの表示が空になっている。

何が起きているのか

電源ボタンを押してスリープに入れた時に、以下の順でcallbackが呼ばれる。

  1. onLoaderReset
  2. onDestroyView
  3. onViewCreated
  4. onActivityCreated

ロック画面が縦だからだと思うのだが、landscapeのActivityが破棄され、portlaitのActivityが再生成される。 onLoaderReset()は呼ばれているが、Activity再生成後のonLoaderFinish()は呼ばれていない。そのため、スリープとロックを解除すると、ListFragmentが空になっている。

対処方法

setRetainInstance(true)を呼ばない、というのが一番確実な対処方法だが、onActivityCreatedでinitLoader()ではなくrestartLoader()を呼ぶという方法でも、試した限り動作するようだ。ただしrestartLoader()を使う方法だと、多少無駄にqueryを行うことになる。

2013-04-15

Spinnerでは、AdapterのgetItemViewType()とgetViewTypeCount()が使えない

ListViewでは、AdapterのgetItemViewType()とgetViewTypeCount()をoverrideすることで、行ごとに違うViewを使うことができる。
しかし、SpinnerではgetItemViewType()とgetViewTypeCount()が呼ばれなく、行ごとに違うViewにするにはListViewとは異なる方法を取る必要がある。

Spinnerのprivateな内部クラスであるDropDownAdapterがAdapterをラップしているのだが、このラッパーは以下のように固定値を返すように実装されていて、setAdapter()でセットしたAdapterのgetItemViewType()とgetViewTypeCount()を呼んでくれない。
        public int getItemViewType(int position) {
            return 0;
        }

        public int getViewTypeCount() {
            return 1;
        }
Spinnerで行ごとに違うViewを使うには、以下のようにすると良い。
  • Viewの中に複数のViewを含めておいて、見せたいViewのVisibilityだけvisibleにし、その他をgoneにする。
  • getView()でconvertViewを再利用するのをやめて、毎回Viewをinflateする。もしくは、convertViewで渡されるViewのタイプを見て、再利用するかどうかを自分で決める。
参考: https://code.google.com/p/android/issues/detail?id=17128

ListViewで、行ごとに違うViewを使う

ListViewでAdapterを使う際、getItemViewType()とgetViewTypeCount()をoverrideすることで、ListView中に複数のViewを混ぜることができる。
例えば、3種類のviewが存在する場合、
int getItemViewType(int position) {
    return position % 3; // 行じ応じたViewのタイプを返す。タイプは0,1,2,3...と0始まりであること。
}

int getViewTypeCount() {
    return 3; // Viewのタイプの数
}
としておく。
View getView(int position, View convertView, ViewGroup parent)
に渡されてくるconvertViewには再利用されるべきViewが渡されてくるのだが、上記のようにgetItemViewType()とgetViewTypeCount()をoverrideしていれば、そのpositionに応じた正しい種類のViewがconvertViewに渡されてくる。

コード例:
int getItemViewType(int position) {
    return position % 3; // 行じ応じたViewのタイプを返す。タイプは0,1,2,3...と0始まりであること。
}

int getViewTypeCount() {
    return 3; // Viewのタイプの数
}

View getView(int position, View convertView, ViewGroup parent) {
    switch (getItemViewType(position)) {
    case 0:
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.item1, parent, false);
        }
        break;
    case 1:
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.item2, parent, false);
        }
        break;
    case 2:
        if (convertView == null) {
            convertView = inflater.inflate(R.layout.item3, parent, false);
        }
        break;
    }
    return convertView;
}

2013-04-02

Google PlayでAndroidアプリのSEO(検索エンジン最適化)をするためのツール

Google Playでアプリをキーワード検索する際に、アプリの説明文によって検索順位が大きく異なる。
つまり、自分のアプリを見つけてもらいやすくするには、SEO(検索エンジン最適化)が必要になる。

しかし、Google Playで自分のアプリの検索順位を調べるのは結構骨が折れる。

そこで、指定したキーワードで検索した場合にアプリが何位に位置するのかを調べるツールを作った。
android-search-ranking

使い方


  $ android-search-ranking -pkg com.gmail.anolivetree -lang en "shrink" "photo resize"
  #2 for shrink
  #20 for photo resize
出力の意味は、"shrink"で検索した場合にはランキングは2位、"photo resize"で検索した場合には20位という事。ランク外の場合には#0と表示される。

2012-11-01

DialogFragmentの落とし穴にはまらないための方法

AndroidにFragmentが導入されて以来、Dialogを直接使うことは非推奨となり、DialogFragmentを使うことが推奨されている。

Dialogはあまり深く考えずに使っても大丈夫だったのだが、DialogFragmentは落とし穴が多数あり、正しく使わないとアプリが落ちてしまう。具体的には、Fragmentの再生成が発生したときに正しく動作しなくなる。

以下では「FragmentからDialogFragmentを開き、結果(OK/Cancel)をFragmentに返す」という場合を例にして、DialogFragmentの落とし穴を回避するための書き方を説明する。

正しいコード

public class OkCancelDialog extends DialogFragment {

    static public interface OkCancelListener {
        void onDialogPositiveClick(long id);
        void onDialogNegativeClick(long id);
    }

    // Fragmentの再生成の時に呼ばれるので、引数なしのpublicなコンストラクタが必要
    public OkCancelDialog() {
    }

    // targetFragmentは、結果を受け取るFragment。OkCancelListenerを実装したFragmentであること。
    public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
        if (targetFragment != null && !(targetFragment instanceof Fragment)) {
            throw new RuntimeException("targetFragment is not Fragment");
        }
        // Fragmentの再生成後でも使いたい値は、bundleに入れてsetArgumentしておく。
        Bundle bundle = new Bundle();
        bundle.putLong("id", dataId);
        setArguments(bundle);
        // 結果を受け取るFragmentは、直接Fragmentの変数には入れずに、
        // setTargetFragment()/getTargetFragment()を使う
        setTargetFragment((Fragment)targetFragment, 0);
    }

    public long getDataId() {
        return getArguments().getLong("id", -1);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage("Message");
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // getTargetFragment()は、Fragmentが再生成された場合でも、正しいinstanceを返してくれる。
                OkCancelListener listener = (OkCancelListener) getTargetFragment();
                listener.onDialogPositiveClick(getDataId());
                dialog.cancel();
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                OkCancelListener listener = (OkCancelListener) getTargetFragment();
                listener.onDialogNegativeClick(getDataId());
                dialog.cancel();
            }
        });
        return builder.create();
    }
}

間違い1 DialogFragmentをinner classにする

public class MyFragment extends Fragment {

    public class OkCancelDialog extends DialogFragment {
        ...
    }
}
Fragmentの再生成時には、自動的にFragmentのpublicな引数なしのコンストラクタが呼ばれる。inner classにするとFragmentの自動生成ができないため、落ちる。

staticなinner classにするのでも良いが、出来ればファイルを分けるほうが(間違えにくいので)良い。

間違い2 引数なしのコンストラクタを作らない


    public OkCancelDialog() {
    }
を省略、またはprivateにすると、Fragmentの再生成時に落ちる。Fragmentの再生成時には、外から引数なしのコンストラクタが自動で呼ばれるためである。

間違い3 結果を受け取るFragmentへの参照を直接保持する

    private OkCancelListener listener = null;

    public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
        listener = targetFragment;
        ...
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        ...
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                listener.onDialogPositiveClick(getDataId());
            }
        }
    }
上のようにすると、Fragmentの再生成時にはメンバ変数listenerはnullになってしまう。なぜなら新しく作られたFragmentは以前とは別のオブジェクトで、かつ引数なしのコンストラクタが呼ばれているためである。しかも結果を受け取るFragmentも再生成されて別のオブジェクトになっているので、もはやlistenerの値は意味を持たない。

setTargetFragment()/getTargetFragment()を使えば、Fragmentが再生成された場合でも、新しく生成されたFragmentの参照を取得することができる。

間違い4 setArguments()を使わずに、値をメンバ変数に覚えておく

    private long dataId = -1;

    public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
        this.dataId = dataId;
        ...
    }
間違い3と似ているが、Fragmentが再生成されたときには、メンバ変数dataIdは-1になっている。再生成後でも使いたい値はBundleに入れ、setArguments()しておけばよい。





AndroidのActivityのライフサイクルに関して多くの人が誤解している点

AndroidのActivityに関していろいろ調べた。おそらく(私も含めて)多くの人が誤解している点をまとめた。

メモリ不足の時、あるActivityだけが破棄されるという事は無い

システムのメモリが足りなくなったときにはLow Memory Killerが動作するが、Low Memory Killerはprocessの中の、あるActivityだけを破棄するということはせず、process全体をkillする。

メモリ不足により、「あるActivityはonDestroyが呼ばれずに破棄され、他のActivityは生きている」、ということは起こらない。processがkillされたとき以外は、onCreateとonDestroyはセットで呼ばれる。

メモリが足りなくなってprocessがkillされるのをシミュレートするには、単にDDMSでprocessを選択し、STOPボタンを押せば良い。

参考:
https://groups.google.com/forum/#!msg/android-developers/WF-STG7GdrI/QrBL0btjCxYJ

processがkillされても、Activityのスタックの記録は残っている

Low Memory Killerによりprocessがkillされても、Activityのスタックの記録(TaskRecord)は残っている。onSaveInstanceState()で保存したBundleも残っているし、Activityを起動したIntent(=getIntent()で取得できるIntent)も残っている。 

processをkillした(killされた)後に、
$ adb shell dumpsys activity
を実行してみると、Activityのスタックが残っている事を確認できる。

processがkillされた後にそのアプリを起動すると、一番上のActivityのonCreateがsavedInstanceState!=nullで呼ばれる。backボタンを押すと、今度は一つ下のActivityのonCreateがsavedInstanceState!=nullで呼ばれる。

Activityのスタックの記録は、暫くすると無くなる

Activityのスタックの記録は15-30分など、暫くすると無くなるが、Low Memory Killerでプロセスがkillされる事と、Activityスタックの記録が無くなる事は、無関係と考えて良い。 

時間が経ってもActivityのスタックの記録が無くならないようにするには、rootのActivityに android:alwaysRetainTaskState="true"
を指定すればよい。 

参考:
http://stackoverflow.com/questions/7107614/for-how-long-does-android-save-a-killed-activitys-state

2012-10-11

AndroidのNotification.BuilderのtitleやtextやcontentInfoはどれがどれなのか

AndroidのNotification.Builderには、title, text, contentInfo, numberなどをセットすることができるが、どれがどこに表示されるのかの説明が公式ページには一切無い。

仕方ないので、自分でGingerbread(SH-06D)とJelly Bean(Galaxy Nexus)で見え方を調べてみた。

上がGingerbreadで表示させた場合。下がJelly Bean。 比べてわかったこと。

  • setContentInfoとsetNumberは、同じ領域に表示される(setContentInfoの方が強い様だ)。
  • setContentInfo(とsetNumber)は、Gingerbreadでは表示されない

2012-09-25

AndroidのUIテストフレームワーク

AndroidのUIテストのフレームワークを調べていたのが、現在はほぼRobotium一択状態だった。

Native Driver

Google製のツールで有望視されていたようだが、開発が止まった

Scirocco

開発が止まっているように見える。最終commitは2011/12/13。 新バージョンのSciroccoはNative Driverベースらしいが、そのNative Driver自体開発が止まっている。

monkeyrunner

絶対座標でクリック位置を指定するので、使いにくい。 別途AndroidViewClientのような物を使う必要がある。

Calabash

ちょっと特殊すぎるので、パス。

2012-07-25

複数のAndroid端末使用時に、ANDROID_SERIALを切り替える

複数のAndroid端末をPCに接続している場合、環境変数ANDROID_SERIALを切り替えるのが結構めんどくさい。
そこで、ANDROID_SERIALを簡単に切り替える事ができるプログラムを作成した。

https://github.com/anolivetree/adbdevices

Go言語で作成しているので、コンパイルにはGoが必要。

後は
alias ad='export ANDROID_SERIAL=`adbdevices`'
と.bashrcにでも書いておけばよい。

2012-02-29

Androidアプリのデバッグオプションの意味

Androidの各種デバッグオプションの意味がMLで説明されていたので、簡単にまとめる。


■debuggable=trueの意味


アプリケーションにデバッガをAttachできるかどうか。作成されるapkに違いは無い。
ただし、エミュレーターで動作している場合は特別で、debuggable=trueではないアプリでもAttachが可能。


■EclipseのRun/Debug session


Debug sessionの場合は、アプリ実行時にデバッガを自動でAttachする。Run sessionの場合はしない。


■Eclipse(ADT)のAndroid toolsでのexport


apkの署名が違う。zipalignが行われるので、多少実行速度は速いはず。

2011-07-14

AndroidのSSLSocketはマルチスレッドに対応しているのか

OpenSSLを知っている人には有名な話だと思うが、OpenSSLではひとつのソケット(SSL型)に対してSSL_read()とSSL_write()を同時に呼ぶことはできない。(実際には多くの場合は動作するが、re-negotiationが動作しない)

OpenSSLのFAQ

この仕様は、blocking IOの時に問題となる。SSL_read()でブロックしている最中に別スレッドからSSL_write()することができないのだ。

SSL上の通信が、Read,Writeがシーケンシャルに行われるような通信の場合には問題とならないが、ReadとWriteが独立して行われる通信の場合、blocking IOのSSLは使えないことになる。

AndroidのSSL部はOpenSSLを使っているが、この辺りがどうなっているのか(制限がそのままあるのか、回避されているのか)を調べてみた。

結論としては、AndroidのSSLはマルチスレッド対応している。以下、その説明。




SSLSocketの実装は、OpenSSLSocketImpl.javaにある。
SSLSocketからは、NativeCrypto.cppの関数が呼ばれている。

NativeCrypto.cppのNativeCrypto_SSL_do_handshake()で、ソケットをnon-blockingにしている。

/*
* Make socket non-blocking, so SSL_connect SSL_read() and SSL_write() don't hang
* forever and we can use select() to find out if the socket is ready.
*/
if (!setBlocking(fd.get(), false)) {
throwSSLExceptionStr(env, "Unable to make socket non blocking");
SSL_clear(ssl);
JNI_TRACE("ssl=%p NativeCrypto_SSL_do_handshake => 0", ssl);
return 0;
}

NativeCrypto.cppのsslRead()とsslWrite()には、以下のコメントがある。

/*
* Helper function which does the actual writing. The Java layer guarantees that
* at most one thread will enter this function at any given time.

関数の中身を見ると、

1. non-blockingでSSL_read()/SSL_write()を呼び出す。
2. 失敗した場合は、sslSelect()を呼び、ブロックする。

としている。

結局、
・AndroidのSSLは、non-blocking IOを使っているが、上位にはblockingのAPIを提供している。
・OpenSSLのblocking IOにあるような、マルチスレッドの呼び出し制限は無い。
ということで、ユーザーにとって使いやすいAPIとなっている。