/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
 *
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
4
 * Copyright (C) 2009 to 2011 Tim Marston <edam@waxworlds.org>
6 by edam
- added GPL header comments to all files
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;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
28
import java.io.FileInputStream;
1 by edam
Initial import
29
import java.io.FileNotFoundException;
30
import java.io.FileReader;
31
import java.io.FilenameFilter;
32
import java.io.IOException;
33
import java.io.UnsupportedEncodingException;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
34
import java.nio.ByteBuffer;
37 by edam
- updated TODO and NEWS
35
import java.util.ArrayList;
1 by edam
Initial import
36
import java.util.Arrays;
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
37
import java.util.HashMap;
1 by edam
Initial import
38
import java.util.HashSet;
36 by edam
- formatting: removed some double-indents on overrunning lines
39
import java.util.Iterator;
1 by edam
Initial import
40
import java.util.List;
37 by edam
- updated TODO and NEWS
41
import java.util.NoSuchElementException;
1 by edam
Initial import
42
import java.util.Set;
43
import java.util.Vector;
44
import java.util.regex.Matcher;
45
import java.util.regex.Pattern;
46
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
47
import org.waxworlds.edam.importcontacts.Importer.ContactData.ExtraDetail;
48
1 by edam
Initial import
49
import android.content.SharedPreferences;
50
import android.provider.Contacts;
51
import android.provider.Contacts.PhonesColumns;
52
53
public class VCFImporter extends Importer
54
{
55
	private int _vCardCount = 0;
56
	private int _progress = 0;
57
58
	public VCFImporter( Doit doit )
59
	{
60
		super( doit );
61
	}
62
63
	@Override
64
	protected void onImport() throws AbortImportException
65
	{
66
		SharedPreferences prefs = getSharedPreferences();
67
68
		// update UI
69
		setProgressMessage( R.string.doit_scanning );
70
71
		// get a list of vcf files
72
		File[] files = null;
73
		try
74
		{
75
			// open directory
19 by edam
- added file chooser
76
			String path = "/sdcard" + prefs.getString( "location", "/" );
77
			File file = new File( path );
78
			if( !file.exists() )
1 by edam
Initial import
79
				showError( R.string.error_locationnotfound );
80
15 by edam
- added facility to enter a filename (instead of a directory to scan) and just use that
81
			// directory, or file?
19 by edam
- added file chooser
82
			if( file.isDirectory() )
15 by edam
- added facility to enter a filename (instead of a directory to scan) and just use that
83
			{
84
				// get files
85
				class VCardFilter implements FilenameFilter {
86
					public boolean accept( File dir, String name ) {
87
						return name.toLowerCase().endsWith( ".vcf" );
88
					}
13 by edam
- converted project to use Android 1.5 SDK
89
				}
19 by edam
- added file chooser
90
				files = file.listFiles( new VCardFilter() );
15 by edam
- added facility to enter a filename (instead of a directory to scan) and just use that
91
			}
92
			else
93
			{
94
				// use just this file
95
				files = new File[ 1 ];
19 by edam
- added file chooser
96
				files[ 0 ] = file;
15 by edam
- added facility to enter a filename (instead of a directory to scan) and just use that
97
			}
1 by edam
Initial import
98
		}
99
		catch( SecurityException e ) {
100
			showError( R.string.error_locationpermissions );
101
		}
102
103
		// check num files and set progress max
104
		if( files != null && files.length > 0 )
105
			setProgressMax( files.length );
106
		else
107
			showError( R.string.error_locationnofiles );
108
109
		// scan through the files
110
		setTmpProgress( 0 );
111
		for( int i = 0; i < files.length; i++ ) {
112
			countVCardFile( files[ i ] );
113
			setTmpProgress( i );
114
		}
115
		setProgressMax( _vCardCount );	// will also update tmp progress
116
117
		// import them
118
		setProgress( 0 );
119
		for( int i = 0; i < files.length; i++ )
120
			importVCardFile( files[ i ] );
121
	}
122
123
	private void countVCardFile( File file ) throws AbortImportException
124
	{
125
		try
126
		{
127
			// open file
128
			BufferedReader reader = new BufferedReader(
36 by edam
- formatting: removed some double-indents on overrunning lines
129
				new FileReader( file ) );
1 by edam
Initial import
130
131
			// read
132
			String line;
133
			boolean inVCard = false;
134
			while( ( line = reader.readLine() ) != null )
135
			{
136
				if( !inVCard ) {
137
					// look for vcard beginning
36 by edam
- formatting: removed some double-indents on overrunning lines
138
					if( line.matches( "^BEGIN:VCARD" ) ) {
1 by edam
Initial import
139
						inVCard = true;
140
						_vCardCount++;
141
					}
142
				}
36 by edam
- formatting: removed some double-indents on overrunning lines
143
				else if( line.matches( "^END:VCARD" ) )
1 by edam
Initial import
144
					inVCard = false;
145
			}
146
147
		}
148
		catch( FileNotFoundException e ) {
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
149
			showError( getText( R.string.error_filenotfound ) +
150
				file.getName() );
1 by edam
Initial import
151
		}
152
		catch( IOException e ) {
153
			showError( getText( R.string.error_ioerror ) + file.getName() );
154
		}
155
	}
156
157
	private void importVCardFile( File file ) throws AbortImportException
158
	{
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
159
		// check file is good
160
		if( !file.exists() )
161
			showError( getText( R.string.error_filenotfound ) +
162
				file.getName() );
163
		if( file.length() == 0 )
164
			showError( getText( R.string.error_fileisempty ) +
165
				file.getName() );
166
1 by edam
Initial import
167
		try
168
		{
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
169
			// open/read file
170
			FileInputStream istream = new FileInputStream( file );
171
			byte[] content = new byte[ (int)file.length() ];
172
			istream.read( content );
173
174
			// import
175
			importVCardFileContent( content, file.getName() );
1 by edam
Initial import
176
		}
177
		catch( FileNotFoundException e ) {
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
178
			showError( getText( R.string.error_filenotfound ) +
179
				file.getName() );
1 by edam
Initial import
180
		}
181
		catch( IOException e ) {
182
			showError( getText( R.string.error_ioerror ) + file.getName() );
183
		}
184
	}
185
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
186
	private void importVCardFileContent( byte[] content, String fileName )
36 by edam
- formatting: removed some double-indents on overrunning lines
187
		throws AbortImportException
1 by edam
Initial import
188
	{
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
189
		// go through lines
1 by edam
Initial import
190
		VCard vCard = null;
36 by edam
- formatting: removed some double-indents on overrunning lines
191
		ContentLineIterator cli = new ContentLineIterator( content );
192
		while( cli.hasNext() )
1 by edam
Initial import
193
		{
36 by edam
- formatting: removed some double-indents on overrunning lines
194
			ByteBuffer buffer = cli.next();
195
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
196
			// get a US-ASCII version of the line for processing
197
			String line;
198
			try {
36 by edam
- formatting: removed some double-indents on overrunning lines
199
				line = new String( buffer.array(), buffer.position(),
200
					buffer.limit() - buffer.position(), "US-ASCII" );
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
201
			}
202
			catch( UnsupportedEncodingException e ) {
203
				// we know US-ASCII is supported, so appease the compiler...
204
				line = "";
205
			}
1 by edam
Initial import
206
207
			if( vCard == null ) {
208
				// look for vcard beginning
36 by edam
- formatting: removed some double-indents on overrunning lines
209
				if( line.matches( "^BEGIN:VCARD" ) ) {
1 by edam
Initial import
210
					setProgress( ++_progress );
211
					vCard = new VCard();
212
				}
213
			}
214
			else {
215
				// look for vcard content or ending
36 by edam
- formatting: removed some double-indents on overrunning lines
216
				if( line.matches( "^END:VCARD" ) )
1 by edam
Initial import
217
				{
218
					// store vcard and do away with it
219
					try {
220
						vCard.finaliseParsing();
221
						importContact( vCard );
222
					}
223
					catch( VCard.ParseException e ) {
224
						skipContact();
225
						if( !showContinue(
36 by edam
- formatting: removed some double-indents on overrunning lines
226
							getText( R.string.error_vcf_parse ).toString()
227
							+ fileName + "\n" + e.getMessage() ) )
228
						{
3 by edam
- added "all done" message
229
							finish( ACTION_ABORT );
36 by edam
- formatting: removed some double-indents on overrunning lines
230
						}
1 by edam
Initial import
231
					}
232
					catch( VCard.SkipContactException e ) {
233
						skipContact();
234
						// do nothing
235
					}
236
					vCard = null;
237
				}
238
				else
239
				{
240
					// try giving the line to the vcard
241
					try {
36 by edam
- formatting: removed some double-indents on overrunning lines
242
						vCard.parseLine( buffer, line,
243
							cli.doesNextLineLookFolded() );
1 by edam
Initial import
244
					}
245
					catch( VCard.ParseException e ) {
246
						skipContact();
247
						if( !showContinue(
36 by edam
- formatting: removed some double-indents on overrunning lines
248
							getText( R.string.error_vcf_parse ).toString()
249
							+ fileName + "\n" + e.getMessage() ) )
250
						{
3 by edam
- added "all done" message
251
							finish( ACTION_ABORT );
36 by edam
- formatting: removed some double-indents on overrunning lines
252
						}
1 by edam
Initial import
253
254
						// although we're continuing, we still need to abort
255
						// this vCard. Further lines will be ignored until we
256
						// get to another BEGIN:VCARD line.
257
						vCard = null;
258
					}
259
					catch( VCard.SkipContactException e ) {
260
						skipContact();
261
						// abort this vCard. Further lines will be ignored until
262
						// we get to another BEGIN:VCARD line.
263
						vCard = null;
264
					}
265
				}
266
			}
267
		}
268
	}
269
36 by edam
- formatting: removed some double-indents on overrunning lines
270
	class ContentLineIterator implements Iterator< ByteBuffer >
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
271
	{
36 by edam
- formatting: removed some double-indents on overrunning lines
272
		protected byte[] _content = null;
273
		protected int _pos = 0;
274
275
		public ContentLineIterator( byte[] content )
276
		{
277
			_content = content;
278
		}
279
280
		@Override
281
		public boolean hasNext()
282
		{
283
			return _pos < _content.length;
284
		}
285
286
		@Override
287
		public ByteBuffer next()
288
		{
289
			int initial_pos = _pos;
290
291
			// find newline
292
			for( ; _pos < _content.length; _pos++ )
293
				if( _content[ _pos ] == '\n' )
294
				{
295
					// adjust for a \r preceding the \n
296
					int to = ( _pos > 0 && _content[ _pos - 1 ] == '\r' &&
297
						_pos > initial_pos )? _pos - 1 : _pos;
298
					_pos++;
299
					return ByteBuffer.wrap( _content, initial_pos,
300
						to - initial_pos );
301
				}
302
303
			// we didn't find one, but were there bytes left?
304
			if( _pos != initial_pos ) {
305
				int to = _pos;
306
				_pos++;
307
				return ByteBuffer.wrap( _content, initial_pos,
308
					to - initial_pos );
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
309
			}
36 by edam
- formatting: removed some double-indents on overrunning lines
310
311
			// no bytes left
312
			throw new NoSuchElementException();
313
		}
314
315
		@Override
316
		public void remove()
317
		{
318
			throw new UnsupportedOperationException();
319
		}
320
321
		/**
322
		 * Does the next line, if there is one, look like it should be folded
323
		 * onto the end of this one?
324
		 * @return
325
		 */
326
		public boolean doesNextLineLookFolded()
327
		{
328
			return _pos > 0 && _pos < _content.length &&
329
				_content[ _pos - 1 ] == '\n' && _content[ _pos ] == ' ';
330
		}
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
331
	}
332
1 by edam
Initial import
333
	private class VCard extends ContactData
334
	{
335
		private final static int NAMELEVEL_NONE = 0;
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
336
		private final static int NAMELEVEL_FN = 1;
337
		private final static int NAMELEVEL_N = 2;
1 by edam
Initial import
338
37 by edam
- updated TODO and NEWS
339
		private final static int MULTILINE_NONE = 0;
340
		private final static int MULTILINE_ENCODED = 1;	// v2.1 quoted-printable
341
		private final static int MULTILINE_ESCAPED = 2;	// v2.1 \\CRLF
342
		private final static int MULTILINE_FOLDED = 3;	// v3.0 folding
343
1 by edam
Initial import
344
		private String _version = null;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
345
		private Vector< ByteBuffer > _buffers = null;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
346
		private int _name_level = NAMELEVEL_NONE;
37 by edam
- updated TODO and NEWS
347
		private int _parser_multiline_state = MULTILINE_NONE;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
348
		private String _parser_current_name_and_params = null;
349
		private String _parser_buffered_value_so_far = "";
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
350
		private String _cached_organisation = null;
351
		private String _cached_title = null;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
352
353
		protected class UnencodeResult
354
		{
355
			private boolean _another_line_required;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
356
			private ByteBuffer _buffer;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
357
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
358
			public UnencodeResult( boolean another_line_required,
359
				ByteBuffer buffer )
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
360
			{
361
				_another_line_required = another_line_required;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
362
				_buffer = buffer;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
363
			}
364
365
			public boolean isAnotherLineRequired()
366
			{
367
				return _another_line_required;
368
			}
369
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
370
			public ByteBuffer getBuffer()
371
			{
372
				return _buffer;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
373
			}
374
		}
1 by edam
Initial import
375
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
376
		@SuppressWarnings("serial")
1 by edam
Initial import
377
		protected class ParseException extends Exception
378
		{
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
379
			@SuppressWarnings("unused")
1 by edam
Initial import
380
			public ParseException( String error )
381
			{
382
				super( error );
383
			}
384
385
			public ParseException( int res )
386
			{
387
				super( VCFImporter.this.getText( res ).toString() );
388
			}
389
		}
390
14 by edam
- got rid of the pretend ImportContacts activity alltogether (and made the Intro activity the startup one)
391
		@SuppressWarnings("serial")
1 by edam
Initial import
392
		protected class SkipContactException extends Exception { }
393
36 by edam
- formatting: removed some double-indents on overrunning lines
394
		private String extractCollonPartFromLine( ByteBuffer buffer,
395
			String line, boolean former )
396
		{
397
			String ret = null;
398
399
			// get a US-ASCII version of the line for processing, unless we were
400
			// supplied with one
401
			if( line == null ) {
402
				try {
403
					line = new String( buffer.array(), buffer.position(),
404
						buffer.limit() - buffer.position(), "US-ASCII" );
405
				}
406
				catch( UnsupportedEncodingException e ) {
407
					// we know US-ASCII is supported, so appease the compiler...
408
					line = "";
409
				}
410
			}
411
412
			// split line into name and value parts and check to make sure we
413
			// only got 2 parts and that the first part is not zero in length
414
			String[] parts = line.split( ":", 2 );
415
			if( parts.length == 2 && parts[ 0 ].length() > 0 )
416
				ret = parts[ former? 0 : 1 ];
417
418
			return ret;
419
		}
420
421
		private String extractNameAndParamsFromLine( ByteBuffer buffer,
422
			String line )
423
		{
424
			return extractCollonPartFromLine( buffer, line, true );
425
		}
426
427
		private String extractValueFromLine( ByteBuffer buffer, String line )
428
		{
429
			return extractCollonPartFromLine( buffer, line, false );
430
		}
431
432
		public void parseLine( ByteBuffer buffer, String line,
433
			boolean next_line_looks_folded )
434
			throws ParseException, SkipContactException,
435
			AbortImportException
436
		{
437
			// do we have a version yet?
1 by edam
Initial import
438
			if( _version == null )
439
			{
36 by edam
- formatting: removed some double-indents on overrunning lines
440
				// tentatively get name and params from line
441
				String name_and_params =
442
					extractNameAndParamsFromLine( buffer, line );
443
444
				// is it a version line?
445
				if( name_and_params != null &&
446
					name_and_params.equals( "VERSION" ) )
1 by edam
Initial import
447
				{
36 by edam
- formatting: removed some double-indents on overrunning lines
448
					// yes, get it!
449
					String value = extractValueFromLine( buffer, line );
450
					if( !value.equals( "2.1" ) && !value.equals( "3.0" ) )
1 by edam
Initial import
451
						throw new ParseException( R.string.error_vcf_version );
36 by edam
- formatting: removed some double-indents on overrunning lines
452
					_version = value;
1 by edam
Initial import
453
36 by edam
- formatting: removed some double-indents on overrunning lines
454
					// parse any buffers we've been accumulating while we waited
455
					// for a version
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
456
					if( _buffers != null )
457
						for( int i = 0; i < _buffers.size(); i++ )
36 by edam
- formatting: removed some double-indents on overrunning lines
458
							parseLine( _buffers.get( i ), null,
459
								i + 1 < _buffers.size() &&
460
								_buffers.get( i + 1 ).hasRemaining() &&
461
								_buffers.get( i + 1 ).get(
462
									_buffers.get( i + 1 ).position() ) == ' ' );
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
463
					_buffers = null;
1 by edam
Initial import
464
				}
465
				else
466
				{
36 by edam
- formatting: removed some double-indents on overrunning lines
467
					// no, so stash this line till we get a version
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
468
					if( _buffers == null )
469
						_buffers = new Vector< ByteBuffer >();
470
					_buffers.add( buffer );
1 by edam
Initial import
471
				}
472
			}
473
			else
474
			{
36 by edam
- formatting: removed some double-indents on overrunning lines
475
				// name and params and the position in the buffer where the
476
				// "value" part of the line start
477
				String name_and_params;
478
				int pos;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
479
37 by edam
- updated TODO and NEWS
480
				if( _parser_multiline_state != MULTILINE_NONE )
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
481
				{
482
					// if we're currently in a multi-line value, use the stored
483
					// property name and parameters
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
484
					name_and_params = _parser_current_name_and_params;
485
37 by edam
- updated TODO and NEWS
486
					// skip some initial line characters, depending on the type
487
					// of multi-line we're handling
36 by edam
- formatting: removed some double-indents on overrunning lines
488
					pos = buffer.position();
37 by edam
- updated TODO and NEWS
489
					switch( _parser_multiline_state )
490
					{
491
					case MULTILINE_FOLDED:
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
492
						pos++;
37 by edam
- updated TODO and NEWS
493
						break;
494
					case MULTILINE_ENCODED:
36 by edam
- formatting: removed some double-indents on overrunning lines
495
						while( pos < buffer.limit() && (
496
							buffer.get( pos ) == ' ' ||
497
							buffer.get( pos ) == '\t' ) )
498
						{
499
							pos++;
500
						}
37 by edam
- updated TODO and NEWS
501
						break;
502
					default:
503
						// do nothing
504
					}
505
506
					// take us out of multi-line so that we can re-detect that
507
					// this line is a multi-line or not
508
					_parser_multiline_state = MULTILINE_NONE;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
509
				}
510
				else
511
				{
36 by edam
- formatting: removed some double-indents on overrunning lines
512
					// get name and params from line, and since we're not
513
					// parsing a subsequent line in a multi-line, this should
514
					// not fail, or it's an error
515
					name_and_params =
516
						extractNameAndParamsFromLine( buffer, line );
517
					if( name_and_params == null )
518
						throw new ParseException(
519
							R.string.error_vcf_malformed );
520
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
521
					// calculate how many chars to skip from beginning of line
522
					// so we skip the property "name:" part
36 by edam
- formatting: removed some double-indents on overrunning lines
523
					pos = buffer.position() + name_and_params.length() + 1;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
524
525
					// reset the saved multi-line state
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
526
					_parser_current_name_and_params = name_and_params;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
527
					_parser_buffered_value_so_far = "";
528
				}
529
36 by edam
- formatting: removed some double-indents on overrunning lines
530
				// get value from buffer, as raw bytes
531
				ByteBuffer value;
532
				value = ByteBuffer.wrap( buffer.array(), pos,
533
					buffer.limit() - pos );
534
1 by edam
Initial import
535
				// get parameter parts
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
536
				String[] name_param_parts = name_and_params.split( ";", -1 );
537
				for( int i = 0; i < name_param_parts.length; i++ )
538
					name_param_parts[ i ] = name_param_parts[ i ].trim();
1 by edam
Initial import
539
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
540
				// parse encoding parameter
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
541
				String encoding = checkParam( name_param_parts, "ENCODING" );
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
542
				if( encoding != null ) encoding = encoding.toUpperCase();
543
				if( encoding != null && !encoding.equals( "8BIT" ) &&
544
					!encoding.equals( "QUOTED-PRINTABLE" ) )
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
545
					//&& !encoding.equals( "BASE64" ) )
546
				{
547
					throw new ParseException( R.string.error_vcf_encoding );
548
				}
549
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
550
				// parse charset parameter
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
551
				String charset = checkParam( name_param_parts, "CHARSET" );
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
552
				if( charset != null ) charset = charset.toUpperCase();
553
				if( charset != null && !charset.equals( "US-ASCII" ) &&
36 by edam
- formatting: removed some double-indents on overrunning lines
554
					!charset.equals( "ASCII" ) &&
555
					!charset.equals( "UTF-8" ) )
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
556
				{
557
					throw new ParseException( R.string.error_vcf_charset );
558
				}
559
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
560
				// do unencoding (or default to a fake unencoding result with
561
				// the raw string)
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
562
				UnencodeResult unencoding_result = null;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
563
				if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
564
					unencoding_result = unencodeQuotedPrintable( value );
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
565
//				else if( encoding != null && encoding.equals( "BASE64" ) )
34 by edam
- check for empty data "values" after parsing line parameters, so that we catch parameter errors (such as unknown encoding types).
566
//					unencoding_result = unencodeBase64( props[ 1 ], charset );
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
567
				if( unencoding_result != null ) {
568
					value = unencoding_result.getBuffer();
37 by edam
- updated TODO and NEWS
569
					if( unencoding_result.isAnotherLineRequired() )
570
						_parser_multiline_state = MULTILINE_ENCODED;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
571
				}
572
573
				// convert 8-bit ASCII charset to US-ASCII
33 by edam
- fixed a couple of java string comparison checks
574
				if( charset == null || charset.equals( "ASCII" ) ) {
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
575
					value = transcodeAsciiToUtf8( value );
576
					charset = "UTF-8";
577
				}
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
578
579
				// process charset
36 by edam
- formatting: removed some double-indents on overrunning lines
580
				String string_value;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
581
				try {
36 by edam
- formatting: removed some double-indents on overrunning lines
582
					string_value = new String( value.array(), value.position(),
583
						value.limit() - value.position(), charset );
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
584
				} catch( UnsupportedEncodingException e ) {
585
					throw new ParseException( R.string.error_vcf_charset );
586
				}
587
37 by edam
- updated TODO and NEWS
588
				// for some entries that have semicolon-separated value parts,
589
				// check to see if the value ends in an escape character, which
590
				// indicates that we have a multi-line value
591
				if( ( name_param_parts[ 0 ].equals( "N" ) ||
592
					name_param_parts[ 0 ].equals( "ORG" ) ||
593
					name_param_parts[ 0 ].equals( "ADR" ) ) &&
594
					doesStringEndInAnEscapeChar( string_value ) )
595
				{
596
					_parser_multiline_state = MULTILINE_ESCAPED;
597
					string_value = string_value.substring( 0,
598
						string_value.length() - 1 );
599
				}
600
36 by edam
- formatting: removed some double-indents on overrunning lines
601
				// now we know whether we're in an encoding multi-line,
602
				// determine if we're in a v3 folded multi-line or not
37 by edam
- updated TODO and NEWS
603
				if( _parser_multiline_state == MULTILINE_NONE &&
604
					_version.equals( "3.0" ) && next_line_looks_folded )
605
				{
606
					_parser_multiline_state = MULTILINE_FOLDED;
607
				}
36 by edam
- formatting: removed some double-indents on overrunning lines
608
37 by edam
- updated TODO and NEWS
609
				// handle multi-lines by buffering them and parsing them when we
610
				// are processing the last line in a multi-line sequence
611
				if( _parser_multiline_state != MULTILINE_NONE ) {
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
612
					_parser_buffered_value_so_far += string_value;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
613
					return;
614
				}
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
615
				String complete_value =
37 by edam
- updated TODO and NEWS
616
					( _parser_buffered_value_so_far + string_value ).trim();
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
617
34 by edam
- check for empty data "values" after parsing line parameters, so that we catch parameter errors (such as unknown encoding types).
618
				// ignore empty values
619
				if( complete_value.length() < 1 ) return;
620
1 by edam
Initial import
621
				// parse some properties
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
622
				if( name_param_parts[ 0 ].equals( "N" ) )
623
					parseN( name_param_parts, complete_value );
624
				else if( name_param_parts[ 0 ].equals( "FN" ) )
625
					parseFN( name_param_parts, complete_value );
626
				else if( name_param_parts[ 0 ].equals( "ORG" ) )
627
					parseORG( name_param_parts, complete_value );
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
628
				else if( name_param_parts[ 0 ].equals( "TITLE" ) )
629
					parseTITLE( name_param_parts, complete_value );
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
630
				else if( name_param_parts[ 0 ].equals( "TEL" ) )
631
					parseTEL( name_param_parts, complete_value );
632
				else if( name_param_parts[ 0 ].equals( "EMAIL" ) )
633
					parseEMAIL( name_param_parts, complete_value );
37 by edam
- updated TODO and NEWS
634
				else if( name_param_parts[ 0 ].equals( "ADR" ) )
635
					parseADR( name_param_parts, complete_value );
636
			}
637
		}
638
639
		private boolean doesStringEndInAnEscapeChar( String string )
640
		{
641
			// count the number of backslashes at the end of the string
642
			int count = 0;
643
			for( int a = string.length() - 1; a >= 0; a-- )
644
				if( string.charAt( a ) == '\\' )
645
					count++;
646
				else
647
					break;
648
649
			// if there are an even number of backslashes then the final one
650
			// doesn't count
651
			return ( count & 1 ) == 1;
652
		}
653
654
		private String[] splitValueBySemicolon( String value )
655
		{
656
			// split string in to parts by semicolon
657
			ArrayList< String > parts = new ArrayList< String >(
658
				Arrays.asList( value.split(  ";" ) ) );
659
660
			// go through parts
661
			for( int a = 0; a < parts.size(); a++ )
662
			{
663
				String str = parts.get( a );
664
665
				// look for parts that end in an escape character, but ignore
666
				// the final part. We've already detected escape chars at the
667
				// end of the final part in parseLine() and handled multi-lines
668
				// accordingly.
669
				if( a < parts.size() - 1 &&
670
					doesStringEndInAnEscapeChar( str ) )
671
				{
672
					// join the next part to this part and remove the next part
673
					parts.set( a, str.substring( 0, str.length() - 1 ) +
674
						';' + parts.get( a + 1 ) );
675
					parts.remove( a + 1 );
676
677
					// re-visit this part
678
					a--;
679
					continue;
680
				}
681
682
				// trim and replace string
683
				str = str.trim();
684
				parts.set( a, str );
685
			}
686
687
			String[] ret = new String[ parts.size() ];
688
			return parts.toArray( ret );
1 by edam
Initial import
689
		}
690
691
		private void parseN( String[] params, String value )
692
		{
693
			// already got a better name?
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
694
			if( _name_level >= NAMELEVEL_N ) return;
1 by edam
Initial import
695
696
			// get name parts
37 by edam
- updated TODO and NEWS
697
			String[] name_parts = splitValueBySemicolon( value );
1 by edam
Initial import
698
699
			// build name
700
			value = "";
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
701
			if( name_parts.length > 1 && name_parts[ 1 ].length() > 0 )
702
				value += name_parts[ 1 ];
703
			if( name_parts.length > 0 && name_parts[ 0 ].length() > 0 )
704
				value += ( value.length() == 0? "" : " " ) + name_parts[ 0 ];
1 by edam
Initial import
705
706
			// set name
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
707
			setName( value );
708
			_name_level = NAMELEVEL_N;
1 by edam
Initial import
709
		}
710
711
		private void parseFN( String[] params, String value )
712
		{
713
			// already got a better name?
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
714
			if( _name_level >= NAMELEVEL_FN ) return;
1 by edam
Initial import
715
716
			// set name
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
717
			setName( value );
718
			_name_level = NAMELEVEL_FN;
1 by edam
Initial import
719
		}
720
721
		private void parseORG( String[] params, String value )
722
		{
723
			// get org parts
37 by edam
- updated TODO and NEWS
724
			String[] org_parts = splitValueBySemicolon( value );
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
725
			if( org_parts == null || org_parts.length < 1 ) return;
726
727
			// build organisation name
728
			StringBuilder builder = new StringBuilder(
729
				String.valueOf( org_parts[ 0 ] ) );
730
			for( int a = 1; a < org_parts.length; a++ )
731
				builder.append( ", " ).append( org_parts[ a ] );
732
			String organisation = builder.toString();
733
734
			// set organisation name (using a title we've previously found)
735
			addOrganisation( organisation, _cached_title, true );
736
737
			// if we've not previously found a title, store this organisation
738
			// name (we'll need it when we find a title to update the
739
			// organisation, by name), else if we *have* previously found a
740
			// title, clear it (since we just used it)
741
			if( _cached_title == null )
742
				_cached_organisation = organisation;
743
			else
744
				_cached_title = null;
745
		}
746
747
		private void parseTITLE( String[] params, String value )
748
		{
749
			// if we previously had an organisation, look it up and append this
750
			// title to it
751
			if( _cached_organisation != null && hasOrganisations() ) {
752
				HashMap< String, ExtraDetail > datas = getOrganisations();
753
				ExtraDetail detail = datas.get( _cached_organisation );
754
				if( detail != null )
755
					detail.setExtra( value );
756
			}
757
758
			// same as when handling organisation, if we've not previously found
759
			// an organisation we store this title, else we clear it (since we
760
			// just appended this title to it)
761
			if( _cached_organisation == null )
762
				_cached_title = value;
763
			else
764
				_cached_organisation = null;
1 by edam
Initial import
765
		}
766
767
		private void parseTEL( String[] params, String value )
768
		{
769
			if( value.length() == 0 ) return;
770
771
			Set< String > types = extractTypes( params, Arrays.asList(
36 by edam
- formatting: removed some double-indents on overrunning lines
772
				"PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
773
				"PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
1 by edam
Initial import
774
775
			// here's the logic...
776
			boolean preferred = types.contains( "PREF" );
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
777
			int type;
1 by edam
Initial import
778
			if( types.contains( "FAX" ) )
779
				if( types.contains( "HOME" ) )
24 by edam
- import phone numbers even when they have no specified type (default to mobile)
780
					type = PhonesColumns.TYPE_FAX_HOME;
1 by edam
Initial import
781
				else
24 by edam
- import phone numbers even when they have no specified type (default to mobile)
782
					type = PhonesColumns.TYPE_FAX_WORK;
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
783
			else if( types.contains( "CELL" ) || types.contains( "VIDEO" ) )
784
				type = PhonesColumns.TYPE_MOBILE;
785
			else if( types.contains( "PAGER" ) )
24 by edam
- import phone numbers even when they have no specified type (default to mobile)
786
				type = PhonesColumns.TYPE_PAGER;
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
787
			else if( types.contains( "WORK" ) )
788
				type = PhonesColumns.TYPE_WORK;
789
			else
790
				type = PhonesColumns.TYPE_HOME;
24 by edam
- import phone numbers even when they have no specified type (default to mobile)
791
792
			// add phone number
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
793
			addNumber( value, type, preferred );
1 by edam
Initial import
794
		}
795
796
		public void parseEMAIL( String[] params, String value )
797
		{
798
			if( value.length() == 0 ) return;
799
800
			Set< String > types = extractTypes( params, Arrays.asList(
36 by edam
- formatting: removed some double-indents on overrunning lines
801
				"PREF", "WORK", "HOME", "INTERNET" ) );
1 by edam
Initial import
802
37 by edam
- updated TODO and NEWS
803
			// add email address
1 by edam
Initial import
804
			boolean preferred = types.contains( "PREF" );
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
805
			int type;
1 by edam
Initial import
806
			if( types.contains( "WORK" ) )
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
807
				type = Contacts.ContactMethods.TYPE_WORK;
1 by edam
Initial import
808
			else
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
809
				type = Contacts.ContactMethods.TYPE_HOME;
810
811
			addEmail( value, type, preferred );
1 by edam
Initial import
812
		}
813
37 by edam
- updated TODO and NEWS
814
		private void parseADR( String[] params, String value )
815
		{
816
			// get address parts
817
			String[] adr_parts = splitValueBySemicolon( value );
818
819
			// build address
820
			value = "";
821
			for( int a = 0; a < adr_parts.length; a++ ) {
822
				if( value.length() > 0 ) value += "\n";
823
				value += adr_parts[ a ].trim();
824
			}
825
826
			Set< String > types = extractTypes( params, Arrays.asList(
827
				"PREF", "WORK", "HOME", "INTERNET" ) );
828
829
			// add address
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
830
			int type;
37 by edam
- updated TODO and NEWS
831
			if( types.contains( "WORK" ) )
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
832
				type = Contacts.ContactMethods.TYPE_WORK;
37 by edam
- updated TODO and NEWS
833
			else
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
834
				type = Contacts.ContactMethods.TYPE_HOME;
835
836
			addAddress( value, type );
37 by edam
- updated TODO and NEWS
837
		}
838
1 by edam
Initial import
839
		public void finaliseParsing()
36 by edam
- formatting: removed some double-indents on overrunning lines
840
			throws ParseException, SkipContactException,
841
			AbortImportException
1 by edam
Initial import
842
		{
843
			// missing version (and data is present)
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
844
			if( _version == null && _buffers != null )
1 by edam
Initial import
845
				throw new ParseException( R.string.error_vcf_malformed );
846
40 by edam
- fixed logic for vcard field types (home, work, cell, etc) so it works
847
			// check if we should import this contact
848
			try {
849
				if( !isImportRequired( this ) )
850
					throw new SkipContactException();
851
			}
852
			catch( ContactNeedsMoreInfoException e ) {
853
				throw new ParseException( R.string.error_vcf_notenoughinfo );
854
			}
1 by edam
Initial import
855
		}
856
857
		private String checkParam( String[] params, String name )
858
		{
35 by edam
- accept parameters that are quoted (this doesn't appear to be part of the standards AFAICT, but Evolution apparently quotes parameter values)
859
			Pattern p = Pattern.compile(
36 by edam
- formatting: removed some double-indents on overrunning lines
860
				"^" + name + "[ \\t]*=[ \\t]*(\"?)(.*)\\1$" );
1 by edam
Initial import
861
			for( int i = 0; i < params.length; i++ ) {
862
				Matcher m = p.matcher( params[ i ] );
863
				if( m.matches() )
35 by edam
- accept parameters that are quoted (this doesn't appear to be part of the standards AFAICT, but Evolution apparently quotes parameter values)
864
					return m.group( 2 );
1 by edam
Initial import
865
			}
866
			return null;
867
		}
868
869
		private Set< String > extractTypes( String[] params,
36 by edam
- formatting: removed some double-indents on overrunning lines
870
			List< String > valid_types )
1 by edam
Initial import
871
		{
872
			HashSet< String > types = new HashSet< String >();
873
874
			// get 3.0-style TYPE= param
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
875
			String type_param;
876
			if( ( type_param = checkParam( params, "TYPE" ) ) != null ) {
877
				String[] parts = type_param.split( "," );
878
				for( int i = 0; i < parts.length; i++ )
879
					if( valid_types.contains( parts[ i ] ) )
880
						types.add( parts[ i ] );
1 by edam
Initial import
881
			}
882
883
			// get 2.1-style type param
884
			if( _version.equals( "2.1" ) ) {
885
				for( int i = 1; i < params.length; i++ )
25 by edam
- fixed bug where parts[0] was assumed to exists after calling split()
886
					if( valid_types.contains( params[ i ] ) )
1 by edam
Initial import
887
						types.add( params[ i ] );
888
			}
889
890
			return types;
891
		}
892
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
893
		private UnencodeResult unencodeQuotedPrintable( ByteBuffer in )
1 by edam
Initial import
894
		{
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
895
			boolean another = false;
896
36 by edam
- formatting: removed some double-indents on overrunning lines
897
			// unencode quoted-printable encoding, as per RFC1521 section 5.1
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
898
			byte[] out = new byte[ in.limit() - in.position() ];
1 by edam
Initial import
899
			int j = 0;
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
900
			for( int i = in.position(); i < in.limit(); i++ )
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
901
			{
902
				// get next char and process...
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
903
				byte ch = in.array()[ i ];
904
				if( ch == '=' && i < in.limit() - 2 )
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
905
				{
906
					// we found a =XX format byte, add it
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
907
					out[ j ] = (byte)(
36 by edam
- formatting: removed some double-indents on overrunning lines
908
							Character.digit( in.array()[ i + 1 ], 16 ) * 16 +
909
							Character.digit( in.array()[ i + 2 ], 16 ) );
1 by edam
Initial import
910
					i += 2;
911
				}
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
912
				else if( ch == '=' && i == in.limit() - 1 )
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
913
				{
914
					// we found a '=' at the end of a line signifying a multi-
915
					// line string, so we don't add it.
916
					another = true;
917
					continue;
918
				}
1 by edam
Initial import
919
				else
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
920
					// just a normal char...
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
921
					out[ j ] = (byte)ch;
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
922
				j++;
1 by edam
Initial import
923
			}
18 by edam
- changed case on charset and encoding warning strings (it looked bad)
924
22 by edam
- read vCard files in as raw bytes now and convert to string only tentatively to check for version no.s and property names and params
925
			return new UnencodeResult( another, ByteBuffer.wrap( out, 0, j ) );
926
		}
927
928
		private ByteBuffer transcodeAsciiToUtf8( ByteBuffer in )
929
		{
930
			// transcode
931
			byte[] out = new byte[ ( in.limit() - in.position() ) * 2 ];
932
			int j = 0;
933
			for( int a = in.position(); a < in.limit(); a++ )
934
			{
935
				// if char is < 127, keep it as-is
936
				if( in.array()[ a ] >= 0 )
937
					out[ j++ ] = in.array()[ a ];
938
939
				// else, convert it to UTF-8
940
				else {
941
					int b = 0xff & (int)in.array()[ a ];
942
					out[ j++ ] = (byte)( 0xc0 | ( b >> 6 ) );
943
					out[ j++ ] = (byte)( 0x80 | ( b & 0x3f ) );
944
				}
945
			}
946
947
			return ByteBuffer.wrap( out, 0, j );
1 by edam
Initial import
948
		}
949
	}
950
}