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