@blog.justoneplanet.info

日々勉強

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

ContentProviderを使ってSQLiteに複数行Insertする

■ContentProvider

以下のようにbulkInsertメソッドをOverrideする。

public class Provider extends ContentProvider {
    public static final Uri URI = Uri.parse("content://com.example.android.hoge/");
    @Override
    public String getType(Uri uri) {
        return null;
    }
    @Override
    public boolean onCreate() {
        return false;
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getReadableDatabase();
        Cursor cursor = sdb.query(
                Table.TABLENAME,
                new String[]{Table.ID, Table.DATA, Table.CREATED},
                selection,
                selectionArgs,
                null,
                null,
                sortOrder,
                null
        );
        return cursor;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        sdb.insert(Table.TABLENAME, null, values);
        getContext().getContentResolver().notifyChange(uri, null);
        return uri;
    }
    
    /**
     * superクラスでinsertを普通に呼ぶようなのでcompileStatementの処理を自前で書く
     */
    @Override
    public int bulkInsert(Uri uri, ContentValues[] values) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        
        sdb.beginTransaction();
        SQLiteStatement stmt = sdb.compileStatement(
            "INSERT INTO `" + Table.TABLENAME + "`(`" + Table.DATA + "`, `" + Table.CREATED + "`) VALUES (?, ?);"
        );
        int length = values.length;
        for(int i = 0; i < length; i++){
            stmt.bindString(1, values[i].getAsString(Table.DATA));
            stmt.bindLong(2, values[i].getAsLong(Table.CREATED));
            stmt.executeInsert();
        }
        sdb.setTransactionSuccessful();
        sdb.endTransaction();
        return length;
    }
    
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.update(Table.TABLENAME, values, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Helper helper = Helper.getInstance(getContext(), null);
        SQLiteDatabase sdb = helper.getWritableDatabase();
        int rows = sdb.delete(Table.TABLENAME, selection, selectionArgs);
        getContext().getContentResolver().notifyChange(uri, null);
        return rows;
    }
    
    private static class Helper extends SQLiteOpenHelper {
        static Helper INSTANCE = null;
        private Helper(Context context, CursorFactory factory) {
            super(context, Table.FILENAME, factory, Table.VERSION);
        }
        public static Helper getInstance(Context context, CursorFactory factory) {
            if (INSTANCE == null) {
                INSTANCE = new Helper(context, factory);
            }
            return INSTANCE;
        }
        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(
                "CREATE TABLE `" + Table.TABLENAME + "`(" +
                " `" + Table.ID      + "` INTEGER PRIMARY KEY AUTOINCREMENT," +
                " `" + Table.CREATED + "` INTEGER," +
                " `" + Table.DATA    + "` TEXT" +
                ");"
            );
        }
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    }
}

super.bulkInsert(uri, values)は内部でinsert(uri, values)を複数回呼び出すようになっており、Insert文が複数回発行されて非常に遅くなる。

■クライアントコード

ContentValues[] contentValues = new ContentValues[length];
for(int i = 0; i < length; i++){
    ContentValues values = new ContentValues();
    values.put(DATA, "data_" + i);
    values.put(CREATED, new Date().getTime());
    contentValues[i] = values;
}
getApplicationContext().getContentResolver().bulkInsert(Provider.URI, contentValues);

Androidで本体の電源を入れた時にアプリを起動させる

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

public class BootBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (
                (intent.getAction().equals(Intent.ACTION_PACKAGE_REPLACED) && intent.getDataString().contains("com.my.app")) ||
                intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)
        ) {
            Intent i = new Intent(context, NotificationService.class);
            context.startService(i);
        }
    }
}

Manifestファイルに以下の記述を付加してBroadcastIntentを受け取れるようにする。

        <receiver android:name=".BootBroadcastReceiver" android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_REPLACED" />
                <data android:scheme="package" android:path="com.my.app" />
            </intent-filter>
        </receiver>

permissionを忘れずに付加する。

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

permissionを書き忘れると、Android3.1以降の端末で上手く検知できなくなる。また、SDカードにインストールしている場合は、Intentを受け取れないのでSDカードがマウントされた時にフックする必要がある。

参考