• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
DialogDisplayer(int resId)108         public DialogDisplayer(int resId) {
109             mResId = resId;
110         }
DialogDisplayer(String errorMessage)111         public DialogDisplayer(String errorMessage) {
112             mResId = R.id.dialog_error_with_message;
113             mErrorMessage = errorMessage;
114         }
115         @Override
run()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
onClick(DialogInterface dialog, int which)126         public void onClick(DialogInterface dialog, int which) {
127             finish();
128         }
129         @Override
onCancel(DialogInterface dialog)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 
sendImportRequest(final List<ImportRequest> requests)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
onServiceConnected(ComponentName name, IBinder binder)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
onServiceDisconnected(ComponentName name)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 
VCardCacheThread(final Uri[] sourceUris)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
finalize()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
run()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          */
copyTo(final Uri sourceUri, String filename)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          */
constructImportRequest(final byte[] data, final Uri localDataUri, final String displayName)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 
getSourceUris()461         public Uri[] getSourceUris() {
462             return mSourceUris;
463         }
464 
cancel()465         public void cancel() {
466             mCanceled = true;
467             if (mVCardParser != null) {
468                 mVCardParser.cancel();
469             }
470         }
471 
472         @Override
onCancel(DialogInterface dialog)473         public void onCancel(DialogInterface dialog) {
474             Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
475             cancel();
476         }
477     }
478 
importVCard(final Uri uri)479     private void importVCard(final Uri uri) {
480         importVCard(new Uri[] {uri});
481     }
482 
importVCard(final Uri[] uris)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
onCreate(Bundle bundle)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
onActivityResult(int requestCode, int resultCode, Intent intent)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 
startImport()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
onCreateDialog(int resId, Bundle bundle)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 
startVCardService()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
onRestoreInstanceState(Bundle savedInstanceState)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 
showFailureNotification(int reasonId)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