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.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.R;
44 import com.android.contacts.activities.RequestImportVCardPermissionsActivity;
45 import com.android.contacts.model.AccountTypeManager;
46 import com.android.contacts.model.account.AccountWithDataSet;
47 import com.android.contactsbind.FeedbackHelper;
48 import com.android.vcard.VCardEntryCounter;
49 import com.android.vcard.VCardParser;
50 import com.android.vcard.VCardParser_V21;
51 import com.android.vcard.VCardParser_V30;
52 import com.android.vcard.VCardSourceDetector;
53 import com.android.vcard.exception.VCardException;
54 import com.android.vcard.exception.VCardNestedException;
55 import com.android.vcard.exception.VCardVersionException;
56 
57 import java.io.ByteArrayInputStream;
58 import java.io.File;
59 import java.io.IOException;
60 import java.io.InputStream;
61 import java.nio.ByteBuffer;
62 import java.nio.channels.Channels;
63 import java.nio.channels.ReadableByteChannel;
64 import java.nio.channels.WritableByteChannel;
65 import java.util.ArrayList;
66 import java.util.Arrays;
67 import 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  */
78 public 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;
DialogDisplayer(int resId)117         public DialogDisplayer(int resId) {
118             mResId = resId;
119         }
DialogDisplayer(String errorMessage)120         public DialogDisplayer(String errorMessage) {
121             mResId = R.id.dialog_error_with_message;
122             mErrorMessage = errorMessage;
123         }
124         @Override
run()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
onClick(DialogInterface dialog, int which)135         public void onClick(DialogInterface dialog, int which) {
136             finish();
137         }
138         @Override
onCancel(DialogInterface dialog)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 
sendImportRequest(final List<ImportRequest> requests)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
onServiceConnected(ComponentName name, IBinder binder)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
onServiceDisconnected(ComponentName name)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 
VCardCacheThread(final Uri[] sourceUris, String[] sourceDisplayNames)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
finalize()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
run()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          */
constructImportRequest(final byte[] data, final Uri localDataUri, final String displayName)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 
getSourceUris()381         public Uri[] getSourceUris() {
382             return mSourceUris;
383         }
384 
cancel()385         public void cancel() {
386             mCanceled = true;
387             if (mVCardParser != null) {
388                 mVCardParser.cancel();
389             }
390         }
391 
392         @Override
onCancel(DialogInterface dialog)393         public void onCancel(DialogInterface dialog) {
394             Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
395             cancel();
396         }
397     }
398 
importVCard(final Uri uri, final String sourceDisplayName)399     private void importVCard(final Uri uri, final String sourceDisplayName) {
400         importVCard(new Uri[] {uri}, new String[] {sourceDisplayName});
401     }
402 
importVCard(final Uri[] uris, final String[] sourceDisplayNames)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 
getDisplayName(Uri sourceUri)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      */
copyTo(final Uri sourceUri, String filename)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      */
readUriToLocalFile(Uri sourceUri)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 
readUriToLocalUri(Uri sourceUri)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.
isStorageUri(Uri uri)541     private boolean isStorageUri(Uri uri) {
542         return uri != null && uri.toString().startsWith(STORAGE_VCARD_URI_PREFIX);
543     }
544 
545     @Override
onCreate(Bundle bundle)546     protected void onCreate(Bundle bundle) {
547         super.onCreate(bundle);
548 
549         getWindow().setHideOverlayWindows(true);
550 
551         Uri sourceUri = getIntent().getData();
552 
553         // Reading uris from non-storage needs the permission granted from the source intent,
554         // instead of permissions from RequestImportVCardPermissionActivity. So skipping requesting
555         // permissions from RequestImportVCardPermissionActivity for uris from non-storage source.
556         if (isStorageUri(sourceUri) && RequestImportVCardPermissionsActivity
557                 .startPermissionActivity(this, isCallerSelf(this))) {
558             return;
559         }
560 
561         String sourceDisplayName = null;
562         if (sourceUri != null) {
563             // Read the uri to local first.
564             String localTmpFileName = getIntent().getStringExtra(LOCAL_TMP_FILE_NAME_EXTRA);
565             sourceDisplayName = getIntent().getStringExtra(SOURCE_URI_DISPLAY_NAME);
566             if (TextUtils.isEmpty(localTmpFileName)) {
567                 localTmpFileName = readUriToLocalFile(sourceUri);
568                 sourceDisplayName = getDisplayName(sourceUri);
569                 if (localTmpFileName == null) {
570                     Log.e(LOG_TAG, "Cannot load uri to local storage.");
571                     showFailureNotification(R.string.fail_reason_io_error);
572                     return;
573                 }
574                 getIntent().putExtra(LOCAL_TMP_FILE_NAME_EXTRA, localTmpFileName);
575                 getIntent().putExtra(SOURCE_URI_DISPLAY_NAME, sourceDisplayName);
576             }
577             sourceUri = Uri.parse(getFileStreamPath(localTmpFileName).toURI().toString());
578         }
579 
580         // Always request required permission for contacts before importing the vcard.
581         if (RequestImportVCardPermissionsActivity.startPermissionActivity(this,
582                 isCallerSelf(this))) {
583             return;
584         }
585 
586         String accountName = null;
587         String accountType = null;
588         String dataSet = null;
589         boolean isNullAccount = false;
590         final Intent intent = getIntent();
591         if (intent != null
592             && intent.hasExtra(SelectAccountActivity.ACCOUNT_NAME)
593             && intent.hasExtra(SelectAccountActivity.ACCOUNT_TYPE)
594             && intent.hasExtra(SelectAccountActivity.DATA_SET)) {
595             accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
596             accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
597             dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
598             isNullAccount = TextUtils.isEmpty(accountName) && TextUtils.isEmpty(accountType)
599                 && TextUtils.isEmpty(dataSet);
600         } else {
601             Log.e(LOG_TAG, "intent does not exist");
602         }
603 
604         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
605         final List<AccountWithDataSet> accountList = accountTypes.blockForWritableAccounts();
606         if ((!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) || isNullAccount) {
607             AccountWithDataSet selected = new AccountWithDataSet(accountName, accountType,
608                 dataSet);
609             if (accountList.contains(selected)) {
610                 mAccount = selected;
611             }
612         }
613 
614         if (accountList.isEmpty()) {
615             mAccount = null;
616         } else if (mAccount == null) {
617             startActivityForResult(new Intent(this, SelectAccountActivity.class),
618                 SELECT_ACCOUNT);
619             return;
620         }
621 
622         if (isCallerSelf(this)) {
623             startImport(sourceUri, sourceDisplayName);
624         } else {
625             ImportVCardDialogFragment.show(this, sourceUri, sourceDisplayName);
626         }
627     }
628 
isCallerSelf(Activity activity)629     private static boolean isCallerSelf(Activity activity) {
630         // {@link Activity#getCallingActivity()} is a safer alternative to
631         // {@link Activity#getCallingPackage()} that works around a
632         // framework bug where getCallingPackage() can sometimes return null even when the
633         // current activity *was* in fact launched via a startActivityForResult() call.
634         //
635         // (The bug happens if the task stack needs to be re-created by the framework after
636         // having been killed due to memory pressure or by the "Don't keep activities"
637         // developer option; see bug 7494866 for the full details.)
638         //
639         // Turns out that {@link Activity#getCallingActivity()} *does* return correct info
640         // even in the case where getCallingPackage() is broken, so the workaround is simply
641         // to get the package name from getCallingActivity().getPackageName() instead.
642         final ComponentName callingActivity = activity.getCallingActivity();
643         if (callingActivity == null) return false;
644         final String packageName = callingActivity.getPackageName();
645         if (packageName == null) return false;
646         return packageName.equals(activity.getApplicationContext().getPackageName());
647     }
648 
649     @Override
onImportVCardConfirmed(Uri sourceUri, String sourceDisplayName)650     public void onImportVCardConfirmed(Uri sourceUri, String sourceDisplayName) {
651         startImport(sourceUri, sourceDisplayName);
652     }
653 
654     @Override
onImportVCardDenied()655     public void onImportVCardDenied() {
656         finish();
657     }
658 
659     @Override
onActivityResult(int requestCode, int resultCode, Intent intent)660     public void onActivityResult(int requestCode, int resultCode, Intent intent) {
661         if (requestCode == SELECT_ACCOUNT) {
662             if (resultCode == Activity.RESULT_OK) {
663                 mAccount = new AccountWithDataSet(
664                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
665                         intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
666                         intent.getStringExtra(SelectAccountActivity.DATA_SET));
667                 final Uri sourceUri = getIntent().getData();
668                 if (sourceUri == null) {
669                     startImport(sourceUri, /* sourceDisplayName =*/ null);
670                 } else {
671                     final String sourceDisplayName = getIntent().getStringExtra(
672                             SOURCE_URI_DISPLAY_NAME);
673                     final String localFileName = getIntent().getStringExtra(
674                             LOCAL_TMP_FILE_NAME_EXTRA);
675                     final Uri localUri = Uri.parse(
676                             getFileStreamPath(localFileName).toURI().toString());
677                     startImport(localUri, sourceDisplayName);
678                 }
679             } else {
680                 if (resultCode != Activity.RESULT_CANCELED) {
681                     Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
682                 }
683                 finish();
684             }
685         } else if (requestCode == REQUEST_OPEN_DOCUMENT) {
686             if (resultCode == Activity.RESULT_OK) {
687                 final ClipData clipData = intent.getClipData();
688                 if (clipData != null) {
689                     final ArrayList<Uri> uris = new ArrayList<>();
690                     final ArrayList<String> sourceDisplayNames = new ArrayList<>();
691                     for (int i = 0; i < clipData.getItemCount(); i++) {
692                         ClipData.Item item = clipData.getItemAt(i);
693                         final Uri uri = item.getUri();
694                         if (uri != null) {
695                             final Uri localUri = readUriToLocalUri(uri);
696                             if (localUri != null) {
697                                 final String sourceDisplayName = getDisplayName(uri);
698                                 uris.add(localUri);
699                                 sourceDisplayNames.add(sourceDisplayName);
700                             }
701                         }
702                     }
703                     if (uris.isEmpty()) {
704                         Log.w(LOG_TAG, "No vCard was selected for import");
705                         finish();
706                     } else {
707                         Log.i(LOG_TAG, "Multiple vCards selected for import: " + uris);
708                         importVCard(uris.toArray(new Uri[0]),
709                                 sourceDisplayNames.toArray(new String[0]));
710                     }
711                 } else {
712                     final Uri uri = intent.getData();
713                     if (uri != null) {
714                         Log.i(LOG_TAG, "vCard selected for import: " + uri);
715                         final Uri localUri = readUriToLocalUri(uri);
716                         if (localUri != null) {
717                             final String sourceDisplayName = getDisplayName(uri);
718                             importVCard(localUri, sourceDisplayName);
719                         } else {
720                             Log.w(LOG_TAG, "No local URI for vCard import");
721                             finish();
722                         }
723                     } else {
724                         Log.w(LOG_TAG, "No vCard was selected for import");
725                         finish();
726                     }
727                 }
728             } else {
729                 if (resultCode != Activity.RESULT_CANCELED) {
730                     Log.w(LOG_TAG, "Result code was not OK nor CANCELED" + resultCode);
731                 }
732                 finish();
733             }
734         }
735     }
736 
startImport(Uri uri, String sourceDisplayName)737     private void startImport(Uri uri, String sourceDisplayName) {
738         // Handle inbound files
739         if (uri != null) {
740             Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
741             importVCard(uri, sourceDisplayName);
742         } else {
743             Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
744             final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
745             intent.addCategory(Intent.CATEGORY_OPENABLE);
746             intent.setType(VCardService.X_VCARD_MIME_TYPE);
747             intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
748             startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
749         }
750     }
751 
752     @Override
onCreateDialog(int resId, Bundle bundle)753     protected Dialog onCreateDialog(int resId, Bundle bundle) {
754         if (resId == R.id.dialog_cache_vcard) {
755             if (mProgressDialogForCachingVCard == null) {
756                 final String title = getString(R.string.caching_vcard_title);
757                 final String message = getString(R.string.caching_vcard_message);
758                 mProgressDialogForCachingVCard = new ProgressDialog(this);
759                 mProgressDialogForCachingVCard.setTitle(title);
760                 mProgressDialogForCachingVCard.setMessage(message);
761                 mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
762                 mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
763                 startVCardService();
764             }
765             return mProgressDialogForCachingVCard;
766         } else if (resId == R.id.dialog_error_with_message) {
767             String message = mErrorMessage;
768             if (TextUtils.isEmpty(message)) {
769                 Log.e(LOG_TAG, "Error message is null while it must not.");
770                 message = getString(R.string.fail_reason_unknown);
771             }
772             final AlertDialog.Builder builder = new AlertDialog.Builder(this)
773                 .setTitle(getString(R.string.reading_vcard_failed_title))
774                 .setIconAttribute(android.R.attr.alertDialogIcon)
775                 .setMessage(message)
776                 .setOnCancelListener(mCancelListener)
777                 .setPositiveButton(android.R.string.ok, mCancelListener);
778             return builder.create();
779         }
780 
781         return super.onCreateDialog(resId, bundle);
782     }
783 
startVCardService()784     /* package */ void startVCardService() {
785         mConnection = new ImportRequestConnection();
786 
787         Log.i(LOG_TAG, "Bind to VCardService.");
788         // We don't want the service finishes itself just after this connection.
789         Intent intent = new Intent(this, VCardService.class);
790         startService(intent);
791         bindService(new Intent(this, VCardService.class),
792                 mConnection, Context.BIND_AUTO_CREATE);
793     }
794 
795     @Override
onRestoreInstanceState(Bundle savedInstanceState)796     protected void onRestoreInstanceState(Bundle savedInstanceState) {
797         super.onRestoreInstanceState(savedInstanceState);
798         if (mProgressDialogForCachingVCard != null) {
799             Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
800             showDialog(R.id.dialog_cache_vcard);
801         }
802     }
803 
showFailureNotification(int reasonId)804     /* package */ void showFailureNotification(int reasonId) {
805         final NotificationManager notificationManager =
806                 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
807         final Notification notification =
808                 NotificationImportExportListener.constructImportFailureNotification(
809                         ImportVCardActivity.this,
810                         getString(reasonId));
811         notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
812                 FAILURE_NOTIFICATION_ID, notification);
813         mHandler.post(new Runnable() {
814             @Override
815             public void run() {
816                 Toast.makeText(ImportVCardActivity.this,
817                         getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
818             }
819         });
820     }
821 }
822