请问cLloCvcfloc是什么意思思啊

&&&&&&&&&&&&&
&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 {
private final String mFileN
public ExportConfirmationListener(String fileName) {
mFileName = fileN
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
mExportingFileName = mFileN
progressDialogShow();
mListenerAdapter = new ListenerThreadBridgeAdapter(ExportVCardActivity.this);
mActualExportThread = new ExportVcardThread(null, ExportVCardActivity.this,
mFileName, mListenerAdapter, false);
mActualExportThread.start();
注意红色部分,很简单,创建一个ExportVcardThread对象,将即将生成的.vcf文件的名称当作参数传入,同时start这个线程。
下面进入ExportVcardThread线程类一探究竟。
2.&ExportVcardThread线程类
查看线程类的run()方法,有如下代码:
outputStream = new FileOutputStream(mFileName);
3 } catch (FileNotFoundException e) {
mErrorReason = mContext.getString(
R.string.spb_strings_fail_reason_could_not_open_file_txt, mFileName,
e.getMessage());
isComposed = false;
11 isComposed = compose(outputStream);
创建了文件输出流,可见对.vcf文件的操作将会以流的形式进行,然后调用compose(outputStream),将创建的输出流对象当作参数传入。compose()方法很大,但其实只做了两件事:(1)查询数据库获得要导出的联系人信息;(2)将联系人信息编码导出到.vcf文件,核心代码如下:
1 private boolean compose(OutputStream outputStream) {
final ContentResolver cr = mContext.getContentResolver();
StringBuilder selection = new StringBuilder();
StringBuilder order = new StringBuilder();
// exclude restricted contacts.
final Uri.Builder contactsUri = ContactsContract.Contacts.CONTENT_URI.buildUpon();
contactsUri.appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, "");
c = cr.query(contactsUri.build(), CLMN, selection.toString(), null,
order.toString());
while (c.moveToNext()) {
if (mCanceled) {
lookupKeys.append(c.getString(lookupClmn));
if (!c.isLast() && count & VCARD_REQUEST_LIMIT) {
lookupKeys.append(":");
final Uri.Builder vcardUri = ContactsContract.Contacts.CONTENT_MULTI_VCARD_URI.buildUpon();
vcardUri.appendPath(lookupKeys.toString());
vcardUri.appendQueryParameter("vcard_type", mVcardTypeStr);
Log.d("D33", "mVcardTypeStr = " + mVcardTypeStr);
Log.d("D33", "vcardUri.build() = " + vcardUri.build());
final InputStream is = cr.openInputStream(vcardUri.build());
if (copyStream(buff, is, outputStream) & 0) {
hasActualEntry = true;
if (mListener != null) {
mListener.incrementProgressBy(count);
count = 0;
lookupKeys.setLength(0);
(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 = 5i6
2 vcardUri.build() = content://com.android.contacts/contacts/as_multi_vcard/A1135i6?vcard_type=default
lookupKeys是将所有联系人的lookupKey连接起来,中间用&:&分隔。然后调用copyStream(buff, is, outputStream),看名字就知道作用是copy输入流到输出流,代码如下:
1 private int copyStream(byte[] buff, InputStream is, OutputStream os) throws IOException {
int copiedLength = 0;
if (is == null || os == null) {
return copiedL
int sz = 0;
sz = is.read(buff);
if (sz & 0) {
os.write(buff, 0, sz);
copiedLength +=
} while (sz & 0);
return copiedL
那么现在最关键的问题就是输入流是如何得到的,如何将联系人编码生成符合vcf标准的信息呢?根据上面提到的vcardUri=com.android.contacts/contacts/as_multi_vcard/A1135i6?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)
throws FileNotFoundException {
final boolean writing = mode.contains("w");
final SQLiteDatabase db = mDbHelper.get().getDatabase(writing);
int match = sUriMatcher.match(uri);
switch (match) {
case CONTACTS_AS_MULTI_VCARD: { // 匹配content://com.android.contacts/contacts/as_multi_vcard
final String lookupKeys = uri.getPathSegments().get(2);
final String[] loopupKeyList = lookupKeys.split(":");
final StringBuilder inBuilder = new StringBuilder();
Uri queryUri = Contacts.CONTENT_URI;
int index = 0;
for (String lookupKey : loopupKeyList) {
if (index == 0) {
inBuilder.append("(");
inBuilder.append(",");
long contactId = lookupContactIdByLookupKey(db, lookupKey);
inBuilder.append(contactId);
inBuilder.append(')');
final String selection = Contacts._ID + " IN " + inBuilder.toString();
// When opening a contact as file, we pass back contents as a
// vCard-encoded stream. We build into a local buffer first,
// then pipe into MemoryFile once the exact size is known.
final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
outputRawContactsAsVCard(queryUri, localStream, selection, null);
return buildAssetFileDescriptor(localStream);
这里首先通过lookupkey取得对应联系人的ID,然后再次生成查询条件selection,然后调用buildAssetFileDescriptor(localStream)方法,这个方法简单的对localStream做了一下封装,然后返回,那么localStream到底是怎么生成的?
进入outputRawContactsAsVCard(queryUri, localStream, selection, null)方法,代码如下:
1 private void outputRawContactsAsVCard(Uri uri, OutputStream stream,
String selection, String[] selectionArgs) {
final Context context = this.getContext();
int vcardconfig = VCardConfig.VCARD_TYPE_DEFAULT;
if(uri.getBooleanQueryParameter(
Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, false)) {
vcardconfig |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
final VCardComposer composer =
new VCardComposer(context, vcardconfig, false);
writer = new BufferedWriter(new OutputStreamWriter(stream));
if (!composer.init(uri, selection, selectionArgs, null, rawContactsUri)) {
(1) 初始化composer
Log.w(TAG, "Failed to init VCardComposer");
while (!composer.isAfterLast()) {
writer.write(composer.createOneEntry());
(2)真正编码联系人信息
(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,
final String sortOrder) {
if (mIsCallLogComposer) {
mCursor = mContentResolver.query(CallLog.Calls.CONTENT_URI, sCallLogProjection,
selection, selectionArgs, sortOrder);
} else if (isMyProfile) {
mCursor = mContentResolver.query(Profile.CONTENT_URI, sContactsProjection,
selection, selectionArgs, sortOrder);
mCursor = mContentResolver.query(Contacts.CONTENT_URI, sContactsProjection,
selection, selectionArgs, sortOrder);
if (mCursor == null) {
mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
return false;
return true;
init()方法里面根据传入的参数,query数据库,得到要导出的联系人。看下面createOneEntry()方法:
1 public boolean createOneEntry() {
if (mCursor == null || mCursor.isAfterLast()) {
mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
return false;
String name = null;
if (mIsCallLogComposer) {
vcard = createOneCallLogEntryInternal();
} else if (mIdColumn &= 0) {
mContactsPhotoId = mCursor.getString(mCursor.getColumnIndex(Contacts.PHOTO_ID));
vcard = createOneEntryInternal(mCursor.getString(mIdColumn));
Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
return false;
return true;
注意红色代码,进入createOneEntryInternal()方法看看,代码如下:
1 private String createOneEntryInternal(final String contactId, final boolean aForceEmpty) {
final Map&String, List&ContentValues&& contentValuesListMap =
new HashMap&String, List&ContentValues&&();
final String selection = Data.CONTACT_ID + "=?";
final String[] selectionArgs = new String[] {contactId};
if (Long.valueOf(contactId)&Profile.MIN_ID) {
uri = RawContactsEntity.PROFILE_CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContactsEntity.FOR_EXPORT_ONLY, "1")
uri = RawContactsEntity.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContactsEntity.FOR_EXPORT_ONLY, "1")
appendStructuredNames(builder, contentValuesListMap);
appendNickNames(builder, contentValuesListMap);
appendPhones(builder, contentValuesListMap);
appendEmails(builder, contentValuesListMap);
appendPostals(builder, contentValuesListMap);
appendIms(builder, contentValuesListMap);
appendWebsites(builder, contentValuesListMap);
appendBirthday(builder, contentValuesListMap);
appendOrganizations(builder, contentValuesListMap);
if (mNeedPhotoForVCard) {
appendPhotos(builder, contentValuesListMap);
appendNotes(builder, contentValuesListMap);
appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD);
return builder.toString();
我们发现调用了好多append***()系列的方法,而且传入的参数都是contentValuesListMap,以appendStructuredNames()方法为例,其又调用了appendStructuredNamesInternal()方法,代码如下:
1 private void appendStructuredNamesInternal(final StringBuilder builder,
final List&ContentValues& contentValuesList) {
final String familyName = primaryContentValues
.getAsString(StructuredName.FAMILY_NAME);
final String middleName = primaryContentValues
.getAsString(StructuredName.MIDDLE_NAME);
final String givenName = primaryContentValues
.getAsString(StructuredName.GIVEN_NAME);
final String prefix = primaryContentValues
.getAsString(StructuredName.PREFIX);
final String suffix = primaryContentValues
.getAsString(StructuredName.SUFFIX);
final String displayName = primaryContentValues
.getAsString(StructuredName.DISPLAY_NAME);
if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
final String encodedF
final String encodedG
final String encodedM
final String encodedP
final String encodedS
if (reallyUseQuotedPrintableToName) {
encodedFamily = encodeQuotedPrintable(familyName);
encodedGiven = encodeQuotedPrintable(givenName);
encodedMiddle = encodeQuotedPrintable(middleName);
encodedPrefix = encodeQuotedPrintable(prefix);
encodedSuffix = encodeQuotedPrintable(suffix);
encodedFamily = escapeCharacters(familyName);
encodedGiven = escapeCharacters(givenName);
encodedMiddle = escapeCharacters(middleName);
encodedPrefix = escapeCharacters(prefix);
encodedSuffix = escapeCharacters(suffix);
// N property. This order is specified by vCard spec and does not depend on countries.
builder.append(VCARD_PROPERTY_NAME);
// VCARD_PROPERTY_NAME = "N"
if (shouldAppendCharsetAttribute(Arrays.asList(
familyName, givenName, middleName, prefix, suffix))) {
builder.append(VCARD_ATTR_SEPARATOR);
builder.append(mVCardAttributeCharset);
if (reallyUseQuotedPrintableToName) {
builder.append(VCARD_ATTR_SEPARATOR);
builder.append(VCARD_ATTR_ENCODING_QP);
builder.append(VCARD_DATA_SEPARATOR);
// VCARD_DATA_SEPARATOR = ":";
builder.append(encodedFamily);
builder.append(VCARD_ITEM_SEPARATOR);
builder.append(encodedGiven);
builder.append(VCARD_ITEM_SEPARATOR);
builder.append(encodedMiddle);
builder.append(VCARD_ITEM_SEPARATOR);
builder.append(encodedPrefix);
builder.append(VCARD_ITEM_SEPARATOR);
builder.append(encodedSuffix);
builder.append(VCARD_COL_SEPARATOR);
final String fullname = displayN
final boolean reallyUseQuotedPrintableToFullname =
mUsesQPToPrimaryProperties &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname);
final String encodedF
if (reallyUseQuotedPrintableToFullname) {
encodedFullname = encodeQuotedPrintable(fullname);
// VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"
} else if (!mIsDoCoMo) {
encodedFullname = escapeCharacters(fullname);
encodedFullname = removeCrLf(fullname);
// FN property
builder.append(VCARD_PROPERTY_FULL_NAME);
// VCARD_PROPERTY_FULL_NAME = "FN"
if (shouldAppendCharsetAttribute(fullname)) {
builder.append(VCARD_ATTR_SEPARATOR);
builder.append(mVCardAttributeCharset);
if (reallyUseQuotedPrintableToFullname) {
builder.append(VCARD_ATTR_SEPARATOR);
builder.append(VCARD_ATTR_ENCODING_QP);
builder.append(VCARD_DATA_SEPARATOR);
builder.append(encodedFullname);
builder.append(VCARD_COL_SEPARATOR);
这个方法很长,我只是截取了其中一部分我们分析需要的代码,该方法的作用如下:
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) {
if (TextUtils.isEmpty(str)) {
return "";
// Replace "\n" and "\r" with "\r\n".
StringBuilder tmpBuilder = new StringBuilder();
int length = str.length();
for (int i = 0; i & i++) {
char ch = str.charAt(i);
if (ch == '\r') {
if (i + 1 & length && str.charAt(i + 1) == '\n') {
tmpBuilder.append("\r\n");
} else if (ch == '\n') {
tmpBuilder.append("\r\n");
tmpBuilder.append(ch);
str = tmpBuilder.toString();
final StringBuilder tmpBuilder = new StringBuilder();
int index = 0;
int lineCount = 0;
byte[] strArray = null;
strArray = str.getBytes(mCharsetString);
while (index & strArray.length) {
tmpBuilder.append(String.format("=%02X", strArray[index]));
Log.d("D44", "tmpBuilder = " + tmpBuilder.toString());
index += 1;
lineCount += 3;
if (lineCount &= QUATED_PRINTABLE_LINE_MAX) {
// Specification requires CRLF must be inserted before the
// length of the line
// becomes more than 76.
// Assuming that the next character is a multi-byte character,
// it will become
// 6 bytes.
// 76 - 6 - 3 = 67
tmpBuilder.append("=\r\n");
lineCount = 0;
return tmpBuilder.toString();
(1)处代码调用str.getBytes(mCharsetString),返回一个字符串的byte数组,编码格式是&UTF-8&;
(2)处代码用到了一个循环,对每一个byte进行编码,编码姓名为&大卫&的log如下:
(32766): str = 大卫
(32766): 1str = 大卫
(32766): mCharsetString = UTF-8
(32766): tmpBuilder = =E5
(32766): tmpBuilder = =E5=A4
(32766): tmpBuilder = =E5=A4=A7
(32766): tmpBuilder = =E5=A4=A7=E5
(32766): tmpBuilder = =E5=A4=A7=E5=8D
(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) {
System.out.println(String.format("=%02X", strArray[index]));
这段代码是我自己写的Demo,编码规则很简单,是不是?最关键的一句是&String.format("=%02X", strArray[index])&,至于这个方法的用法,请问度娘~
现在剩最后一个问题,联系人头像是怎么编码的呢?代码如下:
private void appendPhotos(final StringBuilder builder,
final Map&String, List&ContentValues&& contentValuesListMap) {
final List&ContentValues& contentValuesList = contentValuesListMap
.get(Photo.CONTENT_ITEM_TYPE);
if (contentValuesList != null) {
for (ContentValues contentValues : contentValuesList) {
// When photo id don't equal the photo id showned in contact,
// the photo data don't add to VCard.
if(mContactsPhotoId != null &&
(!mContactsPhotoId.equals(contentValues.getAsString(Data._ID)))){
byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
if (data == null) {
String photoT
// Use some heuristics for guessing the format of the image.
// TODO: there should be some general API for detecting the file format.
if (data.length &= 3 && data[0] == 'G' && data[1] == 'I'
&& data[2] == 'F') {
photoType = "GIF";
} else if (data.length &= 4 && data[0] == (byte) 0x89
&& data[1] == 'P' && data[2] == 'N' && data[3] == 'G') {
// PNG is not officially supported by vcard-2.1 and many FOMA phone can't decode PNG.
// To solve IOT issue, convert PNG files to JPEG.
photoType = "PNG";
} else if (data.length &= 2 && data[0] == (byte) 0xff
&& data[1] == (byte) 0xd8) {
photoType = "JPEG";
Log.d(LOG_TAG, "Unknown photo type. Ignore.");
byte[] newData = convertToSmallJpg(data, photoType);
if (newData != null) {
data = newD
photoType = "JPEG";
final String photoString = VCardUtils.encodeBase64(data);
if (photoString.length() & 0) {
appendVCardPhotoLine(builder, photoString, "TYPE=" + photoType);
(3) 添加TYPE信息
看3处红色代码:
(1)得到头像的byte数组;
(2)将byte数组编码,并返回为String类型;
(3)保存为.vcf信息,比如PHOTO;ENCODING=BASE64;TYPE=JPEG等头信息。
VCardUtils.encodeBase64(data)方法代码如下:
1 private static final char[] ENCODE64 = {
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
static public String encodeBase64(byte[] data) {
if (data == null) {
return "";
char[] charBuffer = new char[(data.length + 2) / 3 * 4];
int position = 0;
int _3byte = 0;
for (int i=0; i&data.length-2; i+=3) {
_3byte = ((data[i] & 0xFF) && 16) + ((data[i+1] & 0xFF) && 8) + (data[i+2] & 0xFF);
charBuffer[position++] = ENCODE64[_3byte && 18];
charBuffer[position++] = ENCODE64[(_3byte && 12) & 0x3F];
charBuffer[position++] = ENCODE64[(_3byte &&
6) & 0x3F];
charBuffer[position++] = ENCODE64[_3byte & 0x3F];
switch(data.length % 3) {
case 1: // [
00][000000]
_3byte = ((data[data.length-1] & 0xFF) && 16);
charBuffer[position++] = ENCODE64[_3byte && 18];
charBuffer[position++] = ENCODE64[(_3byte && 12) & 0x3F];
charBuffer[position++] = PAD;
charBuffer[position++] = PAD;
case 2: // [
00][000000]
_3byte = ((data[data.length-2] & 0xFF) && 16) + ((data[data.length-1] & 0xFF) && 8);
charBuffer[position++] = ENCODE64[_3byte && 18];
charBuffer[position++] = ENCODE64[(_3byte && 12) & 0x3F];
charBuffer[position++] = ENCODE64[(_3byte &&
6) & 0x3F];
charBuffer[position++] = PAD;
return new String(charBuffer);
这里就不做分析了,(3)处的appendVCardPhotoLine()方法代码如下:
1 private void appendVCardPhotoLine(final StringBuilder builder,
final String encodedData, final String photoType) {
StringBuilder tmpBuilder = new StringBuilder();
tmpBuilder.append(VCARD_PROPERTY_PHOTO);
// VCARD_PROPERTY_PHOTO = "PHOTO"
tmpBuilder.append(VCARD_ATTR_SEPARATOR);
if (mIsV30) {
tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30);
tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21);
tmpBuilder.append(VCARD_ATTR_SEPARATOR);
appendTypeAttribute(tmpBuilder, photoType);
tmpBuilder.append(VCARD_DATA_SEPARATOR);
tmpBuilder.append(encodedData);
final String tmpStr = tmpBuilder.toString();
tmpBuilder = new StringBuilder();
int lineCount = 0;
int length = tmpStr.length();
for (int i = 0; i & i++) {
tmpBuilder.append(tmpStr.charAt(i));
lineCount++;
if (lineCount & BASE64_LINE_MAX) {
tmpBuilder.append(VCARD_COL_SEPARATOR);
tmpBuilder.append(VCARD_WS);
lineCount = 0;
builder.append(tmpBuilder.toString());
builder.append(VCARD_COL_SEPARATOR);
builder.append(VCARD_COL_SEPARATOR);
红色代码,标识的就是photo,至于其他的,和姓名类似,就不展开说了。
OK,终于结束了,战线太长了,甚至有点凌乱,不过希望读者沿着主线看,不要太在乎细节,比如那个方法是怎么调用的或者这个参数是什么时候初始化的。
最后两个方法,没有仔细讲,感兴趣的读者自己去看吧,原理在前面就说清楚了。
现在回答一下上一篇文章中()遗留的问题,就是如何解码的问题,我们现在知道如何编码,那如何解码还不简单吗?呵呵
最后将VCardComposer.java代码贴上来,感兴趣的可以自己看看。
* Copyright (C) 2009 The Android Open Source Project
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
16 package com.android.
18 import android.content.ContentR
19 import android.content.ContentV
20 import android.content.C
21 import android.content.E
22 import android.content.Entity.NamedContentV
23 import android.content.EntityI
24 import android.database.C
25 import android.database.sqlite.SQLiteE
26 import android.net.U
27 import android.monDataKinds.E
28 import android.monDataKinds.E
29 import android.monDataKinds.Im;
30 import android.monDataKinds.N
31 import android.monDataKinds.N
32 import android.monDataKinds.O
33 import android.monDataKinds.P
34 import android.monDataKinds.P
35 import android.monDataKinds.R
36 import android.monDataKinds.SipA
37 import android.monDataKinds.StructuredN
38 import android.monDataKinds.StructuredP
39 import android.monDataKinds.W
40 import android.provider.ContactsContract.C
41 import android.provider.ContactsContract.D
42 import android.provider.ContactsContract.RawC
43 import android.provider.ContactsContract.RawContactsE
44 import android.provider.ContactsC
45 import android.text.TextU
46 import android.util.L
48 import java.lang.reflect.InvocationTargetE
49 import java.lang.reflect.M
50 import java.util.ArrayL
51 import java.util.HashM
52 import java.util.L
53 import java.util.M
* The class for composing vCard from Contacts information.
* Usually, this class should be used like this.
* &pre class="prettyprint"&VCardComposer composer =
composer = new VCardComposer(context);
composer.addHandler(
composer.new HandlerForOutputStream(outputStream));
if (!composer.init()) {
// Do something handling the situation.
while (!composer.isAfterLast()) {
if (mCanceled) {
// Assume a user may cancel this operation during the export.
if (!composer.createOneEntry()) {
// Do something handling the error situation.
* } finally {
if (composer != null) {
composer.terminate();
* Users have to manually take care of memory efficiency. Even one vCard may contain
* image of non-trivial size for mobile devices.
* {@link VCardBuilder} is used to build each vCard.
94 public class VCardComposer {
private static final String LOG_TAG = "VCardComposer";
private static final boolean DEBUG = false;
public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
"Failed to get database information";
public static final String FAILURE_REASON_NO_ENTRY =
"There's no exportable in the database";
public static final String FAILURE_REASON_NOT_INITIALIZED =
"The vCard composer object is not correctly initialized";
/** Should be visible only from developers... (no need to translate, hopefully) */
public static final String FAILURE_REASON_UNSUPPORTED_URI =
"The Uri vCard composer received is not supported by the composer.";
public static final String NO_ERROR = "No error";
// Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here,
// since usual vCard devices for Japanese devices already use it.
private static final String SHIFT_JIS = "SHIFT_JIS";
private static final String UTF_8 = "UTF-8";
private static final String SIM_NAME_1 = "SIM1";
private static final String SIM_NAME_2 = "SIM2";
private static final String SIM_NAME_3 = "SIM3";
private static final String SIM_NAME = "SIM";
private static final Map&Integer, String& sImM
sImMap = new HashMap&Integer, String&();
sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
// We don't add Google talk here since it has to be handled separately.
private final int mVCardT
private final ContentResolver mContentR
private final boolean mIsDoCoMo;
* Used only when {@link #mIsDoCoMo} is true. Set to true when the first vCard for DoCoMo
* vCard is emitted.
private boolean mFirstVCardEmittedInDoCoMoC
private Cursor mC
private boolean mCursorSuppliedFromO
private int mIdC
private Uri mContentUriForRawContactsE
private final String mC
private String mCurrentContactID = null;
private boolean mInitD
private String mErrorReason = NO_ERROR;
* Set to false when one of {@link #init()} variants is called, and set to true when
* {@link #terminate()} is called. Initially set to true.
private boolean mTerminateCalled = true;
private static final String[] sContactsProjection = new String[] {
Contacts._ID,
public VCardComposer(Context context) {
this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
* The variant which sets charset to null and sets careHandlerErrors to true.
public VCardComposer(Context context, int vcardType) {
this(context, vcardType, null, true);
public VCardComposer(Context context, int vcardType, String charset) {
this(context, vcardType, charset, true);
* The variant which sets charset to null.
public VCardComposer(final Context context, final int vcardType,
final boolean careHandlerErrors) {
this(context, vcardType, null, careHandlerErrors);
* Constructs for supporting call log entry vCard composing.
* @param context Context to be used during the composition.
* @param vcardType The type of vCard, typically available via {@link VCardConfig}.
* @param charset The charset to be used. Use null when you don't need the charset.
* @param careHandlerErrors If true, This object returns false everytime
public VCardComposer(final Context context, final int vcardType, String charset,
final boolean careHandlerErrors) {
this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors);
* Just for testing for now.
* @param resolver {@link ContentResolver} which used by this object.
public VCardComposer(final Context context, ContentResolver resolver,
final int vcardType, String charset, final boolean careHandlerErrors) {
// Not used right now
// mContext =
mVCardType = vcardT
mContentResolver =
mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
final boolean shouldAppendCharsetParam = !(
VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
if (mIsDoCoMo || shouldAppendCharsetParam) {
// TODO: clean up once we're sure CharsetUtils are really unnecessary any more.
if (SHIFT_JIS.equalsIgnoreCase(charset)) {
/*if (mIsDoCoMo) {
charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
} catch (UnsupportedCharsetException e) {
Log.e(LOG_TAG,
"DoCoMo-specific SHIFT_JIS was not found. "
+ "Use SHIFT_JIS as is.");
charset = SHIFT_JIS;
charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
} catch (UnsupportedCharsetException e) {
// Log.e(LOG_TAG,
// "Career-specific SHIFT_JIS was not found. "
// + "Use SHIFT_JIS as is.");
charset = SHIFT_JIS;
mCharset =
/* Log.w(LOG_TAG,
"The charset \"" + charset + "\" is used while "
+ SHIFT_JIS + " is needed to be used."); */
if (TextUtils.isEmpty(charset)) {
mCharset = SHIFT_JIS;
charset = CharsetUtils.charsetForVendor(charset).name();
} catch (UnsupportedCharsetException e) {
Log.i(LOG_TAG,
"Career-specific \"" + charset + "\" was not found (as usual). "
+ "Use it as is.");
mCharset =
if (TextUtils.isEmpty(charset)) {
mCharset = UTF_8;
charset = CharsetUtils.charsetForVendor(charset).name();
} catch (UnsupportedCharsetException e) {
Log.i(LOG_TAG,
"Career-specific \"" + charset + "\" was not found (as usual). "
+ "Use it as is.");
mCharset =
Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
* Initializes this object using default {@link Contacts#CONTENT_URI}.
* You can call this method or a variant of this method just once. In other words, you cannot
* reuse this object.
* @return Returns true when initialization is successful and all the other
methods are available. Returns false otherwise.
public boolean init() {
return init(null, null);
* Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
* {@link ContentResolver} with {@link Contacts#_ID}.
* String selection = Data.CONTACT_ID + "=?";
* String[] selectionArgs = new String[] {contactId};
* Cursor cursor = mContentResolver.query(
contentUriForRawContactsEntity, null, selection, selectionArgs, null)
* You can call this method or a variant of this method just once. In other words, you cannot
* reuse this object.
* @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
* need to change the default Uri.
@Deprecated
public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
contentUriForRawContactsEntity);
* Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
* arguments.
public boolean init(final String selection, final String[] selectionArgs) {
return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
null, null);
* Note that this is unstable interface, may be deleted in the future.
public boolean init(final Uri contentUri, final String selection,
final String[] selectionArgs, final String sortOrder) {
return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
* @param contentUri Uri for obtaining the list of contactId. Used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selection selection used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selectionArgs selectionArgs used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param sortOrder sortOrder used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
* contactId.
* Note that this is an unstable interface, may be deleted in the future.
public boolean init(final Uri contentUri, final String selection,
final String[] selectionArgs, final String sortOrder,
final Uri contentUriForRawContactsEntity) {
return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
contentUriForRawContactsEntity);
* A variant of init(). Currently just for testing. Use other variants for init().
* First we'll create {@link Cursor} for the list of contactId.
* Cursor cursorForId = mContentResolver.query(
contentUri, projection, selection, selectionArgs, sortOrder);
* After that, we'll obtain data for each contactId in the list.
* Cursor cursorForContent = mContentResolver.query(
contentUriForRawContactsEntity, null,
Data.CONTACT_ID + "=?", new String[] {contactId}, null)
* {@link #createOneEntry()} or its variants let the caller obtain each entry from
* &code&cursorForContent&/code& above.
* @param contentUri Uri for obtaining the list of contactId. Used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param projection projection used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selection selection used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param selectionArgs selectionArgs used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param sortOrder sortOrder used with
* {@link ContentResolver#query(Uri, String[], String, String[], String)}
* @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
* contactId.
* @return true when successful
public boolean init(final Uri contentUri, final String[] projection,
final String selection, final String[] selectionArgs,
final String sortOrder, Uri contentUriForRawContactsEntity) {
if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
return false;
if (!initInterFirstPart(contentUriForRawContactsEntity)) {
return false;
if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
sortOrder)) {
return false;
if (!initInterMainPart()) {
return false;
return initInterLastPart();
* Just for testing for now. Do not use.
public boolean init(Cursor cursor) {
if (!initInterFirstPart(null)) {
return false;
mCursorSuppliedFromOutside = true;
if (!initInterMainPart()) {
return false;
return initInterLastPart();
private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
mContentUriForRawContactsEntity =
(contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
RawContactsEntity.CONTENT_URI);
if (mInitDone) {
Log.e(LOG_TAG, "init() is already called");
return false;
return true;
private boolean initInterCursorCreationPart(
final Uri contentUri, final String[] projection,
final String selection, final String[] selectionArgs, final String sortOrder) {
mCursorSuppliedFromOutside = false;
mCursor = mContentResolver.query(
contentUri, projection, selection, selectionArgs, sortOrder);
if (mCursor == null) {
Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
return false;
return true;
private boolean initInterMainPart() {
if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
if (DEBUG) {
Log.d(LOG_TAG,
String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
closeCursorIfAppropriate();
return false;
mIdColumn = mCursor.getColumnIndex(Contacts._ID);
return mIdColumn &= 0;
private boolean initInterLastPart() {
mInitDone = true;
mTerminateCalled = false;
return true;
* @return a vCard string.
public String createOneEntry() {
return createOneEntry(null);
public String createOneEntry(Method getEntityIteratorMethod) {
if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
mFirstVCardEmittedInDoCoMoCase = true;
// Previously we needed to emit empty data for this specific case, but actually
// this doesn't work now, as resolver doesn't return any data with "-1" contactId.
// TODO: re-introduce or remove this logic. Needs to modify unit test when we
// re-introduce the logic.
// return createOneEntryInternal("-1", getEntityIteratorMethod);
final String vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
getEntityIteratorMethod);
if (!mCursor.moveToNext()) {
Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
private String createOneEntryInternal(final String contactId,
final Method getEntityIteratorMethod) {
final Map&String, List&ContentValues&& contentValuesListMap =
new HashMap&String, List&ContentValues&&();
// The resolver may return the entity iterator with no data. It is possible.
// e.g. If all the data in the contact of the given contact id are not exportable ones,
they are hidden from the view of this method, though contact id itself exists.
EntityIterator entityIterator = null;
final Uri uri = mContentUriForRawContactsE
final String selection = Data.CONTACT_ID + "=?";
final String[] selectionArgs = new String[] {contactId};
if (getEntityIteratorMethod != null) {
// Please note that this branch is executed by unit tests only
entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
mContentResolver, uri, selection, selectionArgs, null);
} catch (IllegalArgumentException e) {
Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
e.getMessage());
} catch (IllegalAccessException e) {
Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
e.getMessage());
} catch (InvocationTargetException e) {
Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e);
throw new RuntimeException("InvocationTargetException has been thrown");
entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
uri, null, selection, selectionArgs, null));
if (entityIterator == null) {
Log.e(LOG_TAG, "EntityIterator is null");
return "";
if (!entityIterator.hasNext()) {
Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
return "";
while (entityIterator.hasNext()) {
Entity entity = entityIterator.next();
for (NamedContentValues namedContentValues : entity.getSubValues()) {
ContentValues contentValues = namedContentValues.
String key = contentValues.getAsString(Data.MIMETYPE);
if (key != null) {
List&ContentValues& contentValuesList =
contentValuesListMap.get(key);
if (contentValuesList == null) {
contentValuesList = new ArrayList&ContentValues&();
contentValuesListMap.put(key, contentValuesList);
contentValuesList.add(contentValues);
} finally {
if (entityIterator != null) {
entityIterator.close();
mCurrentContactID = contactId;
return buildVCard(contentValuesListMap);
private VCardPhoneNumberTranslationCallback mPhoneTranslationC
* Set a callback for phone number formatting. It will be called every time when this object
* receives a phone number for printing.
* When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored
* and the callback should be responsible for everything about phone number formatting.
* Caution: This interface will change. Please don't use without any strong reason.
public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
mPhoneTranslationCallback =
/** return whether the contact's account type is sim account */
private boolean isSimcardAccount(String contactid) {
boolean isSimAccount = false;
Cursor cursor = null;
cursor = mContentResolver.query(RawContacts.CONTENT_URI,
new String[] { RawContacts.ACCOUNT_NAME },
RawContacts.CONTACT_ID + "=?", new String[] { contactid },
if (null != cursor && 0 != cursor.getCount() && cursor.moveToFirst()) {
String accountName = cursor.getString(
cursor.getColumnIndex(RawContacts.ACCOUNT_NAME));
if (SIM_NAME.equals(accountName) || SIM_NAME_1.equals(accountName) ||
SIM_NAME_2.equals(accountName) || SIM_NAME_3.equals(accountName)) {
isSimAccount = true;
} finally {
if (null != cursor) {
cursor.close();
return isSimA
* Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
* {ContactsContract}. Developers can override this method to customize the output.
public String buildVCard(final Map&String, List&ContentValues&& contentValuesListMap) {
if (contentValuesListMap == null) {
Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
return "";
final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
.appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
.appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE),
mPhoneTranslationCallback)
.appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
.appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
.appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
.appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0
&& mCurrentContactID != null && !isSimcardAccount(mCurrentContactID)) {
builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
.appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
.appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
.appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
.appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
return builder.toString();
public void terminate() {
closeCursorIfAppropriate();
mTerminateCalled = true;
private void closeCursorIfAppropriate() {
if (!mCursorSuppliedFromOutside && mCursor != null) {
mCursor.close();
} catch (SQLiteException e) {
Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
mCursor = null;
protected void finalize() throws Throwable {
if (!mTerminateCalled) {
Log.e(LOG_TAG, "finalized() is called before terminate() being called");
} finally {
super.finalize();
* @return returns the number of available entities. The return value is undefined
* when this object is not ready yet (typically when {{@link #init()} is not called
* or when {@link #terminate()} is already called).
public int getCount() {
if (mCursor == null) {
Log.w(LOG_TAG, "This object is not ready yet.");
return mCursor.getCount();
* @return true when there's no entity to be built. The return value is undefined
* when this object is not ready yet.
public boolean isAfterLast() {
if (mCursor == null) {
Log.w(LOG_TAG, "This object is not ready yet.");
return false;
return mCursor.isAfterLast();
* @return Returns the error reason.
public String getErrorReason() {
return mErrorR
&posted on
阅读(...) 评论()

我要回帖

更多关于 loc是什么意思 的文章

 

随机推荐