‘java’ タグのついている投稿

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

MediaPlayerで通知音を鳴らす

AndroidのMediaPlayerで通知音を鳴らすときに、音量設定を反映させます。

MediaPlayer.createメソッドでインスタンスを作成した状態だと、「メディア音量」というものが反映されるので、setAudioStreamTypeメソッドで再生するオーディオのタイプを指定してやります。これでシステムの設定画面で指定した音量が反映されます。

が、setAudioStreamTypeメソッドをMediaPlayer.createで作成したインスタンスに対して実行すると、
error (-38, 0)
とか
prepareAsync called in state 8
とか
setAudioStream called in state 8
とかエラーが起こります。

これはMediaPlayer.createメソッドが、内部的にすでにprepare(準備)メソッドを呼んでいるためで、ストリームタイプの変更は、準備の前に実行しておく必要があります。

コードはこんな感じになりました。

    public static void playSound(Context context, String url) {

        if (url != null) {
            Uri uri = Uri.parse(url);
            // MediaPlayer.createはprepareを実行してしまうのでnew MediaPlayer()を使う
            // MediaPlayer mp = MediaPlayer.create(context, uri);
            MediaPlayer mp = new MediaPlayer(); 
            try {
                mp.setDataSource(context, uri);
                // setAudioStreamTypeはprepare前に実行する必要がある
                mp.setAudioStreamType(AudioManager.STREAM_NOTIFICATION); 
                mp.setLooping(false); 
                // prepareの前後で使えるメソッドが異なる
                mp.prepare(); 
                mp.seekTo(0);
                mp.start(); 
            } catch (IllegalStateException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

ちなみにマナーモードのときは、何も状態をチェックしなくてもストリームタイプを通知音にしておけば、音は鳴りません。MediaPlayer.createで作ると、メディアになってしまうので、マナーモードが反映されません。

それと、一部機種(音が長いと?)ではsetDataSourceが失敗する場合があるそうです。詳しくは以下。

Ringtoneを再生する時にFileDescriptorを使いたい – 日本Androidの会 | Google グループ


Androidのキーストアから証明書をエクスポートする

業務でAndroid開発をしていて、都合上、後から鍵ペアを人に渡さなければいけない場合があります。

新しいapkのたびにキーストアファイルを作っているのなら、別にそれごと渡してしまえばいいのですが、自分用のキーストアに作ってしまった場合、キーストアごと渡すわけにはいきません。

そこでキーストアファイルから証明書をエクスポートして相手に渡すことを考えますが、apkに署名するには、証明書の他に秘密鍵が必要です。

先に結論だけ書いておきます。keytoolコマンドのimportkeystoreを使います。

> keytool -importkeystore -srckeystore .\mykeystore.jks -srcstorepass <<自分のキーストアのパスワード>> -destkeystore app1.jks -deststorepass <<新規作成するキーストアのパスワード>> -deststoretype jks -srcalias app1
<app1> の鍵パスワードを入力してください。

これでapp1.jksというキーストアファイルが新規作成され、自分のキーストアの中から特定のエイリアスの鍵ペアだけをコピーできます。

以下は説明です。

例えば自分用のキーストアの中身がこうなっていたとします。

> keytool -list -v -keystore .\mykeystore.jks -storepass <<キーストアのパスワード>>

キーストアのタイプ: JKS
キーストアのプロバイダ: SUN

キーストアには 2 エントリが含まれます。

別名: app1
作成日: 2012/03/08
エントリタイプ: PrivateKeyEntry
証明連鎖の長さ: 1
証明書[1]:
所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3


*******************************************
*******************************************


別名: app2
作成日: 2012/03/08
エントリタイプ: PrivateKeyEntry
証明連鎖の長さ: 1
証明書[1]:
所有者: C=JP
発行者: C=JP
シリアル番号: 4f57f300
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3

証明書のエクスポートとインポートは以下のコマンドでできますが、秘密鍵がエクスポートできません。

> keytool -exportcert -keystore .\mykeystore.jks -storepass <<キーストアのパスワード>> -alias app1 -file .\app1.cer
証明書がファイル <.\app1.cer> に保存されました。

> keytool -import -keystore app1.jks -storepass <<新規作成するキーストアのパスワード>> -file .\app1.cer
所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3
この証明書を信頼しますか? [no]:  yes
証明書がキーストアに追加されました。

この状態のキーストアはこうなっています。

> keytool -v -list -keystore .\app1.jks -storepass <<キーストアのパスワード>>

キーストアのタイプ: JKS
キーストアのプロバイダ: SUN

キーストアには 1 エントリが含まれます。

別名: app1
作成日: 2012/03/31
エントリのタイプ: trustedCertEntry

所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3


*******************************************
*******************************************

エントリのタイプがtrustedCertEntryになっています。このエントリでapkを署名しようとするとjarsignerがエラーを出します。

jarsigner: 次の証明連鎖が見つかりません: app1。  app1 は、非公開鍵および対応する公開鍵証明連鎖を含む有効な KeyStore 鍵エントリを参照する必要があります。
(英語の場合)
jarsigner: Certificate chain not found for: app1.  app1 must reference a valid KeyStore key entry containing a private key and corresponding public key certificate chain.

Eclipseの場合は、何も言われず、Finishボタンが押せません。

ここで最初の結論のところで書いたimportkeystoreを使って、キーストアからキーストアへのインポートをすることで、秘密鍵を持ったまま新しいキーストアに証明書をコピーできます。コピー後のキーストアファイルはこうなります。

> keytool -list -v -keystore .\app1.jks -storepass <<キーストアのパスワード>>

キーストアのタイプ: JKS
キーストアのプロバイダ: SUN

キーストアには 1 エントリが含まれます。

別名: app1
作成日: 2012/04/01
エントリタイプ: PrivateKeyEntry
証明連鎖の長さ: 1
証明書[1]:
所有者: C=JP
発行者: C=JP
シリアル番号: 4f582f00
有効期間の開始日: Thu Mar 08 00:00:00 JST 2012 終了日: Wed Jul 10 00:00:00 JST 3011
証明書のフィンガープリント:
         MD5:  00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3

エントリタイプがPrivateKeyEntryになっていれば、通常通りapkの署名に使えます。

あと、importkeystoreが使えるのはJDK6以降のようなので、バージョンは確認してください。


Conversion to Dalvik format failed with error 1

[2012-03-05 08:32:06 - Test] Dx 
trouble processing "javax/net/SocketFactory.class":

Ill-advised or mistaken usage of a core class (java.* or javax.*)
when not building a core library.

This is often due to inadvertently including a core library file
in your application's project, when using an IDE (such as
Eclipse). If you are sure you're not intentionally defining a
core class, then this is the most likely explanation of what's
going on.

However, you might actually be trying to define a class in a core
namespace, the source of which you may have taken, for example,
from a non-Android virtual machine project. This will most
assuredly not work. At a minimum, it jeopardizes the
compatibility of your app with future versions of the platform.
It is also often of questionable legality.

If you really intend to build a core library -- which is only
appropriate as part of creating a full virtual machine
distribution, as opposed to compiling an application -- then use
the "--core-library" option to suppress this error message.

If you go ahead and use "--core-library" but are in fact
building an application, then be forewarned that your application
will still fail to build or run, at some point. Please be
prepared for angry customers who find, for example, that your
application ceases to function once they upgrade their operating
system. You will be to blame for this problem.

If you are legitimately using some code that happens to be in a
core package, then the easiest safe alternative you have is to
repackage that code. That is, move the classes in question into
your own package namespace. This means that they will never be in
conflict with core system classes. JarJar is a tool that may help
you in this endeavor. If you find that you cannot do this, then
that is an indication that the path you are on will ultimately
lead to pain, suffering, grief, and lamentation.

[2012-03-05 08:32:06 - Test] Dx 1 error; aborting
[2012-03-05 08:32:06 - Test] Conversion to Dalvik format failed with error 1

Package Explorerからプロジェクトを右クリックして、Properties選択。

Java Build Path→Librariesを開いて、表示されているライブラリを全部Remove。

OKでウィンドウを閉じて、再度Package Explorerを右クリックして、Android ToolsからFix Project Propertiesを選択。

解決。


SimpleDateFormatがメモリを食い尽くす件

java.text.SimpleDateFormatのインスタンスを毎回作ってRFC2822の日付文字列からDate型に変換していたら、GCされずにメモリを食いつぶしてしまったので対策。

	public static Date convertRfc2822toDate(String from) {
		String pattern = "EEE, dd MMM yyyy HH:mm:ss Z";
		SimpleDateFormat format = new SimpleDateFormat(pattern);
		Date date = null;
		synchronized (format) {
			try {
				date = (Date) format.parse(from);
			} catch (ParseException e) {
				date = new Date();
			}
		}
		format = null;
		return date;
	}

上は元のコード。下は修正版。

	private static SimpleDateFormat format = null;

	public static Date convertRfc2822toDate(String from) {
		String pattern = "EEE, dd MMM yyyy HH:mm:ss Z";

		Date date = null;
		if (format == null) {
			format = new SimpleDateFormat();
		}
		synchronized (format) {
			format.applyPattern(pattern);
			try {
				date = format.parse(from);
			} catch (ParseException e) {
				date = new Date();
			}
		}

		return date;
	}

完全なスレッドセーフにはなっていないけど、メモリリークは解消できたので良し。


AndroidとTwitter4Jで公式画像アップロードAPIを使う

Twitter4Jが2.2.5になって、Twitter APIのstatuses/update_with_mediaが使えるようになったというので、さっそく実験してみました。

Twitter4J 2.2.5 released – Twitter4J | Google グループ

とりあえずダウンロードして解凍します。twitter4j-android-2.2.5.zipの方です。

http://twitter4j.org/en/index.html#download

Eclipseで適当にプロジェクト作ります。プロジェクト作ったらプロジェクトのプロパティを開いて、Java Build Pathの設定画面を表示します。

image

Librariesタブを開いて、Add External JARsをクリックします。

image

先ほど展開したフォルダから、libの下にあるtwitter4j-core-android-2.2.5.jarとtwitter4j-media-support-android-2.2.5.jarを選択してJARを取り込みます。

image

あとはこんな感じで実験。ギャラリーの画像を選択してメッセージつけてポストします。

UpdateWithMediaActivity.java

package info.loadlimits.android.updatewithmedia;

import java.io.File;

import twitter4j.TwitterException;
import twitter4j.conf.Configuration;
import twitter4j.conf.ConfigurationBuilder;
import twitter4j.media.ImageUpload;
import twitter4j.media.ImageUploadFactory;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class UpdateWithMediaActivity extends Activity implements
		OnClickListener {
	private final static int REQUEST_PICK = 1;
	private final static String CONSUMER_KEY = "(書き換えてください)";
	private final static String CONSUMER_SECRET = "(書き換えてください)";
	private final static String ACCESS_TOKEN = "(書き換えてください)";
	private final static String ACCESS_TOKEN_SECRET = "(書き換えてください)";

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Button buttonPost = (Button) findViewById(R.id.buttonPost);
		buttonPost.setOnClickListener(this);
	}

	@Override
	public void onClick(View v) {
		Intent intent = new Intent(Intent.ACTION_PICK);
		intent.setType("image/*");
		startActivityForResult(intent, REQUEST_PICK);
	}

	@Override
	protected void onActivityResult(int requestCode, int resultCode, Intent data) {
		super.onActivityResult(requestCode, resultCode, data);
		if (requestCode == REQUEST_PICK && resultCode == RESULT_OK) {
			Uri uri = data.getData();
			ContentResolver cr = getContentResolver();
			String[] columns = { MediaStore.Images.Media.DATA };
			Cursor c = cr.query(uri, columns, null, null, null);

			c.moveToFirst();
			File path = new File(c.getString(0));
			if (!path.exists())
				return;

			ConfigurationBuilder builder = new ConfigurationBuilder();
			builder.setOAuthConsumerKey(CONSUMER_KEY);
			builder.setOAuthConsumerSecret(CONSUMER_SECRET);
			builder.setOAuthAccessToken(ACCESS_TOKEN);
			builder.setOAuthAccessTokenSecret(ACCESS_TOKEN_SECRET);
			// ここでMediaProviderをTWITTERにする
			builder.setMediaProvider("TWITTER");

			Configuration conf = builder.build();

			ImageUpload imageUpload = new ImageUploadFactory(conf)
					.getInstance();

			EditText textTweet = (EditText) findViewById(R.id.textTweet);
			String tweet = textTweet.getText().toString();

			try {
				imageUpload.upload(path, tweet);
			} catch (TwitterException e) {
				e.printStackTrace();
			}
		}
	}
}

res/layout/main.xml

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

    <EditText
        android:id="@+id/textTweet"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <requestFocus />
    </EditText>

    <Button
        android:id="@+id/buttonPost"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/btn_upload" />

</LinearLayout>

manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.loadlimits.android.updatewithmedia"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="9" />

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

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:label="@string/app_name"
            android:name=".UpdateWithMediaActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

ポイントはsetMediaProviderでTWITTERを選択することだけです。

image

image

こんな感じでpic.twitter.comにアップロードできました。

OAuth認証ページとか組み込みたければ、こちらを参考に。

[Android] Android+Twitter4JでOAuthするためのソースコード – adakoda