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

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使うのが一番楽かと。


Symfony2のdoctrine2をenumに対応させる

doctrine2がenumに対応していないので、既存のデータベースからEntityを作ろうとした時にエラーが出ることがあります。

$ php app/console doctrine:mapping:convert yml ./src/Test/HogeBundle/Resources/config/doctrine/metadata/orm --from-database --force

  [Doctrine\DBAL\DBALException]
  Unknown database type enum requested, Doctrine\DBAL\Platforms\MySqlPlatform may not support it.

その場合は、doctrine2のドキュメントによると、enumをstringに関連づければ良いようです。

Mysql Enums — Doctrine 2 ORM 2.1 documentation

BundleのbootでEntityManagerを取得して、マッピングを行います。

<?php

namespace Test\HogeBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class TestHogeBundle extends Bundle
{
    public function boot() {
        $em = $this->container->get('doctrine.orm.default_entity_manager');
        $conn = $em->getConnection();
        $conn->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
    }
}

他の方法として、Enumタイプを作成して関連付けることもできるようです。

※追記

app/config/config.phpに書く方法が紹介されていました。

Doctrine の DBAL レイヤーの使用方法 | Symfony2日本語ドキュメント

$container->loadFromExtension('doctrine', array(
    'dbal' => array(
        'connections' => array(
            'default' => array(
                'mapping_types' => array(
                    'enum'  => 'string',
                ),
            ),
        ),
    ),
));

試していませんが、これでもいいらしいです。


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を作ることになります。

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


WinSerでnode.jsをWindowsのサービスにする

そんな用途がある人は相当少ないとは思うのですが、仕事柄、ニッチな要求ばかりがやってきます。

さて、node.js用に書かれたプログラムを、Windowsでサービス登録してバックグラウンドで自動実行する方法です。

WinSer: node.js applications as windows services

npmコマンドでインストールできます。

npm install winser

実際のプログラムをサービス化するにあたって、package.jsonを作成しておく必要があります。package.jsonのnameがそのままサービス名として登録されます。他の項目はどうでもいいです。

コマンドプロンプトを管理者権限で実行します。

C:\> chcp 437

日本語環境の場合、chcpコマンドでコマンドプロンプトを英語モードにしないと、
You must run this tool as an administrator
というエラーが出ます。

そして、自分の作ったプログラムのディレクトリに移動して、
C:\path\to\node_modules\.bat\winser.cmd -i
でサービスに登録されます。

cmdファイル経由ではなく、
node.exe C:\path\to\winser\winser -i
でもOKです。

ちなみに、サービス自体はnssmが登録します。

NSSM – the Non-Sucking Service Manager

ファイアウォールを通す場合は、nssmを指定します。


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


ISW11FをADBドライバで認識させる方法

Eclipseで開発をするときに、Arrows Z(ISW11F)がPCから認識しなくて困っていました。

PCからはFujitsu Mass storage USB Deviceと認識されてしまう場合の対処法です。

まずはドライバダウンロードします。
http://spf.fmworld.net/oss/usb_driver/isw11f/index.html

解凍したらReadme.txtは必ず読んでください。

インストール前に確認するべき項目は次の通り。

  • C:\Users\(ユーザー名)\.android\adb_usb.iniがあるか
  • 上記adb_usb.iniに「0x04C5」という行があるか(なければ行追加)
  • 端末がUSBデバッグになっているか

それとPCでデバイスマネージャーを起動しておきましょう。

これでUSBで端末を接続して、問題なくFujitsu HSUSB Deviceという項目が出現すればいいのですが、出現せず、下の画像のようになる場合があります。

WS000015

基本的には、すでに誤認識されてしまっているデバイスを削除してやればいいのですが、Fujitsu Mass storage USB Deviceの2行を削除しても、再接続すると元に戻ってしまいました。

なので、このデバイスの親ごと消す必要があります。作業は端末を接続したままで行います。

「ユニバーサル シリアル バス コントローラー」の下の「USB 大容量記憶装置」を右クリックして、削除します。ただし、複数デバイスがある場合は以下のように、デバイスマネージャーの表示を、「デバイス(接続別)」にしてやると、目的のデバイスがグループ化されるので、間違えずに消せるようになります。

WS000011

消したあとは、一度端末を外して、再度USBで接続すると、自動的にドライバのインストールが始まります。

WS000013

こうなれば成功です。ドライバーが見つかりませんと表示されますが、デバイス自体は認識されています。

WS000014

あとは、このFujitsu HSUSB Deviceを選択して、ドライバの更新をして、先ほどダウンロードして解凍したドライバファイルを選択すればAndroid Composite ADB Interfaceとして認識されます。

WS000016

ちなみにMass storage USB Deviceを直接「ドライバーの更新」すれば良いという情報があったのですが、試そうとしたら64bitドライバがない旨のメッセージが出て、進めませんでした。

今回はこちらのサイトが参考になりました。

AndroidがUSBデバッグで接続できない場合(USBストレージとしてしか認識されない場合) | コラビットの中の人


Windows版PHP 5.4.0でAPCを使う

まずはAPCをダウンロードします。

downloads.php.net/pierre/

php_apc-3.1.10-5.4-vc9-x86.zip (2012-04-11 23:46 -0700)
をダウンロードしました。

展開すると、ntsとtsのフォルダが作成されます。使っているPHPがNon Thread Safe版かThread Safe版かによって使う方を決めてください。

それぞれのフォルダの中に、php_apc.dllがあるので、phpがインストールされているフォルダのextフォルダにコピーします。

その後、php.iniを開いて
extension=php_apc.dll
を追加し、Apacheを再起動すればOKです。

ちなみに、dllのバージョンを見ると、5.4.1RC1-devと書かれていました。

phpinfo()ではこのようになります。特に問題なく使えているようです。

WS000000