@blog.justoneplanet.info

日々勉強

Unicode 6.0の絵文字のソースをコード内で扱える形式に変換する

自分用のメモ。元データをJavaやJavaScript内で扱える形式に変換する。以下のようにnodeで実装した。

var fs = require('fs');
var csv = require('csv');
csv()
.from.stream(fs.createReadStream(__dirname+'/EmojiSources.txt'), {"delimiter" : ";"})
.to.path(__dirname+'/sample.txt')
.transform(function(row){
  var elm = row.slice(0, 1); 
  if (elm[0].indexOf('1F') > -1) {
    var code = parseInt("0x" + elm[0], 16);
    code -= 0x10000;
    var hi = code >> 10; 
    var lo = code & 0x3FF;
    hi |= 0xD800;
    lo |= 0xDC00;
    elm[0] = '\\u' + hi.toString(16) + '\\u' + lo.toString(16);
    elm[1] = String.fromCharCode(hi, lo);
  }
  else {
    elm[1] = String.fromCharCode(parseInt(elm[0], 16));
    elm[0] = '\\u' + elm[0] + ''; 
  }
  return elm;
})
.on('record', function(row,index){
  console.log('#' + index + ' ' + JSON.stringify(row));
})
.on('close', function(count){
  console.log('Number of lines: '+count);
})
.on('error', function(error){
  console.log(error.message);
});

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

■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

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
python build_mozc.py gyp --target_platform=Android --android_sdk_home=/home/ubuntu/android-sdk-linux
python 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

ちなみに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でビルドできる。

参考

RDSでインスタンスがブロックされMySQLに接続できなくなる

RDSを使っているとMulti-AZ DeploymentがYesになっている場合でも、以下のようにMySQLに接続できない時間が存在する。

Database connection "SQLSTATE[HY000] [2003] Can't connect to MySQL server on 'endpoint.zone.amazonaws.com' " is missing, or could not be created.

ユーザーのアクセスごとにconnectが発生するようなシステムの場合、上述の接続できない時間の間に多数のユーザーがアクセスし、クライアントが接続エラーを繰り返す場合がある。その場合、以下のエラーが発生する。

Database connection "SQLSTATE[HY000] [1129] Host 'instance.zone.compute.internal' is blocked because of many connection errors; unblock with 'mysqladmin flush-hosts'" is missing, or could not be created.

■上限値を増やす

max_connect_errorsを変更することで接続エラーの上限値を変更できる。

■ブロックを解除する

mysqladmin -h endpoint.zone.amazonaws.com -u user --password=password flush-hosts

参考

Mountain Lion上でCocoaPodsを導入する

メモ。

ln -s /usr/bin/gcc /usr/bin/gcc-4.2
sudo gem install cocoapods
pod setup
cd your_project
vim Podfile

以下のようにプラットフォームと使用するライブラリを記述する。

platform :ios,'6.0'
pod 'JSONKit','~> 1.4'

以下のコマンドでインストールする。

pod install

Podfileから特定のライブラリが削除されていれば、この時にライブラリが削除される。

ビルドできなくなる

以下の表示になってビルドが止まる。

library not found for -lPods

以下のコマンドでxcodeを起動すれば良い。

open your_project_name.xcworkspace/
  • your_project_name.xcworkspace
  • your_project_name.xcodeproj

libraryを入れるとビルドできなくなる

libraryを入れると以下のエラーが表示されてビルドできなくなる。

duplicate symbols for architecture i386

CocoaPodsで導入したライブラリと直接入れたライブラリが衝突している場合に起こる。直接入れたライブラリを削除する。

■PonyDebugger

サーバー

以下のコマンドでローカルのサーバーをインストールして動作させる。

curl -sk https://cloud.github.com/downloads/square/PonyDebugger/bootstrap-ponyd.py | python - --ponyd-symlink=/usr/local/bin/ponyd ~/Library/PonyDebugger

以下のコマンドで起動する。

ponyd serve --listen-interface=127.0.0.1

クライアント

vim Podfile

以下のようにライブラリを追記する。

platform :ios,'6.0'
pod 'PonyDebugger'

以下のコマンドでインストールを行う。

pod install

AppDelegate.m

#if DEBUG
#import <PonyDebugger/PonyDebugger.h>
#endif
PonyDebuggerがNotFoundでimportできない。

target header search pathsに$(inherited)を追記する。

#if DEBUG
    PDDebugger *debugger = [PDDebugger defaultInstance];
    [debugger enableNetworkTrafficDebugging];
    [debugger forwardAllNetworkTraffic];
    [debugger enableCoreDataDebugging];
    [debugger enableViewHierarchyDebugging];
    [debugger connectToURL:[NSURL URLWithString:@"ws://localhost:9000/device"]];
#endif

ブラウザでlocalhost:9000/deviceにアクセスする。

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

EC2のAmazon Linux上でビルドする。おそらくUbuntuでやる方が多数派である。

■準備

curl http://repo.us-east-1.amazonaws.com/latest/main/mirror.list
sudo vim /etc/yum.repos.d/amzn-main-32.repo 

glibc-develの32bit版が必要になるので、以下のようにしてリポジトリを追加する必要がある。

[amzn-main-32]
name=amzn-main-Base-32
baseurl=http://packages.us-east-1.amazonaws.com/2012.09/main/201209eb6a01/i386
metadata_expire=300
priority=1
failovermethod=priority
fastestmirror_enabled=0
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-amazon-ga
enabled=1

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

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

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

sudo yum remove java-1.6.0-openjdk 

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
PATH=~/bin:$PATH
curl https://dl-ssl.google.com/dl/googlesource/git-repo/repo > ~/bin/repo
chmod a+x ~/bin/repo
mkdir android
cd android
repo init -u https://android.googlesource.com/platform/manifest
repo init -u https://android.googlesource.com/platform/manifest -b android-4.0.1_r1

■ダウンロード

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

repo sync

■ビルド

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

make

ちなみに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

ちなみに中国語はGoogle Pinyin IMEがデフォルトで以下のディレクトリにある。

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

SolrでDataImportHandlerを使う

前回の続き。

■インポート元などの設定

solr/conf/data-config.xml

はてなのドキュメントが少し違っていて、以下のようにdocumentノードが無いとエラーになる。

<?xml version="1.0" encoding="UTF-8" ?>
<dataConfig>
  <dataSource
    name="dbname"
    driver="com.mysql.jdbc.Driver"
    url="jdbc:mysql://localhost/dbname"
    user="username"
    password="password"
    batchSize="-1"
    useUnicode="true"
    characterEncoding="utf8"
    useOldUTF8Behavior="true"
    readOnly="true" />
  <document name="items">
    <entity
      name="table"
      dataSource="dbname"
      query="
        SELECT
          `id`,
          `key`,
          `value`
        FROM
          `table`
        WHERE
          `created` &lt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 7 DAY)
        "
      deltaQuery="
        SELECT
          `id`
        FROM
          `table`
        WHERE
          `created` &lt; DATE_SUB(CURRENT_TIMESTAMP, INTERVAL 7 DAY)
        AND
          `created` &gt; DATE_SUB('${dataimporter.last_index_time}', INTERVAL '7 9' DAY_HOUR)
        "
     deltaImportQuery="
        SELECT
          `id`,
          `key`,
          `value`
        FROM
          `table`
        WHERE
          `id` = '${dataimporter.delta.id}'
        "
      transformer="ClobTransformer,DateFormatTransformer">
    </entity>
  </document>
</dataConfig>

solr/conf/solrconfig.xml

以下の記述を追加する。

  <requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
    <lst name="defaults">
     <str name="config">data-config.xml</str>
    </lst>
  </requestHandler>

エラー1

後述のURLでインポートをすると以下のようなエラーが発生する。

Caused by: java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
        at java.net.URLClassLoader$1.run(URLClassLoader.java:217)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:205)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:321)
        at java.net.FactoryURLClassLoader.loadClass(URLClassLoader.java:615)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at org.apache.solr.core.SolrResourceLoader.findClass(SolrResourceLoader.java:378)

以下のコマンドを実行してJDBCドライバを配置する。

wget http://www.mysql.com/get/Downloads/Connector-J/mysql-connector-java-5.1.22.tar.gz/from/http://cdn.mysql.com/
tar xvzf mysql-connector-java-5.1.22.tar.gz
cp mysql-connector-java-5.1.22/mysql-connector-java-5.1.22-bin.jar ./lib/

■インポートの実行

以下のURLにアクセスする。


http://localhost/solr/admin/dataimport.jsp?handler=/dataimport

■差分インポートの実行

ドキュメントを見ると差分インポートの仕方もあるようなのだが、(Jetty経由で叩く?)いまいちよくわからないので、以下のようにcrontabとcurlで実行することにした。

00 * * * * curl "http://localhost/solr/dataimport?clean=false&commit=true&command=delta-import"

参考

緊急地震速報 by Extensionのサーバーを書き直す

そろそろOAuth認証にしておかないとまずいので書きなおすことにした。

■環境構築

必要なパッケージのインストール等をする。

sudo yum -y install openssl-devel gcc-c++ git make
git clone https://github.com/isaacs/nave.git ~/.nave
~/.nave/nave.sh install stable
~/.nave/nave.sh use stable
echo "~/.nave/nave.sh use stable" >> ~/.bash_profile
sudo curl https://npmjs.org/install.sh --insecure | sh

npm -g install forever
npm install twitter
npm install websocket

以下のファイルを編集する。

vim /etc/security/limits.conf

以下の記述を加える。

*               soft    nofile            32768
*               hard    nofile            32768

■実装

以下のようにした。

const PORT = (process.argv[2])? process.argv[2] : 8080;
const FOLLOWING_ID = 16052553;
const CONSUMER_KEY    = "CONSUMER_KEY";
const CONSUMER_SECRET = "CONSUMER_SECRET";
const TOKEN_KEY       = "TOKEN_KEY";
const TOKEN_SECRET    = "TOKEN_SECRET";

var util            = require('util')
  , http            = require('http')
  , twitter         = require('twitter')
  , WebSocketServer = require('websocket').server
;

util.puts('[' + new Date() + ']Listen ' + process.argv[2]);
// build server
var server = http.createServer(function(request, response) {
  util.puts('[' + new Date() + ']Received request for ' + request.url);
  response.writeHead(404);
  response.end();
});
server.listen(PORT, function() {
  util.puts('[' + new Date() + ']Server is listening on port ' + PORT);
});
var wsServer = new WebSocketServer({
  "httpServer"            : server,
  "autoAcceptConnections" : true
});
wsServer.on(
  'connect',
  function(connection){
    util.puts('[' + new Date() + ']Connection accepted.');
    connection.sendUTF('{"status" : "accepted"}');
  }
);
setInterval(function () {wsServer.broadcastUTF("");}, 30 * 1000);

// build twitter connection
var twit = new twitter({
  "consumer_key"        : CONSUMER_KEY,
  "consumer_secret"     : CONSUMER_SECRET,
  "access_token_key"    : TOKEN_KEY,
  "access_token_secret" : TOKEN_SECRET
});
twit.stream('statuses/filter', {"follow" : FOLLOWING_ID}, function(stream) {
  stream.on('data', function(data) {
    if (data['user']['id'] === FOLLOWING_ID) {
      delete data['source'];
      delete data['contributors'];
      delete data['entities'];
      delete data['favorited'];
      delete data['lang'];
      delete data['truncated'];
      delete data['in_reply_to_status_id'];
      delete data['in_reply_to_status_id_str'];
      delete data['in_reply_to_user_id'];
      delete data['in_reply_to_user_id_str'];
      delete data['in_reply_to_screen_name'];
      delete data['user']['name'];
      delete data['user']['screen_name'];
      delete data['user']['url'];
      delete data['user']['description'];
      delete data['user']['protected'];
      delete data['user']['followers_count'];
      delete data['user']['friends_count'];
      delete data['user']['listed_count'];
      delete data['user']['created_at'];
      delete data['user']['favourites_count'];
      delete data['user']['utc_offset'];
      delete data['user']['time_zone'];
      delete data['user']['geo_enabled'];
      delete data['user']['verified'];
      delete data['user']['statuses_count'];
      delete data['user']['lang'];
      delete data['user']['contributors_enabled'];
      delete data['user']['is_translator'];
      delete data['user']['profile_background_color'];
      delete data['user']['profile_background_image_url'];
      delete data['user']['profile_background_image_url_https'];
      delete data['user']['profile_background_tile'];
      delete data['user']['profile_image_url'];
      delete data['user']['profile_image_url_https'];
      delete data['user']['profile_link_color'];
      delete data['user']['profile_sidebar_border_color'];
      delete data['user']['profile_sidebar_fill_color'];
      delete data['user']['profile_text_color'];
      delete data['user']['profile_use_background_image'];
      delete data['user']['default_profile'];
      delete data['user']['default_profile_image'];
      delete data['user']['following'];
      delete data['user']['follow_request_sent'];
      delete data['user']['notifications'];
      util.puts('[' + new Date() + ']EEW ' + data);
      wsServer.broadcastUTF(JSON.stringify(data));
    }   
  }); 
  stream.on('error' , function(err) {
    util.puts('[' + new Date() + ']Error::Stream' + err);
  }); 
});
setTimeout(
  function () {
    throw new Error('[' + new Date() + ']Error::for reboot');
  },  
  1000 * 60 * 60// 1 hour
);

■起動

以前はnohupとか使ってたが、ちゃんとforeverを使うようにする。

ulimit -n 32768
forever start -a --spinSleepTime=10000 -w --watchDirectory=./ -l ~/.forever/eew80.log server.js 80
forever start -a --spinSleepTime=10000 -w --watchDirectory=./ -l ~/.forever/eew443.log server.js 443
forever start -a --spinSleepTime=10000 -w --watchDirectory=./ -l ~/.forever/eew8080.log server.js 8080

■サーバーの再起動

rebootするとforeverが止まるので起動スクリプトを書く。

/etc/rc.d/init.d/eewd

パスは適宜かえるとする。

 /etc/init.d/functions

NAME=eewd
SOURCE_DIR=/var/www/hogehoge.com/eew
SOURCE_FILE=server.js

user=root
pidfile=/root/.forever/pids/$NAME.pid
logfile=/root/.forever/production.log
forever_dir=/root/.nave/installed/0.8.17/bin

node=/root/.nave/installed/0.8.17/bin/node
forever=/root/.nave/installed/0.8.17/bin/forever
sed=sed

export PATH=$PATH:/root/.nave/installed/0.8.17/bin
export NODE_PATH=$NODE_PATH:/root/.nave/installed/0.8.17/lib/node_modules


start() {
  echo "Starting $NAME node instance: "

  if [ "$foreverid" == "" ]; then
    # Create the log and pid files, making sure that
    # the target use has access to them
    touch $logfile
    chown $user $logfile

    touch $pidfile
    chown $user $pidfile

    # Launch the application
    daemon --user=root \
      $forever start -a -d --spinSleepTime=10000 -w --watchDirectory=$SOURCE_DIR -p $forever_dir --pidfile $pidfile -l $logfile $SOURCE_DIR/$SOURCE_FILE
    RETVAL=$?
  else
    echo "Instance already running"
    RETVAL=0
  fi
}

stop() {
  echo -n "Shutting down $NAME node instance : "
  if [ "$foreverid" != "" ]; then
    $node $SOURCE_DIR/prepareForStop.js
    $forever stop -p $forever_dir $id
  else
    echo "Instance is not running";
  fi
  RETVAL=$?
}

if [ -f $pidfile ]; then
  read pid < $pidfile
else
  pid = ""
fi

if [ "$pid" != "" ]; then
  # Gnarly sed usage to obtain the foreverid.
  sed1="/$pid\]/p"
  sed2="s/.*\[\([0-9]\+\)\].*\s$pid\].*/\1/g"
  foreverid=`$forever list -p $forever_dir | $sed -n $sed1 | $sed $sed2`
else
  foreverid=""
fi

case "$1" in
  start)
    start
    ;;
  stop)
    stop
    ;;
  status)
    status -p ${pidfile}
    ;;
  *)
    echo "Usage:  {start|stop|status}"
    exit 1
    ;;
esac
exit $RETVAL

これでしばらくテストして上手く行けばリリースする。