Phonebook 导入SD上的.vcf联系人

2014-01-11 17:29:22

1. 当用户选择Phonebook中从SD卡导入联系人的操作后,程序回调转到ImportVCardActivity,然后用户选择好要导入的.vcf文件,并点击“确定”button,调用ImportVCardActivity中的importMultipleVCardFromExternalStorage()方法:

Phonebook 导入SD上的.vcf联系人
1     private void importMultipleVCardFromExternalStorage(
2             final List<VCardFile> selectedVCardFileList) {
3         mHandler.post(new Runnable() {
4             public void run() {
5                 mVCardReadThread = new VCardReadThread(selectedVCardFileList);
6                 checkFinishingAndShowDialog(R.id.dialog_reading_vcard);
7             }
8         });
9     }
Phonebook 导入SD上的.vcf联系人

先new一个VCardReadThread对象,然后在onCreateDialog()-->getReadingVCardDialog()中start这个Thread,如下:

Phonebook 导入SD上的.vcf联系人
 1 private Dialog getReadingVCardDialog() {
 2         if (mProgressDialogForReadVCard == null) {
 3             String title = getString(R.string.spb_strings_import_all_txt);
 4             String message = getString(R.string.reading_vcard_message);
 5             mProgressDialogForReadVCard = new ProgressDialog(this);
 6             mProgressDialogForReadVCard.setTitle(title);
 7             mProgressDialogForReadVCard.setMessage(message);
 8             mProgressDialogForReadVCard.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
 9             mProgressDialogForReadVCard.setOnCancelListener(mVCardReadThread);
10             mProgressDialogForReadVCard.setCanceledOnTouchOutside(false);
11             mProgressDialogForReadVCard.setOnShowListener(new OnShowListener() {
12                 public void onShow(DialogInterface dialog) {
13                     if (mVCardReadThread != null) {
14                         mVCardReadThread.start();
15                     } else {
16                         SpbLog.e(LOG_TAG, "mVCardReadThread is null");
17                         dialog.dismiss();
18                         mProgressDialogForReadVCard = null;
19                         finish();
20                     }
21                 }
22             });
23         }
24         return mProgressDialogForReadVCard;
25     }
Phonebook 导入SD上的.vcf联系人

我们具体看VCardReadThread类的run()方法,以导入多个.vcf文件为例:

Phonebook 导入SD上的.vcf联系人
 1 // Read multiple files.
 2 List<VCardFile> verifiedVCardFileList = new ArrayList<VCardFile>();
 3 List<VCardSourceDetector> verifiedVCardFileDetector = new ArrayList<VCardSourceDetector>();
 4 int count = 0;
 5 for (VCardFile vcardFile : mSelectedVCardFileList) {
 6     if (mCanceled) {
 7         return;
 8     }
 9     VCardEntryCounter counter = new VCardEntryCounter();
10     VCardSourceDetector detector = new VCardSourceDetector();
11     VCardBuilderCollection builderCollection = new VCardBuilderCollection(
12             Arrays.asList(counter, detector));
13     Uri uri = vcardFile.getUri();
14     boolean result = false;
15     try {
16         result = readOneVCardFile(uri, VCardConfig.DEFAULT_CHARSET,
17                 builderCollection, null, true, null, null);
18     }
19     if (result) {
20         count += counter.getCount();
21         verifiedVCardFileList.add(vcardFile);
22         verifiedVCardFileDetector.add(detector);
23     }
24 }
25 mProgressDialogForReadVCard.setProgressNumberFormat(
26         getString(R.string.spb_reading_contacts_txt));
27 mProgressDialogForReadVCard.setMax(count);
28 mProgressDialogForReadVCard.setProgress(0);
29 mProgressDialogForReadVCard.setIndeterminate(false);
30 
31 int i = 0;
32 for (VCardFile vcardFile : verifiedVCardFileList) {
33     if (mCanceled) {
34         return;
35     }
36     Uri uri = vcardFile.getUri();
37     doActuallyReadOneVCard(uri, mAccount, true, verifiedVCardFileDetector.get(i),
38             mErrorFileNameList);
39     i++;
40 }
Phonebook 导入SD上的.vcf联系人

有两个for循环,第一个是循环中获得了即将导入的联系人的总个数,因为这个信息要显示在ProgressBar进度条上,同时将vcardFile添加到verifiedVCardFileList中,这个List对象会在第二个循环中使用。再次我们先简单的看一下VCardFile是做什么的,代码如下:

Phonebook 导入SD上的.vcf联系人
 1 class VCardFile {
 2     private String mName;
 3     private Uri mUri;
 4     private long mLastModified;
 5 
 6     public VCardFile(String name, Uri uri, long lastModified) {
 7         mName = name;
 8         mUri = uri;
 9         mLastModified = lastModified;
10     }
11 
12     public String getName() {
13         return mName;
14     }
15 
16     public Uri getUri() {
17         return mUri;
18     }
19 
20     public long getLastModified() {
21         return mLastModified;
22     }
23 }
Phonebook 导入SD上的.vcf联系人

可以看到这个类的功能就是简单的封装.vcf File,包括对应的文件名称,uri以及最后一次修改时间。继续看第二个循环,其中调用了如下语句:

1 doActuallyReadOneVCard(uri, mAccount, true, verifiedVCardFileDetector.get(i),
2                                 mErrorFileNameList);

传入了包含所有vcardFile的mErrorFileNameList, 帐号信息等,进入doActuallyReadOneVCard()方法:

Phonebook 导入SD上的.vcf联系人
 1 private boolean doActuallyReadOneVCard(Uri uri, Account account,
 2         boolean showEntryParseProgress,
 3         VCardSourceDetector detector, List<String> errorFileNameList) {
 4     final Context context = ImportVCardActivity.this;
 5     mProgressShower = null;
 6     if (showEntryParseProgress) {
 7         mProgressShower = new ProgressShower(mProgressDialogForReadVCard,
 8                 context.getString(R.string.reading_vcard_message),
 9                 ImportVCardActivity.this, mHandler);
10         mProgressShower.setListener(ImportVCardActivity.this);
11     }
12     mCommitter = new EntryCommitter(mResolver);
13     String estimatedCharset = detector.getEstimatedCharset();
14     VCardDataBuilder builder;
15 
16     // Use iso-8859-1 CHARSET to read VCard files, then use the charset
17     // inside the VCard files to decode special strings.
18     String targetCharset = estimatedCharset != null ? estimatedCharset : "utf-8";
19     String sourceCharset = "iso-8859-1";
20     if (try21) {
21         // the targetCharset parameter (2nd one) will have no effect if
22         // a CHARSET parameter is already provided in vcard. This is
23         // the default value if nothing better can be found.
24         builder = new VCardDataBuilder(sourceCharset, targetCharset, false, vcardType,
25                 mAccount, mAccountType);
26         builder.addEntryHandler(mCommitter);
27         if (mProgressShower != null) {
28             builder.addEntryHandler(mProgressShower);
29         }
30         try {
31             VCardParser parser = new VCardParser_V21(detector);
32             parser.setBaseCharset(targetCharset);
33             if (readOneVCardFile(uri, sourceCharset, builder, detector, false,
34                     parser, mErrorFileNameList)) {
35                 getTestData().fileImportedOK(uri);
36                 return true;
37             } else {
38                 return false;
39             }
40 
41         }
42     }
43 
44     // NOT REACHED
45     return false;
46 }
Phonebook 导入SD上的.vcf联系人

我们的.vcf文件version是“VERSION:2.1”,targetCharset和sourceCharset使用来对.vcf文件进行编码的格式,这个方法中最重要的是调用了readOneVCardFile()方法,同时传入了VCardDataBuilder builder和VCardParser_V21 parser,这两个对象很重要,以后会重点用到。进入readOneVCardFile()方法,在这个方法中调用了callParser(parser, uri, charset, builder),如下:

Phonebook 导入SD上的.vcf联系人
 1 private void callParser(VCardParser parser, Uri uri, String charset,
 2         VCardBuilder builder) throws IOException, VCardException {
 3     mVCardParser = parser;
 4 
 5     if (mBrand == null) {
 6         final String brand = Configuration.getInstance(ImportVCardActivity.this).getBrandName();
 7         mBrand = brand != null ? brand : "";
 8     }
 9     mVCardParser.setBrand(mBrand);
10     InputStream is = null;
11     try {
12         is = mResolver.openInputStream(uri);
13         mVCardParser.parse(is, charset, builder, mCanceled);
14     } finally {
15         try {
16             if (is != null) {
17                 is.close();
18             }
19         } catch (IOException e) {
20             // Failed to close input stream - do nothing
21         }
22     }
23 }
Phonebook 导入SD上的.vcf联系人

我们看到在这里将用流的方式访问.vcf文件,同时讲得到的流对象当作参数传给parse()方法,前面说过,mVCardParser是一个VCardParser_V21,所以进入VCardParser_V21看他的parse()方法,当然,他又调用了重载的parse(is, charset, builder),调用流程如下:

parse(is, charset, builder)-->parseVCardFile()-->parseOneVCard(firstReading)-->parseItems()-->parseItem(),如下:

Phonebook 导入SD上的.vcf联系人
 1 protected boolean parseItem() throws IOException, VCardException {
 2     mEncoding = sDefaultEncoding;
 3 
 4     String line = getNonEmptyLine();   (1)
 5     long start = System.currentTimeMillis();
 6 
 7     mParamCharsetTmp = null;
 8     String[] propertyNameAndValue = separateLineAndHandleGroup(line);
 9     if (propertyNameAndValue == null) {
10         return true;
11     }
12     if (propertyNameAndValue.length != 2) {
13         throw new VCardInvalidLineException("Invalid line \"" + line + "\"");
14     }
15     String propertyName = propertyNameAndValue[0].toUpperCase();   (2)
16     String propertyValue = propertyNameAndValue[1];
17 
18     mTimeParseLineAndHandleGroup += System.currentTimeMillis() - start;
19 
20     if (propertyName.equals("ADR") || propertyName.equals("ORG") ||
21             propertyName.equals("N")
22             // "SOUND" is not multi-part on vCard specification but mere assumption.
23             // Some Japanese phones use this as multi (DoCoMo spec).
24             || propertyName.equals("SOUND")
25             ) {
26         start = System.currentTimeMillis();
27         Log.d("D4", "propertyName = " + propertyName);
28         Log.d("D4", "propertyValue = " + propertyValue);
29         handleMultiplePropertyValue(propertyName, propertyValue);    (3)
30         mTimeParseAdrOrgN += System.currentTimeMillis() - start;
31         return false;
32     } else if (propertyName.equals("AGENT")) {
33         handleAgent(propertyValue);
34         return false;
35     } else if (isValidPropertyName(propertyName)) {
36         if (propertyName.equals("BEGIN")) {
37             if (propertyValue.equals("VCARD")) {
38                 throw new VCardNestedException("This vCard has nested vCard data in it.");
39             } else {
40                 throw new VCardException("Unknown BEGIN type: " + propertyValue);
41             }
42         } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) {
43             throw new VCardVersionException("Incompatible version: " +
44                     propertyValue + " != " + getVersion());
45         }
46         start = System.currentTimeMillis();
47         handlePropertyValue(propertyName, propertyValue);    (3)
48         mTimeParsePropertyValues += System.currentTimeMillis() - start;
49         return false;
50     }
51 
52   throw new VCardNotSupportedException("Unknown property name: \"" +
53               propertyName + "\"");
54 }
Phonebook 导入SD上的.vcf联系人

首先看(1)的代码,从流文件中读取一行,所以可以发现其实就是一行一行读取并解析.vcf文件的。看(2)的代码,取出来propertyName,然后根据propertyName来获得propertyValue,下面是一段log,

Phonebook 导入SD上的.vcf联系人
 1 D/D44444444444444444444444( 9909): propertyName = VERSION
 2 D/D44444444444444444444444( 9909): propertyValue = 2.1
 3 D/D42     ( 9909): propertyName = VERSION
 4 D/D42     ( 9909): propertyValue = 2.1
 5 D/D44444444444444444444444( 9909): propertyName = N
 6 D/D44444444444444444444444( 9909): propertyValue = =E9=AD=8F=E4=BC=9F;;;;
 7 D/D4      ( 9909): propertyName = N
 8 D/D4      ( 9909): propertyValue = =E9=AD=8F=E4=BC=9F;;;;
 9 D/D4      ( 9909): builder.toString().trim() = 
10 D/D44444444444444444444444( 9909): propertyName = FN
11 D/D44444444444444444444444( 9909): propertyValue = =E9=AD=8F=E4=BC=9F
12 D/D42     ( 9909): propertyName = FN
13 D/D42     ( 9909): propertyValue = =E9=AD=8F=E4=BC=9F
14 D/D44444444444444444444444( 9909): propertyName = TEL
15 D/D44444444444444444444444( 9909): propertyValue = 18611976642
16 D/D42     ( 9909): propertyName = TEL
17 D/D42     ( 9909): propertyValue = 18611976642
Phonebook 导入SD上的.vcf联系人

而一个典型的.vcf文件,这个文件中只包含一个联系人,名字是**,号码是18611976642,如下:

Phonebook 导入SD上的.vcf联系人
1 BEGIN:VCARD
2 VERSION:2.1
3 N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E9=AD=8F=E4=BC=9F;;;;
4 FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E9=AD=8F=E4=BC=9F
5 TEL;HOME;VOICE:18611976642
6 END:VCARD
Phonebook 导入SD上的.vcf联系人

这样就取到了联系人包含的所有信息。

在每一个判断语句块里都有一个方法,看(3)的代码,这两个方法最终都会调用mBuilder.propertyValues(v)将propertyValue值所在的list存起来,因此最终的解析工作也是在propertyValues()方法里完成的。下面再看一个更复杂的.vcf文件:

Phonebook 导入SD上的.vcf联系人
BEGIN:VCARD
VERSION:2.1
N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E6=9D=A8=E9=A3=9E=E5=B9=B4;;;;
FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E6=9D=A8=E9=A3=9E=E5=B9=B4
TEL;HOME;VOICE:18896784536
TEL;CELL:9999999999
EMAIL;HOME:yangling@gmail.com
PHOTO;ENCODING=BASE64;TYPE=JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCA
 gMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGB
 IUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQ
 UFBQUFBQUFBQUFBQUFBQUFBT/wAARCABgAGADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAA
 AAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxF
 DKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWm
 NkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMX
 Gx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAA
 AAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIF
 EKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZG
 VmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcb
 HyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD4oB44IakBwCBw
 MUoAHbr2xSbSST2PtQrGauSwEkjsBxzXuH7J+jHV/jBojADy7MS3TcZwVRtpP/AyteHwKd+Tz
 +HSvqr9hzQnn8QeJNW8pnNpaR268f8APRy//tGmtNxPyPtvWdcTVoYooyVjg4ZiOoKjkfnXE6
 LeJqGpOS2wiNAEb+NnzKy89cBh+XtWreSXU9hLIICrkGMDgZBJUH8gprD0y3ksbm6M4QHdIyN
 5gHfanGeygVsrWMWdt5q6V4e8qJyZ5XUgZ5+dy5z+Ax+FZkLtAV+YZBzkewNYQv1lupnmuYEM
 TIAPNGGCocHr/tGlvtfsYkk/4mVsr7QP9Z9T2qlYV2UbgpqOrvIrAr978gQv67qZqE5kW3izn
 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
 U8rBp0QR7xyWAHGfw4rVU3shcyP/Z
Phonebook 导入SD上的.vcf联系人

这个联系人信息如下:

Phonebook 导入SD上的.vcf联系人

可以发现,中文姓名和头像是进行了编码的,其他比如phone number,email等直接文本保存,那么我们进入propertyValues()方法看一下姓名和头像是怎么解析的,前面说过VCardDataBuilder mBuilder很重要,那么当然propertyValues()方法也是在VCardDataBuilder里面喽,代码如下:

Phonebook 导入SD上的.vcf联系人
 1     public void propertyValues(List<String> values) {
 2         if (values == null || values.size() == 0) {
 3             return;
 4         }
 5 
 6         final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET");
 7         String charset =
 8             ((charsetCollection != null) ? charsetCollection.iterator().next() : null);
 9         String targetCharset = CharsetUtils.nameForDefaultVendor(charset);
10 
11         final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING");
12         String encoding =
13             ((encodingCollection != null) ? encodingCollection.iterator().next() : null);
14 
15         if (targetCharset == null || targetCharset.length() == 0) {
16             targetCharset = mTargetCharset;
17         }
18 
19         if (!Charset.isSupported(targetCharset)
20                 && (("shift_jis").equalsIgnoreCase(charset)
21                     || "shift-jis".equalsIgnoreCase(charset)
22                     || "sjis".equalsIgnoreCase(charset))) {
23             Log.w(LOG_TAG, targetCharset + " is not supported. Use shif_jis encoding.");
24             targetCharset = "shift_jis";
25         }
26 
27         for (String value : values) {
28             Log.d("D2D", "value = " + value);
29             Log.d("D2D", "handleOneValue(value, targetCharset, encoding) = " + handleOneValue(value, targetCharset, encoding));
30             mCurrentProperty.addToPropertyValueList(
31                     handleOneValue(value, targetCharset, encoding));
32         }
33     }
Phonebook 导入SD上的.vcf联系人

以杨飞年为例,导入只包含他的信息的.vcf文件,看log如下:

Phonebook 导入SD上的.vcf联系人
D/D2D     (11785): value = 2.1
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 2.1
D/D2D     (11785): value = =E6=9D=A8=E9=A3=9E=E5=B9=B4
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 杨飞年
D/D2D     (11785): value = 
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 
D/D2D     (11785): value = 
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 
D/D2D     (11785): value = 
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 
D/D2D     (11785): value = 
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 
D/D2D     (11785): value = =E6=9D=A8=E9=A3=9E=E5=B9=B4
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 杨飞年
D/D2D     (11785): value = 18896784536
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 18896784536
D/D2D     (11785): value = 9999999999
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = 9999999999
D/D2D     (11785): value = yangling@gmail.com
D/D2D     (11785): handleOneValue(value, targetCharset, encoding) = yangling@gmail.com
D/D2D     (11785): value = /9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBA QIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDA kKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo KCgoKCgoKCgoKCgoKCgoKCgr/wAARCABgAGADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAA AAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxF DKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWm NkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMX Gx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAA AAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIF EKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZG VmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcb HyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD8u45RswhV6bHL hWjjGF2etLHDGBkL97+HZSLBIWZweG/2KmHszCPP0J7LcSMHaqfL81fU/wDwSb8GP4v/AGyPC 88aL5OjpdajL8m7a0cMnls3/bZo6+WLGN1lLOuf+A/dr7//AOCGfgS6v/iF42+If9myStpGiW 9jF8v/AD8TNN/7aVcPd+ImWvwn6keM/HNr4tsoNPsXZIbD5ZZGX7ytGvzL/wB9V5f4G1m18Q+ JZWkm8pltYVSGT/ltJNuupI/m+9tWRf8Avn/Zrf1mfXr3w/cXselMkjK0CL8q7lZmjVv++Vja uW8K6beaFqV/LqyQg+bcPBJ9qVf4vLh+Xd/DGq10R5eU5JW1PT/tMPhT4dHT7C5Zru6uI2Rd3 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 H40+IN5q0dnPpOhyfatJZPlv
Phonebook 导入SD上的.vcf联系人

将“=E6=9D=A8=E9=A3=9E=E5=B9=B4”解析成了“杨飞年”,至于头像,好像还是一堆编码,继续看。

调用了mCurrentProperty.addToPropertyValueList()方法,代码在ContactStruct.java,如下:

Phonebook 导入SD上的.vcf联系人
1 public void addToPropertyValueList(String propertyValue) {
2     // Trim trailing nul-chars, could be present if the value
3     // originally originated from a SIM.
4     propertyValue = StringUtil.trimTrailingNul(propertyValue);
5 
6     if (propertyValue != null) {
7         mPropertyValueList.add(propertyValue.replace("\r\n", "\n"));
8     }
9 }
Phonebook 导入SD上的.vcf联系人

很简单,只是将传进来的propertyValue保存到List<String> mPropertyValueList对象中,到此,我们发现所有联系人相关的信息其实最后都会保存在mPropertyValueList中,也就是说,将以个联系人信息从.vcf文件中读入、解析之后保存到这里,那么最后一个问题就是什么时候将解析后的联系人信息保存到数据库的呢?

还记得我们前面有提到这样的调用流程:

parse(is, charset, builder)-->parseVCardFile()-->parseOneVCard(firstReading)-->parseItems()-->parseItem()

其中调用了parseOneVCard(firstReading)方法,代码如下:

Phonebook 导入SD上的.vcf联系人
 1     private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException {
 2         boolean allowGarbage = false;
 3         if (firstReading) {
 4             if (mNestCount > 0) {
 5                 for (int i = 0; i < mNestCount; i++) {
 6                     if (!readBeginVCard(allowGarbage)) {
 7                         return false;
 8                     }
 9                     allowGarbage = true;
10                 }
11             }
12         }
13 
14         // Allow garbage lines before "BEGIN:VCARD" at any time.
15         // Some of the vCard from KDDI Phone contains garbage lines
16         // between ‘END:VCARD‘ and next ‘BEGIN:VCARD‘.
17         if (!readBeginVCard(true)) {
18             mBuilder.close();
19             return false;
20         }
21         long start;
22         if (mBuilder != null) {
23             start = System.currentTimeMillis();
24             mBuilder.startRecord("VCARD");
25             mTimeReadStartRecord += System.currentTimeMillis() - start;
26         }
27         start = System.currentTimeMillis();
28         parseItems();
29         mTimeParseItems += System.currentTimeMillis() - start;
30         readEndVCard(true, false);
31         if (mBuilder != null) {
32             start = System.currentTimeMillis();
33             mBuilder.endRecord();
34             mTimeReadEndRecord += System.currentTimeMillis() - start;
35         }
36         return true;
37     }
Phonebook 导入SD上的.vcf联系人

其中不光调用了parseItems(),还调用了mBuilder.endRecord(),也就是VCardDataBuilder.endRecord(),代码如下:

Phonebook 导入SD上的.vcf联系人
1     public void endRecord() {
2         mCurrentContactStruct.consolidateFields();
3 
4         for (EntryHandler entryHandler : mEntryHandlers) {
5             entryHandler.onEntryCreated(mCurrentContactStruct);
6         }
7         mCurrentContactStruct.clear();
8     }
Phonebook 导入SD上的.vcf联系人

调用了entryHandler.onEntryCreated(mCurrentContactStruct),代码在EntryCommitter.java,如下:

Phonebook 导入SD上的.vcf联系人
1     public Uri onEntryCreated(final ContactStruct contactStruct) {
2         long start = System.currentTimeMillis();
3         if (contactStruct != null) {
4             mRawContactUri = contactStruct.pushIntoContentResolver(mContentResolver);
5         }
6         mTimeToCommit += System.currentTimeMillis() - start;
7         return mRawContactUri;
8     }
Phonebook 导入SD上的.vcf联系人

EntryCommitter类我们在前面doActuallyReadOneVCard()方法中见过。其又调用了ontactStruct.pushIntoContentResolver(mContentResolver),这一下子又跑到ContactStruct.pushIntoContentResolver()方法,代码部分如下:

Phonebook 导入SD上的.vcf联系人
 1 public Uri pushIntoContentResolver(ContentResolver resolver) {
 2         if (mBuilder == null) {
 3             mBuilder = new ContactOperationBuilder(resolver);
 4         }
 5         mBuilder.openSession();
 6         mBuilder.newInsert(RawContacts.CONTENT_URI);
 7 
 8         String myGroupsId = null;
 9         String myGroupsRowId = null;
10         boolean hasValue = false;
11 
12         if (mAccount != null) {
13             mBuilder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
14             mBuilder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
15 
16             // Assume that caller side creates this group if it does not exist.
17             // TODO: refactor this code along with the change in GoogleSource.java
18             if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) {
19                 Cursor cursor = null;
20                 try {
21                     cursor = resolver.query(Groups.CONTENT_URI,
22                         new String[] {Groups.SOURCE_ID, Groups._ID },
23                         Groups.TITLE + "=?"
24                             + " and " + Groups.ACCOUNT_TYPE + "=?"
25                             + " and " + Groups.ACCOUNT_NAME + "=?",
26                         new String[] {GoogleAccountType.GOOGLE_MY_CONTACTS_GROUP,
27                             mAccount.type, mAccount.name}, null);
28                     if (cursor != null && cursor.moveToFirst()) {
29                         myGroupsId = cursor.getString(0);
30                         myGroupsRowId = cursor.getString(1);
31                     }
32                 } finally {
33                     if (cursor != null) {
34                         cursor.close();
35                     }
36                 }
37             }
38         } else {
39             String account = null;
40             mBuilder.withValue(RawContacts.ACCOUNT_NAME, account);
41             mBuilder.withValue(RawContacts.ACCOUNT_TYPE, account);
42 
43         }
44         mBuilder.build(null, true);
45 
46         // In case that one contact could be committed in different batch
47         // If last commit fail, mDiscardRest set true to get rid of rest of
48         // the contact info.
49 
50         if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName)
51                 && TextUtils.isEmpty(mPhoneticFamilyName) && TextUtils.isEmpty(mPhoneticGivenName) && TextUtils
52                     .isEmpty(mFullName))) {
53             mBuilder.newInsert(Data.CONTENT_URI);
54             mBuilder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
55 
56             mBuilder.withValue(StructuredName.GIVEN_NAME, mGivenName);
57             mBuilder.withValue(StructuredName.FAMILY_NAME, mFamilyName);
58             mBuilder.withValue(StructuredName.MIDDLE_NAME, mMiddleName);
59             mBuilder.withValue(StructuredName.PREFIX, mPrefix);
60             mBuilder.withValue(StructuredName.SUFFIX, mSuffix);
61 
62             mBuilder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName);
63             mBuilder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName);
64             mBuilder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName);
65 
66             mBuilder.withValue(StructuredName.DISPLAY_NAME, getDisplayName());
67             mBuilder.build(StructuredName.RAW_CONTACT_ID, false);
68             hasValue = true;
69         }
70 
71         if (mNickNameList != null && mNickNameList.size() > 0) {
72             boolean first = true;
73             for (String nickName : mNickNameList) {
74                 mBuilder.newInsert(Data.CONTENT_URI);
75                 mBuilder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
76 
77                 mBuilder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
78                 mBuilder.withValue(Nickname.NAME, nickName);
79                 if (first) {
80                     mBuilder.withValue(Data.IS_PRIMARY, 1);
81                     first = false;
82                 }
83                 mBuilder.build(Nickname.RAW_CONTACT_ID, false);
84                 hasValue = true;
85             }
86         }
87         ......
Phonebook 导入SD上的.vcf联系人

看到了吧,在组装ContactOperationBuilder,这是要保存联系人到数据库的节奏啊,果然,继续看mBuilder.build(***)方法,如下:

Phonebook 导入SD上的.vcf联系人
 1 public void build(String key, boolean openAccount) {
 2     if (mDiscardRest) {
 3         return;
 4     }
 5     if (mWrapBuilder == null) {
 6         return;
 7     }
 8 
 9     // if current operation oversize abandon it
10     if (mCurrentOperationSizeInByte >= MAX_OPERATION_SIZE) {
11         mDiscardRest = true;
12         mCurrentOperationSizeInByte = 0;
13         return;
14     }
15 
16     avoidOperationOverFlow();
17 
18     if (key != null) {
19         withValueBackReference(key);
20     }
21 
22     ContentProviderOperation operation = mWrapBuilder.build();
23     if (!openAccount) {
24         mValidDataCommit = true;
25         mOperationList.add(operation);
26     }
27     if (mOperationList.size() >= COMMIT_GATE) {
28         Uri uri = apply();    (1)
29 
30         if (uri != null) {
31             mRawContactId = ContentUris.parseId(uri);
32         }
33 
34     }
35     if (openAccount) {
36         mOperationList.add(operation);
37     }
38 
39     mOperationSizeInByte += mCurrentOperationSizeInByte;
40     mCurrentOperationSizeInByte = 0;
41 }
42 
43 public Uri apply() {
44     ContentProviderResult[] cpr = null;
45     try {
46         if (mOperationList != null && mOperationList.size() > 0) {
47             cpr = mResolver.applyBatch(ContactsContract.AUTHORITY, mOperationList);    (2)
48             for (ContentProviderOperation cpo : mOperationList) {
49                 Log.d("D3", "cpo.toString = " + cpo.toString());
50             }
51         }
52 
53     }
54 
55     if (mOperationList != null) {
56         mOperationList.clear();
57     }
58     mOperationListSizeAtOpenSession = 0;
59 
60     Uri uri = null;
61     // if cpr.length > mCurrentContactRecord means last
62     // applyBatch commit whole contacts and parts.
63     if (cpr != null) {
64         if (cpr.length > mCurrentContactRecord) {
65             uri = cpr[mCurrentContactRecord].uri;
66             mContactAppendum = true;
67         } else if (cpr.length == mCurrentContactRecord) {
68             // return first contact uri in last apply
69             uri = cpr[0].uri;
70         }
71     }
72     mCurrentContactRecord = 0;
73 
74     return uri;
75 }
Phonebook 导入SD上的.vcf联系人

在build()方法(1)代码处,满足条件后会调用apply()方法,看apply()中(2)的方法,保存了吧。

呵呵,Phonebook导入.vcf的过程分析至此结束,至于是如何解码中文姓名和头像的,会专门写文章研究这个问题的,因为要解码肯定得先编码,是不?怎还得先搞清楚到底是如何编码的,才能弄明白要如何解码。

Phonebook 导入SD上的.vcf联系人

上一篇:Vlan Mapping


下一篇:js 判断当前是手机还是电脑