summaryrefslogtreecommitdiffstats
path: root/src/com/android/contacts/common/vcard
diff options
context:
space:
mode:
authorChiao Cheng <chiaocheng@google.com>2012-12-03 17:15:58 -0800
committerChiao Cheng <chiaocheng@google.com>2012-12-03 17:15:58 -0800
commit7903d2473e1120e32fa5380a7d7532d0a21e2180 (patch)
treee38ffca11c65aeb7b12aeac85035548f1f7f4a2f /src/com/android/contacts/common/vcard
parentf0fb1f31683808c3ebfce234dc0a882e6e6cac1c (diff)
downloadpackages_apps_ContactsCommon-7903d2473e1120e32fa5380a7d7532d0a21e2180.tar.gz
packages_apps_ContactsCommon-7903d2473e1120e32fa5380a7d7532d0a21e2180.tar.bz2
packages_apps_ContactsCommon-7903d2473e1120e32fa5380a7d7532d0a21e2180.zip
Moving vcard UI to ContactsCommon.
Moving all class in vcard directory in preparation to move ImportExportDialogFragment. Bug: 6993891 Change-Id: I4391c6e63d20ebe91e240001885a6ce18388e51f
Diffstat (limited to 'src/com/android/contacts/common/vcard')
-rw-r--r--src/com/android/contacts/common/vcard/CancelActivity.java133
-rw-r--r--src/com/android/contacts/common/vcard/CancelRequest.java32
-rw-r--r--src/com/android/contacts/common/vcard/ExportProcessor.java292
-rw-r--r--src/com/android/contacts/common/vcard/ExportRequest.java35
-rw-r--r--src/com/android/contacts/common/vcard/ExportVCardActivity.java302
-rw-r--r--src/com/android/contacts/common/vcard/ImportProcessor.java305
-rw-r--r--src/com/android/contacts/common/vcard/ImportRequest.java110
-rw-r--r--src/com/android/contacts/common/vcard/ImportVCardActivity.java1032
-rw-r--r--src/com/android/contacts/common/vcard/NfcImportVCardActivity.java266
-rw-r--r--src/com/android/contacts/common/vcard/NotificationImportExportListener.java289
-rw-r--r--src/com/android/contacts/common/vcard/ProcessorBase.java75
-rw-r--r--src/com/android/contacts/common/vcard/SelectAccountActivity.java114
-rw-r--r--src/com/android/contacts/common/vcard/VCardCommonArguments.java27
-rw-r--r--src/com/android/contacts/common/vcard/VCardImportExportListener.java36
-rw-r--r--src/com/android/contacts/common/vcard/VCardService.java537
15 files changed, 3585 insertions, 0 deletions
diff --git a/src/com/android/contacts/common/vcard/CancelActivity.java b/src/com/android/contacts/common/vcard/CancelActivity.java
new file mode 100644
index 00000000..20869f74
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/CancelActivity.java
@@ -0,0 +1,133 @@
+/*
+ * 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.common.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.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+
+/**
+ * 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;
+
+ @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 message;
+ if (mType == VCardService.TYPE_IMPORT) {
+ message = getString(R.string.cancel_import_confirmation_message, mDisplayName);
+ } else {
+ message = getString(R.string.cancel_export_confirmation_message, mDisplayName);
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .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)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .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 binder) {
+ VCardService service = ((VCardService.MyBinder) binder).getService();
+
+ try {
+ final CancelRequest request = new CancelRequest(mJobId, mDisplayName);
+ service.handleCancelRequest(request, null);
+ } finally {
+ unbindService(this);
+ }
+
+ finish();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ // do nothing
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/CancelRequest.java b/src/com/android/contacts/common/vcard/CancelRequest.java
new file mode 100644
index 00000000..a5eb4aa3
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/CancelRequest.java
@@ -0,0 +1,32 @@
+/*
+ * 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.common.vcard;
+
+/**
+ * Class representing one request for canceling vCard import/export.
+ */
+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;
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/ExportProcessor.java b/src/com/android/contacts/common/vcard/ExportProcessor.java
new file mode 100644
index 00000000..0d5c2f35
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ExportProcessor.java
@@ -0,0 +1,292 @@
+/*
+ * 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.common.vcard;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContactsEntity;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.vcard.VCardComposer;
+import com.android.vcard.VCardConfig;
+
+import java.io.BufferedWriter;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+/**
+ * Class for processing one export request from a user. Dropped after exporting requested Uri(s).
+ * {@link VCardService} will create another object when there is another export request.
+ */
+public class ExportProcessor extends ProcessorBase {
+ private static final String LOG_TAG = "VCardExport";
+ private static final boolean DEBUG = VCardService.DEBUG;
+
+ private final VCardService mService;
+ private final ContentResolver mResolver;
+ private final NotificationManager mNotificationManager;
+ private final ExportRequest mExportRequest;
+ private final int mJobId;
+ private final String mCallingActivity;
+
+
+ private volatile boolean mCanceled;
+ private volatile boolean mDone;
+
+ public ExportProcessor(VCardService service, ExportRequest exportRequest, int jobId,
+ String callingActivity) {
+ mService = service;
+ mResolver = service.getContentResolver();
+ mNotificationManager =
+ (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
+ mExportRequest = exportRequest;
+ mJobId = jobId;
+ mCallingActivity = callingActivity;
+ }
+
+ @Override
+ public final int getType() {
+ return VCardService.TYPE_EXPORT;
+ }
+
+ @Override
+ public void run() {
+ // 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;
+ } finally {
+ synchronized (this) {
+ mDone = true;
+ }
+ }
+ }
+
+ private void runInternal() {
+ if (DEBUG) Log.d(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
+ final ExportRequest request = mExportRequest;
+ VCardComposer composer = null;
+ Writer writer = null;
+ boolean successful = false;
+ try {
+ if (isCancelled()) {
+ Log.i(LOG_TAG, "Export request is cancelled before handling the request");
+ return;
+ }
+ final Uri uri = request.destUri;
+ final OutputStream outputStream;
+ try {
+ outputStream = mResolver.openOutputStream(uri);
+ } catch (FileNotFoundException e) {
+ Log.w(LOG_TAG, "FileNotFoundException thrown", e);
+ // Need concise title.
+
+ final String errorReason =
+ mService.getString(R.string.fail_reason_could_not_open_file,
+ uri, e.getMessage());
+ doFinishNotification(errorReason, null);
+ return;
+ }
+
+ final String exportType = request.exportType;
+ final int vcardType;
+ if (TextUtils.isEmpty(exportType)) {
+ vcardType = VCardConfig.getVCardTypeFromString(
+ mService.getString(R.string.config_export_vcard_type));
+ } else {
+ vcardType = VCardConfig.getVCardTypeFromString(exportType);
+ }
+
+ composer = new VCardComposer(mService, vcardType, true);
+
+ // for test
+ // int vcardType = (VCardConfig.VCARD_TYPE_V21_GENERIC |
+ // VCardConfig.FLAG_USE_QP_TO_PRIMARY_PROPERTIES);
+ // composer = new VCardComposer(ExportVCardActivity.this, vcardType, true);
+
+ writer = new BufferedWriter(new OutputStreamWriter(outputStream));
+ final Uri contentUriForRawContactsEntity = RawContactsEntity.CONTENT_URI.buildUpon()
+ .appendQueryParameter(RawContactsEntity.FOR_EXPORT_ONLY, "1")
+ .build();
+ // TODO: should provide better selection.
+ if (!composer.init(Contacts.CONTENT_URI, new String[] {Contacts._ID},
+ null, null,
+ null, contentUriForRawContactsEntity)) {
+ final String errorReason = composer.getErrorReason();
+ Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
+ final String translatedErrorReason =
+ translateComposerError(errorReason);
+ final String title =
+ mService.getString(R.string.fail_reason_could_not_initialize_exporter,
+ translatedErrorReason);
+ doFinishNotification(title, null);
+ return;
+ }
+
+ final int total = composer.getCount();
+ if (total == 0) {
+ final String title =
+ mService.getString(R.string.fail_reason_no_exportable_contact);
+ doFinishNotification(title, null);
+ return;
+ }
+
+ int current = 1; // 1-origin
+ while (!composer.isAfterLast()) {
+ if (isCancelled()) {
+ Log.i(LOG_TAG, "Export request is cancelled during composing vCard");
+ return;
+ }
+ try {
+ writer.write(composer.createOneEntry());
+ } catch (IOException e) {
+ final String errorReason = composer.getErrorReason();
+ Log.e(LOG_TAG, "Failed to read a contact: " + errorReason);
+ final String translatedErrorReason =
+ translateComposerError(errorReason);
+ final String title =
+ mService.getString(R.string.fail_reason_error_occurred_during_export,
+ translatedErrorReason);
+ doFinishNotification(title, null);
+ return;
+ }
+
+ // vCard export is quite fast (compared to import), and frequent notifications
+ // bother notification bar too much.
+ if (current % 100 == 1) {
+ doProgressNotification(uri, total, current);
+ }
+ current++;
+ }
+ Log.i(LOG_TAG, "Successfully finished exporting vCard " + request.destUri);
+
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Ask MediaScanner to scan the file: " + request.destUri.getPath());
+ }
+ mService.updateMediaScanner(request.destUri.getPath());
+
+ successful = true;
+ final String filename = uri.getLastPathSegment();
+ final String title = mService.getString(R.string.exporting_vcard_finished_title,
+ filename);
+ doFinishNotification(title, null);
+ } finally {
+ if (composer != null) {
+ composer.terminate();
+ }
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "IOException is thrown during close(). Ignored. " + e);
+ }
+ }
+ mService.handleFinishExportNotification(mJobId, successful);
+ }
+ }
+
+ private String translateComposerError(String errorMessage) {
+ final Resources resources = mService.getResources();
+ if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
+ return resources.getString(R.string.composer_failed_to_get_database_infomation);
+ } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
+ return resources.getString(R.string.composer_has_no_exportable_contact);
+ } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
+ return resources.getString(R.string.composer_not_initialized);
+ } else {
+ return errorMessage;
+ }
+ }
+
+ 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, displayName);
+ final String tickerText =
+ mService.getString(R.string.exporting_contact_list_title);
+ final Notification notification =
+ NotificationImportExportListener.constructProgressNotification(mService,
+ VCardService.TYPE_EXPORT, description, tickerText, mJobId, displayName,
+ totalCount, currentCount);
+ mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+ mJobId, notification);
+ }
+
+ private void doCancelNotification() {
+ if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
+ final String description = mService.getString(R.string.exporting_vcard_canceled_title,
+ mExportRequest.destUri.getLastPathSegment());
+ final Notification notification =
+ NotificationImportExportListener.constructCancelNotification(mService, description);
+ mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+ mJobId, notification);
+ }
+
+ private void doFinishNotification(final String title, final String description) {
+ if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
+ final Intent intent = new Intent();
+ intent.setClassName(mService, mCallingActivity);
+ final Notification notification =
+ NotificationImportExportListener.constructFinishNotification(mService, title,
+ description, intent);
+ mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+ mJobId, notification);
+ }
+
+ @Override
+ public synchronized boolean cancel(boolean mayInterruptIfRunning) {
+ if (DEBUG) Log.d(LOG_TAG, "received cancel request");
+ if (mDone || mCanceled) {
+ return false;
+ }
+ mCanceled = true;
+ return true;
+ }
+
+ @Override
+ public synchronized boolean isCancelled() {
+ return mCanceled;
+ }
+
+ @Override
+ public synchronized boolean isDone() {
+ return mDone;
+ }
+
+ public ExportRequest getRequest() {
+ return mExportRequest;
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/ExportRequest.java b/src/com/android/contacts/common/vcard/ExportRequest.java
new file mode 100644
index 00000000..e05a32c5
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ExportRequest.java
@@ -0,0 +1,35 @@
+/*
+ * 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.common.vcard;
+
+import android.net.Uri;
+
+public class ExportRequest {
+ public final Uri destUri;
+ /**
+ * Can be null.
+ */
+ public final String exportType;
+
+ public ExportRequest(Uri destUri) {
+ this(destUri, null);
+ }
+
+ public ExportRequest(Uri destUri, String exportType) {
+ this.destUri = destUri;
+ this.exportType = exportType;
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/ExportVCardActivity.java b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
new file mode 100644
index 00000000..3d6f6022
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ExportVCardActivity.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2009 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.common.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.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+
+import java.io.File;
+
+/**
+ * Shows a dialog confirming the export and asks actual vCard export to {@link VCardService}
+ *
+ * This Activity first connects to VCardService and ask an available file name and shows it to
+ * a user. After the user's confirmation, it send export request with the file name, assuming the
+ * file name is not reserved yet.
+ */
+public class ExportVCardActivity extends Activity implements ServiceConnection,
+ DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+ private static final String LOG_TAG = "VCardExport";
+ private static final boolean DEBUG = VCardService.DEBUG;
+
+ /**
+ * Handler used when some Message has come from {@link VCardService}.
+ */
+ private class IncomingHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ if (DEBUG) Log.d(LOG_TAG, "IncomingHandler received message.");
+
+ if (msg.arg1 != 0) {
+ Log.i(LOG_TAG, "Message returned from vCard server contains error code.");
+ if (msg.obj != null) {
+ mErrorReason = (String)msg.obj;
+ }
+ showDialog(msg.arg1);
+ return;
+ }
+
+ switch (msg.what) {
+ case VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION:
+ if (msg.obj == null) {
+ Log.w(LOG_TAG, "Message returned from vCard server doesn't contain valid path");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ } else {
+ mTargetFileName = (String)msg.obj;
+ if (TextUtils.isEmpty(mTargetFileName)) {
+ Log.w(LOG_TAG, "Destination file name coming from vCard service is empty.");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ } else {
+ if (DEBUG) {
+ Log.d(LOG_TAG,
+ String.format("Target file name is set (%s). " +
+ "Show confirmation dialog", mTargetFileName));
+ }
+ showDialog(R.id.dialog_export_confirmation);
+ }
+ }
+ break;
+ default:
+ Log.w(LOG_TAG, "Unknown message type: " + msg.what);
+ super.handleMessage(msg);
+ }
+ }
+ }
+
+ /**
+ * True when this Activity is connected to {@link VCardService}.
+ *
+ * Should be touched inside synchronized block.
+ */
+ private boolean mConnected;
+
+ /**
+ * True when users need to do something and this Activity should not disconnect from
+ * VCardService. False when all necessary procedures are done (including sending export request)
+ * or there's some error occured.
+ */
+ private volatile boolean mProcessOngoing = true;
+
+ private VCardService mService;
+ private final Messenger mIncomingMessenger = new Messenger(new IncomingHandler());
+
+ // Used temporarily when asking users to confirm the file name
+ private String mTargetFileName;
+
+ // String for storing error reason temporarily.
+ private String mErrorReason;
+
+ private class ExportConfirmationListener implements DialogInterface.OnClickListener {
+ private final Uri mDestinationUri;
+
+ public ExportConfirmationListener(String path) {
+ this(Uri.parse("file://" + path));
+ }
+
+ public ExportConfirmationListener(Uri uri) {
+ mDestinationUri = uri;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (DEBUG) {
+ Log.d(LOG_TAG,
+ String.format("Try sending export request (uri: %s)", mDestinationUri));
+ }
+ final ExportRequest request = new ExportRequest(mDestinationUri);
+ // The connection object will call finish().
+ mService.handleExportRequest(request, new NotificationImportExportListener(
+ ExportVCardActivity.this));
+ }
+ unbindAndFinish();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ // Check directory is available.
+ if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ Log.w(LOG_TAG, "External storage is in state " + Environment.getExternalStorageState() +
+ ". Cancelling export");
+ showDialog(R.id.dialog_sdcard_not_found);
+ return;
+ }
+
+ final File targetDirectory = Environment.getExternalStorageDirectory();
+ if (!(targetDirectory.exists() &&
+ targetDirectory.isDirectory() &&
+ targetDirectory.canRead()) &&
+ !targetDirectory.mkdirs()) {
+ showDialog(R.id.dialog_sdcard_not_found);
+ return;
+ }
+
+ final String callingActivity = getIntent().getExtras()
+ .getString(VCardCommonArguments.ARG_CALLING_ACTIVITY);
+ Intent intent = new Intent(this, VCardService.class);
+ intent.putExtra(VCardCommonArguments.ARG_CALLING_ACTIVITY, callingActivity);
+
+ if (startService(intent) == null) {
+ Log.e(LOG_TAG, "Failed to start vCard service");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ return;
+ }
+
+ if (!bindService(intent, this, Context.BIND_AUTO_CREATE)) {
+ Log.e(LOG_TAG, "Failed to connect to vCard service.");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ }
+ // Continued to onServiceConnected()
+ }
+
+ @Override
+ public synchronized void onServiceConnected(ComponentName name, IBinder binder) {
+ if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
+ mConnected = true;
+ mService = ((VCardService.MyBinder) binder).getService();
+ mService.handleRequestAvailableExportDestination(mIncomingMessenger);
+ // Wait until MSG_SET_AVAILABLE_EXPORT_DESTINATION message is available.
+ }
+
+ // Use synchronized since we don't want to call unbindAndFinish() just after this call.
+ @Override
+ public synchronized void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Log.d(LOG_TAG, "onServiceDisconnected()");
+ mService = null;
+ mConnected = false;
+ if (mProcessOngoing) {
+ // Unexpected disconnect event.
+ Log.w(LOG_TAG, "Disconnected from service during the process ongoing.");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id, Bundle bundle) {
+ switch (id) {
+ case R.id.dialog_export_confirmation: {
+ return new AlertDialog.Builder(this)
+ .setTitle(R.string.confirm_export_title)
+ .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
+ .setPositiveButton(android.R.string.ok,
+ new ExportConfirmationListener(mTargetFileName))
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
+ }
+ case R.string.fail_reason_too_many_vcard: {
+ mProcessOngoing = false;
+ return new AlertDialog.Builder(this)
+ .setTitle(R.string.exporting_contact_failed_title)
+ .setMessage(getString(R.string.exporting_contact_failed_message,
+ getString(R.string.fail_reason_too_many_vcard)))
+ .setPositiveButton(android.R.string.ok, this)
+ .create();
+ }
+ case R.id.dialog_fail_to_export_with_reason: {
+ mProcessOngoing = false;
+ return new AlertDialog.Builder(this)
+ .setTitle(R.string.exporting_contact_failed_title)
+ .setMessage(getString(R.string.exporting_contact_failed_message,
+ mErrorReason != null ? mErrorReason :
+ getString(R.string.fail_reason_unknown)))
+ .setPositiveButton(android.R.string.ok, this)
+ .setOnCancelListener(this)
+ .create();
+ }
+ case R.id.dialog_sdcard_not_found: {
+ mProcessOngoing = false;
+ return new AlertDialog.Builder(this)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.no_sdcard_message)
+ .setPositiveButton(android.R.string.ok, this).create();
+ }
+ }
+ return super.onCreateDialog(id, bundle);
+ }
+
+ @Override
+ protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
+ if (id == R.id.dialog_fail_to_export_with_reason) {
+ ((AlertDialog)dialog).setMessage(mErrorReason);
+ } else if (id == R.id.dialog_export_confirmation) {
+ ((AlertDialog)dialog).setMessage(
+ getString(R.string.confirm_export_message, mTargetFileName));
+ } else {
+ super.onPrepareDialog(id, dialog, args);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ if (!isFinishing()) {
+ unbindAndFinish();
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onClick() is called");
+ unbindAndFinish();
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onCancel() is called");
+ mProcessOngoing = false;
+ unbindAndFinish();
+ }
+
+ @Override
+ public void unbindService(ServiceConnection conn) {
+ mProcessOngoing = false;
+ super.unbindService(conn);
+ }
+
+ private synchronized void unbindAndFinish() {
+ if (mConnected) {
+ unbindService(this);
+ mConnected = false;
+ }
+ finish();
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/ImportProcessor.java b/src/com/android/contacts/common/vcard/ImportProcessor.java
new file mode 100644
index 00000000..37128755
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ImportProcessor.java
@@ -0,0 +1,305 @@
+/*
+ * 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.common.vcard;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryCommitter;
+import com.android.vcard.VCardEntryConstructor;
+import com.android.vcard.VCardEntryHandler;
+import com.android.vcard.VCardInterpreter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardNotSupportedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class for processing one import request from a user. Dropped after importing requested Uri(s).
+ * {@link VCardService} will create another object when there is another import request.
+ */
+public class ImportProcessor extends ProcessorBase implements VCardEntryHandler {
+ private static final String LOG_TAG = "VCardImport";
+ private static final boolean DEBUG = VCardService.DEBUG;
+
+ private final VCardService mService;
+ private final ContentResolver mResolver;
+ private final ImportRequest mImportRequest;
+ private final int mJobId;
+ private final VCardImportExportListener mListener;
+
+ // TODO: remove and show appropriate message instead.
+ private final List<Uri> mFailedUris = new ArrayList<Uri>();
+
+ private VCardParser mVCardParser;
+
+ private volatile boolean mCanceled;
+ private volatile boolean mDone;
+
+ private int mCurrentCount = 0;
+ private int mTotalCount = 0;
+
+ public ImportProcessor(final VCardService service, final VCardImportExportListener listener,
+ final ImportRequest request, final int jobId) {
+ mService = service;
+ mResolver = mService.getContentResolver();
+ mListener = listener;
+
+ mImportRequest = request;
+ mJobId = jobId;
+ }
+
+ @Override
+ public void onStart() {
+ // do nothing
+ }
+
+ @Override
+ public void onEnd() {
+ // do nothing
+ }
+
+ @Override
+ public void onEntryCreated(VCardEntry entry) {
+ mCurrentCount++;
+ if (mListener != null) {
+ mListener.onImportParsed(mImportRequest, mJobId, entry, mCurrentCount, mTotalCount);
+ }
+ }
+
+ @Override
+ public final int getType() {
+ return VCardService.TYPE_IMPORT;
+ }
+
+ @Override
+ public void run() {
+ // ExecutorService ignores RuntimeException, so we need to show it here.
+ try {
+ runInternal();
+
+ if (isCancelled() && mListener != null) {
+ mListener.onImportCanceled(mImportRequest, mJobId);
+ }
+ } 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;
+ } finally {
+ synchronized (this) {
+ mDone = true;
+ }
+ }
+ }
+
+ private void runInternal() {
+ Log.i(LOG_TAG, String.format("vCard import (id: %d) has started.", mJobId));
+ final ImportRequest request = mImportRequest;
+ if (isCancelled()) {
+ Log.i(LOG_TAG, "Canceled before actually handling parameter (" + request.uri + ")");
+ return;
+ }
+ final int[] possibleVCardVersions;
+ if (request.vcardVersion == ImportVCardActivity.VCARD_VERSION_AUTO_DETECT) {
+ /**
+ * Note: this code assumes that a given Uri is able to be opened more than once,
+ * which may not be true in certain conditions.
+ */
+ possibleVCardVersions = new int[] {
+ ImportVCardActivity.VCARD_VERSION_V21,
+ ImportVCardActivity.VCARD_VERSION_V30
+ };
+ } else {
+ possibleVCardVersions = new int[] {
+ request.vcardVersion
+ };
+ }
+
+ final Uri uri = request.uri;
+ final Account account = request.account;
+ final int estimatedVCardType = request.estimatedVCardType;
+ final String estimatedCharset = request.estimatedCharset;
+ final int entryCount = request.entryCount;
+ mTotalCount += entryCount;
+
+ final VCardEntryConstructor constructor =
+ new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
+ final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
+ constructor.addEntryHandler(committer);
+ constructor.addEntryHandler(this);
+
+ InputStream is = null;
+ boolean successful = false;
+ try {
+ if (uri != null) {
+ Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")");
+ is = mResolver.openInputStream(uri);
+ } else if (request.data != null){
+ Log.i(LOG_TAG, "start importing one vCard (byte[])");
+ is = new ByteArrayInputStream(request.data);
+ }
+
+ if (is != null) {
+ successful = readOneVCard(is, estimatedVCardType, estimatedCharset, constructor,
+ possibleVCardVersions);
+ }
+ } catch (IOException e) {
+ successful = false;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+ }
+
+ mService.handleFinishImportNotification(mJobId, successful);
+
+ if (successful) {
+ // TODO: successful becomes true even when cancelled. Should return more appropriate
+ // 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 (mListener != null) {
+ if (uris != null && uris.size() > 0) {
+ // TODO: construct intent showing a list of imported contact list.
+ mListener.onImportFinished(mImportRequest, mJobId, 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.");
+ mListener.onImportFinished(mImportRequest, mJobId, null);
+ }
+ }
+ }
+ } else {
+ Log.w(LOG_TAG, "Failed to read one vCard file: " + uri);
+ mFailedUris.add(uri);
+ }
+ }
+
+ private boolean readOneVCard(InputStream is, int vcardType, String charset,
+ final VCardInterpreter interpreter,
+ final int[] possibleVCardVersions) {
+ boolean successful = false;
+ final int length = possibleVCardVersions.length;
+ for (int i = 0; i < length; i++) {
+ final int vcardVersion = possibleVCardVersions[i];
+ try {
+ if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
+ // Let the object clean up internal temporary objects,
+ ((VCardEntryConstructor) interpreter).clear();
+ }
+
+ // We need synchronized block here,
+ // since we need to handle mCanceled and mVCardParser at once.
+ // In the worst case, a user may call cancel() just before creating
+ // mVCardParser.
+ synchronized (this) {
+ mVCardParser = (vcardVersion == ImportVCardActivity.VCARD_VERSION_V30 ?
+ new VCardParser_V30(vcardType) :
+ new VCardParser_V21(vcardType));
+ if (isCancelled()) {
+ Log.i(LOG_TAG, "ImportProcessor already recieves cancel request, so " +
+ "send cancel request to vCard parser too.");
+ mVCardParser.cancel();
+ }
+ }
+ mVCardParser.parse(is, interpreter);
+
+ successful = true;
+ break;
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+ } catch (VCardNestedException e) {
+ // This exception should not be thrown here. We should instead handle it
+ // in the preprocessing session in ImportVCardActivity, as we don't try
+ // to detect the type of given vCard here.
+ //
+ // TODO: Handle this case appropriately, which should mean we have to have
+ // code trying to auto-detect the type of given vCard twice (both in
+ // ImportVCardActivity and ImportVCardService).
+ Log.e(LOG_TAG, "Nested Exception is found.");
+ } catch (VCardNotSupportedException e) {
+ Log.e(LOG_TAG, e.toString());
+ } catch (VCardVersionException e) {
+ if (i == length - 1) {
+ Log.e(LOG_TAG, "Appropriate version for this vCard is not found.");
+ } else {
+ // We'll try the other (v30) version.
+ }
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, e.toString());
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ return successful;
+ }
+
+ @Override
+ public synchronized boolean cancel(boolean mayInterruptIfRunning) {
+ if (DEBUG) Log.d(LOG_TAG, "ImportProcessor received cancel request");
+ if (mDone || mCanceled) {
+ return false;
+ }
+ mCanceled = true;
+ synchronized (this) {
+ if (mVCardParser != null) {
+ mVCardParser.cancel();
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized boolean isCancelled() {
+ return mCanceled;
+ }
+
+
+ @Override
+ public synchronized boolean isDone() {
+ return mDone;
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/ImportRequest.java b/src/com/android/contacts/common/vcard/ImportRequest.java
new file mode 100644
index 00000000..0fab8f51
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ImportRequest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.common.vcard;
+
+import android.accounts.Account;
+import android.net.Uri;
+
+import com.android.vcard.VCardSourceDetector;
+
+/**
+ * Class representing one request for importing vCard (given as a Uri).
+ *
+ * Mainly used when {@link ImportVCardActivity} requests {@link VCardService}
+ * to import some specific Uri.
+ *
+ * Note: This object's accepting only One Uri does NOT mean that
+ * there's only one vCard entry inside the instance, as one Uri often has multiple
+ * vCard entries inside it.
+ */
+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 #displayName} instead.
+ *
+ * If this is null {@link #data} contains the byte stream of the vcard.
+ */
+ public final Uri uri;
+
+ /**
+ * Holds the byte stream of the vcard, if {@link #uri} is null.
+ */
+ public final byte[] data;
+
+ /**
+ * String to be displayed to the user to indicate the source of the VCARD.
+ */
+ public final String displayName;
+
+ /**
+ * Can be {@link VCardSourceDetector#PARSE_TYPE_UNKNOWN}.
+ */
+ public final int estimatedVCardType;
+
+ /**
+ * Can be null, meaning no preferable charset is available.
+ */
+ public final String estimatedCharset;
+
+ /**
+ * Assumes that one Uri contains only one version, while there's a (tiny) possibility
+ * we may have two types in one vCard.
+ *
+ * e.g.
+ * BEGIN:VCARD
+ * VERSION:2.1
+ * ...
+ * END:VCARD
+ * BEGIN:VCARD
+ * VERSION:3.0
+ * ...
+ * END:VCARD
+ *
+ * We've never seen this kind of a file, but we may have to cope with it in the future.
+ */
+ public final int vcardVersion;
+
+ /**
+ * The count of vCard entries in {@link #uri}. A receiver of this object can use it
+ * when showing the progress of import. Thus a receiver must be able to torelate this
+ * variable being invalid because of vCard's limitation.
+ *
+ * vCard does not let us know this count without looking over a whole file content,
+ * which means we have to open and scan over {@link #uri} to know this value, while
+ * it may not be opened more than once (Uri does not require it to be opened multiple times
+ * and may become invalid after its close() request).
+ */
+ public final int entryCount;
+
+ public ImportRequest(Account account,
+ byte[] data, Uri uri, String displayName, int estimatedType, String estimatedCharset,
+ int vcardVersion, int entryCount) {
+ this.account = account;
+ this.data = data;
+ this.uri = uri;
+ this.displayName = displayName;
+ this.estimatedVCardType = estimatedType;
+ this.estimatedCharset = estimatedCharset;
+ this.vcardVersion = vcardVersion;
+ this.entryCount = entryCount;
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/ImportVCardActivity.java b/src/com/android/contacts/common/vcard/ImportVCardActivity.java
new file mode 100644
index 00000000..70c98215
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ImportVCardActivity.java
@@ -0,0 +1,1032 @@
+/*
+ * Copyright (C) 2009 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.common.vcard;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.provider.OpenableColumns;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.RelativeSizeSpan;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.AccountSelectionUtil;
+import com.android.vcard.VCardEntryCounter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.VCardSourceDetector;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+
+/**
+ * The class letting users to import vCard. This includes the UI part for letting them select
+ * an Account and posssibly a file if there's no Uri is given from its caller Activity.
+ *
+ * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
+ * finished (with the method {@link Activity#finish()}) after the import and never reuse
+ * any Dialog in the instance. So this code is careless about the management around managed
+ * dialogs stuffs (like how onCreateDialog() is used).
+ */
+public class ImportVCardActivity extends Activity {
+ private static final String LOG_TAG = "VCardImport";
+
+ private static final int SELECT_ACCOUNT = 0;
+
+ /* package */ static final String VCARD_URI_ARRAY = "vcard_uri";
+ /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type";
+ /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset";
+ /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version";
+ /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count";
+
+ /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
+ /* package */ final static int VCARD_VERSION_V21 = 1;
+ /* package */ final static int VCARD_VERSION_V30 = 2;
+
+ private static final String SECURE_DIRECTORY_NAME = ".android_secure";
+
+ /**
+ * Notification id used when error happened before sending an import request to VCardServer.
+ */
+ private static final int FAILURE_NOTIFICATION_ID = 1;
+
+ final static String CACHED_URIS = "cached_uris";
+
+ private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
+
+ private AccountWithDataSet mAccount;
+
+ private ProgressDialog mProgressDialogForScanVCard;
+ private ProgressDialog mProgressDialogForCachingVCard;
+
+ private List<VCardFile> mAllVCardFileList;
+ private VCardScanThread mVCardScanThread;
+
+ private VCardCacheThread mVCardCacheThread;
+ private ImportRequestConnection mConnection;
+ /* package */ VCardImportExportListener mListener;
+
+ private String mErrorMessage;
+
+ private Handler mHandler = new Handler();
+
+ private static class VCardFile {
+ private final String mName;
+ private final String mCanonicalPath;
+ private final long mLastModified;
+
+ public VCardFile(String name, String canonicalPath, long lastModified) {
+ mName = name;
+ mCanonicalPath = canonicalPath;
+ mLastModified = lastModified;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getCanonicalPath() {
+ return mCanonicalPath;
+ }
+
+ public long getLastModified() {
+ return mLastModified;
+ }
+ }
+
+ // Runs on the UI thread.
+ private class DialogDisplayer implements Runnable {
+ private final int mResId;
+ public DialogDisplayer(int resId) {
+ mResId = resId;
+ }
+ public DialogDisplayer(String errorMessage) {
+ mResId = R.id.dialog_error_with_message;
+ mErrorMessage = errorMessage;
+ }
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ showDialog(mResId);
+ }
+ }
+ }
+
+ 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 CancelListener mCancelListener = new CancelListener();
+
+ private class ImportRequestConnection implements ServiceConnection {
+ private VCardService mService;
+
+ public void sendImportRequest(final List<ImportRequest> requests) {
+ Log.i(LOG_TAG, "Send an import request");
+ mService.handleImportRequest(requests, mListener);
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ mService = ((VCardService.MyBinder) binder).getService();
+ Log.i(LOG_TAG,
+ String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
+ Arrays.toString(mVCardCacheThread.getSourceUris())));
+ mVCardCacheThread.start();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ Log.i(LOG_TAG, "Disconnected from VCardService");
+ }
+ }
+
+ /**
+ * Caches given vCard files into a local directory, and sends actual import request to
+ * {@link VCardService}.
+ *
+ * We need to cache given files into local storage. One of reasons is that some data (as Uri)
+ * may have special permissions. Callers may allow only this Activity to access that content,
+ * not what this Activity launched (like {@link VCardService}).
+ */
+ private class VCardCacheThread extends Thread
+ implements DialogInterface.OnCancelListener {
+ private boolean mCanceled;
+ private PowerManager.WakeLock mWakeLock;
+ private VCardParser mVCardParser;
+ private final Uri[] mSourceUris; // Given from a caller.
+ private final byte[] mSource;
+ private final String mDisplayName;
+
+ public VCardCacheThread(final Uri[] sourceUris) {
+ mSourceUris = sourceUris;
+ mSource = null;
+ final Context context = ImportVCardActivity.this;
+ final PowerManager powerManager =
+ (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK |
+ PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ mDisplayName = null;
+ }
+
+ @Override
+ public void finalize() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ Log.w(LOG_TAG, "WakeLock is being held.");
+ mWakeLock.release();
+ }
+ }
+
+ @Override
+ public void run() {
+ Log.i(LOG_TAG, "vCard cache thread starts running.");
+ if (mConnection == null) {
+ throw new NullPointerException("vCard cache thread must be launched "
+ + "after a service connection is established");
+ }
+
+ mWakeLock.acquire();
+ try {
+ if (mCanceled == true) {
+ Log.i(LOG_TAG, "vCard cache operation is canceled.");
+ return;
+ }
+
+ final Context context = ImportVCardActivity.this;
+ // Uris given from caller applications may not be opened twice: consider when
+ // it is not from local storage (e.g. "file:///...") but from some special
+ // provider (e.g. "content://...").
+ // Thus we have to once copy the content of Uri into local storage, and read
+ // it after it.
+ //
+ // We may be able to read content of each vCard file during copying them
+ // to local storage, but currently vCard code does not allow us to do so.
+ int cache_index = 0;
+ ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
+ if (mSource != null) {
+ try {
+ requests.add(constructImportRequest(mSource, null, mDisplayName));
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
+ showFailureNotification(R.string.fail_reason_not_supported);
+ return;
+ }
+ } else {
+ final ContentResolver resolver =
+ ImportVCardActivity.this.getContentResolver();
+ for (Uri sourceUri : mSourceUris) {
+ String filename = null;
+ // Note: caches are removed by VCardService.
+ while (true) {
+ filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
+ final File file = context.getFileStreamPath(filename);
+ if (!file.exists()) {
+ break;
+ } else {
+ if (cache_index == Integer.MAX_VALUE) {
+ throw new RuntimeException("Exceeded cache limit");
+ }
+ cache_index++;
+ }
+ }
+ final Uri localDataUri = copyTo(sourceUri, filename);
+ if (mCanceled) {
+ Log.i(LOG_TAG, "vCard cache operation is canceled.");
+ break;
+ }
+ if (localDataUri == null) {
+ Log.w(LOG_TAG, "destUri is null");
+ break;
+ }
+
+ String displayName = null;
+ Cursor cursor = null;
+ // Try to get a display name from the given Uri. If it fails, we just
+ // pick up the last part of the Uri.
+ try {
+ cursor = resolver.query(sourceUri,
+ new String[] { OpenableColumns.DISPLAY_NAME },
+ null, null, null);
+ if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
+ if (cursor.getCount() > 1) {
+ Log.w(LOG_TAG, "Unexpected multiple rows: "
+ + cursor.getCount());
+ }
+ int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (index >= 0) {
+ displayName = cursor.getString(index);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ if (TextUtils.isEmpty(displayName)){
+ displayName = sourceUri.getLastPathSegment();
+ }
+
+ final ImportRequest request;
+ try {
+ request = constructImportRequest(null, localDataUri, displayName);
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
+ showFailureNotification(R.string.fail_reason_not_supported);
+ return;
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Unexpected IOException", e);
+ showFailureNotification(R.string.fail_reason_io_error);
+ return;
+ }
+ if (mCanceled) {
+ Log.i(LOG_TAG, "vCard cache operation is canceled.");
+ return;
+ }
+ requests.add(request);
+ }
+ }
+ if (!requests.isEmpty()) {
+ mConnection.sendImportRequest(requests);
+ } else {
+ Log.w(LOG_TAG, "Empty import requests. Ignore it.");
+ }
+ } catch (OutOfMemoryError e) {
+ Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
+ System.gc();
+ runOnUiThread(new DialogDisplayer(
+ getString(R.string.fail_reason_low_memory_during_import)));
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "IOException during caching vCard", e);
+ runOnUiThread(new DialogDisplayer(
+ getString(R.string.fail_reason_io_error)));
+ } finally {
+ Log.i(LOG_TAG, "Finished caching vCard.");
+ mWakeLock.release();
+ unbindService(mConnection);
+ mProgressDialogForCachingVCard.dismiss();
+ mProgressDialogForCachingVCard = null;
+ finish();
+ }
+ }
+
+ /**
+ * Copy the content of sourceUri to the destination.
+ */
+ private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
+ Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
+ sourceUri, filename));
+ final Context context = ImportVCardActivity.this;
+ final ContentResolver resolver = context.getContentResolver();
+ ReadableByteChannel inputChannel = null;
+ WritableByteChannel outputChannel = null;
+ Uri destUri = null;
+ try {
+ inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
+ destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
+ outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
+ final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
+ while (inputChannel.read(buffer) != -1) {
+ if (mCanceled) {
+ Log.d(LOG_TAG, "Canceled during caching " + sourceUri);
+ return null;
+ }
+ buffer.flip();
+ outputChannel.write(buffer);
+ buffer.compact();
+ }
+ buffer.flip();
+ while (buffer.hasRemaining()) {
+ outputChannel.write(buffer);
+ }
+ } finally {
+ if (inputChannel != null) {
+ try {
+ inputChannel.close();
+ } catch (IOException e) {
+ Log.w(LOG_TAG, "Failed to close inputChannel.");
+ }
+ }
+ if (outputChannel != null) {
+ try {
+ outputChannel.close();
+ } catch(IOException e) {
+ Log.w(LOG_TAG, "Failed to close outputChannel");
+ }
+ }
+ }
+ return destUri;
+ }
+
+ /**
+ * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
+ * its content.
+ *
+ * @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 displayName Used for displaying information to the user. This variable populates
+ * {@link ImportRequest#displayName}.
+ */
+ private ImportRequest constructImportRequest(final byte[] data,
+ final Uri localDataUri, final String displayName)
+ throws IOException, VCardException {
+ final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
+ VCardEntryCounter counter = null;
+ VCardSourceDetector detector = null;
+ int vcardVersion = VCARD_VERSION_V21;
+ try {
+ boolean shouldUseV30 = false;
+ InputStream is;
+ if (data != null) {
+ is = new ByteArrayInputStream(data);
+ } else {
+ is = resolver.openInputStream(localDataUri);
+ }
+ mVCardParser = new VCardParser_V21();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ mVCardParser.addInterpreter(counter);
+ mVCardParser.addInterpreter(detector);
+ mVCardParser.parse(is);
+ } catch (VCardVersionException e1) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+
+ shouldUseV30 = true;
+ if (data != null) {
+ is = new ByteArrayInputStream(data);
+ } else {
+ is = resolver.openInputStream(localDataUri);
+ }
+ mVCardParser = new VCardParser_V30();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ mVCardParser.addInterpreter(counter);
+ mVCardParser.addInterpreter(detector);
+ mVCardParser.parse(is);
+ } catch (VCardVersionException e2) {
+ throw new VCardException("vCard with unspported version.");
+ }
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
+ } catch (VCardNestedException e) {
+ Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
+ // Go through without throwing the Exception, as we may be able to detect the
+ // version before it
+ }
+ return new ImportRequest(mAccount,
+ data, localDataUri, displayName,
+ detector.getEstimatedType(),
+ detector.getEstimatedCharset(),
+ vcardVersion, counter.getCount());
+ }
+
+ public Uri[] getSourceUris() {
+ return mSourceUris;
+ }
+
+ public void cancel() {
+ mCanceled = true;
+ if (mVCardParser != null) {
+ mVCardParser.cancel();
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
+ cancel();
+ }
+ }
+
+ private class ImportTypeSelectedListener implements
+ DialogInterface.OnClickListener {
+ public static final int IMPORT_ONE = 0;
+ public static final int IMPORT_MULTIPLE = 1;
+ public static final int IMPORT_ALL = 2;
+ public static final int IMPORT_TYPE_SIZE = 3;
+
+ private int mCurrentIndex;
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ switch (mCurrentIndex) {
+ case IMPORT_ALL:
+ importVCardFromSDCard(mAllVCardFileList);
+ break;
+ case IMPORT_MULTIPLE:
+ showDialog(R.id.dialog_select_multiple_vcard);
+ break;
+ default:
+ showDialog(R.id.dialog_select_one_vcard);
+ break;
+ }
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ finish();
+ } else {
+ mCurrentIndex = which;
+ }
+ }
+ }
+
+ private class VCardSelectedListener implements
+ DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
+ private int mCurrentIndex;
+ private Set<Integer> mSelectedIndexSet;
+
+ public VCardSelectedListener(boolean multipleSelect) {
+ mCurrentIndex = 0;
+ if (multipleSelect) {
+ mSelectedIndexSet = new HashSet<Integer>();
+ }
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (mSelectedIndexSet != null) {
+ List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
+ final int size = mAllVCardFileList.size();
+ // We'd like to sort the files by its index, so we do not use Set iterator.
+ for (int i = 0; i < size; i++) {
+ if (mSelectedIndexSet.contains(i)) {
+ selectedVCardFileList.add(mAllVCardFileList.get(i));
+ }
+ }
+ importVCardFromSDCard(selectedVCardFileList);
+ } else {
+ importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
+ }
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+ finish();
+ } else {
+ // Some file is selected.
+ mCurrentIndex = which;
+ if (mSelectedIndexSet != null) {
+ if (mSelectedIndexSet.contains(which)) {
+ mSelectedIndexSet.remove(which);
+ } else {
+ mSelectedIndexSet.add(which);
+ }
+ }
+ }
+ }
+
+ public void onClick(DialogInterface dialog, int which, boolean isChecked) {
+ if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
+ Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
+ mAllVCardFileList.get(which).getCanonicalPath()));
+ } else {
+ onClick(dialog, which);
+ }
+ }
+ }
+
+ /**
+ * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
+ * a vCard file is shown. After the choice, VCardReadThread starts running.
+ */
+ private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
+ private boolean mCanceled;
+ private boolean mGotIOException;
+ private File mRootDirectory;
+
+ // To avoid recursive link.
+ private Set<String> mCheckedPaths;
+ private PowerManager.WakeLock mWakeLock;
+
+ private class CanceledException extends Exception {
+ }
+
+ public VCardScanThread(File sdcardDirectory) {
+ mCanceled = false;
+ mGotIOException = false;
+ mRootDirectory = sdcardDirectory;
+ mCheckedPaths = new HashSet<String>();
+ PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
+ Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK |
+ PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ }
+
+ @Override
+ public void run() {
+ mAllVCardFileList = new Vector<VCardFile>();
+ try {
+ mWakeLock.acquire();
+ getVCardFileRecursively(mRootDirectory);
+ } catch (CanceledException e) {
+ mCanceled = true;
+ } catch (IOException e) {
+ mGotIOException = true;
+ } finally {
+ mWakeLock.release();
+ }
+
+ if (mCanceled) {
+ mAllVCardFileList = null;
+ }
+
+ mProgressDialogForScanVCard.dismiss();
+ mProgressDialogForScanVCard = null;
+
+ if (mGotIOException) {
+ runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
+ } else if (mCanceled) {
+ finish();
+ } else {
+ int size = mAllVCardFileList.size();
+ final Context context = ImportVCardActivity.this;
+ if (size == 0) {
+ runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
+ } else {
+ startVCardSelectAndImport();
+ }
+ }
+ }
+
+ private void getVCardFileRecursively(File directory)
+ throws CanceledException, IOException {
+ if (mCanceled) {
+ throw new CanceledException();
+ }
+
+ // e.g. secured directory may return null toward listFiles().
+ final File[] files = directory.listFiles();
+ if (files == null) {
+ final String currentDirectoryPath = directory.getCanonicalPath();
+ final String secureDirectoryPath =
+ mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME);
+ if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) {
+ Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
+ }
+ return;
+ }
+ for (File file : directory.listFiles()) {
+ if (mCanceled) {
+ throw new CanceledException();
+ }
+ String canonicalPath = file.getCanonicalPath();
+ if (mCheckedPaths.contains(canonicalPath)) {
+ continue;
+ }
+
+ mCheckedPaths.add(canonicalPath);
+
+ if (file.isDirectory()) {
+ getVCardFileRecursively(file);
+ } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
+ file.canRead()){
+ String fileName = file.getName();
+ VCardFile vcardFile = new VCardFile(
+ fileName, canonicalPath, file.lastModified());
+ mAllVCardFileList.add(vcardFile);
+ }
+ }
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ mCanceled = true;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_NEGATIVE) {
+ mCanceled = true;
+ }
+ }
+ }
+
+ private void startVCardSelectAndImport() {
+ int size = mAllVCardFileList.size();
+ if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
+ size == 1) {
+ importVCardFromSDCard(mAllVCardFileList);
+ } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
+ runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type));
+ } else {
+ runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
+ }
+ }
+
+ private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
+ final int size = selectedVCardFileList.size();
+ String[] uriStrings = new String[size];
+ int i = 0;
+ for (VCardFile vcardFile : selectedVCardFileList) {
+ uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
+ i++;
+ }
+ importVCard(uriStrings);
+ }
+
+ private void importVCardFromSDCard(final VCardFile vcardFile) {
+ importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())});
+ }
+
+ private void importVCard(final Uri uri) {
+ importVCard(new Uri[] {uri});
+ }
+
+ private void importVCard(final String[] uriStrings) {
+ final int length = uriStrings.length;
+ final Uri[] uris = new Uri[length];
+ for (int i = 0; i < length; i++) {
+ uris[i] = Uri.parse(uriStrings[i]);
+ }
+ importVCard(uris);
+ }
+
+ private void importVCard(final Uri[] uris) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isFinishing()) {
+ mVCardCacheThread = new VCardCacheThread(uris);
+ mListener = new NotificationImportExportListener(ImportVCardActivity.this);
+ showDialog(R.id.dialog_cache_vcard);
+ }
+ }
+ });
+ }
+
+ private Dialog getSelectImportTypeDialog() {
+ final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener();
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.select_vcard_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setOnCancelListener(mCancelListener)
+ .setNegativeButton(android.R.string.cancel, mCancelListener);
+
+ final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
+ items[ImportTypeSelectedListener.IMPORT_ONE] =
+ getString(R.string.import_one_vcard_string);
+ items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
+ getString(R.string.import_multiple_vcard_string);
+ items[ImportTypeSelectedListener.IMPORT_ALL] =
+ getString(R.string.import_all_vcard_string);
+ builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
+ return builder.create();
+ }
+
+ private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
+ final int size = mAllVCardFileList.size();
+ final VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
+ final AlertDialog.Builder builder =
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.select_vcard_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setOnCancelListener(mCancelListener)
+ .setNegativeButton(android.R.string.cancel, mCancelListener);
+
+ CharSequence[] items = new CharSequence[size];
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ for (int i = 0; i < size; i++) {
+ VCardFile vcardFile = mAllVCardFileList.get(i);
+ SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
+ stringBuilder.append(vcardFile.getName());
+ stringBuilder.append('\n');
+ int indexToBeSpanned = stringBuilder.length();
+ // Smaller date text looks better, since each file name becomes easier to read.
+ // The value set to RelativeSizeSpan is arbitrary. You can change it to any other
+ // value (but the value bigger than 1.0f would not make nice appearance :)
+ stringBuilder.append(
+ "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")");
+ stringBuilder.setSpan(
+ new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ items[i] = stringBuilder;
+ }
+ if (multipleSelect) {
+ builder.setMultiChoiceItems(items, (boolean[])null, listener);
+ } else {
+ builder.setSingleChoiceItems(items, 0, listener);
+ }
+ return builder.create();
+ }
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ String accountName = null;
+ String accountType = null;
+ String dataSet = null;
+ final Intent intent = getIntent();
+ if (intent != null) {
+ accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
+ accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+ dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
+ } else {
+ Log.e(LOG_TAG, "intent does not exist");
+ }
+
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ mAccount = new AccountWithDataSet(accountName, accountType, dataSet);
+ } else {
+ final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+ final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+ if (accountList.size() == 0) {
+ mAccount = null;
+ } else if (accountList.size() == 1) {
+ mAccount = accountList.get(0);
+ } else {
+ startActivityForResult(new Intent(this, SelectAccountActivity.class),
+ SELECT_ACCOUNT);
+ return;
+ }
+ }
+
+ startImport();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == SELECT_ACCOUNT) {
+ if (resultCode == Activity.RESULT_OK) {
+ mAccount = new AccountWithDataSet(
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
+ intent.getStringExtra(SelectAccountActivity.DATA_SET));
+ startImport();
+ } else {
+ if (resultCode != Activity.RESULT_CANCELED) {
+ Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
+ }
+ finish();
+ }
+ }
+ }
+
+ private void startImport() {
+ Intent intent = getIntent();
+ // Handle inbound files
+ Uri uri = intent.getData();
+ if (uri != null) {
+ Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
+ importVCard(uri);
+ } else {
+ Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
+ doScanExternalStorageAndImportVCard();
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int resId, Bundle bundle) {
+ switch (resId) {
+ case R.string.import_from_sdcard: {
+ if (mAccountSelectionListener == null) {
+ throw new NullPointerException(
+ "mAccountSelectionListener must not be null.");
+ }
+ return AccountSelectionUtil.getSelectAccountDialog(this, resId,
+ mAccountSelectionListener, mCancelListener);
+ }
+ case R.id.dialog_searching_vcard: {
+ if (mProgressDialogForScanVCard == null) {
+ String message = getString(R.string.searching_vcard_message);
+ mProgressDialogForScanVCard =
+ ProgressDialog.show(this, "", message, true, false);
+ mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread);
+ mVCardScanThread.start();
+ }
+ return mProgressDialogForScanVCard;
+ }
+ case R.id.dialog_sdcard_not_found: {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(R.string.no_sdcard_message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ case R.id.dialog_vcard_not_found: {
+ final String message = getString(R.string.import_failure_no_vcard_file);
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setMessage(message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ case R.id.dialog_select_import_type: {
+ return getSelectImportTypeDialog();
+ }
+ case R.id.dialog_select_multiple_vcard: {
+ return getVCardFileSelectDialog(true);
+ }
+ case R.id.dialog_select_one_vcard: {
+ return getVCardFileSelectDialog(false);
+ }
+ case R.id.dialog_cache_vcard: {
+ if (mProgressDialogForCachingVCard == null) {
+ final String title = getString(R.string.caching_vcard_title);
+ final String message = getString(R.string.caching_vcard_message);
+ mProgressDialogForCachingVCard = new ProgressDialog(this);
+ mProgressDialogForCachingVCard.setTitle(title);
+ mProgressDialogForCachingVCard.setMessage(message);
+ mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
+ mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
+ startVCardService();
+ }
+ return mProgressDialogForCachingVCard;
+ }
+ case R.id.dialog_io_exception: {
+ String message = (getString(R.string.scanning_sdcard_failed_message,
+ getString(R.string.fail_reason_io_error)));
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ case R.id.dialog_error_with_message: {
+ String message = mErrorMessage;
+ if (TextUtils.isEmpty(message)) {
+ Log.e(LOG_TAG, "Error message is null while it must not.");
+ message = getString(R.string.fail_reason_unknown);
+ }
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.reading_vcard_failed_title))
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
+ return builder.create();
+ }
+ }
+
+ return super.onCreateDialog(resId, bundle);
+ }
+
+ /* package */ void startVCardService() {
+ mConnection = new ImportRequestConnection();
+
+ Log.i(LOG_TAG, "Bind to VCardService.");
+ // We don't want the service finishes itself just after this connection.
+ Intent intent = new Intent(this, VCardService.class);
+ startService(intent);
+ bindService(new Intent(this, VCardService.class),
+ mConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if (mProgressDialogForCachingVCard != null) {
+ Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
+ showDialog(R.id.dialog_cache_vcard);
+ }
+ }
+
+ /**
+ * Scans vCard in external storage (typically SDCard) and tries to import it.
+ * - When there's no SDCard available, an error dialog is shown.
+ * - When multiple vCard files are available, asks a user to select one.
+ */
+ private void doScanExternalStorageAndImportVCard() {
+ // TODO: should use getExternalStorageState().
+ final File file = Environment.getExternalStorageDirectory();
+ if (!file.exists() || !file.isDirectory() || !file.canRead()) {
+ showDialog(R.id.dialog_sdcard_not_found);
+ } else {
+ mVCardScanThread = new VCardScanThread(file);
+ showDialog(R.id.dialog_searching_vcard);
+ }
+ }
+
+ /* package */ void showFailureNotification(int reasonId) {
+ final NotificationManager notificationManager =
+ (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+ final Notification notification =
+ NotificationImportExportListener.constructImportFailureNotification(
+ ImportVCardActivity.this,
+ getString(reasonId));
+ notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
+ FAILURE_NOTIFICATION_ID, notification);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ImportVCardActivity.this,
+ getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/NfcImportVCardActivity.java b/src/com/android/contacts/common/vcard/NfcImportVCardActivity.java
new file mode 100644
index 00000000..96f224a1
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/NfcImportVCardActivity.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2011 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.common.vcard;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryCounter;
+import com.android.vcard.VCardParser;
+import com.android.vcard.VCardParser_V21;
+import com.android.vcard.VCardParser_V30;
+import com.android.vcard.VCardSourceDetector;
+import com.android.vcard.exception.VCardException;
+import com.android.vcard.exception.VCardNestedException;
+import com.android.vcard.exception.VCardVersionException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class NfcImportVCardActivity extends Activity implements ServiceConnection,
+ VCardImportExportListener {
+ private static final String TAG = "NfcImportVCardActivity";
+
+ private static final int SELECT_ACCOUNT = 1;
+
+ private NdefRecord mRecord;
+ private AccountWithDataSet mAccount;
+
+ /* package */ class ImportTask extends AsyncTask<VCardService, Void, ImportRequest> {
+ @Override
+ public ImportRequest doInBackground(VCardService... services) {
+ ImportRequest request = createImportRequest();
+ if (request == null) {
+ return null;
+ }
+
+ ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
+ requests.add(request);
+ services[0].handleImportRequest(requests, NfcImportVCardActivity.this);
+ return request;
+ }
+
+ @Override
+ public void onCancelled() {
+ unbindService(NfcImportVCardActivity.this);
+ }
+
+ @Override
+ public void onPostExecute(ImportRequest request) {
+ unbindService(NfcImportVCardActivity.this);
+ }
+ }
+
+ /* package */ ImportRequest createImportRequest() {
+ VCardParser parser;
+ VCardEntryCounter counter = null;
+ VCardSourceDetector detector = null;
+ int vcardVersion = ImportVCardActivity.VCARD_VERSION_V21;
+ try {
+ ByteArrayInputStream is = new ByteArrayInputStream(mRecord.getPayload());
+ is.mark(0);
+ parser = new VCardParser_V21();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ parser.addInterpreter(counter);
+ parser.addInterpreter(detector);
+ parser.parse(is);
+ } catch (VCardVersionException e1) {
+ is.reset();
+ vcardVersion = ImportVCardActivity.VCARD_VERSION_V30;
+ parser = new VCardParser_V30();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ parser.addInterpreter(counter);
+ parser.addInterpreter(detector);
+ parser.parse(is);
+ } catch (VCardVersionException e2) {
+ return null;
+ }
+ } finally {
+ try {
+ if (is != null) is.close();
+ } catch (IOException e) {
+ }
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Failed reading vcard data", e);
+ return null;
+ } catch (VCardNestedException e) {
+ Log.w(TAG, "Nested Exception is found (it may be false-positive).");
+ // Go through without throwing the Exception, as we may be able to detect the
+ // version before it
+ } catch (VCardException e) {
+ Log.e(TAG, "Error parsing vcard", e);
+ return null;
+ }
+
+ return new ImportRequest(mAccount, mRecord.getPayload(), null,
+ getString(R.string.nfc_vcard_file_name), detector.getEstimatedType(),
+ detector.getEstimatedCharset(), vcardVersion, counter.getCount());
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder binder) {
+ VCardService service = ((VCardService.MyBinder) binder).getService();
+ new ImportTask().execute(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ // Do nothing
+ }
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ Intent intent = getIntent();
+ if (!NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
+ Log.w(TAG, "Unknowon intent " + intent);
+ finish();
+ return;
+ }
+
+ String type = intent.getType();
+ if (type == null ||
+ (!"text/x-vcard".equals(type) && !"text/vcard".equals(type))) {
+ Log.w(TAG, "Not a vcard");
+ //setStatus(getString(R.string.fail_reason_not_supported));
+ finish();
+ return;
+ }
+ NdefMessage msg = (NdefMessage) intent.getParcelableArrayExtra(
+ NfcAdapter.EXTRA_NDEF_MESSAGES)[0];
+ mRecord = msg.getRecords()[0];
+
+ final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+ final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+ if (accountList.size() == 0) {
+ mAccount = null;
+ } else if (accountList.size() == 1) {
+ mAccount = accountList.get(0);
+ } else {
+ startActivityForResult(new Intent(this, SelectAccountActivity.class), SELECT_ACCOUNT);
+ return;
+ }
+
+ startImport();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == SELECT_ACCOUNT) {
+ if (resultCode == RESULT_OK) {
+ mAccount = new AccountWithDataSet(
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
+ intent.getStringExtra(SelectAccountActivity.DATA_SET));
+ startImport();
+ } else {
+ finish();
+ }
+ }
+ }
+
+ private void startImport() {
+ // We don't want the service finishes itself just after this connection.
+ Intent intent = new Intent(this, VCardService.class);
+ startService(intent);
+ bindService(intent, this, Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onImportProcessed(ImportRequest request, int jobId, int sequence) {
+ // do nothing
+ }
+
+ @Override
+ public void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+ int totalCount) {
+ // do nothing
+ }
+
+ @Override
+ public void onImportFinished(ImportRequest request, int jobId, Uri uri) {
+ if (isFinishing()) {
+ Log.i(TAG, "Late import -- ignoring");
+ return;
+ }
+
+ if (uri != null) {
+ Uri contactUri = RawContacts.getContactLookupUri(getContentResolver(), uri);
+ Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
+ startActivity(intent);
+ finish();
+ }
+ }
+
+ @Override
+ public void onImportFailed(ImportRequest request) {
+ if (isFinishing()) {
+ Log.i(TAG, "Late import failure -- ignoring");
+ return;
+ }
+ // TODO: report failure
+ }
+
+ @Override
+ public void onImportCanceled(ImportRequest request, int jobId) {
+ // do nothing
+ }
+
+ @Override
+ public void onExportProcessed(ExportRequest request, int jobId) {
+ // do nothing
+ }
+
+ @Override
+ public void onExportFailed(ExportRequest request) {
+ // do nothing
+ }
+
+ @Override
+ public void onCancelRequest(CancelRequest request, int type) {
+ // do nothing
+ }
+
+ @Override
+ public void onComplete() {
+ // do nothing
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/NotificationImportExportListener.java b/src/com/android/contacts/common/vcard/NotificationImportExportListener.java
new file mode 100644
index 00000000..f873e316
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/NotificationImportExportListener.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2011 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.common.vcard;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract.RawContacts;
+import android.widget.Toast;
+
+import com.android.contacts.common.R;
+import com.android.vcard.VCardEntry;
+
+public class NotificationImportExportListener implements VCardImportExportListener,
+ Handler.Callback {
+ /** The tag used by vCard-related notifications. */
+ /* package */ static final String DEFAULT_NOTIFICATION_TAG = "VCardServiceProgress";
+ /**
+ * The tag used by vCard-related failure notifications.
+ * <p>
+ * Use a different tag from {@link #DEFAULT_NOTIFICATION_TAG} so that failures do not get
+ * replaced by other notifications and vice-versa.
+ */
+ /* package */ static final String FAILURE_NOTIFICATION_TAG = "VCardServiceFailure";
+
+ private final NotificationManager mNotificationManager;
+ private final Activity mContext;
+ private final Handler mHandler;
+
+ public NotificationImportExportListener(Activity activity) {
+ mContext = activity;
+ mNotificationManager = (NotificationManager) activity.getSystemService(
+ Context.NOTIFICATION_SERVICE);
+ mHandler = new Handler(this);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ String text = (String) msg.obj;
+ Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
+ return true;
+ }
+
+ @Override
+ public void onImportProcessed(ImportRequest request, int jobId, int sequence) {
+ // Show a notification about the status
+ final String displayName;
+ final String message;
+ if (request.displayName != null) {
+ displayName = request.displayName;
+ message = mContext.getString(R.string.vcard_import_will_start_message, displayName);
+ } else {
+ displayName = mContext.getString(R.string.vcard_unknown_filename);
+ message = mContext.getString(
+ R.string.vcard_import_will_start_message_with_default_name);
+ }
+
+ // We just want to show notification for the first vCard.
+ if (sequence == 0) {
+ // 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.
+ mHandler.obtainMessage(0, message).sendToTarget();
+ }
+
+ final Notification notification = constructProgressNotification(mContext,
+ VCardService.TYPE_IMPORT, message, message, jobId, displayName, -1, 0);
+ mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+ }
+
+ @Override
+ public void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+ int totalCount) {
+ if (entry.isIgnorable()) {
+ return;
+ }
+
+ final String totalCountString = String.valueOf(totalCount);
+ final String tickerText =
+ mContext.getString(R.string.progress_notifier_message,
+ String.valueOf(currentCount),
+ totalCountString,
+ entry.getDisplayName());
+ final String description = mContext.getString(R.string.importing_vcard_description,
+ entry.getDisplayName());
+
+ final Notification notification = constructProgressNotification(
+ mContext.getApplicationContext(), VCardService.TYPE_IMPORT, description, tickerText,
+ jobId, request.displayName, totalCount, currentCount);
+ mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+ }
+
+ @Override
+ public void onImportFinished(ImportRequest request, int jobId, Uri createdUri) {
+ final String description = mContext.getString(R.string.importing_vcard_finished_title,
+ request.displayName);
+ final Intent intent;
+ if (createdUri != null) {
+ final long rawContactId = ContentUris.parseId(createdUri);
+ final Uri contactUri = RawContacts.getContactLookupUri(
+ mContext.getContentResolver(), ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId));
+ intent = new Intent(Intent.ACTION_VIEW, contactUri);
+ } else {
+ intent = null;
+ }
+ final Notification notification =
+ NotificationImportExportListener.constructFinishNotification(mContext,
+ description, null, intent);
+ mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+ jobId, notification);
+ }
+
+ @Override
+ public void onImportFailed(ImportRequest request) {
+ // 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.
+ mHandler.obtainMessage(0,
+ mContext.getString(R.string.vcard_import_request_rejected_message)).sendToTarget();
+ }
+
+ @Override
+ public void onImportCanceled(ImportRequest request, int jobId) {
+ final String description = mContext.getString(R.string.importing_vcard_canceled_title,
+ request.displayName);
+ final Notification notification =
+ NotificationImportExportListener.constructCancelNotification(mContext, description);
+ mNotificationManager.notify(NotificationImportExportListener.DEFAULT_NOTIFICATION_TAG,
+ jobId, notification);
+ }
+
+ @Override
+ public void onExportProcessed(ExportRequest request, int jobId) {
+ final String displayName = request.destUri.getLastPathSegment();
+ final String message = mContext.getString(R.string.vcard_export_will_start_message,
+ displayName);
+
+ mHandler.obtainMessage(0, message).sendToTarget();
+ final Notification notification =
+ NotificationImportExportListener.constructProgressNotification(mContext,
+ VCardService.TYPE_EXPORT, message, message, jobId, displayName, -1, 0);
+ mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, jobId, notification);
+ }
+
+ @Override
+ public void onExportFailed(ExportRequest request) {
+ mHandler.obtainMessage(0,
+ mContext.getString(R.string.vcard_export_request_rejected_message)).sendToTarget();
+ }
+
+ @Override
+ public void onCancelRequest(CancelRequest request, int type) {
+ final String description = type == VCardService.TYPE_IMPORT ?
+ mContext.getString(R.string.importing_vcard_canceled_title, request.displayName) :
+ mContext.getString(R.string.exporting_vcard_canceled_title, request.displayName);
+ final Notification notification = constructCancelNotification(mContext, description);
+ mNotificationManager.notify(DEFAULT_NOTIFICATION_TAG, request.jobId, notification);
+ }
+
+ /**
+ * 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) {
+ // 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);
+
+ final Notification.Builder builder = new Notification.Builder(context);
+ builder.setOngoing(true)
+ .setProgress(totalCount, currentCount, totalCount == - 1)
+ .setTicker(tickerText)
+ .setContentTitle(description)
+ .setSmallIcon(type == VCardService.TYPE_IMPORT
+ ? android.R.drawable.stat_sys_download
+ : android.R.drawable.stat_sys_upload)
+ .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0));
+ if (totalCount > 0) {
+ builder.setContentText(context.getString(R.string.percentage,
+ String.valueOf(currentCount * 100 / totalCount)));
+ }
+ return builder.getNotification();
+ }
+
+ /**
+ * 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) {
+ return new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setSmallIcon(android.R.drawable.stat_notify_error)
+ .setContentTitle(description)
+ .setContentText(description)
+ .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
+ .getNotification();
+ }
+
+ /**
+ * 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 title, String description, Intent intent) {
+ return new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setSmallIcon(android.R.drawable.stat_sys_download_done)
+ .setContentTitle(title)
+ .setContentText(description)
+ .setContentIntent(PendingIntent.getActivity(context, 0,
+ (intent != null ? intent : new Intent()), 0))
+ .getNotification();
+ }
+
+ /**
+ * Constructs a Notification telling the vCard import has failed.
+ *
+ * @param context
+ * @param reason The reason why the import has failed. Shown in description field.
+ */
+ /* package */ static Notification constructImportFailureNotification(
+ Context context, String reason) {
+ return new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setSmallIcon(android.R.drawable.stat_notify_error)
+ .setContentTitle(context.getString(R.string.vcard_import_failed))
+ .setContentText(reason)
+ .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
+ .getNotification();
+ }
+
+ @Override
+ public void onComplete() {
+ mContext.finish();
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/ProcessorBase.java b/src/com/android/contacts/common/vcard/ProcessorBase.java
new file mode 100644
index 00000000..abc859dd
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/ProcessorBase.java
@@ -0,0 +1,75 @@
+/*
+ * 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.common.vcard;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.RunnableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A base processor class. One instance processes vCard one import/export request (imports a given
+ * vCard or exports a vCard). Expected to be used with {@link ExecutorService}.
+ *
+ * This instance starts itself with {@link #run()} method, and can be cancelled with
+ * {@link #cancel(boolean)}. Users can check the processor's status using {@link #isCancelled()}
+ * and {@link #isDone()} asynchronously.
+ *
+ * {@link #get()} and {@link #get(long, TimeUnit)}, which are form {@link Future}, aren't
+ * supported and {@link UnsupportedOperationException} will be just thrown when they are called.
+ */
+public abstract class ProcessorBase implements RunnableFuture<Object> {
+
+ /**
+ * @return the type of the processor. Must be {@link VCardService#TYPE_IMPORT} or
+ * {@link VCardService#TYPE_EXPORT}.
+ */
+ public abstract int getType();
+
+ @Override
+ public abstract void run();
+
+ /**
+ * Cancels this operation.
+ *
+ * @param mayInterruptIfRunning ignored. When this method is called, the instance
+ * stops processing and finish itself even if the thread is running.
+ *
+ * @see Future#cancel(boolean)
+ */
+ @Override
+ public abstract boolean cancel(boolean mayInterruptIfRunning);
+ @Override
+ public abstract boolean isCancelled();
+ @Override
+ public abstract boolean isDone();
+
+ /**
+ * Just throws {@link UnsupportedOperationException}.
+ */
+ @Override
+ public final Object get() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Just throws {@link UnsupportedOperationException}.
+ */
+ @Override
+ public final Object get(long timeout, TimeUnit unit) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/SelectAccountActivity.java b/src/com/android/contacts/common/vcard/SelectAccountActivity.java
new file mode 100644
index 00000000..d05810db
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/SelectAccountActivity.java
@@ -0,0 +1,114 @@
+/*
+ * 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.common.vcard;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.util.AccountSelectionUtil;
+
+import java.util.List;
+
+public class SelectAccountActivity extends Activity {
+ private static final String LOG_TAG = "SelectAccountActivity";
+
+ public static final String ACCOUNT_NAME = "account_name";
+ public static final String ACCOUNT_TYPE = "account_type";
+ public static final String DATA_SET = "data_set";
+
+ private class CancelListener
+ implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+ }
+
+ private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
+
+ @Override
+ protected void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ // There's three possibilities:
+ // - more than one accounts -> ask the user
+ // - just one account -> use the account without asking the user
+ // - no account -> use phone-local storage without asking the user
+ final int resId = R.string.import_from_sdcard;
+ final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
+ final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
+ if (accountList.size() == 0) {
+ Log.w(LOG_TAG, "Account does not exist");
+ finish();
+ return;
+ } else if (accountList.size() == 1) {
+ final AccountWithDataSet account = accountList.get(0);
+ final Intent intent = new Intent();
+ intent.putExtra(ACCOUNT_NAME, account.name);
+ intent.putExtra(ACCOUNT_TYPE, account.type);
+ intent.putExtra(DATA_SET, account.dataSet);
+ setResult(RESULT_OK, intent);
+ finish();
+ return;
+ }
+
+ Log.i(LOG_TAG, "The number of available accounts: " + accountList.size());
+
+ // Multiple accounts. Let users to select one.
+ mAccountSelectionListener =
+ new AccountSelectionUtil.AccountSelectedListener(
+ this, accountList, resId) {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ final AccountWithDataSet account = mAccountList.get(which);
+ final Intent intent = new Intent();
+ intent.putExtra(ACCOUNT_NAME, account.name);
+ intent.putExtra(ACCOUNT_TYPE, account.type);
+ intent.putExtra(DATA_SET, account.dataSet);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+ };
+ showDialog(resId);
+ return;
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int resId, Bundle bundle) {
+ switch (resId) {
+ case R.string.import_from_sdcard: {
+ if (mAccountSelectionListener == null) {
+ throw new NullPointerException(
+ "mAccountSelectionListener must not be null.");
+ }
+ return AccountSelectionUtil.getSelectAccountDialog(this, resId,
+ mAccountSelectionListener,
+ new CancelListener());
+ }
+ }
+ return super.onCreateDialog(resId, bundle);
+ }
+}
diff --git a/src/com/android/contacts/common/vcard/VCardCommonArguments.java b/src/com/android/contacts/common/vcard/VCardCommonArguments.java
new file mode 100644
index 00000000..c423ca3a
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/VCardCommonArguments.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2012 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.common.vcard;
+
+/**
+ * Argument constants used by many activities and services.
+ */
+public class VCardCommonArguments {
+
+ // Argument used to pass calling activities to the target activity or service.
+ // The value should be a string class name (e.g. com.android.contacts.vcard.VCardCommonArgs)
+ public static final String ARG_CALLING_ACTIVITY = "CALLING_ACTIVITY";
+}
diff --git a/src/com/android/contacts/common/vcard/VCardImportExportListener.java b/src/com/android/contacts/common/vcard/VCardImportExportListener.java
new file mode 100644
index 00000000..e4e4893a
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/VCardImportExportListener.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2011 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.common.vcard;
+
+import android.net.Uri;
+
+import com.android.vcard.VCardEntry;
+
+interface VCardImportExportListener {
+ void onImportProcessed(ImportRequest request, int jobId, int sequence);
+ void onImportParsed(ImportRequest request, int jobId, VCardEntry entry, int currentCount,
+ int totalCount);
+ void onImportFinished(ImportRequest request, int jobId, Uri uri);
+ void onImportFailed(ImportRequest request);
+ void onImportCanceled(ImportRequest request, int jobId);
+
+ void onExportProcessed(ExportRequest request, int jobId);
+ void onExportFailed(ExportRequest request);
+
+ void onCancelRequest(CancelRequest request, int type);
+ void onComplete();
+}
diff --git a/src/com/android/contacts/common/vcard/VCardService.java b/src/com/android/contacts/common/vcard/VCardService.java
new file mode 100644
index 00000000..07c2a3c1
--- /dev/null
+++ b/src/com/android/contacts/common/vcard/VCardService.java
@@ -0,0 +1,537 @@
+/*
+ * 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.common.vcard;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Environment;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.contacts.common.R;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * The class responsible for handling vCard import/export requests.
+ *
+ * This Service creates one ImportRequest/ExportRequest object (as Runnable) per request and push
+ * it to {@link ExecutorService} with single thread executor. The executor handles each request
+ * one by one, and notifies users when needed.
+ */
+// TODO: Using IntentService looks simpler than using Service + ServiceConnection though this
+// works fine enough. Investigate the feasibility.
+public class VCardService extends Service {
+ private final static String LOG_TAG = "VCardService";
+
+ /* package */ final static boolean DEBUG = false;
+
+ /* package */ static final int MSG_IMPORT_REQUEST = 1;
+ /* package */ static final int MSG_EXPORT_REQUEST = 2;
+ /* package */ static final int MSG_CANCEL_REQUEST = 3;
+ /* package */ static final int MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION = 4;
+ /* package */ static final int MSG_SET_AVAILABLE_EXPORT_DESTINATION = 5;
+
+ /**
+ * Specifies the type of operation. Used when constructing a 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_";
+
+
+ private class CustomMediaScannerConnectionClient implements MediaScannerConnectionClient {
+ final MediaScannerConnection mConnection;
+ final String mPath;
+
+ public CustomMediaScannerConnectionClient(String path) {
+ mConnection = new MediaScannerConnection(VCardService.this, this);
+ mPath = path;
+ }
+
+ public void start() {
+ mConnection.connect();
+ }
+
+ @Override
+ public void onMediaScannerConnected() {
+ if (DEBUG) { Log.d(LOG_TAG, "Connected to MediaScanner. Start scanning."); }
+ mConnection.scanFile(mPath, null);
+ }
+
+ @Override
+ public void onScanCompleted(String path, Uri uri) {
+ if (DEBUG) { Log.d(LOG_TAG, "scan completed: " + path); }
+ mConnection.disconnect();
+ removeConnectionClient(this);
+ }
+ }
+
+ // Should be single thread, as we don't want to simultaneously handle import and export
+ // requests.
+ private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();
+
+ private int mCurrentJobId;
+
+ // Stores all unfinished import/export jobs which will be executed by mExecutorService.
+ // Key is jobId.
+ private final SparseArray<ProcessorBase> mRunningJobMap = new SparseArray<ProcessorBase>();
+ // Stores ScannerConnectionClient objects until they finish scanning requested files.
+ // Uses List class for simplicity. It's not costly as we won't have multiple objects in
+ // almost all cases.
+ private final List<CustomMediaScannerConnectionClient> mRemainingScannerConnections =
+ new ArrayList<CustomMediaScannerConnectionClient>();
+
+ /* ** vCard exporter params ** */
+ // If true, VCardExporter is able to emits files longer than 8.3 format.
+ private static final boolean ALLOW_LONG_FILE_NAME = false;
+
+ private File mTargetDirectory;
+ private String mFileNamePrefix;
+ private String mFileNameSuffix;
+ private int mFileIndexMinimum;
+ private int mFileIndexMaximum;
+ private String mFileNameExtension;
+ private Set<String> mExtensionsToConsider;
+ private String mErrorReason;
+ private MyBinder mBinder;
+
+ private String mCallingActivity;
+
+ // File names currently reserved by some export job.
+ private final Set<String> mReservedDestination = new HashSet<String>();
+ /* ** end of vCard exporter params ** */
+
+ public class MyBinder extends Binder {
+ public VCardService getService() {
+ return VCardService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBinder = new MyBinder();
+ if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
+ initExporterParams();
+ }
+
+ private void initExporterParams() {
+ mTargetDirectory = Environment.getExternalStorageDirectory();
+ mFileNamePrefix = getString(R.string.config_export_file_prefix);
+ mFileNameSuffix = getString(R.string.config_export_file_suffix);
+ mFileNameExtension = getString(R.string.config_export_file_extension);
+
+ mExtensionsToConsider = new HashSet<String>();
+ mExtensionsToConsider.add(mFileNameExtension);
+
+ final String additionalExtensions =
+ getString(R.string.config_export_extensions_to_consider);
+ if (!TextUtils.isEmpty(additionalExtensions)) {
+ for (String extension : additionalExtensions.split(",")) {
+ String trimed = extension.trim();
+ if (trimed.length() > 0) {
+ mExtensionsToConsider.add(trimed);
+ }
+ }
+ }
+
+ final Resources resources = getResources();
+ mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
+ mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int id) {
+ mCallingActivity = intent.getExtras().getString(
+ VCardCommonArguments.ARG_CALLING_ACTIVITY);
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
+ cancelAllRequestsAndShutdown();
+ clearCache();
+ super.onDestroy();
+ }
+
+ public synchronized void handleImportRequest(List<ImportRequest> requests,
+ VCardImportExportListener listener) {
+ if (DEBUG) {
+ final ArrayList<String> uris = new ArrayList<String>();
+ final ArrayList<String> displayNames = new ArrayList<String>();
+ for (ImportRequest request : requests) {
+ uris.add(request.uri.toString());
+ displayNames.add(request.displayName);
+ }
+ Log.d(LOG_TAG,
+ String.format("received multiple import request (uri: %s, displayName: %s)",
+ uris.toString(), displayNames.toString()));
+ }
+ final int size = requests.size();
+ for (int i = 0; i < size; i++) {
+ ImportRequest request = requests.get(i);
+
+ if (tryExecute(new ImportProcessor(this, listener, request, mCurrentJobId))) {
+ if (listener != null) {
+ listener.onImportProcessed(request, mCurrentJobId, i);
+ }
+ mCurrentJobId++;
+ } else {
+ if (listener != null) {
+ listener.onImportFailed(request);
+ }
+ // A rejection means executor doesn't run any more. Exit.
+ break;
+ }
+ }
+ }
+
+ public synchronized void handleExportRequest(ExportRequest request,
+ VCardImportExportListener listener) {
+ if (tryExecute(new ExportProcessor(this, request, mCurrentJobId, mCallingActivity))) {
+ final String path = request.destUri.getEncodedPath();
+ if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
+ if (!mReservedDestination.add(path)) {
+ Log.w(LOG_TAG,
+ String.format("The path %s is already reserved. Reject export request",
+ path));
+ if (listener != null) {
+ listener.onExportFailed(request);
+ }
+ return;
+ }
+
+ if (listener != null) {
+ listener.onExportProcessed(request, mCurrentJobId);
+ }
+ mCurrentJobId++;
+ } else {
+ if (listener != null) {
+ listener.onExportFailed(request);
+ }
+ }
+ }
+
+ /**
+ * Tries to call {@link ExecutorService#execute(Runnable)} toward a given processor.
+ * @return true when successful.
+ */
+ private synchronized boolean tryExecute(ProcessorBase processor) {
+ try {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Executor service status: shutdown: " + mExecutorService.isShutdown()
+ + ", terminated: " + mExecutorService.isTerminated());
+ }
+ mExecutorService.execute(processor);
+ mRunningJobMap.put(mCurrentJobId, processor);
+ return true;
+ } catch (RejectedExecutionException e) {
+ Log.w(LOG_TAG, "Failed to excetute a job.", e);
+ return false;
+ }
+ }
+
+ public synchronized void handleCancelRequest(CancelRequest request,
+ VCardImportExportListener listener) {
+ final int jobId = request.jobId;
+ if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
+
+ final ProcessorBase processor = mRunningJobMap.get(jobId);
+ mRunningJobMap.remove(jobId);
+
+ if (processor != null) {
+ processor.cancel(true);
+ final int type = processor.getType();
+ if (listener != null) {
+ listener.onCancelRequest(request, type);
+ }
+ if (type == TYPE_EXPORT) {
+ final String path =
+ ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
+ Log.i(LOG_TAG,
+ String.format("Cancel reservation for the path %s if appropriate", path));
+ if (!mReservedDestination.remove(path)) {
+ Log.w(LOG_TAG, "Not reserved.");
+ }
+ }
+ } else {
+ Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+ }
+ stopServiceIfAppropriate();
+ }
+
+ public synchronized void handleRequestAvailableExportDestination(final Messenger messenger) {
+ if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
+ final String path = getAppropriateDestination(mTargetDirectory);
+ final Message message;
+ if (path != null) {
+ message = Message.obtain(null,
+ VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, 0, 0, path);
+ } else {
+ message = Message.obtain(null,
+ VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION,
+ R.id.dialog_fail_to_export_with_reason, 0, mErrorReason);
+ }
+ try {
+ messenger.send(message);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, "Failed to send reply for available export destination request.", e);
+ }
+ }
+
+ /**
+ * Checks job list and call {@link #stopSelf()} when there's no job and no scanner connection
+ * is remaining.
+ * A new job (import/export) cannot be submitted any more after this call.
+ */
+ private synchronized void stopServiceIfAppropriate() {
+ if (mRunningJobMap.size() > 0) {
+ final int size = mRunningJobMap.size();
+
+ // Check if there are processors which aren't finished yet. If we still have ones to
+ // process, we cannot stop the service yet. Also clean up already finished processors
+ // here.
+
+ // Job-ids to be removed. At first all elements in the array are invalid and will
+ // be filled with real job-ids from the array's top. When we find a not-yet-finished
+ // processor, then we start removing those finished jobs. In that case latter half of
+ // this array will be invalid.
+ final int[] toBeRemoved = new int[size];
+ for (int i = 0; i < size; i++) {
+ final int jobId = mRunningJobMap.keyAt(i);
+ final ProcessorBase processor = mRunningJobMap.valueAt(i);
+ if (!processor.isDone()) {
+ Log.i(LOG_TAG, String.format("Found unfinished job (id: %d)", jobId));
+
+ // Remove processors which are already "done", all of which should be before
+ // processors which aren't done yet.
+ for (int j = 0; j < i; j++) {
+ mRunningJobMap.remove(toBeRemoved[j]);
+ }
+ return;
+ }
+
+ // Remember the finished processor.
+ toBeRemoved[i] = jobId;
+ }
+
+ // We're sure we can remove all. Instead of removing one by one, just call clear().
+ mRunningJobMap.clear();
+ }
+
+ if (!mRemainingScannerConnections.isEmpty()) {
+ Log.i(LOG_TAG, "MediaScanner update is in progress.");
+ return;
+ }
+
+ Log.i(LOG_TAG, "No unfinished job. Stop this service.");
+ mExecutorService.shutdown();
+ stopSelf();
+ }
+
+ /* package */ synchronized void updateMediaScanner(String path) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "MediaScanner is being updated: " + path);
+ }
+
+ if (mExecutorService.isShutdown()) {
+ Log.w(LOG_TAG, "MediaScanner update is requested after executor's being shut down. " +
+ "Ignoring the update request");
+ return;
+ }
+ final CustomMediaScannerConnectionClient client =
+ new CustomMediaScannerConnectionClient(path);
+ mRemainingScannerConnections.add(client);
+ client.start();
+ }
+
+ private synchronized void removeConnectionClient(
+ CustomMediaScannerConnectionClient client) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, "Removing custom MediaScannerConnectionClient.");
+ }
+ mRemainingScannerConnections.remove(client);
+ stopServiceIfAppropriate();
+ }
+
+ /* package */ synchronized void handleFinishImportNotification(
+ int jobId, boolean successful) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
+ + "Result: %b", jobId, (successful ? "success" : "failure")));
+ }
+ mRunningJobMap.remove(jobId);
+ stopServiceIfAppropriate();
+ }
+
+ /* package */ synchronized void handleFinishExportNotification(
+ int jobId, boolean successful) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
+ + "Result: %b", jobId, (successful ? "success" : "failure")));
+ }
+ final ProcessorBase job = mRunningJobMap.get(jobId);
+ mRunningJobMap.remove(jobId);
+ if (job == null) {
+ Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+ } else if (!(job instanceof ExportProcessor)) {
+ Log.w(LOG_TAG,
+ String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
+ } else {
+ final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
+ if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
+ mReservedDestination.remove(path);
+ }
+
+ stopServiceIfAppropriate();
+ }
+
+ /**
+ * Cancels all the import/export requests and calls {@link ExecutorService#shutdown()}, which
+ * means this Service becomes no longer ready for import/export requests.
+ *
+ * Mainly called from onDestroy().
+ */
+ private synchronized void cancelAllRequestsAndShutdown() {
+ for (int i = 0; i < mRunningJobMap.size(); i++) {
+ mRunningJobMap.valueAt(i).cancel(true);
+ }
+ mRunningJobMap.clear();
+ mExecutorService.shutdown();
+ }
+
+ /**
+ * Removes import caches stored locally.
+ */
+ private void clearCache() {
+ for (final String fileName : fileList()) {
+ if (fileName.startsWith(CACHE_FILE_PREFIX)) {
+ // We don't want to keep all the caches so we remove cache files old enough.
+ Log.i(LOG_TAG, "Remove a temporary file: " + fileName);
+ deleteFile(fileName);
+ }
+ }
+ }
+
+ /**
+ * Returns an appropriate file name for vCard export. Returns null when impossible.
+ *
+ * @return destination path for a vCard file to be exported. null on error and mErrorReason
+ * is correctly set.
+ */
+ private String getAppropriateDestination(final File destDirectory) {
+ /*
+ * Here, file names have 5 parts: directory, prefix, index, suffix, and extension.
+ * e.g. "/mnt/sdcard/prfx00001sfx.vcf" -> "/mnt/sdcard", "prfx", "00001", "sfx", and ".vcf"
+ * (In default, prefix and suffix is empty, so usually the destination would be
+ * /mnt/sdcard/00001.vcf.)
+ *
+ * This method increments "index" part from 1 to maximum, and checks whether any file name
+ * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the
+ * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is
+ * returned.
+ *
+ * There may not be any appropriate file name. If there are 99999 vCard files in the
+ * storage, for example, there's no appropriate name, so this method returns
+ * null.
+ */
+
+ // Count the number of digits of mFileIndexMaximum
+ // e.g. When mFileIndexMaximum is 99999, fileIndexDigit becomes 5, as we will count the
+ int fileIndexDigit = 0;
+ {
+ // Calling Math.Log10() is costly.
+ int tmp;
+ for (fileIndexDigit = 0, tmp = mFileIndexMaximum; tmp > 0;
+ fileIndexDigit++, tmp /= 10) {
+ }
+ }
+
+ // %s05d%s (e.g. "p00001s")
+ final String bodyFormat = "%s%0" + fileIndexDigit + "d%s";
+
+ if (!ALLOW_LONG_FILE_NAME) {
+ final String possibleBody =
+ String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix);
+ if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
+ Log.e(LOG_TAG, "This code does not allow any long file name.");
+ mErrorReason = getString(R.string.fail_reason_too_long_filename,
+ String.format("%s.%s", possibleBody, mFileNameExtension));
+ Log.w(LOG_TAG, "File name becomes too long.");
+ return null;
+ }
+ }
+
+ for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
+ boolean numberIsAvailable = true;
+ final String body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
+ // Make sure that none of the extensions of mExtensionsToConsider matches. If this
+ // number is free, we'll go ahead with mFileNameExtension (which is included in
+ // mExtensionsToConsider)
+ for (String possibleExtension : mExtensionsToConsider) {
+ final File file = new File(destDirectory, body + "." + possibleExtension);
+ final String path = file.getAbsolutePath();
+ synchronized (this) {
+ // Is this being exported right now? Skip this number
+ if (mReservedDestination.contains(path)) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("%s is already being exported.", path));
+ }
+ numberIsAvailable = false;
+ break;
+ }
+ }
+ if (file.exists()) {
+ numberIsAvailable = false;
+ break;
+ }
+ }
+ if (numberIsAvailable) {
+ return new File(destDirectory, body + "." + mFileNameExtension).getAbsolutePath();
+ }
+ }
+
+ Log.w(LOG_TAG, "Reached vCard number limit. Maybe there are too many vCard in the storage");
+ mErrorReason = getString(R.string.fail_reason_too_many_vcard);
+ return null;
+ }
+}