2011年8月27日土曜日

static変数の値は次回起動へ継承される

static変数の値は次回起動へ継承される

finish()が実行されるタイミングの続きの話題です。
finish()が実行されるとdestroy()が実行されます。しかし、そのアプリのプロセスは生き残っています。DDMSやデバッガでそのことを確認できることは周知のとおりです。

destroyされた後の状態で、エミュレータのアプリの起動アイコンをクリックすると、destroyされたアプリが立ち上がってくるのです!おお!こんなことができるとは知らなかった。デバッガも立ち上がって来るので、finish()したアプリが同一のプロセスで稼働しているということが確認できます。
他のプラットフォームにおける思い込みを捨てて、認識を新たにする必要があります。

そこで、下記の実験用プログラムを作成し、
1.デバッガから実行しました。実行させても、finish()により、直ぐに終了します。
2.そして、エミュレータにある(finish()によるdestroy後の)アプリの起動アイコンをクリックし実行させました。
つまり合計で2回実行させました。

public class StaticActivity extends Activity {
    static String sStatic = "at field";
    String sNoStatic = "at field";
    
    public StaticActivity(){
        Log.d("StaticAct", "at constructor:sStatic=" + sStatic);
        Log.d("StaticAct", "at constructor:sNoStatic=" + sNoStatic);
        sStatic = "at constructor";
        sNoStatic = "at constructor";
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.d("StaticAct", "onCreate:sStatic=" + sStatic);
        Log.d("StaticAct", "onCreate:sNoStatic=" + sNoStatic);
        sStatic = "at onCreate";
        sNoStatic = "at onCreate";
        finish();
    }
}

ログには次のとおり出力されました。
第一回目の出力
08-26 15:36:28.415: DEBUG/StaticAct(5031): at constructor:sStatic=at field
08-26 15:36:34.515: DEBUG/StaticAct(5031): at constructor:sNoStatic=at field
08-26 15:36:38.855: DEBUG/StaticAct(5031): onCreate:sStatic=at constructor
08-26 15:36:38.855: DEBUG/StaticAct(5031): onCreate:sNoStatic=at construct
第二回目の出力
08-26 15:37:16.655: DEBUG/StaticAct(5031): at constructor:sStatic=at onCreate
08-26 15:37:16.655: DEBUG/StaticAct(5031): at constructor:sNoStatic=at field
08-26 15:37:17.207: DEBUG/StaticAct(5031): onCreate:sStatic=at constructor
08-26 15:37:17.207: DEBUG/StaticAct(5031): onCreate:sNoStatic=at constructor

第二回目のログのstatic変数の最初の内容は、第一回目の実行の最後に代入した値("at onCreate")が残ったままになっています。
このことから、adsaria氏が言うように、「次回、再利用する時にはスタティックなメンバ変数の初期化は行ってくれない。」と言えるのです。

このような仕組みを、便利だと捉えるか、危険だと捉えるか、考え方はいろいろありましょうが、いずれにしても留意はしなければなりません。

2011年8月26日金曜日

finish()が実行されるタイミング

finish()が実行されるタイミング

finish()が実行されるタイミングについてはadsaria moodfinish()ではプロセスは終わらないに書かれているとおりです。
私なりに、私自身が納得できるように、実験用プログラムを作りました。


public class FinishTestActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        ToneGenerator tg;
        tg = new ToneGenerator(AudioManager.STREAM_ALARM, 100);
        tg.startTone(ToneGenerator.TONE_DTMF_1);
        Log.d("FinishTest", "before finish()");
        finish();
        Log.d("FinishTest", "after finish()");
        tg.stopTone();
    }


    @Override
    protected void onDestroy() {
        Log.d("FinishTest", "onDestroy()");
        super.onDestroy();
    }
}

もし、finish()により、いきなりプログラムが終了するのであれば、tg.stopTone()が実行されないことになりますので、ビープ音が鳴り続けます。
しかし、ビープ音は鳴り止みますので、tg.stopTone()は実行されたということです。
ログには次のとおり記録されます。

08-26 11:35:18.325: DEBUG/FinishTest(785): before finish()
08-26 11:35:18.365: DEBUG/FinishTest(785): after finish()
08-26 11:35:19.635: DEBUG/FinishTest(785): onDestroy()

私なりの言葉で説明すると次のとおりになります。
命令の発出の仕方にはsend方式とpost方式があります。
send方式は命令を発出したら、即座にそれを実行する方式です。
post方式は命令を「メッセージ格納器」に入れるだけです。命令を受け取る側は暇になった時に「メッセージ格納器」から命令を取り出して実行するのです。
上記プログラムの例だと、onCreateメソッドの実行が完了した後が、暇になる時です。
finish()はpost方式なのです。


finish()を実行してもプロセスが生きていることは、DDMSやデバッガを使っていれば分かることなので、(内心、変だなと思いつつも)当たり前のようにも感じていました。

finish()ではプロセスは終わらないの記事で重要なのは、次の指摘です。
finish()を呼び出しても、アクティビティはDestroyはされるものの、プロセスとして残っていて(つまりメモリ上にクラスがロードされた状態で残っていて)、次回、再利用する時にはスタティックなメンバ変数の初期化は行ってくれない。
このことから、次の結論が出てきます。
アクティビティ開始時に値が定まっていなければならないメンバ変数の初期化は、宣言時ではなく、onCreate、もしくは必要の応じて、onStart、onResumeで実行する必要がある。
この場合、気になるのが、「次回、再利用する時」です。
onDestroy()した後で、利用者が同一のアプリの起動ボタンを押し下げた時が含まれるのであれば、この事実を周知しなければなりません。実際に役に立つプログラマは少数ですけど。

この続きをstatic変数の値は次回起動へ継承されるに書きました。

2011年8月23日火曜日

非同期でMediaPlayerを使う

非同期で(asynchronously)MediaPlayerを使う

Mediaに書かれているとおりに書いたつもりのsourceを掲載しておきます。
音楽ファイルは次のサイトのを使いました。
MP3ダウンロード(童謡 唱歌 世界の民謡)

動作内容:SDカードにある、複数の音楽ファイル(mp3)を連続再生する。実験・学習目的なので、簡単にするため、serviceを使っていません。

留意点:指定された音楽ファイルが存在しない場合、setDataSourceメソッドでエラーを補足するのでは無く、prepare系でエラーを補足する、というのがMediaPlayerの仕様です。
では、setDataSourceメソッドのIOExceptionは何のために存在するのでしょうか。そんなことまでは、私は興味はありません。


public class SoundWAVActivity extends Activity
    implements
    MediaPlayer.OnCompletionListener,
    MediaPlayer.OnPreparedListener,
    MediaPlayer.OnErrorListener
    {
    
    MediaPlayer mp = null;;
    int iFileNumber = 1647;


    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        StartMusic();
    }
    
    //演奏が終了した場合、呼ばれる。
    @Override
    public void onCompletion(MediaPlayer arg0) {
        if(mp==arg0) StartMusic();
    }


    //演奏の準備が完了したら呼ばれる。
    @Override//Running asynchronously
    public void onPrepared(MediaPlayer ThisMP) {
        if(mp==ThisMP) mp.start();
    }


    //演奏の準備の段階でエラーが発生した時に呼ばれる。
    @Override//Handling asynchronous errors
    public boolean onError(MediaPlayer ThisMP, int what, int extra) {
        if(mp==ThisMP){
            if(what==MediaPlayer.MEDIA_ERROR_SERVER_DIED)
                Log.e("SoundWAV", "onError:MEDIA_ERROR_SERVER_DIED");
            else//ファイルが存在しない場合、ここに来る。
                Log.e("SoundWAV", "onError:MEDIA_ERROR_UNKNOWN");
            //エラーになっても何もしなければ、onCompletionメソッドが呼ばれる。
            finish();
            return true;//次のonCompletion()が呼ばれないようにする。
        }
        return false;
    }


    @Override
    protected void onDestroy() {
        if(mp!=null) mp.release();//Performing cleanup
        super.onDestroy();
    }
    
    private void StartMusic(){
        File f, fSD;
        String sFile;
        
        fSD = Environment.getExternalStorageDirectory();
        fSD = new File(fSD, "music");
        sFile = "m" + String.valueOf(iFileNumber) + ".mp3";
        f = new File(fSD, sFile);
        sFile = f.getPath();//ファイルの存在確認はMediaPlayerで行う。
        iFileNumber++;
        
        if(mp!=null) mp.release();
        mp = new MediaPlayer();
        if(mp==null){
            Log.e("SoundWAV", "mp == null");
            finish();
            return;
        }
        try{
            mp.setDataSource(sFile);
        }
        catch (IOException e) {//ファイルが存在しなくても。ここには来ない。
            Log.e("SoundWAV", "IOException");
            finish();
            return;
        }
        catch (IllegalStateException e) {
            //release()&Newを実行しない場合、ここに来る。
            Log.e("SoundWAV", "IllegalStateException");
            finish();
            return;
        }
        catch (IllegalArgumentException e) {
            Log.e("SoundWAV", "IllegalArgumentException");
            finish();
            return;
        }
        
        mp.setOnCompletionListener(this);
        mp.setOnPreparedListener(this);
        mp.setOnErrorListener(this);
        mp.prepareAsync ();
        Toast.makeText(this, "Start", Toast.LENGTH_SHORT).show();
    }
}

2011年8月19日金曜日

テキストファイルのコード変換 その後

テキストファイルのコード変換 その後


以前テキストファイルのコード変換を書きましたが、過ちがありました。


そして、ここにも問題が有ることが判明しましたので、こっちに後日談を書きました。


入力用のテキストデータとして次の文字列を使用しました。
原文:つかいやすいといえます。


Shift_JISで変換すると正確にコード変換されます。
これをUTF-16で復号すると次のように変換されます。
UTF-16で復号後:苂芩芢苢芷芢苆芢芦苜芷腂


これだと、コード変換じゃなくって、中国語への翻訳ですな。
UTF-16を使うと、エラーが無く、変換ができてしまうのです。中国語に。


Charsetの順序を"UTF-8", "UTF-16", "Shift_JIS"にすると、UTF-16変換がエラー無く実行されるため、Shift_JIS変換が実行されません。このため、上記の不都合が発生してしまいました。


この問題を解決したプログラムを掲載しておきます。



public class CodeChangeActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        int iSize;
        File fSD, f;
        FileInputStream fs;
        byte[] ba;
        
        fSD = Environment.getExternalStorageDirectory();
        f = new File(fSD, "tyi.txt");
        ba = new byte[6000];
        try{
            fs = new FileInputStream(f);
            iSize = fs.read(ba, 0, 6000);
            fs.close();
            
        }
        catch(FileNotFoundException e){
            return;
        }
        catch(IOException e){
            return;
        }
        
        int i;
        Charset charset;
        CharsetDecoder d;
        CoderResult r;
        CharBuffer cb;
        ByteBuffer bb;
        String[] sCharset = 
        {"UTF-8", "Shift_JIS", "EUC-JP", "ISO-2022-JP", "UTF-16"};
        bb = ByteBuffer.wrap(ba);
        cb = null;
        for(i=0; i<sCharset.length; i++){
            if(Charset.isSupported(sCharset[i])==true){
                charset = Charset.forName(sCharset[i]);
                d = charset.newDecoder();
                cb = CharBuffer.allocate(iSize);
                r = d.decode(bb, cb, true);
                if(r.isError()==false) break;
                bb.rewind();
            }
        }
        String s;
        TextView tv;
        if(i>=sCharset.length){
            s = "have no charset";
        }
        else{
            cb.rewind();
            s = cb.toString();
        }
        tv = (TextView)findViewById(R.id.MyText);
        tv.setText(s);
    }
}

CharsetDecoder.decodeメソッドの長所は、コード変換エラーが発生した場合、その時点で実行を停止することです。

2011年8月17日水曜日

ArrayAdapterのsort()を使ってみる

複数の文字列を画面に表示する場合、それらをアルファべット順とかで並べ替えたい(ソートしたい)ですね。

多くのプログラマはjavaベースのCollections.sortを行うのかもしれません。
私が作ったのはArrayAdapterのsortメソッドを使っているので、androidに特化したものです。

compareメソッド内で文字列の比較を行います。一覧内で先頭や末尾に表示したい文字列を設定できます。例えば、ディレクトリを意味する文字列の末尾に/があれば、それを一括りにもできますので、ディレクトリ群を最初に、ファイル群を後に表示することもできます。

androidのファイルシステムでは、ファイルやディレクトリはソートされた状態ではありませんので、このソートを使って表示する必要があります。

AASortActivity.java

public class AASortActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        ListView lv;
        lv = new ListView(this);
        setContentView(lv);
        
        ArrayAdapter<String> aa;
        aa = new ArrayAdapter<String>(this, R.layout.list_item, MyStringData);
        aa.sort(new MyComparator());
        lv.setAdapter(aa);
    }
    
    public class MyComparator implements Comparator<String>{
        @Override
        public int compare(String s1, String s2){
            if(s1.equals("First")==true) return -1;
            if(s2.equals("First")==true) return 1;
            
            if(s1.length()==0) return 1;
            if(s2.length()==0) return -1;
            
            if(s1.charAt(s1.length()-1)=='/'
            && s2.charAt(s2.length()-1)!='/') return -1;
            if(s1.charAt(s1.length()-1)!='/'
            && s2.charAt(s2.length()-1)=='/') return 1;
            
            return s1.compareTo(s2);
        }
    }
    
    static final String[] MyStringData = new String[] {
        "qqq8", "qqq3", "qqq4", "qqq/", "qqqr",
        "www5", "www7", "www/", "www3", "wwwe",
        "First", "..", "",
        "eee2", "eee0", "eee/", "eee6", "eee3"
    };
}

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
</TextView>

この技術を使ってファイル選択ダイアログボックスの作成をしてみました。

2011年8月15日月曜日

テキストファイルのコード変換

Microsoft社のWindowsのディスクにあるテキストファイル内の文字データのコードはShift_JISです。
このファイルを、そのままandroidシステム内のディレクトリにコピーすると、Shift_JISデータのままのファイルになってしまいます。
このことを何も知らずに、androidで当該ファイルを開くと文字化けが発生します。
androidシステムでは文字コードはUTF-8なのです。

この情報をandroid端末利用者に周知して文字化けを未然に防ぐことも一つの方法ですが、周知が行き届くとは限りません。また、利用者側でコード変換をするのは面倒です。

このため、androidアプリ側でコード変換を行います。サンプルのsourceを作成しましたので掲載しておきます。

入力のテキストファイルのコードが何であるか事前にはわかりません。このため、適正な復号が行えるまで、異なったコードで復号を試みることがポイントです。

動作内容:SDカードのルートディレクトリのsammm3.txtファイル内の文字データをandroid用のコードに変換して画面に表示します。


本文章を掲載後、プログラムに誤りを見つけましたので、修正後をテキストファイルのコード変換 その後に書いておきました。


/*
 * 参考になったサイト
 * http://www.ne.jp/asahi/hishidama/home/tech/java/Buffer.html
 * http://www.javadb.jp/Code.sd?id=53
 * http://blog.goo.ne.jp/xmldtp/e/b1fb4c01508e21a1325c41c3a44c7655
 * http://www.opensourcejavaphp.net/java/harmony/tests/api/java/nio/charset/CharsetTest.java.html
 */
public class CodeChangeActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        int iSize;
        File fSD, f;
        FileInputStream fs;
        byte[] ba;
        
        fSD = Environment.getExternalStorageDirectory();
        f = new File(fSD, "sammm3.txt");
        ba = new byte[6000];
        try{
            fs = new FileInputStream(f);
            iSize = fs.read(ba, 0, 6000);
            fs.close();
            
        }
        catch(FileNotFoundException e){
            return;
        }
        catch(IOException e){
            return;
        }
        
        int i;
        Charset charset;
        CharsetDecoder d;
        CoderResult r;
        CharBuffer cb;
        ByteBuffer bb;
        String[] sCharset = {"UTF-8", "UTF-16", "Shift_JIS"};


        bb = ByteBuffer.wrap(ba);
        cb = null;
        for(i=0; i<sCharset.length; i++){
            charset = Charset.forName(sCharset[i]);
            d = charset.newDecoder();
            cb = CharBuffer.allocate(iSize);
            r = d.decode(bb, cb, true);
            if(r.isError()==false) break;
            bb.rewind();
        }
        String s;
        TextView tv;
        if(i>=sCharset.length){
            s = "have no charset";
        }
        else{
            cb.rewind();
            s = cb.toString();
        }
        tv = (TextView)findViewById(R.id.MyText);
        tv.setText(s);
    }
}

2011年8月9日火曜日

【教訓】コンストラクタはpublicにせよ

【教訓】コンストラクタはpublicにせよ

ActivityのクラスにおいてbindServiceメソッドを用いて、Serviceのクラスを立ち上げようとしたところ、下記の実行時エラーがLogに出る。


DEBUG/dalvikvm(5311): newInstance failed: Ljp/(省略)/(省略)/XXXService;.<init>() not accessible to Landroid/app/ActivityThread;

このエラーは、Serviceのクラスのコンストラクタ(constructor)にアクセス修飾子(access modifier)を記述していなかったため発生したものである。

アクセス修飾子を記述しない場合、デフォルトのアクセス制限が課せられる。この制限の強度は、privateよりは弱く、protectedよりは強いというものである。

何も考えずに、何も書かないでおくと、何も想定していないエラーに遭遇する。
どうせ何も考えないのであれば、コンストラクタのアクセス修飾子はpublicにしておこう。

コンストラクタをpublicにした場合における不都合ってのは、普通は、ありえない。逆に、コンストラクタにアクセス制限をかけると上記の災難に遭遇する。
クラス宣言をpublicにしておきながら、そのコンストラクタにはアクセス制限を加えるってのは特殊な設計ですな。そういう場合には、そういうアナウンスをしっかりすべきですな。
実際には、特殊な設計を積極的に行ったのではなく、な~んにも考えずに書きました、ってことかもしれない。

で、繰り返しになりますが、
【教訓】コンストラクタはpublicにせよ

2011年8月5日金曜日

AlertDialogが正常に表示されない件 その3

AlertDialogが正常に表示されない件 その3

これはAlertDialogが正常に表示されない件 その2の続編です。

android developersのR.layoutには各constantsに対する解説が書かれていません。このため、読解するための難易度が上がります。まるで、なぞなぞです。
皆さんは、このなぞなぞをお楽しみ頂けたでしょうか。

私は、このなぞなぞをなんとか解くことができました。sourceは次のとおりです。

public class AlertDialogActivity extends Activity
implements
DialogInterface.OnClickListener
{
AlertDialog ad;

@Override
public void onClick(DialogInterface dialog, int which) {
if(dialog==ad){
           dialog.cancel();
           finish();
}
  }


@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        AlertDialog.Builder b;
        ArrayAdapter<CharSequence> aa;
        ArrayList<CharSequence> al;
        
        al = new ArrayList<CharSequence>();
        al.add("なんじゃ");
        al.add("これは");
        al.add("動かん");
        al.add("や");
        al.add("ないか");
        
        //ここの第二引数が重要
        aa = new ArrayAdapter<CharSequence>(
            this,
            android.R.layout.select_dialog_singlechoice,
            al
        );
        
        b = new AlertDialog.Builder(this);
        b.setTitle("アラートダイアログ");
        b.setSingleChoiceItems(aa, 0, this);
        b.setNegativeButton("閉じる",
            new DialogInterface.OnClickListener(){
                public void onClick(DialogInterface dialog, int id) {
                    dialog.cancel();
                    finish();
                }
            }
        );
        ad = b.show();
    }
}


この技術を使ってファイル選択ダイアログボックスの作成をしてみました。

2011年8月4日木曜日

AlertDialogが正常に表示されない件 その2

AlertDialogが正常に表示されない件 その2

AlertDialogを作成するにはAlertDialog.Builderを使います。
このAlertDialog.Builderには、引数の形式が異なる4種類のsetSingleChoiceItemsメソッドが用意されています。

皆さんの多くが使うのは次の形式でしょう。第一引数がCharSequence[]になっています。
public AlertDialog.Builder setSingleChoiceItems (CharSequence[] items, int checkedItem, DialogInterface.OnClickListener listener)
私も止むを得ずこれを使いました。

第一引数にListAdapterを設定するメソッドも用意されています。
引数にCharSequence[]を設定するよりも、ListAdapterを設定する方が便利です。

私は、リソースから文字列データを取得するのではなく、リソース以外の場所からデータを取得・追加したいのです。
しかし、残念ながら、ListAdapter型setSingleChoiceItemsメソッドは正常に動きません。真っ白に表示されてしまうのです。

どなたか、我こそは腕自慢ってな御仁は、正常に動作するListAdapter型setSingleChoiceItemsメソッドのサンプルプログラムをブログかなんかに掲載してください。謝礼はありませんが。

この続編をAlertDialogが正常に表示されない件 その3に書きました。