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:1135i6 2 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_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 }
这里首先通过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) 初始化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 }
(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<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 }
我们发现调用了好多append***()系列的方法,而且传入的参数都是contentValuesListMap,以appendStructuredNames()方法为例,其又调用了appendStructuredNamesInternal()方法,代码如下:
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 }
这个方法很长,我只是截取了其中一部分我们分析需要的代码,该方法的作用如下:
1. 获取姓名的各个部分,并对其编码。
2. (1)处判断, 如果姓名是中文,那么if (reallyUseQuotedPrintableToName) 成立。
看红色代码,就是.vcf文件中信息编码的header部分,比如“N”,“FN”, “ENCODING=QUOTED-PRINTABLE”等,如下:
此联系人姓名:大卫 号码:9999999
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
现在应该清楚了吧,其实.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 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 }
(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-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
传进来的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<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 }
看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<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 }
这里就不做分析了,(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,终于结束了,战线太长了,甚至有点凌乱,不过希望读者沿着主线看,不要太在乎细节,比如那个方法是怎么调用的或者这个参数是什么时候初始化的。
最后两个方法,没有仔细讲,感兴趣的读者自己去看吧,原理在前面就说清楚了。
现在回答一下上一篇文章中(http://www.cnblogs.com/wlrhnh/p/3515269.html)遗留的问题,就是如何解码的问题,我们现在知道如何编码,那如何解码还不简单吗?呵呵
最后将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 * <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 }