diff options
author | Daisuke Miyakawa <dmiyakawa@google.com> | 2010-11-18 17:28:02 -0800 |
---|---|---|
committer | Daisuke Miyakawa <dmiyakawa@google.com> | 2010-11-22 10:03:42 -0800 |
commit | ab59660a17e896593f2a07c2e1191c2c23e3e353 (patch) | |
tree | 14274e8f6be029f1ae45e3472427b5e654b2dbf0 /src/com | |
parent | 53e3176296eeaab1caff0712c946bb77c3cd72f3 (diff) | |
download | packages_apps_Contacts-ab59660a17e896593f2a07c2e1191c2c23e3e353.tar.gz packages_apps_Contacts-ab59660a17e896593f2a07c2e1191c2c23e3e353.tar.bz2 packages_apps_Contacts-ab59660a17e896593f2a07c2e1191c2c23e3e353.zip |
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
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/android/contacts/vcard/CancelActivity.java | 149 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/CancelImportActivity.java | 128 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/CancelRequest.java (renamed from src/com/android/contacts/vcard/CancelImportRequest.java) | 18 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ExportProcessor.java | 77 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ImportProcessor.java | 89 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ImportProgressNotifier.java | 66 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ImportRequest.java | 18 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ImportVCardActivity.java | 18 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/ProcessorBase.java | 7 | ||||
-rw-r--r-- | src/com/android/contacts/vcard/VCardService.java | 220 |
10 files changed, 456 insertions, 334 deletions
diff --git a/src/com/android/contacts/vcard/CancelActivity.java b/src/com/android/contacts/vcard/CancelActivity.java new file mode 100644 index 000000000..5e3906bdd --- /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 b52ee98a6..000000000 --- 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 index dd10187f7..85893bac1 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 c54df4948..028bcc754 100644 --- a/src/com/android/contacts/vcard/ExportProcessor.java +++ b/src/com/android/contacts/vcard/ExportProcessor.java @@ -48,7 +48,7 @@ public class ExportProcessor extends ProcessorBase { 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 @@ public class ExportProcessor extends ProcessorBase { @Override public final int getType() { - return PROCESSOR_TYPE_EXPORT; + return VCardService.TYPE_EXPORT; } @Override @@ -70,6 +70,13 @@ public class ExportProcessor extends ProcessorBase { // 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 @@ public class ExportProcessor extends ProcessorBase { 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 @@ public class ExportProcessor extends ProcessorBase { 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 @@ public class ExportProcessor extends ProcessorBase { 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 @@ public class ExportProcessor extends ProcessorBase { } } - 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); + 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); + } - mNotificationManager.notify(VCardService.EXPORT_NOTIFICATION_ID, notification); + 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 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 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 049d0a906..5b5ae7992 100644 --- a/src/com/android/contacts/vcard/ImportProcessor.java +++ b/src/com/android/contacts/vcard/ImportProcessor.java @@ -38,6 +38,7 @@ import android.content.Intent; 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 @@ public class ImportProcessor extends ProcessorBase { 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 @@ public class ImportProcessor extends ProcessorBase { // 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 @@ public class ImportProcessor extends ProcessorBase { 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 @@ public class ImportProcessor extends ProcessorBase { // 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 @@ public class ImportProcessor extends ProcessorBase { } } - /* - 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 @@ public class ImportProcessor extends ProcessorBase { } 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 @@ public class ImportProcessor extends ProcessorBase { 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 @@ public class ImportProcessor extends ProcessorBase { @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 @@ public class ImportProcessor extends ProcessorBase { @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 6a24bc0e3..d6d0f3f32 100644 --- a/src/com/android/contacts/vcard/ImportProgressNotifier.java +++ b/src/com/android/contacts/vcard/ImportProgressNotifier.java @@ -21,27 +21,30 @@ import com.android.vcard.VCardEntryHandler; 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 @@ public class ImportProgressNotifier implements VCardEntryHandler { 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 @@ public class ImportProgressNotifier implements VCardEntryHandler { 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 5d4616619..e8b56068b 100644 --- a/src/com/android/contacts/vcard/ImportRequest.java +++ b/src/com/android/contacts/vcard/ImportRequest.java @@ -35,7 +35,22 @@ public class ImportRequest { * 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 class ImportRequest { */ 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 7a72ca213..948b46562 100644 --- a/src/com/android/contacts/vcard/ImportVCardActivity.java +++ b/src/com/android/contacts/vcard/ImportVCardActivity.java @@ -284,7 +284,8 @@ public class ImportVCardActivity extends Activity { 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 @@ public class ImportVCardActivity extends Activity { } /** - * 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 @@ public class ImportVCardActivity extends Activity { 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 17ac6d3e3..833090d91 100644 --- a/src/com/android/contacts/vcard/ProcessorBase.java +++ b/src/com/android/contacts/vcard/ProcessorBase.java @@ -33,12 +33,9 @@ import java.util.concurrent.TimeUnit; */ 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 367634cb5..cad9fc6dc 100644 --- a/src/com/android/contacts/vcard/VCardService.java +++ b/src/com/android/contacts/vcard/VCardService.java @@ -17,13 +17,19 @@ package com.android.contacts.vcard; 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 @@ public class VCardService extends Service { /* 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 @@ public class VCardService extends Service { 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 @@ public class VCardService extends Service { } }); + 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 @@ public class VCardService extends Service { 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 @@ public class VCardService extends Service { } 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 @@ public class VCardService extends Service { } } } + + /** + * 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; + } } |