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