/android/import-contacts

To get this branch, use:
bzr branch http://bzr.ed.am/android/import-contacts

« back to all changes in this revision

Viewing changes to src/org/waxworlds/importcontacts/VCFImporter.java

  • Committer: edam
  • Date: 2009-01-13 06:35:26 UTC
  • Revision ID: edam@waxworlds.org-20090113063526-l9t1s9git4bav60a
- new contact's phone numebrs and email addresses are added to the caches after those contacts are updated to account for the situation where the same contact is imported again from another file (or the contact exists twice in the same file!?)

Show diffs side-by-side

added added

removed removed

1
1
/*
2
2
 * VCFImporter.java
3
3
 *
4
 
 * Copyright (C) 2009 to 2011 Tim Marston <edam@waxworlds.org>
 
4
 * Copyright (C) 2009 Tim Marston <edam@waxworlds.org>
5
5
 *
6
6
 * This file is part of the Import Contacts program (hereafter referred
7
7
 * to as "this program"). For more information, see
21
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
22
 */
23
23
 
24
 
package org.waxworlds.edam.importcontacts;
 
24
package org.waxworlds.importcontacts;
25
25
 
26
26
import java.io.BufferedReader;
27
27
import java.io.File;
28
 
import java.io.FileInputStream;
29
28
import java.io.FileNotFoundException;
30
29
import java.io.FileReader;
31
30
import java.io.FilenameFilter;
32
31
import java.io.IOException;
33
32
import java.io.UnsupportedEncodingException;
34
 
import java.nio.ByteBuffer;
35
 
import java.util.ArrayList;
36
33
import java.util.Arrays;
37
 
import java.util.HashMap;
38
34
import java.util.HashSet;
39
 
import java.util.Iterator;
40
35
import java.util.List;
41
 
import java.util.NoSuchElementException;
42
36
import java.util.Set;
43
37
import java.util.Vector;
44
38
import java.util.regex.Matcher;
45
39
import java.util.regex.Pattern;
46
40
 
47
 
import org.waxworlds.edam.importcontacts.Importer.ContactData.ExtraDetail;
 
41
import org.waxworlds.importcontacts.Importer.AbortImportException;
48
42
 
49
43
import android.content.SharedPreferences;
50
44
import android.provider.Contacts;
73
67
                try
74
68
                {
75
69
                        // open directory
76
 
                        String path = "/sdcard" + prefs.getString( "location", "/" );
77
 
                        File file = new File( path );
78
 
                        if( !file.exists() )
 
70
                        String location = prefs.getString( "location", "" );
 
71
                        File dir = new File( location );
 
72
                        if( !dir.exists() || !dir.isDirectory() )
79
73
                                showError( R.string.error_locationnotfound );
80
74
 
81
 
                        // directory, or file?
82
 
                        if( file.isDirectory() )
83
 
                        {
84
 
                                // get files
85
 
                                class VCardFilter implements FilenameFilter {
86
 
                                        public boolean accept( File dir, String name ) {
87
 
                                                return name.toLowerCase().endsWith( ".vcf" );
88
 
                                        }
89
 
                                }
90
 
                                files = file.listFiles( new VCardFilter() );
91
 
                        }
92
 
                        else
93
 
                        {
94
 
                                // use just this file
95
 
                                files = new File[ 1 ];
96
 
                                files[ 0 ] = file;
97
 
                        }
 
75
                        // get files
 
76
                        class VCardFilter implements FilenameFilter {
 
77
                            public boolean accept( File dir, String name ) {
 
78
                                return name.toLowerCase().endsWith( ".vcf" );
 
79
                            }
 
80
                        }
 
81
                        files = dir.listFiles( new VCardFilter() );
98
82
                }
99
83
                catch( SecurityException e ) {
100
84
                        showError( R.string.error_locationpermissions );
126
110
                {
127
111
                        // open file
128
112
                        BufferedReader reader = new BufferedReader(
129
 
                                new FileReader( file ) );
 
113
                                        new FileReader( file ) );
130
114
 
131
115
                        // read
132
116
                        String line;
135
119
                        {
136
120
                                if( !inVCard ) {
137
121
                                        // look for vcard beginning
138
 
                                        if( line.matches( "^BEGIN:VCARD" ) ) {
 
122
                                        if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
139
123
                                                inVCard = true;
140
124
                                                _vCardCount++;
141
125
                                        }
142
126
                                }
143
 
                                else if( line.matches( "^END:VCARD" ) )
 
127
                                else if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
144
128
                                        inVCard = false;
145
129
                        }
146
130
 
147
131
                }
148
132
                catch( FileNotFoundException e ) {
149
 
                        showError( getText( R.string.error_filenotfound ) +
150
 
                                file.getName() );
 
133
                        showError( getText( R.string.error_filenotfound ) + file.getName() );
151
134
                }
152
135
                catch( IOException e ) {
153
136
                        showError( getText( R.string.error_ioerror ) + file.getName() );
156
139
 
157
140
        private void importVCardFile( File file ) throws AbortImportException
158
141
        {
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
 
 
167
142
                try
168
143
                {
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() );
 
144
                        // open file
 
145
                        BufferedReader reader = new BufferedReader(
 
146
                                        new FileReader( file ) );
 
147
 
 
148
                        // read
 
149
                        StringBuffer content = new StringBuffer();
 
150
                        String line;
 
151
                        while( ( line = reader.readLine() ) != null )
 
152
                                content.append( line ).append( "\n" );
 
153
 
 
154
                        importVCardFileContent( content.toString(), file.getName() );
176
155
                }
177
156
                catch( FileNotFoundException e ) {
178
 
                        showError( getText( R.string.error_filenotfound ) +
179
 
                                file.getName() );
 
157
                        showError( getText( R.string.error_filenotfound ) + file.getName() );
180
158
                }
181
159
                catch( IOException e ) {
182
160
                        showError( getText( R.string.error_ioerror ) + file.getName() );
183
161
                }
184
162
        }
185
163
 
186
 
        private void importVCardFileContent( byte[] content, String fileName )
187
 
                throws AbortImportException
 
164
        private void importVCardFileContent( String content, String fileName )
 
165
                        throws AbortImportException
188
166
        {
189
 
                // go through lines
 
167
                // unfold RFC2425 section 5.8.1 folded lines, except that we must also
 
168
                // handle embedded Quoted-Printable encodings that have a trailing '='.
 
169
                // So we remove these first before doing RFC2425 unfolding.
 
170
                content = content.replaceAll( "=\n[ \\t]", "" )
 
171
                                .replaceAll( "\n[ \\t]", "" );
 
172
 
 
173
                // get lines and parse them
 
174
                String[] lines = content.split( "\n" );
190
175
                VCard vCard = null;
191
 
                ContentLineIterator cli = new ContentLineIterator( content );
192
 
                while( cli.hasNext() )
 
176
                for( int i = 0; i < lines.length; i++ )
193
177
                {
194
 
                        ByteBuffer buffer = cli.next();
195
 
 
196
 
                        // get a US-ASCII version of the line for processing
197
 
                        String line;
198
 
                        try {
199
 
                                line = new String( buffer.array(), buffer.position(),
200
 
                                        buffer.limit() - buffer.position(), "US-ASCII" );
201
 
                        }
202
 
                        catch( UnsupportedEncodingException e ) {
203
 
                                // we know US-ASCII is supported, so appease the compiler...
204
 
                                line = "";
205
 
                        }
 
178
                        String line = lines[ i ];
206
179
 
207
180
                        if( vCard == null ) {
208
181
                                // look for vcard beginning
209
 
                                if( line.matches( "^BEGIN:VCARD" ) ) {
 
182
                                if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
210
183
                                        setProgress( ++_progress );
211
184
                                        vCard = new VCard();
212
185
                                }
213
186
                        }
214
187
                        else {
215
188
                                // look for vcard content or ending
216
 
                                if( line.matches( "^END:VCARD" ) )
 
189
                                if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
217
190
                                {
218
191
                                        // store vcard and do away with it
219
192
                                        try {
223
196
                                        catch( VCard.ParseException e ) {
224
197
                                                skipContact();
225
198
                                                if( !showContinue(
226
 
                                                        getText( R.string.error_vcf_parse ).toString()
227
 
                                                        + fileName + "\n" + e.getMessage() ) )
228
 
                                                {
 
199
                                                                getText( R.string.error_vcf_parse ).toString()
 
200
                                                                + fileName + "\n" + e.getMessage() ) )
229
201
                                                        finish( ACTION_ABORT );
230
 
                                                }
231
202
                                        }
232
203
                                        catch( VCard.SkipContactException e ) {
233
204
                                                skipContact();
239
210
                                {
240
211
                                        // try giving the line to the vcard
241
212
                                        try {
242
 
                                                vCard.parseLine( buffer, line,
243
 
                                                        cli.doesNextLineLookFolded() );
 
213
                                                vCard.parseLine( line );
244
214
                                        }
245
215
                                        catch( VCard.ParseException e ) {
246
216
                                                skipContact();
247
217
                                                if( !showContinue(
248
 
                                                        getText( R.string.error_vcf_parse ).toString()
249
 
                                                        + fileName + "\n" + e.getMessage() ) )
250
 
                                                {
 
218
                                                                getText( R.string.error_vcf_parse ).toString()
 
219
                                                                + fileName + "\n" + e.getMessage() ) )
251
220
                                                        finish( ACTION_ABORT );
252
 
                                                }
253
221
 
254
222
                                                // although we're continuing, we still need to abort
255
223
                                                // this vCard. Further lines will be ignored until we
267
235
                }
268
236
        }
269
237
 
270
 
        class ContentLineIterator implements Iterator< ByteBuffer >
271
 
        {
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 );
309
 
                        }
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
 
                }
331
 
        }
332
 
 
333
238
        private class VCard extends ContactData
334
239
        {
335
240
                private final static int NAMELEVEL_NONE = 0;
336
 
                private final static int NAMELEVEL_FN = 1;
337
 
                private final static int NAMELEVEL_N = 2;
338
 
 
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
 
241
                private final static int NAMELEVEL_ORG = 1;
 
242
                private final static int NAMELEVEL_FN = 2;
 
243
                private final static int NAMELEVEL_N = 3;
343
244
 
344
245
                private String _version = null;
345
 
                private Vector< ByteBuffer > _buffers = null;
346
 
                private int _name_level = NAMELEVEL_NONE;
347
 
                private int _parser_multiline_state = MULTILINE_NONE;
348
 
                private String _parser_current_name_and_params = null;
349
 
                private String _parser_buffered_value_so_far = "";
350
 
                private String _cached_organisation = null;
351
 
                private String _cached_title = null;
352
 
 
353
 
                protected class UnencodeResult
354
 
                {
355
 
                        private boolean _another_line_required;
356
 
                        private ByteBuffer _buffer;
357
 
 
358
 
                        public UnencodeResult( boolean another_line_required,
359
 
                                ByteBuffer buffer )
360
 
                        {
361
 
                                _another_line_required = another_line_required;
362
 
                                _buffer = buffer;
363
 
                        }
364
 
 
365
 
                        public boolean isAnotherLineRequired()
366
 
                        {
367
 
                                return _another_line_required;
368
 
                        }
369
 
 
370
 
                        public ByteBuffer getBuffer()
371
 
                        {
372
 
                                return _buffer;
373
 
                        }
374
 
                }
375
 
 
376
 
                @SuppressWarnings("serial")
 
246
                private Vector< String > _lines = null;
 
247
                private int _nameLevel = NAMELEVEL_NONE;
 
248
 
377
249
                protected class ParseException extends Exception
378
250
                {
379
 
                        @SuppressWarnings("unused")
380
251
                        public ParseException( String error )
381
252
                        {
382
253
                                super( error );
388
259
                        }
389
260
                }
390
261
 
391
 
                @SuppressWarnings("serial")
392
262
                protected class SkipContactException extends Exception { }
393
263
 
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?
 
264
                public void parseLine( String line )
 
265
                                throws ParseException, SkipContactException,
 
266
                                AbortImportException
 
267
                {
 
268
                        // get property halves
 
269
                        String[] props = line.split( ":" );
 
270
                        for( int i = 0; i < props.length; i++ )
 
271
                                props[ i ] = props[ i ].trim();
 
272
                        if( props.length < 2 ||
 
273
                                        props[ 0 ].length() < 1 || props[ 1 ].length() < 1 )
 
274
                                throw new ParseException( R.string.error_vcf_malformed );
 
275
 
438
276
                        if( _version == null )
439
277
                        {
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" ) )
 
278
                                if( props[ 0 ].equals( "VERSION" ) )
447
279
                                {
448
 
                                        // yes, get it!
449
 
                                        String value = extractValueFromLine( buffer, line );
450
 
                                        if( !value.equals( "2.1" ) && !value.equals( "3.0" ) )
 
280
                                        // get version
 
281
                                        if( !props[ 1 ].equals( "2.1" ) &&
 
282
                                                        !props[ 1 ].equals( "3.0" ) )
451
283
                                                throw new ParseException( R.string.error_vcf_version );
452
 
                                        _version = value;
 
284
                                        _version = props[ 1 ];
453
285
 
454
 
                                        // parse any buffers we've been accumulating while we waited
455
 
                                        // for a version
456
 
                                        if( _buffers != null )
457
 
                                                for( int i = 0; i < _buffers.size(); i++ )
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() ) == ' ' );
463
 
                                        _buffers = null;
 
286
                                        // parse any other lines we've accumulated so far
 
287
                                        if( _lines != null )
 
288
                                                for( int i = 0; i < _lines.size(); i++ )
 
289
                                                        parseLine( _lines.get( i ) );
 
290
                                        _lines = null;
464
291
                                }
465
292
                                else
466
293
                                {
467
 
                                        // no, so stash this line till we get a version
468
 
                                        if( _buffers == null )
469
 
                                                _buffers = new Vector< ByteBuffer >();
470
 
                                        _buffers.add( buffer );
 
294
                                        // stash this line till we have a version
 
295
                                        if( _lines == null )
 
296
                                                _lines = new Vector< String >();
 
297
                                        _lines.add( line );
471
298
                                }
472
299
                        }
473
300
                        else
474
301
                        {
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;
479
 
 
480
 
                                if( _parser_multiline_state != MULTILINE_NONE )
481
 
                                {
482
 
                                        // if we're currently in a multi-line value, use the stored
483
 
                                        // property name and parameters
484
 
                                        name_and_params = _parser_current_name_and_params;
485
 
 
486
 
                                        // skip some initial line characters, depending on the type
487
 
                                        // of multi-line we're handling
488
 
                                        pos = buffer.position();
489
 
                                        switch( _parser_multiline_state )
490
 
                                        {
491
 
                                        case MULTILINE_FOLDED:
492
 
                                                pos++;
493
 
                                                break;
494
 
                                        case MULTILINE_ENCODED:
495
 
                                                while( pos < buffer.limit() && (
496
 
                                                        buffer.get( pos ) == ' ' ||
497
 
                                                        buffer.get( pos ) == '\t' ) )
498
 
                                                {
499
 
                                                        pos++;
500
 
                                                }
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;
509
 
                                }
510
 
                                else
511
 
                                {
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
 
 
521
 
                                        // calculate how many chars to skip from beginning of line
522
 
                                        // so we skip the property "name:" part
523
 
                                        pos = buffer.position() + name_and_params.length() + 1;
524
 
 
525
 
                                        // reset the saved multi-line state
526
 
                                        _parser_current_name_and_params = name_and_params;
527
 
                                        _parser_buffered_value_so_far = "";
528
 
                                }
529
 
 
530
 
                                // get value from buffer, as raw bytes
531
 
                                ByteBuffer value;
532
 
                                value = ByteBuffer.wrap( buffer.array(), pos,
533
 
                                        buffer.limit() - pos );
534
 
 
535
302
                                // get parameter parts
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();
539
 
 
540
 
                                // parse encoding parameter
541
 
                                String encoding = checkParam( name_param_parts, "ENCODING" );
542
 
                                if( encoding != null ) encoding = encoding.toUpperCase();
543
 
                                if( encoding != null && !encoding.equals( "8BIT" ) &&
544
 
                                        !encoding.equals( "QUOTED-PRINTABLE" ) )
545
 
                                        //&& !encoding.equals( "BASE64" ) )
546
 
                                {
547
 
                                        throw new ParseException( R.string.error_vcf_encoding );
548
 
                                }
549
 
 
550
 
                                // parse charset parameter
551
 
                                String charset = checkParam( name_param_parts, "CHARSET" );
552
 
                                if( charset != null ) charset = charset.toUpperCase();
553
 
                                if( charset != null && !charset.equals( "US-ASCII" ) &&
554
 
                                        !charset.equals( "ASCII" ) &&
555
 
                                        !charset.equals( "UTF-8" ) )
556
 
                                {
557
 
                                        throw new ParseException( R.string.error_vcf_charset );
558
 
                                }
559
 
 
560
 
                                // do unencoding (or default to a fake unencoding result with
561
 
                                // the raw string)
562
 
                                UnencodeResult unencoding_result = null;
563
 
                                if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
564
 
                                        unencoding_result = unencodeQuotedPrintable( value );
565
 
//                              else if( encoding != null && encoding.equals( "BASE64" ) )
566
 
//                                      unencoding_result = unencodeBase64( props[ 1 ], charset );
567
 
                                if( unencoding_result != null ) {
568
 
                                        value = unencoding_result.getBuffer();
569
 
                                        if( unencoding_result.isAnotherLineRequired() )
570
 
                                                _parser_multiline_state = MULTILINE_ENCODED;
571
 
                                }
572
 
 
573
 
                                // convert 8-bit ASCII charset to US-ASCII
574
 
                                if( charset == null || charset.equals( "ASCII" ) ) {
575
 
                                        value = transcodeAsciiToUtf8( value );
576
 
                                        charset = "UTF-8";
577
 
                                }
578
 
 
579
 
                                // process charset
580
 
                                String string_value;
581
 
                                try {
582
 
                                        string_value = new String( value.array(), value.position(),
583
 
                                                value.limit() - value.position(), charset );
584
 
                                } catch( UnsupportedEncodingException e ) {
585
 
                                        throw new ParseException( R.string.error_vcf_charset );
586
 
                                }
587
 
 
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
 
 
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
603
 
                                if( _parser_multiline_state == MULTILINE_NONE &&
604
 
                                        _version.equals( "3.0" ) && next_line_looks_folded )
605
 
                                {
606
 
                                        _parser_multiline_state = MULTILINE_FOLDED;
607
 
                                }
608
 
 
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 ) {
612
 
                                        _parser_buffered_value_so_far += string_value;
613
 
                                        return;
614
 
                                }
615
 
                                String complete_value =
616
 
                                        ( _parser_buffered_value_so_far + string_value ).trim();
617
 
 
618
 
                                // ignore empty values
619
 
                                if( complete_value.length() < 1 ) return;
 
303
                                String[] params = props[ 0 ].split( ";" );
 
304
                                for( int i = 0; i < params.length; i++ )
 
305
                                        params[ i ] = params[ i ].trim();
620
306
 
621
307
                                // parse some properties
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 );
628
 
                                else if( name_param_parts[ 0 ].equals( "TITLE" ) )
629
 
                                        parseTITLE( name_param_parts, complete_value );
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 );
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 );
 
308
                                if( params[ 0 ].equals( "N" ) )
 
309
                                        parseN( params, props[ 1 ] );
 
310
                                else if( params[ 0 ].equals( "FN" ) )
 
311
                                        parseFN( params, props[ 1 ] );
 
312
                                else if( params[ 0 ].equals( "ORG" ) )
 
313
                                        parseORG( params, props[ 1 ] );
 
314
                                else if( params[ 0 ].equals( "TEL" ) )
 
315
                                        parseTEL( params, props[ 1 ] );
 
316
                                else if( params[ 0 ].equals( "EMAIL" ) )
 
317
                                        parseEMAIL( params, props[ 1 ] );
 
318
                        }
689
319
                }
690
320
 
691
321
                private void parseN( String[] params, String value )
 
322
                                throws ParseException, SkipContactException,
 
323
                                AbortImportException
692
324
                {
693
325
                        // already got a better name?
694
 
                        if( _name_level >= NAMELEVEL_N ) return;
 
326
                        if( _nameLevel >= NAMELEVEL_N ) return;
695
327
 
696
328
                        // get name parts
697
 
                        String[] name_parts = splitValueBySemicolon( value );
 
329
                        String[] nameparts = value.split( ";" );
 
330
                        for( int i = 0; i < nameparts.length; i++ )
 
331
                                nameparts[ i ] = nameparts[ i ].trim();
698
332
 
699
333
                        // build name
700
334
                        value = "";
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 ];
 
335
                        if( nameparts.length > 1 && nameparts[ 1 ].length() > 0 )
 
336
                                value += nameparts[ 1 ];
 
337
                        if( nameparts[ 0 ].length() > 0 )
 
338
                                value += ( value.length() == 0? "" : " " ) + nameparts[ 0 ];
705
339
 
706
340
                        // set name
707
 
                        setName( value );
708
 
                        _name_level = NAMELEVEL_N;
 
341
                        setName( undoCharsetAndEncoding( params, value ) );
 
342
                        _nameLevel = NAMELEVEL_N;
 
343
 
 
344
                        // check now to see if we need to import this contact (to avoid
 
345
                        // parsing the rest of the vCard unnecessarily)
 
346
                        if( !isImportRequired( getName() ) )
 
347
                                throw new SkipContactException();
709
348
                }
710
349
 
711
350
                private void parseFN( String[] params, String value )
 
351
                                throws ParseException, SkipContactException
712
352
                {
713
353
                        // already got a better name?
714
 
                        if( _name_level >= NAMELEVEL_FN ) return;
 
354
                        if( _nameLevel >= NAMELEVEL_FN ) return;
715
355
 
716
356
                        // set name
717
 
                        setName( value );
718
 
                        _name_level = NAMELEVEL_FN;
 
357
                        setName( undoCharsetAndEncoding( params, value ) );
 
358
                        _nameLevel = NAMELEVEL_FN;
719
359
                }
720
360
 
721
361
                private void parseORG( String[] params, String value )
 
362
                                throws ParseException, SkipContactException
722
363
                {
 
364
                        // already got a better name?
 
365
                        if( _nameLevel >= NAMELEVEL_ORG ) return;
 
366
 
723
367
                        // get org parts
724
 
                        String[] org_parts = splitValueBySemicolon( value );
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;
 
368
                        String[] orgparts = value.split( ";" );
 
369
                        for( int i = 0; i < orgparts.length; i++ )
 
370
                                orgparts[ i ] = orgparts[ i ].trim();
 
371
 
 
372
                        // build name
 
373
                        if( orgparts[ 0 ].length() == 0 && orgparts.length > 1 )
 
374
                                value = orgparts[ 1 ];
 
375
                        else
 
376
                                value = orgparts[ 0 ];
 
377
 
 
378
                        // set name
 
379
                        setName( undoCharsetAndEncoding( params, value ) );
 
380
                        _nameLevel = NAMELEVEL_ORG;
765
381
                }
766
382
 
767
383
                private void parseTEL( String[] params, String value )
 
384
                                throws ParseException
768
385
                {
769
386
                        if( value.length() == 0 ) return;
770
387
 
771
388
                        Set< String > types = extractTypes( params, Arrays.asList(
772
 
                                "PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
773
 
                                "PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
 
389
                                        "PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
 
390
                                        "PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
774
391
 
775
392
                        // here's the logic...
776
393
                        boolean preferred = types.contains( "PREF" );
777
 
                        int type;
 
394
                        if( types.contains( "VOICE" ) )
 
395
                                if( types.contains( "WORK" ) )
 
396
                                        addPhone( value, PhonesColumns.TYPE_WORK, preferred );
 
397
                                else
 
398
                                        addPhone( value, PhonesColumns.TYPE_HOME, preferred );
 
399
                        else if( types.contains( "CELL" ) || types.contains( "VIDEO" ) )
 
400
                                addPhone( value, PhonesColumns.TYPE_MOBILE, preferred );
778
401
                        if( types.contains( "FAX" ) )
779
402
                                if( types.contains( "HOME" ) )
780
 
                                        type = PhonesColumns.TYPE_FAX_HOME;
 
403
                                        addPhone( value, PhonesColumns.TYPE_FAX_HOME, preferred );
781
404
                                else
782
 
                                        type = PhonesColumns.TYPE_FAX_WORK;
783
 
                        else if( types.contains( "CELL" ) || types.contains( "VIDEO" ) )
784
 
                                type = PhonesColumns.TYPE_MOBILE;
785
 
                        else if( types.contains( "PAGER" ) )
786
 
                                type = PhonesColumns.TYPE_PAGER;
787
 
                        else if( types.contains( "WORK" ) )
788
 
                                type = PhonesColumns.TYPE_WORK;
789
 
                        else
790
 
                                type = PhonesColumns.TYPE_HOME;
791
 
 
792
 
                        // add phone number
793
 
                        addNumber( value, type, preferred );
 
405
                                        addPhone( value, PhonesColumns.TYPE_FAX_WORK, preferred );
 
406
                        if( types.contains( "PAGER" ) )
 
407
                                addPhone( value, PhonesColumns.TYPE_PAGER, preferred );
794
408
                }
795
409
 
796
410
                public void parseEMAIL( String[] params, String value )
798
412
                        if( value.length() == 0 ) return;
799
413
 
800
414
                        Set< String > types = extractTypes( params, Arrays.asList(
801
 
                                "PREF", "WORK", "HOME", "INTERNET" ) );
 
415
                                        "PREF", "WORK", "HOME", "INTERNET" ) );
802
416
 
803
 
                        // add email address
 
417
                        // here's the logic...
804
418
                        boolean preferred = types.contains( "PREF" );
805
 
                        int type;
806
 
                        if( types.contains( "WORK" ) )
807
 
                                type = Contacts.ContactMethods.TYPE_WORK;
808
 
                        else
809
 
                                type = Contacts.ContactMethods.TYPE_HOME;
810
 
 
811
 
                        addEmail( value, type, preferred );
812
 
                }
813
 
 
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
830
 
                        int type;
831
 
                        if( types.contains( "WORK" ) )
832
 
                                type = Contacts.ContactMethods.TYPE_WORK;
833
 
                        else
834
 
                                type = Contacts.ContactMethods.TYPE_HOME;
835
 
 
836
 
                        addAddress( value, type );
 
419
                        if( types.contains( "WORK" ) )
 
420
                                addEmail( value, Contacts.ContactMethods.TYPE_WORK, preferred );
 
421
                        else
 
422
                                addEmail( value, Contacts.ContactMethods.TYPE_HOME, preferred );
837
423
                }
838
424
 
839
425
                public void finaliseParsing()
840
 
                        throws ParseException, SkipContactException,
841
 
                        AbortImportException
 
426
                                throws ParseException, SkipContactException,
 
427
                                AbortImportException
842
428
                {
843
429
                        // missing version (and data is present)
844
 
                        if( _version == null && _buffers != null )
 
430
                        if( _version == null && _lines != null )
845
431
                                throw new ParseException( R.string.error_vcf_malformed );
846
432
 
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
 
                        }
 
433
                        //  missing name properties?
 
434
                        if( _nameLevel == NAMELEVEL_NONE )
 
435
                                throw new ParseException( R.string.error_vcf_noname );
 
436
 
 
437
                        // check if we should import this one? If we've already got an 'N'-
 
438
                        // type name, this will already have been done by parseN() so we
 
439
                        // mustn't do this here (or it could prompt twice!)
 
440
                        if( _nameLevel < NAMELEVEL_N && !isImportRequired( getName() ) )
 
441
                                throw new SkipContactException();
 
442
                }
 
443
 
 
444
                private String undoCharsetAndEncoding( String[] params, String value )
 
445
                                throws ParseException
 
446
                {
 
447
                        // check encoding/charset
 
448
                        String charset, encoding;
 
449
                        if( ( charset = checkParam( params, "CHARSET" ) ) != null &&
 
450
                                        !charset.equals( "UTF-8" ) && !charset.equals( "UTF-16" ) )
 
451
                                throw new ParseException( R.string.error_vcf_charset );
 
452
                        if( ( encoding = checkParam( params, "ENCODING" ) ) != null &&
 
453
                                        !encoding.equals( "QUOTED-PRINTABLE" ) )
 
454
                                throw new ParseException( R.string.error_vcf_encoding );
 
455
 
 
456
                        // do decoding?
 
457
                        if( encoding != null && encoding.equals( "QUOTED-PRINTABLE" ) )
 
458
                                return unencodeQuotedPrintable( value, charset );
 
459
 
 
460
                        // nothing to do!
 
461
                        return value;
855
462
                }
856
463
 
857
464
                private String checkParam( String[] params, String name )
858
465
                {
859
 
                        Pattern p = Pattern.compile(
860
 
                                "^" + name + "[ \\t]*=[ \\t]*(\"?)(.*)\\1$" );
 
466
                        Pattern p = Pattern.compile( "^" + name + "[ \\t]*=[ \\t]*(.*)$" );
861
467
                        for( int i = 0; i < params.length; i++ ) {
862
468
                                Matcher m = p.matcher( params[ i ] );
863
469
                                if( m.matches() )
864
 
                                        return m.group( 2 );
 
470
                                        return m.group( 1 );
865
471
                        }
866
472
                        return null;
867
473
                }
868
474
 
869
475
                private Set< String > extractTypes( String[] params,
870
 
                        List< String > valid_types )
 
476
                                List< String > validTypes )
871
477
                {
872
478
                        HashSet< String > types = new HashSet< String >();
873
479
 
874
480
                        // get 3.0-style TYPE= param
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 ] );
 
481
                        String typeParam;
 
482
                        if( ( typeParam = checkParam( params, "TYPE" ) ) != null ) {
 
483
                                String[] bits = typeParam.split( "," );
 
484
                                for( int i = 0; i < bits.length; i++ )
 
485
                                        if( validTypes.contains( bits[ i ] ) )
 
486
                                                types.add( bits[ i ] );
881
487
                        }
882
488
 
883
489
                        // get 2.1-style type param
884
490
                        if( _version.equals( "2.1" ) ) {
885
491
                                for( int i = 1; i < params.length; i++ )
886
 
                                        if( valid_types.contains( params[ i ] ) )
 
492
                                        if( validTypes.contains( params[ i ] ) )
887
493
                                                types.add( params[ i ] );
888
494
                        }
889
495
 
890
496
                        return types;
891
497
                }
892
498
 
893
 
                private UnencodeResult unencodeQuotedPrintable( ByteBuffer in )
 
499
                private String unencodeQuotedPrintable( String str, String charset )
894
500
                {
895
 
                        boolean another = false;
 
501
                        // default encoding scheme
 
502
                        if( charset == null ) charset = "UTF-8";
896
503
 
897
 
                        // unencode quoted-printable encoding, as per RFC1521 section 5.1
898
 
                        byte[] out = new byte[ in.limit() - in.position() ];
 
504
                        // unencode quoted-pritable encoding, as per RFC1521 section 5.1
 
505
                        byte[] bytes = new byte[ str.length() ];
899
506
                        int j = 0;
900
 
                        for( int i = in.position(); i < in.limit(); i++ )
901
 
                        {
902
 
                                // get next char and process...
903
 
                                byte ch = in.array()[ i ];
904
 
                                if( ch == '=' && i < in.limit() - 2 )
905
 
                                {
906
 
                                        // we found a =XX format byte, add it
907
 
                                        out[ j ] = (byte)(
908
 
                                                        Character.digit( in.array()[ i + 1 ], 16 ) * 16 +
909
 
                                                        Character.digit( in.array()[ i + 2 ], 16 ) );
 
507
                        for( int i = 0; i < str.length(); i++, j++ ) {
 
508
                                char ch = str.charAt( i );
 
509
                                if( ch == '=' && i < str.length() - 2 ) {
 
510
                                        bytes[ j ] = (byte)(
 
511
                                                        Character.digit( str.charAt( i + 1 ), 16 ) * 16 +
 
512
                                                        Character.digit( str.charAt( i + 2 ), 16 ) );
910
513
                                        i += 2;
911
514
                                }
912
 
                                else if( ch == '=' && i == in.limit() - 1 )
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
 
                                }
919
515
                                else
920
 
                                        // just a normal char...
921
 
                                        out[ j ] = (byte)ch;
922
 
                                j++;
923
 
                        }
924
 
 
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 );
 
516
                                        bytes[ j ] = (byte)ch;
 
517
                        }
 
518
                        try {
 
519
                                return new String( bytes, 0, j, charset );
 
520
                        } catch( UnsupportedEncodingException e ) { }
 
521
                        return null;
948
522
                }
949
523
        }
950
524
}