/android/import-contacts

To get this branch, use:
bzr branch http://bzr.ed.am/android/import-contacts
1 by edam
Initial import
1
package org.waxworlds.importcontacts;
2
3
import java.io.BufferedReader;
4
import java.io.File;
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;
13
import java.util.Set;
14
import java.util.Vector;
15
import java.util.regex.Matcher;
16
import java.util.regex.Pattern;
17
18
import org.waxworlds.importcontacts.Importer.AbortImportException;
19
20
import android.content.SharedPreferences;
21
import android.provider.Contacts;
22
import android.provider.Contacts.PhonesColumns;
23
24
public class VCFImporter extends Importer
25
{
26
	private int _vCardCount = 0;
27
	private int _progress = 0;
28
29
	public VCFImporter( Doit doit )
30
	{
31
		super( doit );
32
	}
33
34
	@Override
35
	protected void onImport() throws AbortImportException
36
	{
37
		SharedPreferences prefs = getSharedPreferences();
38
39
		// update UI
40
		setProgressMessage( R.string.doit_scanning );
41
42
		// get a list of vcf files
43
		File[] files = null;
44
		try
45
		{
46
			// open directory
47
			String location = prefs.getString( "location", "" );
48
			File dir = new File( location );
49
			if( !dir.exists() || !dir.isDirectory() )
50
				showError( R.string.error_locationnotfound );
51
52
			// get files
53
			class VCardFilter implements FilenameFilter {
54
			    public boolean accept( File dir, String name ) {
55
			        return name.toLowerCase().endsWith( ".vcf" );
56
			    }
57
			}
58
			files = dir.listFiles( new VCardFilter() );
59
		}
60
		catch( SecurityException e ) {
61
			showError( R.string.error_locationpermissions );
62
		}
63
64
		// check num files and set progress max
65
		if( files != null && files.length > 0 )
66
			setProgressMax( files.length );
67
		else
68
			showError( R.string.error_locationnofiles );
69
70
		// scan through the files
71
		setTmpProgress( 0 );
72
		for( int i = 0; i < files.length; i++ ) {
73
			countVCardFile( files[ i ] );
74
			setTmpProgress( i );
75
		}
76
		setProgressMax( _vCardCount );	// will also update tmp progress
77
78
		// import them
79
		setProgress( 0 );
80
		for( int i = 0; i < files.length; i++ )
81
			importVCardFile( files[ i ] );
82
	}
83
84
	private void countVCardFile( File file ) throws AbortImportException
85
	{
86
		try
87
		{
88
			// open file
89
			BufferedReader reader = new BufferedReader(
90
					new FileReader( file ) );
91
92
			// read
93
			String line;
94
			boolean inVCard = false;
95
			while( ( line = reader.readLine() ) != null )
96
			{
97
				if( !inVCard ) {
98
					// look for vcard beginning
99
					if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
100
						inVCard = true;
101
						_vCardCount++;
102
					}
103
				}
104
				else if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
105
					inVCard = false;
106
			}
107
108
		}
109
		catch( FileNotFoundException e ) {
110
			showError( getText( R.string.error_filenotfound ) + file.getName() );
111
		}
112
		catch( IOException e ) {
113
			showError( getText( R.string.error_ioerror ) + file.getName() );
114
		}
115
	}
116
117
	private void importVCardFile( File file ) throws AbortImportException
118
	{
119
		try
120
		{
121
			// open file
122
			BufferedReader reader = new BufferedReader(
123
					new FileReader( file ) );
124
125
			// read
126
			StringBuffer content = new StringBuffer();
127
			String line;
128
			while( ( line = reader.readLine() ) != null )
129
				content.append( line ).append( "\n" );
130
131
			importVCardFileContent( content.toString(), file.getName() );
132
		}
133
		catch( FileNotFoundException e ) {
134
			showError( getText( R.string.error_filenotfound ) + file.getName() );
135
		}
136
		catch( IOException e ) {
137
			showError( getText( R.string.error_ioerror ) + file.getName() );
138
		}
139
	}
140
141
	private void importVCardFileContent( String content, String fileName )
142
			throws AbortImportException
143
	{
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]", "" );
149
150
		// get lines and parse them
151
		String[] lines = content.split( "\n" );
152
		VCard vCard = null;
153
		for( int i = 0; i < lines.length; i++ )
154
		{
155
			String line = lines[ i ];
156
157
			if( vCard == null ) {
158
				// look for vcard beginning
159
				if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
160
					setProgress( ++_progress );
161
					vCard = new VCard();
162
				}
163
			}
164
			else {
165
				// look for vcard content or ending
166
				if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
167
				{
168
					// store vcard and do away with it
169
					try {
170
						vCard.finaliseParsing();
171
						importContact( vCard );
172
					}
173
					catch( VCard.ParseException e ) {
174
						skipContact();
175
						if( !showContinue(
176
								getText( R.string.error_vcf_parse ).toString()
177
								+ fileName + "\n" + e.getMessage() ) )
178
							abort();
179
					}
180
					catch( VCard.SkipContactException e ) {
181
						skipContact();
182
						// do nothing
183
					}
184
					vCard = null;
185
				}
186
				else
187
				{
188
					// try giving the line to the vcard
189
					try {
190
						vCard.parseLine( line );
191
					}
192
					catch( VCard.ParseException e ) {
193
						skipContact();
194
						if( !showContinue(
195
								getText( R.string.error_vcf_parse ).toString()
196
								+ fileName + "\n" + e.getMessage() ) )
197
							abort();
198
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.
202
						vCard = null;
203
					}
204
					catch( VCard.SkipContactException e ) {
205
						skipContact();
206
						// abort this vCard. Further lines will be ignored until
207
						// we get to another BEGIN:VCARD line.
208
						vCard = null;
209
					}
210
				}
211
			}
212
		}
213
	}
214
215
	private class VCard extends ContactData
216
	{
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;
221
222
		private String _version = null;
223
		private Vector< String > _lines = null;
224
		private int _nameLevel = NAMELEVEL_NONE;
225
226
		protected class ParseException extends Exception
227
		{
228
			public ParseException( String error )
229
			{
230
				super( error );
231
			}
232
233
			public ParseException( int res )
234
			{
235
				super( VCFImporter.this.getText( res ).toString() );
236
			}
237
		}
238
239
		protected class SkipContactException extends Exception { }
240
241
		public void parseLine( String line )
242
				throws ParseException, SkipContactException,
243
				AbortImportException
244
		{
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 );
252
253
			if( _version == null )
254
			{
255
				if( props[ 0 ].equals( "VERSION" ) )
256
				{
257
					// get 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 ];
262
263
					// parse any other lines we've accumulated so far
264
					if( _lines != null )
265
						for( int i = 0; i < _lines.size(); i++ )
266
							parseLine( _lines.get( i ) );
267
					_lines = null;
268
				}
269
				else
270
				{
271
					// stash this line till we have a version
272
					if( _lines == null )
273
						_lines = new Vector< String >();
274
					_lines.add( line );
275
				}
276
			}
277
			else
278
			{
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();
283
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 ] );
295
			}
296
		}
297
298
		private void parseN( String[] params, String value )
299
				throws ParseException, SkipContactException,
300
				AbortImportException
301
		{
302
			// already got a better name?
303
			if( _nameLevel >= NAMELEVEL_N ) return;
304
305
			// get name parts
306
			String[] nameparts = value.split( ";" );
307
			for( int i = 0; i < nameparts.length; i++ )
308
				nameparts[ i ] = nameparts[ i ].trim();
309
310
			// build name
311
			value = "";
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 ];
316
317
			// set name
318
			setName( undoCharsetAndEncoding( params, value ) );
319
			_nameLevel = NAMELEVEL_N;
320
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();
325
		}
326
327
		private void parseFN( String[] params, String value )
328
				throws ParseException, SkipContactException
329
		{
330
			// already got a better name?
331
			if( _nameLevel >= NAMELEVEL_FN ) return;
332
333
			// set name
334
			setName( undoCharsetAndEncoding( params, value ) );
335
			_nameLevel = NAMELEVEL_FN;
336
		}
337
338
		private void parseORG( String[] params, String value )
339
				throws ParseException, SkipContactException
340
		{
341
			// already got a better name?
342
			if( _nameLevel >= NAMELEVEL_ORG ) return;
343
344
			// get org parts
345
			String[] orgparts = value.split( ";" );
346
			for( int i = 0; i < orgparts.length; i++ )
347
				orgparts[ i ] = orgparts[ i ].trim();
348
349
			// build name
350
			if( orgparts[ 0 ].length() == 0 && orgparts.length > 1 )
351
				value = orgparts[ 1 ];
352
			else
353
				value = orgparts[ 0 ];
354
355
			// set name
356
			setName( undoCharsetAndEncoding( params, value ) );
357
			_nameLevel = NAMELEVEL_ORG;
358
		}
359
360
		private void parseTEL( String[] params, String value )
361
				throws ParseException
362
		{
363
			if( value.length() == 0 ) return;
364
365
			Set< String > types = extractTypes( params, Arrays.asList(
366
					"PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
367
					"PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
368
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 );
374
				else
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 );
381
				else
382
					addPhone( value, PhonesColumns.TYPE_FAX_WORK, preferred );
383
			if( types.contains( "PAGER" ) )
384
				addPhone( value, PhonesColumns.TYPE_PAGER, preferred );
385
		}
386
387
		public void parseEMAIL( String[] params, String value )
388
		{
389
			if( value.length() == 0 ) return;
390
391
			Set< String > types = extractTypes( params, Arrays.asList(
392
					"PREF", "WORK", "HOME", "INTERNET" ) );
393
394
			// here's the logic...
395
			boolean preferred = types.contains( "PREF" );
396
			if( types.contains( "WORK" ) )
397
				addEmail( value, Contacts.ContactMethods.TYPE_WORK, preferred );
398
			else
399
				addEmail( value, Contacts.ContactMethods.TYPE_HOME, preferred );
400
		}
401
402
		public void finaliseParsing()
403
				throws ParseException, SkipContactException,
404
				AbortImportException
405
		{
406
			// missing version (and data is present)
407
			if( _version == null && _lines != null )
408
				throw new ParseException( R.string.error_vcf_malformed );
409
410
			//  missing name properties?
411
			if( _nameLevel == NAMELEVEL_NONE )
412
				throw new ParseException( R.string.error_vcf_noname );
413
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();
419
		}
420
421
		private String undoCharsetAndEncoding( String[] params, String value )
422
				throws ParseException
423
		{
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 );
432
433
			// do decoding?
434
			if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
435
				return unencodeQuotedPrintable( value, charset );
436
437
			// nothing to do!
438
			return value;
439
		}
440
441
		private String checkParam( String[] params, String name )
442
		{
443
			Pattern p = Pattern.compile( "^" + name + "[ \\t]*=[ \\t]*(.*)$" );
444
			for( int i = 0; i < params.length; i++ ) {
445
				Matcher m = p.matcher( params[ i ] );
446
				if( m.matches() )
447
					return m.group( 1 );
448
			}
449
			return null;
450
		}
451
452
		private Set< String > extractTypes( String[] params,
453
				List< String > validTypes )
454
		{
455
			HashSet< String > types = new HashSet< String >();
456
457
			// get 3.0-style TYPE= param
458
			String typeParam;
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 ] );
464
			}
465
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 ] );
471
			}
472
473
			return types;
474
		}
475
476
		private String unencodeQuotedPrintable( String str, String charset )
477
		{
478
			// default encoding scheme
479
			if( charset == null ) charset = "UTF-8";
480
481
			// unencode quoted-pritable encoding, as per RFC1521 section 5.1
482
			byte[] bytes = new byte[ str.length() ];
483
			int j = 0;
484
			for( int i = 0; i < str.length(); i++, j++ ) {
485
				char ch = str.charAt( i );
486
				if( ch == '=' && i < str.length() - 2 ) {
487
					bytes[ j ] = (byte)(
488
							Character.digit( str.charAt( i + 1 ), 16 ) * 16 +
489
							Character.digit( str.charAt( i + 2 ), 16 ) );
490
					i += 2;
491
				}
492
				else
493
					bytes[ j ] = (byte)ch;
494
			}
495
			try {
496
				return new String( bytes, 0, j, charset );
497
			} catch( UnsupportedEncodingException e ) { }
498
			return null;
499
		}
500
	}
501
}