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