ページ

2011年12月18日日曜日

AdMob画像の場所に、代替画像を表示する

AdMob画像の場所に、代替画像を表示する

副題:待ち時間に、愛想笑い微笑みを

アプリの画面の表示と同時に、AdMob画像が表示されるのでは無い。
アプリの画面が表示された後、インターネットから広告のデータをダウンロードしてから、その広告データに基づきAdMob画像が表示されるのである。AdMob画像が物理画面の下端にあるのであれば気にはならない。

しかし、何も考慮せずに、AdMob画像を物理画面の上端に配置した場合、AdMob画像が表示された時、その画像がその位置に「挿入される」ということになるため、その画像の下に配置しているViewが、一斉に下にズレてしまう。このような振る舞いは、利用者に違和感を与える。
if(あなたの感じ方=="別に問題では無い。") return;

この問題への対処の方法として、AdMob画像を表示する位置に、予め別の画像を表示するようにしておいて、そこに後からAdMob画像を表示するようにすれば良い。
この方法を講じるためには、愛想笑い微笑みを描いた横320×縦50の画像を作成し、この画像をres>drawableディレクトリに入れておかねばならない。
if(あなたの画力=="苦笑い程度しか描く画力が無い") return;

下記のxmlで実装する。画像ファイル名はbanner.pngである。FrameLayoutを使うことにより、同一の場所に表示させることができる。

<?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="fill_parent"
    android:background="#a00"
    >
    <FrameLayout
        android:id="@+id/FrameLayout"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:gravity="center"
        >
        <ImageView
            android:src="@drawable/banner"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            >
        </ImageView>
        <LinearLayout
            android:id="@+id/AdMob"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            >
        </LinearLayout>
    </FrameLayout>
    <Button
        android:text="@string/Button"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/FrameLayout"
        >
    </Button>
</RelativeLayout>

この方法だと、AdMob画像が表示される前も後もButtonが下にズレることは無い。かつ、AdMob画像が表示されるまでの短い時間ではあるが、愛想笑い微笑みの画像を表示させることができて、利用者を飽きさせない。
if(あなたの感想=="飽きはしないが、好感を持てるということも無い") return;

2011年12月16日金曜日

AdMobを中央に配置する。

AdMobを中央に配置する。

副題:その空間に、愛想笑いを。

何も考慮せずにAdMobのlayoutを作成すると、AdMobの広告は物理画面の左詰めになる。この場合、端末が横位置になった場合、広告の右側に無意味な空白が発生する。
if(あなたの考え=="別に構わないじゃないか") return;

このような無意味な空白はAdMob側で何か措置を施して頂きたいが、現状ではどうにもならないので、プログラマ側でなんとかする。
無意味な空間を、なんとか取りツクロうのである。言わば、気まずい雰囲気の中で行われる、愛想笑いのような仕掛けだ。
if(あなたの表情=="笑い顔には自信が無い") return;

取り敢えず考えられるのは、広告を横方向の中央に配置すれば良い、ということであろう。中央に配置することにより、無意味な空間を左右に分散させるのである。これにより、無意味と感じる程度が低下することが期待できる。
if(あなたの感想=="左側にも無意味な空間が広がるだけだ") return;

背景が画像である場合は、FrameLayoutを使うことになるであろう。そのためのlayout用xmlのsample codeは次のとおりである。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ViewGroup"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="#a00"
    >
    ここに背景用のViewを設置する。
    <LinearLayout
        android:id="@+id/AdView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|center_horizontal"
        >
    </LinearLayout>
</FrameLayout>

他のViewがBottunやScrollView等であれば、次のようにRelativeLayoutを用いることになる。
<?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="fill_parent"
    android:background="#a00"
    >
    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:gravity="center_horizontal"
        >
        <LinearLayout
            android:id="@+id/AdView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            >
        </LinearLayout>
    </LinearLayout>
    ここに他のViewを書いていく。
</RelativeLayout>

上記例では、中央に配置したことを視覚的に明確にするため、背景を赤色で塗ってある。しかし、広告を目立たせるためには、背景を目立たない色にするべきであろう。
if(あなたの趣味=="背景は広告よりも目立つようにすべきだ") return;

javaのcodeは、AdMob Version 4.3.1の実装に書いたとおりである。

後日談は、画面サイズに応じてAdMobのサイズを変えるに書きました。

2011年12月3日土曜日

AdMob Version 4.3.1の実装

AdMob Version 4.3.1の実装

AdMobは、アプリ内に広告を表示する機能を提供する。教科書どおりに作れば良い。ここでは、難易度の高かった項目を解説する。

問題意識
現在、androidのplatform 2.2 API level 8で稼働するアプリを開発している。
教科書では、AndroidManifest.xmlに次の1行を挿入するように指示がされている。
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
ところが、screenSize及びsmallestScreenSizeは、platform 3.2 API level 13以上が必要とされるため、Error: String Types not allowedというコンパイルエラーが発生する。

screenSize及びsmallestScreenSizeを削除すると、次の趣旨の実行時エラーが発生する。
E/Ads(11952): The android:configChanges value of the com.google.ads.AdActivity must include screenSize.
E/Ads(11952): The android:configChanges value of the com.google.ads.AdActivity must include smallestScreenSize.
E/Ads(11952): You must have AdActivity declared in AndroidManifest.xml with configChanges.
どのようにすれば良いのか。

結論
回答はAdmob in Android 2.2にあった。この問答から、次のような手順を講じれば良いことがわかった。
  1. Eclipseを最新版にする。(必須では無い)
  2. Android SDKを最新版にする。
  3. (重要)Eclipseのメニューコマンド>Project>Properties>Android>Project Build Target>platform 4.0 API level 14(最新版)を選択する。このコマンドはJava Build Path>Librariesに連動している。
  4. AndroidManifest.xmlに次の1行を追加する。
    <uses-sdk android:minSdkVersion="8"/>
  5. Project>Cleanを実行する。
Project>Properties>Java Build Path>Librariesの設定は手作業で行うのではなく、上記3の操作を行えば、自動的にJava Build Path>Librariesの設定が行われる。

この手順によって作成されたアプリは、platform 2.2 API level 8で無事稼働する。
上記の各手順の値を具体的にどうするかは、各プログラマーの置かれた立場によって異なる。

参考:AdMob
教科書:Google AdMob Ads SDK

実機をテスト用端末に設定する方法
開発者は、広告をクリックしてはいけない。広告を誤ってクリックしてしまうことを避けるため、広告を(本物の広告では無く)開発用の広告にしておかねばならない。

AndroidManifest.xmlの書き方は、教科書に掲載されているとおり、そのまま書けば良い。

layout用のxmlは、例えば、次のようにする。普通にlayoutを作成すれば良いだけだ。
<?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"
    >
    <LinearLayout
        android:id="@+id/AdView"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" 
        android:layout_alignParentBottom="true"
        >
    </LinearLayout>
(以下省略)

上記layout用xmlを用いるとして、Javaにおけるsourceは、取り合えずonCreateメソッド内で次のとおり書いて、一旦、実機で実行させる。
        AdView adView;
        LinearLayout ll;
        AdRequest ar;
        
        //第三番目の引数にはAdMob publisher IDを指定する。
        adView = new AdView(this, AdSize.BANNER, "xxxxxxxx");  
        ll = (LinearLayout)findViewById(R.id.AdView);  
        ll.addView(adView);
        
        ar = new AdRequest();
        //ar.setTesting(true);//4.0.4の場合使える。4.3.1では使えない。
        ar.addTestDevice(AdRequest.TEST_EMULATOR);
        adView.loadAd(ar);

上記プログラムを実機で実行させると、LogCatに次の趣旨の行が出力される。
I/Ads(12492): To get test ads on this device, call adRequest.addTestDevice("yyyyyyyy");
この"yyyyyyyy"の部分を引用して、adView.loadAd(ar);の直前に、次の行を書き込む。
        ar.addTestDevice("yyyyyyyy");
そうすると、次回の実行時から、実機では、本物の広告を表示するのでは無く、テスト用の広告を表示する。

本番では、addTestDeviceメソッドを削除する。
もっとも、削除を忘れても実害は無い。何故なら、指定したIDの端末だけがテスト用になっているだけだからだ。(このあたりは、実際には試してはいません。)

そういう意味で、Version 4.0.4に比べて、4.3.1は進化したと言える。Ver.4.0.4では、setTesting(true)メソッドにより、全ての端末がテスト機になってしまう。このメソッドの削除を忘れて出荷した場合、喜劇悲劇が生まれる。

ここで、敢えて、Ver.4.0.4を使い、setTesting(true)を実装させたまま、出荷してしまうってのも、一興である。「削除を忘れたのではありません。敢えて、残しておいたのです」と。(-_-;)

いや、それはダメです。パブリッシャー様向けのガイドラインとポリシーには、「アプリ内広告では SDK のメソッドのみを使用し、SDK の最新のバージョンを使用する必要があります」と書かれています。

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);
    }
}