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