2011年10月27日木曜日

WebView#loadDataが文字化けする(Android3.0以降)

Android2.3以前であれば、METAタグにContent-Typeを指定することで解決した。
webView.loadData(
    "<html>"
      + "<head>"
      + "<meta http-quiv=\"Content-Type\" content=\"text/html charset=utf-8\" />"
      + "</head>"
      + "<body>"
      + htmlBodyData
      + "</body>"
      + "</html>",
    "text/html",
    "utf-8");

しかし、Android3.0以降では上記の対処でも文字化けした。
いろいろ調べた結果、WebView#loadDataWithBaseURLを使用することで、とりあえず文字化けを回避できた。
webView.loadDataWithBaseURL(
    null,
    "<html>"
      + "<head>"
      + "<meta http-quiv=\"Content-Type\" content=\"text/html charset=utf-8\" />"
      + "</head>"
      + "<body>"
      + htmlBodyData
      + "</body>"
      + "</html>",
    "text/html",
    "utf-8",
    null);

ソースを軽く眺めると、loadDataとloadDataWithBaseURLでは最初のデータの入れ方からして違っていたので、loadDataがメンテされてないんだろうな…。

2011年10月26日水曜日

RのID等の値が定数でなくなったためswitch文でエラー(ADTr14)

Non-constant Fields in Case Labels

どこかで聞いてたけど、変換の仕方忘れてたのでメモ。

switchステートメントにカーソル合わせて、WindowsはCtrl+1、MacはCmd+1を押す。
すると、ifステートメントに早変わり!

2011年10月3日月曜日

Android端末のAPN切り替え

Managing APN Settings on Google Android
基本的には上記リンク先でいける。

調べてて気になったことを以下に。
・端末(OSバージョン?)によって、ソース内コメントのテーブル定義より拡張されていることがある。GalaxySは'nwkname'というカラムがあるが、Xperia Arcにはなかった。
・またpreferapnによるAPN変更時は、ContentValuesのkeyを'apn_id'とする必要がある。(値は'_id'カラムのものを使用する)指定した条件が一意のものでないと、APNは切り替わらない。
SharedPrefernceにapn_idという使用中のAPN情報を持っているようだ。


com.android.providers.telephony.TelephonyProviderを読むと、もう少し細かい仕組みがわかった。(早くandroid.git.kernel.org復旧しないかな)

指定できるURIは5つある。
content://telephony/carriers/
=>URL_TELEPHONY
content://telephony/carriers/current
=>URL_CURRENT
content://telephony/carriers/#
=>URL_ID
content://telephony/carriers/restore
=>URL_RESTOREAPN
content://telephony/carriers/preferapn
=>URL_PREFERAPN

それぞれ、SQLクエリによって動作が異なる。
URL_TELEPHONY
=>条件通りのCRUDを行う。

URL_CURRENT
=>query()はcurrent列がNULLでないAPNを取得。insertは特殊で、current IS NOT NULLの行をNULL化してnumericが同一の行のcurrent列を1にする。delete/updateは特徴なし。

URL_ID
=>uriのgetLastPathSegmentなどを条件としてCRUDを行うようだけど、動作よくわからず。このUriに対してLastPath叩くと"carriers"が返ってくるし。便利な使い方でもあるんだろか。

URL_RESTOREAPN
=>delete()でのみ使用可能。preferapnの設定や追加したAPNを初期化する。

URL_PREFERAPN
=>query()は現在設定されているAPNを返す。insert/updateはvaluesで指定したapn_idを設定して、APNを切り替える。deleteはpreferredAPNを初期化する。

以下は、調べる時に使ったソースコード。検証用にログ出すだけのメソッド多め。APNクラスはsetter/getterが無駄に長いので省略。

package com.test.apn;

import java.util.LinkedList;
import java.util.List;

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;

import com.test.apn.entity.APN;

public class ApnManager {

    private static final String TAG = ApnManager.class.getName();
    
    private static final Uri APN_TABLE_URI = Uri.parse("content://telephony/carriers/");
    private static final Uri APN_PREFERRED_URI = Uri.parse("content://telephony/carriers/preferapn");
    private static final Uri APN_CURRENT_URI = Uri.parse("content://telephony/carriers/current");
    private static final Uri APN_ID_URI = Uri.parse("content://telephony/carriers/#");
    private static final Uri APN_RESTORE_URI = Uri.parse("content://telephony/carriers/restore");
    
    private Context mContext;

    public ApnManager(Context context) {
        mContext = context;
    }

    public List<APN> getApnList() {
        ContentResolver resolver = mContext.getContentResolver();
        Cursor c = resolver.query(APN_TABLE_URI, null, null, null, null);
        printAllData(c);
        
        c.moveToFirst();
        List<APN> apns = new LinkedList<APN>();
        while (! c.isAfterLast()) {
            apns.add(createAPNEntity(c));
            c.moveToNext();
        }
        c.close();
        
        return apns;
    }
    
    public APN getCurrentPreferredApn() {
        ContentResolver resolver = mContext.getContentResolver();
        Cursor c = resolver.query(APN_PREFERRED_URI, null, null, null, null);
        printAllData(c);
        
        if (! c.moveToFirst()) {
            return null;
        }
        
        APN apn = createAPNEntity(c);
        c.close();
        return apn;
    }
    
    public void restoreApn() {
        ContentResolver cr = mContext.getContentResolver();
        int restoreCount = cr.delete(APN_RESTORE_URI, null, null);
        Log.d(TAG, "restore = " + restoreCount);
    }
    
    public APN getApn(long id) {
        ContentResolver resolver = mContext.getContentResolver();
        Cursor c = resolver.query(APN_TABLE_URI, null, "_id="+id, null, null);
        if (! c.moveToFirst()) {
            return null;
        }

        APN apn = createAPNEntity(c);
        c.close();
        
        return apn;
    }
    
    public void setDefaultApn(long id) {
        ContentResolver cr = mContext.getContentResolver();
        ContentValues values = new ContentValues();
        values.put("apn_id", id);
        int updateCount = cr.update(APN_PREFERRED_URI, values, null, null);
        Log.d(TAG, "update = " + updateCount);
    }
    
    public void getCurrentApn() {
        ContentResolver resolver = mContext.getContentResolver();
        Cursor c = resolver.query(APN_CURRENT_URI, null, null, null, null);
        Log.d(TAG, "current => ");
        printAllData(c);
        
        c.moveToFirst();
        List<APN> apns = new LinkedList<APN>();
        while (! c.isAfterLast()) {
            apns.add(createAPNEntity(c));
            c.moveToNext();
        }
        c.close();
        
        return;
    }
    
    public void getApnId() {
        ContentResolver resolver = mContext.getContentResolver();
        Cursor c = resolver.query(APN_ID_URI, null, null, null, null);
        printAllData(c);
        
        c.moveToFirst();
        List<APN> apns = new LinkedList<APN>();
        while (! c.isAfterLast()) {
            apns.add(createAPNEntity(c));
            c.moveToNext();
        }
        c.close();
    }
    
    private APN createAPNEntity(Cursor c) {
        APN apn = new APN();
        
        apn.setId(c.getInt(c.getColumnIndex("_id")));
        apn.setName(c.getString(c.getColumnIndex("name")));
        apn.setNumeric(c.getString(c.getColumnIndex("numeric")));
        apn.setMcc(c.getString(c.getColumnIndex("mcc")));
        apn.setMnc(c.getString(c.getColumnIndex("mnc")));
        apn.setApn(c.getString(c.getColumnIndex("apn")));
        apn.setUser(c.getString(c.getColumnIndex("user")));
        apn.setServer(c.getString(c.getColumnIndex("server")));
        apn.setPassword(c.getString(c.getColumnIndex("password")));
        apn.setProxy(c.getString(c.getColumnIndex("proxy")));
        apn.setPort(c.getString(c.getColumnIndex("port")));
        apn.setMmsproxy(c.getString(c.getColumnIndex("mmsproxy")));
        apn.setMmsport(c.getString(c.getColumnIndex("mmsport")));
        apn.setMmsc(c.getString(c.getColumnIndex("mmsc")));
        apn.setType(c.getString(c.getColumnIndex("type")));
        apn.setCurrent(c.getInt(c.getColumnIndex("current")));
        
        return apn;
    }
    
    private void printAllData(Cursor c) {
        if (! c.moveToFirst()) {
            return;
        }
        
        StringBuilder columnLog = new StringBuilder();
        String[] columns = c.getColumnNames();
        for (String column : columns) {
            columnLog.append(column);
            columnLog.append(",");
        }
        Log.d(TAG, columnLog.toString());
        
        int colSize = c.getColumnCount();
        while (! c.isAfterLast()) {
            StringBuilder values = new StringBuilder();
            for (int i = 0; i < colSize; i++) {
                values.append(c.getString(i));
                values.append(",");
            }
            Log.d(TAG, values.toString());
            
            c.moveToNext();
        }
        
        return;
    }
}

2011年9月28日水曜日

workerで動作するApacheでphp-mysqlが動作しない

http://www.bruteberry.com/archives/2011/04/phpinfomysql.php

redmineインストール済みのCentOSにphpMyAdmin入れようとして、DBに接続できなくて結構ハマッたのでメモ。
とりあえずApacheの設定をデフォルトのpreforkに戻して解決。

2011年9月22日木曜日

In-app Billingの設定をCSVインポート/エクスポートできる機能がデベロッパーコンソールに追加

http://developer.android.com/guide/market/billing/billing_admin.html#billing-bulk-add


In-app Billingの設定をCSVインポート/エクスポートできる機能がデベロッパーコンソールに追加されてた。いつの間にか。
これは地味に嬉しい機能!
IDはコンテンツ管理してるリモートサーバでも識別材料とするから、リモートサーバを一次ソースとしてCSVに吐き出せば、デベコンで設定するときのtypoによる事故防止等につながる。

ID手打ちとかアナログ過ぎてアレだと思ってたので、少しは改善へ向かってる模様。
あとは、Google Checkoutみたいにアプリの詳細なダウンロード数だけを閲覧できるような仕組みがあるとよいのだけれど。デベコンのアカウントだけができることが多すぎる。

2011年7月14日木曜日

Androidマーケットに関する問題を知り、報告するためには

知識の断片 - In-app Billingで(softbankを除く?)キャリア決済ができない

この件では、以下の点がわからないことがこの問題の見通しが悪い理由となったように思う。
Googleへバグ報告する方法
今回Issueにいきなり書いていた。Dev Guideの示すバグ報告先読んでのことかもしれないけど、これってサンプルコードに関するBTSのような気がする。じゃあ正しい報告の仕方ってどうするんだろう。
Googleが抱える既知の問題を知る方法
問題あがってるようだけど、Googleへちゃんとリーチしてるのか?どのような対処を行う予定なのか?これらの状況が私からは見えなかったからADLで質問することになった。

これらの点については、以下のMLでGoogleの方が参考情報として道筋を立ててくれた。これは、Androidマーケットに関わる所だけだけど。
Android Market クライアントアプリのバージョンアップのお知らせ

Googleへ問題や質問を報告するメールフォーム
Contacting Support

既知の問題(Known Issue)の掲載先
Android Market for Developer Known Issues

注意すべきは、どれも言語をEnglishにしないと見えない点。日本語にすると、Known Issueに至っては、
現在のところ、記録されている問題はありません。
なんてのたまう始末。
翻訳済みでないなら、原文ページに飛ばしてほしい。それかMicorsoftのMSDNみたいに機械翻訳かけてその旨記載しておくとか。

問題かどうかの切り分けとして、最初にMLとかStackOverflowを使うのもよいかもしれない。

2011年6月13日月曜日

Themeを設定したDialogがActivityに重なって表示されない

Themeを指定したProgressDialogが、2.3以降の端末(バージョンアップ後のGalaxySとかXperiaArcとか)で表題のような状態になった。
ちょうど別のActivityに遷移してしまったような感じ。
ProgressDialog dlg = new ProgressDialog(this, R.style.ProgressDialog);


一応下記のようにThemeにはparentでTheme.Dialog.Alertを指定していたのに、Dialogたるべき属性を継承してくれてない。

styles.xml



しょうがないので、継承したかったAlertDialogのThemeから設定を引っ張ってきた。

styles.xml


ネタ元はココ

windowContentOverlayはあってもなくても見た目変化ない気がする。念のため。

2011年5月17日火曜日

In-app Billingで(softbankを除く?)キャリア決済ができない

これは、私個人ではまだ解決に至っていない問題。(2011/5/17時点)

アプリ内課金を実装してみて、いざ実機(Xperia Arc SO-01C)で確認してみると、クレジットカード払いは上手くいったが、「NTT Docomo利用料金と一緒に支払い」を選択すると、購入に失敗する。
spモードパスワードを入力するダイアログを閉じた後、何も音沙汰がない。

ログを確認してみると、spモードパスワード入力ダイアログである、
com.android.vending/.billing.CarrierPasswordActivity
を起動するインテントを飛ばした後に、REQUEST_PURCHASEに対してRESULT_USER_CANCELEDが返ってきている。
継続しなければならない課金認証処理中に、作り上予期してないアクティビティが起動することによって中断されてしまっているのだろうか。

ネットで調べてみると、同様の事象で苦戦している人がいた。
In-app Billing(アプリ内課金)+SPモード決済=動作不良?
私個人ではDocomo以外では未検証だったのだけど、これによればauでも同様に発生する問題みたい。softbankはパスワードを要求されないから大丈夫だったともある。


そもそも、Android2.1以前の端末ではアプリ内課金キャリア決済もまともに動かないようで。
せっかく普及してきたAndroidユーザーの多くが対象にならないのなら、しばらくは様子見がよいのかな。

---------------------
追記(2011/7/5)

Android Developer Lab Tokyoにおいて、AndroidマーケットのセッションでIn-app BillingおよびCarrier Billing(キャリア決済)について触れられていたので、もしかしてこの件知らずにこの機能オススメしてるんじゃなかろうかと心配になった。
そこでこの件について尋ねてみたところ、その場にいらっしゃったGoogleのエンジニアの方からも問題解決を加速するよう働きかけてくださりました。
Issue 23: Direct carrier billing doesn't function

これは既知の問題で、数週間以内に Android Market アプリケーションのアップデートに含まれて修正される予定です。­ いましばらくお待ちください。
とのこと。

----------------------
追記(2011/7/14)

問題修正されたAndroidマーケットアプリが近日リリースされるとのこと。
この問題はこれでおしまいかな。

アプリ内課金のキャリア課金に関して

Android Market クライアントアプリのバージョンアップのお知らせ

ADLで質問してからの流れは非常に早いんだけど、実際言わなかったらどうなっていたんだろう、なんて思った。ほんとはバックグラウンドで動いてただけで私が質問したことなんて何の関係もなかったのならいいんだけど…

2011年3月23日水曜日

Designing for PerformanceからAvoid Enumsが消えてた

In-app Billingのテスト実装をするためサンプルソースを読んでいたら、
ガッツリenum型が使われていた。

以前パフォーマンスの問題で単なるintと同じ扱いするだけならなるべく使うなって、
Designing for Performanceに記述があったのに、と思って改めて調べてみると、
記述が削除されてた。


推測される理由?

static final intはガラケーを彷彿させて嫌だったので、
パフォーマンスに影響ない限りは使うべきところには今後enumを使っていきたい。


しかもこのサンプル、int型のレスポンスコードをordinal()で取ってるんだよなあ。
Effective Javaの項目31にこういう使い方は微妙って書いてあったような気が。
私が誤読してるのかな?
まあコピペしちゃいけない処理だしいいのか。

2011年3月22日火曜日

AndroidでXMLを読み込む(ユーティリティを使ったSAXパーサ)

AndroidでのXMLの読み込み方法は、

  1. SAX
  2. DOM
  3. XMLプルパーサ
がある。

今回は、1.SAXを使用した。

SAXでのXMLの解析は、
処理の大部分はハンドラに記述するわけだけど、
対象のXMLがネストの深い構造だったりすると、途端にソースの可読性が下がる。
(私がまだ綺麗な書き方を知らないだけかもしれない…)

AndroidでSAXを扱う場合、
android.saxパッケージにユーティリティクラスが用意されている。これホントに便利。
プルパーサに書き換えようと思っていたけど、とりあえずはこっちでいいかも。

※参考
AndroidでXMLを扱う
http://www.ibm.com/developerworks/jp/xml/library/x-android/



要素の属性を取得する場合は、
StartElementListenerTextlementListenerを使用する必要がある。
<xml>
<root>
<tag id="A001">data</tag>
</root>
みたいなものには、

public List<Data> parse() {
    final Data currentData = new Data();
    List<Data> list = new ArrayList<Data>();

    RootElement root = new RootElement("root");
    Element tag = root.getChild("tag");
    tag.setTextElementListener( new TextElementListener() {
        @Override
        public void start(Attributes attributes) {
            // id属性を取得
            String id = attributes.getValue("id");
            currentData.setId(id);
        }

        @Override
        public void end(String body) {
            // tag要素を取得
            currentData.setData(body);
            list.add(new Data(currentData));
        }
    });
    try {
        Xml.parse(getInputStream(), Xml.Encoding.UTF_8, root.getContentHandler());
        return list;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

最初はよく理解せず、
ElementListenerとEndTextElementListenerを組み合わせていたけど、
リスナの呼び出し順が、
  1. ElementListener$start
  2. ElementListener$end
  3. EndTextElementListener$end
で、当然意図していたのと違っていた。(2と3が逆だと思った)

Simple Typeの要素にはTextElementListenerを、
Complex Typeの要素にはElementListenerを設定してあげるのが正しいやり方。

2011年3月11日金曜日

ViewAnimator.bringChildToFront()のバグ

というのか仕様というのか。

ViewAnimatorでアニメーションでどのViewが上に描画されるかは、ViewGroupで管理されている子Viewの配列順のようだ。
そこで、確実に上に描画されるようにViewAnimator.bringChildToFront()を使っていると、どうも期待する描画にならなくなってくる。

そこでViewAnimatorのソースを見てみると、どうも表示している子Viewの配列番号を指すべきprivateフィールドであるmWhichChildが、ViewAnimator.bringChildToFront()の呼び出しによりズレていっていた。
ズレが発生した後に、mWhichChildを参照している
ViewAnimator.showNext()
ViewAnimator.showPrevious()
ViewAnimator.getDisplayedChild()
ViewAnimator.getCurrentView()
なんかを使うとアプリの挙動が意図しないモノになる。

対処方法としては、

  1. 子Viewの並び替えが必要ないように最初から追加しておく
  2. showNext(),showPrevious()を使用せず、setDisplayedChild()で表示する。
  3. カスタムViewAnimatorクラスを作成する。
てところだろうか。

1は、ある程度の固定数の子Viewしか持たないことがわかっていれば何も問題ない。
今回は可変の数の子Viewが必要となるため、ListViewのようにViewを使い回し、必要なViewにだけ描画を行うようにしたかったので却下。
2は、更にViewAnimatorを使用する側のクラスでmWhichChildのような子View管理データを持つ必要がある。面倒。
今回は3で解決した。流用が効くし。ViewAnimatorからソース引っ張ってきて、以下のメソッドを追加。

@Override
public void bringChildToFront(View child) {
    int index  = indexOfChild(child);
    if (index < mWhichChild) {
        mWhichChild--;
    } else if (index == mWhichChild) {
        mWhichChild = getChildCount() - 1;
    }
    super.bringChildToFront(child);
}

ViewAnimatorってViewFlipper/ViewSwitcher/ImageSwitcher/TextSwitcherとかの親クラスになってて、結構この辺で問題起きそうな気がする。

addViewとかで現在表示しているViewの裏側になるindex指定してもmWhichChildはズレる可能性ありそう。
でも今回の用途には使用しないので詳しくはみてない。

2011年3月10日木曜日

開発の記録として

Macbook airを買ったので、プライベートでの開発を頑張る。
家は椅子が折りたたみ式の木製の椅子で落ち着かず、誘惑も多々あるので、
外で憧れのカフェプログラミングてのをやってみたい。
当面はAndroid開発をメインに。

とりあえずeclipseをインストール。