2014/08/17

onActivityResult 内で DialogFragment.show() を実行すると IllegalStateException が発生する

タイトルの通り、FragmentActivity.onActivityResult() 内で DialogFragment.show() を実行すると、以下の様に IllegalStateException が発生して落ちてしまいます。

Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

onActivityResult() が呼ばれる時には Activity は復活しているはずなので、IllegalStateException はおかしいと思うのですが…

調べてみると、Support Library v4 の既知のバグらしい。

確かに、android.support.v4.app.DialogFragmentandroid.app.DialogFragment に変更すると問題なく動作しました。

2.x 系のシェアは 15% 以下になってきたようなので、新規のアプリでは 2.x 系をサポートしないのが良いかもしれません。
とはいえ、既存のアプリは対応せざるを得ず、まだまだ Support Library を使う必要がありそうですが…

回避方法

回避方法を調べてみたのですが、大きく分けると次の 3通りに分類できそうです。

  1. Support Library を使わない
    新規で作成するのであれば、これが正攻法ですね。
    既存のアプリでも、2.x 系のサポートを終わらせて、移行するのも良いかもしれません。
    ただ、残念ながらそうもいかない場合も多いと思います。
  2. DialogFragment.show() を Override して使う
    DialogFragment.show() の中は以下のようになっています。

    android.support.v4.app.DialogFragment (Support Library rev. 20)
        public void show(FragmentManager manager, String tag) {
            mDismissed = false;
            mShownByMe = true;
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commit();
        }
    

    そこで、DialogFragment の子クラスで以下のように show() を Override します

    public class TestDialog extends DialogFragment {
        // 省略
    
        @Override
        public void show(FragmentManager manager, String tag) {
            // mDismissed = false;
            // mShownByMe = true;
            try {
                Field mDismissedField = DialogFragment.class.getDeclaredField("mDismissed");
                mDismissedField.setAccessible(true);
                mDismissedField.set(this, true);
                Field mShownByMeField = DialogFragment.class.getDeclaredField("mShownByMe");
                mShownByMeField.setAccessible(true);
                mShownByMeField.set(this, true);
            } catch (NoSuchFieldException |
                    IllegalAccessException |
                    IllegalArgumentException e) {
                throw new RuntimeException(e);
            }
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commitAllowingStateLoss();
        }
    
        // 省略
    }
    

    commit()commitAllowingStateLoss() に書き換えたいだけなのですが、リフレクションを使わないといけないため、とても見にくくなってしまいました。
    mDissmissedmShownByMe を無視して書いてある例もありましたが、あまりお勧めはできません。

    この方法、Dialog の状態を保存しておいて、表示の時に復活させようとすると失敗します。
    また、Support Library を更新するたびに、DialogFragment.show() の内容を確認して、修正があったら小クラスの方にも修正を加えなければなりません。
    結構面倒くさいです。
  3. onActivityResult() 内では Dialog を表示せず、onResumeFragments() 内で表示する
    現時点で最も現実的な解はこれかもしれません。
    onActivityResult() 内で Dialog を表示することを諦め、フラグを設定するだけにします。
    そして、onResumeFragments() 内でフラグをチェックして Dialog を表示するようにします。

    public class MainActivity extends FragmentActivity {
        private static final String TEST_DIALOG_TAG = "dialog";
        private static final int NO_DIALOG = -1;
        private int mShowDialogNo = NO_DIALOG;
    
        // 省略
    
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data){
            mShowDialogNo = requestCode;
        }
    
        @Override
        protected void onResumeFragments() {
            super.onResumeFragments();
    
            switch (mShowDialogNo) {
            case 1:
                DialogFragment dialog = (DialogFragment) getSupportFragmentManager().findFragmentByTag(TEST_DIALOG_TAG);
                if (dialog == null) {
                    dialog = new TestDialog();
                }
                dialog.show(getSupportFragmentManager(), TEST_DIALOG_TAG);
                break;
            default:
                break;
            }
            mShowDialog = NO_DIALOG;
        }
    }
    

    記述量は増えてしまいますが、仕方がありません。

補足

一部情報では Support Library rev. 18 で修正されたとあったのですが、rev. 20 で確認したところ、修正されている形跡はありませんでした。
残念ながら、2014年8月16日現在でもこの問題は存在するようです。
2014/08/12

Android の Log をリリース時にだけ表示しないようにする方法

Android に限らず、プログラムを書く上で Log を仕込むのは重要な作業です。
しかし、デバッグ用に出力した Log はリリースビルドでは出力しないようにしたいものです。
Java にはプリプロセッサがありませんので、Android アプリの開発時、私は以下の様な方法(もしくは、同様のライブラリを作成すること)でデバッグ時にログを出力しないようにしてきました。

if (BuildConfig.DEBUG) {
   LOG.d(TAG, "This is a log message");
}

しかし、先日購入した 50 Android Hacks 開発現場ですぐに役立つヒントとコード に、もっとエレガントな方法が載っていました。

それは ProGuard を利用して、後から Log を削除する方法です。
確かに、Android の開発環境ではリリース時にしか ProGuard がかからないようになっていますから、うってつけの方法と言えます何故、今まで気付かなかったのだろうか…

以下、Android Studio と Eclipse での具体的な方法です本の中の tips を勝手に公開してしまいました。著者の方々申し訳ありません。
Android Studio に関してなど+αがあるのでご容赦いただきたいです。
皆様、他にも良い tips が載っていますので、是非本を買ってください。なお、著者の方々と私には一切の利害関係はございません。


Android Studio

proguard-rules.pro に以下の記述を追加

proguard-rules.pro
-assumenosideeffects public class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
    public static *** wtf(...);
}

以下のように ProGuard を有効にします。
proguard-android-optimize.txt を使っているところに注意。デフォルトでは proguard-android.txt なので書き換える必要があります。

app/build.gradle
    buildTypes {
        release {
            runProguard true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }


Eclipse

proguard-project.txt に以下の記述を追加。

proguard-project.txt
-assumenosideeffects public class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
    public static *** wtf(...);
}

以下のように ProGuard を有効にします。
proguard-android-optimize.txt を使っているところに注意。デフォルトでは proguard-android.txt なので書き換える必要があります。

project.properties
proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt



2014/07/22

compileSdkVersion を 'android-L' にすると INSTALL_FAILED_OLDER_SDK が発生する

Android Studio を 0.8.1 にして New Project を作成すると、app/build.gradle は以下のようになっています。

app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 'android-L'
    buildToolsVersion "20.0.0"

    defaultConfig {
        applicationId "com.kokufu.test"
        minSdkVersion 8
        targetSdkVersion 'L'
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
// 以下略

これ、そのままコンパイルすると、以下のようなエラーが発生してしまいます。
pkg: /data/local/tmp/com.kokufu.test
Failure [INSTALL_FAILED_OLDER_SDK]

もちろん、Android L (API 20, L preview) はインストールしてあります。

で、検索してみたところ、やっぱりバグみたいです。

というわけで、compileSdkVersion を 20 にしてやるとうまくコンパイル出来るようになりました。

app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 20
    buildToolsVersion "20.0.0"

    defaultConfig {
        applicationId "com.kokufu.test"
        minSdkVersion 8
        targetSdkVersion 20
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            runProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
// 以下略
2014/07/11

Nexus10 用に Android 用有線LANアダプタを買ってみました

有線LANしか無い環境に Nexus 10 を持っていかなければならなくなったので、OTG対応有線LANアダプタを買ってみました。

注意
ただ、最初にお断りしておかねばなりません。
一応、動作はしたのですが、かなり微妙な動きになっているので、決してお勧め出来るものではありません。
「ちゃんと動作した」という報告を期待してこのページに来られた方には申し訳ないのですが、その点御了承の上、お読みください。

購入したのは以下。
Nexus 7 (2012) には対応しているけど、Nexus 7 (2013) には対応していないとのこと。この時点で、結構ドキドキ。



で、届いた商品がこちら。
型番は RCG-MULA01 のようです。
Model No は VK-88772A。Amazon の写真と微妙に違うなぁ。

で、Nexus 10 (Android 4.4.4) につないでみたところ、うんともすんとも言わず…
ダメかなーと思いながら、Nexus 10 を再起動してみると・・・動いたもちろん、WiFiは切っています。!!


よしよし、と思って使っていたのですが、次に使おうと思ってつなぐと動かず…。今度は再起動しても動かず。
しかし、USB を挿抜すると動きました。

現在、どのような状況下でうまく動くのかがハッキリわかっていません。
どうも、有効なIPアドレスが取れるという意味ですLANケーブルを刺した状態でUSBをつなぐのが最低条件のようです。
一度うまくいくと、USBを挿抜しても大丈夫なことが多いです。

今後、何かわかったら情報を更新したいと思います。
何か情報をお持ちの方がいらっしゃいましたら、是非教えて下さい。

ちなみに、Nexus 7 (2012) では今のところ問題なく動作しています。

2014/06/29

BeagleBoard-xM の USER ボタンでシャットダウンさせる方法

@ Ubuntu 13.10 (GNU/Linux 3.13.3-armv7-x10 armv7l)

私は、BeagleBoard-xM を MPD サーバとして使用しているのですが、キーボードもディスプレイも外してしまっています。
そのため、シャットダウンするためにはキーボードとディスプレイをつなぐか、SSH でログインしないといけません。
どっちにせよ、結構面倒くさいです。

というわけで、普段使われていない USER ボタンをシャットダウンボタンに変更してみました。
USER ボタン

/etc/init.d/user_button_shutdown.pl を作成
#!/usr/bin/perl

my $pid = fork;
die "$!" unless defined $pid;

my $comm = $ARGV[0];

if ($pid) {
  exit 0;
}

print $comm;
if ($comm ne "start") {
  exit 0;
}

open (INPUT, '/dev/input/event0') or die "$!";

my $inp = 0;
while( !($inp) ){
    read(INPUT, $inp, 1);
}
close(INPUT);

exec("poweroff");

exit 0;

起動時に実行されるようにする。
$ sudo chown root:root
$ sudo chmod 744
$ sudo update-rc.d user_button_shutdown.pl defaults
update-rc.d: warning: /etc/init.d/user_button_shutdown.pl missing LSB information
update-rc.d: see 
 Adding system startup for /etc/init.d/user_button_shutdown.pl ...
   /etc/rc0.d/K20user_button_shutdown.pl -> ../init.d/user_button_shutdown.pl
   /etc/rc1.d/K20user_button_shutdown.pl -> ../init.d/user_button_shutdown.pl
   /etc/rc6.d/K20user_button_shutdown.pl -> ../init.d/user_button_shutdown.pl
   /etc/rc2.d/S20user_button_shutdown.pl -> ../init.d/user_button_shutdown.pl
   /etc/rc3.d/S20user_button_shutdown.pl -> ../init.d/user_button_shutdown.pl
   /etc/rc4.d/S20user_button_shutdown.pl -> ../init.d/user_button_shutdown.pl
   /etc/rc5.d/S20user_button_shutdown.pl -> ../init.d/user_button_shutdown.pl

問題点

すごく雑に作ったので、 missing LSB information が出ちゃうけど、現実的には問題ないから放置しております…

あと、キーボード等がつながっていると、USR Switch が /dev/input/event1 や event2 に割り当てられてしまう問題が。
動的に確認する方法はないものだろうか?