1
package org.waxworlds.importcontacts;
3
import java.io.BufferedReader;
5
import java.io.FileNotFoundException;
6
import java.io.FileReader;
7
import java.io.FilenameFilter;
8
import java.io.IOException;
9
import java.io.UnsupportedEncodingException;
10
import java.util.Arrays;
11
import java.util.HashSet;
12
import java.util.List;
14
import java.util.Vector;
15
import java.util.regex.Matcher;
16
import java.util.regex.Pattern;
18
import org.waxworlds.importcontacts.Importer.AbortImportException;
20
import android.content.SharedPreferences;
21
import android.provider.Contacts;
22
import android.provider.Contacts.PhonesColumns;
24
public class VCFImporter extends Importer
26
private int _vCardCount = 0;
27
private int _progress = 0;
29
public VCFImporter( Doit doit )
35
protected void onImport() throws AbortImportException
37
SharedPreferences prefs = getSharedPreferences();
40
setProgressMessage( R.string.doit_scanning );
42
// get a list of vcf files
47
String location = prefs.getString( "location", "" );
48
File dir = new File( location );
49
if( !dir.exists() || !dir.isDirectory() )
50
showError( R.string.error_locationnotfound );
53
class VCardFilter implements FilenameFilter {
54
public boolean accept( File dir, String name ) {
55
return name.toLowerCase().endsWith( ".vcf" );
58
files = dir.listFiles( new VCardFilter() );
60
catch( SecurityException e ) {
61
showError( R.string.error_locationpermissions );
64
// check num files and set progress max
65
if( files != null && files.length > 0 )
66
setProgressMax( files.length );
68
showError( R.string.error_locationnofiles );
70
// scan through the files
72
for( int i = 0; i < files.length; i++ ) {
73
countVCardFile( files[ i ] );
76
setProgressMax( _vCardCount ); // will also update tmp progress
80
for( int i = 0; i < files.length; i++ )
81
importVCardFile( files[ i ] );
84
private void countVCardFile( File file ) throws AbortImportException
89
BufferedReader reader = new BufferedReader(
90
new FileReader( file ) );
94
boolean inVCard = false;
95
while( ( line = reader.readLine() ) != null )
98
// look for vcard beginning
99
if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
104
else if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
109
catch( FileNotFoundException e ) {
110
showError( getText( R.string.error_filenotfound ) + file.getName() );
112
catch( IOException e ) {
113
showError( getText( R.string.error_ioerror ) + file.getName() );
117
private void importVCardFile( File file ) throws AbortImportException
122
BufferedReader reader = new BufferedReader(
123
new FileReader( file ) );
126
StringBuffer content = new StringBuffer();
128
while( ( line = reader.readLine() ) != null )
129
content.append( line ).append( "\n" );
131
importVCardFileContent( content.toString(), file.getName() );
133
catch( FileNotFoundException e ) {
134
showError( getText( R.string.error_filenotfound ) + file.getName() );
136
catch( IOException e ) {
137
showError( getText( R.string.error_ioerror ) + file.getName() );
141
private void importVCardFileContent( String content, String fileName )
142
throws AbortImportException
144
// unfold RFC2425 section 5.8.1 folded lines, except that we must also
145
// handle embedded Quoted-Printable encodings that have a trailing '='.
146
// So we remove these first before doing RFC2425 unfolding.
147
content = content.replaceAll( "=\n[ \\t]", "" )
148
.replaceAll( "\n[ \\t]", "" );
150
// get lines and parse them
151
String[] lines = content.split( "\n" );
153
for( int i = 0; i < lines.length; i++ )
155
String line = lines[ i ];
157
if( vCard == null ) {
158
// look for vcard beginning
159
if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
160
setProgress( ++_progress );
165
// look for vcard content or ending
166
if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
168
// store vcard and do away with it
170
vCard.finaliseParsing();
171
importContact( vCard );
173
catch( VCard.ParseException e ) {
176
getText( R.string.error_vcf_parse ).toString()
177
+ fileName + "\n" + e.getMessage() ) )
180
catch( VCard.SkipContactException e ) {
188
// try giving the line to the vcard
190
vCard.parseLine( line );
192
catch( VCard.ParseException e ) {
195
getText( R.string.error_vcf_parse ).toString()
196
+ fileName + "\n" + e.getMessage() ) )
199
// although we're continuing, we still need to abort
200
// this vCard. Further lines will be ignored until we
201
// get to another BEGIN:VCARD line.
204
catch( VCard.SkipContactException e ) {
206
// abort this vCard. Further lines will be ignored until
207
// we get to another BEGIN:VCARD line.
215
private class VCard extends ContactData
217
private final static int NAMELEVEL_NONE = 0;
218
private final static int NAMELEVEL_ORG = 1;
219
private final static int NAMELEVEL_FN = 2;
220
private final static int NAMELEVEL_N = 3;
222
private String _version = null;
223
private Vector< String > _lines = null;
224
private int _nameLevel = NAMELEVEL_NONE;
226
protected class ParseException extends Exception
228
public ParseException( String error )
233
public ParseException( int res )
235
super( VCFImporter.this.getText( res ).toString() );
239
protected class SkipContactException extends Exception { }
241
public void parseLine( String line )
242
throws ParseException, SkipContactException,
245
// get property halves
246
String[] props = line.split( ":" );
247
for( int i = 0; i < props.length; i++ )
248
props[ i ] = props[ i ].trim();
249
if( props.length < 2 ||
250
props[ 0 ].length() < 1 || props[ 1 ].length() < 1 )
251
throw new ParseException( R.string.error_vcf_malformed );
253
if( _version == null )
255
if( props[ 0 ].equals( "VERSION" ) )
258
if( !props[ 1 ].equals( "2.1" ) &&
259
!props[ 1 ].equals( "3.0" ) )
260
throw new ParseException( R.string.error_vcf_version );
261
_version = props[ 1 ];
263
// parse any other lines we've accumulated so far
265
for( int i = 0; i < _lines.size(); i++ )
266
parseLine( _lines.get( i ) );
271
// stash this line till we have a version
273
_lines = new Vector< String >();
279
// get parameter parts
280
String[] params = props[ 0 ].split( ";" );
281
for( int i = 0; i < params.length; i++ )
282
params[ i ] = params[ i ].trim();
284
// parse some properties
285
if( params[ 0 ].equals( "N" ) )
286
parseN( params, props[ 1 ] );
287
else if( params[ 0 ].equals( "FN" ) )
288
parseFN( params, props[ 1 ] );
289
else if( params[ 0 ].equals( "ORG" ) )
290
parseORG( params, props[ 1 ] );
291
else if( params[ 0 ].equals( "TEL" ) )
292
parseTEL( params, props[ 1 ] );
293
else if( params[ 0 ].equals( "EMAIL" ) )
294
parseEMAIL( params, props[ 1 ] );
298
private void parseN( String[] params, String value )
299
throws ParseException, SkipContactException,
302
// already got a better name?
303
if( _nameLevel >= NAMELEVEL_N ) return;
306
String[] nameparts = value.split( ";" );
307
for( int i = 0; i < nameparts.length; i++ )
308
nameparts[ i ] = nameparts[ i ].trim();
312
if( nameparts.length > 1 && nameparts[ 1 ].length() > 0 )
313
value += nameparts[ 1 ];
314
if( nameparts[ 0 ].length() > 0 )
315
value += ( value.length() == 0? "" : " " ) + nameparts[ 0 ];
318
setName( undoCharsetAndEncoding( params, value ) );
319
_nameLevel = NAMELEVEL_N;
321
// check now to see if we need to import this contact (to avoid
322
// parsing the rest of the vCard unnecessarily)
323
if( !isImportRequired( getName() ) )
324
throw new SkipContactException();
327
private void parseFN( String[] params, String value )
328
throws ParseException, SkipContactException
330
// already got a better name?
331
if( _nameLevel >= NAMELEVEL_FN ) return;
334
setName( undoCharsetAndEncoding( params, value ) );
335
_nameLevel = NAMELEVEL_FN;
338
private void parseORG( String[] params, String value )
339
throws ParseException, SkipContactException
341
// already got a better name?
342
if( _nameLevel >= NAMELEVEL_ORG ) return;
345
String[] orgparts = value.split( ";" );
346
for( int i = 0; i < orgparts.length; i++ )
347
orgparts[ i ] = orgparts[ i ].trim();
350
if( orgparts[ 0 ].length() == 0 && orgparts.length > 1 )
351
value = orgparts[ 1 ];
353
value = orgparts[ 0 ];
356
setName( undoCharsetAndEncoding( params, value ) );
357
_nameLevel = NAMELEVEL_ORG;
360
private void parseTEL( String[] params, String value )
361
throws ParseException
363
if( value.length() == 0 ) return;
365
Set< String > types = extractTypes( params, Arrays.asList(
366
"PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
367
"PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
369
// here's the logic...
370
boolean preferred = types.contains( "PREF" );
371
if( types.contains( "VOICE" ) )
372
if( types.contains( "WORK" ) )
373
addPhone( value, PhonesColumns.TYPE_WORK, preferred );
375
addPhone( value, PhonesColumns.TYPE_HOME, preferred );
376
else if( types.contains( "CELL" ) || types.contains( "VIDEO" ) )
377
addPhone( value, PhonesColumns.TYPE_MOBILE, preferred );
378
if( types.contains( "FAX" ) )
379
if( types.contains( "HOME" ) )
380
addPhone( value, PhonesColumns.TYPE_FAX_HOME, preferred );
382
addPhone( value, PhonesColumns.TYPE_FAX_WORK, preferred );
383
if( types.contains( "PAGER" ) )
384
addPhone( value, PhonesColumns.TYPE_PAGER, preferred );
387
public void parseEMAIL( String[] params, String value )
389
if( value.length() == 0 ) return;
391
Set< String > types = extractTypes( params, Arrays.asList(
392
"PREF", "WORK", "HOME", "INTERNET" ) );
394
// here's the logic...
395
boolean preferred = types.contains( "PREF" );
396
if( types.contains( "WORK" ) )
397
addEmail( value, Contacts.ContactMethods.TYPE_WORK, preferred );
399
addEmail( value, Contacts.ContactMethods.TYPE_HOME, preferred );
402
public void finaliseParsing()
403
throws ParseException, SkipContactException,
406
// missing version (and data is present)
407
if( _version == null && _lines != null )
408
throw new ParseException( R.string.error_vcf_malformed );
410
// missing name properties?
411
if( _nameLevel == NAMELEVEL_NONE )
412
throw new ParseException( R.string.error_vcf_noname );
414
// check if we should import this one? If we've already got an 'N'-
415
// type name, this will already have been done by parseN() so we
416
// mustn't do this here (or it could prompt twice!)
417
if( _nameLevel < NAMELEVEL_N && !isImportRequired( getName() ) )
418
throw new SkipContactException();
421
private String undoCharsetAndEncoding( String[] params, String value )
422
throws ParseException
424
// check encoding/charset
425
String charset, encoding;
426
if( ( charset = checkParam( params, "CHARSET" ) ) != null &&
427
!charset.equals( "UTF-8" ) && !charset.equals( "UTF-16" ) )
428
throw new ParseException( R.string.error_vcf_charset );
429
if( ( encoding = checkParam( params, "ENCODING" ) ) != null &&
430
!encoding.equals( "QUOTED-PRINTABLE" ) )
431
throw new ParseException( R.string.error_vcf_encoding );
434
if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
435
return unencodeQuotedPrintable( value, charset );
441
private String checkParam( String[] params, String name )
443
Pattern p = Pattern.compile( "^" + name + "[ \\t]*=[ \\t]*(.*)$" );
444
for( int i = 0; i < params.length; i++ ) {
445
Matcher m = p.matcher( params[ i ] );
452
private Set< String > extractTypes( String[] params,
453
List< String > validTypes )
455
HashSet< String > types = new HashSet< String >();
457
// get 3.0-style TYPE= param
459
if( ( typeParam = checkParam( params, "TYPE" ) ) != null ) {
460
String[] bits = typeParam.split( "," );
461
for( int i = 0; i < bits.length; i++ )
462
if( validTypes.contains( bits[ i ] ) )
463
types.add( bits[ i ] );
466
// get 2.1-style type param
467
if( _version.equals( "2.1" ) ) {
468
for( int i = 1; i < params.length; i++ )
469
if( validTypes.contains( params[ i ] ) )
470
types.add( params[ i ] );
476
private String unencodeQuotedPrintable( String str, String charset )
478
// default encoding scheme
479
if( charset == null ) charset = "UTF-8";
481
// unencode quoted-pritable encoding, as per RFC1521 section 5.1
482
byte[] bytes = new byte[ str.length() ];
484
for( int i = 0; i < str.length(); i++, j++ ) {
485
char ch = str.charAt( i );
486
if( ch == '=' && i < str.length() - 2 ) {
488
Character.digit( str.charAt( i + 1 ), 16 ) * 16 +
489
Character.digit( str.charAt( i + 2 ), 16 ) );
493
bytes[ j ] = (byte)ch;
496
return new String( bytes, 0, j, charset );
497
} catch( UnsupportedEncodingException e ) { }