Home | History | Annotate | Download | only in vcard
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 package android.pim.vcard;
     17 
     18 import android.content.ContentResolver;
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.content.Entity;
     22 import android.content.EntityIterator;
     23 import android.content.Entity.NamedContentValues;
     24 import android.database.Cursor;
     25 import android.database.sqlite.SQLiteException;
     26 import android.net.Uri;
     27 import android.pim.vcard.exception.VCardException;
     28 import android.provider.ContactsContract.Contacts;
     29 import android.provider.ContactsContract.Data;
     30 import android.provider.ContactsContract.RawContacts;
     31 import android.provider.ContactsContract.RawContactsEntity;
     32 import android.provider.ContactsContract.CommonDataKinds.Email;
     33 import android.provider.ContactsContract.CommonDataKinds.Event;
     34 import android.provider.ContactsContract.CommonDataKinds.Im;
     35 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     36 import android.provider.ContactsContract.CommonDataKinds.Note;
     37 import android.provider.ContactsContract.CommonDataKinds.Organization;
     38 import android.provider.ContactsContract.CommonDataKinds.Phone;
     39 import android.provider.ContactsContract.CommonDataKinds.Photo;
     40 import android.provider.ContactsContract.CommonDataKinds.Relation;
     41 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     42 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     43 import android.provider.ContactsContract.CommonDataKinds.Website;
     44 import android.util.CharsetUtils;
     45 import android.util.Log;
     46 
     47 import java.io.BufferedWriter;
     48 import java.io.FileOutputStream;
     49 import java.io.IOException;
     50 import java.io.OutputStream;
     51 import java.io.OutputStreamWriter;
     52 import java.io.UnsupportedEncodingException;
     53 import java.io.Writer;
     54 import java.lang.reflect.InvocationTargetException;
     55 import java.lang.reflect.Method;
     56 import java.nio.charset.UnsupportedCharsetException;
     57 import java.util.ArrayList;
     58 import java.util.HashMap;
     59 import java.util.List;
     60 import java.util.Map;
     61 
     62 /**
     63  * <p>
     64  * The class for composing VCard from Contacts information. Note that this is
     65  * completely differnt implementation from
     66  * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore.
     67  * </p>
     68  *
     69  * <p>
     70  * Usually, this class should be used like this.
     71  * </p>
     72  *
     73  * <pre class="prettyprint">VCardComposer composer = null;
     74  * try {
     75  *     composer = new VCardComposer(context);
     76  *     composer.addHandler(
     77  *             composer.new HandlerForOutputStream(outputStream));
     78  *     if (!composer.init()) {
     79  *         // Do something handling the situation.
     80  *         return;
     81  *     }
     82  *     while (!composer.isAfterLast()) {
     83  *         if (mCanceled) {
     84  *             // Assume a user may cancel this operation during the export.
     85  *             return;
     86  *         }
     87  *         if (!composer.createOneEntry()) {
     88  *             // Do something handling the error situation.
     89  *             return;
     90  *         }
     91  *     }
     92  * } finally {
     93  *     if (composer != null) {
     94  *         composer.terminate();
     95  *     }
     96  * } </pre>
     97  */
     98 public class VCardComposer {
     99     private static final String LOG_TAG = "VCardComposer";
    100 
    101     public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
    102     public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
    103     public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
    104 
    105     public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
    106         "Failed to get database information";
    107 
    108     public static final String FAILURE_REASON_NO_ENTRY =
    109         "There's no exportable in the database";
    110 
    111     public static final String FAILURE_REASON_NOT_INITIALIZED =
    112         "The vCard composer object is not correctly initialized";
    113 
    114     /** Should be visible only from developers... (no need to translate, hopefully) */
    115     public static final String FAILURE_REASON_UNSUPPORTED_URI =
    116         "The Uri vCard composer received is not supported by the composer.";
    117 
    118     public static final String NO_ERROR = "No error";
    119 
    120     public static final String VCARD_TYPE_STRING_DOCOMO = "docomo";
    121 
    122     private static final String SHIFT_JIS = "SHIFT_JIS";
    123     private static final String UTF_8 = "UTF-8";
    124 
    125     /**
    126      * Special URI for testing.
    127      */
    128     public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard";
    129     public static final Uri VCARD_TEST_AUTHORITY_URI =
    130         Uri.parse("content://" + VCARD_TEST_AUTHORITY);
    131     public static final Uri CONTACTS_TEST_CONTENT_URI =
    132         Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts");
    133 
    134     private static final Map<Integer, String> sImMap;
    135 
    136     static {
    137         sImMap = new HashMap<Integer, String>();
    138         sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
    139         sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
    140         sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
    141         sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
    142         sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
    143         sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
    144         // Google talk is a special case.
    145     }
    146 
    147     public static interface OneEntryHandler {
    148         public boolean onInit(Context context);
    149         public boolean onEntryCreated(String vcard);
    150         public void onTerminate();
    151     }
    152 
    153     /**
    154      * <p>
    155      * An useful example handler, which emits VCard String to outputstream one by one.
    156      * </p>
    157      * <p>
    158      * The input OutputStream object is closed() on {@link #onTerminate()}.
    159      * Must not close the stream outside.
    160      * </p>
    161      */
    162     public class HandlerForOutputStream implements OneEntryHandler {
    163         @SuppressWarnings("hiding")
    164         private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream";
    165 
    166         final private OutputStream mOutputStream; // mWriter will close this.
    167         private Writer mWriter;
    168 
    169         private boolean mOnTerminateIsCalled = false;
    170 
    171         /**
    172          * Input stream will be closed on the detruction of this object.
    173          */
    174         public HandlerForOutputStream(OutputStream outputStream) {
    175             mOutputStream = outputStream;
    176         }
    177 
    178         public boolean onInit(Context context) {
    179             try {
    180                 mWriter = new BufferedWriter(new OutputStreamWriter(
    181                         mOutputStream, mCharsetString));
    182             } catch (UnsupportedEncodingException e1) {
    183                 Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString);
    184                 mErrorReason = "Encoding is not supported (usually this does not happen!): "
    185                         + mCharsetString;
    186                 return false;
    187             }
    188 
    189             if (mIsDoCoMo) {
    190                 try {
    191                     // Create one empty entry.
    192                     mWriter.write(createOneEntryInternal("-1", null));
    193                 } catch (VCardException e) {
    194                     Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " +
    195                             e.getMessage());
    196                     return false;
    197                 } catch (IOException e) {
    198                     Log.e(LOG_TAG,
    199                             "IOException occurred during exportOneContactData: "
    200                                     + e.getMessage());
    201                     mErrorReason = "IOException occurred: " + e.getMessage();
    202                     return false;
    203                 }
    204             }
    205             return true;
    206         }
    207 
    208         public boolean onEntryCreated(String vcard) {
    209             try {
    210                 mWriter.write(vcard);
    211             } catch (IOException e) {
    212                 Log.e(LOG_TAG,
    213                         "IOException occurred during exportOneContactData: "
    214                                 + e.getMessage());
    215                 mErrorReason = "IOException occurred: " + e.getMessage();
    216                 return false;
    217             }
    218             return true;
    219         }
    220 
    221         public void onTerminate() {
    222             mOnTerminateIsCalled = true;
    223             if (mWriter != null) {
    224                 try {
    225                     // Flush and sync the data so that a user is able to pull
    226                     // the SDCard just after
    227                     // the export.
    228                     mWriter.flush();
    229                     if (mOutputStream != null
    230                             && mOutputStream instanceof FileOutputStream) {
    231                             ((FileOutputStream) mOutputStream).getFD().sync();
    232                     }
    233                 } catch (IOException e) {
    234                     Log.d(LOG_TAG,
    235                             "IOException during closing the output stream: "
    236                                     + e.getMessage());
    237                 } finally {
    238                     try {
    239                         mWriter.close();
    240                     } catch (IOException e) {
    241                     }
    242                 }
    243             }
    244         }
    245 
    246         @Override
    247         public void finalize() {
    248             if (!mOnTerminateIsCalled) {
    249                 onTerminate();
    250             }
    251         }
    252     }
    253 
    254     private final Context mContext;
    255     private final int mVCardType;
    256     private final boolean mCareHandlerErrors;
    257     private final ContentResolver mContentResolver;
    258 
    259     private final boolean mIsDoCoMo;
    260     private final boolean mUsesShiftJis;
    261     private Cursor mCursor;
    262     private int mIdColumn;
    263 
    264     private final String mCharsetString;
    265     private boolean mTerminateIsCalled;
    266     private final List<OneEntryHandler> mHandlerList;
    267 
    268     private String mErrorReason = NO_ERROR;
    269 
    270     private static final String[] sContactsProjection = new String[] {
    271         Contacts._ID,
    272     };
    273 
    274     public VCardComposer(Context context) {
    275         this(context, VCardConfig.VCARD_TYPE_DEFAULT, true);
    276     }
    277 
    278     public VCardComposer(Context context, int vcardType) {
    279         this(context, vcardType, true);
    280     }
    281 
    282     public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) {
    283         this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors);
    284     }
    285 
    286     /**
    287      * Construct for supporting call log entry vCard composing.
    288      */
    289     public VCardComposer(final Context context, final int vcardType,
    290             final boolean careHandlerErrors) {
    291         mContext = context;
    292         mVCardType = vcardType;
    293         mCareHandlerErrors = careHandlerErrors;
    294         mContentResolver = context.getContentResolver();
    295 
    296         mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
    297         mUsesShiftJis = VCardConfig.usesShiftJis(vcardType);
    298         mHandlerList = new ArrayList<OneEntryHandler>();
    299 
    300         if (mIsDoCoMo) {
    301             String charset;
    302             try {
    303                 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
    304             } catch (UnsupportedCharsetException e) {
    305                 Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
    306                 charset = SHIFT_JIS;
    307             }
    308             mCharsetString = charset;
    309         } else if (mUsesShiftJis) {
    310             String charset;
    311             try {
    312                 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
    313             } catch (UnsupportedCharsetException e) {
    314                 Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
    315                 charset = SHIFT_JIS;
    316             }
    317             mCharsetString = charset;
    318         } else {
    319             mCharsetString = UTF_8;
    320         }
    321     }
    322 
    323     /**
    324      * Must be called before {@link #init()}.
    325      */
    326     public void addHandler(OneEntryHandler handler) {
    327         if (handler != null) {
    328             mHandlerList.add(handler);
    329         }
    330     }
    331 
    332     /**
    333      * @return Returns true when initialization is successful and all the other
    334      *          methods are available. Returns false otherwise.
    335      */
    336     public boolean init() {
    337         return init(null, null);
    338     }
    339 
    340     public boolean init(final String selection, final String[] selectionArgs) {
    341         return init(Contacts.CONTENT_URI, selection, selectionArgs, null);
    342     }
    343 
    344     /**
    345      * Note that this is unstable interface, may be deleted in the future.
    346      */
    347     public boolean init(final Uri contentUri, final String selection,
    348             final String[] selectionArgs, final String sortOrder) {
    349         if (contentUri == null) {
    350             return false;
    351         }
    352 
    353         if (mCareHandlerErrors) {
    354             List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
    355                     mHandlerList.size());
    356             for (OneEntryHandler handler : mHandlerList) {
    357                 if (!handler.onInit(mContext)) {
    358                     for (OneEntryHandler finished : finishedList) {
    359                         finished.onTerminate();
    360                     }
    361                     return false;
    362                 }
    363             }
    364         } else {
    365             // Just ignore the false returned from onInit().
    366             for (OneEntryHandler handler : mHandlerList) {
    367                 handler.onInit(mContext);
    368             }
    369         }
    370 
    371         final String[] projection;
    372         if (Contacts.CONTENT_URI.equals(contentUri) ||
    373                 CONTACTS_TEST_CONTENT_URI.equals(contentUri)) {
    374             projection = sContactsProjection;
    375         } else {
    376             mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
    377             return false;
    378         }
    379         mCursor = mContentResolver.query(
    380                 contentUri, projection, selection, selectionArgs, sortOrder);
    381 
    382         if (mCursor == null) {
    383             mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
    384             return false;
    385         }
    386 
    387         if (getCount() == 0 || !mCursor.moveToFirst()) {
    388             try {
    389                 mCursor.close();
    390             } catch (SQLiteException e) {
    391                 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
    392             } finally {
    393                 mCursor = null;
    394                 mErrorReason = FAILURE_REASON_NO_ENTRY;
    395             }
    396             return false;
    397         }
    398 
    399         mIdColumn = mCursor.getColumnIndex(Contacts._ID);
    400 
    401         return true;
    402     }
    403 
    404     public boolean createOneEntry() {
    405         return createOneEntry(null);
    406     }
    407 
    408     /**
    409      * @param getEntityIteratorMethod For Dependency Injection.
    410      * @hide just for testing.
    411      */
    412     public boolean createOneEntry(Method getEntityIteratorMethod) {
    413         if (mCursor == null || mCursor.isAfterLast()) {
    414             mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
    415             return false;
    416         }
    417         String vcard;
    418         try {
    419             if (mIdColumn >= 0) {
    420                 vcard = createOneEntryInternal(mCursor.getString(mIdColumn),
    421                         getEntityIteratorMethod);
    422             } else {
    423                 Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
    424                 return true;
    425             }
    426         } catch (VCardException e) {
    427             Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage());
    428             return false;
    429         } catch (OutOfMemoryError error) {
    430             // Maybe some data (e.g. photo) is too big to have in memory. But it
    431             // should be rare.
    432             Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry.");
    433             System.gc();
    434             // TODO: should tell users what happened?
    435             return true;
    436         } finally {
    437             mCursor.moveToNext();
    438         }
    439 
    440         // This function does not care the OutOfMemoryError on the handler side
    441         // :-P
    442         if (mCareHandlerErrors) {
    443             List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
    444                     mHandlerList.size());
    445             for (OneEntryHandler handler : mHandlerList) {
    446                 if (!handler.onEntryCreated(vcard)) {
    447                     return false;
    448                 }
    449             }
    450         } else {
    451             for (OneEntryHandler handler : mHandlerList) {
    452                 handler.onEntryCreated(vcard);
    453             }
    454         }
    455 
    456         return true;
    457     }
    458 
    459     private String createOneEntryInternal(final String contactId,
    460             Method getEntityIteratorMethod) throws VCardException {
    461         final Map<String, List<ContentValues>> contentValuesListMap =
    462                 new HashMap<String, List<ContentValues>>();
    463         // The resolver may return the entity iterator with no data. It is possible.
    464         // e.g. If all the data in the contact of the given contact id are not exportable ones,
    465         //      they are hidden from the view of this method, though contact id itself exists.
    466         EntityIterator entityIterator = null;
    467         try {
    468             final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon()
    469                     .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1")
    470                     .build();
    471             final String selection = Data.CONTACT_ID + "=?";
    472             final String[] selectionArgs = new String[] {contactId};
    473             if (getEntityIteratorMethod != null) {
    474                 // Please note that this branch is executed by some tests only
    475                 try {
    476                     entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
    477                             mContentResolver, uri, selection, selectionArgs, null);
    478                 } catch (IllegalArgumentException e) {
    479                     Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
    480                             e.getMessage());
    481                 } catch (IllegalAccessException e) {
    482                     Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
    483                             e.getMessage());
    484                 } catch (InvocationTargetException e) {
    485                     Log.e(LOG_TAG, "InvocationTargetException has been thrown: ");
    486                     StackTraceElement[] stackTraceElements = e.getCause().getStackTrace();
    487                     for (StackTraceElement element : stackTraceElements) {
    488                         Log.e(LOG_TAG, "    at " + element.toString());
    489                     }
    490                     throw new VCardException("InvocationTargetException has been thrown: " +
    491                             e.getCause().getMessage());
    492                 }
    493             } else {
    494                 entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
    495                         uri, null, selection, selectionArgs, null));
    496             }
    497 
    498             if (entityIterator == null) {
    499                 Log.e(LOG_TAG, "EntityIterator is null");
    500                 return "";
    501             }
    502 
    503             if (!entityIterator.hasNext()) {
    504                 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
    505                 return "";
    506             }
    507 
    508             while (entityIterator.hasNext()) {
    509                 Entity entity = entityIterator.next();
    510                 for (NamedContentValues namedContentValues : entity.getSubValues()) {
    511                     ContentValues contentValues = namedContentValues.values;
    512                     String key = contentValues.getAsString(Data.MIMETYPE);
    513                     if (key != null) {
    514                         List<ContentValues> contentValuesList =
    515                                 contentValuesListMap.get(key);
    516                         if (contentValuesList == null) {
    517                             contentValuesList = new ArrayList<ContentValues>();
    518                             contentValuesListMap.put(key, contentValuesList);
    519                         }
    520                         contentValuesList.add(contentValues);
    521                     }
    522                 }
    523             }
    524         } finally {
    525             if (entityIterator != null) {
    526                 entityIterator.close();
    527             }
    528         }
    529 
    530         final VCardBuilder builder = new VCardBuilder(mVCardType);
    531         builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
    532                 .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
    533                 .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
    534                 .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
    535                 .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
    536                 .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
    537                 .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
    538         if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
    539             builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
    540         }
    541         builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
    542                 .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
    543                 .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
    544                 .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
    545         return builder.toString();
    546     }
    547 
    548     public void terminate() {
    549         for (OneEntryHandler handler : mHandlerList) {
    550             handler.onTerminate();
    551         }
    552 
    553         if (mCursor != null) {
    554             try {
    555                 mCursor.close();
    556             } catch (SQLiteException e) {
    557                 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
    558             }
    559             mCursor = null;
    560         }
    561 
    562         mTerminateIsCalled = true;
    563     }
    564 
    565     @Override
    566     public void finalize() {
    567         if (!mTerminateIsCalled) {
    568             terminate();
    569         }
    570     }
    571 
    572     public int getCount() {
    573         if (mCursor == null) {
    574             return 0;
    575         }
    576         return mCursor.getCount();
    577     }
    578 
    579     public boolean isAfterLast() {
    580         if (mCursor == null) {
    581             return false;
    582         }
    583         return mCursor.isAfterLast();
    584     }
    585 
    586     /**
    587      * @return Return the error reason if possible.
    588      */
    589     public String getErrorReason() {
    590         return mErrorReason;
    591     }
    592 }
    593