2011年3月5日土曜日

Android Tips TextView内の文字列リンクから特定のActivtyを呼び出す ~ContentProviderとLinkifyの理解~

Linkifyのリンクからパラメーターを渡してActivityを呼び出す処理が必要になり、ググッたらContentProviderの理解が必要だとわかりました。 しかし、ContentProviderの情報は色々と分散していて理解しにくかったので、まとめます。

・ContentProviderとは?

ContentProviderを利用すると、異なるアプリケーション間でデータを共有できる。
COntentProviderを利用することで、特定のデータタイプを他のアプリケーションが利用できる。

例えば、Androidで利用されている正式なContentProviderには、電話帳がある。
電話帳には、名前、住所、電話番号がリストで表示されている。

content://contacts/people/

という特定のURIを利用することで、どのアプリケーションであってもこのデータにアクセスすることができる。
androidURIの構成は以下の通り。

スキームcontent://
オーソリティcontacts
パスpeople

WEBのURIとほぼ同じですね。オーソリティがホストにあたります。ということで、実装して試してみます。

開発用os(jar)は現在メインで開発している2.1、エミュレータは2.3で確認

1、まずはエミュレーターを立ち上げて電話帳にデータを投入


2、では、実装していきます。

2-1、

AndroidManifest.xmlに

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

を記述。

2-2、

Activityクラスを作成します。

以下ソースコードです。

public class ProviderTest extends ListActivity {

    public static final String TAG = "ProviderTest";
    
    private ArrayList<ContactPojo> list;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.provider);
    }
    
    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG, "onStart start ");
        
        ContentResolver resolver = this.getContentResolver();
        Cursor c = resolver.query(Contacts.CONTENT_URI, new String [] {Contacts.DISPLAY_NAME} , null, null, null);
        
        this.list = new ArrayList<ContactPojo>();
        while(c.moveToNext()) {
            
            ContactPojo contactPojo = new ContactPojo();
            
            contactPojo.display_name = c.getString(0);
            
            this.list.add(contactPojo);
        }
        
        if (c != null) {
            c.close();
        }
        
        Log.d(TAG, "onStart end ");
    }
    
    @Override
    public void onResume() {
        super.onResume();
        setListAdapter(new ProviderTestList(this, R.layout.provider_list, this.list));
    }
    

    class ProviderTestList extends ArrayAdapter<ArrayList<ContactPojo>> {
        private Context mContext;
        private ArrayList<ContactPojo> list;

        public ProviderTestList(Context context, int textViewResourceId, ArrayList<ContactPojo> list) {
            super(context, textViewResourceId);
            this.mContext = context;
            this.list = list;
        }
        
        @Override
        public int getCount() {
            return this.list.size();
        }

        @Override
        public long getItemId(int position) {
            return position;
        }
        
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            
            View row = convertView;
            ViewWapper viewWapper = null;
                
            if (row == null) {
                LayoutInflater inflater = getLayoutInflater(); 
                row = inflater.inflate(R.layout.provider_list, parent, false);
                viewWapper = new ViewWapper(row);
                row.setTag(viewWapper);
            } else {
                viewWapper = (ViewWapper)row.getTag();
            }
            
            
            ContactPojo contactPojo = (ContactPojo)this.list.get(position);
            
            viewWapper.getDispName().setText(contactPojo.display_name);
               
            contactPojo = null; // release
            
            return row;
            
        }
    }
        
    /**
     * ホルダーパターンView
     */
    class ViewWapper {
        
        View base;
        TextView disp_name;
        
        ViewWapper (View base) {
            this.base = base;
        }
        
        TextView getDispName() {
            if (disp_name == null) {
                disp_name = (TextView)base.findViewById(R.id.disp_name);
            }
            return disp_name;
        }
        
    }
    
}

一覧を表示するように、ListActivityで作成してます。

特に重要なのは
  • getContentResolverでContentResolverを取得していること。
  • Cursorオブジェクトを取得していること。
です。

続いて表示xmlのprovider.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
     <ListView 
         android:id="@id/android:list"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />
        
     <TextView 
        android:id="@id/android:empty" 
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" 
        android:text="データはありません"  />
        
</RelativeLayout>

加えて、リストxmlのprovider_list.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    
    <TextView 
        android:id="@+id/disp_name" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        />
        

</RelativeLayout>

上記のソースをビルドして実行します。

上記のような感じで出力されます。

ContentResolverのqueryで指定してる「Contacts.CONTENT_URI」が電話帳のデータにアクセスする為のURIです。Contacts.CONTENT_URIのURIの形式を取得してみましょう。以下が結果。
  • URIスキーム(Contacts.CONTENT_URI.getScheme()):content
  • ホスト名(Contacts.CONTENT_URI.getHost()):com.android.contacts
  • パス(Contacts.CONTENT_URI.getPath()):/contacts
となります。…というか、URI違うじゃん。調査したところ、androidのversionによって違うみたいです。
確認のために、自分でURIを作成してアクセスしてみます。

getContentResolverの後ろのコードに以下の処理を追加します。

ContentResolver resolver = this.getContentResolver();
    
    Uri.Builder builder = new Uri.Builder();
    builder.scheme("content");
    builder.authority("com.android.contacts");
    builder.path("contacts");
    
    //Cursor c = resolver.query(Contacts.CONTENT_URI, new String [] {Contacts.DISPLAY_NAME} , null, null, null);
    Cursor c = resolver.query(builder.build(), new String [] {Contacts.DISPLAY_NAME} , null, null, null);
        

ビルドして実行します。
同じ結果になりましたね。

以上の処理から理解できるのは、URIで電話帳にアクセスすることができ、結果をCursorオブジェクトで取得していることですね。

気になるのは、ContentProviderがCursorを返す仕組みになっていることです。
データベースを通さない処理の場合はどうするのでしょうか…。また、intentを利用する仕組みとどう関連があるのか…。

とにかく、ContentProviderは理解したのでLinkifyとintentを関連付けるサンプルを作成していきます。

3、Linkfyサンプルを十分に読み込んで実装していきます。

参照するのは

http://android-developers.blogspot.com/2008/03/wikinotes-for-android-routing-intents.html

です。

まずは文字列リンクを作成します。

getViewメソッドのContactPojoオブジェクトを取り出している下に、以下のコード処理を追加します。

ContactPojo contactPojo = (ContactPojo)this.list.get(position);
    
    // viewWapper.getDispName().setText(contactPojo.display_name);
    
    Pattern matcher = Pattern.compile("テスト");
    viewWapper.getDispName().setText(contactPojo.display_name);
    String viewURL =    "content://jp.co.sample.android.muzukashii/name/";
    Linkify.addLinks(viewWapper.getDispName(), matcher, viewURL);

ビルドして実行します。

おお、リンクが作成されましたね。

次はAndroidManifest.xmlに

<provider android:name="jp.co.sample.LinkTestProvider" android:authorities="jp.co.sample.android.muzukashii" />

を追記します。

  • android:authoritiesは、URIのホストと同じ意味です。URIのROOTとして扱われます。
  • android:nameはjavaクラスのフルパスを指定します。

では、呼び出されるLinkTestProviderクラスを作成していきましょう。このクラスでは、ContentProviderクラスを継承します。

注意すべき点は、updateやinsertをOverrideする必要があるということです。なんかずさんな抽象化ですね。DB使わない人もいるでしょうに。

気持ち悪いけどここは無視して作成します。で、作成したのが以下のコードです。

public class LinkTestProvider extends ContentProvider {
    
    public static final String TAG = "LinkTestProvider";
    
    public static final String AUTHORITY =
            "jp.co.sample.android.muzukashii";

    
    private static final int NOTE_NAME = 2;

    
    private static final UriMatcher URI_MATCHER;


    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public String getType(Uri uri) {
        switch (URI_MATCHER.match(uri)) {
          
        case NOTE_NAME:
            
            return "hatoyama.kan.owawa/minsyu.kiero";

        default:
            throw new IllegalArgumentException("Unknown URL " + uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public boolean onCreate() {
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        return 0;
    }
    
    static {

        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
        URI_MATCHER.addURI(AUTHORITY, "name/*",
                           NOTE_NAME);

    }

}

上記のコードで重要なのは、staticとgetTypeメソッドです。
  • staticでUriMatcherにURIを作成しています。
  • 投げるURIに応じたUriMatcherを全て作成します。
URIはProviderTestのgetViewメソッドに追加した以下のソースと対応しています。

Pattern matcher = Pattern.compile("テスト");
    viewWapper.getDispName().setText(contactPojo.display_name);
    String viewURL =    "content://jp.co.sample.android.muzukashii/name/";
    Linkify.addLinks(viewWapper.getDispName(), matcher, viewURL);

getTypeメソッドで、Uriを振り分ける処理を行っています。
色々なパターンで分岐可能なので、以下の公式サイトのソースを参考にしてみてください。

http://developer.android.com/reference/android/content/UriMatcher.html

上記のソースではcase NOTE_NAME:に処理が入り、「hatoyama.kan.owawa/minsyu.kiero」文字列が返却されます。

この文字列はMIMEタイプを表しています。なので、このMIMEタイプによって呼び出されるActivityクラスを作成するようにします。

作成したのが以下のクラスです。

public class GoalActivity extends Activity {
    
    private TextView sendparam;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.goal);
        
        this.sendparam = (TextView)findViewById(R.id.sendparam);
        
        Intent intent = getIntent();
        Uri uri = intent.getData();

        if (uri != null) {
            this.sendparam.setText(uri.getPath());
        }

    }
    
    @Override
    public void onStart() {
        super.onStart();
    }
    
    @Override
    public void onResume() {
        super.onResume();
    }
}

上記の処理では、URIのパスを取得して画面に表示しています。

続いて表示xmlのgoal.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    
    <TextView 
        android:id="@+id/sendparam" 
        android:layout_width="fill_parent" 
        android:layout_height="wrap_content" 
        />
        
</LinearLayout>

最後にandroidManifest.xmlにGoalActivityのintentを設定します。

<activity android:name=".GoalActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:mimeType="hatoyama.kan.owawa/minsyu.kiero"/>
            </intent-filter>
        </activity>

intent-filterを設定します。android:mimeTypeは、LinkTestProviderクラスのgetTypeメソッドで返すMIMEタイプ文字列に対応します。

では、上記のソースをビルドして実行します。
続いて、リンク文字をタップします。

おお。見事にActvityが呼び出されて画面遷移できましたね。

URIのpathもきちんと取得できています。

上記の処理から理解できるのは、
  • 文字リンクのパターンを利用して特定のURIを作成することで、activityの呼び出しを制御できる。
  • Providerクラスで受けとったURIをgetTypeメソッドで分岐し、任意のactivityを呼び出すことが出来る。様々なパラメーターも付加出来る。
これまでやってきたことを応用することで、WEBのような処理を行うUIが色々作れそうですね。

かなり長くなってしまいましたが、これで終了です。お疲れ様でした。

android開発にそれなりに通じていないと、内容的に少し難しかったかもしれませんが、すごく役に立つパターンだと思うので、是非勉強してみてください。

みなさんが素晴らしいアプリを開発して、僕を唸らせてくれることを期待しています。

でわ。

追伸

URIの概念がよくわからないという人は以下の書籍がオススメです。WEB開発に携わるエンジニアは是非是非読んでもらいたい名著です。

この記事がお役にたちましたらシェアをお願いします

このエントリーをはてなブックマークに追加

0 件のコメント:

コメントを投稿

Related Posts Plugin for WordPress, Blogger...