@blog.justoneplanet.info

日々勉強

ZipInputStreamから文字列を取り出す

メモ。

■ZipInputStream

Zipはzip化されたデータとヘッダー情報から成る。

public static String readZIPStream(InputStream inputStream) throws IOException {
    String str = "";
    String readed;
    ZipInputStream zis = new ZipInputStream(inputStream);
    InputStreamReader inputStreamReader = new InputStreamReader(zis);
    BufferedReader bufferReader = new BufferedReader(inputStreamReader);
    while (zis.getNextEntry() != null) {
        while ((readed = bufferReader.readLine()) != null) {
            str += readed + "\n";
        }
        zis.closeEntry();
    }
    bufferReader.close();
    inputStreamReader.close();
    zis.close();
    return str;
}

■GZIPInputStream

GZipはzip化されたデータ本体で、GZipの場合は勝手が違う。

public static String readGZIPStream(InputStream inputStream) throws IOException {
    String str = "";
    GZIPInputStream gzis = new GZIPInputStream(inputStream);
    InputStreamReader inputStreamReader = new InputStreamReader(gzis);
    BufferedReader bufferReader = new BufferedReader(inputStreamReader);
    String readed;
    while ((readed = bufferReader.readLine()) != null) {
        str += readed;
    }
    bufferReader.close();
    inputStreamReader.close();
    gzis.close();
    return str;
}

BufferedReaderをインスタンス化するときにサイズを指定しない場合は8192となる。

mozc for Androidにトグルが一定時間で確定される機能を追加する

mozc本体のコメントに将来的にそういった機能をつけるような事が書かれていて、その時コンフリクトする事が予想される。

■クライアント側

以下のように定義し、ViewManager.javaでタイマーでコマンドが実行されるようにする。

ViewManager.java

  private final int keycodeKaomojiToggleCommit;
    keycodeKaomojiToggleCommit = res.getInteger(R.integer.key_kaomoji_toggle_commit);

とりあえず定義だけ上述のようにする。タイマー部分はよきに計らってやる。

KeycodeConverter.java

  public static final ProtoCommands.KeyEvent KAOMOJI_TOGGLE_COMMIT =
      ProtoCommands.KeyEvent.newBuilder().setSpecialKey(SpecialKey.KAOMOJI_TOGGLE_COMMIT).build();

keycode.xml

  <integer name="key_kaomoji_toggle_commit">-10025</integer>

ProtoCommands.java

サーバー側をビルドすると生成される。

■サーバー側

composer/composer.cc

実装しようとしている機能はカーソルを右に動かす動作と非常に似ている。そしてカーソルを動かす処理はこのファイルの以下のメソッドである。

void Composer::MoveCursorRight() {
  if (position_ < composition_->GetLength()) {
    ++position_;
  }
  UpdateInputMode();

  typing_corrector_.Invalidate();
}

そこでpositionを移動させない関数を追加する。引き数で条件分岐してもいいのだが右に移動させる機能とトグルをコミットさせる機能は本質的に別物であるので別にする。コンフリクトすると面倒なだけでもある。

void Composer::KaomojiToggleCommit() {
  UpdateInputMode();
  typing_corrector_.Invalidate();
}

composer/composer.h

  void KaomojiToggleCommit();

次に上述を呼び出す処理を実装する。

session/session.h

  bool KaomojiToggleCommit(mozc::commands::Command *command);

session/session.cc

bool Session::KaomojiToggleCommit(commands::Command *command) {
  if (context_->GetRequest().crossing_edge_behavior() == commands::Request::COMMIT_WITHOUT_CONSUMING && context_->composer().GetLength() == context_->composer().GetCursor()) {
    Commit(command);

    // Do not consume.
    command->mutable_output()->set_consumed(false);
    return true;
  }

  command->mutable_output()->set_consumed(true);
  if (CommitIfPassword(command)) {
    return true;
  }
  context_->mutable_composer()->KaomojiToggleCommit();
  if (Suggest(command->input())) {
    Output(command);
    return true;
  }
  OutputComposition(command);
  return true;
}

コピペで申し訳ない限りである。このメソッドは同ファイルの以下の部分から呼び出される。

    case keymap::CompositionState::MOVE_CURSOR_RIGHT:
      return MoveCursorRight(command);

    case keymap::CompositionState::KAOMOJI_TOGGLE_COMMIT:
      return KaomojiToggleCommit(command);

    case keymap::CompositionState::MOVE_CURSOR_TO_BEGINNING:
      return MoveCursorToBeginning(command);

定義したメソッドがクライアントからのコマンドで呼び出されるようにする。

■CompositionState

以下のようにCompositionStateで実行されるコマンドの定義と登録をする。

session/internal/keymap_interface.h

  MOVE_CURSOR_RIGHT,
  KAOMOJI_TOGGLE_COMMIT,
  MOVE_CURSOR_TO_BEGINNING,

session/internal/keymap.cc

以下のようにして、コマンド名KaomojiToggleCommitで、CompositionState::KAOMOJI_TOGGLE_COMMITが実行されるようになる。

RegisterCompositionCommand("KaomojiToggleCommit", CompositionState::KAOMOJI_TOGGLE_COMMIT);

data/keymap/mobile.tsv

以下のようにして、KaomojiToggleCommitキーでKaomojiToggleCommitコマンドが実行される。

Composition	Right	MoveCursorRight
Composition	KaomojiToggleCommit	KaomojiToggleCommit
Composition	Space	Convert

一番左の列はstatusなのだがIME全体で入力の状態というものが存在するという事を理解しておく必要がある。

■Specialキーの追加とKeyEvent

強引ではあるがトグルをコミットさせる(実際には視認できない)KaomojiToggleCommitキーを追加する。

session/commands.proto

下のように記述して定義する。ビルドした時にKAOMOJI_TOGGLE_COMMITが追加されたProtoCommands.javaが生成され、クライアント側からSpecialKeyのKAOMOJI_TOGGLE_COMMITを叩ける。

COMMA = 70;  // Numpad [,]
KAOMOJI_TOGGLE_COMMIT = 71;
NUM_SPECIALKEYS = 72;

data/usage_stats/stats.def

追加したSpecialKeyなどがusage stats用のリストに存在せず、Debugビルドするとトグルがコミットされた時に落ちるので、以下のように追加する。

Performed_Composition_KaomojiToggleCommit
# Virtual Key for software keyboard
KAOMOJI_TOGGLE_COMMIT

この定義により、今回のキーとコマンドがusage_stats_list.hに追加される。

commands.protoから、commands.pb.hとcommands.pb.ccが生成されるが、キー操作がフックされるために以下の2ファイルの書き換えが必要となる。

session/random_keyevents_generator.cc

commands.pb.hがincludeされ以下のようにkSpecialKeysの配列ができる。

  commands::KeyEvent::RIGHT,
  commands::KeyEvent::KAOMOJI_TOGGLE_COMMIT,
  commands::KeyEvent::UP,

session/key_parser.cc

    keycode_map_["right"] = KeyEvent::RIGHT;
    keycode_map_["kaomojitogglecommit"] = KeyEvent::KAOMOJI_TOGGLE_COMMIT;
    keycode_map_["enter"] = KeyEvent::ENTER;

SessionCommandの追加

都合によりSpecialKeyを追加したが本来ならばSessionCommandの追加で特に問題はない。

data/usage_stats/stats.def
SendCommand_KaomojiToggleCommit
session/commands.proto

以下のようにしてSessionCommandを追加できる。

    KAOMOJI_TOGGLE_COMMIT = 24;

    // Number of commands.
    // When new command is added, the command should use below number
    // and NUM_OF_COMMANDS should be incremented.
    NUM_OF_COMMANDS = 25;
session/session.cc

以下のようにしてSessionCommandをフックする。

    case commands::SessionCommand::KAOMOJI_TOGGLE_COMMIT:
      result = KaomojiToggleCommit(command);
      break;  

上述のようにするとSessionExecutorから以下のように呼び出せる。

  public void toggleCommit(int candidateId, EvaluationCallback callback) {
    Input.Builder inputBuilder = Input.newBuilder()
        .setType(CommandType.SEND_COMMAND)
        .setCommand(SessionCommand.newBuilder()
            .setType(SessionCommand.CommandType.KAOMOJI_TOGGLE_COMMIT)
            .setId(candidateId));
    evaluateAsynchronously(inputBuilder, null, callback);
  }

■ダウンロード

みんなの顔文字キーボード

Androidでキーボードの設定画面に遷移させる

設定画面から遷移できるキーボードを有効化する画面に遷移する。

Intent intent = new Intent();
intent.setAction(Settings.ACTION_INPUT_METHOD_SETTINGS);
startActivity(intent);

入力に使用するキーボードを選択するダイアログを表示する。

InputMethodManager inputMethodManager = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
inputMethodManager.showInputMethodPicker();

任意のパッケージ名のInputMethodが有効化されているか判定する。

private boolean isEnabled(Context context) {
    InputMethodManager inputMethodManager = InputMethodManager.class.cast(
            context.getSystemService(Context.INPUT_METHOD_SERVICE)
    );
    if (inputMethodManager == null) {
        Log.i("InputMethodManager", "not found.");
        return false;
    }
    String packageName = context.getPackageName();
    // 有効なInputMethod一覧に任意のパッケージ名が含まれているか確認する
    for (InputMethodInfo inputMethodInfo : inputMethodManager.getEnabledInputMethodList()) {
        if (inputMethodInfo.getServiceName().startsWith(packageName)) {
            return true;
        }
    }
    return false;
}

任意のパッケージ名のInputMethodが入力方法として選択されているかどうか判定する。

private boolean isDefault(Context context) {
    InputMethodInfo info = null;
    InputMethodManager inputMethodManager = InputMethodManager.class.cast(
            context.getSystemService(Context.INPUT_METHOD_SERVICE)
    );
    if (inputMethodManager == null) {
        Log.i("InputMethodManager", "not found.");
        return false;
    }
    String packageName = context.getPackageName();
    // InputMethod一覧に任意のパッケージ名が含まれているか確認する
    for (InputMethodInfo inputMethodInfo : inputMethodManager.getInputMethodList()) {
        if (inputMethodInfo.getPackageName().equals(packageName)) {
            info = inputMethodInfo;
        }
    }
    if (info == null) {
        Log.i("InputMethodInfo", "not found.");
        return false;
    }

    // infoのidと現在有効になってるIMEのidを比較する
    String currentIme = android.provider.Settings.Secure.getString(
        context.getContentResolver(),
        android.provider.Settings.Secure.DEFAULT_INPUT_METHOD
    );
    return info.getId().equals(currentIme);
}

Kindle Fire HDでcanvas.drawPictureするとUnsupportedOperationExceptionでクラッシュする

以下の部分でUnsupportedOperationExceptionが発生しクラッシュする。

canvas.drawPicture(picture);

drawPicture()はハードウェアアクセラレーションに対応していないらしいので以下のように修正した。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && canvas.isHardwareAccelerated()) {
  picture.draw(canvas);
}
else {
  canvas.drawPicture(picture);
}

追記

ImageViewなどのViewの描画でdrawableのdrawの中で以下の処理がされるとkindleでは描画できない。

picture.draw(canvas);

以下のようにハードウェアアクセラレーションが有効になっているViewでソフトウェアレンダリングするように指定した。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {// for kindle
  imageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}

以下のようにPictureDrawableをBitmapに変換してViewにセットしても上手く表示できる。

  canvas.drawBitmap(pictureDrawable2Bitmap(picture), new Matrix(), new Paint());
private static Bitmap pictureDrawable2Bitmap(Picture picture){
  PictureDrawable pictureDrawable = new PictureDrawable(picture);
  Bitmap bitmap = Bitmap.createBitmap(pictureDrawable.getIntrinsicWidth(),pictureDrawable.getIntrinsicHeight(), Config.ARGB_8888);
  Canvas canvas = new Canvas(bitmap);
  canvas.drawPicture(pictureDrawable.getPicture());
  return bitmap;
}

今回はdrawが頻繁にコールされるのでこの手法は取らなかった。

Fused Location Providerを試してみる

ライブラリのコピー。

cp ~/android-sdk-mac_x86/extras/google/google_play_services/libproject/google-play-services_lib/libs/* ./lib/

パーミッションの付加。

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

コード。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    mLocationClient = new LocationClient(getApplicationContext(), this, this);
    mLocationClient.connect();
}
@Override
protected void onDestroy() {
    mLocationClient.disconnect();
    super.onDestroy();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    return true;
}

@Override
public void onConnectionFailed(ConnectionResult result) {
}
@Override
public void onConnected(Bundle bundle) {
    Location location = mLocationClient.getLastLocation();
    android.util.Log.e("latitude", "" + location.getLatitude());
    android.util.Log.e("longitude", "" + location.getLongitude());
}
@Override
public void onDisconnected() {
}

参考

jarを作る

mkdir bin
javac -d bin *.java
cd bin
jar cvf hoge.jar ./

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

参考

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

ちょっと不確かなのだが[debug now]の後に[full clean import]をすれば0からインデックスが構築される。構築中は古いインデックスが使われるようだ。

■差分インポートの実行

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

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

参考

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;
        }
    }