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");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of 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,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.common.vcard;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Dialog;
     22 import android.app.Notification;
     23 import android.app.NotificationManager;
     24 import android.app.ProgressDialog;
     25 import android.content.ClipData;
     26 import android.content.ComponentName;
     27 import android.content.ContentResolver;
     28 import android.content.Context;
     29 import android.content.DialogInterface;
     30 import android.content.Intent;
     31 import android.content.ServiceConnection;
     32 import android.database.Cursor;
     33 import android.net.Uri;
     34 import android.os.Bundle;
     35 import android.os.Handler;
     36 import android.os.IBinder;
     37 import android.os.PowerManager;
     38 import android.provider.OpenableColumns;
     39 import android.text.TextUtils;
     40 import android.util.Log;
     41 import android.widget.Toast;
     42 
     43 import com.android.contacts.common.R;
     44 import com.android.contacts.common.activity.RequestImportVCardPermissionsActivity;
     45 import com.android.contacts.common.model.AccountTypeManager;
     46 import com.android.contacts.common.model.account.AccountWithDataSet;
     47 import com.android.vcard.VCardEntryCounter;
     48 import com.android.vcard.VCardParser;
     49 import com.android.vcard.VCardParser_V21;
     50 import com.android.vcard.VCardParser_V30;
     51 import com.android.vcard.VCardSourceDetector;
     52 import com.android.vcard.exception.VCardException;
     53 import com.android.vcard.exception.VCardNestedException;
     54 import com.android.vcard.exception.VCardVersionException;
     55 
     56 import java.io.ByteArrayInputStream;
     57 import java.io.File;
     58 import java.io.IOException;
     59 import java.io.InputStream;
     60 import java.nio.ByteBuffer;
     61 import java.nio.channels.Channels;
     62 import java.nio.channels.ReadableByteChannel;
     63 import java.nio.channels.WritableByteChannel;
     64 import java.util.ArrayList;
     65 import java.util.Arrays;
     66 import java.util.List;
     67 
     68 /**
     69  * The class letting users to import vCard. This includes the UI part for letting them select
     70  * an Account and posssibly a file if there's no Uri is given from its caller Activity.
     71  *
     72  * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
     73  * finished (with the method {@link Activity#finish()}) after the import and never reuse
     74  * any Dialog in the instance. So this code is careless about the management around managed
     75  * dialogs stuffs (like how onCreateDialog() is used).
     76  */
     77 public class ImportVCardActivity extends Activity {
     78     private static final String LOG_TAG = "VCardImport";
     79 
     80     private static final int SELECT_ACCOUNT = 0;
     81 
     82     /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
     83     /* package */ final static int VCARD_VERSION_V21 = 1;
     84     /* package */ final static int VCARD_VERSION_V30 = 2;
     85 
     86     private static final int REQUEST_OPEN_DOCUMENT = 100;
     87 
     88     /**
     89      * Notification id used when error happened before sending an import request to VCardServer.
     90      */
     91     private static final int FAILURE_NOTIFICATION_ID = 1;
     92 
     93     private AccountWithDataSet mAccount;
     94 
     95     private ProgressDialog mProgressDialogForCachingVCard;
     96 
     97     private VCardCacheThread mVCardCacheThread;
     98     private ImportRequestConnection mConnection;
     99     /* package */ VCardImportExportListener mListener;
    100 
    101     private String mErrorMessage;
    102 
    103     private Handler mHandler = new Handler();
    104 
    105     // Runs on the UI thread.
    106     private class DialogDisplayer implements Runnable {
    107         private final int mResId;
    108         public DialogDisplayer(int resId) {
    109             mResId = resId;
    110         }
    111         public DialogDisplayer(String errorMessage) {
    112             mResId = R.id.dialog_error_with_message;
    113             mErrorMessage = errorMessage;
    114         }
    115         @Override
    116         public void run() {
    117             if (!isFinishing()) {
    118                 showDialog(mResId);
    119             }
    120         }
    121     }
    122 
    123     private class CancelListener
    124         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
    125         @Override
    126         public void onClick(DialogInterface dialog, int which) {
    127             finish();
    128         }
    129         @Override
    130         public void onCancel(DialogInterface dialog) {
    131             finish();
    132         }
    133     }
    134 
    135     private CancelListener mCancelListener = new CancelListener();
    136 
    137     private class ImportRequestConnection implements ServiceConnection {
    138         private VCardService mService;
    139 
    140         public void sendImportRequest(final List<ImportRequest> requests) {
    141             Log.i(LOG_TAG, "Send an import request");
    142             mService.handleImportRequest(requests, mListener);
    143         }
    144 
    145         @Override
    146         public void onServiceConnected(ComponentName name, IBinder binder) {
    147             mService = ((VCardService.MyBinder) binder).getService();
    148             Log.i(LOG_TAG,
    149                     String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
    150                             Arrays.toString(mVCardCacheThread.getSourceUris())));
    151             mVCardCacheThread.start();
    152         }
    153 
    154         @Override
    155         public void onServiceDisconnected(ComponentName name) {
    156             Log.i(LOG_TAG, "Disconnected from VCardService");
    157         }
    158     }
    159 
    160     /**
    161      * Caches given vCard files into a local directory, and sends actual import request to
    162      * {@link VCardService}.
    163      *
    164      * We need to cache given files into local storage. One of reasons is that some data (as Uri)
    165      * may have special permissions. Callers may allow only this Activity to access that content,
    166      * not what this Activity launched (like {@link VCardService}).
    167      */
    168     private class VCardCacheThread extends Thread
    169             implements DialogInterface.OnCancelListener {
    170         private boolean mCanceled;
    171         private PowerManager.WakeLock mWakeLock;
    172         private VCardParser mVCardParser;
    173         private final Uri[] mSourceUris;  // Given from a caller.
    174         private final byte[] mSource;
    175         private final String mDisplayName;
    176 
    177         public VCardCacheThread(final Uri[] sourceUris) {
    178             mSourceUris = sourceUris;
    179             mSource = null;
    180             final Context context = ImportVCardActivity.this;
    181             final PowerManager powerManager =
    182                     (PowerManager)context.getSystemService(Context.POWER_SERVICE);
    183             mWakeLock = powerManager.newWakeLock(
    184                     PowerManager.SCREEN_DIM_WAKE_LOCK |
    185                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
    186             mDisplayName = null;
    187         }
    188 
    189         @Override
    190         public void finalize() {
    191             if (mWakeLock != null && mWakeLock.isHeld()) {
    192                 Log.w(LOG_TAG, "WakeLock is being held.");
    193                 mWakeLock.release();
    194             }
    195         }
    196 
    197         @Override
    198         public void run() {
    199             Log.i(LOG_TAG, "vCard cache thread starts running.");
    200             if (mConnection == null) {
    201                 throw new NullPointerException("vCard cache thread must be launched "
    202                         + "after a service connection is established");
    203             }
    204 
    205             mWakeLock.acquire();
    206             try {
    207                 if (mCanceled == true) {
    208                     Log.i(LOG_TAG, "vCard cache operation is canceled.");
    209                     return;
    210                 }
    211 
    212                 final Context context = ImportVCardActivity.this;
    213                 // Uris given from caller applications may not be opened twice: consider when
    214                 // it is not from local storage (e.g. "file:///...") but from some special
    215                 // provider (e.g. "content://...").
    216                 // Thus we have to once copy the content of Uri into local storage, and read
    217                 // it after it.
    218                 //
    219                 // We may be able to read content of each vCard file during copying them
    220                 // to local storage, but currently vCard code does not allow us to do so.
    221                 int cache_index = 0;
    222                 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
    223                 if (mSource != null) {
    224                     try {
    225                         requests.add(constructImportRequest(mSource, null, mDisplayName));
    226                     } catch (VCardException e) {
    227                         Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
    228                         showFailureNotification(R.string.fail_reason_not_supported);
    229                         return;
    230                     }
    231                 } else {
    232                     final ContentResolver resolver =
    233                             ImportVCardActivity.this.getContentResolver();
    234                     for (Uri sourceUri : mSourceUris) {
    235                         String filename = null;
    236                         // Note: caches are removed by VCardService.
    237                         while (true) {
    238                             filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
    239                             final File file = context.getFileStreamPath(filename);
    240                             if (!file.exists()) {
    241                                 break;
    242                             } else {
    243                                 if (cache_index == Integer.MAX_VALUE) {
    244                                     throw new RuntimeException("Exceeded cache limit");
    245                                 }
    246                                 cache_index++;
    247                             }
    248                         }
    249                         Uri localDataUri = null;
    250 
    251                         try {
    252                             localDataUri = copyTo(sourceUri, filename);
    253                         } catch (SecurityException e) {
    254                             Log.e(LOG_TAG, "SecurityException", e);
    255                             showFailureNotification(R.string.fail_reason_io_error);
    256                             return;
    257                         }
    258                         if (mCanceled) {
    259                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
    260                             break;
    261                         }
    262                         if (localDataUri == null) {
    263                             Log.w(LOG_TAG, "destUri is null");
    264                             break;
    265                         }
    266 
    267                         String displayName = null;
    268                         Cursor cursor = null;
    269                         // Try to get a display name from the given Uri. If it fails, we just
    270                         // pick up the last part of the Uri.
    271                         try {
    272                             cursor = resolver.query(sourceUri,
    273                                     new String[] { OpenableColumns.DISPLAY_NAME },
    274                                     null, null, null);
    275                             if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
    276                                 if (cursor.getCount() > 1) {
    277                                     Log.w(LOG_TAG, "Unexpected multiple rows: "
    278                                             + cursor.getCount());
    279                                 }
    280                                 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
    281                                 if (index >= 0) {
    282                                     displayName = cursor.getString(index);
    283                                 }
    284                             }
    285                         } finally {
    286                             if (cursor != null) {
    287                                 cursor.close();
    288                             }
    289                         }
    290                         if (TextUtils.isEmpty(displayName)){
    291                             displayName = sourceUri.getLastPathSegment();
    292                         }
    293 
    294                         final ImportRequest request;
    295                         try {
    296                             request = constructImportRequest(null, localDataUri, displayName);
    297                         } catch (VCardException e) {
    298                             Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
    299                             showFailureNotification(R.string.fail_reason_not_supported);
    300                             return;
    301                         } catch (IOException e) {
    302                             Log.e(LOG_TAG, "Unexpected IOException", e);
    303                             showFailureNotification(R.string.fail_reason_io_error);
    304                             return;
    305                         }
    306                         if (mCanceled) {
    307                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
    308                             return;
    309                         }
    310                         requests.add(request);
    311                     }
    312                 }
    313                 if (!requests.isEmpty()) {
    314                     mConnection.sendImportRequest(requests);
    315                 } else {
    316                     Log.w(LOG_TAG, "Empty import requests. Ignore it.");
    317                 }
    318             } catch (OutOfMemoryError e) {
    319                 Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
    320                 System.gc();
    321                 runOnUiThread(new DialogDisplayer(
    322                         getString(R.string.fail_reason_low_memory_during_import)));
    323             } catch (IOException e) {
    324                 Log.e(LOG_TAG, "IOException during caching vCard", e);
    325                 runOnUiThread(new DialogDisplayer(
    326                         getString(R.string.fail_reason_io_error)));
    327             } finally {
    328                 Log.i(LOG_TAG, "Finished caching vCard.");
    329                 mWakeLock.release();
    330                 unbindService(mConnection);
    331                 mProgressDialogForCachingVCard.dismiss();
    332                 mProgressDialogForCachingVCard = null;
    333                 finish();
    334             }
    335         }
    336 
    337         /**
    338          * Copy the content of sourceUri to the destination.
    339          */
    340         private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
    341             Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
    342                     sourceUri, filename));
    343             final Context context = ImportVCardActivity.this;
    344             final ContentResolver resolver = context.getContentResolver();
    345             ReadableByteChannel inputChannel = null;
    346             WritableByteChannel outputChannel = null;
    347             Uri destUri = null;
    348             try {
    349                 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
    350                 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
    351                 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
    352                 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
    353                 while (inputChannel.read(buffer) != -1) {
    354                     if (mCanceled) {
    355                         Log.d(LOG_TAG, "Canceled during caching " + sourceUri);
    356                         return null;
    357                     }
    358                     buffer.flip();
    359                     outputChannel.write(buffer);
    360                     buffer.compact();
    361                 }
    362                 buffer.flip();
    363                 while (buffer.hasRemaining()) {
    364                     outputChannel.write(buffer);
    365                 }
    366             } finally {
    367                 if (inputChannel != null) {
    368                     try {
    369                         inputChannel.close();
    370                     } catch (IOException e) {
    371                         Log.w(LOG_TAG, "Failed to close inputChannel.");
    372                     }
    373                 }
    374                 if (outputChannel != null) {
    375                     try {
    376                         outputChannel.close();
    377                     } catch(IOException e) {
    378                         Log.w(LOG_TAG, "Failed to close outputChannel");
    379                     }
    380                 }
    381             }
    382             return destUri;
    383         }
    384 
    385         /**
    386          * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
    387          * its content.
    388          *
    389          * @arg localDataUri Uri actually used for the import. Should be stored in
    390          * app local storage, as we cannot guarantee other types of Uris can be read
    391          * multiple times. This variable populates {@link ImportRequest#uri}.
    392          * @arg displayName Used for displaying information to the user. This variable populates
    393          * {@link ImportRequest#displayName}.
    394          */
    395         private ImportRequest constructImportRequest(final byte[] data,
    396                 final Uri localDataUri, final String displayName)
    397                 throws IOException, VCardException {
    398             final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
    399             VCardEntryCounter counter = null;
    400             VCardSourceDetector detector = null;
    401             int vcardVersion = VCARD_VERSION_V21;
    402             try {
    403                 boolean shouldUseV30 = false;
    404                 InputStream is;
    405                 if (data != null) {
    406                     is = new ByteArrayInputStream(data);
    407                 } else {
    408                     is = resolver.openInputStream(localDataUri);
    409                 }
    410                 mVCardParser = new VCardParser_V21();
    411                 try {
    412                     counter = new VCardEntryCounter();
    413                     detector = new VCardSourceDetector();
    414                     mVCardParser.addInterpreter(counter);
    415                     mVCardParser.addInterpreter(detector);
    416                     mVCardParser.parse(is);
    417                 } catch (VCardVersionException e1) {
    418                     try {
    419                         is.close();
    420                     } catch (IOException e) {
    421                     }
    422 
    423                     shouldUseV30 = true;
    424                     if (data != null) {
    425                         is = new ByteArrayInputStream(data);
    426                     } else {
    427                         is = resolver.openInputStream(localDataUri);
    428                     }
    429                     mVCardParser = new VCardParser_V30();
    430                     try {
    431                         counter = new VCardEntryCounter();
    432                         detector = new VCardSourceDetector();
    433                         mVCardParser.addInterpreter(counter);
    434                         mVCardParser.addInterpreter(detector);
    435                         mVCardParser.parse(is);
    436                     } catch (VCardVersionException e2) {
    437                         throw new VCardException("vCard with unspported version.");
    438                     }
    439                 } finally {
    440                     if (is != null) {
    441                         try {
    442                             is.close();
    443                         } catch (IOException e) {
    444                         }
    445                     }
    446                 }
    447 
    448                 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
    449             } catch (VCardNestedException e) {
    450                 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
    451                 // Go through without throwing the Exception, as we may be able to detect the
    452                 // version before it
    453             }
    454             return new ImportRequest(mAccount,
    455                     data, localDataUri, displayName,
    456                     detector.getEstimatedType(),
    457                     detector.getEstimatedCharset(),
    458                     vcardVersion, counter.getCount());
    459         }
    460 
    461         public Uri[] getSourceUris() {
    462             return mSourceUris;
    463         }
    464 
    465         public void cancel() {
    466             mCanceled = true;
    467             if (mVCardParser != null) {
    468                 mVCardParser.cancel();
    469             }
    470         }
    471 
    472         @Override
    473         public void onCancel(DialogInterface dialog) {
    474             Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
    475             cancel();
    476         }
    477     }
    478 
    479     private void importVCard(final Uri uri) {
    480         importVCard(new Uri[] {uri});
    481     }
    482 
    483     private void importVCard(final Uri[] uris) {
    484         runOnUiThread(new Runnable() {
    485             @Override
    486             public void run() {
    487                 if (!isFinishing()) {
    488                     mVCardCacheThread = new VCardCacheThread(uris);
    489                     mListener = new NotificationImportExportListener(ImportVCardActivity.this);
    490                     showDialog(R.id.dialog_cache_vcard);
    491                 }
    492             }
    493         });
    494     }
    495 
    496     @Override
    497     protected void onCreate(Bundle bundle) {
    498         super.onCreate(bundle);
    499 
    500         if (RequestImportVCardPermissionsActivity.startPermissionActivity(this)) {
    501             return;
    502         }
    503 
    504         String accountName = null;
    505         String accountType = null;
    506         String dataSet = null;
    507         final Intent intent = getIntent();
    508         if (intent != null) {
    509             accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
    510             accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
    511             dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
    512         } else {
    513             Log.e(LOG_TAG, "intent does not exist");
    514         }
    515 
    516         if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
    517             mAccount = new AccountWithDataSet(accountName, accountType, dataSet);
    518         } else {
    519             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
    520             final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
    521             if (accountList.size() == 0) {
    522                 mAccount = null;
    523             } else if (accountList.size() == 1) {
    524                 mAccount = accountList.get(0);
    525             } else {
    526                 startActivityForResult(new Intent(this, SelectAccountActivity.class),
    527                         SELECT_ACCOUNT);
    528                 return;
    529             }
    530         }
    531 
    532         startImport();
    533     }
    534 
    535     @Override
    536     public void onActivityResult(int requestCode, int resultCode, Intent intent) {
    537         if (requestCode == SELECT_ACCOUNT) {
    538             if (resultCode == Activity.RESULT_OK) {
    539                 mAccount = new AccountWithDataSet(
    540                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
    541                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
    542                         intent.getStringExtra(SelectAccountActivity.DATA_SET));
    543                 startImport();
    544             } else {
    545                 if (resultCode != Activity.RESULT_CANCELED) {
    546                     Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
    547                 }
    548                 finish();
    549             }
    550         } else if (requestCode == REQUEST_OPEN_DOCUMENT) {
    551             if (resultCode == Activity.RESULT_OK) {
    552                 final ClipData clipData = intent.getClipData();
    553                 if (clipData != null) {
    554                     final ArrayList<Uri> uris = new ArrayList<>();
    555                     for (int i = 0; i < clipData.getItemCount(); i++) {
    556                         ClipData.Item item = clipData.getItemAt(i);
    557                         final Uri uri = item.getUri();
    558                         if (uri != null) {
    559                             uris.add(uri);
    560                         }
    561                     }
    562                     if (uris.isEmpty()) {
    563                         Log.w(LOG_TAG, "No vCard was selected for import");
    564                         finish();
    565                     } else {
    566                         Log.i(LOG_TAG, "Multiple vCards selected for import: " + uris);
    567                         importVCard(uris.toArray(new Uri[0]));
    568                     }
    569                 } else {
    570                     final Uri uri = intent.getData();
    571                     if (uri != null) {
    572                         Log.i(LOG_TAG, "vCard selected for import: " + uri);
    573                         importVCard(uri);
    574                     } else {
    575                         Log.w(LOG_TAG, "No vCard was selected for import");
    576                         finish();
    577                     }
    578                 }
    579             } else {
    580                 if (resultCode != Activity.RESULT_CANCELED) {
    581                     Log.w(LOG_TAG, "Result code was not OK nor CANCELED" + resultCode);
    582                 }
    583                 finish();
    584             }
    585         }
    586     }
    587 
    588     private void startImport() {
    589         // Handle inbound files
    590         Uri uri = getIntent().getData();
    591         if (uri != null) {
    592             Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
    593             importVCard(uri);
    594         } else {
    595             Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
    596             final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    597             intent.addCategory(Intent.CATEGORY_OPENABLE);
    598             intent.setType(VCardService.X_VCARD_MIME_TYPE);
    599             intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
    600             intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
    601             startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
    602         }
    603     }
    604 
    605     @Override
    606     protected Dialog onCreateDialog(int resId, Bundle bundle) {
    607         switch (resId) {
    608             case R.id.dialog_cache_vcard: {
    609                 if (mProgressDialogForCachingVCard == null) {
    610                     final String title = getString(R.string.caching_vcard_title);
    611                     final String message = getString(R.string.caching_vcard_message);
    612                     mProgressDialogForCachingVCard = new ProgressDialog(this);
    613                     mProgressDialogForCachingVCard.setTitle(title);
    614                     mProgressDialogForCachingVCard.setMessage(message);
    615                     mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
    616                     mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
    617                     startVCardService();
    618                 }
    619                 return mProgressDialogForCachingVCard;
    620             }
    621             case R.id.dialog_error_with_message: {
    622                 String message = mErrorMessage;
    623                 if (TextUtils.isEmpty(message)) {
    624                     Log.e(LOG_TAG, "Error message is null while it must not.");
    625                     message = getString(R.string.fail_reason_unknown);
    626                 }
    627                 final AlertDialog.Builder builder = new AlertDialog.Builder(this)
    628                     .setTitle(getString(R.string.reading_vcard_failed_title))
    629                     .setIconAttribute(android.R.attr.alertDialogIcon)
    630                     .setMessage(message)
    631                     .setOnCancelListener(mCancelListener)
    632                     .setPositiveButton(android.R.string.ok, mCancelListener);
    633                 return builder.create();
    634             }
    635         }
    636 
    637         return super.onCreateDialog(resId, bundle);
    638     }
    639 
    640     /* package */ void startVCardService() {
    641         mConnection = new ImportRequestConnection();
    642 
    643         Log.i(LOG_TAG, "Bind to VCardService.");
    644         // We don't want the service finishes itself just after this connection.
    645         Intent intent = new Intent(this, VCardService.class);
    646         startService(intent);
    647         bindService(new Intent(this, VCardService.class),
    648                 mConnection, Context.BIND_AUTO_CREATE);
    649     }
    650 
    651     @Override
    652     protected void onRestoreInstanceState(Bundle savedInstanceState) {
    653         super.onRestoreInstanceState(savedInstanceState);
    654         if (mProgressDialogForCachingVCard != null) {
    655             Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
    656             showDialog(R.id.dialog_cache_vcard);
    657         }
    658     }
    659 
    660     /* package */ void showFailureNotification(int reasonId) {
    661         final NotificationManager notificationManager =
    662                 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
    663         final Notification notification =
    664                 NotificationImportExportListener.constructImportFailureNotification(
    665                         ImportVCardActivity.this,
    666                         getString(reasonId));
    667         notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
    668                 FAILURE_NOTIFICATION_ID, notification);
    669         mHandler.post(new Runnable() {
    670             @Override
    671             public void run() {
    672                 Toast.makeText(ImportVCardActivity.this,
    673                         getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
    674             }
    675         });
    676     }
    677 }
    678