• Phonebook 导入SD上的.vcf联系人


    2014-01-11 17:29:22

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

    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     }

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

     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     }

    我们具体看VCardReadThread类的run()方法,以导入多个.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 }

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

     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 }

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

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

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

     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 }

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

     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 }

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

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

     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 }

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

     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

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

    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

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

    在每一个判断语句块里都有一个方法,看(3)的代码,这两个方法最终都会调用mBuilder.propertyValues(v)将propertyValue值所在的list存起来,因此最终的解析工作也是在propertyValues()方法里完成的。下面再看一个更复杂的.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
     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
     U8rBp0QR7xyWAHGfw4rVU3shcyP/Z

    这个联系人信息如下:

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

     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     }

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

    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

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

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

    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("
    ", "
    "));
    8     }
    9 }

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

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

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

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

     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     }

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

    1     public void endRecord() {
    2         mCurrentContactStruct.consolidateFields();
    3 
    4         for (EntryHandler entryHandler : mEntryHandlers) {
    5             entryHandler.onEntryCreated(mCurrentContactStruct);
    6         }
    7         mCurrentContactStruct.clear();
    8     }

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

    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     }

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

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

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

     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 }

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

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

  • 相关阅读:
    QSetting
    类中函数前、后、参数加const
    delete指针
    自定义数组类
    手动调用构造函数
    windows和linux平台下的通用时间测试函数
    多线程编程学习
    Android 利用ImageView显示图片
    特征描述算子-sift
    opencv边界扩展
  • 原文地址:https://www.cnblogs.com/wlrhnh/p/3515269.html
Copyright © 2020-2023  润新知