Allow users to cancel each import/export.

- add cancel capability for vCard export.
- use jobId for Notification id, so that users can cancel each
  vCard import/export request.

Note:
As for Notification id, it may conflict with each other when
VCardService is shutdown.

Minor changes:
- add file name to each notification: users can see "xxx.vcf
  is successfully imported" instead of "vcard is successfully
  imported"
- rename mCancelled to mCanceled. strings.xml has "canceled",
  so inconsistent inside the app. Ignore the inconsistency
  between the spell in the app and Future#isCancelled().

Bug: 3215008
Change-Id: I7532e3d1b35a8bbeb694e47077554e36190482ed
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 09d5a6e..56d9a02 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -544,7 +544,7 @@
             </intent-filter>
         </activity>
 
-        <activity android:name=".vcard.CancelImportActivity"
+        <activity android:name=".vcard.CancelActivity"
             android:theme="@style/BackgroundOnly" />
 
         <activity android:name=".vcard.SelectAccountActivity"
diff --git a/res/values/ids.xml b/res/values/ids.xml
index 6cb53ed..35d5f54 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -31,8 +31,9 @@
     <item type="id" name="dialog_io_exception"/>
     <item type="id" name="dialog_error_with_message"/>
 
-    <!-- For vcard.CancelImportActivity -->
-    <item type="id" name="dialog_cancel_import_confirmation"/>
+    <!-- For vcard.CancelActivity -->
+    <item type="id" name="dialog_cancel_confirmation"/>
+    <item type="id" name="dialog_cancel_failed"/>
 
     <!-- For ContactDeletionInteraction -->
     <item type="id" name="dialog_delete_contact_confirmation"/>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 52b28df..31492f0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -805,21 +805,28 @@
          [CHAR LIMIT=40] -->
     <string name="reading_vcard_canceled_title">Reading vCard data was canceled</string>
 
-    <!-- The title shown when reading vCard finished -->
-    <string name="importing_vcard_finished_title">Finished importing vCard</string>
+    <!-- The title shown when reading vCard finished
+         The argument is file name the user imported.
+         [CHAR LIMIT=40] -->
+    <string name="importing_vcard_finished_title">Finished importing vCard <xliff:g id="filename" example="import.vcf">%s</xliff:g></string>
 
     <!-- The title shown when importing vCard is canceled (probably by a user)
+         The argument is file name the user canceled importing.
          [CHAR LIMIT=40] -->
-    <string name="importing_vcard_canceled_title">Reading vCard data was canceled</string>
+    <string name="importing_vcard_canceled_title">Importing <xliff:g id="filename" example="import.vcf">%s</xliff:g> was canceled</string>
 
     <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later
-         when there are already other import/export requests. [CHAR LIMIT=30] -->
-    <string name="vcard_import_will_start_message">vCard importer will start shortly.</string>
+         when there are already other import/export requests.
+         The argument is file name the user imported.
+         [CHAR LIMIT=40] -->
+    <string name="vcard_import_will_start_message"><xliff:g id="filename" example="import.vcf">%s</xliff:g> will be imported shortly.</string>
     <!-- The message shown when a given vCard import request is rejected by the system. [CHAR LIMIT=NONE] -->
     <string name="vcard_import_request_rejected_message">vCard import request is rejected. Please try later.</string>
     <!-- The message shown when vCard export request is accepted. The system may start that work soon, or do it later
-         when there are already other import/export requests. [CHAR LIMIT=30] -->
-    <string name="vcard_export_will_start_message">vCard exporter will start shortly.</string>
+         when there are already other import/export requests.
+         The argument is file name the user exported.
+         [CHAR LIMIT=40] -->
+    <string name="vcard_export_will_start_message"><xliff:g id="filename" example="import.vcf">%s</xliff:g> will be exported shortly.</string>
     <!-- The message shown when a given vCard export request is rejected by the system. [CHAR LIMIT=NONE] -->
     <string name="vcard_export_request_rejected_message">vCard export request is rejected. Please try later.</string>
 
@@ -857,7 +864,12 @@
     <string name="fail_reason_too_long_filename">Required filename is too long (\"<xliff:g id="filename">%s</xliff:g>\")</string>
 
     <!-- The title shown when exporting vCard is successfuly finished [CHAR LIMIT=40] -->
-    <string name="exporting_vcard_finished_title">Finished exporting vCard</string>
+    <string name="exporting_vcard_finished_title">Finished exporting <xliff:g id="filename" example="export.vcf">%s</xliff:g></string>
+
+    <!-- The title shown when exporting vCard is canceled (probably by a user)
+         The argument is file name the user canceled importing.
+         [CHAR LIMIT=40] -->
+    <string name="exporting_vcard_canceled_title">Exporting <xliff:g id="filename" example="export.vcf">%s</xliff:g> was canceled</string>
 
     <!-- Dialog title shown when the application is exporting contact data outside -->
     <string name="exporting_contact_list_title">Exporting contact data</string>
@@ -899,13 +911,23 @@
     <string name="exporting_contact_list_progress"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> contacts</string>
 
     <!-- Title shown in a Dialog confirming a user's cancel request toward existing vCard import. [CHAR LIMIT=40] -->
-    <string name="cancel_import_confirmation_title">Canceling import vCard</string>
+    <string name="cancel_import_confirmation_title">Canceling vCard import</string>
 
     <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard import.
-         The argument is the Uri for the vCard import the user wants to cancel.
-         [CHAR LIMIT=70]
-      -->
-    <string name="cancel_import_confirmation_message">Are you sure to cancel importing vCard?</string>
+         The argument is file name for the vCard import the user wants to cancel.
+         [CHAR LIMIT=128] -->
+    <string name="cancel_import_confirmation_message">Are you sure to cancel importing <xliff:g id="filename" example="import.vcf">%s</xliff:g>?</string>
+
+    <!-- Title shown in a Dialog confirming a user's cancel request toward existing vCard export. [CHAR LIMIT=128] -->
+    <string name="cancel_export_confirmation_title">Canceling vCard export</string>
+
+    <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard export.
+         The argument is file name for the vCard export the user wants to cancel.
+         [CHAR LIMIT=128] -->
+    <string name="cancel_export_confirmation_message">Are you sure to cancel exporting <xliff:g id="filename" example="export.vcf">%s</xliff:g>?</string>
+
+    <!-- Title shown in a Dialog telling users cancel vCard import/export operation is failed. [CHAR LIMIT=40] -->
+    <string name="cancel_vcard_import_or_export_failed">Failed to cancel vCard import/export</string>
 
     <!-- The string used to describe Contacts as a searchable item within system search settings. -->
     <string name="search_settings_description">Names of your contacts</string>
diff --git a/src/com/android/contacts/vcard/CancelActivity.java b/src/com/android/contacts/vcard/CancelActivity.java
new file mode 100644
index 0000000..5e3906b
--- /dev/null
+++ b/src/com/android/contacts/vcard/CancelActivity.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.vcard;
+
+import com.android.contacts.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * The Activity for canceling vCard import/export.
+ */
+public class CancelActivity extends Activity implements ServiceConnection {
+    private final String LOG_TAG = "VCardCancel";
+
+    /* package */ final static String JOB_ID = "job_id";
+    /* package */ final static String DISPLAY_NAME = "display_name";
+
+    /**
+     * Type of the process to be canceled. Only used for choosing appropriate title/message.
+     * Must be {@link VCardService#TYPE_IMPORT} or {@link VCardService#TYPE_EXPORT}.
+     */
+    /* package */ final static String TYPE = "type";
+
+    private class RequestCancelListener implements DialogInterface.OnClickListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            bindService(new Intent(CancelActivity.this,
+                    VCardService.class), CancelActivity.this, Context.BIND_AUTO_CREATE);
+        }
+    }
+
+    private class CancelListener
+            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            finish();
+        }
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            finish();
+        }
+    }
+
+    private final CancelListener mCancelListener = new CancelListener();
+    private int mJobId;
+    private String mDisplayName;
+    private int mType;
+    private Messenger mMessenger;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final Uri uri = getIntent().getData();
+        mJobId = Integer.parseInt(uri.getQueryParameter(JOB_ID));
+        mDisplayName = uri.getQueryParameter(DISPLAY_NAME);
+        mType = Integer.parseInt(uri.getQueryParameter(TYPE));
+        showDialog(R.id.dialog_cancel_confirmation);
+    }
+
+    @Override
+    protected Dialog onCreateDialog(int id, Bundle bundle) {
+        switch (id) {
+        case R.id.dialog_cancel_confirmation: {
+            final String title;
+            final String message;
+            if (mType == VCardService.TYPE_IMPORT) {
+                title = getString(R.string.cancel_import_confirmation_title);
+                message = getString(R.string.cancel_import_confirmation_message, mDisplayName);
+            } else {
+                title = getString(R.string.cancel_export_confirmation_title);
+                message = getString(R.string.cancel_export_confirmation_message, mDisplayName);
+            }
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                    .setTitle(title)
+                    .setMessage(message)
+                    .setPositiveButton(android.R.string.ok, new RequestCancelListener())
+                    .setOnCancelListener(mCancelListener)
+                    .setNegativeButton(android.R.string.cancel, mCancelListener);
+            return builder.create();
+        }
+        case R.id.dialog_cancel_failed:
+            final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                    .setTitle(R.string.cancel_vcard_import_or_export_failed)
+                    .setIcon(android.R.drawable.ic_dialog_alert)
+                    .setMessage(getString(R.string.fail_reason_unknown))
+                    .setOnCancelListener(mCancelListener)
+                    .setPositiveButton(android.R.string.ok, mCancelListener);
+            return builder.create();
+        default:
+            Log.w(LOG_TAG, "Unknown dialog id: " + id);
+            break;
+        }
+        return super.onCreateDialog(id, bundle);
+    }
+
+    @Override
+    public void onServiceConnected(ComponentName name, IBinder service) {
+        mMessenger = new Messenger(service);
+
+        boolean callFinish = false;
+        try {
+            final CancelRequest request = new CancelRequest(mJobId, mDisplayName);
+            mMessenger.send(Message.obtain(null, VCardService.MSG_CANCEL_REQUEST, request));
+            callFinish = true;
+        } catch (RemoteException e) {
+            Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+            showDialog(R.id.dialog_cancel_failed);
+            // finish() should be called from the Dialog
+        } finally {
+            unbindService(this);
+        }
+
+        if (callFinish) {
+            finish();
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected(ComponentName name) {
+        mMessenger = null;
+    }
+}
diff --git a/src/com/android/contacts/vcard/CancelImportActivity.java b/src/com/android/contacts/vcard/CancelImportActivity.java
deleted file mode 100644
index b52ee98..0000000
--- a/src/com/android/contacts/vcard/CancelImportActivity.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2010 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.contacts.vcard;
-
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.ServiceConnection;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.RemoteException;
-import android.util.Log;
-
-import com.android.contacts.R;
-
-/**
- * The Activity for canceling ongoing vCard import.
- *
- * Currently we ignore tha case where there are more than one import requests
- * with a same Uri in the queue.
- */
-public class CancelImportActivity extends Activity {
-    private final String LOG_TAG = "VCardImporter";
-
-    /* package */ final String EXTRA_TARGET_URI = "extra_target_uri";
-
-    private class CustomConnection implements ServiceConnection {
-        private Messenger mMessenger;
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            mMessenger = new Messenger(service);
-
-            try {
-                mMessenger.send(Message.obtain(null,
-                        VCardService.MSG_CANCEL_IMPORT_REQUEST,
-                        null));
-                finish();
-            } catch (RemoteException e) {
-                Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
-                CancelImportActivity.this.showDialog(R.string.fail_reason_unknown);
-            } finally {
-                CancelImportActivity.this.unbindService(this);
-            }
-        }
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
-            mMessenger = null;
-        }
-    }
-
-    private class RequestCancelImportListener implements DialogInterface.OnClickListener {
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            bindService(new Intent(CancelImportActivity.this,
-                    VCardService.class), mConnection, Context.BIND_AUTO_CREATE);
-        }
-    }
-
-    private class CancelListener
-            implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            finish();
-        }
-        @Override
-        public void onCancel(DialogInterface dialog) {
-            finish();
-        }
-    }
-
-    private final CancelListener mCancelListener = new CancelListener();
-    private final CustomConnection mConnection = new CustomConnection();
-    // private String mTargetUri;
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        showDialog(R.id.dialog_cancel_import_confirmation);
-    }
-
-    @Override
-    protected Dialog onCreateDialog(int resId, Bundle bundle) {
-        switch (resId) {
-
-        case R.id.dialog_cancel_import_confirmation: {
-            return getConfirmationDialog();
-        }
-        case R.string.fail_reason_unknown:
-            final AlertDialog.Builder builder = new AlertDialog.Builder(this)
-                .setTitle(getString(R.string.reading_vcard_failed_title))
-                .setIcon(android.R.drawable.ic_dialog_alert)
-                .setMessage(getString(resId))
-                .setOnCancelListener(mCancelListener)
-                .setPositiveButton(android.R.string.ok, mCancelListener);
-            return builder.create();
-        }
-        return super.onCreateDialog(resId, bundle);
-    }
-
-    private Dialog getConfirmationDialog() {
-        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
-                .setTitle(R.string.cancel_import_confirmation_title)
-                .setMessage(R.string.cancel_import_confirmation_message)
-                .setPositiveButton(android.R.string.ok, new RequestCancelImportListener())
-                .setOnCancelListener(mCancelListener)
-                .setNegativeButton(android.R.string.cancel, mCancelListener);
-        return builder.create();
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/CancelImportRequest.java b/src/com/android/contacts/vcard/CancelRequest.java
similarity index 60%
rename from src/com/android/contacts/vcard/CancelImportRequest.java
rename to src/com/android/contacts/vcard/CancelRequest.java
index dd10187..85893ba 100644
--- a/src/com/android/contacts/vcard/CancelImportRequest.java
+++ b/src/com/android/contacts/vcard/CancelRequest.java
@@ -15,14 +15,18 @@
  */
 package com.android.contacts.vcard;
 
-import android.net.Uri;
-
 /**
- * Class representing one request for canceling vCard import (given as a Uri).
+ * Class representing one request for canceling vCard import/export.
  */
-public class CancelImportRequest {
-    public final Uri uri;
-    public CancelImportRequest(Uri uri) {
-        this.uri = uri;
+public class CancelRequest {
+    public final int jobId;
+    /**
+     * Name used for showing users some useful info. Typically a file name.
+     * Must not be used to do some actual operations.
+     */
+    public final String displayName;
+    public CancelRequest(int jobId, String displayName) {
+        this.jobId = jobId;
+        this.displayName = displayName;
     }
 }
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ExportProcessor.java b/src/com/android/contacts/vcard/ExportProcessor.java
index c54df49..028bcc7 100644
--- a/src/com/android/contacts/vcard/ExportProcessor.java
+++ b/src/com/android/contacts/vcard/ExportProcessor.java
@@ -48,7 +48,7 @@
     private final ExportRequest mExportRequest;
     private final int mJobId;
 
-    private volatile boolean mCancelled;
+    private volatile boolean mCanceled;
     private volatile boolean mDone;
 
     public ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId) {
@@ -62,7 +62,7 @@
 
     @Override
     public final int getType() {
-        return PROCESSOR_TYPE_EXPORT;
+        return VCardService.TYPE_EXPORT;
     }
 
     @Override
@@ -70,6 +70,13 @@
         // ExecutorService ignores RuntimeException, so we need to show it here.
         try {
             runInternal();
+
+            if (isCancelled()) {
+                doCancelNotification();
+            }
+        } catch (OutOfMemoryError e) {
+            Log.e(LOG_TAG, "OutOfMemoryError thrown during import", e);
+            throw e;
         } catch (RuntimeException e) {
             Log.e(LOG_TAG, "RuntimeException thrown during export", e);
             throw e;
@@ -86,7 +93,7 @@
         VCardComposer composer = null;
         boolean successful = false;
         try {
-            if (mCancelled) {
+            if (isCancelled()) {
                 Log.i(LOG_TAG, "Export request is cancelled before handling the request");
                 return;
             }
@@ -147,7 +154,7 @@
 
             int current = 1;  // 1-origin
             while (!composer.isAfterLast()) {
-                if (mCancelled) {
+                if (isCancelled()) {
                     Log.i(LOG_TAG, "Export request is cancelled during composing vCard");
                     return;
                 }
@@ -173,7 +180,9 @@
             Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri);
 
             successful = true;
-            final String title = mService.getString(R.string.exporting_vcard_finished_title);
+            final String filename = uri.getLastPathSegment();
+            final String title = mService.getString(R.string.exporting_vcard_finished_title,
+                    filename);
             doFinishNotification(title, "");
         } finally {
             if (composer != null) {
@@ -197,58 +206,46 @@
         }
     }
 
-    private void doProgressNotification(Uri uri, int total, int current) {
-        final String title = mService.getString(R.string.exporting_contact_list_title);
-        final String filename = uri.getLastPathSegment();
+    private void doProgressNotification(Uri uri, int totalCount, int currentCount) {
+        final String displayName = uri.getLastPathSegment();
         final String description =
-                mService.getString(R.string.exporting_contact_list_message, filename);
-
-        final RemoteViews remoteViews = new RemoteViews(mService.getPackageName(),
-                R.layout.status_bar_ongoing_event_progress_bar);
-        remoteViews.setTextViewText(R.id.status_description, description);
-        remoteViews.setProgressBar(R.id.status_progress_bar, total, current, (total == -1));
-
-        final String percentage = mService.getString(R.string.percentage,
-                String.valueOf((current * 100)/total));
-        remoteViews.setTextViewText(R.id.status_progress_text, percentage);
-        remoteViews.setImageViewResource(R.id.status_icon, android.R.drawable.stat_sys_upload);
-
-        final Notification notification = new Notification();
-        notification.icon = android.R.drawable.stat_sys_upload;
-        notification.flags |= Notification.FLAG_ONGOING_EVENT;
-        notification.tickerText = title;
-        notification.contentView = remoteViews;
-        notification.contentIntent =
-                PendingIntent.getActivity(mService, 0,
-                        new Intent(mService, ContactBrowserActivity.class), 0);
-
-        mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification);
+                mService.getString(R.string.exporting_contact_list_message, displayName);
+        final String tickerText =
+                mService.getString(R.string.exporting_contact_list_title);
+        final Notification notification =
+                VCardService.constructProgressNotification(mService, VCardService.TYPE_EXPORT,
+                        description, tickerText, mJobId, displayName, totalCount, currentCount);
+        mNotificationManager.notify(mJobId, notification);
     }
 
-    private void doFinishNotification(final String title, final String message) {
-        final Notification notification = new Notification();
-        notification.icon = android.R.drawable.stat_sys_upload_done;
-        notification.flags |= Notification.FLAG_AUTO_CANCEL;
-        notification.setLatestEventInfo(mService, title, message, null);
+    private void doCancelNotification() {
+        final String description = mService.getString(R.string.exporting_vcard_canceled_title,
+                mExportRequest.destUri.getLastPathSegment());
+        final Notification notification =
+                VCardService.constructCancelNotification(mService, description);
+        mNotificationManager.notify(mJobId, notification);
+    }
+
+    private void doFinishNotification(final String title, final String description) {
         final Intent intent = new Intent(mService, ContactBrowserActivity.class);
-        notification.contentIntent =
-                PendingIntent.getActivity(mService, 0, intent, 0);
-        mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification);
+        final Notification notification =
+                VCardService.constructFinishNotification(mService, description, intent);
+        mNotificationManager.notify(mJobId, notification);
     }
 
     @Override
     public synchronized boolean cancel(boolean mayInterruptIfRunning) {
         Log.i(LOG_TAG, "received cancel request");
-        if (mDone || mCancelled) {
+        if (mDone || mCanceled) {
             return false;
         }
-        mCancelled = true;
+        mCanceled = true;
         return true;
     }
 
     @Override
     public synchronized boolean isCancelled() {
-        return mCancelled;
+        return mCanceled;
     }
 
     @Override
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
index 049d0a9..5b5ae79 100644
--- a/src/com/android/contacts/vcard/ImportProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -38,6 +38,7 @@
 import android.net.Uri;
 import android.provider.ContactsContract.RawContacts;
 import android.util.Log;
+import android.widget.RemoteViews;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -57,30 +58,32 @@
     private final ImportRequest mImportRequest;
     private final int mJobId;
 
-    private final ImportProgressNotifier mNotifier = new ImportProgressNotifier();
+    private final ImportProgressNotifier mNotifier;
 
     // TODO: remove and show appropriate message instead.
     private final List<Uri> mFailedUris = new ArrayList<Uri>();
 
     private VCardParser mVCardParser;
 
-    private volatile boolean mCancelled;
+    private volatile boolean mCanceled;
     private volatile boolean mDone;
 
     public ImportProcessor(final VCardService service, final ImportRequest request,
-            int jobId) {
+            final int jobId) {
         mService = service;
         mResolver = mService.getContentResolver();
         mNotificationManager = (NotificationManager)
                 mService.getSystemService(Context.NOTIFICATION_SERVICE);
-        mNotifier.init(mService, mNotificationManager);
+
         mImportRequest = request;
         mJobId = jobId;
+        mNotifier = new ImportProgressNotifier(service, mNotificationManager, jobId,
+                request.originalUri.getLastPathSegment());
     }
 
     @Override
     public final int getType() {
-        return PROCESSOR_TYPE_IMPORT;
+        return VCardService.TYPE_IMPORT;
     }
 
     @Override
@@ -88,6 +91,13 @@
         // ExecutorService ignores RuntimeException, so we need to show it here.
         try {
             runInternal();
+
+            if (isCancelled()) {
+                doCancelNotification();
+            }
+        } catch (OutOfMemoryError e) {
+            Log.e(LOG_TAG, "OutOfMemoryError thrown during import", e);
+            throw e;
         } catch (RuntimeException e) {
             Log.e(LOG_TAG, "RuntimeException thrown during import", e);
             throw e;
@@ -101,7 +111,7 @@
     private void runInternal() {
         Log.i(LOG_TAG, String.format("vCard import (id: %d) has started.", mJobId));
         final ImportRequest request = mImportRequest;
-        if (mCancelled) {
+        if (isCancelled()) {
             Log.i(LOG_TAG, "Canceled before actually handling parameter (" + request.uri + ")");
             return;
         }
@@ -145,18 +155,19 @@
             // value
             if (isCancelled()) {
                 Log.i(LOG_TAG, "vCard import has been canceled (uri: " + uri + ")");
+                // Cancel notification will be done outside this method.
             } else {
                 Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri);
-            }
-            List<Uri> uris = committer.getCreatedUris();
-            if (uris != null && uris.size() > 0) {
-                doFinishNotification(uris.get(0));
-            } else {
-                // Not critical, but suspicious.
-                Log.w(LOG_TAG,
-                        "Created Uris is null or 0 length " +
-                        "though the creation itself is successful.");
-                doFinishNotification(null);
+                List<Uri> uris = committer.getCreatedUris();
+                if (uris != null && uris.size() > 0) {
+                    doFinishNotification(uris.get(0));
+                } else {
+                    // Not critical, but suspicious.
+                    Log.w(LOG_TAG,
+                            "Created Uris is null or 0 length " +
+                            "though the creation itself is successful.");
+                    doFinishNotification(null);
+                }
             }
         } else {
             Log.w(LOG_TAG, "Failed to read one vCard file: " + uri);
@@ -164,32 +175,17 @@
         }
     }
 
-    /*
-    private void doErrorNotification(int id) {
-        final Notification notification = new Notification();
-        notification.icon = android.R.drawable.stat_sys_download_done;
-        notification.flags |= Notification.FLAG_AUTO_CANCEL;
-        final String title = mService.getString(R.string.reading_vcard_failed_title);
-        final PendingIntent intent =
-                PendingIntent.getActivity(mService, 0, new Intent(), 0);
-        notification.setLatestEventInfo(mService, title, "", intent);
-        mNotificationManager.notify(MESSAGE_ID, notification);
+    private void doCancelNotification() {
+        final String description = mService.getString(R.string.importing_vcard_canceled_title,
+                mImportRequest.originalUri.getLastPathSegment());
+        final Notification notification =
+                VCardService.constructCancelNotification(mService, description);
+        mNotificationManager.notify(mJobId, notification);
     }
-    */
 
     private void doFinishNotification(final Uri createdUri) {
-        final Notification notification = new Notification();
-        final String title;
-        notification.flags |= Notification.FLAG_AUTO_CANCEL;
-
-        if (isCancelled()) {
-            notification.icon = android.R.drawable.stat_notify_error;
-            title = mService.getString(R.string.importing_vcard_canceled_title);
-        } else {
-            notification.icon = android.R.drawable.stat_sys_download_done;
-            title = mService.getString(R.string.importing_vcard_finished_title);
-        }
-
+        final String description = mService.getString(R.string.importing_vcard_finished_title,
+                mImportRequest.originalUri.getLastPathSegment());
         final Intent intent;
         if (createdUri != null) {
             final long rawContactId = ContentUris.parseId(createdUri);
@@ -200,10 +196,9 @@
         } else {
             intent = null;
         }
-
-        notification.setLatestEventInfo(mService, title, "",
-                PendingIntent.getActivity(mService, 0, intent, 0));
-        mNotificationManager.notify(VCardService.IMPORT_NOTIFICATION_ID, notification);
+        final Notification notification =
+                   VCardService.constructFinishNotification(mService, description, intent);
+        mNotificationManager.notify(mJobId, notification);
     }
 
     private boolean readOneVCard(Uri uri, int vcardType, String charset,
@@ -231,7 +226,7 @@
                     mVCardParser = (vcardVersion == ImportVCardActivity.VCARD_VERSION_V30 ?
                             new VCardParser_V30(vcardType) :
                                 new VCardParser_V21(vcardType));
-                    if (mCancelled) {
+                    if (isCancelled()) {
                         Log.i(LOG_TAG, "ImportProcessor already recieves cancel request, so " +
                                 "send cancel request to vCard parser too.");
                         mVCardParser.cancel();
@@ -278,10 +273,10 @@
     @Override
     public synchronized boolean cancel(boolean mayInterruptIfRunning) {
         Log.i(LOG_TAG, "ImportProcessor received cancel request");
-        if (mDone || mCancelled) {
+        if (mDone || mCanceled) {
             return false;
         }
-        mCancelled = true;
+        mCanceled = true;
         synchronized (this) {
             if (mVCardParser != null) {
                 mVCardParser.cancel();
@@ -292,7 +287,7 @@
 
     @Override
     public synchronized boolean isCancelled() {
-        return mCancelled;
+        return mCanceled;
     }
 
 
diff --git a/src/com/android/contacts/vcard/ImportProgressNotifier.java b/src/com/android/contacts/vcard/ImportProgressNotifier.java
index 6a24bc0..d6d0f3f 100644
--- a/src/com/android/contacts/vcard/ImportProgressNotifier.java
+++ b/src/com/android/contacts/vcard/ImportProgressNotifier.java
@@ -21,27 +21,30 @@
 
 import android.app.Notification;
 import android.app.NotificationManager;
-import android.app.PendingIntent;
 import android.content.Context;
-import android.content.Intent;
-import android.widget.RemoteViews;
 
 /**
- * {@link VCardEntryHandler} implementation which lets the system update
- * the current status of vCard import.
+ * {@link VCardEntryHandler} implementation letting the system update the current status of
+ * vCard import.
  */
 public class ImportProgressNotifier implements VCardEntryHandler {
     private static final String LOG_TAG = "VCardImport";
 
-    private Context mContext;
-    private NotificationManager mNotificationManager;
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    private final int mJobId;
+    private final String mDisplayName;
 
     private int mCurrentCount;
     private int mTotalCount;
 
-    public void init(Context context, NotificationManager notificationManager) {
+    public ImportProgressNotifier(
+            Context context, NotificationManager notificationManager,
+            int jobId, String displayName) {
         mContext = context;
         mNotificationManager = notificationManager;
+        mJobId = jobId;
+        mDisplayName = displayName;
     }
 
     public void onStart() {
@@ -53,12 +56,6 @@
             return;
         }
 
-        // We don't use onStart() since:
-        // - We cannot know name there but here.
-        // - There's high probability where name comes soon after the beginning of entry, so
-        //   we don't need to hurry to show something.
-
-
         final String totalCountString;
         synchronized (this) {
             totalCountString = String.valueOf(mTotalCount);
@@ -68,52 +65,19 @@
                         String.valueOf(mCurrentCount),
                         totalCountString,
                         contactStruct.getDisplayName());
-
-
-        final Context context = mContext.getApplicationContext();
-
         final String description = mContext.getString(R.string.importing_vcard_description,
                 contactStruct.getDisplayName());
-        final RemoteViews remoteViews =
-                new RemoteViews(context.getPackageName(),
-                R.layout.status_bar_ongoing_event_progress_bar);
-        remoteViews.setTextViewText(R.id.status_description, description);
-        remoteViews.setProgressBar(R.id.status_progress_bar, mTotalCount, mCurrentCount,
-                mTotalCount == -1);
-        final String percentage;
-        if (mTotalCount > 0) {
-            percentage = context.getString(R.string.percentage,
-                    String.valueOf(mCurrentCount * 100/mTotalCount));
-        } else {
-            percentage = "";
-        }
-        remoteViews.setTextViewText(R.id.status_progress_text, percentage);
-        remoteViews.setImageViewResource(R.id.status_icon, android.R.drawable.stat_sys_download);
 
-        final Notification notification = new Notification();
-        notification.icon = android.R.drawable.stat_sys_download;
-        notification.tickerText = tickerText;
-        notification.contentView = remoteViews;
-        notification.flags |= Notification.FLAG_ONGOING_EVENT;
-
-        final PendingIntent pendingIntent =
-                PendingIntent.getActivity(context, 0,
-                        new Intent(context, CancelImportActivity.class),
-                        PendingIntent.FLAG_UPDATE_CURRENT);
-
-        notification.contentIntent = pendingIntent;
-        // notification.setLatestEventInfo(context, title, description, pendingIntent);
-        mNotificationManager.notify(VCardService.IMPORT_NOTIFICATION_ID, notification);
+        final Notification notification = VCardService.constructProgressNotification(
+                mContext.getApplicationContext(), VCardService.TYPE_IMPORT, description, tickerText,
+                mJobId, mDisplayName, mTotalCount, mCurrentCount);
+        mNotificationManager.notify(mJobId, notification);
     }
 
     public synchronized void addTotalCount(int additionalCount) {
         mTotalCount += additionalCount;
     }
 
-    public synchronized void resetTotalCount() {
-        mTotalCount = 0;
-    }
-
     public void onEnd() {
     }
 }
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ImportRequest.java b/src/com/android/contacts/vcard/ImportRequest.java
index 5d46166..e8b5606 100644
--- a/src/com/android/contacts/vcard/ImportRequest.java
+++ b/src/com/android/contacts/vcard/ImportRequest.java
@@ -35,7 +35,22 @@
      * Can be null (typically when there's no Account available in the system).
      */
     public final Account account;
+    /**
+     * Uri to be imported. May have different content than originally given from users, so
+     * when displaying user-friendly information (e.g. "importing xxx.vcf"), use
+     * {@link #originalUri} instead.
+     */
     public final Uri uri;
+
+    /**
+     * Original uri given from users.
+     * Useful when showing user-friendly information ("importing xxx.vcf"), as
+     * {@link #uri} may have different name than the original (like "import_tmp_1.vcf").
+     *
+     * This variable must not be used for doing actual processing like re-import, as the app
+     * may not have right permission to do so.
+     */
+    public final Uri originalUri;
     /**
      * Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}.
      */
@@ -74,10 +89,11 @@
      */
     public final int entryCount;
     public ImportRequest(Account account,
-            Uri uri, int estimatedType, String estimatedCharset,
+            Uri uri, Uri originalUri, int estimatedType, String estimatedCharset,
             int vcardVersion, int entryCount) {
         this.account = account;
         this.uri = uri;
+        this.originalUri = originalUri;
         this.estimatedVCardType = estimatedType;
         this.estimatedCharset = estimatedCharset;
         this.vcardVersion = vcardVersion;
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index 7a72ca2..948b465 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -284,7 +284,8 @@
                         Log.w(LOG_TAG, "destUri is null");
                         break;
                     }
-                    final ImportRequest parameter = constructImportRequest(localDataUri);
+                    final ImportRequest parameter = constructImportRequest(
+                            localDataUri, sourceUri);
                     if (mCanceled) {
                         Log.i(LOG_TAG, "vCard cache operation is canceled.");
                         return;
@@ -359,13 +360,17 @@
         }
 
         /**
-         * Reads the Uri (possibly multiple times) and constructs {@link ImportRequest} from
+         * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
          * its content.
          *
-         * Uri should be local one, as we cannot guarantee other types of Uris can be read
-         * multiple times.
+         * @arg localDataUri Uri actually used for the import. Should be stored in
+         * app local storage, as we cannot guarantee other types of Uris can be read
+         * multiple times. This variable populates {@link ImportRequest#uri}.
+         * @arg originalUri Uri requested to be imported. Used mainly for displaying
+         * information. This variable populates {@link ImportRequest#originalUri}.
          */
-        private ImportRequest constructImportRequest(final Uri localDataUri) {
+        private ImportRequest constructImportRequest(
+                final Uri localDataUri, final Uri originalUri) {
             final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
             VCardEntryCounter counter = null;
             VCardSourceDetector detector = null;
@@ -419,7 +424,8 @@
                 Log.e(LOG_TAG, "IOException during constructing ImportRequest", e);
                 return null;
             }
-            return new ImportRequest(mAccount, localDataUri,
+            return new ImportRequest(mAccount,
+                    localDataUri, originalUri,
                     detector.getEstimatedType(),
                     detector.getEstimatedCharset(),
                     vcardVersion, counter.getCount());
diff --git a/src/com/android/contacts/vcard/ProcessorBase.java b/src/com/android/contacts/vcard/ProcessorBase.java
index 17ac6d3..833090d 100644
--- a/src/com/android/contacts/vcard/ProcessorBase.java
+++ b/src/com/android/contacts/vcard/ProcessorBase.java
@@ -33,12 +33,9 @@
  */
 public abstract class ProcessorBase implements RunnableFuture<Object> {
 
-    public static final int PROCESSOR_TYPE_IMPORT = 1;
-    public static final int PROCESSOR_TYPE_EXPORT = 2;
-
     /**
-     * @return the type of the processor. Must be {@link #PROCESSOR_TYPE_IMPORT} or
-     * {@link #PROCESSOR_TYPE_EXPORT}.
+     * @return the type of the processor. Must be {@link VCardService#TYPE_IMPORT} or
+     * {@link VCardService#TYPE_EXPORT}.
      */
     public abstract int getType();
 
diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java
index 367634c..cad9fc6 100644
--- a/src/com/android/contacts/vcard/VCardService.java
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -17,13 +17,19 @@
 
 import com.android.contacts.R;
 
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.app.Service;
+import android.content.Context;
 import android.content.Intent;
+import android.net.Uri;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
 import android.util.Log;
+import android.widget.RemoteViews;
 import android.widget.Toast;
 
 import java.util.HashMap;
@@ -46,10 +52,14 @@
 
     /* package */ static final int MSG_IMPORT_REQUEST = 1;
     /* package */ static final int MSG_EXPORT_REQUEST = 2;
-    /* package */ static final int MSG_CANCEL_IMPORT_REQUEST = 3;
+    /* package */ static final int MSG_CANCEL_REQUEST = 3;
 
-    /* package */ static final int IMPORT_NOTIFICATION_ID = 1000;
-    /* package */ static final int EXPORT_NOTIFICATION_ID = 1001;
+    /**
+     * Specifies the type of operation. Used when constructing a {@link Notification}, canceling
+     * some operation, etc.
+     */
+    /* package */ static final int TYPE_IMPORT = 1;
+    /* package */ static final int TYPE_EXPORT = 2;
 
     /* package */ static final String CACHE_FILE_PREFIX = "import_tmp_";
 
@@ -65,8 +75,8 @@
                     handleExportRequest((ExportRequest)msg.obj);
                     break;
                 }
-                case MSG_CANCEL_IMPORT_REQUEST: {
-                    handleCancelAllImportRequest();
+                case MSG_CANCEL_REQUEST: {
+                    handleCancelRequest((CancelRequest)msg.obj);
                     break;
                 }
                 // TODO: add cancel capability for export..
@@ -78,6 +88,8 @@
         }
     });
 
+    private NotificationManager mNotificationManager;
+
     // Should be single thread, as we don't want to simultaneously handle import and export
     // requests.
     private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
@@ -90,6 +102,12 @@
             new HashMap<Integer, ProcessorBase>();
 
     @Override
+    public void onCreate() {
+        super.onCreate();
+        mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+    }
+
+    @Override
     public int onStartCommand(Intent intent, int flags, int id) {
         return START_STICKY;
     }
@@ -108,66 +126,74 @@
     }
 
     private synchronized void handleImportRequest(ImportRequest request) {
-        tryExecute(new ImportProcessor(this, request, mCurrentJobId),
-                R.string.vcard_import_will_start_message,
-                R.string.vcard_import_request_rejected_message);
+        if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) {
+            final String displayName = request.originalUri.getLastPathSegment(); 
+            final String message = getString(R.string.vcard_import_will_start_message,
+                    displayName);
+            // TODO: Ideally we should detect the current status of import/export and show
+            // "started" when we can import right now and show "will start" when we cannot.
+            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+
+            final Notification notification =
+                    constructProgressNotification(
+                            this, TYPE_IMPORT, message, message, mCurrentJobId,
+                            displayName, -1, 0);
+            mNotificationManager.notify(mCurrentJobId, notification);
+            mCurrentJobId++;
+        } else {
+            // TODO: a little unkind to show Toast in this case, which is shown just a moment.
+            // Ideally we should show some persistent something users can notice more easily.
+            Toast.makeText(this, getString(R.string.vcard_import_request_rejected_message),
+                    Toast.LENGTH_LONG).show();
+        }
     }
 
     private synchronized void handleExportRequest(ExportRequest request) {
-        tryExecute(new ExportProcessor(this, request, mCurrentJobId),
-                R.string.vcard_export_will_start_message,
-                R.string.vcard_export_request_rejected_message);
+        if (tryExecute(new ExportProcessor(this, request, mCurrentJobId))) {
+            final String displayName = request.destUri.getLastPathSegment();
+            final String message = getString(R.string.vcard_export_will_start_message,
+                    displayName);
+            Toast.makeText(this, message, Toast.LENGTH_LONG).show();
+            final Notification notification =
+                    constructProgressNotification(this, TYPE_EXPORT, message, message,
+                            mCurrentJobId, displayName, -1, 0);
+            mNotificationManager.notify(mCurrentJobId, notification);
+            mCurrentJobId++;
+        } else {
+            Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
+                    Toast.LENGTH_LONG).show();
+        }
     }
 
     /**
-     * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor and
-     * shows appropriate Toast using given resource ids.
-     * Updates relevant instances when successful.
+     * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
+     * @return true when successful.
      */
-    private synchronized void tryExecute(ProcessorBase processor,
-            int successfulMessageId, int rejectedMessageId) {
+    private synchronized boolean tryExecute(ProcessorBase processor) {
         try {
             mExecutorService.execute(processor);
             mRunningJobMap.put(mCurrentJobId, processor);
-            mCurrentJobId++;
-            // TODO: Ideally we should detect the current status of import/export and show
-            // "started" when we can import right now and show "will start" when we cannot.
-            Toast.makeText(this, getString(successfulMessageId), Toast.LENGTH_LONG).show();
+            return true;
         } catch (RejectedExecutionException e) {
             Log.w(LOG_TAG, "Failed to excetute a job.", e);
-            // TODO: a little unkind to show Toast in this case, which is shown just a moment.
-            // Ideally we should show some persistent something users can notice more easily.
-            Toast.makeText(this, getString(rejectedMessageId), Toast.LENGTH_LONG).show();
+            return false;
         }
     }
 
-    private void handleCancelAllImportRequest() {
-        Log.i(LOG_TAG, "Received cancel import request.");
-        cancelAllImportRequests();
-    }
+    private void handleCancelRequest(CancelRequest request) {
+        final int jobId = request.jobId;
+        Log.i(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
+        final ProcessorBase processor = mRunningJobMap.remove(jobId);
 
-    private synchronized void cancelAllImportRequests() {
-        for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
-            final ProcessorBase processor = entry.getValue();
-            if (processor.getType() == ProcessorBase.PROCESSOR_TYPE_IMPORT) {
-                final int jobId = entry.getKey();
-                processor.cancel(true);
-                mRunningJobMap.remove(jobId);
-                Log.i(LOG_TAG, String.format("Canceling job %d", jobId));
-            }
-        }
-        stopServiceWhenNoJob();
-    }
-
-    private synchronized void cancelAllExportRequests() {
-        for (final Map.Entry<Integer, ProcessorBase> entry : mRunningJobMap.entrySet()) {
-            final ProcessorBase processor = entry.getValue();
-            if (processor.getType() == ProcessorBase.PROCESSOR_TYPE_EXPORT) {
-                final int jobId = entry.getKey();
-                processor.cancel(true);
-                mRunningJobMap.remove(jobId);
-                Log.i(LOG_TAG, String.format("Canceling job %d", jobId));
-            }
+        if (processor != null) {
+            processor.cancel(true);
+            final String description = processor.getType() == TYPE_IMPORT ?
+                    getString(R.string.importing_vcard_canceled_title, request.displayName) :
+                            getString(R.string.exporting_vcard_canceled_title, request.displayName);
+            final Notification notification = constructCancelNotification(this, description);
+            mNotificationManager.notify(jobId, notification);
+        } else {
+            Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
         }
         stopServiceWhenNoJob();
     }
@@ -241,4 +267,100 @@
             }
         }
     }
+
+    /**
+     * Constructs a {@link Notification} showing the current status of import/export.
+     * Users can cancel the process with the Notification.
+     *
+     * @param context
+     * @param type import/export
+     * @param description Content of the Notification.
+     * @param tickerText
+     * @param jobId
+     * @param displayName Name to be shown to the Notification (e.g. "finished importing XXXX").
+     * Typycally a file name.
+     * @param totalCount The number of vCard entries to be imported. Used to show progress bar.
+     * -1 lets the system show the progress bar with "indeterminate" state.
+     * @param currentCount The index of current vCard. Used to show progress bar.
+     */
+    /* package */ static Notification constructProgressNotification(
+            Context context, int type, String description, String tickerText,
+            int jobId, String displayName, int totalCount, int currentCount) {
+        final RemoteViews remoteViews =
+                new RemoteViews(context.getPackageName(),
+                        R.layout.status_bar_ongoing_event_progress_bar);
+        remoteViews.setTextViewText(R.id.status_description, description);
+        remoteViews.setProgressBar(R.id.status_progress_bar, totalCount, currentCount,
+                totalCount == -1);
+        final String percentage;
+        if (totalCount > 0) {
+            percentage = context.getString(R.string.percentage,
+                    String.valueOf(currentCount * 100/totalCount));
+        } else {
+            percentage = "";
+        }
+        remoteViews.setTextViewText(R.id.status_progress_text, percentage);
+        final int icon = (type == TYPE_IMPORT ? android.R.drawable.stat_sys_download :
+                android.R.drawable.stat_sys_upload);
+        remoteViews.setImageViewResource(R.id.status_icon, icon);
+
+        final Notification notification = new Notification();
+        notification.icon = icon;
+        notification.tickerText = tickerText;
+        notification.contentView = remoteViews;
+        notification.flags |= Notification.FLAG_ONGOING_EVENT;
+
+        // Note: We cannot use extra values here (like setIntExtra()), as PendingIntent doesn't
+        // preserve them across multiple Notifications. PendingIntent preserves the first extras
+        // (when flag is not set), or update them when PendingIntent#getActivity() is called
+        // (See PendingIntent#FLAG_UPDATE_CURRENT). In either case, we cannot preserve extras as we
+        // expect (for each vCard import/export request).
+        //
+        // We use query parameter in Uri instead.
+        // Scheme and Authority is arbitorary, assuming CancelActivity never refers them.
+        final Intent intent = new Intent(context, CancelActivity.class);
+        final Uri uri = (new Uri.Builder())
+                .scheme("invalidscheme")
+                .authority("invalidauthority")
+                .appendQueryParameter(CancelActivity.JOB_ID, String.valueOf(jobId))
+                .appendQueryParameter(CancelActivity.DISPLAY_NAME, displayName)
+                .appendQueryParameter(CancelActivity.TYPE, String.valueOf(type)).build();
+        intent.setData(uri);
+
+        notification.contentIntent = PendingIntent.getActivity(context, 0, intent, 0);
+        return notification;
+    }
+
+    /**
+     * Constructs a Notification telling users the process is canceled.
+     *
+     * @param context
+     * @param description Content of the Notification
+     */
+    /* package */ static Notification constructCancelNotification(
+            Context context, String description) {
+        final Notification notification = new Notification();
+        notification.flags |= Notification.FLAG_AUTO_CANCEL;
+        notification.icon = android.R.drawable.stat_notify_error;
+        notification.setLatestEventInfo(context, description, description,
+                PendingIntent.getActivity(context, 0, null, 0));
+        return notification;
+    }
+
+    /**
+     * Constructs a Notification telling users the process is finished.
+     *
+     * @param context
+     * @param description Content of the Notification
+     * @param intent Intent to be launched when the Notification is clicked. Can be null.
+     */
+    /* package */ static Notification constructFinishNotification(
+            Context context, String description, Intent intent) {
+        final Notification notification = new Notification();
+        notification.flags |= Notification.FLAG_AUTO_CANCEL;
+        notification.icon = android.R.drawable.stat_sys_download_done;
+        notification.setLatestEventInfo(context, description, description,
+                PendingIntent.getActivity(context, 0, intent, 0));
+        return notification;
+    }
 }