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