Androidの連絡先からvCardを作成する

# 概要
`ContactsContract.Contacts.CONTENT_VCARD_URI`を使って連絡先をvCard形式で取得します。

https://developer.android.com/reference/android/provider/ContactsContract.Contacts#CONTENT_URI

# 詳細
Androidには、連絡先からvCard形式のデータを取得するContent URIがあるので、それを利用して連絡先をvCardにして取得を行います。
利用するには `READ_CONTACTS` のパーミッションが必要になるので適宜取得してください。

## vCardを取得したい連絡先を選択

まずはvCard形式で取得したい連絡先を選択します。おそらく最も手軽であろう連絡先読み込みIntentで連絡先をリストから選択して、その結果を受け取るようにします。

https://developer.android.com/guide/topics/providers/contacts-provider?hl=ja#Intents

```Kotlin
        val pickContact = registerForActivityResult(ActivityResultContracts.PickContact()) {

        }
        pickContact.launch()
```

このコードで、連絡先アクティビティが開き、連絡先を指定したらregisterForActivityResultのコールバックにURIが戻ってきます。

## 連絡先からLOOKUP_KEYを取得

次に、連絡先から、vCardの取得に必要なLOOKUP_KEYを取得します。
取得したURI(it)でLOOKUP_KEY取得用のCursorを作り、Cursorから目的の連絡先のLOOKUP_KEYを取得します。

```Kotlin
        val pickContact = registerForActivityResult(ActivityResultContracts.PickContact()) {
            val projection = arrayOf(
                ContactsContract.Contacts.LOOKUP_KEY,
            )

            val cursorForLookupKey = contentResolver.query(
                it!!,
                projection,
                null,
                null,
                null
            )
            var lookupKey: String = ""
            cursorForLookupKey?.apply {
                val index: Int = getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)
                while (moveToNext()) {
                    lookupKey = getString(index)
                }
            }

            cursorForLookupKey?.close()
        }
        pickContact.launch()
```

## vCard URIからvCardを読み込み

そして、取得したLOOKUP_KEYと合わせてvCard取得用のURIを作成します。
この作成には `Uri#withAppendedPath(Uri, String)` を使います。

vCard用のURIが作成できたら、`ContentResolver#openAssetFileDescriptor(Uri, String)`でassetFileDescriptorを取得し、FileInputStreamをassetFileDescriptorで作成して読み込みます。

assetFileDescriptorのlengthは `UNKNOWN_LENGTH` になっている場合があるので、FileInputStreamを1バイトずつ読み込みます。

以下、vCard取得する処理の全体像になります。

```Kotlin
        val pickContact = registerForActivityResult(ActivityResultContracts.PickContact()) {
            val projection = arrayOf(
                ContactsContract.Contacts.LOOKUP_KEY,
            )

            val cursorForLookupKey = contentResolver.query(
                it!!,
                projection,
                null,
                null,
                null
            )
            var lookupKey: String = ""
            cursorForLookupKey?.apply {
                val index: Int = getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)
                while (moveToNext()) {
                    lookupKey = getString(index)
                }
            }

            cursorForLookupKey?.close()

            // 追加
            val vcardUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI,
                lookupKey)
            val assetFileDescriptor = contentResolver.openAssetFileDescriptor(vcardUri,"r")
            val fileDescriptor = assetFileDescriptor?.fileDescriptor
            val input = FileInputStream(fileDescriptor)

            var bytes = byteArrayOf()
            var nextByte = 0
            while (true) {
                nextByte = input.read()
                if (nextByte == -1) {
                    break
                }
                bytes += nextByte.toByte()
            }
            val vCard = String(bytes)
            Log.d(TAG,"vCard ${vCard}")
            assetFileDescriptor?.close()
        }
        pickContact.launch()
```

## vCardのデータについて

出力されたvCardはこんな感じになります。QUOTED-PRINTABLEでエンコードされている点に注意しましょう。

```
BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E7=B9=94=E7=94=B0;=E4=BF=A1=E9=95=B7;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E7=B9=94=E7=94=B0=E4=BF=A1=E9=95=B7
X-PHONETIC-FIRST-NAME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E3=81=AE=E3=81=B6=E3=81=AA=E3=81=8C
X-PHONETIC-LAST-NAME;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E3=81=8A=E3=81=A0
TEL;WORK:090-222-xxxxx
EMAIL;WORK:nobu@example.com
ORG:
PHOTO;ENCODING=BASE64;JPEG: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


END:VCARD
```

vCard文字列に関する公式のAPIは存在しないと思うので、[ex-vcard](https://github.com/mangstadt/ez-vcard)などで確認するのが簡単だと思います。

https://github.com/mangstadt/ez-vcard