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