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