/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 2013 Tim Marston <tim@ed.am>
 
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
 
 * to as "this program").  For more information, see
8
 
 * http://ed.am/dev/android/import-contacts
 
7
 * to as "this program"). For more information, see
 
8
 * http://www.waxworlds.org/edam/software/android/import-contacts
9
9
 *
10
10
 * This program is free software: you can redistribute it and/or modify
11
11
 * it under the terms of the GNU General Public License as published by
21
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
22
 */
23
23
 
24
 
package am.ed.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.Locale;
42
 
import java.util.NoSuchElementException;
43
36
import java.util.Set;
44
37
import java.util.Vector;
45
38
import java.util.regex.Matcher;
46
39
import java.util.regex.Pattern;
47
40
 
 
41
import org.waxworlds.importcontacts.Importer.AbortImportException;
 
42
 
48
43
import android.content.SharedPreferences;
49
 
import android.os.Environment;
 
44
import android.provider.Contacts;
 
45
import android.provider.Contacts.PhonesColumns;
50
46
 
51
 
public class VcardImporter extends Importer
 
47
public class VCFImporter extends Importer
52
48
{
53
 
        private int _vcard_count = 0;
 
49
        private int _vCardCount = 0;
54
50
        private int _progress = 0;
55
51
 
56
 
        public VcardImporter( Doit doit )
 
52
        public VCFImporter( Doit doit )
57
53
        {
58
54
                super( doit );
59
55
        }
70
66
                File[] files = null;
71
67
                try
72
68
                {
73
 
                        // check SD card is mounted
74
 
                        String state = Environment.getExternalStorageState();
75
 
                        if( !Environment.MEDIA_MOUNTED.equals( state ) &&
76
 
                                !Environment.MEDIA_MOUNTED_READ_ONLY.equals( state ) )
77
 
                        {
78
 
                                showError( R.string.error_nosdcard );
79
 
                        }
80
 
 
81
69
                        // open directory
82
 
                        File file = new File( Environment.getExternalStorageDirectory(),
83
 
                                prefs.getString( "location", "/" ) );
84
 
                        if( !file.exists() )
 
70
                        String location = prefs.getString( "location", "" );
 
71
                        File dir = new File( location );
 
72
                        if( !dir.exists() || !dir.isDirectory() )
85
73
                                showError( R.string.error_locationnotfound );
86
74
 
87
 
                        // directory, or file?
88
 
                        if( file.isDirectory() )
89
 
                        {
90
 
                                // get files
91
 
                                class VCardFilter implements FilenameFilter {
92
 
                                        public boolean accept( File dir, String name ) {
93
 
                                                return name.toLowerCase( Locale.ENGLISH )
94
 
                                                        .endsWith( ".vcf" );
95
 
                                        }
96
 
                                }
97
 
                                files = file.listFiles( new VCardFilter() );
98
 
                        }
99
 
                        else
100
 
                        {
101
 
                                // use just this file
102
 
                                files = new File[ 1 ];
103
 
                                files[ 0 ] = file;
104
 
                        }
 
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() );
105
82
                }
106
83
                catch( SecurityException e ) {
107
84
                        showError( R.string.error_locationpermissions );
119
96
                        countVCardFile( files[ i ] );
120
97
                        setTmpProgress( i );
121
98
                }
122
 
                setProgressMax( _vcard_count ); // will also update tmp progress
 
99
                setProgressMax( _vCardCount );  // will also update tmp progress
123
100
 
124
101
                // import them
125
102
                setProgress( 0 );
126
103
                for( int i = 0; i < files.length; i++ )
127
104
                        importVCardFile( files[ i ] );
128
 
                setProgress( _vcard_count );
129
105
        }
130
106
 
131
107
        private void countVCardFile( File file ) throws AbortImportException
134
110
                {
135
111
                        // open file
136
112
                        BufferedReader reader = new BufferedReader(
137
 
                                new FileReader( file ) );
 
113
                                        new FileReader( file ) );
138
114
 
139
115
                        // read
140
116
                        String line;
141
 
                        boolean in_vcard = false;
 
117
                        boolean inVCard = false;
142
118
                        while( ( line = reader.readLine() ) != null )
143
119
                        {
144
 
                                if( !in_vcard )
145
 
                                {
 
120
                                if( !inVCard ) {
146
121
                                        // look for vcard beginning
147
 
                                        if( line.matches( "(?i)BEGIN[ \t]*:[ \t]*VCARD.*" ) ) {
148
 
                                                in_vcard = true;
149
 
                                                _vcard_count++;
150
 
                                        }
151
 
                                        // check for vMsg files
152
 
                                        else if( line.matches( "(?i)BEGIN[ \t]*:[ \t]*VMSG.*" ) ) {
153
 
                                                showError( getText( R.string.error_vcf_vmsgfile )
154
 
                                                        + file.getName() );
 
122
                                        if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
 
123
                                                inVCard = true;
 
124
                                                _vCardCount++;
155
125
                                        }
156
126
                                }
157
 
                                else if( line.matches( "(?i)END[ \t]*:[ \t]*VCARD.*" ) )
158
 
                                        in_vcard = false;
 
127
                                else if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
 
128
                                        inVCard = false;
159
129
                        }
160
 
                        reader.close();
161
130
 
162
131
                }
163
132
                catch( FileNotFoundException e ) {
164
 
                        showError( getText( R.string.error_filenotfound ) +
165
 
                                file.getName() );
 
133
                        showError( getText( R.string.error_filenotfound ) + file.getName() );
166
134
                }
167
135
                catch( IOException e ) {
168
136
                        showError( getText( R.string.error_ioerror ) + file.getName() );
171
139
 
172
140
        private void importVCardFile( File file ) throws AbortImportException
173
141
        {
174
 
                // check file is good
175
 
                if( !file.exists() )
176
 
                        showError( getText( R.string.error_filenotfound ) +
177
 
                                file.getName() );
178
 
                if( file.length() == 0 )
179
 
                        showError( getText( R.string.error_fileisempty ) +
180
 
                                file.getName() );
181
 
 
182
142
                try
183
143
                {
184
 
                        // open/read file
185
 
                        FileInputStream istream = new FileInputStream( file );
186
 
                        byte[] content = new byte[ (int)file.length() ];
187
 
                        istream.read( content );
188
 
                        istream.close();
189
 
 
190
 
                        // import
191
 
                        importVCardFileContent( content, file.getName() );
192
 
                }
193
 
                catch( OutOfMemoryError e ) {
194
 
                        showError( R.string.error_outofmemory );
 
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() );
195
155
                }
196
156
                catch( FileNotFoundException e ) {
197
 
                        showError( getText( R.string.error_filenotfound ) +
198
 
                                file.getName() );
 
157
                        showError( getText( R.string.error_filenotfound ) + file.getName() );
199
158
                }
200
159
                catch( IOException e ) {
201
160
                        showError( getText( R.string.error_ioerror ) + file.getName() );
202
161
                }
203
162
        }
204
163
 
205
 
        private void importVCardFileContent( byte[] content, String fileName )
206
 
                throws AbortImportException
 
164
        private void importVCardFileContent( String content, String fileName )
 
165
                        throws AbortImportException
207
166
        {
208
 
                // go through lines
209
 
                Vcard vcard = null;
210
 
                int vcard_start_line = 0;
211
 
                ContentLineIterator cli = new ContentLineIterator( content );
212
 
                while( cli.hasNext() )
 
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" );
 
175
                VCard vCard = null;
 
176
                for( int i = 0; i < lines.length; i++ )
213
177
                {
214
 
                        ContentLine content_line = cli.next();
215
 
 
216
 
                        // get a US-ASCII version of the string, for processing
217
 
                        String line = content_line.getUsAsciiLine();
218
 
 
219
 
                        if( vcard == null ) {
 
178
                        String line = lines[ i ];
 
179
 
 
180
                        if( vCard == null ) {
220
181
                                // look for vcard beginning
221
 
                                if( line.matches( "(?i)BEGIN[ \t]*:[ \t]*VCARD.*" ) ) {
222
 
                                        setProgress( _progress++ );
223
 
                                        vcard = new Vcard();
224
 
                                        vcard_start_line = cli.getLineNumber();
 
182
                                if( line.matches( "^BEGIN[ \\t]*:[ \\t]*VCARD" ) ) {
 
183
                                        setProgress( ++_progress );
 
184
                                        vCard = new VCard();
225
185
                                }
226
186
                        }
227
187
                        else {
228
188
                                // look for vcard content or ending
229
 
                                if( line.matches( "(?i)END[ \t]*:[ \t]*VCARD.*" ) )
 
189
                                if( line.matches( "^END[ \\t]*:[ \\t]*VCARD" ) )
230
190
                                {
231
 
                                        // finalise the vcard/contact
 
191
                                        // store vcard and do away with it
232
192
                                        try {
233
 
                                                vcard.finaliseVcard();
234
 
 
235
 
                                                // pass the finalised contact to the importer
236
 
                                                importContact( vcard );
237
 
                                        }
238
 
                                        catch( Vcard.ParseException e ) {
239
 
                                                showContinueOrAbort(
240
 
                                                        getText( R.string.error_vcf_parse ).toString()
241
 
                                                        + fileName +
242
 
                                                        getText( R.string.error_vcf_parse_line ).toString()
243
 
                                                        + cli.getLineNumber() + ":\n" + e.getMessage() );
244
 
                                                skipContact();
245
 
                                        }
246
 
                                        catch( ContactData.ContactNotIdentifiableException e ) {
247
 
                                                showContinueOrAbort(
248
 
                                                        getText( R.string.error_vcf_parse ).toString()
249
 
                                                        + fileName +
250
 
                                                        getText( R.string.error_vcf_parse_line ).toString()
251
 
                                                        + vcard_start_line + ":\n" + getText(
252
 
                                                                R.string.error_vcf_notenoughinfo ).toString() );
253
 
                                                skipContact();
254
 
                                        }
255
 
                                        catch( Vcard.SkipImportException e ) {
256
 
                                                skipContact();
257
 
                                        }
258
 
 
259
 
                                        // discard this vcard
260
 
                                        vcard = null;
 
193
                                                vCard.finaliseParsing();
 
194
                                                importContact( vCard );
 
195
                                        }
 
196
                                        catch( VCard.ParseException e ) {
 
197
                                                skipContact();
 
198
                                                if( !showContinue(
 
199
                                                                getText( R.string.error_vcf_parse ).toString()
 
200
                                                                + fileName + "\n" + e.getMessage() ) )
 
201
                                                        finish( ACTION_ABORT );
 
202
                                        }
 
203
                                        catch( VCard.SkipContactException e ) {
 
204
                                                skipContact();
 
205
                                                // do nothing
 
206
                                        }
 
207
                                        vCard = null;
261
208
                                }
262
209
                                else
263
210
                                {
264
211
                                        // try giving the line to the vcard
265
212
                                        try {
266
 
                                                vcard.parseLine( content_line );
 
213
                                                vCard.parseLine( line );
267
214
                                        }
268
 
                                        catch( Vcard.ParseException e ) {
 
215
                                        catch( VCard.ParseException e ) {
269
216
                                                skipContact();
270
 
                                                showContinueOrAbort(
271
 
                                                        getText( R.string.error_vcf_parse ).toString()
272
 
                                                        + fileName +
273
 
                                                        getText( R.string.error_vcf_parse_line ).toString()
274
 
                                                        + cli.getLineNumber() + "\n" + e.getMessage() );
 
217
                                                if( !showContinue(
 
218
                                                                getText( R.string.error_vcf_parse ).toString()
 
219
                                                                + fileName + "\n" + e.getMessage() ) )
 
220
                                                        finish( ACTION_ABORT );
275
221
 
276
 
                                                // Although we're continuing, we still need to abort
277
 
                                                // this vCard.  Further lines will be ignored until we
 
222
                                                // although we're continuing, we still need to abort
 
223
                                                // this vCard. Further lines will be ignored until we
278
224
                                                // get to another BEGIN:VCARD line.
279
 
                                                vcard = null;
 
225
                                                vCard = null;
280
226
                                        }
281
 
                                        catch( Vcard.SkipImportException e ) {
 
227
                                        catch( VCard.SkipContactException e ) {
282
228
                                                skipContact();
283
 
                                                // Abort this vCard.  Further lines will be ignored until
 
229
                                                // abort this vCard. Further lines will be ignored until
284
230
                                                // we get to another BEGIN:VCARD line.
285
 
                                                vcard = null;
 
231
                                                vCard = null;
286
232
                                        }
287
233
                                }
288
234
                        }
289
235
                }
290
236
        }
291
237
 
292
 
        class ContentLine
293
 
        {
294
 
                private ByteBuffer _buffer;
295
 
                private boolean _folded_next;
296
 
                private String _line;
297
 
 
298
 
                public ContentLine( ByteBuffer buffer, boolean folded_next )
299
 
                {
300
 
                        _buffer = buffer;
301
 
                        _folded_next = folded_next;
302
 
                        _line = null;
303
 
                }
304
 
 
305
 
                public ByteBuffer getBuffer()
306
 
                {
307
 
                        return _buffer;
308
 
                }
309
 
 
310
 
                public boolean doesNextLineLookFolded()
311
 
                {
312
 
                        return _folded_next;
313
 
                }
314
 
 
315
 
                public String getUsAsciiLine()
316
 
                {
317
 
                        // generated line and cache it
318
 
                        if( _line == null ) {
319
 
                                try {
320
 
                                        _line = new String( _buffer.array(), _buffer.position(),
321
 
                                                _buffer.limit() - _buffer.position(), "US-ASCII" );
322
 
                                }
323
 
                                catch( UnsupportedEncodingException e ) {
324
 
                                        // we know US-ASCII *is* supported, so appease the
325
 
                                        // compiler...
326
 
                                }
327
 
                        }
328
 
 
329
 
                        // return cached line
330
 
                        return _line;
331
 
                }
332
 
        }
333
 
 
334
 
        class ContentLineIterator implements Iterator< ContentLine >
335
 
        {
336
 
                protected byte[] _content = null;
337
 
                protected int _pos = 0;
338
 
                protected int _line = 0;
339
 
 
340
 
                public ContentLineIterator( byte[] content )
341
 
                {
342
 
                        _content = content;
343
 
                }
344
 
 
345
 
                @Override
346
 
                public boolean hasNext()
347
 
                {
348
 
                        return _pos < _content.length;
349
 
                }
350
 
 
351
 
                @Override
352
 
                public ContentLine next()
353
 
                {
354
 
                        int initial_pos = _pos;
355
 
 
356
 
                        // find newline
357
 
                        for( ; _pos < _content.length; _pos++ )
358
 
                                if( _content[ _pos ] == '\n' )
359
 
                                {
360
 
                                        // adjust for a \r preceding the \n
361
 
                                        int to = ( _pos > 0 && _content[ _pos - 1 ] == '\r' &&
362
 
                                                _pos > initial_pos )? _pos - 1 : _pos;
363
 
                                        _pos++;
364
 
                                        _line++;
365
 
                                        return new ContentLine(
366
 
                                                ByteBuffer.wrap( _content, initial_pos,
367
 
                                                        to - initial_pos ),
368
 
                                                doesNextLineLookFolded() );
369
 
                                }
370
 
 
371
 
                        // we didn't find one, but were there bytes left?
372
 
                        if( _pos != initial_pos ) {
373
 
                                int to = _pos;
374
 
                                _pos++;
375
 
                                _line++;
376
 
                                return new ContentLine(
377
 
                                        ByteBuffer.wrap( _content, initial_pos,
378
 
                                                to - initial_pos ),
379
 
                                        doesNextLineLookFolded() );
380
 
                        }
381
 
 
382
 
                        // no bytes left
383
 
                        throw new NoSuchElementException();
384
 
                }
385
 
 
386
 
                @Override
387
 
                public void remove()
388
 
                {
389
 
                        throw new UnsupportedOperationException();
390
 
                }
391
 
 
392
 
                /**
393
 
                 * Does the next line, if there is one, look like it should be folded
394
 
                 * onto the end of this one?
395
 
                 * @return
396
 
                 */
397
 
                private boolean doesNextLineLookFolded()
398
 
                {
399
 
                        return _pos > 0 && _pos < _content.length &&
400
 
                                _content[ _pos - 1 ] == '\n' &&
401
 
                                ( _content[ _pos ] == ' ' || _content[ _pos ] == '\t' );
402
 
                }
403
 
 
404
 
                public int getLineNumber()
405
 
                {
406
 
                        return _line;
407
 
                }
408
 
        }
409
 
 
410
 
        private class Vcard extends ContactData
 
238
        private class VCard extends ContactData
411
239
        {
412
240
                private final static int NAMELEVEL_NONE = 0;
413
 
                private final static int NAMELEVEL_N = 1;
 
241
                private final static int NAMELEVEL_ORG = 1;
414
242
                private final static int NAMELEVEL_FN = 2;
415
 
 
416
 
                private final static int MULTILINE_NONE = 0;
417
 
                private final static int MULTILINE_ENCODED = 1; // v2.1 quoted-printable
418
 
                private final static int MULTILINE_ESCAPED = 2; // v2.1 \\CRLF
419
 
                private final static int MULTILINE_FOLDED = 3;  // MIME-DIR folding
 
243
                private final static int NAMELEVEL_N = 3;
420
244
 
421
245
                private String _version = null;
422
 
                private Vector< ContentLine > _content_lines = null;
423
 
                private int _name_level = NAMELEVEL_NONE;
424
 
                private int _parser_multiline_state = MULTILINE_NONE;
425
 
                private String _parser_current_name_and_params = null;
426
 
                private String _parser_buffered_value_so_far = "";
427
 
                private String _cached_organisation = null;
428
 
                private String _cached_title = null;
429
 
 
430
 
                protected class UnencodeResult
431
 
                {
432
 
                        private boolean _another_line_required;
433
 
                        private ByteBuffer _buffer;
434
 
 
435
 
                        public UnencodeResult( boolean another_line_required,
436
 
                                ByteBuffer buffer )
437
 
                        {
438
 
                                _another_line_required = another_line_required;
439
 
                                _buffer = buffer;
440
 
                        }
441
 
 
442
 
                        public boolean isAnotherLineRequired()
443
 
                        {
444
 
                                return _another_line_required;
445
 
                        }
446
 
 
447
 
                        public ByteBuffer getBuffer()
448
 
                        {
449
 
                                return _buffer;
450
 
                        }
451
 
                }
452
 
 
453
 
                @SuppressWarnings("serial")
 
246
                private Vector< String > _lines = null;
 
247
                private int _nameLevel = NAMELEVEL_NONE;
 
248
 
454
249
                protected class ParseException extends Exception
455
250
                {
456
 
                        @SuppressWarnings("unused")
457
251
                        public ParseException( String error )
458
252
                        {
459
253
                                super( error );
461
255
 
462
256
                        public ParseException( int res )
463
257
                        {
464
 
                                super( VcardImporter.this.getText( res ).toString() );
 
258
                                super( VCFImporter.this.getText( res ).toString() );
465
259
                        }
466
260
                }
467
261
 
468
 
                @SuppressWarnings("serial")
469
 
                protected class SkipImportException extends Exception { }
470
 
 
471
 
                private String extractCollonPartFromLine( ContentLine content_line,
472
 
                        boolean former )
473
 
                {
474
 
                        // split line into name and value parts and check to make sure we
475
 
                        // only got 2 parts and that the first part is not zero in length
476
 
                        String[] parts = content_line.getUsAsciiLine().split( ":", 2 );
477
 
                        if( parts.length == 2 && parts[ 0 ].length() > 0 )
478
 
                                return parts[ former? 0 : 1 ].trim();
479
 
 
480
 
                        return null;
481
 
                }
482
 
 
483
 
                private String extractNameAndParamsFromLine( ContentLine content_line )
484
 
                {
485
 
                        return extractCollonPartFromLine( content_line, true );
486
 
                }
487
 
 
488
 
                private String extractValueFromLine( ContentLine content_line )
489
 
                {
490
 
                        return extractCollonPartFromLine( content_line, false );
491
 
                }
492
 
 
493
 
                public void parseLine( ContentLine content_line )
494
 
                        throws ParseException, SkipImportException,
495
 
                        AbortImportException
496
 
                {
497
 
                        // do we have a version yet?
 
262
                protected class SkipContactException extends Exception { }
 
263
 
 
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
 
498
276
                        if( _version == null )
499
277
                        {
500
 
                                // tentatively get name and params from line
501
 
                                String name_and_params =
502
 
                                        extractNameAndParamsFromLine( content_line );
503
 
 
504
 
                                // is it a version line?
505
 
                                if( name_and_params != null &&
506
 
                                        name_and_params.equalsIgnoreCase( "VERSION" ) )
 
278
                                if( props[ 0 ].equals( "VERSION" ) )
507
279
                                {
508
 
                                        // yes, get it!
509
 
                                        String value = extractValueFromLine( content_line );
510
 
                                        if( value == null || (
511
 
                                                !value.equals( "2.1" ) && !value.equals( "3.0" ) ) )
512
 
                                        {
 
280
                                        // get version
 
281
                                        if( !props[ 1 ].equals( "2.1" ) &&
 
282
                                                        !props[ 1 ].equals( "3.0" ) )
513
283
                                                throw new ParseException( R.string.error_vcf_version );
514
 
                                        }
515
 
                                        _version = value;
 
284
                                        _version = props[ 1 ];
516
285
 
517
 
                                        // parse any buffers we've been accumulating while we waited
518
 
                                        // for a version
519
 
                                        if( _content_lines != null )
520
 
                                                for( int i = 0; i < _content_lines.size(); i++ )
521
 
                                                        parseLine( _content_lines.get( i ) );
522
 
                                        _content_lines = 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;
523
291
                                }
524
292
                                else
525
293
                                {
526
 
                                        // no, so stash this line till we get a version
527
 
                                        if( _content_lines == null )
528
 
                                                _content_lines = new Vector< ContentLine >();
529
 
                                        _content_lines.add( content_line );
 
294
                                        // stash this line till we have a version
 
295
                                        if( _lines == null )
 
296
                                                _lines = new Vector< String >();
 
297
                                        _lines.add( line );
530
298
                                }
531
299
                        }
532
300
                        else
533
301
                        {
534
 
                                // name and params and the position in the buffer where the
535
 
                                // "value" part of the line starts
536
 
                                String name_and_params;
537
 
                                int pos;
538
 
 
539
 
                                if( _parser_multiline_state != MULTILINE_NONE )
540
 
                                {
541
 
                                        // if we're currently in a multi-line value, use the stored
542
 
                                        // property name and parameters
543
 
                                        name_and_params = _parser_current_name_and_params;
544
 
 
545
 
                                        // skip some initial line characters, depending on the type
546
 
                                        // of multi-line we're handling
547
 
                                        pos = content_line.getBuffer().position();
548
 
                                        switch( _parser_multiline_state )
549
 
                                        {
550
 
                                        case MULTILINE_FOLDED:
551
 
                                                pos++;
552
 
                                                break;
553
 
                                        case MULTILINE_ENCODED:
554
 
                                                while( pos < content_line.getBuffer().limit() && (
555
 
                                                        content_line.getBuffer().get( pos ) == ' ' ||
556
 
                                                        content_line.getBuffer().get( pos ) == '\t' ) )
557
 
                                                {
558
 
                                                        pos++;
559
 
                                                }
560
 
                                                break;
561
 
                                        default:
562
 
                                                // do nothing
563
 
                                        }
564
 
 
565
 
                                        // take us out of multi-line so that we can re-detect that
566
 
                                        // this line is a multi-line or not
567
 
                                        _parser_multiline_state = MULTILINE_NONE;
568
 
                                }
569
 
                                else
570
 
                                {
571
 
                                        // skip empty lines
572
 
                                        if( content_line.getUsAsciiLine().trim().length() == 0 )
573
 
                                                return;
574
 
 
575
 
                                        // get name and params from line, and since we're not
576
 
                                        // parsing a subsequent line in a multi-line, this should
577
 
                                        // not fail, or it's an error
578
 
                                        name_and_params =
579
 
                                                extractNameAndParamsFromLine( content_line );
580
 
                                        if( name_and_params == null )
581
 
                                                throw new ParseException(
582
 
                                                        R.string.error_vcf_malformed );
583
 
 
584
 
                                        // calculate how many chars to skip from beginning of line
585
 
                                        // so we skip the property "name:" part
586
 
                                        pos = content_line.getBuffer().position() +
587
 
                                                name_and_params.length() + 1;
588
 
 
589
 
                                        // reset the saved multi-line state
590
 
                                        _parser_current_name_and_params = name_and_params;
591
 
                                        _parser_buffered_value_so_far = "";
592
 
                                }
593
 
 
594
 
                                // get value from buffer, as raw bytes
595
 
                                ByteBuffer value;
596
 
                                value = ByteBuffer.wrap( content_line.getBuffer().array(), pos,
597
 
                                        content_line.getBuffer().limit() - pos );
598
 
 
599
302
                                // get parameter parts
600
 
                                String[] name_param_parts = name_and_params.split( ";", -1 );
601
 
                                for( int i = 0; i < name_param_parts.length; i++ )
602
 
                                        name_param_parts[ i ] = name_param_parts[ i ].trim();
603
 
 
604
 
                                // determine whether we care about this entry
605
 
                                final HashSet< String > interesting_fields =
606
 
                                        new HashSet< String >( Arrays.asList( new String[] { "N",
607
 
                                                "FN", "ORG", "TITLE", "TEL", "EMAIL", "ADR", "LABEL" }
608
 
                                ) );
609
 
                                boolean is_interesting_field =
610
 
                                        interesting_fields.contains(
611
 
                                                name_param_parts[ 0 ].toUpperCase( Locale.ENGLISH ) );
612
 
 
613
 
                                // parse encoding parameter
614
 
                                String encoding = checkParam( name_param_parts, "ENCODING" );
615
 
                                if( encoding != null )
616
 
                                        encoding = encoding.toUpperCase( Locale.ENGLISH );
617
 
                                if( is_interesting_field && encoding != null &&
618
 
                                        !encoding.equalsIgnoreCase( "8BIT" ) &&
619
 
                                        !encoding.equalsIgnoreCase( "QUOTED-PRINTABLE" ) )
620
 
                                        //&& !encoding.equalsIgnoreCase( "BASE64" ) )
621
 
                                {
622
 
                                        throw new ParseException( R.string.error_vcf_encoding );
623
 
                                }
624
 
 
625
 
                                // parse charset parameter
626
 
                                String charset = checkParam( name_param_parts, "CHARSET" );
627
 
                                if( charset != null )
628
 
                                        charset = charset.toUpperCase( Locale.ENGLISH );
629
 
                                if( charset != null &&
630
 
                                        !charset.equalsIgnoreCase( "US-ASCII" ) &&
631
 
                                        !charset.equalsIgnoreCase( "ASCII" ) &&
632
 
                                        !charset.equalsIgnoreCase( "UTF-8" ) )
633
 
                                {
634
 
                                        throw new ParseException( R.string.error_vcf_charset );
635
 
                                }
636
 
 
637
 
                                // do unencoding (or default to a fake unencoding result with
638
 
                                // the raw string)
639
 
                                UnencodeResult unencoding_result = null;
640
 
                                if( encoding != null &&
641
 
                                        encoding.equalsIgnoreCase( "QUOTED-PRINTABLE" ) )
642
 
                                {
643
 
                                        unencoding_result = unencodeQuotedPrintable( value );
644
 
                                }
645
 
//                              else if( encoding != null &&
646
 
//                                      encoding.equalsIgnoreCase( "BASE64" ) )
647
 
//                              {
648
 
//                                      unencoding_result = unencodeBase64( props[ 1 ], charset );
649
 
//                              }
650
 
                                if( unencoding_result != null ) {
651
 
                                        value = unencoding_result.getBuffer();
652
 
                                        if( unencoding_result.isAnotherLineRequired() )
653
 
                                                _parser_multiline_state = MULTILINE_ENCODED;
654
 
                                }
655
 
 
656
 
                                // convert 8-bit US-ASCII charset to UTF-8 (where no charset is
657
 
                                // specified for a v2.1 vcard entry, we assume it's US-ASCII)
658
 
                                if( ( charset == null && _version.equals( "2.1" ) ) ||
659
 
                                        ( charset != null && (
660
 
                                                charset.equalsIgnoreCase( "ASCII" ) ||
661
 
                                                charset.equalsIgnoreCase( "US-ASCII" ) ) ) )
662
 
                                {
663
 
                                        value = transcodeAsciiToUtf8( value );
664
 
                                }
665
 
 
666
 
                                // process charset (value is now in UTF-8)
667
 
                                String string_value;
668
 
                                try {
669
 
                                        string_value = new String( value.array(), value.position(),
670
 
                                                value.limit() - value.position(), "UTF-8" );
671
 
                                } catch( UnsupportedEncodingException e ) {
672
 
                                        throw new ParseException( R.string.error_vcf_charset );
673
 
                                }
674
 
 
675
 
                                // for some entries that have semicolon-separated value parts,
676
 
                                // check to see if the value ends in an escape character, which
677
 
                                // indicates that we have a multi-line value
678
 
                                if( ( name_param_parts[ 0 ].equalsIgnoreCase( "N" ) ||
679
 
                                        name_param_parts[ 0 ].equalsIgnoreCase( "ORG" ) ||
680
 
                                        name_param_parts[ 0 ].equalsIgnoreCase( "ADR" ) ) &&
681
 
                                        doesStringEndInAnEscapeChar( string_value ) )
682
 
                                {
683
 
                                        _parser_multiline_state = MULTILINE_ESCAPED;
684
 
                                        string_value = string_value.substring( 0,
685
 
                                                string_value.length() - 1 );
686
 
                                }
687
 
 
688
 
                                // if we know we're not in an encoding-based multi-line, check
689
 
                                // to see if we're in a folded multi-line
690
 
                                if( _parser_multiline_state == MULTILINE_NONE &&
691
 
                                        content_line.doesNextLineLookFolded() )
692
 
                                {
693
 
                                        _parser_multiline_state = MULTILINE_FOLDED;
694
 
                                }
695
 
 
696
 
                                // handle multi-lines by buffering them and parsing them when we
697
 
                                // are processing the last line in a multi-line sequence
698
 
                                if( _parser_multiline_state != MULTILINE_NONE ) {
699
 
                                        _parser_buffered_value_so_far += string_value;
700
 
                                        return;
701
 
                                }
702
 
                                String complete_value =
703
 
                                        ( _parser_buffered_value_so_far + string_value ).trim();
704
 
 
705
 
                                // ignore empty values
706
 
                                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();
707
306
 
708
307
                                // parse some properties
709
 
                                if( name_param_parts[ 0 ].equalsIgnoreCase( "N" ) )
710
 
                                        parseN( name_param_parts, complete_value );
711
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "FN" ) )
712
 
                                        parseFN( name_param_parts, complete_value );
713
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "ORG" ) )
714
 
                                        parseORG( name_param_parts, complete_value );
715
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "TITLE" ) )
716
 
                                        parseTITLE( name_param_parts, complete_value );
717
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "TEL" ) )
718
 
                                        parseTEL( name_param_parts, complete_value );
719
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "EMAIL" ) )
720
 
                                        parseEMAIL( name_param_parts, complete_value );
721
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "ADR" ) )
722
 
                                        parseADR( name_param_parts, complete_value );
723
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "LABEL" ) )
724
 
                                        parseLABEL( name_param_parts, complete_value );
725
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "NOTE" ) )
726
 
                                        parseNOTE( name_param_parts, complete_value );
727
 
                                else if( name_param_parts[ 0 ].equalsIgnoreCase( "BDAY" ) )
728
 
                                        parseBDAY( name_param_parts, complete_value );
729
 
                        }
730
 
                }
731
 
 
732
 
                private boolean doesStringEndInAnEscapeChar( String string )
733
 
                {
734
 
                        // count the number of backslashes at the end of the string
735
 
                        int count = 0;
736
 
                        for( int a = string.length() - 1; a >= 0; a-- )
737
 
                                if( string.charAt( a ) == '\\' )
738
 
                                        count++;
739
 
                                else
740
 
                                        break;
741
 
 
742
 
                        // if there are an even number of backslashes then the final one
743
 
                        // doesn't count
744
 
                        return ( count & 1 ) == 1;
745
 
                }
746
 
 
747
 
                private String[] splitValueByCharacter( String value, char character )
748
 
                {
749
 
                        // split string in to parts by specified character
750
 
                        ArrayList< String > parts = new ArrayList< String >(
751
 
                                Arrays.asList( value.split( "" + character ) ) );
752
 
 
753
 
                        // go through parts
754
 
                        for( int a = 0; a < parts.size(); a++ )
755
 
                        {
756
 
                                String str = parts.get( a );
757
 
 
758
 
                                // Look for parts that end in an escape character, but ignore
759
 
                                // the final part.  We've already detected escape chars at the
760
 
                                // end of the final part in parseLine() and handled multi-lines
761
 
                                // accordingly.
762
 
                                if( a < parts.size() - 1 &&
763
 
                                        doesStringEndInAnEscapeChar( str ) )
764
 
                                {
765
 
                                        // append the escaped character, join the next part to this
766
 
                                        // part and remove the next part
767
 
                                        parts.set( a, str.substring( 0, str.length() - 1 ) +
768
 
                                                character + parts.get( a + 1 ) );
769
 
                                        parts.remove( a + 1 );
770
 
 
771
 
                                        // re-visit this part
772
 
                                        a--;
773
 
                                        continue;
774
 
                                }
775
 
 
776
 
                                // trim and replace string
777
 
                                str = str.trim();
778
 
                                parts.set( a, str );
779
 
                        }
780
 
 
781
 
                        String[] ret = new String[ parts.size() ];
782
 
                        return parts.toArray( ret );
783
 
                }
784
 
 
785
 
                private String unescapeValue( String value )
786
 
                {
787
 
                        StringBuilder ret = new StringBuilder( value.length() );
788
 
                        boolean in_escape = false;
789
 
                        for( int a = 0; a < value.length(); a++ )
790
 
                        {
791
 
                                int c = value.codePointAt( a );
792
 
 
793
 
                                // process a normal character
794
 
                                if( !in_escape ) {
795
 
                                        if( c == '\\' )
796
 
                                                in_escape = true;
797
 
                                        else
798
 
                                                ret.append( Character.toChars( c ) );
799
 
                                        continue;
800
 
                                }
801
 
 
802
 
                                // process an escape sequence
803
 
                                in_escape = false;
804
 
                                switch( c )
805
 
                                {
806
 
                                case 'T':
807
 
                                case 't':
808
 
                                        // add tab (invalid/non-standard, but accepted)
809
 
                                        ret.append( '\t' );
810
 
                                        break;
811
 
                                case 'N':
812
 
                                case 'n':
813
 
                                        // add newline
814
 
                                        ret.append( '\n' );
815
 
                                        break;
816
 
                                case '\\':
817
 
                                case ',':
818
 
                                case ';':
819
 
                                        // add escaped character
820
 
                                        ret.append( Character.toChars( c ) );
821
 
                                        break;
822
 
                                default:
823
 
                                        // unknown escape sequence, so add it unescaped
824
 
                                        // (invalid/non-standard, but accepted)
825
 
                                        ret.append( "\\" );
826
 
                                        ret.append( Character.toChars( c ) );
827
 
                                        break;
828
 
                                }
829
 
                        }
830
 
 
831
 
                        return ret.toString();
 
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
                        }
832
319
                }
833
320
 
834
321
                private void parseN( String[] params, String value )
 
322
                                throws ParseException, SkipContactException,
 
323
                                AbortImportException
835
324
                {
836
325
                        // already got a better name?
837
 
                        if( _name_level >= NAMELEVEL_N ) return;
 
326
                        if( _nameLevel >= NAMELEVEL_N ) return;
838
327
 
839
328
                        // get name parts
840
 
                        String[] name_parts = splitValueByCharacter( value, ';' );
 
329
                        String[] nameparts = value.split( ";" );
 
330
                        for( int i = 0; i < nameparts.length; i++ )
 
331
                                nameparts[ i ] = nameparts[ i ].trim();
841
332
 
842
333
                        // build name
843
334
                        value = "";
844
 
                        final int[] part_order = { 3, 1, 2, 0, 4 };
845
 
                        for( int a = 0; a < part_order.length; a++ )
846
 
                                if( name_parts.length > part_order[ a ] &&
847
 
                                        name_parts[ part_order[ a ] ].length() > 0 )
848
 
                                {
849
 
                                        // split this part in to it's comma-separated bits
850
 
                                        String[] name_part_parts = splitValueByCharacter(
851
 
                                                name_parts[ part_order[ a ] ], ',' );
852
 
                                        for( int b = 0; b < name_part_parts.length; b++ )
853
 
                                                if( name_part_parts[ b ].length() > 0 )
854
 
                                                {
855
 
                                                        if( value.length() > 0 ) value += " ";
856
 
                                                        value += name_part_parts[ b ];
857
 
                                                }
858
 
                                }
 
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 ];
859
339
 
860
340
                        // set name
861
 
                        setName( unescapeValue( value ) );
862
 
                        _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();
863
348
                }
864
349
 
865
350
                private void parseFN( String[] params, String value )
 
351
                                throws ParseException, SkipContactException
866
352
                {
867
353
                        // already got a better name?
868
 
                        if( _name_level >= NAMELEVEL_FN ) return;
 
354
                        if( _nameLevel >= NAMELEVEL_FN ) return;
869
355
 
870
356
                        // set name
871
 
                        setName( unescapeValue( value ) );
872
 
                        _name_level = NAMELEVEL_FN;
 
357
                        setName( undoCharsetAndEncoding( params, value ) );
 
358
                        _nameLevel = NAMELEVEL_FN;
873
359
                }
874
360
 
875
361
                private void parseORG( String[] params, String value )
 
362
                                throws ParseException, SkipContactException
876
363
                {
 
364
                        // already got a better name?
 
365
                        if( _nameLevel >= NAMELEVEL_ORG ) return;
 
366
 
877
367
                        // get org parts
878
 
                        String[] org_parts = splitValueByCharacter( value, ';' );
879
 
                        if( org_parts == null || org_parts.length < 1 ) return;
880
 
 
881
 
                        // build organisation name
882
 
                        StringBuilder builder = new StringBuilder(
883
 
                                String.valueOf( org_parts[ 0 ] ) );
884
 
                        for( int a = 1; a < org_parts.length; a++ )
885
 
                                builder.append( ", " ).append( org_parts[ a ] );
886
 
                        String organisation = unescapeValue( builder.toString() );
887
 
 
888
 
                        // set organisation name (using a title we've previously found)
889
 
                        addOrganisation( organisation, _cached_title, true );
890
 
 
891
 
                        // if we've not previously found a title, store this organisation
892
 
                        // name (we'll need it when we find a title to update the
893
 
                        // organisation, by name), else if we *have* previously found a
894
 
                        // title, clear it (since we just used it)
895
 
                        if( _cached_title == null )
896
 
                                _cached_organisation = organisation;
897
 
                        else
898
 
                                _cached_title = null;
899
 
                }
900
 
 
901
 
                private void parseTITLE( String[] params, String value )
902
 
                {
903
 
                        value = unescapeValue( value );
904
 
 
905
 
                        // if we previously had an organisation, look it up and append this
906
 
                        // title to it
907
 
                        if( _cached_organisation != null && hasOrganisations() ) {
908
 
                                HashMap< String, ExtraDetail > datas = getOrganisations();
909
 
                                ExtraDetail detail = datas.get( _cached_organisation );
910
 
                                if( detail != null )
911
 
                                        detail.setExtra( value );
912
 
                        }
913
 
 
914
 
                        // same as when handling organisation, if we've not previously found
915
 
                        // an organisation we store this title, else we clear it (since we
916
 
                        // just appended this title to it)
917
 
                        if( _cached_organisation == null )
918
 
                                _cached_title = value;
919
 
                        else
920
 
                                _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;
921
381
                }
922
382
 
923
383
                private void parseTEL( String[] params, String value )
 
384
                                throws ParseException
924
385
                {
925
386
                        if( value.length() == 0 ) return;
926
387
 
927
388
                        Set< String > types = extractTypes( params, Arrays.asList(
928
 
                                "PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
929
 
                                "PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
 
389
                                        "PREF", "HOME", "WORK", "VOICE", "FAX", "MSG", "CELL",
 
390
                                        "PAGER", "BBS", "MODEM", "CAR", "ISDN", "VIDEO" ) );
930
391
 
931
392
                        // here's the logic...
932
 
                        boolean is_preferred = types.contains( "PREF" );
933
 
                        int type;
 
393
                        boolean preferred = types.contains( "PREF" );
 
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 );
934
401
                        if( types.contains( "FAX" ) )
935
402
                                if( types.contains( "HOME" ) )
936
 
                                        type = TYPE_FAX_HOME;
 
403
                                        addPhone( value, PhonesColumns.TYPE_FAX_HOME, preferred );
937
404
                                else
938
 
                                        type = TYPE_FAX_WORK;
939
 
                        else if( types.contains( "CELL" ) || types.contains( "VIDEO" ) )
940
 
                                type = TYPE_MOBILE;
941
 
                        else if( types.contains( "PAGER" ) )
942
 
                                type = TYPE_PAGER;
943
 
                        else if( types.contains( "WORK" ) )
944
 
                                type = TYPE_WORK;
945
 
                        else
946
 
                                type = TYPE_HOME;
947
 
 
948
 
                        // add phone number
949
 
                        addNumber( value, type, is_preferred );
 
405
                                        addPhone( value, PhonesColumns.TYPE_FAX_WORK, preferred );
 
406
                        if( types.contains( "PAGER" ) )
 
407
                                addPhone( value, PhonesColumns.TYPE_PAGER, preferred );
950
408
                }
951
409
 
952
410
                public void parseEMAIL( String[] params, String value )
954
412
                        if( value.length() == 0 ) return;
955
413
 
956
414
                        Set< String > types = extractTypes( params, Arrays.asList(
957
 
                                "PREF", "WORK", "HOME", "INTERNET" ) );
958
 
 
959
 
                        // add email address
960
 
                        boolean is_preferred = types.contains( "PREF" );
961
 
                        int type;
962
 
                        if( types.contains( "WORK" ) )
963
 
                                type = TYPE_WORK;
964
 
                        else
965
 
                                type = TYPE_HOME;
966
 
 
967
 
                        addEmail( unescapeValue( value ), type, is_preferred );
968
 
                }
969
 
 
970
 
                private void parseADR( String[] params, String value )
971
 
                {
972
 
                        // get address parts
973
 
                        String[] adr_parts = splitValueByCharacter( value, ';' );
974
 
 
975
 
                        // build address
976
 
                        value = "";
977
 
                        for( int a = 0; a < adr_parts.length; a++ )
978
 
                                if( adr_parts[ a ].length() > 0 )
979
 
                                {
980
 
                                        // version 3.0 vCards allow further splitting by comma
981
 
                                        if( _version.equals( "3.0" ) )
982
 
                                        {
983
 
                                                // split this part in to it's comma-separated bits and
984
 
                                                // add them on individual lines
985
 
                                                String[] adr_part_parts =
986
 
                                                        splitValueByCharacter( adr_parts[ a ], ',' );
987
 
                                                for( int b = 0; b < adr_part_parts.length; b++ )
988
 
                                                        if( adr_part_parts[ b ].length() > 0 )
989
 
                                                        {
990
 
                                                                if( value.length() > 0 ) value += "\n";
991
 
                                                                value += adr_part_parts[ b ];
992
 
                                                        }
993
 
                                        }
994
 
                                        else
995
 
                                        {
996
 
                                                // add this part on an individual line
997
 
                                                if( value.length() > 0 ) value += "\n";
998
 
                                                value += adr_parts[ a ];
999
 
                                        }
1000
 
                                }
1001
 
 
1002
 
                        Set< String > types = extractTypes( params, Arrays.asList(
1003
 
                                "PREF", "WORK", "HOME" ) );
1004
 
 
1005
 
                        // add address
1006
 
                        int type;
1007
 
                        if( types.contains( "WORK" ) )
1008
 
                                type = TYPE_WORK;
1009
 
                        else
1010
 
                                type = TYPE_HOME;
1011
 
 
1012
 
                        addAddress( unescapeValue( value ), type );
1013
 
                }
1014
 
 
1015
 
                private void parseLABEL( String[] params, String value )
1016
 
                {
1017
 
                        Set< String > types = extractTypes( params, Arrays.asList(
1018
 
                                "PREF", "WORK", "HOME" ) );
1019
 
 
1020
 
                        // add address
1021
 
                        int type;
1022
 
                        if( types.contains( "WORK" ) )
1023
 
                                type = TYPE_WORK;
1024
 
                        else
1025
 
                                type = TYPE_HOME;
1026
 
 
1027
 
                        addAddress( unescapeValue( value ), type );
1028
 
                }
1029
 
 
1030
 
                private void parseNOTE( String[] params, String value )
1031
 
                {
1032
 
                        addNote( unescapeValue( value ) );
1033
 
                }
1034
 
 
1035
 
                private void parseBDAY( String[] params, String value )
1036
 
                {
1037
 
                        setBirthday( value );
1038
 
                }
1039
 
 
1040
 
                public void finaliseVcard()
1041
 
                        throws ParseException, ContactNotIdentifiableException,
1042
 
                                SkipImportException, AbortImportException
1043
 
                {
1044
 
                        // if there was content present, but no version line, then it must
1045
 
                        // be a version 2.1 vCard; process that content now
1046
 
                        if( _version == null && _content_lines != null ) {
1047
 
                                _version = "2.1";
1048
 
                                for( int i = 0; i < _content_lines.size(); i++ )
1049
 
                                        parseLine( _content_lines.get( i ) );
1050
 
                                _content_lines = null;
1051
 
                        }
1052
 
 
1053
 
                        // finalise the parent class
1054
 
                        finalise();
1055
 
                }
1056
 
 
1057
 
                /**
1058
 
                 * Amongst the params, find the value of the first, only, of any with
1059
 
                 * the specified name.
1060
 
                 *
1061
 
                 * @param params
1062
 
                 * @param name
1063
 
                 * @return a value, or null
1064
 
                 */
 
415
                                        "PREF", "WORK", "HOME", "INTERNET" ) );
 
416
 
 
417
                        // here's the logic...
 
418
                        boolean preferred = types.contains( "PREF" );
 
419
                        if( types.contains( "WORK" ) )
 
420
                                addEmail( value, Contacts.ContactMethods.TYPE_WORK, preferred );
 
421
                        else
 
422
                                addEmail( value, Contacts.ContactMethods.TYPE_HOME, preferred );
 
423
                }
 
424
 
 
425
                public void finaliseParsing()
 
426
                                throws ParseException, SkipContactException,
 
427
                                AbortImportException
 
428
                {
 
429
                        // missing version (and data is present)
 
430
                        if( _version == null && _lines != null )
 
431
                                throw new ParseException( R.string.error_vcf_malformed );
 
432
 
 
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;
 
462
                }
 
463
 
1065
464
                private String checkParam( String[] params, String name )
1066
465
                {
1067
 
                        String[] res = checkParams( params, name );
1068
 
                        return res.length > 0? res[ 0 ] : null;
1069
 
                }
1070
 
 
1071
 
                /**
1072
 
                 * Amongst the params, find the values of any with the specified name.
1073
 
                 *
1074
 
                 * @param params
1075
 
                 * @param name
1076
 
                 * @return an array of values, or null
1077
 
                 */
1078
 
                private String[] checkParams( String[] params, String name )
1079
 
                {
1080
 
                        HashSet< String > ret = new HashSet< String >();
1081
 
 
1082
 
                        Pattern p = Pattern.compile(
1083
 
                                "^" + name + "[ \\t]*=[ \\t]*(\"?)(.*)\\1$",
1084
 
                                Pattern.CASE_INSENSITIVE );
 
466
                        Pattern p = Pattern.compile( "^" + name + "[ \\t]*=[ \\t]*(.*)$" );
1085
467
                        for( int i = 0; i < params.length; i++ ) {
1086
468
                                Matcher m = p.matcher( params[ i ] );
1087
469
                                if( m.matches() )
1088
 
                                        ret.add( m.group( 2 ) );
 
470
                                        return m.group( 1 );
1089
471
                        }
1090
 
 
1091
 
                        return (String[]) ret.toArray( new String[ ret.size() ] );
 
472
                        return null;
1092
473
                }
1093
474
 
1094
 
                /**
1095
 
                 * Amongst the params, return any type values present.  For v2.1 vCards,
1096
 
                 * those types are just parameters.  For v3.0, they are prefixed with
1097
 
                 * "TYPE=".  There may also be multiple type parameters.
1098
 
                 *
1099
 
                 * @param params an array of params to look for types in
1100
 
                 * @param valid_types an list of upper-case type values to look for
1101
 
                 * @return a set of present type values
1102
 
                 */
1103
475
                private Set< String > extractTypes( String[] params,
1104
 
                        List< String > valid_types )
 
476
                                List< String > validTypes )
1105
477
                {
1106
478
                        HashSet< String > types = new HashSet< String >();
1107
479
 
1108
480
                        // get 3.0-style TYPE= param
1109
 
                        String type_params[] = checkParams( params, "TYPE" );
1110
 
                        for( int a = 0; a < type_params.length; a++ )
1111
 
                        {
1112
 
                                // check for a comma-separated list of types (why? I don't think
1113
 
                                // this is in the specs!)
1114
 
                                String[] parts = type_params[ a ].split( "," );
1115
 
                                for( int i = 0; i < parts.length; i++ ) {
1116
 
                                        String ucpart = parts[ i ].toUpperCase( Locale.ENGLISH );
1117
 
                                        if( valid_types.contains( ucpart ) )
1118
 
                                                types.add( ucpart );
1119
 
                                }
 
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 ] );
1120
487
                        }
1121
488
 
1122
489
                        // get 2.1-style type param
1123
490
                        if( _version.equals( "2.1" ) ) {
1124
 
                                for( int i = 1; i < params.length; i++ ) {
1125
 
                                        String ucparam = params[ i ].toUpperCase( Locale.ENGLISH );
1126
 
                                        if( valid_types.contains( ucparam ) )
1127
 
                                                types.add( ucparam );
1128
 
                                }
 
491
                                for( int i = 1; i < params.length; i++ )
 
492
                                        if( validTypes.contains( params[ i ] ) )
 
493
                                                types.add( params[ i ] );
1129
494
                        }
1130
495
 
1131
496
                        return types;
1132
497
                }
1133
498
 
1134
 
                private UnencodeResult unencodeQuotedPrintable( ByteBuffer in )
 
499
                private String unencodeQuotedPrintable( String str, String charset )
1135
500
                {
1136
 
                        boolean another = false;
 
501
                        // default encoding scheme
 
502
                        if( charset == null ) charset = "UTF-8";
1137
503
 
1138
 
                        // unencode quoted-printable encoding, as per RFC1521 section 5.1
1139
 
                        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() ];
1140
506
                        int j = 0;
1141
 
                        for( int i = in.position(); i < in.limit(); i++ )
1142
 
                        {
1143
 
                                // get next char and process...
1144
 
                                byte ch = in.array()[ i ];
1145
 
                                if( ch == '=' && i < in.limit() - 2 )
1146
 
                                {
1147
 
                                        // we found a =XX format byte, add it
1148
 
                                        out[ j ] = (byte)(
1149
 
                                                        Character.digit( in.array()[ i + 1 ], 16 ) * 16 +
1150
 
                                                        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 ) );
1151
513
                                        i += 2;
1152
514
                                }
1153
 
                                else if( ch == '=' && i == in.limit() - 1 )
1154
 
                                {
1155
 
                                        // we found a '=' at the end of a line signifying a multi-
1156
 
                                        // line string, so we don't add it
1157
 
                                        another = true;
1158
 
                                        continue;
1159
 
                                }
1160
515
                                else
1161
 
                                        // just a normal char...
1162
 
                                        out[ j ] = (byte)ch;
1163
 
                                j++;
1164
 
                        }
1165
 
 
1166
 
                        return new UnencodeResult( another, ByteBuffer.wrap( out, 0, j ) );
1167
 
                }
1168
 
 
1169
 
                private ByteBuffer transcodeAsciiToUtf8( ByteBuffer in )
1170
 
                {
1171
 
                        // transcode
1172
 
                        byte[] out = new byte[ ( in.limit() - in.position() ) * 2 ];
1173
 
                        int j = 0;
1174
 
                        for( int a = in.position(); a < in.limit(); a++ )
1175
 
                        {
1176
 
                                // if char is < 127, keep it as-is
1177
 
                                if( in.array()[ a ] >= 0 )
1178
 
                                        out[ j++ ] = in.array()[ a ];
1179
 
 
1180
 
                                // else, convert it to UTF-8
1181
 
                                else {
1182
 
                                        int b = 0xff & (int)in.array()[ a ];
1183
 
                                        out[ j++ ] = (byte)( 0xc0 | ( b >> 6 ) );
1184
 
                                        out[ j++ ] = (byte)( 0x80 | ( b & 0x3f ) );
1185
 
                                }
1186
 
                        }
1187
 
 
1188
 
                        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;
1189
522
                }
1190
523
        }
1191
524
}