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 static final String LOCAL_TMP_FILE_NAME_EXTRA =
94             "com.android.contacts.common.vcard.LOCAL_TMP_FILE_NAME";
95 
96     private static final String SOURCE_URI_DISPLAY_NAME =
97             "com.android.contacts.common.vcard.SOURCE_URI_DISPLAY_NAME";
98 
99     private static final String STORAGE_VCARD_URI_PREFIX = "file:///storage";
100 
101     private AccountWithDataSet mAccount;
102 
103     private ProgressDialog mProgressDialogForCachingVCard;
104 
105     private VCardCacheThread mVCardCacheThread;
106     private ImportRequestConnection mConnection;
107     /* package */ VCardImportExportListener mListener;
108 
109     private String mErrorMessage;
110 
111     private Handler mHandler = new Handler();
112 
113     // Runs on the UI thread.
114     private class DialogDisplayer implements Runnable {
115         private final int mResId;
DialogDisplayer(int resId)116         public DialogDisplayer(int resId) {
117             mResId = resId;
118         }
DialogDisplayer(String errorMessage)119         public DialogDisplayer(String errorMessage) {
120             mResId = R.id.dialog_error_with_message;
121             mErrorMessage = errorMessage;
122         }
123         @Override
run()124         public void run() {
125             if (!isFinishing()) {
126                 showDialog(mResId);
127             }
128         }
129     }
130 
131     private class CancelListener
132         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
133         @Override
onClick(DialogInterface dialog, int which)134         public void onClick(DialogInterface dialog, int which) {
135             finish();
136         }
137         @Override
onCancel(DialogInterface dialog)138         public void onCancel(DialogInterface dialog) {
139             finish();
140         }
141     }
142 
143     private CancelListener mCancelListener = new CancelListener();
144 
145     private class ImportRequestConnection implements ServiceConnection {
146         private VCardService mService;
147 
sendImportRequest(final List<ImportRequest> requests)148         public void sendImportRequest(final List<ImportRequest> requests) {
149             Log.i(LOG_TAG, "Send an import request");
150             mService.handleImportRequest(requests, mListener);
151         }
152 
153         @Override
onServiceConnected(ComponentName name, IBinder binder)154         public void onServiceConnected(ComponentName name, IBinder binder) {
155             mService = ((VCardService.MyBinder) binder).getService();
156             Log.i(LOG_TAG,
157                     String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
158                             Arrays.toString(mVCardCacheThread.getSourceUris())));
159             mVCardCacheThread.start();
160         }
161 
162         @Override
onServiceDisconnected(ComponentName name)163         public void onServiceDisconnected(ComponentName name) {
164             Log.i(LOG_TAG, "Disconnected from VCardService");
165         }
166     }
167 
168     /**
169      * Caches given vCard files into a local directory, and sends actual import request to
170      * {@link VCardService}.
171      *
172      * We need to cache given files into local storage. One of reasons is that some data (as Uri)
173      * may have special permissions. Callers may allow only this Activity to access that content,
174      * not what this Activity launched (like {@link VCardService}).
175      */
176     private class VCardCacheThread extends Thread
177             implements DialogInterface.OnCancelListener {
178         private boolean mCanceled;
179         private PowerManager.WakeLock mWakeLock;
180         private VCardParser mVCardParser;
181         private final Uri[] mSourceUris;  // Given from a caller.
182         private final String[] mSourceDisplayNames; // Display names for each Uri in mSourceUris.
183         private final byte[] mSource;
184         private final String mDisplayName;
185 
VCardCacheThread(final Uri[] sourceUris, String[] sourceDisplayNames)186         public VCardCacheThread(final Uri[] sourceUris, String[] sourceDisplayNames) {
187             mSourceUris = sourceUris;
188             mSourceDisplayNames = sourceDisplayNames;
189             mSource = null;
190             final Context context = ImportVCardActivity.this;
191             final PowerManager powerManager =
192                     (PowerManager)context.getSystemService(Context.POWER_SERVICE);
193             mWakeLock = powerManager.newWakeLock(
194                     PowerManager.SCREEN_DIM_WAKE_LOCK |
195                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
196             mDisplayName = null;
197         }
198 
199         @Override
finalize()200         public void finalize() {
201             if (mWakeLock != null && mWakeLock.isHeld()) {
202                 Log.w(LOG_TAG, "WakeLock is being held.");
203                 mWakeLock.release();
204             }
205         }
206 
207         @Override
run()208         public void run() {
209             Log.i(LOG_TAG, "vCard cache thread starts running.");
210             if (mConnection == null) {
211                 throw new NullPointerException("vCard cache thread must be launched "
212                         + "after a service connection is established");
213             }
214 
215             mWakeLock.acquire();
216             try {
217                 if (mCanceled == true) {
218                     Log.i(LOG_TAG, "vCard cache operation is canceled.");
219                     return;
220                 }
221 
222                 final Context context = ImportVCardActivity.this;
223                 // Uris given from caller applications may not be opened twice: consider when
224                 // it is not from local storage (e.g. "file:///...") but from some special
225                 // provider (e.g. "content://...").
226                 // Thus we have to once copy the content of Uri into local storage, and read
227                 // it after it.
228                 //
229                 // We may be able to read content of each vCard file during copying them
230                 // to local storage, but currently vCard code does not allow us to do so.
231                 int cache_index = 0;
232                 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
233                 if (mSource != null) {
234                     try {
235                         requests.add(constructImportRequest(mSource, null, mDisplayName));
236                     } catch (VCardException e) {
237                         Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
238                         showFailureNotification(R.string.fail_reason_not_supported);
239                         return;
240                     }
241                 } else {
242                     int i = 0;
243                     for (Uri sourceUri : mSourceUris) {
244                         if (mCanceled) {
245                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
246                             break;
247                         }
248 
249                         String sourceDisplayName = mSourceDisplayNames[i++];
250 
251                         final ImportRequest request;
252                         try {
253                             request = constructImportRequest(null, sourceUri, sourceDisplayName);
254                         } catch (VCardException e) {
255                             Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
256                             showFailureNotification(R.string.fail_reason_not_supported);
257                             return;
258                         } catch (IOException e) {
259                             Log.e(LOG_TAG, "Unexpected IOException", e);
260                             showFailureNotification(R.string.fail_reason_io_error);
261                             return;
262                         }
263                         if (mCanceled) {
264                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
265                             return;
266                         }
267                         requests.add(request);
268                     }
269                 }
270                 if (!requests.isEmpty()) {
271                     mConnection.sendImportRequest(requests);
272                 } else {
273                     Log.w(LOG_TAG, "Empty import requests. Ignore it.");
274                 }
275             } catch (OutOfMemoryError e) {
276                 Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
277                 System.gc();
278                 runOnUiThread(new DialogDisplayer(
279                         getString(R.string.fail_reason_low_memory_during_import)));
280             } catch (IOException e) {
281                 Log.e(LOG_TAG, "IOException during caching vCard", e);
282                 runOnUiThread(new DialogDisplayer(
283                         getString(R.string.fail_reason_io_error)));
284             } finally {
285                 Log.i(LOG_TAG, "Finished caching vCard.");
286                 mWakeLock.release();
287                 unbindService(mConnection);
288                 mProgressDialogForCachingVCard.dismiss();
289                 mProgressDialogForCachingVCard = null;
290                 finish();
291             }
292         }
293 
294         /**
295          * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
296          * its content.
297          *
298          * @arg localDataUri Uri actually used for the import. Should be stored in
299          * app local storage, as we cannot guarantee other types of Uris can be read
300          * multiple times. This variable populates {@link ImportRequest#uri}.
301          * @arg displayName Used for displaying information to the user. This variable populates
302          * {@link ImportRequest#displayName}.
303          */
constructImportRequest(final byte[] data, final Uri localDataUri, final String displayName)304         private ImportRequest constructImportRequest(final byte[] data,
305                 final Uri localDataUri, final String displayName)
306                 throws IOException, VCardException {
307             final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
308             VCardEntryCounter counter = null;
309             VCardSourceDetector detector = null;
310             int vcardVersion = VCARD_VERSION_V21;
311             try {
312                 boolean shouldUseV30 = false;
313                 InputStream is;
314                 if (data != null) {
315                     is = new ByteArrayInputStream(data);
316                 } else {
317                     is = resolver.openInputStream(localDataUri);
318                 }
319                 mVCardParser = new VCardParser_V21();
320                 try {
321                     counter = new VCardEntryCounter();
322                     detector = new VCardSourceDetector();
323                     mVCardParser.addInterpreter(counter);
324                     mVCardParser.addInterpreter(detector);
325                     mVCardParser.parse(is);
326                 } catch (VCardVersionException e1) {
327                     try {
328                         is.close();
329                     } catch (IOException e) {
330                     }
331 
332                     shouldUseV30 = true;
333                     if (data != null) {
334                         is = new ByteArrayInputStream(data);
335                     } else {
336                         is = resolver.openInputStream(localDataUri);
337                     }
338                     mVCardParser = new VCardParser_V30();
339                     try {
340                         counter = new VCardEntryCounter();
341                         detector = new VCardSourceDetector();
342                         mVCardParser.addInterpreter(counter);
343                         mVCardParser.addInterpreter(detector);
344                         mVCardParser.parse(is);
345                     } catch (VCardVersionException e2) {
346                         throw new VCardException("vCard with unspported version.");
347                     }
348                 } finally {
349                     if (is != null) {
350                         try {
351                             is.close();
352                         } catch (IOException e) {
353                         }
354                     }
355                 }
356 
357                 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
358             } catch (VCardNestedException e) {
359                 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
360                 // Go through without throwing the Exception, as we may be able to detect the
361                 // version before it
362             }
363             return new ImportRequest(mAccount,
364                     data, localDataUri, displayName,
365                     detector.getEstimatedType(),
366                     detector.getEstimatedCharset(),
367                     vcardVersion, counter.getCount());
368         }
369 
getSourceUris()370         public Uri[] getSourceUris() {
371             return mSourceUris;
372         }
373 
cancel()374         public void cancel() {
375             mCanceled = true;
376             if (mVCardParser != null) {
377                 mVCardParser.cancel();
378             }
379         }
380 
381         @Override
onCancel(DialogInterface dialog)382         public void onCancel(DialogInterface dialog) {
383             Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
384             cancel();
385         }
386     }
387 
importVCard(final Uri uri, final String sourceDisplayName)388     private void importVCard(final Uri uri, final String sourceDisplayName) {
389         importVCard(new Uri[] {uri}, new String[] {sourceDisplayName});
390     }
391 
importVCard(final Uri[] uris, final String[] sourceDisplayNames)392     private void importVCard(final Uri[] uris, final String[] sourceDisplayNames) {
393         runOnUiThread(new Runnable() {
394             @Override
395             public void run() {
396                 if (!isFinishing()) {
397                     mVCardCacheThread = new VCardCacheThread(uris, sourceDisplayNames);
398                     mListener = new NotificationImportExportListener(ImportVCardActivity.this);
399                     showDialog(R.id.dialog_cache_vcard);
400                 }
401             }
402         });
403     }
404 
getDisplayName(Uri sourceUri)405     private String getDisplayName(Uri sourceUri) {
406         if (sourceUri == null) {
407             return null;
408         }
409         final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
410         String displayName = null;
411         Cursor cursor = null;
412         // Try to get a display name from the given Uri. If it fails, we just
413         // pick up the last part of the Uri.
414         try {
415             cursor = resolver.query(sourceUri,
416                     new String[] { OpenableColumns.DISPLAY_NAME },
417                     null, null, null);
418             if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
419                 if (cursor.getCount() > 1) {
420                     Log.w(LOG_TAG, "Unexpected multiple rows: "
421                             + cursor.getCount());
422                 }
423                 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
424                 if (index >= 0) {
425                     displayName = cursor.getString(index);
426                 }
427             }
428         } finally {
429             if (cursor != null) {
430                 cursor.close();
431             }
432         }
433         if (TextUtils.isEmpty(displayName)){
434             displayName = sourceUri.getLastPathSegment();
435         }
436         return displayName;
437     }
438 
439     /**
440      * Copy the content of sourceUri to the destination.
441      */
copyTo(final Uri sourceUri, String filename)442     private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
443         Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
444                 sourceUri, filename));
445         final Context context = ImportVCardActivity.this;
446         final ContentResolver resolver = context.getContentResolver();
447         ReadableByteChannel inputChannel = null;
448         WritableByteChannel outputChannel = null;
449         Uri destUri = null;
450         try {
451             inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
452             destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
453             outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
454             final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
455             while (inputChannel.read(buffer) != -1) {
456                 buffer.flip();
457                 outputChannel.write(buffer);
458                 buffer.compact();
459             }
460             buffer.flip();
461             while (buffer.hasRemaining()) {
462                 outputChannel.write(buffer);
463             }
464         } finally {
465             if (inputChannel != null) {
466                 try {
467                     inputChannel.close();
468                 } catch (IOException e) {
469                     Log.w(LOG_TAG, "Failed to close inputChannel.");
470                 }
471             }
472             if (outputChannel != null) {
473                 try {
474                     outputChannel.close();
475                 } catch(IOException e) {
476                     Log.w(LOG_TAG, "Failed to close outputChannel");
477                 }
478             }
479         }
480         return destUri;
481     }
482 
483     /**
484      * Reads the file from {@param sourceUri} and copies it to local cache file.
485      * Returns the local file name which stores the file from sourceUri.
486      */
readUriToLocalFile(Uri sourceUri)487     private String readUriToLocalFile(Uri sourceUri) {
488         // Read the uri to local first.
489         int cache_index = 0;
490         String localFilename = null;
491         // Note: caches are removed by VCardService.
492         while (true) {
493             localFilename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
494             final File file = getFileStreamPath(localFilename);
495             if (!file.exists()) {
496                 break;
497             } else {
498                 if (cache_index == Integer.MAX_VALUE) {
499                     throw new RuntimeException("Exceeded cache limit");
500                 }
501                 cache_index++;
502             }
503         }
504         try {
505             copyTo(sourceUri, localFilename);
506         } catch (SecurityException e) {
507             Log.e(LOG_TAG, "SecurityException", e);
508             showFailureNotification(R.string.fail_reason_io_error);
509             return null;
510         } catch (IOException e) {
511             Log.e(LOG_TAG, "IOException during caching vCard", e);
512             showFailureNotification(R.string.fail_reason_io_error);
513             return null;
514         }
515 
516         if (localFilename == null) {
517             Log.e(LOG_TAG, "Cannot load uri to local storage.");
518             showFailureNotification(R.string.fail_reason_io_error);
519             return null;
520         }
521 
522         return localFilename;
523     }
524 
readUriToLocalUri(Uri sourceUri)525     private Uri readUriToLocalUri(Uri sourceUri) {
526         final String fileName = readUriToLocalFile(sourceUri);
527         if (fileName == null) {
528             return null;
529         }
530         return Uri.parse(getFileStreamPath(fileName).toURI().toString());
531     }
532 
533     // Returns true if uri is from Storage.
isStorageUri(Uri uri)534     private boolean isStorageUri(Uri uri) {
535         return uri != null && uri.toString().startsWith(STORAGE_VCARD_URI_PREFIX);
536     }
537 
538     @Override
onCreate(Bundle bundle)539     protected void onCreate(Bundle bundle) {
540         super.onCreate(bundle);
541 
542         Uri sourceUri = getIntent().getData();
543 
544         // Reading uris from non-storage needs the permission granted from the source intent,
545         // instead of permissions from RequestImportVCardPermissionActivity. So skipping requesting
546         // permissions from RequestImportVCardPermissionActivity for uris from non-storage source.
547         if (isStorageUri(sourceUri)
548                 && RequestImportVCardPermissionsActivity.startPermissionActivity(this)) {
549             return;
550         }
551 
552         String sourceDisplayName = null;
553         if (sourceUri != null) {
554             // Read the uri to local first.
555             String localTmpFileName = getIntent().getStringExtra(LOCAL_TMP_FILE_NAME_EXTRA);
556             sourceDisplayName = getIntent().getStringExtra(SOURCE_URI_DISPLAY_NAME);
557             if (TextUtils.isEmpty(localTmpFileName)) {
558                 localTmpFileName = readUriToLocalFile(sourceUri);
559                 sourceDisplayName = getDisplayName(sourceUri);
560                 if (localTmpFileName == null) {
561                     Log.e(LOG_TAG, "Cannot load uri to local storage.");
562                     showFailureNotification(R.string.fail_reason_io_error);
563                     return;
564                 }
565                 getIntent().putExtra(LOCAL_TMP_FILE_NAME_EXTRA, localTmpFileName);
566                 getIntent().putExtra(SOURCE_URI_DISPLAY_NAME, sourceDisplayName);
567             }
568             sourceUri = Uri.parse(getFileStreamPath(localTmpFileName).toURI().toString());
569         }
570 
571         // Always request required permission for contacts before importing the vcard.
572         if (RequestImportVCardPermissionsActivity.startPermissionActivity(this)) {
573             return;
574         }
575 
576         String accountName = null;
577         String accountType = null;
578         String dataSet = null;
579         final Intent intent = getIntent();
580         if (intent != null) {
581             accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
582             accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
583             dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
584         } else {
585             Log.e(LOG_TAG, "intent does not exist");
586         }
587 
588         if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
589             mAccount = new AccountWithDataSet(accountName, accountType, dataSet);
590         } else {
591             final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
592             final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
593             if (accountList.size() == 0) {
594                 mAccount = null;
595             } else if (accountList.size() == 1) {
596                 mAccount = accountList.get(0);
597             } else {
598                 startActivityForResult(new Intent(this, SelectAccountActivity.class),
599                         SELECT_ACCOUNT);
600                 return;
601             }
602         }
603 
604         startImport(sourceUri, sourceDisplayName);
605     }
606 
607     @Override
onActivityResult(int requestCode, int resultCode, Intent intent)608     public void onActivityResult(int requestCode, int resultCode, Intent intent) {
609         if (requestCode == SELECT_ACCOUNT) {
610             if (resultCode == Activity.RESULT_OK) {
611                 mAccount = new AccountWithDataSet(
612                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
613                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
614                         intent.getStringExtra(SelectAccountActivity.DATA_SET));
615                 final Uri sourceUri = getIntent().getData();
616                 if (sourceUri == null) {
617                     startImport(sourceUri, /* sourceDisplayName =*/ null);
618                 } else {
619                     final String sourceDisplayName = getIntent().getStringExtra(
620                             SOURCE_URI_DISPLAY_NAME);
621                     final String localFileName = getIntent().getStringExtra(
622                             LOCAL_TMP_FILE_NAME_EXTRA);
623                     final Uri localUri = Uri.parse(
624                             getFileStreamPath(localFileName).toURI().toString());
625                     startImport(localUri, sourceDisplayName);
626                 }
627             } else {
628                 if (resultCode != Activity.RESULT_CANCELED) {
629                     Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
630                 }
631                 finish();
632             }
633         } else if (requestCode == REQUEST_OPEN_DOCUMENT) {
634             if (resultCode == Activity.RESULT_OK) {
635                 final ClipData clipData = intent.getClipData();
636                 if (clipData != null) {
637                     final ArrayList<Uri> uris = new ArrayList<>();
638                     final ArrayList<String> sourceDisplayNames = new ArrayList<>();
639                     for (int i = 0; i < clipData.getItemCount(); i++) {
640                         ClipData.Item item = clipData.getItemAt(i);
641                         final Uri uri = item.getUri();
642                         if (uri != null) {
643                             final Uri localUri = readUriToLocalUri(uri);
644                             if (localUri != null) {
645                                 final String sourceDisplayName = getDisplayName(uri);
646                                 uris.add(localUri);
647                                 sourceDisplayNames.add(sourceDisplayName);
648                             }
649                         }
650                     }
651                     if (uris.isEmpty()) {
652                         Log.w(LOG_TAG, "No vCard was selected for import");
653                         finish();
654                     } else {
655                         Log.i(LOG_TAG, "Multiple vCards selected for import: " + uris);
656                         importVCard(uris.toArray(new Uri[0]),
657                                 sourceDisplayNames.toArray(new String[0]));
658                     }
659                 } else {
660                     final Uri uri = intent.getData();
661                     if (uri != null) {
662                         Log.i(LOG_TAG, "vCard selected for import: " + uri);
663                         final Uri localUri = readUriToLocalUri(uri);
664                         if (localUri != null) {
665                             final String sourceDisplayName = getDisplayName(uri);
666                             importVCard(localUri, sourceDisplayName);
667                         } else {
668                             Log.w(LOG_TAG, "No local URI for vCard import");
669                             finish();
670                         }
671                     } else {
672                         Log.w(LOG_TAG, "No vCard was selected for import");
673                         finish();
674                     }
675                 }
676             } else {
677                 if (resultCode != Activity.RESULT_CANCELED) {
678                     Log.w(LOG_TAG, "Result code was not OK nor CANCELED" + resultCode);
679                 }
680                 finish();
681             }
682         }
683     }
684 
startImport(Uri uri, String sourceDisplayName)685     private void startImport(Uri uri, String sourceDisplayName) {
686         // Handle inbound files
687         if (uri != null) {
688             Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
689             importVCard(uri, sourceDisplayName);
690         } else {
691             Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
692             final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
693             intent.addCategory(Intent.CATEGORY_OPENABLE);
694             intent.setType(VCardService.X_VCARD_MIME_TYPE);
695             intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
696             startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
697         }
698     }
699 
700     @Override
onCreateDialog(int resId, Bundle bundle)701     protected Dialog onCreateDialog(int resId, Bundle bundle) {
702         if (resId == R.id.dialog_cache_vcard) {
703             if (mProgressDialogForCachingVCard == null) {
704                 final String title = getString(R.string.caching_vcard_title);
705                 final String message = getString(R.string.caching_vcard_message);
706                 mProgressDialogForCachingVCard = new ProgressDialog(this);
707                 mProgressDialogForCachingVCard.setTitle(title);
708                 mProgressDialogForCachingVCard.setMessage(message);
709                 mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
710                 mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
711                 startVCardService();
712             }
713             return mProgressDialogForCachingVCard;
714         } else if (resId == R.id.dialog_error_with_message) {
715             String message = mErrorMessage;
716             if (TextUtils.isEmpty(message)) {
717                 Log.e(LOG_TAG, "Error message is null while it must not.");
718                 message = getString(R.string.fail_reason_unknown);
719             }
720             final AlertDialog.Builder builder = new AlertDialog.Builder(this)
721                 .setTitle(getString(R.string.reading_vcard_failed_title))
722                 .setIconAttribute(android.R.attr.alertDialogIcon)
723                 .setMessage(message)
724                 .setOnCancelListener(mCancelListener)
725                 .setPositiveButton(android.R.string.ok, mCancelListener);
726             return builder.create();
727         }
728 
729         return super.onCreateDialog(resId, bundle);
730     }
731 
startVCardService()732     /* package */ void startVCardService() {
733         mConnection = new ImportRequestConnection();
734 
735         Log.i(LOG_TAG, "Bind to VCardService.");
736         // We don't want the service finishes itself just after this connection.
737         Intent intent = new Intent(this, VCardService.class);
738         startService(intent);
739         bindService(new Intent(this, VCardService.class),
740                 mConnection, Context.BIND_AUTO_CREATE);
741     }
742 
743     @Override
onRestoreInstanceState(Bundle savedInstanceState)744     protected void onRestoreInstanceState(Bundle savedInstanceState) {
745         super.onRestoreInstanceState(savedInstanceState);
746         if (mProgressDialogForCachingVCard != null) {
747             Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
748             showDialog(R.id.dialog_cache_vcard);
749         }
750     }
751 
showFailureNotification(int reasonId)752     /* package */ void showFailureNotification(int reasonId) {
753         final NotificationManager notificationManager =
754                 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
755         final Notification notification =
756                 NotificationImportExportListener.constructImportFailureNotification(
757                         ImportVCardActivity.this,
758                         getString(reasonId));
759         notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
760                 FAILURE_NOTIFICATION_ID, notification);
761         mHandler.post(new Runnable() {
762             @Override
763             public void run() {
764                 Toast.makeText(ImportVCardActivity.this,
765                         getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
766             }
767         });
768     }
769 }
770