2013年12月22日日曜日

SDK Managerでインストールエラー

EclipseのAndroid SDK Managerで任意のSDK Platformをインストール(アップデート)しようとした場合、「フォルダーが使用中であるか、又はウイルスソフトを停止せよ」という趣旨のエラーメッセージが出ることがあるかもしれません。
私の場合は、
1.Package Exploler画面を表示し、開いているプロジェクトを全て閉じて、
2.Eclipseを再起動する
と、無事にインストールができました。

2013年11月30日土曜日

YouTubeService has leaked

バージョンが5.3.24のYouTubeにおいて、YouTube API Demosで、PlayerViewDemoActivity("Simple PlayerView")を起動し、そしてこのActivityを終了させると、次のログが出力される。アプリの動作には問題はない。原因は不明です。

E/ActivityThread(3632): Service com.google.android.youtube.api.service.YouTubeService has leaked IntentReceiver com.google.android.apps.youtube.core.utils.ConnectivityReceiver@424ccd10 that was originally registered here. Are you missing a call to unregisterReceiver()?
E/ActivityThread(3632): android.app.IntentReceiverLeaked: Service com.google.android.youtube.api.service.YouTubeService has leaked IntentReceiver com.google.android.apps.youtube.core.utils.ConnectivityReceiver@424ccd10 that was originally registered here. Are you missing a call to unregisterReceiver()?

YouTubeのバージョンの取得方法
String s = YouTubeIntents.getInstalledYouTubeVersionName(getBaseContext());
Log.d("PlayerViewDemoActivity", "Installed YouTube Version==" + s);

Installed YouTube Version==5.3.24 (2013/11/30)

対応方法(案)
不都合が生じるYouTubeのバージョンをアプリ開発者が相互に報告しあって、不都合が生じるバージョンのYouTubeがインストールされている場合、アプリの振る舞いを工夫する。例えば、起動を中止する。
地味な対応策だな。

Android player crashes when on attempt to play and ads videoによると、YouTubeのバージョンが、
5.2.27
5.3.23
において不都合が発生しているようである。

2014.01.23: YouTubeのversionが5.3.32の場合も上記不都合が発生することを確認しました。
2014.03.15: YouTubeのversionが5.5.27の場合も上記不都合が発生することを確認しました。
2014.04.20: YouTubeのversionが5.6.32の場合も上記不都合が発生することを確認しました。
2014.05.04: YouTubeのversionが5.6.35の場合も上記不都合が発生することを確認しました。
2014.05.09: YouTubeのversionが5.6.36の場合も上記不都合が発生することを確認しました。

(参考)
Various crashes using Android YouTube Player API

2013年10月19日土曜日

TimePicker NullPointerException on ICS

TimePicker NullPointerException on ICS

下記のエラーが発生した。
E/AndroidRuntime(13629): FATAL EXCEPTION: main
E/AndroidRuntime(13629): java.lang.NullPointerException
E/AndroidRuntime(13629): at android.widget.TimePicker.updateInputState(TimePicker.java:580)
(以下省略)

このエラーの発生に再現性は無い。

質疑応答は下記のとおり。
TimePicker NullPointerException on ICS

TimePicker.javaの580行目近辺は次のようになっている。
565    private void More ...updateInputState() {
566        // Make sure that if the user changes the value and the IME is active
567        // for one of the inputs if this widget, the IME is closed. If the user
568        // changed the value via the IME and there is a next input the IME will
569        // be shown, otherwise the user chose another means of changing the
570        // value and having the IME up makes no sense.
571        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
572        if (inputMethodManager != null) {
573            if (inputMethodManager.isActive(mHourSpinnerInput)) {
574                mHourSpinnerInput.clearFocus();
575                inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
576            } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
577                mMinuteSpinnerInput.clearFocus();
578                inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
579            } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
580                mAmPmSpinnerInput.clearFocus();
581                inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
582            }
583        }

このコードを読むと、午前午後スピナーがnullであったと想定される。
このため、TimePicker#setIs24HourView (Boolean is24HourView)を必ず使わねばならない、と言えるのではないだろうか。エラーが発生した私のコードでは、午前午後スピナーの使用の有無について明記していなかった。

見本は次のとおり。
        tp.setIs24HourView(false);
        tp.setCurrentHour(iHour);
        tp.setCurrentMinute(iMinute);
上記コードにおいて、setCurrentHourメソッドの引数に12-23の値を代入した場合、午前午後スピナーには午後と表示され、時間は0-11で表示される。

2013年7月23日火曜日

SimpleExpandableListAdapter

SimpleExpandableListAdapter

Adapter系のクラスを使うメリットは、データ群を一括して取り扱えることだ。
例えば、SimpleAdapterの場合、文字列群と画像群と、そしてそれら以外のデータ(例えば、自分で作成した任意のクラス等)をも、一括して、対応するView(例えば、TextViewやImageView等)に代入することができた。

(参考)SimpleAdapterの簡単すぎる基本形
    String sFrom[] = {"Bitmap", "Title"};
    int iTo[] = new int[]{
        R.id.Image_List_Favorite,
   R.id.Text_List_Favorite_Title
    };
      sa = new SimpleAdapter(mainActivity, al, R.layout.list_keyword, sFrom, iTo);
上記は、"Bitmap"と命名した画像データを、R.id.Image_List_FavoriteというImageViewに代入&表示する。

しかし、SimpleExpandableListAdapterの場合、一括して代入できるのは文字列データだけである。画像データはできないし、それ以外のデータもできない。仕様です。

画像データを、対応するImageViewに代入する場合、それ用のプログラムをしなければならない。
また、ChildViewに、例えばButtonを配置した場合、そのButtonに対する利用者からのクリック状況の把握の仕方もプログラムしなければならない。

サンプルプログラムは下記のとおりです。

    String sFrom[] = {"Title"};
    int iTo[] = new int[]{
        R.id.Text_List_Favorite_Title//自動的に代入できるのは、文字列データだけである。画像データを取り扱うことはできない。
    };
    private ExpandableListView lv;
    private MySA sa;
    private ArrayList<ArrayList<Map<String, Object>>> ChildrenList;
 
    /* 画像データを表示させ、かつ、ChildViewにButtonを実装させるため、
    SimpleExpandableListAdapterを拡張して使う。
    */
    class MySA extends SimpleExpandableListAdapter{
        /*
         * constructorは、Eclipseが自動生成したものをそのまま使う。
         * constructorは、ごちゃごちゃしているが、何も問題は無い。見ないでおこう。
         */
        public MySA(Context context,
                List<? extends Map<String, ?>> groupData, int groupLayout,
                String[] groupFrom, int[] groupTo,
                List<? extends List<? extends Map<String, ?>>> childData,
                int childLayout, String[] childFrom, int[] childTo) {
            super(context, groupData, groupLayout, groupFrom, groupTo, childData,
            childLayout, childFrom, childTo);
        }

    /*以下はプログラミングする箇所である。*/
        @Override
        public View getChildView(int groupPosition, int childPosition,
            boolean isLastChild, View convertView, ViewGroup parent) {

            View v = super.getChildView(groupPosition, childPosition, isLastChild,
                convertView, parent);
            /*
             * superによる処理が終わった後のViewを使うこと。
    super内で文字列の代入が行われているのである。
             * 以下は、ImageViewへ画像を代入する処理である。
    つまり、文字列データの処理を先にしてから画像データの処理を行う。
             */
            RelativeLayout rl = (RelativeLayout)v;//私のPGではRelativeLayoutである
            ImageView iv = (ImageView)rl.findViewById(R.id.Image_List_Favorite);
            Map<String, Object> m = ChildrenList.get(groupPosition).get(childPosition);
            Bitmap bm = (Bitmap)m.get("Bitmap");
            iv.setImageBitmap(bm);
            
            /*
             * 下記newChildView()においてButtonにリスナーを付けクリックイベントを捕捉できるようにする。しかし、それだけでは個々のButtonを識別できない。
このため、識別できるようにするため、次のようにタグを付ける。
             */
            Button btn = (Button)rl.findViewById(R.id.Button_List_Favorite);
            long l = ExpandableListView.getPackedPositionForChild(
                    groupPosition, childPosition);
            btn.setTag(l);
            return v;
        }

        /*
         * 次のnewChildView()は、上記getChildView()のsuper.getChildView()内において、
         * アプリ開始時にChildViewが新規に生成する時に呼ばれる。
         * 一定数のChildViewを生成し終えたら、もはや生成されることはない。
         * 生成後の処理は、getChildView()で行うことになる。
         * layout用xmlにおいてViewを書いてあるので、基本的にはnewChildView()をイジル必要は無い。
         */
        @Override
        public View newChildView(boolean isLastChild, ViewGroup parent) {
            View v = super.newChildView(isLastChild, parent);
            RelativeLayout rl = (RelativeLayout)v;//私のPGではRelativeLayoutである
            Button btn = (Button)rl.findViewById(R.id.Button_List_Favorite);
            btn.setOnClickListener(FavoriteView.this);
            return v;
        }
    }
    //以上が、SimpleExpandableListAdapterの拡張クラスでした。

    public 自分のメソッド(){
        
        lv = (ExpandableListView)****.findViewById(R.id.ListFavorite);
       
        ArrayList<Map<String, String>> parentList = new ArrayList<Map<String, String>>();
        ArrayList<String> als = Group用文字列データ群を取得する。
        for(String s : als){
            Map<String, String> m = new HashMap<String, String>();
            m.put("Parent_Text", s);
            parentList.add(m);
        }
        
        ChildrenList = Child用文字列データ群を取得する。
        
        sa = new MySA(
            ****,

            //Group用文字列データ群の代入
            parentList,//データそのもの
            android.R.layout.simple_expandable_list_item_1,
            new String[]{"Parent_Text"},//入力
            new int[]{android.R.id.text1},//出力

            //Child用文字列データ群の代入
            ChildrenList,//データそのもの
            R.layout.list_favorite,//layout用xmlファイル
            sFrom,//入力
            iTo//出力
        );
        
        lv.setAdapter(sa);
        lv.setOnChildClickListener(this);
    }
    
    @Override
    public void onClick(View v) {
        if(v.getId()==R.id.Button_List_Favorite){
        //サンプルなので、ログを表示するだけです。
        //任意のChild内に表示されたボタンが押されたことを取得します。
            Button btn = (Button)v;
            long l = (Long)btn.getTag();
            int iG = ExpandableListView.getPackedPositionGroup(l);
            int iC = ExpandableListView.getPackedPositionChild(l);
if(BuildConfig.DEBUG) Log.d(TAG, "onClick(). " + iG + ". " + iC);
            return;
        }
    }

    @Override
    public boolean onChildClick(ExpandableListView arg0, View v, int iG, int iC, long arg4) {
        //サンプルなので、ログを表示するだけです。
        //任意のChildが押されたことを取得します。
if(BuildConfig.DEBUG) Log.d(TAG, "onChildClick(). " + iG + ". " + iC);
        return false;
    }


以下は、ChildViewのlayout用xmlファイルであり、ファイル名はR.layout.list_favorite.xmlです。
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    >
    <Button
        android:id="@+id/Button_List_Favorite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:drawableLeft="@android:drawable/ic_menu_edit"
        android:focusable="false"
        >
    </Button>
    <ImageView
        android:id="@+id/Image_List_Favorite"
        android:contentDescription="@string/app_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:focusable="false"
        >
    </ImageView>
    <TextView
        android:id="@+id/Text_List_Favorite_Title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:layout_toRightOf="@id/Image_List_Favorite"
        android:layout_toLeftOf="@id/Button_List_Favorite"
        android:layout_alignBottom="@id/Image_List_Favorite"
        android:focusable="false"
        >
    </TextView>
</RelativeLayout>

2013年7月17日水曜日

実行時にSeekBarの色を変える

実行時にSeekBarの色を変える

XMLにおいて、色を変える方法については、次のサイトで紹介されています。
AndroidのProgressBar / SeekBarに画像を使う
Cool android SeekBar, custom made by me

これらを参考にさせて頂いて、下記のとおり、実行時に色を変えるプログラムを作ってみました。

        sb = (SeekBar)llMain.findViewById(R.id.SeekBar_Volume);
        if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB){
        /*
        * LightではSeekBarが見えにくいので、色を着ける。
        * HONEYCOMB未満でこれを実行すると、即時の描画が行われない。
        * ClipDrawableは、描き変えや、読み出しを行えないため、新規に作成する。
        */
       LayerDrawable ld = (LayerDrawable)sb.getProgressDrawable();
       ColorDrawable dBlue = new ColorDrawable(0xff4f4fdf);
       ClipDrawable cd = new ClipDrawable(dBlue, 0x03, ClipDrawable.HORIZONTAL);
    ld.setDrawableByLayerId(android.R.id.progress, cd);
        }

Android3.0以上から、ホロというテーマが使われています。私はテーマをLightに変えました。すると、何故か、SeekBarに色が付かなくなりました。そこで、色を着けてやろうということです。

本来なら、テーマにおいて上記と同等の修正をすべきでしょう。テーマの技法を使って色を変える方法が分かる人は、各自のブログにでも載せておきましょう。

2013年7月5日金曜日

onNavigationItemSelected()

ActionBar.OnNavigationListener#onNavigationItemSelected()

第ゼロ番目のアイテムが(利用者の操作によって選択されたのではなく)自動的に選択され、このメソッドが呼ばれる。
ActionBar#setSelectedNavigationItem()の引数にマイナス1を代入しましたが、第ゼロ番目が選択されてしまいます。引数に、任意の項目のIDを代入すると、その代入した値の項目が選択された状態になります。
この不都合を無くすため、第ゼロ番目の項目をダミーにしました。
これでいいのか?(ここで笑う)

Spinnerを使う場合、初期化(初回)の時点で、アイテムが自動的に選択されてしまうという問題があります。
この問題の解決は次のサイトに書かれています。
Android Spinner : Avoid onItemSelected calls during initialization
spinner1に連動して変化するspinner2の初期値設定

boolean型変数を大域変数として設けておき、onItemSelected()において、初期化(初回)時点とそうでない時点を切り分けるという方法ですね。
他に方法は無いものかと考えましたが、分かりませんでした。

ちなみに、onItemSelected()は、選択が変更された場合に呼ばれます。逆に言えば、既に選択済みの項目をクリックしても呼ばれません。
つまり、上記方策を講じて、初回における自動選択問題を回避したとして、利用者の要求が自動選択済みの項目を選択したいということである場合には、その項目をクリックしても、onItemSelected()は呼ばれないということです。一旦別の項目を選択してから、要求する項目を再度クリックし直さねばならないのです。
こんなアホなアプリを作成することはできません。

この不都合を無くすため、第ゼロ番目の項目をダミーにします。
これでいいのか?(ここで再度笑う)

私は(不幸なことに)ArrayAdapter.NO_SELECTIONというのを見つけてしまいました。で、(何かを示唆するかのように)Adapterには、何の解説もありません。(無駄な)期待をして、これをActionBar#setSelectedNavigationItem()の引数に与えて実行してみました。結果、(予期したとおり)第ゼロ番目の項目が選択されたという趣旨でonNavigationItemSelected()が呼ばれました。
こんなんでいいのか?(ここで、もう一度笑う)

第ゼロ番目の項目をダミーにした場合、その必要ではない項目もドロップダウンリストに表示されてしまって不細工である。このため、ダミーをリストに表示しないようにしたいと思い、次のとおりアダプターをカスタマイズしてみました。

    public class Adapter_SR extends ArrayAdapter<String>{
        public Adapter_SR(Context context, int textViewResourceId, List<String> objects) {
            super(context, textViewResourceId, objects);
        }

        @Override
        public View getView(int iID, View v, ViewGroup vg) {
            if(iID==0){
                LayoutInflater inf = getLayoutInflater();
                TextView tv = (TextView)v;
                tv = (TextView)inf.inflate(R.layout.bar_sr, vg, false);
                tv.setText((String)getItem(iID));
                return tv;
            }
            else return super.getView(iID, v, vg);
        }

        @Override
        public View getDropDownView(int iID, View v, ViewGroup vg) {
            LayoutInflater inf = getLayoutInflater();
            TextView tv = (TextView)v;
            if(tv==null){
                if(iID==0){
                    tv = (TextView)inf.inflate(R.layout.bar_sr, vg, false);
                    tv.setTag("Zero");
                }
                else{
                    tv = (TextView)inf.inflate(android.R.layout.simple_spinner_dropdown_item, vg, false);
                    tv.setText((String)getItem(iID));
                }
            }
            else{
                if(iID==0){
                    tv = (TextView)inf.inflate(R.layout.bar_sr, vg, false);
                    tv.setTag("Zero");
                }
                else{
                    String s = (String)tv.getTag();
                    if(s!=null && s.equals("Zero")==true){
                        tv = (TextView)inf.inflate(android.R.layout.simple_spinner_dropdown_item, vg, false);
                        tv.setText((String)getItem(iID));
                    }
                }
            }
            return tv;
        }
    }


2013年7月1日月曜日

ClassNotFoundException

RuntimeException: Unable to instantiate activity ComponentInfo  java.lang.ClassNotFoundException in loader dalvik.system.PathClassLoader

ActionBar.OnNavigationListener(Added in API level 11, HONEYCOMB, Android 3.0)の実装について、これはinterfaceであるからして、Activityにinterfaceとしてimplementsしていました。
Android4.2で稼働させるには問題はありませんでした。
ところが、このアプリをAndroid2.3で実行させると、onCreate()が実行されることなく、上記エラーが発生します。

この実装をanonymousにしたところ、Android2.3でも稼働することができました。
こんなふうなanonymousの使い方ってあるのですね。

(参考)
Action Bar

2013年6月26日水曜日

Buttonの背景色

Buttonの背景色

Buttonの色が背景色と混合されてしまって、不細工な色になってしまっている。

Android2.3の場合は、背景の色とButtonの画像とは別々であった。
しかし、少なくともAndroid4.2では、背景色とButtonの画像が混じり合って表示されるため、不細工になっている(主観の問題ですが)。背景色が真っ白の場合は問題は無いです。
この現象が発生するViewの構成
<LinearLayout>...上位にあるView。これに赤い色等の背景色を付ける
    <Button>...画像関係は何も操作しない。
背景の赤い色とButtonの灰色とが混じり合って、赤灰色のButtonが出来上がります。

解決策の模索
1.背景色を表示するViewとButtonとの間に、背景色が真っ白なViewを配置する。
<LinearLayout>...これに赤い色等の背景色を付ける
    <LinearLayout>...これに真っ白な背景色を付ける。
        <Button>...画像関係は何も操作しない。
しかし、LinearLayoutとButtonとは形状が異なるため、白色がハミ出てしまいます。ある種の御仁はハミ出た白色に魅力を感じるでしょう。

2.ThemeをLightにする。
res/values-v11やres/values-v14には、styles.xmlファイルがあるので、この中を次のようにする。
    <style name="AppBaseTheme" parent="android:Theme.Light">
    </style>
このようにすることで、Android2.3の場合と同じ表示になります。
Holoというのが新しいデザインです。Lightは昔のです。何か時代の波に残された感じがしますが、昔の方が良かったなとも感じます。

3.ボタンをShapeDrawableで描画する
これはまだ実施していないので、問題解決になるかどうかは知りません。

参考にしたサイトを掲示しておきます。

余談
画像処理においてsetBackgroundという名前が用いられているが、この言葉使いは誤解を生じる。私はズ~っとforegroundという画像処理の何かが存在するものだと思い込んでいたのだ(アホ)。なので、背景の画像と混じり合わないようにするため、setBackgroundColor()の引数にアルファ値を0xffにした値を代入したりしていた。そんなことをした結果、Buttonが真っ白や真っ黒になってしまい、ボタンの画像が消失した意味が分からなかった。ボタンの画像は、foregroundに描かれているに違いないという思い込みがあったのだ。(そんな思い込みをするのはアンタだけや!)
テキストはsetText()で処理をするのであるから、テキストがforegroundである訳はない。もし、テキストがforegroundであるならば、setTextという名前を用いずに、setForeground(String text)という名前にすべきだ。
反意語に相当するものが存在しないのに、一方の反意語だけを使って表現するのはやめて欲しい。
正確に表現しようとするならば、setGraphicなのではないかと思う。

余談に対するツッコミ
Buttonの内部にはViewとTextViewがあって、backgroundはViewに存在します。TextViewにはdrawable top, drawable left等が存在します。
たぶん、Viewが基底部分にあって、その画像であるため、backgroundという用語を使用しているのだろうと思います。

ちなみに、SeekBarは、ViewとProgressBarから構成されています。Viewにおけるbackgroundで何か画像を設定して、ProgressBarにおけるdrawableで何か画像を設定した場合、Viewの画像が背景に見えて、ProgressBarにおける画像が手前に見えるようになります。

2013年6月11日火曜日

ClassCastException android.widget.* cannot be cast to android.widget.*

ClassCastException android.widget.* cannot be cast to android.widget.*

下記のエラーが出た。
具体例1.java.lang.ClassCastException: android.widget.ImageView cannot be cast to android.widget.Button

具体例2.java.lang.ClassCastException: android.widget.LinearLayout cannot be cast to android.widget.Button

私が遭遇した上記エラーのViewの階層構造の概念図は次のとおりである。
<LinearLayout> parent
    <LinearLayout> child
        <Button> has it's original ID.

<LinearLayout> parentの場所から、ButtonのIDをfindViewById()を使って指定して、そのButtonを使うことはできるし、エラーは発生しなかった。アプリに1個しか存在しないIDの値を使うのであるから問題はない。

この方法で以前は問題なく動作していたが、原因は不明であるが、ある時点以降、ButtonのIDを指定すると、「それはImageViewなのでButtonにcastすることはできない」という趣旨のエラーが出て、アプリがクラッシュするようになった。エラー発生箇所によっては、ImageViewではなく、LinearLayoutであったりTextViewであったりした。XMLやIDを調べたが、ButtonはButtonであった。しかしながら、ImageViewであるとされるIDの値は、ButtonのIDの値と同一なのである。このImageViewは私には身に覚えのないViewだ。変だね。

そこで、日本人が得意とするゲーム感覚で、<LinearLayout> childの場所から、つまり、Buttonの直接上のViewから、ButtonのIDを指定して使ったところ、問題無く動作するようになった。
非論理的不都合には、非論理的対応をするのが良いのかもしれない。むむむ!

なお、<LinearLayout> childでは、addView()やremoveAllViews()を使って、その内部にViewを実行時に加えたり削除したりしていた。

leaked ServiceConnection

Activity <App Name> has leaked ServiceConnection <ServiceConnection Name>@ that was originally bound here

このエラーに関しては次のサイトで議論されている。

実行順序は次のとおりである。
  1. onDestroy()....前のActivityが実行したonDestroy()
  2. unbindService()
  3. 上記エラーログ
  4. onUnbind()....Service
  5. onDestroy()....Service
  6. onCreate()....次のActivityが実行したonCreate()
現象の原因は、Answersの<4>が参考になりそうである。
対策としてはAnswersの<3>と<8>に書かれてあるが、この対策が真実であるのか私は疑問に感じた。ので、他の方法を模索した。が、成功しなかった。
結局、対策は、Answersの<3>と<8>に書かれてあるように、unbindService()をonDestroy()で行っているのであれば、そのbindService()はonCreate()で行わねばならない、ということであった。
このような対応策が非論理的であると感じてしまうのは、果たして私だけだろうか。。。。(あんた、だけや!)。。。むむむ!

2013年6月10日月曜日

Adding Recent Query Suggestions

Adding Recent Query Suggestions

前回の記事Action Barに検索Viewを設けるでは、入力欄を設けただけだった。ここで作業を終わってしまうのであれば、単なる、マニアである。SearchManagerはSuggestionsを利用してこそ有益になる。

Adding Recent Query Suggestionsに書かれているとおり下記のとおりサンプルプログラムを作ってみた。この記事で実現されるのは、利用者が過去に入力した文字列をそのまま候補として掲示することである。

public class MainActivity extends Activity{
    private final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.main, menu);
        SearchView sV;
        SearchManager sM = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
        ComponentName c = getComponentName();
        SearchableInfo si = sM.getSearchableInfo(c);
        MenuItem mi = menu.findItem(R.id.action_Search);
        sV = (SearchView)mi.getActionView();
        sV.setSearchableInfo(si);
        sV.setIconifiedByDefault(false);
     
        /*
         * setSubmitButtonEnabled()について
         * trueは、送信ボタンが右端に表示される。
         * falseは、表示されない。
         */
        sV.setSubmitButtonEnabled(true);
     
        /*
         * setMaxWidth()について
         * これを設定しない場合、幅が狭い。このため設定した方が良い。
         * 引数には、物理画面の横幅の大きさを超える値を設定しても問題は無い。
         * 超えた値であっても、送信ボタンやアイコンは表示される。タイトル文字列は見えない。
         */
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        sV.setMaxWidth(metrics.widthPixels);
//        setTitle("");これは不要です。
        return true;
    }

    @Override
    protected void onNewIntent(Intent intent){
        setIntent(intent);
        if(Intent.ACTION_SEARCH.equals(intent.getAction())==true){
            String q = intent.getStringExtra(SearchManager.QUERY);
            SearchRecentSuggestions srs;
            srs = new SearchRecentSuggestions(this,
                MySuggestionProvider.AUTHORITY,
                MySuggestionProvider.MODE
                );
            srs.saveRecentQuery(q, null);
            Log.d(TAG, "onNewIntent() : query==" + q);
        }
    }
}

MySuggestionProvider.java
public class MySuggestionProvider extends SearchRecentSuggestionsProvider {
    public final static String AUTHORITY = "com.example.searchable.MySuggestionProvider";
    public final static int MODE = DATABASE_MODE_QUERIES;

    public MySuggestionProvider() {
        setupSuggestions(AUTHORITY, MODE);
    }
}


AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.searchable"
    android:versionCode="1"
    android:versionName="1.0"
    >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17"
        >
    </uses-sdk>
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        >
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEARCH" />
            </intent-filter>
            <meta-data
                android:name="android.app.searchable"
                android:resource="@xml/searchable"
                />
        </activity>
        <provider
            android:name=".MySuggestionProvider"
            android:authorities="com.example.searchable.MySuggestionProvider"
            >
        </provider>
    </application>
</manifest>


res/xml/searchable.xml
<?xml version="1.0" encoding="utf-8"?>
<searchable
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_name"
    android:hint="@string/SearchHint"
    android:searchSuggestAuthority="com.example.searchable.MySuggestionProvider"
    android:searchSuggestSelection=" ?"
    >
</searchable>

2013年6月7日金曜日

実験:Locale.getDefault()で得られる情報

実験:Locale.getDefault()で得られる情報

本体設定で端末の言語を変更できる。
言語を変更した後、Locale.getDefault()でLocale情報を取得した場合、言語は当然変わるだろうが、国情報はどうなるのか。

結論:国情報も変わる。

本体設定で言語をEnglishに変更した後、Locale.getDefault()でLocaleを取得し、これでgetCountry()を実行させると、USが返る。

本体設定では「Language」だけを変更するような表記になっているのに、国情報も変わってしまうのだね。むむむ。

ちなみに、getResources().getConfiguration().localeも同様の結果になった。

「オレは英語を話すんだけど、住所地と国籍は日本なんだ」という御仁への配慮は、(以下省略)

2013年5月22日水曜日

Action Barに検索Viewを設ける

Action Barに検索Viewを設ける

検索を行うViewは、EditTextとButtonを併記すれば自作できる。これは簡単だ。
しかし、Androidシステムとして、検索に関する仕掛けが用意されているので、それを用いた方が、利用者の立場から見て親和性の高いViewを構築できる。

この記事は、検索用のViewだけを表示する仕掛けに関してだけである。検索するコマンドは実装していない。

私の所感
SearchView等を使わず、EditTextとButtonを用いて自作する方がずっと簡単だ!
にも関わらず、小難しいSearchViewを作っちゃったってことは、検索に関してマニアックになっている重要視しているってことだね。私としては、このようなマニアックなことは好きなので設計者の意向を尊重してSearchViewを活用してみることにした。

参考にしたサイト
Search Overview
Action Bar..."Action View"とは術語(専門用語)である。このAction Viewの具体例として、例えば、SearchViewがある。
Android Tutorial: Adding Search to Your Apps

簡単なサンプルプログラムを作ってみました。
Action Barに検索用文字列入力欄が表示され、そこに文字列を入力して実行すると、入力した文字列がログに表示されます。

public class MainActivity extends Activity{
    private final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu){
        getMenuInflater().inflate(R.menu.main, menu);
        SearchView sV;
        SearchManager sM = (SearchManager)getSystemService(Context.SEARCH_SERVICE);
        ComponentName c = getComponentName();
        SearchableInfo si = sM.getSearchableInfo(c);
        MenuItem mi = menu.findItem(R.id.action_Search);
     
/*
 * getActionView()について
 * menuのxmlにおいて、android:actionViewClassで設定したactionViewを取得する。
 * もし、android:actionViewClassが存在しない場合、getActionView()の戻値はnullである。
 */
        sV = (SearchView)mi.getActionView();
        sV.setSearchableInfo(si);
     
        /*
         * setIconifiedByDefault()について
         * falseは、文字列入力領域が表示される。
         * trueは、文字列入力領域は表示されずに、虫メガネアイコンだけが表示される。
         */
        sV.setIconifiedByDefault(false);
        return true;
    }

    @Override
    protected void onNewIntent(Intent intent) {
    /*
     * getIntent()で得られるIntentは、原初の時点でのIntentのままである。
     * setIntent()を使うことにより、最新のIntentに更新される。
     */
        setIntent(intent);
        if(Intent.ACTION_SEARCH.equals(intent.getAction())==true){
            String q = intent.getStringExtra(SearchManager.QUERY);
            Log.d(TAG, "onNewIntent() : query==" + q);
        }
    }
}

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.searchable"
    android:versionCode="1"
    android:versionName="1.0"
    >
    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="17"
        >
    </uses-sdk>
    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        >
        <activity
            android:name=".MainActivity"
            android:launchMode="singleTop"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEARCH" />
            </intent-filter>
            <meta-data
                android:name="android.app.searchable"
                android:resource="@xml/searchable"
                />
        </activity>
    </application>
</manifest>

searchable.xml
<?xml version="1.0" encoding="utf-8"?>
<searchable
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:label="@string/app_name"
    android:hint="@string/SearchHint"
    >
</searchable>

menu/main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
    <item
        android:id="@+id/action_Search"
        android:actionViewClass="android.widget.SearchView"
        android:showAsAction="always"
        >
    </item>
</menu>

2013年5月9日木曜日

音量の設定が変更されたという通知を受け取る

音量の設定が変更されたという通知を受け取る

副題:本体設定が変更されたことを取得する

標題について調べたところ、下記にコードがありました。
音量設定の取得について

このコードを参考に、下記のとおり私なりに作ってみました。このプログラムの実行中に、音量を変えると、ログが表示されます。
これは、本体設定が変更されたことを取得するためのプログラムなのですね。
一部の機種においては、Settings.System.VOLUME_MUSICの値が11に固定されています。固定されているということは、変更が発生しないということでありますので、その結果、ContentObserver#onChange()が呼ばれないということになってしまいます。この機種においては、"volume_music_speaker"に有効な値が代入されていました。
"volume_music_speaker"は「本体設定」の値の一覧取得プログラムを使って調べました。

public class MainActivity extends Activity{
    final private String TAG = "MainActivity";
    ContentResolver cr;
    MyObserver mo;
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        cr = getContentResolver();
        int i = Settings.System.getInt(cr, "volume_music_speaker", -1);
        Uri u;
        if(i>=0){
            u = Settings.System.getUriFor("volume_music_speaker");
        }
        else{
            u = Settings.System.getUriFor(Settings.System.VOLUME_MUSIC);
        }
        mo = new MyObserver(new Handler());
        cr.registerContentObserver(u, true, mo);
    }

    @Override
    protected void onDestroy() {
        Log.v(TAG, "onDestroy()");
        cr.unregisterContentObserver(mo);
        super.onDestroy();
    }

    private class MyObserver extends ContentObserver{
        final private String TAG = "It's ME!";
        public MyObserver(Handler h){
            super(h);
        }

        @Override
        public void onChange(boolean selfChange, Uri uri) {
            AudioManager am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
            Log.v(TAG, "onChange(). uri==" + uri +
                ". volume==" + am.getStreamVolume(AudioManager.STREAM_MUSIC));
        }

        @Override
        public void onChange(boolean selfChange){
            onChange(selfChange, null);
        }
    }
}

2013年4月27日土曜日

実験:configuration changeによるActivityの再起動を、コードで実現できるのか

実験:configuration changeによるActivityの再起動過程を、コードで再現できるのか

結論:私がやった限りでは、できなさそうである。

configuration changeが発生すると、Activityが再起動される途中で、onSaveInstanceStateメソッドが呼ばれる。
しかしながら、このメソッドはOSが呼び出すのであって、プログラマがコードで呼び出すことはできない。

onConfigurationChangedメソッド内で、Intentに与えるフラグを操作して、新たにActivityを起動してやれば、onSaveInstanceStateメソッドが呼ばれる。
しかし、この場合、旧Activityのdestroyが、新ActivityのonCreateの後に発生する。この挙動は、私の希望ではない。
finishメソッドを使うと、onSaveInstanceStateメソッドは呼ばれない。

onSaveInstanceStateメソッドは、次のとおり書かれてある。
This method is called before an activity may be killed so that when it comes back some time in the future it can restore its state.
しかしながら、実際には、killされない時にも呼ばれる場合がある。
onSaveInstanceStateメソッド内では、killされることを前提にコードを書いてはいけない。データを保存することに徹した方が良い。

端末がスリープ状態にある時に、Eclipseからデバッグ目的でアプリを起動すると、onSaveInstanceStateメソッド&onDestroy()が繰り返し呼ばれる。この問題に対して、AndroidManifest.xmlにおいてandroid:launchMode="singleTop"を施してみたが、効果は無かった。

Android Configuration Changeに書かれてあるように、onDestroy()の中でgetChangingConfigurations()を使うことも役に立ちそうである。

    @Override
    protected void onDestroy(){
        int iConfig = getChangingConfigurations();
        iConfig &= ActivityInfo.CONFIG_ORIENTATION;
        if(iConfig>0){
            Log.d(TAG, "onDestroy() : ConfigurationChange");
        }

この話題はこれ以上つっこむことなく、ここらで引き揚げることにする。

参考
Android: onSaveInstanceState not being called from activity

2013年4月23日火曜日

RadioButtonの背景色を変更する方法

RadioButtonの背景色を変更する方法

RadioButtonの背景色を変更すると、RadioButtonの形状が崩れることが指摘されています。
Android RadioButton の画像とテキストの間隔を広げる
私は、形状崩れをなんとかしたいと思います。
なお、この現象は少なくともAndroid 4.2.2では発生しません。2.2及び2.3では発生します。

解決方法 その1
xmlにおいて背景色を設定した場合に、形状崩れが発生するのです。実行時に背景色を設定すれば問題は発生しません。

解決方法 その2
RadioGroupを使用せず、javaのコードにおいて、RadioButtonに対する押し下げをonClickメソッドで補足し、他のRadioButtonをsetChecked(false)してやるのです。なお、個々のRadioButtonの上位に個別にLinearLayout等を配置し、このLinearLayoutに対して背景色を設定します。

2013年4月16日火曜日

自分のアプリをZ順の最上位にする

自分のアプリをZ順の最上位にする

自分のアプリのActivityが、画面の前面に無い場合、つまりZ順における最上位では無い場合、詳しく言えば利用者の視点で端末の画面を見た時に他のアプリが表示されていて自分のアプリが見えない場合、自分のアプリのActivityを前面に出してやるにはどうするのか。

自分のアプリのActivityが稼働していない状態であれば、Intentを使えば良いことはわかります。次のとおりです。

Intent it;
it = new Intent(this, MainActivity.class);
it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(it);//Activityを起動し,上にしてやる

しかしながら、他のアプリが前面に出ていて、自分のアプリのActivityがバックグラウンドにある場合、つまり自分のActivityが生きておりかつ、後ろに隠れている場合、自分のアプリのActivityを前面に出す方法はどうすれば良いでしょうか。
多くのサイトではIntentを使うように案内されています。
確かに、Intentすれば、前面には出ますが、IntentされたActivityは新規に作成されてしまうのです。onCreateメソッドが実行されてしまい、Activityが複数存在することになってしまいます。

このため、何か違うような気がしたのでrequestFocus()やbringToFront()を試しましたがうまくいきませんでした。次のサイトで議論があるように、getWindow().setActive()でもダメなようです。
ブロードキャストの受信で、自分自身を前面に表示させる方法について

結論は、やっぱり、Intentを使うのです。コードは次のとおりです。Serviceにおいて実行させます。

Intent it;
it = new Intent(this, MainActivity.class);//ここでのthisはServiceです。
it.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(it);//Activityを上にしてやる

これを使えば、ActivityのonCreateメソッドは実行されず、複数のActivityが生成されることなく、自分のアプリをZ順の最上位にすることができます。Intent.FLAG_ACTIVITY_NEW_TASKがあるため、新規に作成されてしまうように思えますが、新規には作成されませんし、これが無いと実行時にエラーになります。
最近の開発環境は良くなっており、エラーになった場合、このIntent.FLAG_ACTIVITY_NEW_TASKを設定しなさいという趣旨のメッセージがログに表示されます。

過去の私の記事において、Intent.FLAG_ACTIVITY_SINGLE_TOPは使う機会が無いという趣旨の記述をしてしまいました。むむむ! 起動モード
そうでもないということですね。

2013年4月15日月曜日

SimpleAdapter : レコード毎に色を変えて表示する

SimpleAdapter : レコード毎に色を変えて表示する

文字列だけで構成されるレコードを一覧にして表示させた場合、レコード間の区切りが見分けにくくなります。
その対処方法として、 レコード毎に色を変えて表示するとか、レコード間の幅を大きくするという方法があります。
javaのコードの概要は下記のとおりです。概要を示したものであるため、このママでは動作しません(見れば分かる)
色分けのフラグをレコード毎に設定し、そのフラグに基づいて背景色を変更すること、そして、背景色の設定にはアルファ値を適当な値で設定することがポイントです。アルファ値を0xffに設定した場合、利用者がそのレコードを選択しても「選択したことを示す背景色」が表示されません。

    ArrayList<Map<String, Object>> al;//List表示用データ
        al = new ArrayList<Map<String, Object>>();
        
            b = cs.moveToLast();
            int i = 0;
            while(b==true){
                String s;
                Map<String, Object> m;
                m = new HashMap<String, Object>();
                s = cs.getString(cs.getColumnIndex("Keyword"));
                m.put("Keyword", s);
                m.put("Background", i%2);//ゼロか1を代入する
                al.add(m);
                i++;
                b = cs.moveToPrevious();
            }
        }
        cs.close();
        
        String sFrom[] = {"Keyword", "Background"};
        int iTo[] = new int[]{ R.id.Keyword_Text, R.id.Layout_Keyword_Background };
        
        SimpleAdapter sa;
        SimpleAdapter.ViewBinder savb;
        savb = new SimpleAdapter.ViewBinder(){
            @Override
            public boolean setViewValue(View v, Object data, String s){
                if(v.getId()==R.id.Layout_Keyword_Background){
                    int i = (Integer)data;
                    if(i==1) v.setBackgroundColor(0x60ffdddd);
                    else v.setBackgroundColor(0x00ffffff);
                    return true;
                }
                return false;
            }
        };
        
        sa = new SimpleAdapter(mainActivity, al, R.layout.list_keyword, sFrom, iTo);
        sa.setViewBinder(savb);
        ListView lv;
        lv = (ListView)mainActivity.findViewById(R.id.ListKeyword);
        lv.setOnItemClickListener(this);
        lv.setAdapter(sa);
        lv.setDividerHeight(10);

list_keyword.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/Layout_Keyword_Background"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="10dp"
    >
    
    <TextView
        android:id="@+id/Keyword_Text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textIsSelectable="false"
        >
    </TextView>
</LinearLayout>

面白い動画を見つけました
メ~テレ ぬいぐるみのラパン

2013年3月29日金曜日

YouTubeInitializationResult

YouTubeInitializationResult

YouTubeInitializationResultは、onInitializationFailureメソッドの第二引数として添付されてくる。
インストールされているYouTubeアプリのバージョンが古い場合に有益な振る舞いをしてくれる。
getErrorDialogメソッドには、引数にDialogInterface.OnCancelListenerが存在しない版もある。この版の場合、setOnCancelListenerメソッドでリスナを追加するのであろうか。
(めったにいないであろう、私のような)利用者が「戻る」キーを押し下げることも有りうるのであるから、OnCancelListenerの実装は必須では無いだろうか。

YouTubeアプリのバージョンが古い場合、「YouTubeアプリを更新」画面が表示される。そして「YouTubeアプリを更新」ボタンを押し下げると、onSaveInstanceStateメソッドが呼び出される。このタイミングで呼び出されるのであるから、onSaveInstanceStateメソッド内に記載する各種オブジェクトの初期化状況について注意を払い、NullPointerExceptionが発生しないようにする必要がある。

「YouTubeアプリを更新」ボタンの押し下げ後、画面はGoogle PlayのYouTubeアプリ更新画面になる。この画面で「戻る」キーを押し下げると、onActivityResultメソッドが呼び出される。この場合、このメソッドの第2引数はRESULT_OKではない。

Google PlayのYouTubeアプリ更新画面で、適切に更新を実行した後、onActivityResultメソッド内で、YouTubePlayerView#initializeメソッドを実行するように仕込んでいたが、原因不明のエラーが発生してしまった。YouTubePlayerView#initializeメソッドとは別の要因のエラーかもしれない。もはや当方ではYouTubeアプリの更新実験はできない。

static final int ID_Error_requestCode_YouTubeAppDialog = 24;
private YouTubePlayerView ytView;
Dialog DlgYouTubeAPI;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ytView = (YouTubePlayerView)findViewById(R.id.youtube_view);
    ytView.initialize(DEVELOPER_KEY, this);
    (以下省略)

@Override
public void onInitializationFailure(Provider arg0, YouTubeInitializationResult arg1){
    if(arg1.isUserRecoverableError()==true){
        //インストールされているYouTubeアプリのバージョンが古い場合
        DlgYouTubeAPI = arg1.getErrorDialog(this, ID_Error_requestCode_YouTubeAppDialog, this);
        DlgYouTubeAPI.show();//新版のインストールを促すダイアログが表示される。
    }
    else{//Recoverableでは無いエラーも存在しえる。
    (以下省略)

@Override
public void onCancel(DialogInterface arg0){
    if(arg0==DlgYouTubeAPI){//新版のインストールを促すダイアログ表示時に、「戻る」キーを押し下げた場合、ここに来る。
        finish();
        return;
    }(以下省略)

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data){
    switch(requestCode){
    case ID_Error_requestCode_YouTubeAppDialog:
        if(resultCode==RESULT_OK){
            //ここに何を書けば良いのだ?
            finish();//取り敢えず、再起動をしていただこうかね?
        }
        else{//Google PlayのYouTubeアプリ更新画面で「戻る」キーの押し下げ。
            finish();
        }
        break;
    }
    super.onActivityResult(requestCode, resultCode, data);
}

2013年3月3日日曜日

has leaked ServiceConnection com.google.android.youtube.player.internal.r

has leaked ServiceConnection com.google.android.youtube.player.internal.r

インターネットに接続しない環境下で、YouTubePlayerView.initialize()を実行すると、onInitializationFailureメソッドで結果を受け取ることになる。そして「戻る」キーを押し下げてアプリを終了させると下記のエラーが出る。むむむ!
E/ActivityThread(7188): Activity jp.Androyer.xxx.MainActivity has leaked ServiceConnection com.google.android.youtube.player.internal.r$e@41babb20 that was originally bound here
E/ActivityThread(7188): android.app.ServiceConnectionLeaked: Activity jp.Androyer.xxx.MainActivity has leaked ServiceConnection com.google.android.youtube.player.internal.r$e@41babb20 that was originally bound here
Leak while switching activity with YouTubeThumbnailViewsに似たようなエラーがあるが、回答はonInitializationSuccessメソッドで結果を受け取ることを前提にしている。
この回答から察するに、おそらく、エラーになったにも関わらず、YouTubePlayerを生成してしまい、かつYouTubePlayerをrelease()していないからだと思われます。

インターネットへの接続状態判断であれば、別の方法でできるので問題はない。しかし、他の原因でエラーが発生した場合、どうなるのか不安になる。

2013年2月23日土曜日

実験:getDrawingCache()により取得するBitmapには子Viewも含まれるのか

実験:getDrawingCache()により取得するBitmapには子Viewも含まれるのか

結論:含まれる

親View{
 子View
 子View
 ...


上記関係にあるViewにおいて、親ViewでgetDrawingCache()して取得したBitmapには子Viewの画像も描かれている。
実験に使ったコードは下記のとおり。

public class MainActivity extends Activity
    implements
    OnClickListener
    {
    Button btn1, btn2;
    Bitmap bm;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn1 = (Button)findViewById(R.id.Button1);
        btn1.setOnClickListener(this);
        btn2 = (Button)findViewById(R.id.Button2);
        btn2.setOnClickListener(this);
        bm = null;
    }

    @Override
    protected void onDestroy() {
        if(bm!=null) bm.recycle();
        super.onDestroy();
    }

    @Override
    public void onClick(View v) {
        if(bm!=null){
            bm.recycle();
            bm = null;
        }
     
        if(v==btn1) v = (RelativeLayout)findViewById(R.id.RelativeLayout);
        else{
            if(v==btn2) v = (TextView)findViewById(R.id.TextView);
            else v = null;
        }
        if(v!=null){
            v.setDrawingCacheEnabled(true);
            bm = v.getDrawingCache().copy(Bitmap.Config.ARGB_8888, false);
            v.setDrawingCacheEnabled(false);
            ImageView iv = (ImageView)findViewById(R.id.Image2);
            iv.setImageBitmap(bm);
        }
    }
}

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    >
    <RelativeLayout
        android:id="@+id/RelativeLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#9999ff"
        >
        <ImageView
            android:id="@+id/Image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/ic_launcher"
            android:contentDescription="@string/app_name"
            android:background="#99ff99"
            >
        </ImageView>
        <Button
            android:id="@+id/Button1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/Image"
            android:text="@string/Button1" 
            >
        </Button>
        <TextView
            android:id="@+id/TextView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/Image"
            android:layout_below="@id/Button1"
            android:gravity="center"
            android:text="@string/hello_world" 
            android:background="#ff9999"
        />
    </RelativeLayout>
    <Button
        android:id="@+id/Button2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/Button2" 
        >
    </Button>
    <ImageView
        android:id="@+id/Image2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/ic_launcher"
        android:contentDescription="@string/app_name"
        >
    </ImageView>
</LinearLayout>


2013年2月16日土曜日

NoClassDefFoundError

NoClassDefFoundError

NoClassDefFoundErrorに関する仕掛けは、次のサイトに書かれてある。
Androidアプリ実行時にNoClassDefFoundError
実行時にクラスパスが通ってない場合に発生するそうです。

私の場合は、Libsフォルダー内に、本来あるべきjarファイルを復活させたら治りました。

発生原因は、Libsフォルダー内の、何かのjarファイルを誤って削除してしまっていたことのようです。

2013年2月15日金曜日

Access Not Configured

Access Not Configured

detailMessage "403 Forbidden\n{\n  "code" : 403,\n  "errors" : [ {\n    "domain" : "usageLimits",\n    "message" : "Access Not Configured",\n    "reason" : "accessNotConfigured"\n  } ],\n  "message" : "Access Not Configured"\n}" (id=xxxxxxxxxx)

上記エラーに出会うかもしれない。もし、出会ったなら、keyの作成に問題があったのかもしれない。
APIs Consoleにアクセスして、
API Access>Simple API Access>Delete key...>Delete key
Create new Android Key...>空欄には何も記入せず>Create
これで再度チャレンジしてみよう。

2013年2月10日日曜日

BitmapFactory.Options inSampleSize

BitmapFactory.Options inSampleSize

私は、inSampleSizeに3を代入すれば、(Bitmapの大きさが)3分の1になるものと思い込んでいました。しかし、挙動が変なので、調べてみたのです。

BitmapFactory.Options inSampleSizeに関しては、次のサイトで動作検証がなされています。
BitmapFactory.Options inSampleSize の性能テストとか動作とか
貴重なデータを公開して頂きありがとうございます。手間が省けました。
上記サイトでの検証結果では、inSampleSizeに3を代入しても(Bitmapの大きさが)2分の1になってしまうのです。むむむ!

検証結果からすると、inSampleSizeに関して次のように解釈できます。
inSampleSizeを用いて縮小する場合、inSampleSizeに代入されている値の小さい方の直近の「2の累乗の倍数」で縮小される。
例えば、inSampleSizeに10を代入したとしても、(10分の1になるのではなく)8分の1に縮小されるのです。

念のため、inSampleSizeに関する留意点を書いておきます。

  1. inSampleSizeに関する仕掛けは、メモリの節約を目的としている。
  2. inSampleSizeには、縮小する倍率を代入する。例えば、2分の1に縮小したい場合には2を代入する。
  3. inSampleSizeは、int型であるため(float型では無いという意味で)大雑把な縮小しかできない。
  4. inSampleSizeは、上記の検証結果のとおり、大雑把な縮小しかできない。
  5. (inSampleSizeを使って作成された)この大雑把に縮小されたBitmapに対して、必要に応じて、本来目的としていたサイズに(更に微調整して)縮小する。
  6. 縮小する大きさが2分の1よりも大きい場合(例えば、3分の2の大きさに縮小したい場合)、inSampleSizeに関する仕掛けを使うことに意味は無い。この場合、別の仕掛けで縮小する。



2013年1月27日日曜日

画面サイズに応じてAdMobのサイズを変える

画面サイズに応じてAdMobのサイズを変える

従前は、画面が縦位置か横位置かによって、また、使われる端末の画面の大きさによって、AdMobの幅や位置を調整しなければならなかった(しなくても良いのだが)。

だが、これは不便だ。以前私は、このような不便に対する不満をこのブログに書いておいた。AdMobを中央に配置する。
そうしたところ、この要望に応えて頂いたのが、Smart Bannersなのである(私の要望に応えた訳では無いだろうが)。

Smart Bannersを使うことによって、AdMobは自動的に自分でサイズが変わることができる。これにより、プログラマが画面のサイズを取得してAdMobのサイズや位置を調整する必要は無くなった。自動的に、横中央に配置してくれるのである。

sample codeは次のとおりである。
    AdView adView;
    LinearLayout ll;
    AdRequest ar;
        
    //第二番目の引数にSMART_BANNERを指定する。
    //第三番目の引数にはAdMob publisher IDを指定する。
    adView = new AdView(this, AdSize.SMART_BANNER, "xxxxxxx");
    ll = (LinearLayout)findViewById(R.id.AdSettings);  
    ll.addView(adView);
    ar = new AdRequest();
    adView.loadAd(ar);

2013年1月15日火曜日

Localeをsortする

Localeをsortする

Stringをsortすることは、ArrayAdapterのsort()を使ってみるのとおりです。

今回はArrayAdapterを使ってLocaleをsortします。

toString()メソッドを実装しているObjectであればArrayAdapterで取り扱えます。幸いにして、Locale classはtoString()メソッドを実装しています。

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ListView lv;
        lv = new ListView(this);
        setContentView(lv);
        
        ArrayAdapter<Locale> aa;
        aa = new ArrayAdapter<Locale>(this, R.layout.activity_main, ls);
        aa.sort(new MyComparator());
        lv.setAdapter(aa);
    }
    
    public class MyComparator implements Comparator<Locale>{
        @Override
        public int compare(Locale l1, Locale l2){
            int iRet;
            iRet = l1.getLanguage().compareTo(l2.getLanguage());
            if(iRet==0){
                return l1.getCountry().compareTo(l2.getCountry());
            }
            else return iRet;
        }
    }

    Locale[] ls = {//ランダムに並んでいる
        Locale.CHINA,
        Locale.FRENCH,
        Locale.CANADA,
        Locale.JAPANESE,
        Locale.GERMANY,
        Locale.ENGLISH,
        Locale.CANADA,
        Locale.GERMAN,
        Locale.JAPAN,
        Locale.US,
        Locale.FRANCE,
        Locale.UK
    };
}

R.layout.activity_mainのxmlは次のとおりです。
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/hello_world" 
    >
</TextView>

2013年1月13日日曜日

onCreateOptionsMenuが呼び出されない

onCreateOptionsMenuが呼び出されない
onCreateOptionsMenu is not called in 4.0

標題については次のサイトで議論されている。
Options menu not showing in ICS using compatibility library
Android3.0以降 onCreateOptionsMenuが呼び出されない

これらのサイトで書かれているように、AndroidManifest.xmlファイルでTheme関連を全て削除したが、引き続き問題は発生し続けた。

私のプログラムのonCreateメソッドには次の一文があった。
requestWindowFeature(Window.FEATURE_NO_TITLE);
これを削除すると問題が解決した。

これを削除した場合であっても、AndroidManifest.xmlファイルでThemeが設定されていると問題は発生する。
AndroidManifest.xmlファイルでThemeを設定したい場合には次の一文を追加すれば良い。
requestWindowFeature(Window.FEATURE_ACTION_BAR);

以上を整理すると、onCreateメソッドにおいて次のように書けば良い。
public void onCreate(Bundle icicle){
    super.onCreate(icicle);
    if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.ICE_CREAM_SANDWICH){
        requestWindowFeature(Window.FEATURE_ACTION_BAR);
    }
    else{
        requestWindowFeature(Window.FEATURE_NO_TITLE);
    }
    setContentView(R.layout.inputbyhand);

参考にしたサイト

2013年1月6日日曜日

動的にオプションメニューを変更する

動的にオプションメニューを変更する
dynamically changing option menu

動的にoption menuを変更する場合、addメソッドやremoveitemメソッドを使いたくなる。

しかしながら、removeitemメソッドを使った後で、addメソッドを使いMenuItemを追加しても、その追加されたMenuItemはMenuの最後尾に配置される。
追加されたMenuItemを任意の位置に配置するコマンドは無いようである。

このため、MenuItem#setVisible()を使うことになる。
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        MenuItem mi;
        mi = menu.findItem(R.id.Menu_Next);
        if(bDiskFile==true){//必要に応じ
            if(mi.isVisible()==false){
                mi.setVisible(true);
            }
        }
        else{
            if(mi.isVisible()==true){
                mi.setVisible(false);
            }
        }
        return super.onPrepareOptionsMenu(menu);
    }

addメソッドの第一引数にはint groupIdがある。これは、任意の複数のMenuItemを追加する場合に、それらをグループ化するものである。Java codeでaddメソッドを使って、1個1個MenuItemを追加していく場合に使う。
詳細は次のサイトにsample codeがある。
Options Menu in Android (Code)

xmlでgroupを設定するsampleは次のサイトにある。
Android 奔走記 XML で Menu

2013年1月3日木曜日

GalleryからPicasaの画像を取得する

GalleryからPicasaの画像を取得する

Galleryを起動するには次のようにする。
    Intent it = new Intent();
    it.setType("image/*");
    it.setAction(Intent.ACTION_PICK);
    startActivityForResult(it, ID_Result_Gallery);

Galleryから戻ってくると、onActivityResultメソッドで値を取得する。
    protected void onActivityResult(int r, int resultCode, Intent data) {
    if(r==ID_Result_Gallery){
        if(resultCode==RESULT_OK){
            Uri u = data.getData();
            //この記事は、この場所で何をするのかという内容です
        }
        else{//Galleryの操作途中でキャンセルした場合
        }

Galleryから取得したUriのデータ内容が端末に存在する画像ファイルを指す場合、そのUriのデータを、フルパスファイル名(String型)に変換できる。次がそのprogramです。
    ContentResolver cr = getContentResolver();
    String[] columns = { MediaColumns.DATA };
    Cursor c = cr.query(u, columns, null, null, null);
    if(c==null) return;
    c.moveToFirst();
    i = c.getColumnIndex(MediaColumns.DATA);
    s = c.getString(i);
    c.close();

しかし、Uriのデータ内容が、Picasaの画像を指す場合、そのUriのデータからフルパスファイル名(String型)には変換できない。Cursor#getString()の戻値はnullになる。
ただし、次のようにprogramすれば表示用の名前(実行上はファイル名)を取得できる。
    ContentResolver cr = getContentResolver();
    String[] columns = { MediaColumns.DATA,  MediaColumns.DISPLAY_NAME};
    Cursor c = cr.query(u, columns, null, null, null);
    if(c==null) return;
    c.moveToFirst();
    i = c.getColumnIndex(MediaColumns.DISPLAY_NAME);
    s = c.getString(i);
    c.close();

Picasaの画像データは端末に存在しないため、ファイルのパス名を取得できない仕掛けになっているのであろう。

上記プログラムで知り得たUriの値をメモしておいて、その値を使って、onCreate()の時点で、Picasaの画像を呼び出すと、アプリがクラッシュする。面白いので、読者諸兄も下記のプログラムを実行してみよう。読者諸兄をショウモナイことに誘う。
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Uri u;
        Uri.Builder ub = new Uri.Builder();
        ub.scheme(ContentResolver.SCHEME_CONTENT);
        
        // * test pattern 1 Picasaのファイル
        ub.authority("com.google.android.gallery3d.provider");
        ub.path("/picasa/item/0000000000000000000");
        
        // * test pattern 2 端末内のファイル
        //ub.authority("media");
        //ub.path("/external/images/media/142");
        
        u = ub.build();
        
        ContentResolver cr = getContentResolver();
        
        //pattern 1 の場合次の行でクラッシュする。
        Cursor c = cr.query(u, null, null, null, null);

Picasaの画像データは「保護されている状態」にあるのかもしれない。プログラムで直接参照しに行くと死に行くことになる(シャレ)。
Galleryから戻って来た後であれば、当該Uriを指定して当該画像を取得することはできる。

ただし、ある画像ファイルをGalleryで選択した後で、(Picasaの)別の画像ファイルをプログラムで直接参照しに行くと死に行くことになる(再度シャレ)。
つまり、端末の利用者がGalleryでPicasaの画像を選択した後でなければ、そのPicasaの画像をプログラムで取り扱えないのである。

利用者がGalleryで選択しなければ当該画像を取得できない。Uriのデータだけを使えばPicasaデータを取得できるというものではない。端末に存在する画像ファイルであればUriのデータだけで取得できるのに。
利用者にいちいちGalleryで選択して頂くのは面倒である。このため、アプリを構築する場合には、取得した画像を自分の端末に保存することになるであろう。


次に留意点を書いておく。
androidのバージョンが4.0以上である場合
Uriの<scheme>にはcontentが代入されている。
画像データを取得するにはContentResolver#openInputStream()を使う。
AndroidManifestファイルにpermissionを設定する必要は無い。

androidのバージョンが2.3の場合
Uriの<scheme>にはhttpsが代入されている。
画像データを取得するにはHttpGet等を使う。
HttpGet等を使うのであるから、AndroidManifestファイルにInternet関係のpermissionを設定する必要がある。

4.0はpermissionを設定する必要がないので優れていると評価できる。私の手元には3.0の端末が無いため検証はできない。

sample programは次のサイトに掲載した。
GalleryからPicasaの画像を取得する

以下はファイル名の話である
PicasaにおけるMediaColumns.DISPLAY_NAMEのデータも保存しておく必要はある。このデータをアプリのタイトルバーに表示するのである。また、「保存した画像を選択する」時に表示する文字列としても利用する。

MediaColumns.DISPLAY_NAMEはDISPLAYの名前であって、(実行上はファイルの名前ではあるが)正確にはファイルの名前では無い。もしかして、nullである可能性もある。このため、nullの場合には"unknown"という文字列をprogramで挿入すれば良い。
ファイル名として取り扱えない文字が含まれている可能性を考慮しなければならない。何故考慮しなければならないのか。何故なら、私は、この仕様の詳細を知らないからである(ここで笑う)。
このため、MediaColumns.DISPLAY_NAMEで取得した文字列は、あくまで表示用として使い、ファイル名に応用するのはやめた方が良い。

不確実な前提を基に、適当な情報の提供をしているが、この情報が諸兄のお役に立てれば不幸中の幸いである(再度ここで笑う)。