‘Android’ カテゴリーのアーカイブ

AndroidのIn-App BillingでIAB_PERMISSION_ERROR

公開鍵設定して、テストアカウントが登録してあるのを確認して、署名したapkをアップロードして、アプリ内課金を実行、しようとしたら以下のエラーに遭遇。

I/ActivityManager(96): Starting: Intent { act=android.intent.action.VIEW cmp=com.android.vending/com.google.android.finsky.activities.IabActivity (has extras) } from pid -1
D/Finsky(23209): [1] SelfUpdateScheduler.checkForSelfUpdate: Skipping self-update. Local Version [8014017] >= Server Version [0]
I/Mozc(167): RUN EMOJI PROVIDER DETECTION: null
I/ActivityManager(96): Displayed com.android.vending/com.google.android.finsky.activities.IabActivity: +284ms
W/Finsky(23209): [1] CarrierParamsAction.run: Saving carrier billing params failed.
E/Finsky(23209): [31] FileBasedKeyValueStore.delete: Attempt to delete '***' failed!
D/Finsky(23209): [1] GetBillingCountriesAction.run: Skip getting fresh list of billing countries.
E/Finsky(23209): [1] CarrierBillingUtils.isDcb30: CarrierBillingParameters are null, fallback to 2.0
E/Finsky(23209): [1] CheckoutPurchase.setError: type=IAB_PERMISSION_ERROR, code=12, message=null

結局、「1時間ほど待ったら解決してた」というオチ。


AlertDialogのViewとの隙間を埋める

AndroidのAlertDialogを継承したクラスで、タイトルなし、setViewでダイアログいっぱいにViewを広げようとしたら、上下に隙間が開いてしまいました。

image

解決方法は簡単で、setView(View view)ではなく、setView(View view, int viewSpacingLeft, int viewSpacingTop, int viewSpacingRight, int viewSpacingBottom)を使うだけです。

image

コードのサンプルは以下のような感じです。

public class MyDialog extends AlertDialog {

    public MyDialog(Context context) {
        super(context);
    }

    @Override
    public void show() {

        LayoutInflater inflater = (LayoutInflater) this.getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        View layout = inflater.inflate(R.layout.my_dialog, null);

        GridView gridView = (GridView) layout.findViewById(R.id.gridView1);

        // this.setView(layout); // <- 上下に余白ができる
        this.setView(layout, 0, 0, 0, 0); // <- ぴったり埋まる

        super.show();

    }

}

ちなみに、AlertControllerの中ではこのように処理されています。

            customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel);
            FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom);
            custom.addView(mView, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
            if (mViewSpacingSpecified) {
                custom.setPadding(mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
                        mViewSpacingBottom);
            }
            if (mListView != null) {
                ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0;
            }

R.id.customはalert_dialog.xmlで定義されています。

    <FrameLayout android:id="@+id/customPanel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1">
        <FrameLayout android:id="@+android:id/custom"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="5dip"
            android:paddingBottom="5dip" />
    </FrameLayout>

上下5dip開いてますね。


androidでセパレーター画像を横幅いっぱいにする

長さが足りない素材で、セパレーターを画面の幅いっぱいに広げるテクニック。

下の画像は、上側のセパレーターが広げたもので、下のセパレーターが素材のままです。

image

layout.xml

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

    <ImageView
        android:id="@+id/separator"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:contentDescription="@string/separator"
        android:scaleType="fitXY"
        android:src="@drawable/separator_repeat" />

</LinearLayout>

separator_repeat.xml

<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/separator"
    android:dither="true"
    android:tileMode="mirror" />

肝はbitmapのtileModeと、ImageViewのscaleTypeです。

tileMode="mirror"は左右を反転しながら繰り返し表示をしてくれるので、繋ぎ目が目立ちにくくなります。

scaleType="fitXY"は、指定しないとImageViewが画面いっぱいに広がっていても、画像は広がってくれません。指定すれば、タイル表示が有効になります。


Androidの設定画面で使うカスタムPreferenceを作る

PreferenceActivityを使ってそこそこ簡単に設定画面を作れるのは良いのですが、特別な機能を持った項目を追加したい場合があります。

今回は、設定画面からWebのページへジャンプする項目を作る要件があったので、Preferenceを拡張して実装しました。

単純にPreferenceをres/xml/pref.xmlに書いて、PreferenceActivityの中でクリックイベントを設定することもできますが、なるべくコードをすっきりさせたいですし、再利用性も考えてLinkPreferenceというクラスを作成します。リンク先をXMLのアトリビュートに書くだけでジャンプ先を指定できます。

まずはpref.xmlにLinkPreferenceを追加します。

res/xml/pref.xml

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:hoge="http://schemas.android.com/apk/res/hoge" >

    <info.loadlimits.android.preference.LinkPreference
        android:key="link_preference"
        android:title="サイトをブラウザで表示"
        hoge:url="http://blog.loadlimits.info/" />

</PreferenceScreen>

名前空間は適当に定義しておきます。どうせここ以外では使われません。3行目にxmlns:hogeを指定します。8行目で指定されたURLがジャンプ先になります。

res/values/attr.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <attr name="url" format="string" />

    <declare-styleable name="LinkPreference">
        <attr name="android:key" />
        <attr name="android:title" />
        <attr name="android:summary" />
        <attr name="url" />
    </declare-styleable>

</resources>

attr.xmlで新しく追加するプロパティを定義します。declare-styleableを指定することで、R.styleable.LinkPreferenceやR.styleable.LinkPreference_urlが自動的に定義されます。

あとは実際のLinkPreferenceの中身を書けば完了です。

src/info/loadlimits/android/preference/LinkPreference.java

package info.loadlimits.android.preference;

import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.net.Uri;
import android.preference.Preference;
import android.util.AttributeSet;

public class LinkPreference extends Preference {

    private String mUrl;

    public LinkPreference(Context context) {
        this(context, null);
    }

    public LinkPreference(Context context, AttributeSet attrs) {
        super(context, attrs);

        TypedArray a = context.obtainStyledAttributes(attrs,
                R.styleable.LinkPreference);
        mUrl = a.getString(R.styleable.LinkPreference_url);
        a.recycle();
    }

    @Override
    protected void onClick() {
        Uri uri = Uri.parse(mUrl);
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        getContext().startActivity(intent);
    }

}

こういうカスタムPreferenceや、他のカスタムViewも含めてどこかでまとめて公開して再利用がしやすいようにしたいですね。


AndroidのMediaPlayerで動画が表示されない現象

Nexus Oneで動画プレイヤーを開発していて、Galaxy S2で試したら音は鳴るけど映像が表示されないという状況がありました。

問題点としては以下の2つ。

  • 動画の縦横サイズが取れない
  • ホルダーのサイズを固定しても出力されない

どうやらMediaPlayer.createがハマりどころのようです。

以下がNexus Oneで動いていたコードです。

            mMediaPlayer = MediaPlayer.create(this, Uri.parse(moviefile));
            mMediaPlayer.setOnCompletionListener(this);
            mMediaPlayer.setDisplay(holder);
            
            int videoHeight = mMediaPlayer.getVideoHeight();
            int videoWidth = mMediaPlayer.getVideoWidth();

これが、Galaxy S2に持ってくると、getVideoWidthとgetVideoHeightが0を返します。

結論としては、以下のように書き換えました。

            // mMediaPlayer = MediaPlayer.create(this, Uri.parse(moviefile));
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.reset();
            mMediaPlayer.setDataSource(this, Uri.parse(moviefile));
            // setDisplayをprepareの前に持ってこないといけない
            mMediaPlayer.setDisplay(holder);
            mMediaPlayer.setOnCompletionListener(this);
            mMediaPlayer.setOnPreparedListener(this);
            mMediaPlayer.prepare();

(中略)

    @Override
    public void onPrepared(MediaPlayer mp) {
        int videoHeight = mp.getVideoHeight();
        int videoWidth = mp.getVideoWidth();

prepareメソッドを呼んだ後でも、やっぱりビデオサイズは取れないので、OnPreparedListenerを登録して、そちらで動画サイズを取得する必要があります。

prepareAsyncがあるのだからprepare後は当然mMediaPlayerからサイズ取れると思ったのですが。

prepareの動作が端末(バージョン?)ごとにちょっと違うみたいですね。

それと、以下のエラーが出ますが原因不明です。特に問題は起きていないのですが…

06-04 14:16:56.665: E/Surface(16331): Surface::init token -2 identity 278

06-04 14:16:56.670: E/MediaPlayer-JNI(16331): setDataSource: outside path in JNI is �x@

結局VideoView使うのが一番楽かと。


AndroidのMessage.sendToTarget()の動作を調べた

com.android.internal.app.AlertControllerを読んでます。

とりあえずsendToTargetはHandlerに対して、sendMessage(this)をしているだけです。

ここのHandlerはMessage.obtain(handler, what, listener)で指定したhandlerです。
thisはもちろんMessage。

ではHandler.sendMessage(Message msg)は何をしているのかという話です。

Handler.sendMessage(Message msg)はHandler.sendMessageDelayed(Message msg, 0)を呼んでいるだけで、sendMessageDelayedではsendMessageAtTimeを呼んでいるだけです。
sendMessageAtTimeを呼び出すときにシステムクロックの現在時刻を引数に渡しています。

Handler.sendMessageAtTimeは、MessageQueueにMessageオブジェクトと時刻を渡してキューイングしています。

あとはAndroidの肝であるLooperがメッセージを実行します。
msg.target.dispatchMessage(msg);

msg.targetは先ほどのHandlerです。

Handler.dispatchMessage(msg)は、msg.callbackが定義されていればmsg.callback.run()を実行しますが、前述のobtainでMessageオブジェクトを作った場合はcallbackは定義されていないので、代わりにHandlerのインスタンスを作ったときに渡されたコールバックがあれば、そちらを実行します。

大抵の場合、コールバックをコンストラクタに渡さずに、Handlerを継承するかしてhandleMessageをオーバーライドするかと思います。
オーバーライドしないと、Handler.handleMessageは何もしません。
    public void handleMessage(Message msg) {
    }

ということで、結局、Looperのキューを介して自前のHandlerに処理が戻ってくるという流れでした。
処理をHandlerにまとめて書くのでなければ、handleMessageを書かずにMessageにコールバックを渡しておくというのでもいいですね。
ただし、Message.obtain(Handler h, Runnable callback)で渡しておく必要があります。
その場合、Runnableが渡せるのはHandler.obtainMessageにはないので、Message.obtainでMessageを作ることになります。

何というか、一目瞭然な図が欲しい。


Cordova(PhoneGap)でAndroid開発する際の勘所

PhoneGapの練習も兼ねてアプリひとつ作りました。

ヨルニンゲン (Jorningen) ※現在公開中のバージョンはJavaに書き直しました

https://play.google.com/store/apps/details?id=info.play_smart.android.Jorningen

device-2012-05-21-143445

生活時間帯が標準時間とズレている人が、自分の中では今何時なのかを把握するための時計アプリです。
例えば明け方6時に寝る人は、朝6時を夜12時のつもりとして登録しておくと、自分時間を表示しておけます。
分単位で、自分の中での今の時間を入力できるところが、世界時計とはちょっとコンセプトの違うところです。
アプリ名は夜人間ですが、昼人間も使えます。

で、AndroidアプリでPhoneGapを使ってみたのでハマりそうな部分やポイントを書いておきます。ちなみに使ったバージョンは1.7.0です。xui.jsとデータの保存にlawnchairを使いました。

Apache Cordova

PhoneGapのAndroid版は、名前がCordovaになったので検索時など注意しましょう。2011年のセットアップ記事とか見るとハマるかも。

ダウンロードはこちらから。ここからダウンロードできるのはgithub上にあるtarballです。
PhoneGap

思い切りが必要

結構思い切らないとPhoneGapを使うのは難しいです。Javaが必要な部分はJavaで書いて連携させればいいとか、思わないほうがいいです。すべてJavaScriptでやる気持ちでやりましょう。

なるべくJavaを使わない

双方の呼び出しは何とかなりますが、オブジェクトの共有はしにくいです。

OptionMenuは諦める

メニューボタンを押したときの挙動をJava側に書くことはできますが、メニューを出し分けたければJSの状態を常にJava側にフィードバックしておかないといけなくなります。

x$(document).on("menubutton", func);でJS側で完結させましょう。

代わりに設定ページに遷移させる

ページ遷移は、一枚のHTMLの中に複数のdivを作っておいて、切り替えて使うのが良さそうです。以下のサンプルが参考になると思います。

https://github.com/alunny/phonegap-start

上記サンプルのlawnchairはちょっとバージョン古いので、気をつけてください。

sqlite returned: error code = 14, msg = cannot open file at source line 25467

lawnchairを使っていて、このエラーが表示されたりしますが、動作に支障はないと気付くのに結構時間を使いました…

開発環境的な面では楽

Chromeとデベロッパーツールでほぼ開発できるので、実機への転送は画面の確認程度です。

ただし、chromeではdevicereadyイベントが起きないので、その辺の処理が必要なら開発時用と実機用で分ける必要はあります。

xui.jsは機能的にはかなり弱い

http://phonegap.com/tools

http://xuijs.com/

xui.jsがよく使われるようですが、機能はかなり少ないです。そのかわり、軽いです。どうも自分でextendして使うような感じです。見た目に関してはサポートしてくれないので、その辺の手間もかけたくない場合はSenchaTouchとかjQuery Mobileとかの方がいいと思います。

多言語化しにくい

https://github.com/phonegap/phonegap-plugins/tree/master/Android

Globalizationプラグインは地域とか言語を取得してくれるだけなので、文章を差し替えたりはできません。

assetsからindex.htmlを読み込む際にoncreateで切り分けるのも可能ですが、文章だけ変えたindex.htmlのコピーを作ることになるのでちょっと更新が手間になります。

でも多分values/string.xmlとか使ってしまうとブラウザでの開発がやりにくくなるので、JSのフレームワークの機能に頼るのがベターではないかと思います。

JSでnavigator.languageを取得しても英語にしかならない

さっき多言語化しにくいと書きましたが、言語を切り分けようとしてnavigator.languageを参照してもenしか返ってきません。

http://groups.google.com/group/phonegap/browse_thread/thread/f6b6ba2021ee7fb4/239e7ecb71619579

上記URLの方法でUserAgentから言語を取り出すようです。

ちなみにアプリの起動中では切り替わらないようで、アプリの強制終了→起動をしないと言語設定が有効になりません。

戻るボタンでアプリから抜けても、プロセスが終了しない

ひとつ上の言語切り替えとセットで少しハマりました。これは別にいいのかな。

Ripple使うと少し楽

Chromeの拡張のRipple Mobile Environment Emulatorをインストールすると、画面サイズを端末ごとに合わせてくれたり、位置情報のエミュレーションをしてくれたりします。

Chrome ウェブストア – Ripple Mobile Environment Emulator (Beta)

ローカルのファイルを読ませるためには、プラグインにローカルファイルへのアクセスの許可をする必要があります。

Chromeの拡張機能を管理→Ripple→アイコン左の三角をクリックして詳細を表示→ファイルのURLへのアクセスを許可するにチェック、でOKです。

でも画面サイズがうまく一致しなかったです。謎。

それと、スタンドアローン版もあるのですが、ローカルファイルの指定の仕方がわからず、結局使ってません。それと、バージョンが微妙に古いです。

機能とか使い方とか、別エントリでまとめようと思います。

ネットワークのパーミッションが必要

ネットワークを使わなくても、android.permission.ACCESS_NETWORK_STATEが必要です。これがないとSecurityExceptionが発生します。

他のパーミッションは全部消しても大丈夫です。

結論

結論としては、簡単なアプリならいいかな、という一般的な感想ですが、とりあえずJSのフレームワーク選定はものすごく重要です。自由にレイアウトできる軽量な画面遷移フレームワークがあったらいいかな、と。

あとはHTML+CSSを組むのにやたら時間がかかってしまったので、これを何かGUIのエディタでどうにかすべきだと思いました。Dreamweaverとかでもいいですが、もっとCSSの知識不要で作りたいところです。


AndroidのTimePickerDialogのタイトルを設定する

    class MyTimePickerDialog extends TimePickerDialog {

        public MyTimePickerDialog(Context context, OnTimeSetListener callBack,
                int hourOfDay, int minute, boolean is24HourView) {
            super(context, callBack, hourOfDay, minute, is24HourView);
        }

        @Override
        public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
            // do nothing
        }

    }

時刻を変更するたびにタイトルを上書きしてしまうので、継承してonTimingChangedを何もしないようにオーバーライドしてしまえばOKです。

また、インスタンスを作ったときにも自動的にタイトルを設定しているので、自分でタイトルを上書きするのも忘れずに。

            final MyTimePickerDialog timePickerDialog = new MyTimePickerDialog(
                    this, new TimePickerDialog.OnTimeSetListener() {
                        @Override
                        public void onTimeSet(TimePicker view, int hourOfDay,
                                int minute) {
                            // do something
                        }
                    }, 0, 0, true);
            // ここでタイトルを設定する
            timePickerDialog.setTitle(R.string.dialog_title);
            timePickerDialog.show();


getExternalFilesDirがnullを返す場合

API Level 8から使えるようになったContext.getExternalFilesDirは外部ストレージのパスに、パッケージ名を追加して、さらにディレクトリが存在しなければ作成もしておいてくれるという、今までEnvironment.getExternalStorageDirectoryを使って書いていた部分を短くできる便利なメソッドです。

で、このメソッドがnullを返す場合、大抵はパーミッション不足です。

AndroidManifest.xmlに外部ストレージへの書き込みを許可する行を追加しましょう。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

何が問題かって、このメソッドがパーミッションのExceptionを投げてくれずに、nullしか返さないことです。

ちなみにこのメソッドを使ってファイルをコピーするプログラムはこんな感じになりました。

    File dst = new File(mContext.getExternalFilesDir(null), "dst.txt");
    // dst = "/mnt/sdcard/Android/data/com.sample/files/dst.txt"

    FileChannel srcChannel = null;
    FileChannel destChannel = null;

    try {
        // ファイルのコピー
        if (dst.exists()) {
            dst.delete();
        }
        dst.createNewFile();
        FileInputStream in = mContext.openFileInput("src.txt");
        FileOutputStream out = new FileOutputStream(dst, false);

        srcChannel = in.getChannel();
        destChannel = out.getChannel();

        srcChannel.transferTo(0, srcChannel.size(), destChannel);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (srcChannel != null) {
            try {
                srcChannel.close();
            } catch (IOException e) {
            }
        }
        if (destChannel != null) {
            try {
                destChannel.close();
            } catch (IOException e) {
            }
        }
    }


Intent連携でのメール送信とFLAG_ACTIVITY_NEW_TASK

androidからIntentを使って外部Activityを起動するときに、contextがActivity contextじゃないと、AndroidRuntimeExceptionが発生します。

サービスや、独自のアプリケーションクラスからapplication contextを使って、メールクライアントを選択させてメール送信するためにstartActivityを実行したいという用事です。

E/AndroidRuntime(7525): android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

FLAG_ACTIVITY_NEW_TASKをsetFlagsしてやればいいだけなのですが、その辺から適当にコードをコピーしてくるとハマる場合があります。

問題のコードはこれ。

        Intent intent = new Intent(Intent.ACTION_SEND);
        String[] to = { "hoge@sample.com" };
        intent.putExtra(Intent.EXTRA_EMAIL, to);
        intent.putExtra(Intent.EXTRA_TEXT, "test");
        intent.putExtra(Intent.EXTRA_SUBJECT, "send mail test");
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setType("message/rfc822");

        context.startActivity(Intent.createChooser(intent, "Choose Email Client"));

一見問題なさそうなのですが、これは結局Intent.createChooserがIntentを返すので、そのIntentに対してフラグを指定しなければいけないということでした。

なので、以下のようにする必要があります。

        Intent intent1 = new Intent(Intent.ACTION_SEND);
        String[] to = { "hoge@sample.com" };
        intent1.putExtra(Intent.EXTRA_EMAIL, to);
        intent1.putExtra(Intent.EXTRA_TEXT, "test");
        intent1.putExtra(Intent.EXTRA_SUBJECT, "send mail test");
        intent1.setType("message/rfc822");

        Intent intent2 = Intent.createChooser(intent1, "Choose Email Client");
        intent2.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent2);

android – How can i call startActivity() to use ACTION_SEND from a different class – Stack Overflow