/android/import-contacts

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

« back to all changes in this revision

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

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

Show diffs side-by-side

added added

removed removed

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