2011年11月26日土曜日

CheckBoxを右端に配置する


CheckBoxを右端に配置する

下記の例では、TextViewは、CheckBoxの縦幅の中央に配置されます。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
    <CheckBox
        android:id="@+id/CheckBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checked="true"
        android:layout_alignParentRight="true"
        >
    </CheckBox>
    <TextView
        android:text="aaaaaa"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_alignLeft="@id/CheckBox"
        android:layout_alignParentLeft="true"
        android:layout_alignBottom="@id/CheckBox"
        android:gravity="center_vertical"
        >
    </TextView>
</RelativeLayout>

2011年11月23日水曜日

Releasing statement in a finalizer

Releasing statement in a finalizer

下記の趣旨の警告が出た。
WARN/SQLiteCompiledSql: Releasing statement in a finalizer. Please ensure that you explicitly call close() on your cursor:
WARN/SQLiteCompiledSql: android.database.sqlite.DatabaseObjectNotClosedException: Application did not close the cursor or database object that was opened here

WARN/SQLiteCompiledSqlと書かれてあるため、データベース関係が原因であろうと想像する。databaseをcloseすれば解決はできる。他のサイトを見ても、同様の趣旨の回答が書かれてある。警告に書かれてあるとおりであるので、何も問題は無い。

しかし、私のアプリの場合、そこでcloseをしてはいけないのである。データベースを使い続けたいのである。

解決のヒントはfinalizerであった。finalizerをキーワードにして調べた。
参考:

上記エラーが発生するのは、エミュレータである。実機では発生しない。
そこで、エミュレータのheapサイズを、デフォルトの24から200に増やしてみた。すると、上記警告は発生しなくなった。
設定方法は次のとおりである。
Window>Android SDK and AVD Manager>AVDを編集する画面を表示させる。
Target欄に適当な事項を設定すると、HardWare欄にMax VM application heap sizeの行が表示されるので、そこのValueを24から200に変えれば良い。

これで問題の原因が特定できた。ガベージコレクション(garbage collection; GC)の仕掛けが原因であった。heapサイズが小さい場合、警告を発するのである。

これは、警告が発せられたという程度の問題である。データベースを破棄しているのではない。なのでアプリとしての実害は無い。
データベースを引き続きそのまま使い続けたいなら、警告を無視すれば良い。
ただし、ズラズラと長いメッセージが表示されるのは迷惑なので、heap sizeを大きくすれば良い。

追記(2012年5月3日)
heap sizeを大きくしてみたところ、エミュレーターが起動しなくなった。そこで、heap size以外の方法で対処することにした。
実は、この警告が発生する箇所では、SQLiteStatement Classを使ってレコードのINSERTを行っていた。
SQLiteStatement Classを使うのをやめて、ContentValues Classを使う方法にした。
すると、警告は出なくなった。
むむむ!

参考:ContentValues Classのsample code
                ContentValues v;
                v = new ContentValues();
                v.put("Item1", "aaa");
                v.put("Item2", "bbb");
                mDb.insert("MyTable", null, v);

2011年11月21日月曜日

RelativeLayout内に書くViewの順番

RelativeLayout内に書くViewの順番

この記事には、筆者自身がよく分かっていない事柄を、臆面も無く、書いてある。例えば、「宣言」とか「参照」とか「生成」という用語の使い方だ。
しかし、筆者の経験からして、機能面ではほぼ間違いのない事実であるので、ここに書き留めておき備忘録としておきたい。

RelativeLayoutをxmlで書く方法
RelativeLayoutは、それに含まれるViewを相対的な位置に配置するGroupViewである。相互の相対的な位置であるから、配置コマンドの中にViewを指定して書くことになる。
Viewを指定して書く場合、その書く順番に留意をしなければならない。IDを宣言しているコマンドを上に書いて、その下に、そのIDを参照しているコマンドを書くのである。
下記のxmlのsample codeの例では、IDを宣言している箇所を赤色で塗った。そして、それを参照している箇所を青色で塗った。
宣言を先に書き、参照を後に書くのである。

下記sampleでは、BottomViewを、物理画面の下端に配置するため、そのコマンドを、xmlの一番下に書いてしまいそうになる。しかし、そのようにすると、参照ができなくなり、エラーになる。
このエラーを発生させないようにするため、+記号付きの書式で書くこともできるが、「エラーを発生させたくない」ということを動機として+記号付き書式を採用して良いのであろうか。。。。まあ、別に良いですけど。

xml sample code
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    >
    <TextView
        android:id="@+id/TopView"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:text="@string/ThisIs"
        android:layout_alignParentTop="true"
        >
    </TextView>
    <TextView
        android:id="@+id/BottomView"
        android:text="@string/ThisIs"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        android:layout_alignParentBottom="true"
        >
    </TextView>
    <ScrollView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" 
        android:layout_above="@id/BottomView"
        android:layout_below="@id/TopView"
        >
(以下省略)

IDの書式
以上の事柄を理解するためには、resourceのIDを取り扱うコマンドの書式を理解しておく必要がある。次のような似た書き方がある。
  • "@+id/TopView"
  • "@id/TopView"
@の次に+記号を付けるかどうかの違いである。
筆者は、これらを正確に理解していない。しかし、経験上、次のことが言える。

+記号を付けると、その+記号の右側の文字列のディレクトリがR.javaに生成される。そして、そのディレクトリの下に、/の右側にある文字列をIDとするものが生成される。
例えば、適当なViewの属性に、次のように書いてみる。
<LinearLayout
    android:id="@+OhMyGod/What"
(以下省略)
これをコンパイルして、genディレクトリの下にあるR.javaのRを見ると、OhMyGodがあり、更にその下にWhatがある。
このことから、+記号を付けた書式では、IDが生成されることが分かる。

+記号の無い書式は、お馴染みである。
例えば、"@string/app_name"というのは、string resourceの中のapp_nameというIDを指している。そして、このIDに関連付けられている文字列を参照することができるのである。
つまり、+記号の無い書式は、resourceへの参照機能を有するのである。生成はしない。
xmlは、上の行から下の行に向かって解析される。上の方で、IDの生成を行っておき、少なくとも、その行よりも下で、そのIDへの参照を行わねばならない。生成が無い段階で、そのIDを参照しようとするとエラーになる。

実際の書き方
以上の説明は、書式の趣旨に沿って書く方法であった。
試作品を作る場合や、複雑な形状のRelativeLayoutを作る場合であれば、+記号がある書式を用いることも有り得る。例:"@+id/TopView"
この書式は、既に述べたように、IDへの参照では無く、IDの生成である。

同一のIDをあちこちで生成したとしても、エラーにはならず、生成したIDをお互いに共有する仕掛けになっているようである。このため、上記に書いたような、書く順番を無視して書くことができる。
エラーを発する「参照書式」よりも、エラーを発しない「ID生成書式」が好まれるかもしれない。

もし、エラーを発する書式で書くのであれば、エラーを発生させないように書かざるを得ない。そうすれば、堅牢な作りになると思われる。IDの生成は(複数書くのではなく)1か所だけに留める書き方にするのである。
もし、エラーを発生する仕掛けがあるならば、それを喜んで使えば良い。そして、エラーを発生させないように書けば良い。

参考
android developersのCommon Layout ObjectsのRelativeLayoutに下記の注意書きがあります。
Note that the attributes that refer to relative elements (e.g., layout_toLeft) refer to the ID using the syntax of a relative resource (@id/id).

For example, assigning the parameter toLeft="my_button" to a TextView would place the TextView to the left of the View with the ID my_button (which must be written in the XML before the TextView).

2011年11月20日日曜日

起動直後にViewのサイズを動的に変更する

起動直後にViewのサイズを動的に変更する

アプリが立ち上がってしまった後でViewのサイズを取得・変更することは比較的簡単にできるかもしれません。
しかし、困難なのは、起動直後に、動的に変更した後の画面を、一番最初に利用者に見せることです。

この記事は、様々なViewの縦幅を揃えるの続編です。幅を静的に整えるのであればxmlでダミーを挿入すれば良いのです。
しかし、本番用のアプリに、ダミーを挿入したままだと、関係者に説明が付きません。黙っていれば、誰も気が付きませんが。

そこで、本番用のために、xmlだけで対応するのではなく、codeで、Viewの生成時における幅を取得することにします。

試行錯誤 その1
Activityが起動してくる途中で次のような様々なメソッドが実行されます。
  1. onCreate
  2. onStart
  3. onResume
  4. onPostResume
一番最後に実行されるのがonPostResumeです。残念なことに、この時点においても、xmlで定義したViewのサイズを取得することはできません。
onPostResumeの中で、Handler.post(Runnable)を実行させ、onPostResumeを抜け出た直後にViewのサイズの取得を試みましたが、できませんでした。
Handler.postDelayed(Runnable, 1000)を実行させたところ、抜け出た1秒後に取得ができました。
しかし、できるからといって、こんな方法はダメです。何故ならば、この場合は1秒後に取得できましたが、端末の状況によっては、もっと長い時間がかかることも想定しなければならないからです。実際には、そんなことは無いでしょうけど。

試行錯誤 その2
そこで、次の2点を満足させる方法を探し求めることになります。
  1. Viewをextendsしてcodingする。
  2. xmlで定義したViewをcodeでも取り扱えるようにする。
この仕掛けを講じるに際し、下記のサイトの記事を参考にしました。
Activity内でのViewの遷移について

この記事で注目すべきは、Activityと、extendsしたViewとの関連付けです。
xmlのrootの要素に、Package名を書いてあるのです。この方法により、関連付けができます。この方法は、結構重要だと思います。

この方法を使って、onPostResume内で、LayoutInflaterでinflateさせて、カスタムView内のonFinishInflateでサイズを取得するように試みましたが、できませんでした。残念です。

結論
Viewの幅と高さを取得する方法を考えるの投稿欄にコメントされているように、onWindowFocusChangedを使う方法しか無いようです。
まさか、こんな名前のメソッドで対応するとは、全く想像できませんでした。このため、遠回りな試行錯誤をしてしまいました。
参考:ビューのサイズを取得する

public class AdjustHeightActivity extends Activity{
    boolean IsStarted;
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        IsStarted = false;
        setContentView(R.layout.main);
    }


    @Override
    public void onWindowFocusChanged(boolean hasFocus){
        super.onWindowFocusChanged(hasFocus);
        if(IsStarted==false){
            LinearLayout ll, llChild;
            View v;
            int i, iHeight, iChildren, iMaxHeight;
            
            iMaxHeight = 0;
            ll = (LinearLayout)findViewById(R.id.Main);
            iChildren = ll.getChildCount();
            for(i=0; i<iChildren; i++){
                llChild = (LinearLayout)ll.getChildAt(i);
                v = llChild.getChildAt(0);
                iHeight = v.getMeasuredHeight();
                if(iMaxHeight<iHeight) iMaxHeight = iHeight;
            }
            for(i=0; i<iChildren; i++){
                llChild = (LinearLayout)ll.getChildAt(i);
                v = llChild.getChildAt(0);
                iHeight = v.getHeight();
                iHeight = iMaxHeight - iHeight;
                iHeight /= 2;
                llChild.setPadding(0, iHeight, 0, iHeight);
            }
            IsStarted = true;
        }
    }
}

2011年11月19日土曜日

様々なViewの縦幅を揃える

様々なViewの縦幅を揃える

ViewGroupの中で使い道が無いと思われるものの一つとしてFrameLayoutを挙げることができるのではないだろうか。ここでは、このFrameLayoutの活用方法を無理矢理考える。

様々なViewを配置した場合、個々のViewの縦幅が異なるため、見た目が不細工になる。
個々のViewの縦幅を統一する簡便な方法として、FrameLayoutを用いる方法がある。

FrameLayoutの中に、表示したいViewと共に、縦幅が最も高いViewをダミーで挿入するのである。
ダミーViewを挿入するため、無駄なresourceを設けることにはなるが、作成が簡単であるため、試作品の作成には向いている。

ここでは静的な揃え方を書きましたが、動的に揃える書き方は、起動直後にViewのサイズを動的に変更するに書きました。

下記の例では、TextViewとSeekBarとSpinnerの3個のViewの高さを、全て、Spinnerの高さと同じ高さに揃えている。
高さ調整のため、 TextViewとSeekBarに対にしたSpinnerを非表示にしている。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <FrameLayout
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"
        >
        <TextView  
            android:layout_width="fill_parent" 
            android:layout_height="wrap_content" 
            android:text="@string/hello"
            android:layout_gravity="center_vertical"
            >
        </TextView>
        <Spinner
            android:layout_width="fill_parent" 
            android:layout_height="wrap_content" 
            android:visibility="invisible"
            >
        </Spinner>
    </FrameLayout>
    <FrameLayout
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content"
        >
        <SeekBar
            android:layout_width="fill_parent" 
            android:layout_height="wrap_content" 
            android:layout_gravity="center_vertical"
            >
        </SeekBar>
        <Spinner
            android:layout_width="fill_parent" 
            android:layout_height="wrap_content" 
            android:visibility="invisible"
            >
        </Spinner>
    </FrameLayout>
    <Spinner
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        >
    </Spinner>
</LinearLayout>

2011年11月11日金曜日

bindServiceの寿命

bindServiceの寿命


BIND_AUTO_CREATEフラグを付けて、bindServiceメソッドだけでServiceオブジェクトを生成・起動した場合、起動元でunbindServiceメソッドを実行すると、起動先のServiceオブジェクトはdestroyされる。
例えば、次のとおり、bindServiceメソッドだけでServiceを生成した場合、unbindServiceメソッドを実行すると、Serviceオブジェクトはdestroyする。
            Intent it;
            it = new Intent(this, MyService.class);
            bindService(it, conn, BIND_AUTO_CREATE);


同一のServiceクラスを引数として、startServiceメソッドとbindServiceメソッドとを実行させた場合、そこで稼働するのは、同一の1個のServiceオブジェクトである。
つまり、startServiceメソッドやbindServiceメソッド毎に、異なったオブジェクトのServiceが起動してくるのでは無い。
例えば、次のように実行しても、生成されるServiceオブジェクトは1個である
            Intent it;
            it = new Intent(this, MyService.class);
            bindService(it, conn, BIND_AUTO_CREATE);
            it = new Intent(this, MyService.class);
            it.putExtra("MyMes", true);
            startService(it);

上記2のように、startServiceメソッドとbindServiceメソッドが混在して実行した場合、unbindServiceをしても、Serviceオブジェクトがdestroyされることは無い。
startServiceメソッドとbindServiceメソッドが混在して実行されていても、(bindServiceメソッドに基づく)Messengerでのやり取りは可能である。


unbindServiceメソッドを実行した後も、Serviceを継続して存続させたいのであれば、事前にstartServiceメソッドをダミーで実行させておけば良い。

2011年11月10日木曜日

onStartCommandとその戻値について

onStartCommandとその戻値について

副題:鳩ポッポは永遠に

問題
私が現在開発中のプログラムで、Serviceがnull pointer errorを引き起こす。
Activityで作成した画面で「戻る」コマンドを実行したり、EclipseのDebugのTerminate and Removeコマンドを実行すると、Serviceがヌルポするのである。
この原因が明確になるまでは「データベースのCursorをcloseしていないです」という、全く根拠の無いエラーメッセージが出ていたため、問題解決には時間を要した。

原因
ServiceのonStartメソッドのIntent引数を使って、ある種の処理をしていたため、上記問題が発生した。
何も対策を講じていない場合であって、プロセスが強制終了した場合、ServiceのonStartメソッド又はonStartCommandメソッドが再起動してくる。そして、そのメソッドの第一番目のIntentの引数の内容はnullになっているのである。
私が開発中のプログラムでは、onStartメソッドの第一番目の引数の(nullが代入された)Intent変数を使おうとしたため、errorになった。

再現性
実験的に作ったサンプルプログラムでは、「戻る」コマンドを実行した程度では、このような現象は発生しない。
この問題をサンプルプログラムで再現するには、DDMSのStop Processを使って、プロセスを強制終了させれば良い。そうすれば、しばらく経つと、Serviceが起動してくる。
onStartメソッド又はonStartCommandメソッドを実行してしまったServiceで、この問題は発生する。
bindServiceメソッドだけを使って生成したServiceでは、(onStartメソッド又はonStartCommandメソッドは実行されないので)このような問題は発生しない。

考察
こんなのが仕様なのだろうか。バグじゃないのか?こんな変な振る舞いが仕様である訳がない。
と、思ってしまうが、仕様であるらしい。なぜならば、このような仕様こそが、♪背景音楽再生向きなのじゃ~♪、とServiceの記事に書かれている。
This mode makes sense for things that will be explicitly started and stopped to run for arbitrary periods of time, such as a service performing background music playback.
このような仕様は、background musicには良いとされているが、foreground musicには向いていない。プロセスが生成されて再起動してくるのである。再起動した後には、音楽の先頭から演奏が始まるのだ。
ごく限られたニーズにしか対応していないように感じてしまうのは、果たして私だけだろうか。
次の中から選んでください。
  1. background music用に作成した仕様なので問題は無い。
  2. ここまで巧妙に作ってあるのだから、バグである訳が無い。
  3. ニーズが少ない仕様に感じられるので、もっと多彩な活用事例を紹介して欲しい。
  4. onStartCommandメソッドの戻値の在り方を学習しなければならないのが面倒である。自分のアプリと関係無いことに付き合わされる。
  5. 本当はバグです。
  6. むむむ!

対策
同様のお悩みを持った御仁のサイトは次のとおりである。
Android: Serviceがヌルポで落ちる
このサイトに対応策が書かれています。
この記事に触発されて、onStartCommandメソッドの戻値についてServiceの記事を読みました。しかし、Serviceの記事は意味不明な内容でした。このため、簡単なプログラムを作成し実験してみた。

onStartCommandメソッドの戻値として次を3個のどれかを使えば良い。
START_STICKY_COMPATIBILITY
START_NOT_STICKY
START_REDELIVER_INTENT

START_STICKY_COMPATIBILITYを使えば、再起動してきません。再起動しないのが、本来の在り方だと思いますので、これを使うのが良いでしょう。

START_REDELIVER_INTENTを使えば、初回起動時と同じ値が入ってきます。しかも、START_FLAG_REDELIVERYを活用すれば、初回起動時と再起動時との見分けができます。

もし、START_STICKYを使う場合には、又はonStartメソッドを使う場合には、Intent引数がnullである場合への対策を講じれば良い。

START_CONTINUATION_MASKは、何のためにあるのかわかりません。Maskを施す必要性がどこにあるのでしょうか。

Serviceを実装し、そのonStartCommandメソッドの引数を使っているアプリは、テスト工程に次の作業を追加しなければならない。

  • アプリを強制終了した場合における、その後のServiceの振る舞い。


詳細
詳細は、同名の別サイトonStartCommandとその戻値についてに書いておきました。

2011年11月9日水曜日

画像と文字列の一覧表を表示する

画像と文字列の一覧表を表示する

画像と文字列を含むViewを一覧にして表示する方法は、次に説明しました。
android.R.drawableのアイコンを一覧表示する
android.R.drawableに含まれる全ての画像を一覧表示させる
これらの方法は、画像の全てがresource IDとして取り扱える場合に使用できます。
SimpleAdapterは、デフォルトでは、resource IDの画像を取り扱えるのです。

しかし、事情により、画像が、resource IDとして取り扱うことができず、Drawableである場合、SimpleAdapterの振る舞いを修正しなければなりません。
この修正には、SimpleAdapter.ViewBinderを使います。

sample codeを下記のとおり掲載しておきます。共有により起動可能なアプリの一覧を、アプリの名称とアイコンを対にして表示します。
この例では、Drawable型の画像とresource IDによる画像を一覧表の中に混在させて表示します。
学習・実験を目的とするため、冗長に書いてあります。
参考:共有を事前に登録する

2011年11月14日 アイコンが重複して表示されるバグを修正しました。

Resources r;
Intent it;
PackageManager pm;
String sFrom[] = {"Image", "String"};
int iTo[] =  new int[] {R.id.Image, R.id.TextAppName};
final List<Map<String, Object>> data;
Map<String, Object> m;
SimpleAdapter sa;
SimpleAdapter.ViewBinder savb;

savb = new SimpleAdapter.ViewBinder(){
    @Override
    public boolean setViewValue(View view, Object data, String textRepresentation){
        if(view.getId()==R.id.Image){
            if(data instanceof Integer){
                Integer ig;
                ig = (Integer)data;
                view.setBackgroundResource(ig.intValue());
            }
            if(data instanceof Drawable){
                view.setBackgroundDrawable((Drawable)data);
            }
        }
        return false;
    }
};

r = getResources();
it = new Intent();
//起動するアプリを限定させて頂きます。
it.setAction(Intent.ACTION_SEND);
it.setType("text/plain");
pm = getPackageManager();
lri = pm.queryIntentActivities(it, PackageManager.MATCH_DEFAULT_ONLY);
Collections.sort(lri, new ResolveInfo.DisplayNameComparator(pm));

data =new ArrayList<Map<String, Object>>();
if(lri.size()==0){
    m = new HashMap<String, Object>();
    m.put("String", r.getString(R.string.noData));
    m.put("Image", 0);
    data.add(m);
}
else{
    int i;
    for(i=0; i<lri.size(); i++){
        m = new HashMap<String, Object>();
        m.put("String", lri.get(i).loadLabel(pm).toString());
        
        //Drawabledeで取り扱う。
        m.put("Image", lri.get(i).loadIcon(pm));
        
        //リソースで取得すると、表示できない。
        //m.put("Image", lri.get(i).getIconResource());
        data.add(m);
    }
    //最後に、resource IDによるアイコンを表示する。
    m = new HashMap<String, Object>();
    m.put("Image", android.R.drawable.ic_menu_revert);
    m.put("String", r.getString(R.string.noShare));
    data.add(m);
}

sa = new SimpleAdapter(this, data, R.layout.listitem, sFrom, iTo);
sa.setViewBinder(savb);
lv = (ListView)findViewById(R.id.ListView);
lv.setAdapter(sa);//一覧の表示を提供する。
lv.setOnItemClickListener(this);

2011年11月8日火曜日

「本体設定」の値の一覧取得プログラム

「本体設定」の値の一覧取得プログラム

「本体設定」の値の一覧を取得するプログラムを作成しました。下記のとおり掲載しておきます。

【このプログラムの特徴】「本体設定」の全ての値を取得できる
例えば、音関係の本体設定情報を取得する場合には、AudioManagerクラスを使います。これはこれで使い易いですが、このような設定情報取得用のクラスが準備されていない設定情報も存在します。例えば、端末メーカー依存の設定情報です。端末メーカー依存の設定情報等は、その存在すら不明です。
下記に掲載したプログラムを使えば、「本体設定」の全ての値を取得できます。
この情報を活用すれば、端末メーカー依存のアプリを開発できます。

【使用対象者等】android programmerが実機で使う
【制約】当然のことながら、利用・活用は自己責任です。
【動作内容】....実は、大したことはやってない。が、実用性はあると思う。
Settings.System及びSettings.Secureを使って取得します。
Settings.Secureは、読み込みはできますが、書き込みはできません。
【参考】
「本体設定」の「音の設定」画面を表示する
ContentResolverで本体設定情報を取得する

public class SettingsActivity extends Activity

    implements
    OnClickListener
    {
    boolean bFlag;
    Button bu;
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        bu = (Button)findViewById(R.id.Button);
        bu.setOnClickListener(this);
        bFlag = true;
    }
    
    @Override
    public void onClick(View v) {
        if(v==bu) Show();
    }
    
    void Show(){
        String sTV;
        ArrayAdapter<String> aa;
        ListView lv;
        ContentResolver cr;
        Cursor c;
        Uri u;
        String p[];
        TextView tv;
        
        aa = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        cr = getContentResolver();
        tv = (TextView)findViewById(R.id.Text);
        if(bFlag==true){
            u =  Settings.System.CONTENT_URI;
            sTV = "Settings.System : ";
            bFlag = false;
        }
        else{
            u =  Settings.Secure.CONTENT_URI;
            sTV = "Settings.Secure : ";
            bFlag = true;
        }
        p = new String[]{
                Settings.NameValueTable.NAME,
                Settings.NameValueTable.VALUE
        };
        c = cr.query(u, p, null, null, null);
        if(c.moveToFirst()==true){
            do{
                String s;
                s = c.getString(c.getColumnIndex(Settings.NameValueTable.NAME));
                s = s + " : " + c.getString(c.getColumnIndex(Settings.NameValueTable.VALUE));
                aa.add(s);
            }while(c.moveToNext()==true);
        }
        aa.sort(new MyComparator());
        sTV = sTV + String.valueOf(c.getCount());
        c.close();
        tv.setText(sTV);
        lv = (ListView)findViewById(R.id.ListView);
        lv.setAdapter(aa);
    }
    
    public class MyComparator implements Comparator<String>{
        @Override
        public int compare(String s1, String s2){
            return s1.compareTo(s2);
        }
    }
}


main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <Button
        android:id="@+id/Button"
        android:text="@string/Button"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        >
    </Button>
    <TextView
        android:id="@+id/Text"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        >
    </TextView>
    <ListView
        android:id="@+id/ListView"
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        >
    </ListView>
</LinearLayout>

2011年11月7日月曜日

ContentResolverで本体設定情報を取得する

ContentResolverで本体設定情報を取得する

ContentResolverの振る舞いを調べるため、その学習・実験材料として、代表的なコマンドであるマナーモードに関する情報を取得します。

マナーモードを取得する方法は2種類あります。
一方はAudioManagerを使う方法であり、他方はContentResolverを使う方法です。
本当にマナーモードを取得したいのであれば、AudioManagerを使えばよろしいです。
両方の方法を実装した、実用的なサンプルプログラムを掲載しておきます。
マナーモードは、Settings.Secureに属するのではなく、Settings.Systemに属し、mode_ringerという名前です。

public class Resolver2Activity extends Activity


    implements
    OnClickListener
    {
    boolean bFlag;
    Button bu;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        bu = (Button)findViewById(R.id.Button);
        bu.setOnClickListener(this);
        bFlag = true;
        
        int iRet;
        String s;
        AudioManager am;
        am = (AudioManager)getSystemService(AUDIO_SERVICE);
        iRet = am.getRingerMode();
        switch(iRet){
        case AudioManager.RINGER_MODE_SILENT:
            s = "RINGER_MODE_SILENT";
            s = s + " : " + String.valueOf(AudioManager.RINGER_MODE_SILENT);
            break;
        case AudioManager.RINGER_MODE_NORMAL:
            s = "RINGER_MODE_NORMAL";
            s = s + " : " + String.valueOf(AudioManager.RINGER_MODE_NORMAL);
            break;
        case AudioManager.RINGER_MODE_VIBRATE:
            s = "RINGER_MODE_VIBRATE";
            s = s + " : " + String.valueOf(AudioManager.RINGER_MODE_VIBRATE);
            break;
        default: s = ""; break;
        }
        TextView tv;
        tv = (TextView)findViewById(R.id.TextAudioManager);
        tv.setText(s);
    }
    
    @Override
    public void onClick(View v) {
        if(v==bu) UsingContentResolver();
    }
    
    void UsingContentResolver(){
        ArrayAdapter<String> aa;
        ListView lv;
        ContentResolver cr;
        Cursor c;
        Uri u;
        String p[];
        TextView tv;
        
        aa = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        cr = getContentResolver();
        tv = (TextView)findViewById(R.id.Text);
        if(bFlag==true){
            u =  Settings.System.CONTENT_URI;
            tv.setText("Settings.System");
            bFlag = false;
        }
        else{
            u =  Settings.Secure.CONTENT_URI;
            tv.setText("Settings.Secure");
            bFlag = true;
        }
        p = new String[]{
                Settings.NameValueTable.NAME,
                Settings.NameValueTable.VALUE
        };
        c = cr.query(u, p, Settings.NameValueTable.NAME + " = 'mode_ringer'", null, null);
        if(c.moveToFirst()==true){
            do{
                String s;
                s = c.getString(c.getColumnIndex(Settings.NameValueTable.NAME));
                s = s + " : " + c.getString(c.getColumnIndex(Settings.NameValueTable.VALUE));
                aa.add(s);
            }while(c.moveToNext()==true);
        }
        c.close();
        lv = (ListView)findViewById(R.id.ListView);
        lv.setAdapter(aa);
    }
}

2011年11月4日金曜日

本体設定の画面を表示する

本体設定の画面を表示する

本体設定の画面を表示するには、Settingsを使う。SettingsをIntentで使えば、「本体設定」コマンドをクリックした時に表示される画面を表示させることができる。

Settingsの基本的な使い方
    Intent it;
    it = new Intent(Settings.ACTION_APPLICATION_SETTINGS);
    try{
        startActivity(it);
    }
    catch(ActivityNotFoundException e){
        Log.e(TAG, "ActivityNotFoundException");
    }

様々なACTION_*について
上記の例ではSettings.ACTION_APPLICATION_SETTINGSを使ったが、他にも様々なACTION_がある。このACTION_*に関する留意点は次のとおりである。
  1. platformによっては実行できないものがある。
  2. platformを満たしていても、実行できない場合がある。
このため、Intentの発行に際しては、例外処理を施しておく必要がある。

Settingsの記事内容について
実機を操作してみると分かるが、実機では表示される画面があるにも関わらず、その画面を呼び出すACTIONがSettingsに掲載されていない場合がある。
例えば、端末のメーカーが独自に実装した画面などである。また、Settingsには動かないACTIONが記載されていることもある。
Settingsに掲載されていないACTION情報、又は正確なACTION情報を取得する方法は次のとおりである。
なお、この方法は、ddmsのLog欄でわかるAndroid端末の動作を参考にした。
  1. ACTION情報調査用のプログラムを作成し、デバッグモードで実行させる。
  2. DDMSを使い、端末利用者が画面を切り替えた時点における、各種Intentの発行のログを確認する。
  3. そのログに書かれてあるとおり、本番用のプログラムに書く。
この手順の詳細は下記のとおりである。

正確なACTION情報で本体設定画面を呼び出す
1 調査用プログラム作成
次のプログラムを作成しデバッグモードで実行する。すると、「設定」画面が表示される。この「設定」画面を操作して、調べたい画面を表示させる。
public class SettingsActivity extends Activity{
    final String TAG = "me";
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Intent it;
        it = new Intent(Settings.ACTION_SETTINGS);
        try{
            startActivity(it);
        }
        catch(ActivityNotFoundException e){
            Log.e(TAG, "ActivityNotFoundException");
        }
    }
}

2 DDMSでログを見る
端末操作者が、例えば、「音声入出力」>「テキスト読み上げの設定」をクリックすると、ログには次のような趣旨の内容が表示される。
Starting: Intent { act=android.intent.action.MAIN cmp=com.android.settings/.VoiceInputOutputSettings }
Starting: Intent { act=android.intent.action.MAIN cmp=com.android.settings/.TextToSpeechSettings }
この情報を基に、プログラミングをする。

3 本番用プログラム作成
上記のログ情報を基に、下記のようにExplicit Intentを発行する。VoiceInputOutputSettings画面を経由せず、いきなりTextToSpeechSettings画面を表示するため、端末利用者からすれば便利である。
public class SettingsActivity extends Activity{
    final String TAG = "me";
    @Override
    public void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Intent it;
        it = new Intent();
        it.setClassName("com.android.settings", "com.android.settings.TextToSpeechSettings");
        try{
            Log.i(TAG, "startActivity : TextToSpeechSettings");
            startActivity(it);
        }
        catch(ActivityNotFoundException e){
            Log.e(TAG, "ActivityNotFoundException");
        }
    }
}

2011年11月2日水曜日

「本体設定」の「音の設定」画面を表示する

「本体設定」の「音の設定」画面を表示する

下記のとおりです。Platformによって、コマンドが異なるようです。

なお、startActivityForResultメソッドを使った場合、設定画面が閉じられた時に、onActivityResultメソッドで「閉じられた」という通知を受けとることができます。ただし、第三番目の引数Intentの値はnullです。

AudioManagerのgetRingerModeメソッドにより、「マナーモード」の状態を取得できます。RINGER_MODE_SILENT及びRINGER_MODE_VIBRATEがマナーモードであることを示します。

下記のプログラムは動作原理を理解するための、学習・実験用ですので、実用性はありません。

稼働中の端末のPlatform(API level・バージョン) を取得するに関連記事を書いておきました。


public class SettingsActivity extends Activity
    implements
    OnClickListener
    {
    final String TAG = "me";
    Button bu;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        bu = (Button)findViewById(R.id.Button);
        bu.setOnClickListener(this);
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data){
        if(requestCode==0){
            int iRet;
            String s;
            AudioManager am;
            am = (AudioManager)getSystemService(AUDIO_SERVICE);
            iRet = am.getRingerMode();
            switch(iRet){
            case AudioManager.RINGER_MODE_SILENT:
                s = "RINGER_MODE_SILENT";
                break;
            case AudioManager.RINGER_MODE_NORMAL:
                s = "RINGER_MODE_NORMAL";
                break;
            case AudioManager.RINGER_MODE_VIBRATE:
                s = "RINGER_MODE_VIBRATE";
                break;
            default: s = ""; break;
            }
            TextView tv;
            tv = (TextView)findViewById(R.id.Text);
            tv.setText(s);
        }
        super.onActivityResult(requestCode, resultCode, data);
    }


    @Override
    public void onClick(View v){
        if(v==bu){
            Intent it;
            
            //Platform 2.2の場合
            it = new Intent(Settings.ACTION_SOUND_SETTINGS);
            try{
                startActivityForResult(it, 0);
            }
            catch(ActivityNotFoundException e){
                Log.e(TAG, "ActivityNotFoundException 1");
                // Platform 1.6の場合
                it = new Intent(Settings.ACTION_DISPLAY_SETTINGS);
                try{
                    startActivityForResult(it, 0);
                }
                catch(ActivityNotFoundException e2){
                    Log.e(TAG, "ActivityNotFoundException 2");
                }
            }
        }
    }
}