博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Phonebook 导出联系人到SD卡(.vcf)
阅读量:5032 次
发布时间:2019-06-12

本文共 53660 字,大约阅读时间需要 178 分钟。

2014-01-13 16:53:55 

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

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

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

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 }

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

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

2. ExportVcardThread线程类

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

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

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

(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:1135i62 vcardUri.build() = content://com.android.contacts/contacts/as_multi_vcard/1135i3%3A1135i6?vcard_type=default

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

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 }

那么现在最关键的问题就是输入流是如何得到的,如何将联系人编码生成符合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,代码:

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_vcard12             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 a32             // 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 }

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

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

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) 初始化composer14             Log.w(TAG, "Failed to init VCardComposer");15             return;16         }    17 18         while (!composer.isAfterLast()) {19             writer.write(composer.createOneEntry());    (2)真正编码联系人信息20         }    21     }22 }

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

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

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 }

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

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 }

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

1 private String createOneEntryInternal(final String contactId, final boolean aForceEmpty) { 2     final Map
> contentValuesListMap = 3 new HashMap
>(); 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 }

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

1 private void appendStructuredNamesInternal(final StringBuilder builder, 2         final List
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 = primaryContentValues10 .getAsString(StructuredName.PREFIX);11 final String suffix = primaryContentValues12 .getAsString(StructuredName.SUFFIX);13 final String displayName = primaryContentValues14 .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 property76 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 }

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

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

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

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

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

1 BEGIN:VCARD2 VERSION:2.13 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:99999996 END:VCARD

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

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

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 the41             // length of the line42             // becomes more than 76.43             // Assuming that the next character is a multi-byte character,44             // it will become45             // 6 bytes.46             // 76 - 6 - 3 = 6747             tmpBuilder.append("=\r\n");48             lineCount = 0;49         }50     }51 52     return tmpBuilder.toString();53 }

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

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

1 D/D44     (32766): str = 大卫2 D/D44     (32766): 1str = 大卫3 D/D44     (32766): mCharsetString = UTF-84 D/D44     (32766): tmpBuilder = =E55 D/D44     (32766): tmpBuilder = =E5=A46 D/D44     (32766): tmpBuilder = =E5=A4=A77 D/D44     (32766): tmpBuilder = =E5=A4=A7=E58 D/D44     (32766): tmpBuilder = =E5=A4=A7=E5=8D9 D/D44     (32766): tmpBuilder = =E5=A4=A7=E5=8D=AB

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

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

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 }

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

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

1     private void appendPhotos(final StringBuilder builder, 2             final Map
> contentValuesListMap) { 3 final List
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) 0x8926 && 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) 0xff31 && 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 }

看3处红色代码:

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

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

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

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

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

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

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     }

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

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

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

现在回答一下上一篇文章中()遗留的问题,就是如何解码的问题,我们现在知道如何编码,那如何解码还不简单吗?呵呵

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

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  * 

57 * The class for composing vCard from Contacts information. 58 *

59 *

60 * Usually, this class should be used like this. 61 *

62 *
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  * }
86 *

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 *

90 *

91 * {

@link VCardBuilder} is used to build each vCard. 92 *

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
sImMap;124 125 static {126 sImMap = new HashMap
();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 DoCoMo142 * 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 when160 * {
@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 everytime198 */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 * @hide208 */209 public VCardComposer(final Context context, ContentResolver resolver,210 final int vcardType, String charset, final boolean careHandlerErrors) {211 // Not used right now212 // 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 cannot285 * reuse this object.286 *287 * @return Returns true when initialization is successful and all the other288 * 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} from296 * {
@link ContentResolver} with {
@link Contacts#_ID}.297 *
298 * String selection = Data.CONTACT_ID + "=?";299 * String[] selectionArgs = new String[] {contactId};300 * Cursor cursor = mContentResolver.query(301 * contentUriForRawContactsEntity, null, selection, selectionArgs, null)302 * 303 *304 * You can call this method or a variant of this method just once. In other words, you cannot305 * reuse this object.306 *307 * @deprecated Use {
@link #init(Uri, String[], String, String[], String, Uri)} if you really308 * need to change the default Uri.309 */310 @Deprecated311 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 selection318 * 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 with335 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}336 * @param selection selection used with337 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}338 * @param selectionArgs selectionArgs used with339 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}340 * @param sortOrder sortOrder used with341 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}342 * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each343 * 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 *
359 * Cursor cursorForId = mContentResolver.query(360 * contentUri, projection, selection, selectionArgs, sortOrder);361 * 362 *363 * After that, we'll obtain data for each contactId in the list.364 *365 *
366 * Cursor cursorForContent = mContentResolver.query(367 * contentUriForRawContactsEntity, null,368 * Data.CONTACT_ID + "=?", new String[] {contactId}, null)369 * 370 *371 * {
@link #createOneEntry()} or its variants let the caller obtain each entry from372 *
cursorForContent above.373 *374 * @param contentUri Uri for obtaining the list of contactId. Used with375 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}376 * @param projection projection used with377 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}378 * @param selection selection used with379 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}380 * @param selectionArgs selectionArgs used with381 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}382 * @param sortOrder sortOrder used with383 * {
@link ContentResolver#query(Uri, String[], String, String[], String)}384 * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each385 * contactId.386 * @return true when successful387 *388 * @hide389 */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 * @hide415 */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 * @hide481 */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 actually486 // 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 we488 // 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
> contentValuesListMap =503 new HashMap
>();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 only514 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
contentValuesList =549 contentValuesListMap.get(key);550 if (contentValuesList == null) {551 contentValuesList = new ArrayList
();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 *

571 * Set a callback for phone number formatting. It will be called every time when this object572 * receives a phone number for printing.573 *

574 *

575 * When this is set { @link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored576 * and the callback should be responsible for everything about phone number formatting.577 *

578 *

579 * Caution: This interface will change. Please don't use without any strong reason.580 *

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 in614 * {ContactsContract}. Developers can override this method to customize the output.615 */616 public String buildVCard(final Map
> 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) == 0631 && 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 @Override660 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 undefined672 * when this object is not ready yet (typically when { { @link #init()} is not called673 * 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 undefined685 * 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

 

 

 

转载于:https://www.cnblogs.com/wlrhnh/p/3517818.html

你可能感兴趣的文章
MySQL临时表
查看>>
关于使用jqmobi前端框架在phonegap平台上开发时的日期时间选择控件
查看>>
HDU 4638 Group (莫队算法||线段树离散查询)
查看>>
精神到处文章老,学问深时意气平(努力方向)——Leo2014年终总结
查看>>
Android-ListView 下拉刷新
查看>>
批量判断流量大于300的小脚本
查看>>
SDN
查看>>
cf 11B Jumping Jack(贪心,数学证明一下,,)
查看>>
POJ 2418 Hardwood Species(STL在map应用)
查看>>
Python开发之路
查看>>
Codeforces 449.C Jzzhu and Apples
查看>>
取石子游戏HDU1846
查看>>
前端常见英文缩写含义
查看>>
POJ_3967_Ideal Path
查看>>
将Ubuntu下网卡名称enss改为eth0
查看>>
VS 里附加库目录的设置
查看>>
移动端jq及zepto事件绑定
查看>>
记五一清北(济南)
查看>>
Centos非管理员安装Python和pip
查看>>
切片器化繁为简,盘它 !
查看>>