/android/import-contacts

To get this branch, use:
bzr branch http://bzr.ed.am/android/import-contacts
6 by edam
- added GPL header comments to all files
1
/*
2
 * VCFImporter.java
3
 *
4
 * Copyright (C) 2009 Tim Marston <edam@waxworlds.org>
5
 *
6
 * This file is part of the Import Contacts program (hereafter referred
7
 * to as "this program"). For more information, see
8
 * http://www.waxworlds.org/edam/software/android/import-contacts
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 */
23
1 by edam
Initial import
24
package org.waxworlds.importcontacts;
25
26
import java.io.BufferedReader;
27
import java.io.File;
28
import java.io.FileNotFoundException;
29
import java.io.FileReader;
30
import java.io.FilenameFilter;
31
import java.io.IOException;
32
import java.io.UnsupportedEncodingException;
33
import java.util.Arrays;
34
import java.util.HashSet;
35
import java.util.List;
36
import java.util.Set;
37
import java.util.Vector;
38
import java.util.regex.Matcher;
39
import java.util.regex.Pattern;
40
41
import org.waxworlds.importcontacts.Importer.AbortImportException;
42
43
import android.content.SharedPreferences;
44
import android.provider.Contacts;
45
import android.provider.Contacts.PhonesColumns;
46
47
public class VCFImporter extends Importer
48
{
49
	private int _vCardCount = 0;
50
	private int _progress = 0;
51
52
	public VCFImporter( Doit doit )
53
	{
54
		super( doit );
55
	}
56
57
	@Override
58
	protected void onImport() throws AbortImportException
59
	{
60
		SharedPreferences prefs = getSharedPreferences();
61
62
		// update UI
63
		setProgressMessage( R.string.doit_scanning );
64
65
		// get a list of vcf files
66
		File[] files = null;
67
		try
68
		{
69
			// open directory
70
			String location = prefs.getString( "location", "" );
71
			File dir = new File( location );
72
			if( !dir.exists() || !dir.isDirectory() )
73
				showError( R.string.error_locationnotfound );
74
75
			// get files
76
			class VCardFilter implements FilenameFilter {
77
			    public boolean accept( File dir, String name ) {
78
			        return name.toLowerCase().endsWith( ".vcf" );
79
			    }
80
			}
81
			files = dir.listFiles( new VCardFilter() );
82
		}
83
		catch( SecurityException e ) {
84
			showError( R.string.error_locationpermissions );
85
		}
86
87
		// check num files and set progress max
88
		if( files != null && files.length > 0 )
89
			setProgressMax( files.length );
90
		else
91
			showError( R.string.error_locationnofiles );
92
93
		// scan through the files
94
		setTmpProgress( 0 );
95
		for( int i = 0; i < files.length; i++ ) {
96
			countVCardFile( files[ i ] );
97
			setTmpProgress( i );
98
		}
99
		setProgressMax( _vCardCount );	// will also update tmp progress
100
101
		// import them
102
		setProgress( 0 );
103
		for( int i = 0; i < files.length; i++ )
104
			importVCardFile( files[ i ] );
105
	}
106
107
	private void countVCardFile( File file ) throws AbortImportException
108
	{
109
		try
110
		{
111
			// open file
112
			BufferedReader reader = new BufferedReader(
113
					new FileReader( file ) );
114
115
			// read
116
			String line;
117
			boolean inVCard = false;
118
			while( ( line = reader.readLine() ) != null )
119
			{
120
				if( !inVCard ) {
121
					// look for vcard beginning
122
					if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
123
						inVCard = true;
124
						_vCardCount++;
125
					}
126
				}
127
				else if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
128
					inVCard = false;
129
			}
130
131
		}
132
		catch( FileNotFoundException e ) {
133
			showError( getText( R.string.error_filenotfound ) + file.getName() );
134
		}
135
		catch( IOException e ) {
136
			showError( getText( R.string.error_ioerror ) + file.getName() );
137
		}
138
	}
139
140
	private void importVCardFile( File file ) throws AbortImportException
141
	{
142
		try
143
		{
144
			// open file
145
			BufferedReader reader = new BufferedReader(
146
					new FileReader( file ) );
147
148
			// read
149
			StringBuffer content = new StringBuffer();
150
			String line;
151
			while( ( line = reader.readLine() ) != null )
152
				content.append( line ).append( "\n" );
153
154
			importVCardFileContent( content.toString(), file.getName() );
155
		}
156
		catch( FileNotFoundException e ) {
157
			showError( getText( R.string.error_filenotfound ) + file.getName() );
158
		}
159
		catch( IOException e ) {
160
			showError( getText( R.string.error_ioerror ) + file.getName() );
161
		}
162
	}
163
164
	private void importVCardFileContent( String content, String fileName )
165
			throws AbortImportException
166
	{
167
		// unfold RFC2425 section 5.8.1 folded lines, except that we must also
168
		// handle embedded Quoted-Printable encodings that have a trailing '='.
169
		// So we remove these first before doing RFC2425 unfolding.
170
		content = content.replaceAll( "=\n[ \\t]", "" )
171
				.replaceAll( "\n[ \\t]", "" );
172
173
		// get lines and parse them
174
		String[] lines = content.split( "\n" );
175
		VCard vCard = null;
176
		for( int i = 0; i < lines.length; i++ )
177
		{
178
			String line = lines[ i ];
179
180
			if( vCard == null ) {
181
				// look for vcard beginning
182
				if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
183
					setProgress( ++_progress );
184
					vCard = new VCard();
185
				}
186
			}
187
			else {
188
				// look for vcard content or ending
189
				if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
190
				{
191
					// store vcard and do away with it
192
					try {
193
						vCard.finaliseParsing();
194
						importContact( vCard );
195
					}
196
					catch( VCard.ParseException e ) {
197
						skipContact();
198
						if( !showContinue(
199
								getText( R.string.error_vcf_parse ).toString()
200
								+ fileName + "\n" + e.getMessage() ) )
3 by edam
- added "all done" message
201
							finish( ACTION_ABORT );
1 by edam
Initial import
202
					}
203
					catch( VCard.SkipContactException e ) {
204
						skipContact();
205
						// do nothing
206
					}
207
					vCard = null;
208
				}
209
				else
210
				{
211
					// try giving the line to the vcard
212
					try {
213
						vCard.parseLine( line );
214
					}
215
					catch( VCard.ParseException e ) {
216
						skipContact();
217
						if( !showContinue(
218
								getText( R.string.error_vcf_parse ).toString()
219
								+ fileName + "\n" + e.getMessage() ) )
3 by edam
- added "all done" message
220
							finish( ACTION_ABORT );
1 by edam
Initial import
221
222
						// although we're continuing, we still need to abort
223
						// this vCard. Further lines will be ignored until we
224
						// get to another BEGIN:VCARD line.
225
						vCard = null;
226
					}
227
					catch( VCard.SkipContactException e ) {
228
						skipContact();
229
						// abort this vCard. Further lines will be ignored until
230
						// we get to another BEGIN:VCARD line.
231
						vCard = null;
232
					}
233
				}
234
			}
235
		}
236
	}
237
238
	private class VCard extends ContactData
239
	{
240
		private final static int NAMELEVEL_NONE = 0;
241
		private final static int NAMELEVEL_ORG = 1;
242
		private final static int NAMELEVEL_FN = 2;
243
		private final static int NAMELEVEL_N = 3;
244
245
		private String _version = null;
246
		private Vector< String > _lines = null;
247
		private int _nameLevel = NAMELEVEL_NONE;
248
249
		protected class ParseException extends Exception
250
		{
251
			public ParseException( String error )
252
			{
253
				super( error );
254
			}
255
256
			public ParseException( int res )
257
			{
258
				super( VCFImporter.this.getText( res ).toString() );
259
			}
260
		}
261
262
		protected class SkipContactException extends Exception { }
263
264
		public void parseLine( String line )
265
				throws ParseException, SkipContactException,
266
				AbortImportException
267
		{
268
			// get property halves
269
			String[] props = line.split( ":" );
270
			for( int i = 0; i < props.length; i++ )
271
				props[ i ] = props[ i ].trim();
272
			if( props.length < 2 ||
273
					props[ 0 ].length() < 1 || props[ 1 ].length() < 1 )
274
				throw new ParseException( R.string.error_vcf_malformed );
275
276
			if( _version == null )
277
			{
278
				if( props[ 0 ].equals( "VERSION" ) )
279
				{
280
					// get version
281
					if( !props[ 1 ].equals( "2.1" ) &&
282
							!props[ 1 ].equals( "3.0" ) )
283
						throw new ParseException( R.string.error_vcf_version );
284
					_version = props[ 1 ];
285
286
					// parse any other lines we've accumulated so far
287
					if( _lines != null )
288
						for( int i = 0; i < _lines.size(); i++ )
289
							parseLine( _lines.get( i ) );
290
					_lines = null;
291
				}
292
				else
293
				{
294
					// stash this line till we have a version
295
					if( _lines == null )
296
						_lines = new Vector< String >();
297
					_lines.add( line );
298
				}
299
			}
300
			else
301
			{
302
				// get parameter parts
303
				String[] params = props[ 0 ].split( ";" );
304
				for( int i = 0; i < params.length; i++ )
305
					params[ i ] = params[ i ].trim();
306
307
				// parse some properties
308
				if( params[ 0 ].equals( "N" ) )
309
					parseN( params, props[ 1 ] );
310
				else if( params[ 0 ].equals( "FN" ) )
311
					parseFN( params, props[ 1 ] );
312
				else if( params[ 0 ].equals( "ORG" ) )
313
					parseORG( params, props[ 1 ] );
314
				else if( params[ 0 ].equals( "TEL" ) )
315
					parseTEL( params, props[ 1 ] );
316
				else if( params[ 0 ].equals( "EMAIL" ) )
317
					parseEMAIL( params, props[ 1 ] );
318
			}
319
		}
320
321
		private void parseN( String[] params, String value )
322
				throws ParseException, SkipContactException,
323
				AbortImportException
324
		{
325
			// already got a better name?
326
			if( _nameLevel >= NAMELEVEL_N ) return;
327
328
			// get name parts
329
			String[] nameparts = value.split( ";" );
330
			for( int i = 0; i < nameparts.length; i++ )
331
				nameparts[ i ] = nameparts[ i ].trim();
332
333
			// build name
334
			value = "";
335
			if( nameparts.length > 1 && nameparts[ 1 ].length() > 0 )
336
				value += nameparts[ 1 ];
337
			if( nameparts[ 0 ].length() > 0 )
338
				value += ( value.length() == 0? "" : " " ) + nameparts[ 0 ];
339
340
			// set name
341
			setName( undoCharsetAndEncoding( params, value ) );
342
			_nameLevel = NAMELEVEL_N;
343
344
			// check now to see if we need to import this contact (to avoid
345
			// parsing the rest of the vCard unnecessarily)
346
			if( !isImportRequired( getName() ) )
347
				throw new SkipContactException();
348
		}
349
350
		private void parseFN( String[] params, String value )
351
				throws ParseException, SkipContactException
352
		{
353
			// already got a better name?
354
			if( _nameLevel >= NAMELEVEL_FN ) return;
355
356
			// set name
357
			setName( undoCharsetAndEncoding( params, value ) );
358
			_nameLevel = NAMELEVEL_FN;
359
		}
360
361
		private void parseORG( String[] params, String value )
362
				throws ParseException, SkipContactException
363
		{
364
			// already got a better name?
365
			if( _nameLevel >= NAMELEVEL_ORG ) return;
366
367
			// get org parts
368
			String[] orgparts = value.split( ";" );
369
			for( int i = 0; i < orgparts.length; i++ )
370
				orgparts[ i ] = orgparts[ i ].trim();
371
372
			// build name
373
			if( orgparts[ 0 ].length() == 0 && orgparts.length > 1 )
374
				value = orgparts[ 1 ];
375
			else
376
				value = orgparts[ 0 ];
377
378
			// set name
379
			setName( undoCharsetAndEncoding( params, value ) );
380
			_nameLevel = NAMELEVEL_ORG;
381
		}
382
383
		private void parseTEL( String[] params, String value )
384
				throws ParseException
385
		{
386
			if( value.length() == 0 ) return;
387
388
			Set< String > types = extractTypes( params, Arrays.asList(
389
					"PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
390
					"PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
391
392
			// here's the logic...
393
			boolean preferred = types.contains( "PREF" );
394
			if( types.contains( "VOICE" ) )
395
				if( types.contains( "WORK" ) )
396
					addPhone( value, PhonesColumns.TYPE_WORK, preferred );
397
				else
398
					addPhone( value, PhonesColumns.TYPE_HOME, preferred );
399
			else if( types.contains( "CELL" ) || types.contains( "VIDEO" ) )
400
				addPhone( value, PhonesColumns.TYPE_MOBILE, preferred );
401
			if( types.contains( "FAX" ) )
402
				if( types.contains( "HOME" ) )
403
					addPhone( value, PhonesColumns.TYPE_FAX_HOME, preferred );
404
				else
405
					addPhone( value, PhonesColumns.TYPE_FAX_WORK, preferred );
406
			if( types.contains( "PAGER" ) )
407
				addPhone( value, PhonesColumns.TYPE_PAGER, preferred );
408
		}
409
410
		public void parseEMAIL( String[] params, String value )
411
		{
412
			if( value.length() == 0 ) return;
413
414
			Set< String > types = extractTypes( params, Arrays.asList(
415
					"PREF", "WORK", "HOME", "INTERNET" ) );
416
417
			// here's the logic...
418
			boolean preferred = types.contains( "PREF" );
419
			if( types.contains( "WORK" ) )
420
				addEmail( value, Contacts.ContactMethods.TYPE_WORK, preferred );
421
			else
422
				addEmail( value, Contacts.ContactMethods.TYPE_HOME, preferred );
423
		}
424
425
		public void finaliseParsing()
426
				throws ParseException, SkipContactException,
427
				AbortImportException
428
		{
429
			// missing version (and data is present)
430
			if( _version == null && _lines != null )
431
				throw new ParseException( R.string.error_vcf_malformed );
432
433
			//  missing name properties?
434
			if( _nameLevel == NAMELEVEL_NONE )
435
				throw new ParseException( R.string.error_vcf_noname );
436
437
			// check if we should import this one? If we've already got an 'N'-
438
			// type name, this will already have been done by parseN() so we
439
			// mustn't do this here (or it could prompt twice!)
440
			if( _nameLevel < NAMELEVEL_N && !isImportRequired( getName() ) )
441
				throw new SkipContactException();
442
		}
443
444
		private String undoCharsetAndEncoding( String[] params, String value )
445
				throws ParseException
446
		{
447
			// check encoding/charset
448
			String charset, encoding;
449
			if( ( charset = checkParam( params, "CHARSET" ) ) != null &&
450
					!charset.equals( "UTF-8" ) && !charset.equals( "UTF-16" ) )
451
				throw new ParseException( R.string.error_vcf_charset );
452
			if( ( encoding = checkParam( params, "ENCODING" ) ) != null &&
453
					!encoding.equals( "QUOTED-PRINTABLE" ) )
454
				throw new ParseException( R.string.error_vcf_encoding );
455
456
			// do decoding?
457
			if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
458
				return unencodeQuotedPrintable( value, charset );
459
460
			// nothing to do!
461
			return value;
462
		}
463
464
		private String checkParam( String[] params, String name )
465
		{
466
			Pattern p = Pattern.compile( "^" + name + "[ \\t]*=[ \\t]*(.*)$" );
467
			for( int i = 0; i < params.length; i++ ) {
468
				Matcher m = p.matcher( params[ i ] );
469
				if( m.matches() )
470
					return m.group( 1 );
471
			}
472
			return null;
473
		}
474
475
		private Set< String > extractTypes( String[] params,
476
				List< String > validTypes )
477
		{
478
			HashSet< String > types = new HashSet< String >();
479
480
			// get 3.0-style TYPE= param
481
			String typeParam;
482
			if( ( typeParam = checkParam( params, "TYPE" ) ) != null ) {
483
				String[] bits = typeParam.split( "," );
484
				for( int i = 0; i < bits.length; i++ )
485
					if( validTypes.contains( bits[ i ] ) )
486
						types.add( bits[ i ] );
487
			}
488
489
			// get 2.1-style type param
490
			if( _version.equals( "2.1" ) ) {
491
				for( int i = 1; i < params.length; i++ )
492
					if( validTypes.contains( params[ i ] ) )
493
						types.add( params[ i ] );
494
			}
495
496
			return types;
497
		}
498
499
		private String unencodeQuotedPrintable( String str, String charset )
500
		{
501
			// default encoding scheme
502
			if( charset == null ) charset = "UTF-8";
503
504
			// unencode quoted-pritable encoding, as per RFC1521 section 5.1
505
			byte[] bytes = new byte[ str.length() ];
506
			int j = 0;
507
			for( int i = 0; i < str.length(); i++, j++ ) {
508
				char ch = str.charAt( i );
509
				if( ch == '=' && i < str.length() - 2 ) {
510
					bytes[ j ] = (byte)(
511
							Character.digit( str.charAt( i + 1 ), 16 ) * 16 +
512
							Character.digit( str.charAt( i + 2 ), 16 ) );
513
					i += 2;
514
				}
515
				else
516
					bytes[ j ] = (byte)ch;
517
			}
518
			try {
519
				return new String( bytes, 0, j, charset );
520
			} catch( UnsupportedEncodingException e ) { }
521
			return null;
522
		}
523
	}
524
}