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