Home | History | Annotate | Download | only in vcard
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.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;
    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