@blog.justoneplanet.info

日々勉強

Android用にmozcをUbuntuでビルドする

Google日本語入力Android版がオープンソースになったという事で早速ビルドしてみる。

■準備

EC2でUbuntu 12.04を使用。

sudo apt-get update
sudo apt-get install g++ python subversion ibus make libibus-1.0-dev libzinnia-dev unzip ant qt4-dev-tools libqt4-core libqtgui4 git

■JDK

scp -i hogehoge.pem -r ~/Downloads/jdk-6u41-linux-x64.bin user@host:/home/user/jdk1.6.0_41
chmod 0700 jdk-6u41-linux-x64.bin
./jdk-6u41-linux-x64.bin
PATH=$PATH:$HOME/jdk1.6.0_41/bin
JAVA_HOME=$HOME/jdk1.6.0_41
export JAVA_HOME

■SDK

cd ~
wget http://dl.google.com/android/android-sdk_r21.1-linux.tgz
tar zxvf android-sdk_r21.1-linux.tgz
export PATH=/home/ubuntu/android-sdk-linux/tools:"$PATH"

cd ~/android-sdk-linux/platforms/
wget http://dl.google.com/android/repository/android-17_r01.zip
unzip android-17_r01.zip
mv android-4.2 android-17

wget http://dl.google.com/android/repository/android-2.2_r03-linux.zip
unzip android-2.2_r03-linux.zip
mv android-2.2_r03-linux android-8

■NDK

cd ~
wget http://dl.google.com/android/ndk/android-ndk-r8e-linux-x86_64.tar.bz2
tar -jvxf android-ndk-r8e-linux-x86_64.tar.bz2
export PATH=/home/ubuntu/android-ndk-r8e:"$PATH"

■mozc

cd ~/
svn co http://src.chromium.org/svn/trunk/tools/depot_tools
export PATH="$PATH":`pwd`/depot_tools

mkdir -p ~/src/mozc
cd ~/src/mozc
gclient config http://mozc.googlecode.com/svn/trunk/src
gclient sync

ちなみにリビジョンを指定するには以下のようにする。

gclient sync --revision r178

mac

macでgclient syncを実行すると以下のようなエラーが出て完了できない。

Server certificate verification failed: issuer is not trusted (https://zinnia.svn.sourceforge.net)

以下のコマンドで事前に取り込んでおく。

svn ls https://src.chromium.org
svn ls https://zinnia.svn.sourceforge.net
cd ~/src/mozc/src
./build_mozc.py gyp --target_platform=Android --android_sdk_home=/home/ubuntu/android-sdk-linux
./build_mozc.py build_tools -c Release
android update project -s -p android -t android-8
python build_mozc.py build android/android.gyp:apk -c Release_Android

x86

./build_mozc.py gyp --target_platform=Android --android_sdk_home=/home/ubuntu/android-sdk-linux --android_arch_abi=x86

ちなみにcleanは以下のコマンドで行う。

./build_mozc.py clean

リリースビルドしようとするとPlatform toolsが無いと言われて怒られる。

BUILD FAILED
/home/ubuntu/src/mozc/src/android/build.xml:86: The following error occurred while executing this line:
/home/ubuntu/android-sdk-linux/tools/ant/build.xml:401: SDK Platform Tools component is missing. Please install it with the SDK Manager (tools/android)

Ubuntuマシンがローカルに欲しくなる。この状態で既に共有ライブラリなどは生成されているのでローカルにダウンロードしeclipseでビルドできる。

参考

Factory Imagesを使ってAndroidのバージョンを上げる

Factory Images for Nexus Devicesからダウンロードする。

wget https://dl.google.com/dl/android/aosp/hoge-fuga-factory-piyo.tgz
tar xvzf hoge-fuga-factory-piyo.tgz
cd hoge-fuga/
adb reboot bootloader
fastboot oem unlock
fastboot flash bootloader bootloader-xxxx-yyy.img
fastboot reboot-bootloader
fastboot flash radio radio-xxxx-zzz.img
fastboot reboot-bootloader
fastboot update image-hoge-fuga.zip

書いてなかったので書いておく。

Androidのソースコードを取得してビルドする

■準備

必要なパッケージをインストールする。その他は適宜。

sudo yum install make git bison libxslt flex gperf gcc-c++ glibc-devel zlib-devel

以下はUbuntu12.0.4の場合である。

sudo apt-get install git gnupg flex bison gperf build-essential zip curl libc6-dev libncurses5-dev:i386 x11proto-core-dev libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-glx:i386 libgl1-mesa-dev g++-multilib mingw32 tofrodos python-markdown libxml2-utils xsltproc zlib1g-dev:i386

OpenJDKが邪魔なので一旦消す。

sudo yum remove java-1.6.0-openjdk 

Ubuntuは以下のコマンドを実行する。

sudo apt-get purge openjdk-\* icedtea-\* icedtea6-\*

OpenJDKでビルドしようとすると以下のようにエラーがでる。

Checking build tools versions...
************************************************************
You are attempting to build with an unsupported JDK.
 
You use OpenJDK but only Sun/Oracle JDK is supported.
Please follow the machine setup instructions at
    https://source.android.com/source/download.html
************************************************************

oracleのサイトに行って、JDK6をダウンロードしてインストールする。かなり省略するが、以下の様な感じにPATHを通せば良い。

PATH=$PATH:$HOME/jdk1.6.0_39/bin
JAVA_HOME=$HOME/jdk1.6.0_39
export JAVA_HOME

oracleアカウントが必要になってかなりイライラさせられるが、ダウンロードの準備をする。

mkdir ~/bin
curl http://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo
PATH=~/bin:$PATH
mkdir android
cd android
repo init -u https://android.googlesource.com/platform/manifest

以下のコマンドでbranchをリストアップし切り替える。

ls -1 .repo/manifests.git/logs/refs/remotes/origin/
repo init -u https://android.googlesource.com/platform/manifest -b android-4.0.1_r1

■ダウンロード

以下のコマンドでダウンロードする。恐らく1時間以上かかる。

repo sync

■ビルド

source build/envsetup.sh

largeでも4時間以上かかるらしいのでそれよりも大きいインスタンスを選択する事を勧める。

make

考えてみたらlargeだと4コアなので以下のコマンドでビルドする。

make -j4

ちなみにOpenWnnをビルドしたかったので以下のように実行した。smallインスタンスでも1時間以上かかるのでlarge以上にするのが良い。

make OpenWnn

共有ライブラリは以下のパスに生成される。

out/target/product/generic/obj/lib/libwnndict.so
out/target/product/generic/obj/lib/libWnnJpnDic.so
out/target/product/generic/obj/lib/libWnnEngDic.so

OpenWnnのソースの取得

リモートのソースをそのまま持ってきても問題ないのだが、OpenWnnのソースの取得だけだったら、以下のコマンドですぐに終わる。

git clone https://android.googlesource.com/platform/packages/inputmethods/OpenWnn.git

ちなみに中国語は以下のディレクトリにある。

git clone https://android.googlesource.com/platform/packages/inputmethods/PinyinIME.git

その他の言語はLatin語としてまとまっていて以下のコマンドを実行する。

git clone https://android.googlesource.com/platform/packages/inputmethods/LatinIME.git

必要ならば適切なブランチに切り替える。

git checkout -b ics-mr1-release origin/ics-mr1-release

以下のコマンドを実行してリモートの共有ライブラリをローカルに配置する。

cd OpenWnn/libs/
mkdir armeabi
cd armeabi
scp -i your_key.pem user@host:/home/user/android/out/target/product/generic/obj/lib/libwnndict.so ./
scp -i your_key.pem user@host:/home/user/android/out/target/product/generic/obj/lib/libWnnJpnDic.so ./
scp -i your_key.pem user@host:/home/user/android/out/target/product/generic/obj/lib/libWnnEngDic.so ./

EclipseのProject ExplorerでImport > Existing Android ProjectでImportする。この時、Copy projects into work spaceにチェックを入れる必要がある。

参考

TrivialDriveのコードを読んでみる

Implementing In-app Billingがv3になったのでサンプルコードを適当に読んでみた。

■購入処理

MainActivityのレイアウトファイルのボタンは以下のようにImageViewで記述してある。

<ImageView
    android:id="@+id/infinite_gas_button"
    android:src="@drawable/infinite_gas"
    android:onClick="onInfiniteGasButtonClicked"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

onInfiniteGasButtonClickedメソッド

まず以下の処理でデバイスが購読に対応してるか確認。

if (!mHelper.subscriptionsSupported()) {
    complain("Subscriptions not supported on your device yet. Sorry!");
    return;
}
IabHelper.java

サービスを操作したりしているクラスで以下の部分で、マーケットアプリのバージョンを確認してbooleanをセットしている。

mServiceConn = new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
        logDebug("Billing service disconnected.");
        mService = null;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        logDebug("Billing service connected.");
        mService = IInAppBillingService.Stub.asInterface(service);
        String packageName = mContext.getPackageName();
        try {
            logDebug("Checking for in-app billing 3 support.");
            
            // check for in-app billing v3 support
            int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
            if (response != BILLING_RESPONSE_RESULT_OK) {
                if (listener != null) listener.onIabSetupFinished(new IabResult(response,
                        "Error checking for billing v3 support."));
                
                // if in-app purchases aren't supported, neither are subscriptions.
                mSubscriptionsSupported = false;
                return;
            }
            logDebug("In-app billing version 3 supported for " + packageName);
            
            // check for v3 subscriptions support
            response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
            if (response == BILLING_RESPONSE_RESULT_OK) {
                logDebug("Subscriptions AVAILABLE.");
                mSubscriptionsSupported = true;
            }
            else {
                logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
            }
            
            mSetupDone = true;
        }
        catch (RemoteException e) {
            if (listener != null) {
                listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
                                            "RemoteException while setting up in-app billing."));
            }
            e.printStackTrace();
            return;
        }

        if (listener != null) {
            listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
        }
    }
};

以下の部分から購入処理が始まる。

/* TODO: for security, generate your payload here for verification. See the comments on 
 *        verifyDeveloperPayload() for more info. Since this is a SAMPLE, we just use 
 *        an empty string, but on a production app you should carefully generate this. */
String payload = "";
        
setWaitScreen(true);// 画面切り替えの際の表示上の処理
Log.d(TAG, "Launching purchase flow for infinite gas subscription.");
mHelper.launchPurchaseFlow(this,
    SKU_INFINITE_GAS, IabHelper.ITEM_TYPE_SUBS, 
    RC_REQUEST, mPurchaseFinishedListener, payload);        
IabHelper.javaのlaunchPurchaseFlowメソッド

マーケットアプリのバージョン・購読に対応しているか確認する。

checkSetupDone("launchPurchaseFlow");
flagStartAsync("launchPurchaseFlow");
IabResult result;

if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
    IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, 
            "Subscriptions are not available.");
    if (listener != null) listener.onIabPurchaseFinished(r, null);
    return;
}

getBuyIntentでサービスに対して購入用のインテント生成処理を要求する。購入用のIntentを引き数にしてstartIntentSenderForResultをコールして実際に購入処理をする。

    try {
        logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
        Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
        int response = getResponseCodeFromBundle(buyIntentBundle);
        if (response != BILLING_RESPONSE_RESULT_OK) {
            logError("Unable to buy item, Error response: " + getResponseDesc(response));

            result = new IabResult(response, "Unable to buy item");
            if (listener != null) listener.onIabPurchaseFinished(result, null);
            return;
        }

        PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
        logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
        mRequestCode = requestCode;
        mPurchaseListener = listener;
        mPurchasingItemType = itemType;
        act.startIntentSenderForResult(pendingIntent.getIntentSender(),
                                       requestCode, new Intent(),
                                       Integer.valueOf(0), Integer.valueOf(0),
                                       Integer.valueOf(0));
    }

購読完了処理

正しくリクエストがされるとonActivityResultが呼ばれる。

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.d(TAG, "onActivityResult(" + requestCode + "," + resultCode + "," + data);

    // Pass on the activity result to the helper for handling
    if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
        // not handled, so handle it ourselves (here's where you'd
        // perform any handling of activity results not related to in-app
        // billing...
        super.onActivityResult(requestCode, resultCode, data);
    }
    else {
        Log.d(TAG, "onActivityResult handled by IABUtil.");
    }
}
handleActivityResult

色々とチェックしてるのだが最終的に以下のコードが呼ばれる。

if (mPurchaseListener != null) {
    mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
}

プロパティに保持してUIを更新しているだけっぽい。

// Callback for when a purchase is finished
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
    public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
        Log.d(TAG, "Purchase finished: " + result + ", purchase: " + purchase);
        if (result.isFailure()) {
            complain("Error purchasing: " + result);
            setWaitScreen(false);
            return;
        }
        if (!verifyDeveloperPayload(purchase)) {
            complain("Error purchasing. Authenticity verification failed.");
            setWaitScreen(false);
            return;
        }

        Log.d(TAG, "Purchase successful.");

        if (purchase.getSku().equals(SKU_GAS)) {
            // bought 1/4 tank of gas. So consume it.
            Log.d(TAG, "Purchase is gas. Starting gas consumption.");
            mHelper.consumeAsync(purchase, mConsumeFinishedListener);
        }
        else if (purchase.getSku().equals(SKU_PREMIUM)) {
            // bought the premium upgrade!
            Log.d(TAG, "Purchase is premium upgrade. Congratulating user.");
            alert("Thank you for upgrading to premium!");
            mIsPremium = true;
            updateUi();
            setWaitScreen(false);
        }
        else if (purchase.getSku().equals(SKU_INFINITE_GAS)) {
            // bought the infinite gas subscription
            Log.d(TAG, "Infinite gas subscription purchased.");
            alert("Thank you for subscribing to infinite gas!");
            mSubscribedToInfiniteGas = true;
            mTank = TANK_MAX;
            updateUi();
            setWaitScreen(false);
        }
    }
};

大体こんな流れである。当初アプリを再インストールした時の処理などは書かれていないんだろうと思い込んでたが、そんなことはなかった。

■購入情報の確認

アプリを再インストールしても購読した分がアプリに反映されなくてはいけない。

MainActivity.javaのmGotInventoryListenerの生成

mSubscribedToInfiniteGasに書込がされる部分は以下の部分である。

Purchase infiniteGasPurchase = inventory.getPurchase(SKU_INFINITE_GAS);
mSubscribedToInfiniteGas = (infiniteGasPurchase != null && 
    verifyDeveloperPayload(infiniteGasPurchase));
Log.d(TAG, "User " + (mSubscribedToInfiniteGas ? "HAS" : "DOES NOT HAVE") 
                   + " infinite gas subscription.");
if (mSubscribedToInfiniteGas) mTank = TANK_MAX;

getPurchaseは以下のようになっている。購入アイテムのHashMapからskuをkeyにして値を返す。

public Purchase getPurchase(String sku) {
    return mPurchaseMap.get(sku);
}

mPurchaseMapはaddPurchaseメソッドでセットされている。

void addPurchase(Purchase p) {
    mPurchaseMap.put(p.getSku(), p);
}

このメソッドがコールされるのはIabHelperのqueryPurchases。getPurchasesでService側にownedItemsを問い合わせる。

Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), itemType, continueToken);

取得したownedItemsからpurchaseDataListを取得しsignatureが一致しているものをinvに追加する。

ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
            RESPONSE_INAPP_ITEM_LIST);
ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
            RESPONSE_INAPP_PURCHASE_DATA_LIST);
ArrayList<String> signatureList = ownedItems.getStringArrayList(
            RESPONSE_INAPP_SIGNATURE_LIST);

for (int i = 0; i < purchaseDataList.size(); ++i) {
    String purchaseData = purchaseDataList.get(i);
    String signature = signatureList.get(i);
    String sku = ownedSkus.get(i);
    if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
        logDebug("Sku is owned: " + sku);
        Purchase purchase = new Purchase(itemType, purchaseData, signature);

        if (TextUtils.isEmpty(purchase.getToken())) {
            logWarn("BUG: empty/null token!");
            logDebug("Purchase data: " + purchaseData);
        }

        // Record ownership and token
        inv.addPurchase(purchase);
    }
    else {
        logWarn("Purchase signature verification **FAILED**. Not adding item.");
        logDebug("   Purchase data: " + purchaseData);
        logDebug("   Signature: " + signature);
        verificationFailed = true;
    }
}

購入アイテムのHashMapは生成された。もう少し辿るとqueryPurchasesはqueryInventoryメソッドからコールされる。queryInventoryは以下のqueryInventoryAsyncからコールされる。

public void queryInventoryAsync(final boolean querySkuDetails,
                           final List<String> moreSkus,
                           final QueryInventoryFinishedListener listener) {
    final Handler handler = new Handler();
    checkSetupDone("queryInventory");
    flagStartAsync("refresh inventory");
    (new Thread(new Runnable() {
        public void run() {
            IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
            Inventory inv = null;
            try {
                inv = queryInventory(querySkuDetails, moreSkus);
            }
            catch (IabException ex) {
                result = ex.getResult();
            }

            flagEndAsync();

            final IabResult result_f = result;
            final Inventory inv_f = inv;
            handler.post(new Runnable() {
                public void run() {
                    listener.onQueryInventoryFinished(result_f, inv_f);
                }
            });
        }
    })).start();
}

queryInventoryAsyncはonIabSetupFinishedの中でコールされ、onIabSetupFinished自体はサービスがConnectedした時にコールされる。

mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
    public void onIabSetupFinished(IabResult result) {
        Log.d(TAG, "Setup finished.");

        if (!result.isSuccess()) {
            // Oh noes, there was a problem.
            complain("Problem setting up in-app billing: " + result);
            return;
        }

        // Hooray, IAB is fully set up. Now, let's get an inventory of stuff we own.
        Log.d(TAG, "Setup successful. Querying inventory.");
        mHelper.queryInventoryAsync(mGotInventoryListener);
    }
});

という事でServiceがbindされた時にServiceに購入アイテムの問い合わせを要求する。そしてHashMapが完成してUIに反映される。

サンプルアプリのアップロード

必要情報を入力して公開できる状態にする必要がある。但し実際に公開せず下書きの状態にしておく。

サンプル商品情報の入力

必要情報を入力し公開状態にする必要がある。

全てのアプリケーション>アプリ>アプリ内アイテム

商品を登録する。日本円における最低料金は99円だが、各通貨ごとにも最低料金があるので全て満たす必要がある。

設定>アカウントの詳細

開発者のアカウントでは購入できないので、テスト用のアクセス権がある Gmail アカウントでテスト用のアカウントを設定する。

エラー

このバージョンのアプリは、Google Playを通じたお支払いはご利用になれません。詳しくはヘルプセンターをご覧ください。

アプリが署名されている必要がありadbで署名されたapkをインストールする必要がある。

adb -d install ./adb -d install ~/TrivialDrive/TrivialDrive.apk 
出版社はこのアイテムを購入できません

Google Play の規約にもある通り開発者アカウントでは購入できない。

AndroidでKeyboardより手前にViewを表示する

■PopupWindowを使う

// 中身のViewの生成
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View contentView = inflater.inflate(R.layout.content, null);

// popupウィンドウの生成
popupWindow = new PopupWindow(contentView);
popupWindow.setWidth(WindowManager.LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);

以下のコードで表示できる。

View contentView = ((ViewGroup)findViewById(android.R.id.content)).getChildAt(0);
popupWindow.showAtLocation(contentView, Gravity.BOTTOM, 0, 0);

廃案

DialogFragmentを使う

PopupWindowと似たような表示ができると思い試してみたがKeyBoardの上に表示することができなかった。

public static class SampleDialog extends DialogFragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        getDialog().getWindow().setGravity(Gravity.BOTTOM);
        getDialog().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
        return super.onCreateView(inflater, container, savedInstanceState);
    }
}

以下のコードで表示できる。

SampleDialog dialog = new SampleDialog();
dialog.show(getSupportFragmentManager(), "hoge");

Pinyin Inputをビルドする

ソースを取得してビルドしようとするとコンパイルエラーで通らない。これはOpenWnnについても同様である。

■フィールド変数に保持したpaddingへのアクセス

どうやらプリインアプリはアクセスできるようだ。

int measuredWidth = mPaddingLeft + mPaddingRight;
int measuredHeight = mPaddingTop + mPaddingBottom;

以下のように書き換える。

int measuredWidth = getPaddingLeft() + getPaddingRight();
int measuredHeight = getPaddingTop() + getPaddingBottom();

■フィールド変数に保持したcontextへのアクセス

どうやらプリインアプリはアクセスできるようだ。

Resources r = mContext.getResources();

以下のように書き換える。

Resources r = getContext().getResources();

■リソースへのアクセス

CharSequence itemInputMethod = getString(com.android.internal.R.string.inputMethod);

以下のように書き換える。

CharSequence itemInputMethod = getString(Resources.getSystem().getIdentifier("inputMethod", "string", "android"));

■Androidのソースコードに含まれていてSDKにはexportされていないクラス

if ("1".equals(SystemProperties.get("ro.kernel.qemu"))) {

以下のように書き換える。

if ("1".equals(System.getProperty("ro.kernel.qemu"))) {

参考

AndroidのListViewで選択状態の項目の背景色を変える

意外と面倒だったのでメモしておく。

■実装

res/layout-v11/cell.xml

API level 11以降の場合は以下のように記述すれば終わる。

<?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="wrap_content"
    android:background="?android:attr/activatedBackgroundIndicator"
    android:orientation="vertical">
    <TextView 
        android:id="@+id/name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    <TextView 
        android:id="@+id/text"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

res/layout/cell.xml

上述のlayoutファイルしかないとAPI level 10以下で実行できなくなるので以下のレイアウトファイルを追加する。

<?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="wrap_content"
    android:orientation="vertical">
    <TextView 
        android:id="@+id/name"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
    <TextView 
        android:id="@+id/text"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

DataListCursorAdapter.java

bindViewでonDataAssignedを実行するようにする。

class DataListCursorAdapter extends CursorAdapter {
    private Listener mListener;
    public static class ViewHolder {
        public TextView name;
        public TextView text;
    }
    @SuppressWarnings("deprecation")
    public DataListCursorAdapter(Context context, Cursor c, Listener listener) {
        super(context, c);
        mListener = listener;
    }
    public DataListCursorAdapter(Context context, Cursor c, Listener listener, boolean autoRequery) {
        super(context, c, false);
        mListener = listener;
    }
    public DataListCursorAdapter(Context context, Cursor c, Listener listener, int flags) {
        super(context, c, FLAG_REGISTER_CONTENT_OBSERVER);
        mListener = listener;
    }
    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        try {
            ViewHolder holder = (ViewHolder) view.getTag();
            JSONObject json = new JSONObject(cursor.getString(cursor.getColumnIndex(Table.DATA)));
            final Data data = new Data(json);
            mListener.onDataAssigned(data, view);
            
            holder.name.setText(data.name);
            holder.text.setText(data.text);
        }
        catch (JSONException e) {
        }
    }
    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.cell, null);
        ViewHolder holder = new ViewHolder();
        holder.name      = (TextView) view.findViewById(R.id.name);
        holder.text      = (TextView) view.findViewById(R.id.text);
        view.setTag(holder);
        return view;
    }
    public interface Listener {
        public void onDataAssigned(Data data, View view);
    }
}

DataListFragment.java

ListViewは画面サイズを敷き詰めるのに十分な数のViewを生成し、スクロールして表示される項目についてはViewの中身を入れ替えるだけで実現しているので省メモリで高速である。一方で選択項目のViewの背景色を変えるとそのViewを再利用した項目の背景色も変わってしまう。従って、以下のようにinterfaceを実装してHoney Comb未満ではデータがbindされた時にリストの背景色を確認・変更するようにする。

    @Override
    public void onDataAssigned(Data data, View view) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            return;
        }
        if (TextUtils.equals(mLastSelectedDataId, data.id)) {
            view.setBackgroundColor(0xfff7aecb);
        }
        else {
            view.setBackgroundColor(0x00000000);
        }
    }

最後にタップしたリストのデータをフィールドに保持するようにする。

    @Override
    public void onItemClick(AdapterView<?> listView, View view, int position, long id) {
        Cursor cursor = mArrayCursorAdapter.getCursor();
        JSONObject json = new JSONObject(cursor.getString(cursor.getColumnIndex(Table.DATA)));
        final Data data = new Data(json);
        if (data != null && TextUtils.equals(mLastSelectedDataId, data.id)) {
            getListView().setItemChecked(position, false);
            mLastSelectedDataId = null;
        }
        else if (data != null) {
            getListView().setItemChecked(position, true);
            mLastSelectedDataId = data.id;
        }
    }

AndroidでIsolatedContextを使ってみる

■コード

以下のようにしてIsolatedContextのサブクラスを定義する。

public class HogeActivityTest extends ActivityInstrumentationTestCase2<Kaomoji> {
    private Kaomoji mActivity;
    private static final MockContentResolver RESOLVER = new MockContentResolver();
    private static class TestIsolatedContext extends IsolatedContext {
        private Context mContext;
        public TestIsolatedContext(ContentResolver resolver, Context targetContext) {
            super(resolver, targetContext);
            mContext = targetContext;
        }
        @Override
        public SharedPreferences getSharedPreferences(String name, int mode) {
            return mContext.getSharedPreferences("prefix_" + name, mode);
        }
    }
}

必要なメソッドはOverrideして実装する。以下のようにしてインスタンス化して、production環境に影響しない事を確かめる。

TestIsolatedContext context = new TestIsolatedContext(RESOLVER, mActivity);
SharedPreferences preferences = context.getSharedPreferences(Constants.KEY_USER_DATA, Context.MODE_PRIVATE);
String str = preferences.getString(Constants.KEY_USER_STRING, null);
assertEquals(str, null);// production側で文字列が保存されているのに関係しない

参考

ちょうどIsolatedContextを使ってるテストがなかったので何となく間に合わせ的なコードになってしまった。

AndroidでContentProviderをテストする

IsolatedContextの記事を書こうとしらProviderTestCase2の事を書いてなかった事に気づいたので書いておく。

■コード

public class ProviderTest extends ProviderTestCase2<Provider> {
    private Provider mProvider;
    private Uri mAuthority;

    public ProviderTest() {
        super(Provider.class, "");
    }
    
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mProvider = getProvider();
        mAuthority = Provider.URI;
    }
    
    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        mProvider = null;
        mAuthority = null;
    }
    
    public void testOnCreate() {
        assertFalse(mProvider.onCreate());
    }
    
    public void testGetType() {
        assertEquals(mProvider.getType(Provider.URI), null);
    }
    
    public void test1Insert() {
        String key   = "key1";
        String value = "value1";
        ContentValues values = new ContentValues();
        values.put("key", key);
        values.put("value", value);
        Uri result = mProvider.insert(mAuthority, values);
        assertEquals(mAuthority, result);
        
        Cursor cursor = mProvider.query(
                mAuthority,
                new String[]{"_id", "key", "value"},
                "`key` = ?",
                new String[]{key},
                null
        );
        assertTrue(cursor.moveToFirst());
        assertTrue(0 < cursor.getCount());
        assertEquals(cursor.getString(cursor.getColumnIndex("key")), key);
        assertEquals(cursor.getString(cursor.getColumnIndex("value")), value);
        cursor.close();
    }
    
    public void test2Update() {
        String key      = "key1";
        String value    = "valuevalue";
        ContentValues values = new ContentValues();
        values.put("key", key);
        values.put("value", value);
        int row = mProvider.update(
                mAuthority,
                values,
                "`key` = ?",
                new String[]{key}
        );
        assertTrue(0 < row);
        
        Cursor cursor = mProvider.query(
                mAuthority,
                new String[]{"_id", "key", "value"},
                "`key` = ?",
                new String[]{key},
                null
        );
        assertTrue(cursor.moveToFirst());
        assertTrue(0 < cursor.getCount());
        assertEquals(cursor.getString(cursor.getColumnIndex("key")), key);
        assertEquals(cursor.getString(cursor.getColumnIndex("value")), value);
        cursor.close();
    }
    
    public void test3Delete() {
        String key = "key1";
        int row = mProvider.delete(mAuthority, "`key` = ?", new String[]{key});
        assertTrue(row > 0);
    }
}

ProviderTestCase2のコンストラクタがIsolatedContextオブジェクトを生成し、production環境とは別の環境で実行される。メソッドの実行順序を指定しているのが良くない。