Phonebook 导出联系人到SD卡(.vcf)

2014-01-13 16:53:55 

1. 在Phonebook中导出联系人到内部存储,SD卡或者通过蓝牙、彩信、邮件等分享联系人时,通常会先将选择的联系人打包生成.vcf文件,然后将.vcf文件分享出去或者导出到存储设备上。以Phonebook中导出联系人到SD卡为例,前戏部分跳过,直奔主题。

2. 当用户选择导出联系人到SD卡时,会提示用户具体导出的路径等,然后需要用户点击“确定”button,此时启动ExportVcardThread线程执行具体的导出操作。代码的调用流程如下:

启动ExportVCardActivity,弹出一个Dialog提示用户并让用户确定,确认button的事件监听是ExportConfirmationListener, 代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private class ExportConfirmationListener implements DialogInterface.OnClickListener {
 2     private final String mFileName;
 3 
 4     public ExportConfirmationListener(String fileName) {
 5         mFileName = fileName;
 6     }
 7 
 8     public void onClick(DialogInterface dialog, int which) {
 9         if (which == DialogInterface.BUTTON_POSITIVE) {
10             mExportingFileName = mFileName;
11             progressDialogShow();
12 
13             mListenerAdapter = new ListenerThreadBridgeAdapter(ExportVCardActivity.this);
14             mActualExportThread = new ExportVcardThread(null, ExportVCardActivity.this,
15                     mFileName, mListenerAdapter, false);
16             mActualExportThread.start();
17         }
18     }
19 }
Phonebook 导出联系人到SD卡(.vcf)

注意红色部分,很简单,创建一个ExportVcardThread对象,将即将生成的.vcf文件的名称当作参数传入,同时start这个线程。

下面进入ExportVcardThread线程类一探究竟。

2. ExportVcardThread线程类

查看线程类的run()方法,有如下代码:

Phonebook 导出联系人到SD卡(.vcf)
 1 try {
 2     outputStream = new FileOutputStream(mFileName);
 3 } catch (FileNotFoundException e) {
 4     mErrorReason = mContext.getString(
 5             R.string.spb_strings_fail_reason_could_not_open_file_txt, mFileName,
 6             e.getMessage());
 7     isComposed = false;
 8     return;
 9 }
10 
11 isComposed = compose(outputStream);
Phonebook 导出联系人到SD卡(.vcf)

创建了文件输出流,可见对.vcf文件的操作将会以流的形式进行,然后调用compose(outputStream),将创建的输出流对象当作参数传入。compose()方法很大,但其实只做了两件事:(1)查询数据库获得要导出的联系人信息;(2)将联系人信息编码导出到.vcf文件,核心代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private boolean compose(OutputStream outputStream) {
 2 
 3     try {
 4         final ContentResolver cr = mContext.getContentResolver();
 5         StringBuilder selection = new StringBuilder();
 6         StringBuilder order = new StringBuilder();
 7 
 8         // exclude restricted contacts.
 9         final Uri.Builder contactsUri = ContactsContract.Contacts.CONTENT_URI.buildUpon();
10         contactsUri.appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, "");
11         c = cr.query(contactsUri.build(), CLMN, selection.toString(), null,
12                     order.toString());    (1)
13 
14         while (c.moveToNext()) {
15             if (mCanceled) {
16                 break;
17             }
18             count++;
19             lookupKeys.append(c.getString(lookupClmn));
20             if (!c.isLast() && count < VCARD_REQUEST_LIMIT) {
21                 lookupKeys.append(":");
22                 continue;
23             }
24             final Uri.Builder vcardUri = ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI.buildUpon();
25             vcardUri.appendPath(lookupKeys.toString());
26             vcardUri.appendQueryParameter("vcard_type", mVcardTypeStr);
27             Log.d("D33", "mVcardTypeStr = " + mVcardTypeStr);
28             Log.d("D33", "vcardUri.build() = " + vcardUri.build());
29             final InputStream is = cr.openInputStream(vcardUri.build());    (2)
30             if (copyStream(buff, is, outputStream) > 0) {
31                 hasActualEntry = true;
32             }
33 
34             if (mListener != null) {
35                 mListener.incrementProgressBy(count);
36             }
37             count = 0;
38             lookupKeys.setLength(0);
39         }
40     }
41     return success;
42 }
Phonebook 导出联系人到SD卡(.vcf)

(1)处代码负责query联系人,稍微提一下,

selection=_id IN (SELECT contacts._id FROM contacts,raw_contacts JOIN accounts ON account_id=accounts._id WHERE contacts.name_raw_contact_id = raw_contacts._id AND accounts.account_type != ‘com.***.sdncontacts‘ AND raw_contacts.is_restricted=0) AND in_visible_group=1,

这个就是查询联系人的条件,也就是说只有满足这个条件的联系人才会被导出,因此,不是所有联系人都会被导出的,比如Facebook联系人就不会被导出。

(2)处的几行代码主要是获得了一个输入流,cr.openInputStream(vcardUri.build()),看代码可以发现,首先是将符合条件的联系人的lookupkey全部保存到lookupKeys,并且调用vcardUri.appendPath(lookupKeys.toString()):

1 lookupKeys = 1135i3:1135i6
2 vcardUri.build() = content://com.android.contacts/contacts/as_multi_vcard/1135i3%3A1135i6?vcard_type=default

lookupKeys是将所有联系人的lookupKey连接起来,中间用“:”分隔。然后调用copyStream(buff, is, outputStream),看名字就知道作用是copy输入流到输出流,代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private int copyStream(byte[] buff, InputStream is, OutputStream os) throws IOException {
 2     int copiedLength = 0;
 3     if (is == null || os == null) {
 4         return copiedLength;
 5     }
 6 
 7     int sz = 0;
 8     do {
 9         sz = is.read(buff);
10         if (sz > 0) {
11             os.write(buff, 0, sz);
12             copiedLength += sz;
13         }
14     } while (sz > 0);
15 
16     return copiedLength;
17 }
Phonebook 导出联系人到SD卡(.vcf)

那么现在最关键的问题就是输入流是如何得到的,如何将联系人编码生成符合vcf标准的信息呢?根据上面提到的vcardUri=com.android.contacts/contacts/as_multi_vcard/1135i3%3A1135i6?vcard_type=default,发现我们需要深入Phonebook的数据库走一遭了。

3. ContactsProvider2类探索

 在ContactsProvider2.java中有如下方法,可以匹配到uri=content://com.android.contacts/contacts/as_multi_vcard,代码:

Phonebook 导出联系人到SD卡(.vcf)
 1 private AssetFileDescriptor openAssetFileInner(Uri uri, String mode)
 2         throws FileNotFoundException {
 3 
 4     final boolean writing = mode.contains("w");
 5 
 6     final SQLiteDatabase db = mDbHelper.get().getDatabase(writing);
 7 
 8     int match = sUriMatcher.match(uri);
 9     switch (match) {
10 
11         case CONTACTS_AS_MULTI_VCARD: { // 匹配content://com.android.contacts/contacts/as_multi_vcard
12             final String lookupKeys = uri.getPathSegments().get(2);
13             final String[] loopupKeyList = lookupKeys.split(":");
14             final StringBuilder inBuilder = new StringBuilder();
15             Uri queryUri = Contacts.CONTENT_URI;
16             int index = 0;
17 
18             for (String lookupKey : loopupKeyList) {
19                 if (index == 0) {
20                     inBuilder.append("(");
21                 } else {
22                     inBuilder.append(",");
23                 }
24                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
25                 inBuilder.append(contactId);
26                 index++;
27             }
28             inBuilder.append(‘)‘);
29             final String selection = Contacts._ID + " IN " + inBuilder.toString();
30 
31             // When opening a contact as file, we pass back contents as a
32             // vCard-encoded stream. We build into a local buffer first,
33             // then pipe into MemoryFile once the exact size is known.
34             final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
35             outputRawContactsAsVCard(queryUri, localStream, selection, null);
36             return buildAssetFileDescriptor(localStream);
37         }
38     }
39 }
Phonebook 导出联系人到SD卡(.vcf)

这里首先通过lookupkey取得对应联系人的ID,然后再次生成查询条件selection,然后调用buildAssetFileDescriptor(localStream)方法,这个方法简单的对localStream做了一下封装,然后返回,那么localStream到底是怎么生成的?

进入outputRawContactsAsVCard(queryUri, localStream, selection, null)方法,代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private void outputRawContactsAsVCard(Uri uri, OutputStream stream,
 2         String selection, String[] selectionArgs) {
 3     final Context context = this.getContext();
 4     int vcardconfig = VCardConfig.VCARD_TYPE_DEFAULT;
 5     if(uri.getBooleanQueryParameter(
 6             Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, false)) {
 7         vcardconfig |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
 8     }    
 9     final VCardComposer composer =
10             new VCardComposer(context, vcardconfig, false);  
11     try {
12         writer = new BufferedWriter(new OutputStreamWriter(stream));
13         if (!composer.init(uri, selection, selectionArgs, null, rawContactsUri)) {   (1) 初始化composer
14             Log.w(TAG, "Failed to init VCardComposer");
15             return;
16         }    
17 
18         while (!composer.isAfterLast()) {
19             writer.write(composer.createOneEntry());    (2)真正编码联系人信息
20         }    
21     }
22 }
Phonebook 导出联系人到SD卡(.vcf)

(1)处代码对composer做了初始化,传入的参数有uri,selection等;(2)处调用createOneEntry()方法,做具体生成vcf的操作。

4. 进入VCardComposer类,这个类位于frameworks/opt/vcard/java/com/android/vcard/VCardComposer.java,当然,不同的厂商为了满足自己的需求或许会对这个类进行扩展甚至重写。

Phonebook 导出联系人到SD卡(.vcf)
 1 public boolean init(final String selection, final String[] selectionArgs, final boolean isMyProfile,
 2         final String sortOrder) {
 3     if (mIsCallLogComposer) {
 4         mCursor = mContentResolver.query(CallLog.Calls.CONTENT_URI, sCallLogProjection,
 5                 selection, selectionArgs, sortOrder);
 6     } else if (isMyProfile) {
 7         mCursor = mContentResolver.query(Profile.CONTENT_URI, sContactsProjection,
 8                 selection, selectionArgs, sortOrder);
 9     } else {
10         mCursor = mContentResolver.query(Contacts.CONTENT_URI, sContactsProjection,
11                 selection, selectionArgs, sortOrder);
12     }
13 
14     if (mCursor == null) {
15         mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
16         return false;
17     }
18 
19     return true;
20 }
Phonebook 导出联系人到SD卡(.vcf)

init()方法里面根据传入的参数,query数据库,得到要导出的联系人。看下面createOneEntry()方法:

Phonebook 导出联系人到SD卡(.vcf)
 1 public boolean createOneEntry() {
 2     if (mCursor == null || mCursor.isAfterLast()) {
 3         mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
 4         return false;
 5     }
 6     String name = null;
 7     String vcard;
 8     try {
 9         if (mIsCallLogComposer) {
10             vcard = createOneCallLogEntryInternal();
11         } else if (mIdColumn >= 0) {
12             mContactsPhotoId = mCursor.getString(mCursor.getColumnIndex(Contacts.PHOTO_ID));
13             vcard = createOneEntryInternal(mCursor.getString(mIdColumn));   (1)
14         } else {
15             Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
16             return false;
17         }
18     }
19 
20     return true;
21 }
Phonebook 导出联系人到SD卡(.vcf)

注意红色代码,进入createOneEntryInternal()方法看看,代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private String createOneEntryInternal(final String contactId, final boolean aForceEmpty) {
 2     final Map<String, List<ContentValues>> contentValuesListMap =
 3             new HashMap<String, List<ContentValues>>();
 4     final String selection = Data.CONTACT_ID + "=?";
 5     final String[] selectionArgs = new String[] {contactId};
 6     final Uri uri;
 7     if (Long.valueOf(contactId)>Profile.MIN_ID) {
 8         uri = RawContactsEntity.PROFILE_CONTENT_URI.buildUpon()
 9                 .appendQueryParameter(ContactsContract.RawContactsEntity.FOR_EXPORT_ONLY, "1")
10                 .build();
11     } else {
12         uri = RawContactsEntity.CONTENT_URI.buildUpon()
13                 .appendQueryParameter(ContactsContract.RawContactsEntity.FOR_EXPORT_ONLY, "1")
14                 .build();
15     }
16 
17     appendStructuredNames(builder, contentValuesListMap);
18     appendNickNames(builder, contentValuesListMap);
19     appendPhones(builder, contentValuesListMap);
20     appendEmails(builder, contentValuesListMap);
21     appendPostals(builder, contentValuesListMap);
22     appendIms(builder, contentValuesListMap);
23     appendWebsites(builder, contentValuesListMap);
24     appendBirthday(builder, contentValuesListMap);
25     appendOrganizations(builder, contentValuesListMap);
26     if (mNeedPhotoForVCard) {
27         appendPhotos(builder, contentValuesListMap);
28     }
29     appendNotes(builder, contentValuesListMap);
30     appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD);
31 
32     return builder.toString();
33 }
Phonebook 导出联系人到SD卡(.vcf)

我们发现调用了好多append***()系列的方法,而且传入的参数都是contentValuesListMap,以appendStructuredNames()方法为例,其又调用了appendStructuredNamesInternal()方法,代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private void appendStructuredNamesInternal(final StringBuilder builder,
 2         final List<ContentValues> contentValuesList) {
 3     final String familyName = primaryContentValues
 4             .getAsString(StructuredName.FAMILY_NAME);
 5     final String middleName = primaryContentValues
 6             .getAsString(StructuredName.MIDDLE_NAME);
 7     final String givenName = primaryContentValues
 8             .getAsString(StructuredName.GIVEN_NAME);
 9     final String prefix = primaryContentValues
10             .getAsString(StructuredName.PREFIX);
11     final String suffix = primaryContentValues
12             .getAsString(StructuredName.SUFFIX);
13     final String displayName = primaryContentValues
14             .getAsString(StructuredName.DISPLAY_NAME);
15 
16     if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
17         final String encodedFamily;
18         final String encodedGiven;
19         final String encodedMiddle;
20         final String encodedPrefix;
21         final String encodedSuffix;
22 
23         if (reallyUseQuotedPrintableToName) {   (1)
24             encodedFamily = encodeQuotedPrintable(familyName);
25             encodedGiven = encodeQuotedPrintable(givenName);
26             encodedMiddle = encodeQuotedPrintable(middleName);
27             encodedPrefix = encodeQuotedPrintable(prefix);
28             encodedSuffix = encodeQuotedPrintable(suffix);
29         } else {
30             encodedFamily = escapeCharacters(familyName);
31             encodedGiven = escapeCharacters(givenName);
32             encodedMiddle = escapeCharacters(middleName);
33             encodedPrefix = escapeCharacters(prefix);
34             encodedSuffix = escapeCharacters(suffix);
35         }
36 
37         // N property. This order is specified by vCard spec and does not depend on countries.
38         builder.append(VCARD_PROPERTY_NAME);  // VCARD_PROPERTY_NAME = "N"
39         if (shouldAppendCharsetAttribute(Arrays.asList(
40                 familyName, givenName, middleName, prefix, suffix))) {
41             builder.append(VCARD_ATTR_SEPARATOR);
42             builder.append(mVCardAttributeCharset);
43         }
44         if (reallyUseQuotedPrintableToName) {
45             builder.append(VCARD_ATTR_SEPARATOR);
46             builder.append(VCARD_ATTR_ENCODING_QP);
47         }
48 
49         builder.append(VCARD_DATA_SEPARATOR);  // VCARD_DATA_SEPARATOR = ":";
50         builder.append(encodedFamily);
51         builder.append(VCARD_ITEM_SEPARATOR);
52         builder.append(encodedGiven);
53         builder.append(VCARD_ITEM_SEPARATOR);
54         builder.append(encodedMiddle);
55         builder.append(VCARD_ITEM_SEPARATOR);
56         builder.append(encodedPrefix);
57         builder.append(VCARD_ITEM_SEPARATOR);
58         builder.append(encodedSuffix);
59         builder.append(VCARD_COL_SEPARATOR);
60 
61         final String fullname = displayName;
62         final boolean reallyUseQuotedPrintableToFullname =
63             mUsesQPToPrimaryProperties &&
64             !VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname);
65 
66         final String encodedFullname;
67         if (reallyUseQuotedPrintableToFullname) {
68             encodedFullname = encodeQuotedPrintable(fullname);  // VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"
69         } else if (!mIsDoCoMo) {
70             encodedFullname = escapeCharacters(fullname);
71         } else {
72             encodedFullname = removeCrLf(fullname);
73         }
74 
75         // FN property
76         builder.append(VCARD_PROPERTY_FULL_NAME);  // VCARD_PROPERTY_FULL_NAME = "FN"
77         if (shouldAppendCharsetAttribute(fullname)) {
78             builder.append(VCARD_ATTR_SEPARATOR);
79             builder.append(mVCardAttributeCharset);
80         }
81         if (reallyUseQuotedPrintableToFullname) {
82             builder.append(VCARD_ATTR_SEPARATOR);
83             builder.append(VCARD_ATTR_ENCODING_QP);
84         }
85         builder.append(VCARD_DATA_SEPARATOR);
86         builder.append(encodedFullname);
87         builder.append(VCARD_COL_SEPARATOR);
88     }
89 }
Phonebook 导出联系人到SD卡(.vcf)

这个方法很长,我只是截取了其中一部分我们分析需要的代码,该方法的作用如下:

1. 获取姓名的各个部分,并对其编码。

2. (1)处判断, 如果姓名是中文,那么if (reallyUseQuotedPrintableToName) 成立。

看红色代码,就是.vcf文件中信息编码的header部分,比如“N”,“FN”, “ENCODING=QUOTED-PRINTABLE”等,如下:

此联系人姓名:大卫 号码:9999999

Phonebook 导出联系人到SD卡(.vcf)
1 BEGIN:VCARD
2 VERSION:2.1
3 N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:;=E5=A4=A7=E5=8D=AB;;;  //姓名部分
4 FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E5=A4=A7=E5=8D=AB  //姓名部分
5 TEL;HOME;VOICE:9999999
6 END:VCARD
Phonebook 导出联系人到SD卡(.vcf)

现在应该清楚了吧,其实.vcf编码说白了就是组合而已,将姓名,号码等信息取出来,然后和相应的header组合在一起,就够成了一个符合标准的.vcf信息。

在前一篇文章中我们提到过,像电话号码、email等信息是原文保存的,如果姓名是英语,也是原文保存,但是中文姓名比较麻烦,就像这个联系人一样,“大卫”被编码成“E5=A4=A7=E5=8D=AB”,那这个编码是怎么回事呢?我们还得看看encodedFamily = encodeQuotedPrintable(familyName),进入encodeQuotedPrintable(familyName)方法,代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private String encodeQuotedPrintable(String str) {
 2     if (TextUtils.isEmpty(str)) {
 3         return "";
 4     }
 5     {
 6         // Replace "\n" and "\r" with "\r\n".
 7         StringBuilder tmpBuilder = new StringBuilder();
 8         int length = str.length();
 9         for (int i = 0; i < length; i++) {
10             char ch = str.charAt(i);
11             if (ch == ‘\r‘) {
12                 if (i + 1 < length && str.charAt(i + 1) == ‘\n‘) {
13                     i++;
14                 }
15                 tmpBuilder.append("\r\n");
16             } else if (ch == ‘\n‘) {
17                 tmpBuilder.append("\r\n");
18             } else {
19                 tmpBuilder.append(ch);
20             }
21         }
22         str = tmpBuilder.toString();
23     }
24 
25     final StringBuilder tmpBuilder = new StringBuilder();
26     int index = 0;
27     int lineCount = 0;
28     byte[] strArray = null;
29 
30     try {
31         strArray = str.getBytes(mCharsetString);  (1)
32     }
33     while (index < strArray.length) {
34         tmpBuilder.append(String.format("=%02X", strArray[index]));  (2)
35         Log.d("D44", "tmpBuilder = " + tmpBuilder.toString());
36         index += 1;
37         lineCount += 3;
38 
39         if (lineCount >= QUATED_PRINTABLE_LINE_MAX) {
40             // Specification requires CRLF must be inserted before the
41             // length of the line
42             // becomes more than 76.
43             // Assuming that the next character is a multi-byte character,
44             // it will become
45             // 6 bytes.
46             // 76 - 6 - 3 = 67
47             tmpBuilder.append("=\r\n");
48             lineCount = 0;
49         }
50     }
51 
52     return tmpBuilder.toString();
53 }
Phonebook 导出联系人到SD卡(.vcf)

(1)处代码调用str.getBytes(mCharsetString),返回一个字符串的byte数组,编码格式是“UTF-8”;

(2)处代码用到了一个循环,对每一个byte进行编码,编码姓名为“大卫”的log如下:

Phonebook 导出联系人到SD卡(.vcf)
1 D/D44     (32766): str = 大卫
2 D/D44     (32766): 1str = 大卫
3 D/D44     (32766): mCharsetString = UTF-8
4 D/D44     (32766): tmpBuilder = =E5
5 D/D44     (32766): tmpBuilder = =E5=A4
6 D/D44     (32766): tmpBuilder = =E5=A4=A7
7 D/D44     (32766): tmpBuilder = =E5=A4=A7=E5
8 D/D44     (32766): tmpBuilder = =E5=A4=A7=E5=8D
9 D/D44     (32766): tmpBuilder = =E5=A4=A7=E5=8D=AB
Phonebook 导出联系人到SD卡(.vcf)

传进来的familyName是“大卫”,编码的过程如log所示。

OK,现在我们终于明白了是如何生成.vcf文件的了,对于可以用英文字符表示的信息,加header信息,原文保存;对于中文或者其他语言表示的信息,进行编码,编码规则如下:

Phonebook 导出联系人到SD卡(.vcf)
1 String str = "大卫";
2 byte[] strArray = null;
3 strArray = str.getBytes("UTF-8");
4 int index = 0;
5 while (index < strArray.length) {
6     System.out.println(String.format("=%02X", strArray[index]));
7     index++;
8 }
Phonebook 导出联系人到SD卡(.vcf)

这段代码是我自己写的Demo,编码规则很简单,是不是?最关键的一句是“String.format("=%02X", strArray[index])”,至于这个方法的用法,请问度娘~

现在剩最后一个问题,联系人头像是怎么编码的呢?代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1     private void appendPhotos(final StringBuilder builder,
 2             final Map<String, List<ContentValues>> contentValuesListMap) {
 3         final List<ContentValues> contentValuesList = contentValuesListMap
 4                 .get(Photo.CONTENT_ITEM_TYPE);
 5         if (contentValuesList != null) {
 6             for (ContentValues contentValues : contentValuesList) {
 7 
 8                 // When photo id don‘t equal the photo id showned in contact,
 9                 // the photo data don‘t add to VCard.
10                 if(mContactsPhotoId != null &&
11                         (!mContactsPhotoId.equals(contentValues.getAsString(Data._ID)))){
12                     continue;
13                 }
14 
15                 byte[] data = contentValues.getAsByteArray(Photo.PHOTO);  (1)
16                 if (data == null) {
17                     continue;
18                 }
19                 String photoType;
20                 // Use some heuristics for guessing the format of the image.
21                 // TODO: there should be some general API for detecting the file format.
22                 if (data.length >= 3 && data[0] == ‘G‘ && data[1] == ‘I‘
23                         && data[2] == ‘F‘) {
24                     photoType = "GIF";
25                 } else if (data.length >= 4 && data[0] == (byte) 0x89
26                         && data[1] == ‘P‘ && data[2] == ‘N‘ && data[3] == ‘G‘) {
27                     // PNG is not officially supported by vcard-2.1 and many FOMA phone can‘t decode PNG.
28                     // To solve IOT issue, convert PNG files to JPEG.
29                     photoType = "PNG";
30                 } else if (data.length >= 2 && data[0] == (byte) 0xff
31                         && data[1] == (byte) 0xd8) {
32                     photoType = "JPEG";
33                 } else {
34                     Log.d(LOG_TAG, "Unknown photo type. Ignore.");
35                     continue;
36                 }
37                 byte[] newData = convertToSmallJpg(data, photoType);
38                 if (newData != null) {
39                     data = newData;
40                     photoType = "JPEG";
41                 }
42                 final String photoString = VCardUtils.encodeBase64(data);  (2)
43                 if (photoString.length() > 0) {
44                     appendVCardPhotoLine(builder, photoString, "TYPE=" + photoType);  (3) 添加TYPE信息
45                 }
46             }
47         }
48     }
Phonebook 导出联系人到SD卡(.vcf)

看3处红色代码:

(1)得到头像的byte数组;

(2)将byte数组编码,并返回为String类型;

(3)保存为.vcf信息,比如PHOTO;ENCODING=BASE64;TYPE=JPEG等头信息。

VCardUtils.encodeBase64(data)方法代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private static final char[] ENCODE64 = {
 2         ‘A‘,‘B‘,‘C‘,‘D‘,‘E‘,‘F‘,‘G‘,‘H‘,‘I‘,‘J‘,‘K‘,‘L‘,‘M‘,‘N‘,‘O‘,‘P‘,
 3         ‘Q‘,‘R‘,‘S‘,‘T‘,‘U‘,‘V‘,‘W‘,‘X‘,‘Y‘,‘Z‘,‘a‘,‘b‘,‘c‘,‘d‘,‘e‘,‘f‘,
 4         ‘g‘,‘h‘,‘i‘,‘j‘,‘k‘,‘l‘,‘m‘,‘n‘,‘o‘,‘p‘,‘q‘,‘r‘,‘s‘,‘t‘,‘u‘,‘v‘,
 5         ‘w‘,‘x‘,‘y‘,‘z‘,‘0‘,‘1‘,‘2‘,‘3‘,‘4‘,‘5‘,‘6‘,‘7‘,‘8‘,‘9‘,‘+‘,‘/‘
 6     };
 7 
 8     static public String encodeBase64(byte[] data) {
 9         if (data == null) {
10             return "";
11         }
12 
13         char[] charBuffer = new char[(data.length + 2) / 3 * 4];
14         int position = 0;
15         int _3byte = 0;
16         for (int i=0; i<data.length-2; i+=3) {
17             _3byte = ((data[i] & 0xFF) << 16) + ((data[i+1] & 0xFF) << 8) + (data[i+2] & 0xFF);
18             charBuffer[position++] = ENCODE64[_3byte >> 18];
19             charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F];
20             charBuffer[position++] = ENCODE64[(_3byte >>  6) & 0x3F];
21             charBuffer[position++] = ENCODE64[_3byte & 0x3F];
22         }
23         switch(data.length % 3) {
24         case 1: // [111111][11 0000][0000 00][000000]
25             _3byte = ((data[data.length-1] & 0xFF) << 16);
26             charBuffer[position++] = ENCODE64[_3byte >> 18];
27             charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F];
28             charBuffer[position++] = PAD;
29             charBuffer[position++] = PAD;
30             break;
31         case 2: // [111111][11 1111][1111 00][000000]
32             _3byte = ((data[data.length-2] & 0xFF) << 16) + ((data[data.length-1] & 0xFF) << 8);
33             charBuffer[position++] = ENCODE64[_3byte >> 18];
34             charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F];
35             charBuffer[position++] = ENCODE64[(_3byte >>  6) & 0x3F];
36             charBuffer[position++] = PAD;
37             break;
38         }
39 
40         return new String(charBuffer);
41     }
Phonebook 导出联系人到SD卡(.vcf)

这里就不做分析了,(3)处的appendVCardPhotoLine()方法代码如下:

Phonebook 导出联系人到SD卡(.vcf)
 1 private void appendVCardPhotoLine(final StringBuilder builder,
 2             final String encodedData, final String photoType) {
 3         StringBuilder tmpBuilder = new StringBuilder();
 4         tmpBuilder.append(VCARD_PROPERTY_PHOTO);  // VCARD_PROPERTY_PHOTO = "PHOTO"
 5         tmpBuilder.append(VCARD_ATTR_SEPARATOR);
 6         if (mIsV30) {
 7             tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30);
 8         } else {
 9             tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21);
10         }
11         tmpBuilder.append(VCARD_ATTR_SEPARATOR);
12         appendTypeAttribute(tmpBuilder, photoType);
13         tmpBuilder.append(VCARD_DATA_SEPARATOR);
14         tmpBuilder.append(encodedData);
15 
16         final String tmpStr = tmpBuilder.toString();
17         tmpBuilder = new StringBuilder();
18         int lineCount = 0;
19         int length = tmpStr.length();
20         for (int i = 0; i < length; i++) {
21             tmpBuilder.append(tmpStr.charAt(i));
22             lineCount++;
23             if (lineCount > BASE64_LINE_MAX) {
24                 tmpBuilder.append(VCARD_COL_SEPARATOR);
25                 tmpBuilder.append(VCARD_WS);
26                 lineCount = 0;
27             }
28         }
29         builder.append(tmpBuilder.toString());
30         builder.append(VCARD_COL_SEPARATOR);
31         builder.append(VCARD_COL_SEPARATOR);
32     }
Phonebook 导出联系人到SD卡(.vcf)

红色代码,标识的就是photo,至于其他的,和姓名类似,就不展开说了。

OK,终于结束了,战线太长了,甚至有点凌乱,不过希望读者沿着主线看,不要太在乎细节,比如那个方法是怎么调用的或者这个参数是什么时候初始化的。

最后两个方法,没有仔细讲,感兴趣的读者自己去看吧,原理在前面就说清楚了。

现在回答一下上一篇文章中(http://www.cnblogs.com/wlrhnh/p/3515269.html)遗留的问题,就是如何解码的问题,我们现在知道如何编码,那如何解码还不简单吗?呵呵

最后将VCardComposer.java代码贴上来,感兴趣的可以自己看看。

Phonebook 导出联系人到SD卡(.vcf)
  1 /*
  2  * Copyright (C) 2009 The Android Open Source Project
  3  *
  4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5  * use this file except in compliance with the License. You may obtain a copy of
  6  * the License at
  7  *
  8  * http://www.apache.org/licenses/LICENSE-2.0
  9  *
 10  * Unless required by applicable law or agreed to in writing, software
 11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 13  * License for the specific language governing permissions and limitations under
 14  * the License.
 15  */
 16 package com.android.vcard;
 17 
 18 import android.content.ContentResolver;
 19 import android.content.ContentValues;
 20 import android.content.Context;
 21 import android.content.Entity;
 22 import android.content.Entity.NamedContentValues;
 23 import android.content.EntityIterator;
 24 import android.database.Cursor;
 25 import android.database.sqlite.SQLiteException;
 26 import android.net.Uri;
 27 import android.provider.ContactsContract.CommonDataKinds.Email;
 28 import android.provider.ContactsContract.CommonDataKinds.Event;
 29 import android.provider.ContactsContract.CommonDataKinds.Im;
 30 import android.provider.ContactsContract.CommonDataKinds.Nickname;
 31 import android.provider.ContactsContract.CommonDataKinds.Note;
 32 import android.provider.ContactsContract.CommonDataKinds.Organization;
 33 import android.provider.ContactsContract.CommonDataKinds.Phone;
 34 import android.provider.ContactsContract.CommonDataKinds.Photo;
 35 import android.provider.ContactsContract.CommonDataKinds.Relation;
 36 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
 37 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
 38 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 39 import android.provider.ContactsContract.CommonDataKinds.Website;
 40 import android.provider.ContactsContract.Contacts;
 41 import android.provider.ContactsContract.Data;
 42 import android.provider.ContactsContract.RawContacts;
 43 import android.provider.ContactsContract.RawContactsEntity;
 44 import android.provider.ContactsContract;
 45 import android.text.TextUtils;
 46 import android.util.Log;
 47 
 48 import java.lang.reflect.InvocationTargetException;
 49 import java.lang.reflect.Method;
 50 import java.util.ArrayList;
 51 import java.util.HashMap;
 52 import java.util.List;
 53 import java.util.Map;
 54 
 55 /**
 56  * <p>
 57  * The class for composing vCard from Contacts information.
 58  * </p>
 59  * <p>
 60  * Usually, this class should be used like this.
 61  * </p>
 62  * <pre class="prettyprint">VCardComposer composer = null;
 63  * try {
 64  *     composer = new VCardComposer(context);
 65  *     composer.addHandler(
 66  *             composer.new HandlerForOutputStream(outputStream));
 67  *     if (!composer.init()) {
 68  *         // Do something handling the situation.
 69  *         return;
 70  *     }
 71  *     while (!composer.isAfterLast()) {
 72  *         if (mCanceled) {
 73  *             // Assume a user may cancel this operation during the export.
 74  *             return;
 75  *         }
 76  *         if (!composer.createOneEntry()) {
 77  *             // Do something handling the error situation.
 78  *             return;
 79  *         }
 80  *     }
 81  * } finally {
 82  *     if (composer != null) {
 83  *         composer.terminate();
 84  *     }
 85  * }</pre>
 86  * <p>
 87  * Users have to manually take care of memory efficiency. Even one vCard may contain
 88  * image of non-trivial size for mobile devices.
 89  * </p>
 90  * <p>
 91  * {@link VCardBuilder} is used to build each vCard.
 92  * </p>
 93  */
 94 public class VCardComposer {
 95     private static final String LOG_TAG = "VCardComposer";
 96     private static final boolean DEBUG = false;
 97 
 98     public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
 99         "Failed to get database information";
100 
101     public static final String FAILURE_REASON_NO_ENTRY =
102         "There‘s no exportable in the database";
103 
104     public static final String FAILURE_REASON_NOT_INITIALIZED =
105         "The vCard composer object is not correctly initialized";
106 
107     /** Should be visible only from developers... (no need to translate, hopefully) */
108     public static final String FAILURE_REASON_UNSUPPORTED_URI =
109         "The Uri vCard composer received is not supported by the composer.";
110 
111     public static final String NO_ERROR = "No error";
112 
113     // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here,
114     // since usual vCard devices for Japanese devices already use it.
115     private static final String SHIFT_JIS = "SHIFT_JIS";
116     private static final String UTF_8 = "UTF-8";
117 
118     private static final String SIM_NAME_1 = "SIM1";
119     private static final String SIM_NAME_2 = "SIM2";
120     private static final String SIM_NAME_3 = "SIM3";
121     private static final String SIM_NAME = "SIM";
122 
123     private static final Map<Integer, String> sImMap;
124 
125     static {
126         sImMap = new HashMap<Integer, String>();
127         sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
128         sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
129         sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
130         sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
131         sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
132         sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
133         // We don‘t add Google talk here since it has to be handled separately.
134     }
135 
136     private final int mVCardType;
137     private final ContentResolver mContentResolver;
138 
139     private final boolean mIsDoCoMo;
140     /**
141      * Used only when {@link #mIsDoCoMo} is true. Set to true when the first vCard for DoCoMo
142      * vCard is emitted.
143      */
144     private boolean mFirstVCardEmittedInDoCoMoCase;
145 
146     private Cursor mCursor;
147     private boolean mCursorSuppliedFromOutside;
148     private int mIdColumn;
149     private Uri mContentUriForRawContactsEntity;
150 
151     private final String mCharset;
152 
153     private String mCurrentContactID = null;
154 
155     private boolean mInitDone;
156     private String mErrorReason = NO_ERROR;
157 
158     /**
159      * Set to false when one of {@link #init()} variants is called, and set to true when
160      * {@link #terminate()} is called. Initially set to true.
161      */
162     private boolean mTerminateCalled = true;
163 
164     private static final String[] sContactsProjection = new String[] {
165         Contacts._ID,
166     };
167 
168     public VCardComposer(Context context) {
169         this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
170     }
171 
172     /**
173      * The variant which sets charset to null and sets careHandlerErrors to true.
174      */
175     public VCardComposer(Context context, int vcardType) {
176         this(context, vcardType, null, true);
177     }
178 
179     public VCardComposer(Context context, int vcardType, String charset) {
180         this(context, vcardType, charset, true);
181     }
182 
183     /**
184      * The variant which sets charset to null.
185      */
186     public VCardComposer(final Context context, final int vcardType,
187             final boolean careHandlerErrors) {
188         this(context, vcardType, null, careHandlerErrors);
189     }
190 
191     /**
192      * Constructs for supporting call log entry vCard composing.
193      *
194      * @param context Context to be used during the composition.
195      * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
196      * @param charset The charset to be used. Use null when you don‘t need the charset.
197      * @param careHandlerErrors If true, This object returns false everytime
198      */
199     public VCardComposer(final Context context, final int vcardType, String charset,
200             final boolean careHandlerErrors) {
201         this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors);
202     }
203 
204     /**
205      * Just for testing for now.
206      * @param resolver {@link ContentResolver} which used by this object.
207      * @hide
208      */
209     public VCardComposer(final Context context, ContentResolver resolver,
210             final int vcardType, String charset, final boolean careHandlerErrors) {
211         // Not used right now
212         // mContext = context;
213         mVCardType = vcardType;
214         mContentResolver = resolver;
215 
216         mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
217 
218         charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
219         final boolean shouldAppendCharsetParam = !(
220                 VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
221 
222         if (mIsDoCoMo || shouldAppendCharsetParam) {
223             // TODO: clean up once we‘re sure CharsetUtils are really unnecessary any more.
224             if (SHIFT_JIS.equalsIgnoreCase(charset)) {
225                 /*if (mIsDoCoMo) {
226                     try {
227                         charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
228                     } catch (UnsupportedCharsetException e) {
229                         Log.e(LOG_TAG,
230                                 "DoCoMo-specific SHIFT_JIS was not found. "
231                                 + "Use SHIFT_JIS as is.");
232                         charset = SHIFT_JIS;
233                     }
234                 } else {
235                     try {
236                         charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
237                     } catch (UnsupportedCharsetException e) {
238                         // Log.e(LOG_TAG,
239                         // "Career-specific SHIFT_JIS was not found. "
240                         // + "Use SHIFT_JIS as is.");
241                         charset = SHIFT_JIS;
242                     }
243                 }*/
244                 mCharset = charset;
245             } else {
246                 /* Log.w(LOG_TAG,
247                         "The charset \"" + charset + "\" is used while "
248                         + SHIFT_JIS + " is needed to be used."); */
249                 if (TextUtils.isEmpty(charset)) {
250                     mCharset = SHIFT_JIS;
251                 } else {
252                     /*
253                     try {
254                         charset = CharsetUtils.charsetForVendor(charset).name();
255                     } catch (UnsupportedCharsetException e) {
256                         Log.i(LOG_TAG,
257                                 "Career-specific \"" + charset + "\" was not found (as usual). "
258                                 + "Use it as is.");
259                     }*/
260                     mCharset = charset;
261                 }
262             }
263         } else {
264             if (TextUtils.isEmpty(charset)) {
265                 mCharset = UTF_8;
266             } else {
267                 /*try {
268                     charset = CharsetUtils.charsetForVendor(charset).name();
269                 } catch (UnsupportedCharsetException e) {
270                     Log.i(LOG_TAG,
271                             "Career-specific \"" + charset + "\" was not found (as usual). "
272                             + "Use it as is.");
273                 }*/
274                 mCharset = charset;
275             }
276         }
277 
278         Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
279     }
280 
281     /**
282      * Initializes this object using default {@link Contacts#CONTENT_URI}.
283      *
284      * You can call this method or a variant of this method just once. In other words, you cannot
285      * reuse this object.
286      *
287      * @return Returns true when initialization is successful and all the other
288      *          methods are available. Returns false otherwise.
289      */
290     public boolean init() {
291         return init(null, null);
292     }
293 
294     /**
295      * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
296      * {@link ContentResolver} with {@link Contacts#_ID}.
297      * <code>
298      * String selection = Data.CONTACT_ID + "=?";
299      * String[] selectionArgs = new String[] {contactId};
300      * Cursor cursor = mContentResolver.query(
301      *         contentUriForRawContactsEntity, null, selection, selectionArgs, null)
302      * </code>
303      *
304      * You can call this method or a variant of this method just once. In other words, you cannot
305      * reuse this object.
306      *
307      * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
308      * need to change the default Uri.
309      */
310     @Deprecated
311     public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
312         return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
313                 contentUriForRawContactsEntity);
314     }
315 
316     /**
317      * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
318      * arguments.
319      */
320     public boolean init(final String selection, final String[] selectionArgs) {
321         return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
322                 null, null);
323     }
324 
325     /**
326      * Note that this is unstable interface, may be deleted in the future.
327      */
328     public boolean init(final Uri contentUri, final String selection,
329             final String[] selectionArgs, final String sortOrder) {
330         return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
331     }
332 
333     /**
334      * @param contentUri Uri for obtaining the list of contactId. Used with
335      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
336      * @param selection selection used with
337      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
338      * @param selectionArgs selectionArgs used with
339      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
340      * @param sortOrder sortOrder used with
341      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
342      * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
343      * contactId.
344      * Note that this is an unstable interface, may be deleted in the future.
345      */
346     public boolean init(final Uri contentUri, final String selection,
347             final String[] selectionArgs, final String sortOrder,
348             final Uri contentUriForRawContactsEntity) {
349         return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
350                 contentUriForRawContactsEntity);
351     }
352 
353     /**
354      * A variant of init(). Currently just for testing. Use other variants for init().
355      *
356      * First we‘ll create {@link Cursor} for the list of contactId.
357      *
358      * <code>
359      * Cursor cursorForId = mContentResolver.query(
360      *         contentUri, projection, selection, selectionArgs, sortOrder);
361      * </code>
362      *
363      * After that, we‘ll obtain data for each contactId in the list.
364      *
365      * <code>
366      * Cursor cursorForContent = mContentResolver.query(
367      *         contentUriForRawContactsEntity, null,
368      *         Data.CONTACT_ID + "=?", new String[] {contactId}, null)
369      * </code>
370      *
371      * {@link #createOneEntry()} or its variants let the caller obtain each entry from
372      * <code>cursorForContent</code> above.
373      *
374      * @param contentUri Uri for obtaining the list of contactId. Used with
375      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
376      * @param projection projection used with
377      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
378      * @param selection selection used with
379      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
380      * @param selectionArgs selectionArgs used with
381      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
382      * @param sortOrder sortOrder used with
383      * {@link ContentResolver#query(Uri, String[], String, String[], String)}
384      * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
385      * contactId.
386      * @return true when successful
387      *
388      * @hide
389      */
390     public boolean init(final Uri contentUri, final String[] projection,
391             final String selection, final String[] selectionArgs,
392             final String sortOrder, Uri contentUriForRawContactsEntity) {
393         if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
394             if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
395             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
396             return false;
397         }
398 
399         if (!initInterFirstPart(contentUriForRawContactsEntity)) {
400             return false;
401         }
402         if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
403                 sortOrder)) {
404             return false;
405         }
406         if (!initInterMainPart()) {
407             return false;
408         }
409         return initInterLastPart();
410     }
411 
412     /**
413      * Just for testing for now. Do not use.
414      * @hide
415      */
416     public boolean init(Cursor cursor) {
417         if (!initInterFirstPart(null)) {
418             return false;
419         }
420         mCursorSuppliedFromOutside = true;
421         mCursor = cursor;
422         if (!initInterMainPart()) {
423             return false;
424         }
425         return initInterLastPart();
426     }
427 
428     private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
429         mContentUriForRawContactsEntity =
430                 (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
431                         RawContactsEntity.CONTENT_URI);
432         if (mInitDone) {
433             Log.e(LOG_TAG, "init() is already called");
434             return false;
435         }
436         return true;
437     }
438 
439     private boolean initInterCursorCreationPart(
440             final Uri contentUri, final String[] projection,
441             final String selection, final String[] selectionArgs, final String sortOrder) {
442         mCursorSuppliedFromOutside = false;
443         mCursor = mContentResolver.query(
444                 contentUri, projection, selection, selectionArgs, sortOrder);
445         if (mCursor == null) {
446             Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
447             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
448             return false;
449         }
450         return true;
451     }
452 
453     private boolean initInterMainPart() {
454         if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
455             if (DEBUG) {
456                 Log.d(LOG_TAG,
457                     String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
458             }
459             closeCursorIfAppropriate();
460             return false;
461         }
462         mIdColumn = mCursor.getColumnIndex(Contacts._ID);
463         return mIdColumn >= 0;
464     }
465 
466     private boolean initInterLastPart() {
467         mInitDone = true;
468         mTerminateCalled = false;
469         return true;
470     }
471 
472     /**
473      * @return a vCard string.
474      */
475     public String createOneEntry() {
476         return createOneEntry(null);
477     }
478 
479     /**
480      * @hide
481      */
482     public String createOneEntry(Method getEntityIteratorMethod) {
483         if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
484             mFirstVCardEmittedInDoCoMoCase = true;
485             // Previously we needed to emit empty data for this specific case, but actually
486             // this doesn‘t work now, as resolver doesn‘t return any data with "-1" contactId.
487             // TODO: re-introduce or remove this logic. Needs to modify unit test when we
488             // re-introduce the logic.
489             // return createOneEntryInternal("-1", getEntityIteratorMethod);
490         }
491 
492         final String vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
493                 getEntityIteratorMethod);
494         if (!mCursor.moveToNext()) {
495             Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
496         }
497         return vcard;
498     }
499 
500     private String createOneEntryInternal(final String contactId,
501             final Method getEntityIteratorMethod) {
502         final Map<String, List<ContentValues>> contentValuesListMap =
503                 new HashMap<String, List<ContentValues>>();
504         // The resolver may return the entity iterator with no data. It is possible.
505         // e.g. If all the data in the contact of the given contact id are not exportable ones,
506         //      they are hidden from the view of this method, though contact id itself exists.
507         EntityIterator entityIterator = null;
508         try {
509             final Uri uri = mContentUriForRawContactsEntity;
510             final String selection = Data.CONTACT_ID + "=?";
511             final String[] selectionArgs = new String[] {contactId};
512             if (getEntityIteratorMethod != null) {
513                 // Please note that this branch is executed by unit tests only
514                 try {
515                     entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
516                             mContentResolver, uri, selection, selectionArgs, null);
517                 } catch (IllegalArgumentException e) {
518                     Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
519                             e.getMessage());
520                 } catch (IllegalAccessException e) {
521                     Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
522                             e.getMessage());
523                 } catch (InvocationTargetException e) {
524                     Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e);
525                     throw new RuntimeException("InvocationTargetException has been thrown");
526                 }
527             } else {
528                 entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
529                         uri, null, selection, selectionArgs, null));
530             }
531 
532             if (entityIterator == null) {
533                 Log.e(LOG_TAG, "EntityIterator is null");
534                 return "";
535             }
536 
537             if (!entityIterator.hasNext()) {
538                 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
539                 return "";
540             }
541 
542             while (entityIterator.hasNext()) {
543                 Entity entity = entityIterator.next();
544                 for (NamedContentValues namedContentValues : entity.getSubValues()) {
545                     ContentValues contentValues = namedContentValues.values;
546                     String key = contentValues.getAsString(Data.MIMETYPE);
547                     if (key != null) {
548                         List<ContentValues> contentValuesList =
549                                 contentValuesListMap.get(key);
550                         if (contentValuesList == null) {
551                             contentValuesList = new ArrayList<ContentValues>();
552                             contentValuesListMap.put(key, contentValuesList);
553                         }
554                         contentValuesList.add(contentValues);
555                     }
556                 }
557             }
558         } finally {
559             if (entityIterator != null) {
560                 entityIterator.close();
561             }
562         }
563         mCurrentContactID = contactId;
564 
565         return buildVCard(contentValuesListMap);
566     }
567 
568     private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback;
569     /**
570      * <p>
571      * Set a callback for phone number formatting. It will be called every time when this object
572      * receives a phone number for printing.
573      * </p>
574      * <p>
575      * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored
576      * and the callback should be responsible for everything about phone number formatting.
577      * </p>
578      * <p>
579      * Caution: This interface will change. Please don‘t use without any strong reason.
580      * </p>
581      */
582     public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
583         mPhoneTranslationCallback = callback;
584     }
585 
586     /** return whether the contact‘s account type is sim account */
587     private boolean isSimcardAccount(String contactid) {
588         boolean isSimAccount = false;
589         Cursor cursor = null;
590         try {
591             cursor = mContentResolver.query(RawContacts.CONTENT_URI,
592                     new String[] { RawContacts.ACCOUNT_NAME },
593                     RawContacts.CONTACT_ID + "=?", new String[] { contactid },
594                     null);
595             if (null != cursor && 0 != cursor.getCount() && cursor.moveToFirst()) {
596                 String accountName = cursor.getString(
597                         cursor.getColumnIndex(RawContacts.ACCOUNT_NAME));
598                 if (SIM_NAME.equals(accountName) || SIM_NAME_1.equals(accountName) ||
599                         SIM_NAME_2.equals(accountName) || SIM_NAME_3.equals(accountName)) {
600                     isSimAccount = true;
601                 }
602             }
603         } finally {
604             if (null != cursor) {
605                 cursor.close();
606             }
607         }
608 
609         return isSimAccount;
610     }
611 
612     /**
613      * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
614      * {ContactsContract}. Developers can override this method to customize the output.
615      */
616     public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
617         if (contentValuesListMap == null) {
618             Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
619             return "";
620         } else {
621             final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
622             builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
623                     .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
624                     .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE),
625                             mPhoneTranslationCallback)
626                     .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
627                     .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
628                     .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
629                     .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
630             if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0
631                     && mCurrentContactID != null && !isSimcardAccount(mCurrentContactID)) {
632                 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
633             }
634             builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
635                     .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
636                     .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
637                     .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
638                     .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
639             return builder.toString();
640         }
641     }
642 
643     public void terminate() {
644         closeCursorIfAppropriate();
645         mTerminateCalled = true;
646     }
647 
648     private void closeCursorIfAppropriate() {
649         if (!mCursorSuppliedFromOutside && mCursor != null) {
650             try {
651                 mCursor.close();
652             } catch (SQLiteException e) {
653                 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
654             }
655             mCursor = null;
656         }
657     }
658 
659     @Override
660     protected void finalize() throws Throwable {
661         try {
662             if (!mTerminateCalled) {
663                 Log.e(LOG_TAG, "finalized() is called before terminate() being called");
664             }
665         } finally {
666             super.finalize();
667         }
668     }
669 
670     /**
671      * @return returns the number of available entities. The return value is undefined
672      * when this object is not ready yet (typically when {{@link #init()} is not called
673      * or when {@link #terminate()} is already called).
674      */
675     public int getCount() {
676         if (mCursor == null) {
677             Log.w(LOG_TAG, "This object is not ready yet.");
678             return 0;
679         }
680         return mCursor.getCount();
681     }
682 
683     /**
684      * @return true when there‘s no entity to be built. The return value is undefined
685      * when this object is not ready yet.
686      */
687     public boolean isAfterLast() {
688         if (mCursor == null) {
689             Log.w(LOG_TAG, "This object is not ready yet.");
690             return false;
691         }
692         return mCursor.isAfterLast();
693     }
694 
695     /**
696      * @return Returns the error reason.
697      */
698     public String getErrorReason() {
699         return mErrorReason;
700     }
701 }
View Code

Phonebook 导出联系人到SD卡(.vcf)

上一篇:关于JAVA环境变量那点事


下一篇:Unity开发之 MissionManager