@blog.justoneplanet.info

日々勉強

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"

参考

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環境とは別の環境で実行される。メソッドの実行順序を指定しているのが良くない。

Mapをクエリ形式の文字列に変換する

今月は1つも記事を書いてなかったので間に合わせでメモを書く。

// key1 : value1, key2 : value2
ArrayList<NameValuePair> nameValuePair = new ArrayList<NameValuePair>();
Iterator<String> it = data.keySet().iterator();
while (it.hasNext()) {
    String key = it.next();
    nameValuePair.add(new BasicNameValuePair(key, data.get(key)));
}
mQuery = URLEncodedUtils.format(nameValuePair, "UTF-8");
// key1=value1&key2=value2

AndroidでGCMを使ってみる

■クライアント側の実装

C2DMと対して変わらない。sender IDがメールアドレスでなくなったのは嬉しい。

gcm.jar

SDK/extras/google/の中からgcm.jarをlibsディレクトリの中にコピーする。プロジェクトをrefreshすれば現行のADTは勝手に組み込む。

manifest.xml

Android 2.2以降でないとダメなので以下のようにminSdkVersionを8とする。

<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="15"/>

permissionを追加する。

<uses-permission android:name="android.permission.INTERNET" /> 
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" />

アプリケーションがGCMを受け取るために以下の記述を付加する。

<permission
    android:name="org.sample.permission.C2D_MESSAGE"
    android:protectionLevel="signature" />
<uses-permission android:name="org.sample.permission.C2D_MESSAGE" />

receiverを追加する。

<receiver
    android:name="com.google.android.gcm.GCMBroadcastReceiver"
    android:permission="com.google.android.c2dm.permission.SEND" >
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="org.sample" />
    </intent-filter>
</receiver>

GCMIntentService

GCMBaseIntentServiceを継承したクラスを作る。

public class GCMIntentService extends GCMBaseIntentService {
    /**
     * 登録トークンを受け取った時に実行される。{@inheritDoc}
     */
    @Override
    public void onRegistered(Context context, String registration) {
        if (BuildConfig.DEBUG) Log.d("onRegistered", "registration id:" + registration);
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        Editor editor = preferences.edit();
        editor.putString(Constants.KEY_C2DM_REGISTRATION, registration);
        editor.commit();
    }
    /**
     * デバイスが登録解除した時に実行される。{@inheritDoc}
     */
    @Override
    protected void onUnregistered(Context arg0, String arg1) {
    }
    /**
     * クラウドメッセージが受信した時に実行される。{@inheritDoc}
     */
    @Override
    protected void onMessage(Context context, Intent intent) {
        if (BuildConfig.DEBUG) Log.d("onMessage", "onMessage:" + intent.getExtras().toString());
        setNotification(context);
    }
    /**
     * 登録エラーの時に実行される。
     */
    @Override
    public void onError(Context context, String errorId) {
        if (BuildConfig.DEBUG) Log.d("onError", "onError:" + errorId);
    }
    @Override
    protected boolean onRecoverableError(Context context, String errorId) {
        if (BuildConfig.DEBUG) Log.d("onError", "onError:" + errorId);
        return super.onRecoverableError(context, errorId);
    }
    
    private void setNotification(Context context) {
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        Notification notification = new Notification(
                R.drawable.ic_notification,
                "受信しましたよ",
                System.currentTimeMillis()
        );
        Intent intent = new Intent(context, HelloActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(
                context,
                100,
                intent,
                Intent.FLAG_ACTIVITY_NEW_TASK
        );
        notification.setLatestEventInfo(
                context,
                "タイトル",
                "内容",
                pendingIntent
        );
        notificationManager.notify(0, notification);// 適当なIDを指定する
    }
}

manifestファイルに以下の記述を追加する。

<service android:name=".GCMIntentService"/>

onCreateとかonResumeとかに以下の記述を付加する。

        GCMRegistrar.checkDevice(this);
        GCMRegistrar.checkManifest(this);
        final String regId = GCMRegistrar.getRegistrationId(this);
        if (regId.equals("")) {
            GCMRegistrar.register(this, "your sender id");
        }
        else {
            Log.v("onResume", "Already registered");
        }

■サーバー側の実装

node-gcmを使う。ToothlessGear / node-gcmh2soft / node-gcmのどちらでも問題ないがnodeのバージョンの関係で今回は前者を使う。後者の方がコールバック関数のerrorの引数がnodeっぽくて良いと思われる。

var gcm = require('node-gcm');
var sender  = new gcm.Sender('YOUR_API_KEY');
var registrationIds = ['device_registration_id'];

var message = new gcm.Message();
message.addData('content', 'text');
message.collapseKey    = 'hogehoge';
message.delayWhileIdle = true;
message.timeToLive = 3;

sender.send(message, registrationIds, 3, function (result) {
  console.log(result);
}); 

参考

SimpleCursorTreeAdapterでLoaderManagerを使う

以下のように実装してみた。

public class ExpandableListAdapter extends SimpleCursorTreeAdapter implements LoaderManager.LoaderCallbacks<Cursor> {
    private Context mContext;
    private LoaderManager mManager;
    
    public ExpandableListAdapter(
            Context context, ExpandableListAdapterListener listener, LoaderManager manager, Cursor groupCursor,
            int groupLayout, String[] groupFrom, int[] groupTo,
            int childLayout, String[] childFrom, int[] childTo) {
        super(context, groupCursor, groupLayout, groupFrom, groupTo, childLayout, childFrom, childTo);
        mContext  = context;
        mManager  = manager;
    }
    @Override
    protected Cursor getChildrenCursor(Cursor groupCursor) {
        final long idGroup = groupCursor.getLong(groupCursor.getColumnIndex("_id"));
        Bundle bundle = new Bundle();
        bundle.putLong("idGroup", idGroup);
        int groupPos = groupCursor.getPosition();
        if (mManager.getLoader(groupPos) != null && !mManager.getLoader(groupPos).isReset()) {
            mManager.restartLoader(groupPos, bundle, this);
        }
        else {
            mManager.initLoader(groupPos, bundle, this);
        }
        return null;
    }
    @Override
    public Loader<Cursor> onCreateLoader(int groupPos, Bundle bundle) {
        long idGroup = bundle.getLong("idGroup");
        return new CursorLoader(
                mContext,
                Provider.URI,
                new String[]{Table.ID, Table.ID_GROUP, Table.TITLE, Table.CONTEXT},
                Table.ID_GROUP + " = ?",
                new String[]{String.valueOf(idGroup)},
                Table.CREATED + " DESC"
        );
    }
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        setChildrenCursor(loader.getId(), cursor);
    }
    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
    }
}

getChildrenCursorでnullを返すとsetChildrenCursorが実行されるまで子要素が消えるのが表示上気になる。あと、getChildrenCursorでnullを返すとAndroid 1.6でクラッシュする。

Androidで圏外になった時に処理をする

■実装

以下の様なBroascastReceiverを用意する。

public class ConnectionReceiver extends BroadcastReceiver {
    private Observer mObserver;
    public ConnectionReceiver(Observer observer) {
        mObserver = observer;
    }
    @Override
    public void onReceive(Context context, Intent intent) {
        ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo info = manager.getActiveNetworkInfo();
        if (info == null) {
            mObserver.onDisconnect();
        }
        else {
            mObserver.onConnect();
        }
    }
    interface Observer {
        void onConnect();
        void onDisconnect();
    }
}

MainActivity

以下のようにActivityを実装する。

public class MainActivity extends ActionBarActivity implements ConnectionReceiver.Observer {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.talk);
        IntentFilter filter = new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE");
        mConnectionReceiver = new ConnectionReceiver(this);
        registerReceiver(mConnectionReceiver, filter);
    }
    @Override
    protected void onDestroy() {
        unregisterReceiver(mConnectionReceiver);
        super.onDestroy();
    }
    @Override
    public void onConnect() {
        Toast.makeText(getApplicationContext(), "onConnect", Toast.LENGTH_LONG).show();
    }
    @Override
    public void onDisconnect() {
        Toast.makeText(getApplicationContext(), "onDisconnect", Toast.LENGTH_LONG).show();
    }
}

android.permission.ACCESS_NETWORK_STATEがpermissionに必要になる。

AndroidでCursorLoaderを使ってみる

以下のようにしてCursorLoaderを使用する。

public class MainFragment extends ListFragment
    implements LoaderManager.LoaderCallbacks<Cursor> {
    private ListView mListView;

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        mListView = getListView();
        mListView.setCacheColorHint(0);
        mAdapter = new EmoticonSimpleCursorAdapter(
                getActivity().getApplicationContext(),
                R.layout.list,
                null,
                new String[]{Table.TITLE, Table.CONTENT},
                new int[]{R.id.list_title, R.id.list_content},
                this
        );
        setListAdapter(mAdapter);
        super.onViewCreated(view, savedInstanceState);
    }
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getLoaderManager().initLoader(0, null, this);
    }
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        getLoaderManager().destroyLoader(0);
    }
    private void reload() {
        getLoaderManager().restartLoader(0, null, this);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity().getApplicationContext(), Provider.URI, null, null, null, Table.CREATED + " DESC");
    }
    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        mAdapter.swapCursor(data);
    }
    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        mAdapter.swapCursor(null);
    }
}