2014-01-13 16:53:55

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

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

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

 private class ExportConfirmationListener implements DialogInterface.OnClickListener {
private final String mFileName; public ExportConfirmationListener(String fileName) {
mFileName = fileName;
} public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
mExportingFileName = mFileName;
progressDialogShow(); mListenerAdapter = new ListenerThreadBridgeAdapter(ExportVCardActivity.this);
mActualExportThread = new ExportVcardThread(null, ExportVCardActivity.this,
mFileName, mListenerAdapter, false);
mActualExportThread.start();
}
}
}

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

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

2. ExportVcardThread线程类

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

 try {
outputStream = new FileOutputStream(mFileName);
} catch (FileNotFoundException e) {
mErrorReason = mContext.getString(
R.string.spb_strings_fail_reason_could_not_open_file_txt, mFileName,
e.getMessage());
isComposed = false;
return;
} isComposed = compose(outputStream);

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

 private boolean compose(OutputStream outputStream) {

     try {
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()); (1) while (c.moveToNext()) {
if (mCanceled) {
break;
}
count++;
lookupKeys.append(c.getString(lookupClmn));
if (!c.isLast() && count < VCARD_REQUEST_LIMIT) {
lookupKeys.append(":");
continue;
}
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()); (2)
if (copyStream(buff, is, outputStream) > 0) {
hasActualEntry = true;
} if (mListener != null) {
mListener.incrementProgressBy(count);
}
count = 0;
lookupKeys.setLength(0);
}
}
return success;
}

(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()):

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

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

 private int copyStream(byte[] buff, InputStream is, OutputStream os) throws IOException {
int copiedLength = 0;
if (is == null || os == null) {
return copiedLength;
} int sz = 0;
do {
sz = is.read(buff);
if (sz > 0) {
os.write(buff, 0, sz);
copiedLength += sz;
}
} while (sz > 0); return copiedLength;
}

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

 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("(");
} else {
inBuilder.append(",");
}
long contactId = lookupContactIdByLookupKey(db, lookupKey);
inBuilder.append(contactId);
index++;
}
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)方法,代码如下:

 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);
try {
writer = new BufferedWriter(new OutputStreamWriter(stream));
if (!composer.init(uri, selection, selectionArgs, null, rawContactsUri)) { (1) 初始化composer
Log.w(TAG, "Failed to init VCardComposer");
return;
} 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,当然,不同的厂商为了满足自己的需求或许会对这个类进行扩展甚至重写。

 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);
} else {
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()方法:

 public boolean createOneEntry() {
if (mCursor == null || mCursor.isAfterLast()) {
mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
return false;
}
String name = null;
String vcard;
try {
if (mIsCallLogComposer) {
vcard = createOneCallLogEntryInternal();
} else if (mIdColumn >= 0) {
mContactsPhotoId = mCursor.getString(mCursor.getColumnIndex(Contacts.PHOTO_ID));
vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); (1)
} else {
Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
return false;
}
} return true;
}

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

 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};
final Uri uri;
if (Long.valueOf(contactId)>Profile.MIN_ID) {
uri = RawContactsEntity.PROFILE_CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContactsEntity.FOR_EXPORT_ONLY, "1")
.build();
} else {
uri = RawContactsEntity.CONTENT_URI.buildUpon()
.appendQueryParameter(ContactsContract.RawContactsEntity.FOR_EXPORT_ONLY, "1")
.build();
} 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()方法,代码如下:

 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 encodedFamily;
final String encodedGiven;
final String encodedMiddle;
final String encodedPrefix;
final String encodedSuffix; if (reallyUseQuotedPrintableToName) { (1)
encodedFamily = encodeQuotedPrintable(familyName);
encodedGiven = encodeQuotedPrintable(givenName);
encodedMiddle = encodeQuotedPrintable(middleName);
encodedPrefix = encodeQuotedPrintable(prefix);
encodedSuffix = encodeQuotedPrintable(suffix);
} else {
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 = displayName;
final boolean reallyUseQuotedPrintableToFullname =
mUsesQPToPrimaryProperties &&
!VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname); final String encodedFullname;
if (reallyUseQuotedPrintableToFullname) {
encodedFullname = encodeQuotedPrintable(fullname); // VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"
} else if (!mIsDoCoMo) {
encodedFullname = escapeCharacters(fullname);
} else {
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

 BEGIN:VCARD
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 //姓名部分
TEL;HOME;VOICE:9999999
END:VCARD

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

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

 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 < length; i++) {
char ch = str.charAt(i);
if (ch == '\r') {
if (i + 1 < length && str.charAt(i + 1) == '\n') {
i++;
}
tmpBuilder.append("\r\n");
} else if (ch == '\n') {
tmpBuilder.append("\r\n");
} else {
tmpBuilder.append(ch);
}
}
str = tmpBuilder.toString();
} final StringBuilder tmpBuilder = new StringBuilder();
int index = 0;
int lineCount = 0;
byte[] strArray = null; try {
strArray = str.getBytes(mCharsetString); (1)
}
while (index < strArray.length) {
tmpBuilder.append(String.format("=%02X", strArray[index])); (2)
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如下:

 D/D44     (32766): str = 大卫
D/D44 (32766): 1str = 大卫
D/D44 (32766): mCharsetString = UTF-8
D/D44 (32766): tmpBuilder = =E5
D/D44 (32766): tmpBuilder = =E5=A4
D/D44 (32766): tmpBuilder = =E5=A4=A7
D/D44 (32766): tmpBuilder = =E5=A4=A7=E5
D/D44 (32766): tmpBuilder = =E5=A4=A7=E5=8D
D/D44 (32766): tmpBuilder = =E5=A4=A7=E5=8D=AB

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

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

 String str = "大卫";
byte[] strArray = null;
strArray = str.getBytes("UTF-8");
int index = 0;
while (index < strArray.length) {
System.out.println(String.format("=%02X", strArray[index]));
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)))){
continue;
} byte[] data = contentValues.getAsByteArray(Photo.PHOTO); (1)
if (data == null) {
continue;
}
String photoType;
// 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";
} else {
Log.d(LOG_TAG, "Unknown photo type. Ignore.");
continue;
}
byte[] newData = convertToSmallJpg(data, photoType);
if (newData != null) {
data = newData;
photoType = "JPEG";
}
final String photoString = VCardUtils.encodeBase64(data); (2)
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)方法代码如下:

 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: // [111111][11 0000][0000 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;
break;
case 2: // [111111][11 1111][1111 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;
break;
} return new String(charBuffer);
}

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

 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);
} else {
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 < length; 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,终于结束了,战线太长了,甚至有点凌乱,不过希望读者沿着主线看,不要太在乎细节,比如那个方法是怎么调用的或者这个参数是什么时候初始化的。

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

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

最后将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.
*/
package com.android.vcard; import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Entity;
import android.content.Entity.NamedContentValues;
import android.content.EntityIterator;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Event;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Note;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.RawContactsEntity;
import android.provider.ContactsContract;
import android.text.TextUtils;
import android.util.Log; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map; /**
* <p>
* The class for composing vCard from Contacts information.
* </p>
* <p>
* Usually, this class should be used like this.
* </p>
* <pre class="prettyprint">VCardComposer composer = null;
* try {
* composer = new VCardComposer(context);
* composer.addHandler(
* composer.new HandlerForOutputStream(outputStream));
* if (!composer.init()) {
* // Do something handling the situation.
* return;
* }
* while (!composer.isAfterLast()) {
* if (mCanceled) {
* // Assume a user may cancel this operation during the export.
* return;
* }
* if (!composer.createOneEntry()) {
* // Do something handling the error situation.
* return;
* }
* }
* } finally {
* if (composer != null) {
* composer.terminate();
* }
* }</pre>
* <p>
* Users have to manually take care of memory efficiency. Even one vCard may contain
* image of non-trivial size for mobile devices.
* </p>
* <p>
* {@link VCardBuilder} is used to build each vCard.
* </p>
*/
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> sImMap; static {
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 mVCardType;
private final ContentResolver mContentResolver; 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 mFirstVCardEmittedInDoCoMoCase; private Cursor mCursor;
private boolean mCursorSuppliedFromOutside;
private int mIdColumn;
private Uri mContentUriForRawContactsEntity; private final String mCharset; private String mCurrentContactID = null; private boolean mInitDone;
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.
* @hide
*/
public VCardComposer(final Context context, ContentResolver resolver,
final int vcardType, String charset, final boolean careHandlerErrors) {
// Not used right now
// mContext = context;
mVCardType = vcardType;
mContentResolver = resolver; 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) {
try {
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;
}
} else {
try {
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 = charset;
} else {
/* Log.w(LOG_TAG,
"The charset \"" + charset + "\" is used while "
+ SHIFT_JIS + " is needed to be used."); */
if (TextUtils.isEmpty(charset)) {
mCharset = SHIFT_JIS;
} else {
/*
try {
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 = charset;
}
}
} else {
if (TextUtils.isEmpty(charset)) {
mCharset = UTF_8;
} else {
/*try {
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 = charset;
}
} 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}.
* <code>
* String selection = Data.CONTACT_ID + "=?";
* String[] selectionArgs = new String[] {contactId};
* Cursor cursor = mContentResolver.query(
* contentUriForRawContactsEntity, null, selection, selectionArgs, null)
* </code>
*
* 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.
*
* <code>
* Cursor cursorForId = mContentResolver.query(
* contentUri, projection, selection, selectionArgs, sortOrder);
* </code>
*
* After that, we'll obtain data for each contactId in the list.
*
* <code>
* Cursor cursorForContent = mContentResolver.query(
* contentUriForRawContactsEntity, null,
* Data.CONTACT_ID + "=?", new String[] {contactId}, null)
* </code>
*
* {@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
*
* @hide
*/
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.
* @hide
*/
public boolean init(Cursor cursor) {
if (!initInterFirstPart(null)) {
return false;
}
mCursorSuppliedFromOutside = true;
mCursor = cursor;
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);
} /**
* @hide
*/
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");
}
return vcard;
} 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;
try {
final Uri uri = mContentUriForRawContactsEntity;
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
try {
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");
}
} else {
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.values;
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 mPhoneTranslationCallback;
/**
* <p>
* Set a callback for phone number formatting. It will be called every time when this object
* receives a phone number for printing.
* </p>
* <p>
* 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.
* </p>
* <p>
* Caution: This interface will change. Please don't use without any strong reason.
* </p>
*/
public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
mPhoneTranslationCallback = callback;
} /** return whether the contact's account type is sim account */
private boolean isSimcardAccount(String contactid) {
boolean isSimAccount = false;
Cursor cursor = null;
try {
cursor = mContentResolver.query(RawContacts.CONTENT_URI,
new String[] { RawContacts.ACCOUNT_NAME },
RawContacts.CONTACT_ID + "=?", new String[] { contactid },
null);
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 isSimAccount;
} /**
* 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 "";
} else {
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) {
try {
mCursor.close();
} catch (SQLiteException e) {
Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
}
mCursor = null;
}
} @Override
protected void finalize() throws Throwable {
try {
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 0;
}
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 mErrorReason;
}
}

Phonebook 导出联系人到SD卡(.vcf)的更多相关文章

  1. 使用Arduino和SD卡模块记录数据并导出到Excel

    在本篇文章中,我们将学习如何基于Arduino开发板使用一个SD卡模块.同时结合DS3231实时时钟模块,我们将制作一个数据记录仪的示例,在示例中,我们将温度传感器的数据存储到SD卡中,并将这些数据导 ...

  2. Ye.云狐J2刷机笔记 | 完美切换内部存储卡和SD卡的改法.vold.fstab

    ================================================================================Ye.完美切换内部存储卡和SD卡成功.v ...

  3. 基于V7的新版RL-USB V6.X + RL-FlashFS V6.X模板,操作CLASS10的SD卡速度12-15MB/S,含RTX5和FreeRTOS两版

    说明: 1.如果需要RL-USB源码的话,将DAPLink(CMSIS-DAP)里面的USB代码导出来即可,DAPLink开源了RL-USB的Device代码.      也可以反过来,在工程模板的基 ...

  4. android计算每个目录剩余空间丶总空间以及SD卡剩余空间

    ublic class MemorySpaceCheck { /** * 计算剩余空间 * @param path * @return */ public static String getAvail ...

  5. Android将应用调试log信息保存在SD卡

    转载:http://blog.csdn.net/way_ping_li/article/details/8487866 把自己应用的调试信息写入到SD卡中. package com.sdmc.hote ...

  6. SD卡的监听

    摘要:在一般应用中,如果需要对占用空间比较大的文件操作,需要监听SD卡的状态,Android中对SD卡的监听状态操作步骤如下: 一.创建一个类继承于BroadcastReceiver public c ...

  7. Android SD卡存储

    原创文章,转载请注明出处:http://www.cnblogs.com/baipengzhan/p/Android_SDcard_store.html 一 概念 SD卡存储空间比较大,当需要存取较大的 ...

  8. 关于手机的内置SD卡与外置SD卡

    对于安卓2.3的系统来说,Environment.getExternalStorageDirectory()获取的目录是内置SD卡还是外置SD卡是无法保证的, 和手机厂商的修改有关,只能通过Envir ...

  9. Android从网络某个地址下载文件、写入SD卡

    首先创建一个HttpDownloader类,获取下载文件的网络地址,将文件下载下来以String流的方式返回: public String download(String urlStr){ //url ...

随机推荐

  1. google bookmarket api

    引用: 最近做了google书签同步的模块,发现google并没有公开bookmark相关的api接口,在网上也找了些资料,通过自己抓包分析,测试,总结下使用bookmark接口的心得,我是在andr ...

  2. 较全的IT方面帮助文档

    http://www.shouce.ren/post/d/id/108632 XSLT参考手册-新.CHMhttp://www.shouce.ren/post/d/id/108633 XSL-FO参考 ...

  3. NaN

    not a number 全称, 任何数/0 js会出现NaN alert(NaN==NaN); // false isNaN(NaN); // true alert(isNaN(10)); // f ...

  4. Codeforces 749B:Parallelogram is Back(计算几何)

    http://codeforces.com/problemset/problem/749/B 题意:已知平行四边形三个顶点,求另外一个顶点可能的位置. 思路:用向量来做. #include <c ...

  5. mysql之show engine innodb status解读

    注:以下内容为根据<高性能mysql第三版>和<mysql技术内幕innodb存储引擎>的innodb status部分的个人理解,如果有错误,还望指正!!   innodb存 ...

  6. 第四章 函数(JavaScript:语言精粹)

    函数包含一组语句,用来指定对象的行为,其代码可以用来重复使用.   一般来说,编程就是将一组需求分解成一组函数和数据结构的技能.   概览:函数对象 | 函数字面量 | 调用 | 方法调用模式 | 函 ...

  7. mysql5.6中 order by 多个字段排序问题

    今天用order by排序 后面跟了多个字段,如sql语句: SELECT a.id,a.loginname,a.address,u.id,u.`name`,u.address FROM admin_ ...

  8. excel如何设置输入数字后单元格自动填充颜色

    在使用excel的过程中,有时需要在输入数字时,突出显示这些单元格,突出显示可以用有填充颜色的单元格来表示.为了实现这样的效果,需要借助excel的条件格式. 工具/原料 电脑 Excel 2010 ...

  9. 高级智能研究计划(IARPA):大脑皮层建模

    哈哈,看到了一篇我最感兴趣的领域的新闻报导,可以深挖里面的各种细节. Quanta Magazine: Illuminating Science - 原文出处 卡内基·梅隆大学 - Tai Sing ...

  10. Git最佳实践

    1.git init 2.git add. 3.git add README.md 4.git commit -m "init" 5.git remote add origin h ...