Refactor ImportVCardService. Split the Service into several components.
Bug: 2733143
Change-Id: Iefcb0919d5c740f1ae8a63e5dc8095a39dbd0cd5
diff --git a/src/com/android/contacts/ImportProgressNotifier.java b/src/com/android/contacts/ImportProgressNotifier.java
new file mode 100644
index 0000000..e24b307
--- /dev/null
+++ b/src/com/android/contacts/ImportProgressNotifier.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.widget.RemoteViews;
+
+import com.android.vcard.VCardEntry;
+import com.android.vcard.VCardEntryHandler;
+
+/**
+ * {@link VCardEntryHandler} implementation which lets the system update
+ * the current status of vCard import.
+ */
+public class ImportProgressNotifier implements VCardEntryHandler {
+ private Context mContext;
+ private NotificationManager mNotificationManager;
+
+ private int mCurrentCount;
+ private int mTotalCount;
+
+ public void init(Context context, NotificationManager notificationManager) {
+ mContext = context;
+ mNotificationManager = notificationManager;
+ }
+
+ public void onStart() {
+ }
+
+ public void onEntryCreated(VCardEntry contactStruct) {
+ mCurrentCount++; // 1 origin.
+ if (contactStruct.isIgnorable()) {
+ return;
+ }
+
+ // We don't use startEntry() since:
+ // - We cannot know name there but here.
+ // - There's high probability where name comes soon after the beginning of entry, so
+ // we don't need to hurry to show something.
+ final String packageName = "com.android.contacts";
+ final RemoteViews remoteViews = new RemoteViews(packageName,
+ R.layout.status_bar_ongoing_event_progress_bar);
+ final String title = mContext.getString(R.string.reading_vcard_title);
+ String totalCountString;
+ synchronized (this) {
+ totalCountString = String.valueOf(mTotalCount);
+ }
+ final String text = mContext.getString(R.string.progress_notifier_message,
+ String.valueOf(mCurrentCount),
+ totalCountString,
+ contactStruct.getDisplayName());
+
+ // TODO: uploading image does not work correctly. (looks like a static image).
+ remoteViews.setTextViewText(R.id.description, text);
+ remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
+ mTotalCount == -1);
+ final String percentage =
+ mContext.getString(R.string.percentage,
+ String.valueOf(mCurrentCount * 100/mTotalCount));
+ remoteViews.setTextViewText(R.id.progress_text, percentage);
+ remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_download);
+
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download;
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ notification.contentView = remoteViews;
+
+ notification.contentIntent =
+ PendingIntent.getActivity(mContext, 0,
+ new Intent(mContext, ContactsListActivity.class), 0);
+ mNotificationManager.notify(ImportVCardService.NOTIFICATION_ID, notification);
+ }
+
+ public synchronized void addTotalCount(int additionalCount) {
+ mTotalCount += additionalCount;
+ }
+
+ public synchronized void resetTotalCount() {
+ mTotalCount = 0;
+ }
+
+ public void onEnd() {
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ImportRequest.java b/src/com/android/contacts/ImportRequest.java
new file mode 100644
index 0000000..56a33b1
--- /dev/null
+++ b/src/com/android/contacts/ImportRequest.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+
+import android.accounts.Account;
+import android.net.Uri;
+
+import com.android.vcard.VCardSourceDetector;
+
+/**
+ * Class representing one request for reading vCard (as a Uri representation).
+ *
+ * Mainly used when {@link ImportVCardActivity} requests {@link ImportVCardService}
+ * 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;
+ public final Uri uri;
+ /**
+ * 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,
+ Uri uri, int estimatedType, String estimatedCharset,
+ int vcardVersion, int entryCount) {
+ this.account = account;
+ this.uri = uri;
+ this.estimatedVCardType = estimatedType;
+ this.estimatedCharset = estimatedCharset;
+ this.vcardVersion = vcardVersion;
+ this.entryCount = entryCount;
+ }
+}
diff --git a/src/com/android/contacts/ImportRequestProcessor.java b/src/com/android/contacts/ImportRequestProcessor.java
new file mode 100644
index 0000000..d19680b
--- /dev/null
+++ b/src/com/android/contacts/ImportRequestProcessor.java
@@ -0,0 +1,361 @@
+/*
+ * 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;
+
+import android.accounts.Account;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract.RawContacts;
+import android.util.Log;
+
+import com.android.vcard.VCardEntryCommitter;
+import com.android.vcard.VCardEntryConstructor;
+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.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * Class for processing incoming import request from {@link ImportVCardActivity}.
+ *
+ * This class is designed so that a user ({@link Service}) does not need to (and should not)
+ * recreate multiple instances, as this holds total count of vCard entries to be imported.
+ */
+public class ImportRequestProcessor {
+ private static final String LOG_TAG = "ImportRequestProcessor";
+
+ private final Service mService;
+
+ private ContentResolver mResolver;
+ private NotificationManager mNotificationManager;
+
+ private final List<Uri> mFailedUris = new ArrayList<Uri>();
+ private final List<Uri> mCreatedUris = new ArrayList<Uri>();
+ private final ImportProgressNotifier mNotifier = new ImportProgressNotifier();
+
+ private VCardParser mVCardParser;
+
+ // TODO(dmiyakawa): better design for testing?
+ /* package */ interface CommitterGenerator {
+ public VCardEntryCommitter generate(ContentResolver resolver);
+ }
+
+ private static class DefaultCommitterGenerator implements CommitterGenerator {
+ public VCardEntryCommitter generate(ContentResolver resolver) {
+ return new VCardEntryCommitter(resolver);
+ }
+ }
+
+ /* package */ CommitterGenerator mCommitterGenerator = new DefaultCommitterGenerator();
+
+ /**
+ * Meaning a controller of this object requests the operation should be canceled
+ * or not, which implies {@link #mReadyForRequest} should be set to false soon, but
+ * it does not meaning cancel request is able to immediately stop this object,
+ * so we have two variables.
+ */
+ private boolean mCanceled;
+
+ /**
+ * Meaning that this object is able to accept import requests.
+ */
+ private boolean mReadyForRequest;
+ private final Queue<ImportRequest> mPendingRequests =
+ new LinkedList<ImportRequest>();
+
+ /* package */ interface ThreadStarter {
+ public void start();
+ }
+ /* package */ ThreadStarter mThreadStarter = new ThreadStarter() {
+ public void start() {
+ final Thread thread = new Thread(new Runnable() {
+ public void run() {
+ process();
+ }
+ });
+ thread.start();
+ }
+ };
+
+
+ public ImportRequestProcessor(final Service service) {
+ mService = service;
+ }
+
+ public synchronized void pushRequest(ImportRequest parameter) {
+ if (mResolver == null) {
+ // Service object may not ready at the construction time
+ // (e.g. ContentResolver may be null).
+ mResolver = mService.getContentResolver();
+ mNotificationManager =
+ (NotificationManager)mService.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ final boolean needThreadStart;
+ if (!mReadyForRequest) {
+ mFailedUris.clear();
+ mCreatedUris.clear();
+
+ mNotifier.init(mService, mNotificationManager);
+ needThreadStart = true;
+ } else {
+ needThreadStart = false;
+ }
+ final int count = parameter.entryCount;
+ if (count > 0) {
+ mNotifier.addTotalCount(count);
+ }
+ mPendingRequests.add(parameter);
+ if (needThreadStart) {
+ mThreadStarter.start();
+ }
+
+ mReadyForRequest = true;
+ }
+
+ /**
+ * Starts processing import requests. Never stops until all given requests are
+ * processed or some error happens, assuming this method is called from a
+ * {@link Thread} object.
+ */
+ /* package */ void process() {
+ if (!mReadyForRequest) {
+ throw new RuntimeException(
+ "process() is called after this object finishing its process.");
+ }
+ try {
+ while (!mCanceled) {
+ final ImportRequest parameter;
+ synchronized (this) {
+ if (mPendingRequests.size() == 0) {
+ mReadyForRequest = false;
+ break;
+ } else {
+ parameter = mPendingRequests.poll();
+ }
+ } // synchronized (this)
+ handleOneRequest(parameter);
+ }
+
+ // Currenty we don't have an appropriate way to let users see all entries
+ // imported in this procedure. Instead, we show them entries only when
+ // there's just one created uri.
+ doFinishNotification(mCreatedUris.size() == 1 ? mCreatedUris.get(0) : null);
+ } finally {
+ // TODO: verify this works fine.
+ mReadyForRequest = false; // Just in case.
+ mNotifier.resetTotalCount();
+ }
+ }
+
+ /**
+ * Would be run inside syncronized block.
+ */
+ /* package */ boolean handleOneRequest(final ImportRequest parameter) {
+ if (mCanceled) {
+ Log.i(LOG_TAG, "Canceled before actually handling parameter ("
+ + parameter.uri + ")");
+ return false;
+ }
+ final int[] possibleVCardVersions;
+ if (parameter.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[] {
+ parameter.vcardVersion
+ };
+ }
+
+ final Uri uri = parameter.uri;
+ final Account account = parameter.account;
+ final int estimatedVCardType = parameter.estimatedVCardType;
+ final String estimatedCharset = parameter.estimatedCharset;
+
+ final VCardEntryConstructor constructor =
+ new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
+ final VCardEntryCommitter committer = mCommitterGenerator.generate(mResolver);
+ constructor.addEntryHandler(committer);
+ constructor.addEntryHandler(mNotifier);
+
+ final boolean successful =
+ readOneVCard(uri, estimatedVCardType, estimatedCharset,
+ constructor, possibleVCardVersions);
+ if (successful) {
+ List<Uri> uris = committer.getCreatedUris();
+ if (uris != null) {
+ mCreatedUris.addAll(uris);
+ } else {
+ // Not critical, but suspicious.
+ Log.w(LOG_TAG,
+ "Created Uris is null while the creation itself is successful.");
+ }
+ } else {
+ mFailedUris.add(uri);
+ }
+
+ return successful;
+ }
+
+ /*
+ private void doErrorNotification(int id) {
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download_done;
+ final String title = mService.getString(R.string.reading_vcard_failed_title);
+ final PendingIntent intent =
+ PendingIntent.getActivity(mService, 0, new Intent(), 0);
+ notification.setLatestEventInfo(mService, title, "", intent);
+ mNotificationManager.notify(MESSAGE_ID, notification);
+ }
+ */
+
+ private void doFinishNotification(Uri createdUri) {
+ final Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_download_done;
+ final String title = mService.getString(R.string.reading_vcard_finished_title);
+
+ final Intent intent;
+ if (createdUri != null) {
+ final long rawContactId = ContentUris.parseId(createdUri);
+ final Uri contactUri = RawContacts.getContactLookupUri(
+ mResolver, ContentUris.withAppendedId(
+ RawContacts.CONTENT_URI, rawContactId));
+ intent = new Intent(Intent.ACTION_VIEW, contactUri);
+ } else {
+ intent = null;
+ }
+
+ final PendingIntent pendingIntent =
+ PendingIntent.getActivity(mService, 0, intent, 0);
+ notification.setLatestEventInfo(mService, title, "", pendingIntent);
+ mNotificationManager.notify(ImportVCardService.NOTIFICATION_ID, notification);
+ }
+
+ private boolean readOneVCard(Uri uri, int vcardType, String charset,
+ VCardInterpreter interpreter,
+ final int[] possibleVCardVersions) {
+ boolean successful = false;
+ final int length = possibleVCardVersions.length;
+ for (int i = 0; i < length; i++) {
+ InputStream is = null;
+ final int vcardVersion = possibleVCardVersions[i];
+ try {
+ if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
+ // Let the object clean up internal temporary objects,
+ ((VCardEntryConstructor) interpreter).clear();
+ }
+
+ is = mResolver.openInputStream(uri);
+
+ // 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 (mCanceled) {
+ 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 intsead 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.getMessage());
+ } 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.getMessage());
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ return successful;
+ }
+
+ public synchronized boolean isReadyForRequest() {
+ return mReadyForRequest;
+ }
+
+ public boolean isCanceled() {
+ return mCanceled;
+ }
+
+ public void cancel() {
+ mCanceled = true;
+ synchronized (this) {
+ if (mVCardParser != null) {
+ mVCardParser.cancel();
+ }
+ }
+ }
+
+ public List<Uri> getCreatedUris() {
+ return mCreatedUris;
+ }
+
+ public List<Uri> getFailedUris() {
+ return mFailedUris;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index f263f7b..ade0fdb 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -44,7 +44,6 @@
import android.text.style.RelativeSizeSpan;
import android.util.Log;
-import com.android.contacts.ImportVCardService.RequestParameter;
import com.android.contacts.model.Sources;
import com.android.contacts.util.AccountSelectionUtil;
import com.android.vcard.VCardEntryCounter;
@@ -123,10 +122,10 @@
private class CustomConnection implements ServiceConnection {
private Messenger mMessenger;
/**
- * Stores {@link RequestParameter} objects until actual connection is established.
+ * Stores {@link ImportRequest} objects until actual connection is established.
*/
- private Queue<RequestParameter> mPendingRequests =
- new LinkedList<RequestParameter>();
+ private Queue<ImportRequest> mPendingRequests =
+ new LinkedList<ImportRequest>();
private boolean mConnected = false;
private boolean mNeedFinish = false;
@@ -147,7 +146,7 @@
}
}
- public synchronized void requestSend(final RequestParameter parameter) {
+ public synchronized void requestSend(final ImportRequest parameter) {
// Log.d("@@@", "requestSend(): " + (mMessenger != null) + ", "
// + mPendingRequests.size());
if (mMessenger != null) {
@@ -157,11 +156,11 @@
}
}
- private void sendMessage(final RequestParameter parameter) {
+ private void sendMessage(final ImportRequest parameter) {
// Log.d("@@@", "sendMessage()");
try {
mMessenger.send(Message.obtain(null,
- ImportVCardService.IMPORT_REQUEST,
+ ImportVCardService.MSG_IMPORT_REQUEST,
parameter));
} catch (RemoteException e) {
Log.e(LOG_TAG, "RemoteException is thrown when trying to import vCard");
@@ -171,13 +170,12 @@
}
public void onServiceConnected(ComponentName name, IBinder service) {
- // Log.d("@@@", "onServiceConnected()");
synchronized (this) {
mMessenger = new Messenger(service);
// Send pending requests thrown from this Activity before an actual connection
// is established.
while (!mPendingRequests.isEmpty()) {
- final RequestParameter parameter = mPendingRequests.poll();
+ final ImportRequest parameter = mPendingRequests.poll();
if (parameter == null) {
throw new NullPointerException();
}
@@ -328,7 +326,7 @@
Log.w(LOG_TAG, "destUri is null");
break;
}
- final RequestParameter parameter = constructRequestParameter(localDataUri);
+ final ImportRequest parameter = constructRequestParameter(localDataUri);
if (mCanceled) {
return;
}
@@ -413,10 +411,10 @@
}
/**
- * Reads the Uri once (or twice) and constructs {@link RequestParameter} from
+ * Reads the Uri once (or twice) and constructs {@link ImportRequest} from
* its content.
*/
- private RequestParameter constructRequestParameter(final Uri uri) {
+ private ImportRequest constructRequestParameter(final Uri uri) {
final ContentResolver resolver =
ImportVCardActivity.this.getContentResolver();
VCardEntryCounter counter = null;
@@ -475,7 +473,7 @@
Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
return null;
}
- return new RequestParameter(mAccount, uri,
+ return new ImportRequest(mAccount, uri,
detector.getEstimatedType(),
detector.getEstimatedCharset(),
vcardVersion, counter.getCount());
diff --git a/src/com/android/contacts/ImportVCardService.java b/src/com/android/contacts/ImportVCardService.java
index e79bf82..f576d4b 100644
--- a/src/com/android/contacts/ImportVCardService.java
+++ b/src/com/android/contacts/ImportVCardService.java
@@ -15,135 +15,45 @@
*/
package com.android.contacts;
-import android.accounts.Account;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
import android.app.Service;
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.Context;
import android.content.Intent;
-import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
-import android.provider.ContactsContract.RawContacts;
import android.util.Log;
-import android.widget.RemoteViews;
import android.widget.Toast;
-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.VCardSourceDetector;
-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.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-
/**
* The class responsible for importing vCard from one ore multiple Uris.
*/
public class ImportVCardService extends Service {
private final static String LOG_TAG = "ImportVCardService";
- /* package */ static final int IMPORT_REQUEST = 1;
+ /* package */ static final int MSG_IMPORT_REQUEST = 1;
- private static final int MESSAGE_ID = 1000;
-
- // TODO: Too many static classes. Create separate files for them.
+ /* package */ static final int NOTIFICATION_ID = 1000;
/**
- * Class representing one request for reading vCard (as a Uri representation).
+ * Small vCard file is imported soon, so any meassage saying "vCard import started" is
+ * not needed. We show the message when the size of vCard is larger than this constant.
*/
- /* package */ static class RequestParameter {
- public final Account account;
- /**
- * Note: One Uri does not mean there's only one vCard entry since one Uri
- * often has multiple vCard entries.
- */
- public final Uri uri;
- /**
- * 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;
- public final int entryCount;
-
- public RequestParameter(Account account,
- Uri uri, int estimatedType, String estimatedCharset,
- int vcardVersion, int entryCount) {
- this.account = account;
- this.uri = uri;
- this.estimatedVCardType = estimatedType;
- this.estimatedCharset = estimatedCharset;
- this.vcardVersion = vcardVersion;
- this.entryCount = entryCount;
- }
- }
+ private static final int IMPORT_NOTIFICATION_THRESHOLD = 10;
public class ImportRequestHandler extends Handler {
+ private final ImportRequestProcessor mRequestProcessor =
+ new ImportRequestProcessor(ImportVCardService.this);
@Override
public void handleMessage(Message msg) {
- Log.d("@@@", "handleMessange: " + msg.what);
switch (msg.what) {
- case IMPORT_REQUEST: {
- Log.d("@@@", "IMPORT_REQUEST");
- final RequestParameter parameter = (RequestParameter)msg.obj;
- Toast.makeText(ImportVCardService.this,
- getString(R.string.vcard_importer_start_message),
- Toast.LENGTH_LONG).show();
- final boolean needThreadStart;
- if (mRequestHandler == null || !mRequestHandler.isRunning()) {
- mRequestHandler = new RequestHandler();
- mRequestHandler.init(ImportVCardService.this);
- needThreadStart = true;
- } else {
- needThreadStart = false;
+ case MSG_IMPORT_REQUEST: {
+ final ImportRequest parameter = (ImportRequest)msg.obj;
+ if (parameter.entryCount > IMPORT_NOTIFICATION_THRESHOLD) {
+ Toast.makeText(ImportVCardService.this,
+ getString(R.string.vcard_importer_start_message),
+ Toast.LENGTH_LONG).show();
}
- mRequestHandler.addRequest(parameter);
- if (needThreadStart) {
- mThread = new Thread(new Runnable() {
- public void run() {
- mRequestHandler.handleRequests();
- }
- });
- mThread.start();
- }
+ mRequestProcessor.pushRequest(parameter);
break;
}
default:
@@ -153,341 +63,7 @@
}
}
- // TODO(dmiyakawa): better design for testing?
- /* package */ interface CommitterGenerator {
- public VCardEntryCommitter generate(ContentResolver resolver);
- }
- /* package */ static class DefaultCommitterGenerator implements CommitterGenerator {
- public VCardEntryCommitter generate(ContentResolver resolver) {
- return new VCardEntryCommitter(resolver);
- }
- }
-
- /**
- * For testability, we don't inherit Thread here.
- */
- private static class RequestHandler {
- private ImportVCardService mService;
- private ContentResolver mResolver;
- private NotificationManager mNotificationManager;
-
- private final List<Uri> mFailedUris = new ArrayList<Uri>();
- private final List<Uri> mCreatedUris = new ArrayList<Uri>();
- private ProgressNotifier mProgressNotifier = new ProgressNotifier();
-
- private VCardParser mVCardParser;
-
- /* package */ CommitterGenerator mCommitterGenerator = new DefaultCommitterGenerator();
-
- /**
- * Meaning a controller of this object requests the operation should be canceled
- * or not, which implies {@link #mRunning} should be set to false soon, but
- * it does not meaning cancel request is able to immediately stop this object,
- * so we have two variables.
- */
- private boolean mCanceled;
- /**
- * Meaning this object is actually running.
- */
- private boolean mRunning = true;
- private final Queue<RequestParameter> mPendingRequests =
- new LinkedList<RequestParameter>();
-
- public void init(ImportVCardService service) {
- // TODO: Based on fragile fact. fix this.
- mService = service;
- mResolver = service.getContentResolver();
- mNotificationManager =
- (NotificationManager)service.getSystemService(Context.NOTIFICATION_SERVICE);
- mProgressNotifier.init(mService, mNotificationManager);
- }
-
- public void handleRequests() {
- try {
- while (!mCanceled) {
- final RequestParameter parameter;
- synchronized (this) {
- if (mPendingRequests.size() == 0) {
- mRunning = false;
- break;
- } else {
- parameter = mPendingRequests.poll();
- }
- } // synchronized (this)
- handleOneRequest(parameter);
- }
-
- // Currenty we don't have an appropriate way to let users see all entries
- // imported in this procedure. Instead, we show them entries only when
- // there's just one created uri.
- doFinishNotification(mCreatedUris.size() == 1 ? mCreatedUris.get(0) : null);
- } finally {
- // TODO: verify this works fine.
- mRunning = false; // Just in case.
- // mService.stopSelf();
- }
- }
-
- /**
- * Would be run inside syncronized block.
- */
- public boolean handleOneRequest(final RequestParameter parameter) {
- if (mCanceled) {
- Log.i(LOG_TAG, "Canceled before actually handling parameter ("
- + parameter.uri + ")");
- return false;
- }
- final int[] possibleVCardVersions;
- if (parameter.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[] {
- parameter.vcardVersion
- };
- }
-
- final Uri uri = parameter.uri;
- final Account account = parameter.account;
- final int estimatedVCardType = parameter.estimatedVCardType;
- final String estimatedCharset = parameter.estimatedCharset;
-
- final VCardEntryConstructor constructor =
- new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset);
- final VCardEntryCommitter committer = mCommitterGenerator.generate(mResolver);
- constructor.addEntryHandler(committer);
- constructor.addEntryHandler(mProgressNotifier);
-
- final boolean successful =
- readOneVCard(uri, estimatedVCardType, estimatedCharset,
- constructor, possibleVCardVersions);
- if (successful) {
- List<Uri> uris = committer.getCreatedUris();
- if (uris != null) {
- mCreatedUris.addAll(uris);
- } else {
- // Not critical, but suspicious.
- Log.w(LOG_TAG,
- "Created Uris is null while the creation itself is successful.");
- }
- } else {
- mFailedUris.add(uri);
- }
-
- return successful;
- }
-
- /*
- private void doErrorNotification(int id) {
- final Notification notification = new Notification();
- notification.icon = android.R.drawable.stat_sys_download_done;
- final String title = mService.getString(R.string.reading_vcard_failed_title);
- final PendingIntent intent =
- PendingIntent.getActivity(mService, 0, new Intent(), 0);
- notification.setLatestEventInfo(mService, title, "", intent);
- mNotificationManager.notify(MESSAGE_ID, notification);
- }
- */
-
- private void doFinishNotification(Uri createdUri) {
- final Notification notification = new Notification();
- notification.icon = android.R.drawable.stat_sys_download_done;
- final String title = mService.getString(R.string.reading_vcard_finished_title);
-
- final Intent intent;
- if (createdUri != null) {
- final long rawContactId = ContentUris.parseId(createdUri);
- final Uri contactUri = RawContacts.getContactLookupUri(
- mResolver, ContentUris.withAppendedId(
- RawContacts.CONTENT_URI, rawContactId));
- intent = new Intent(Intent.ACTION_VIEW, contactUri);
- } else {
- intent = null;
- }
-
- final PendingIntent pendingIntent =
- PendingIntent.getActivity(mService, 0, intent, 0);
- notification.setLatestEventInfo(mService, title, "", pendingIntent);
- mNotificationManager.notify(MESSAGE_ID, notification);
- }
-
- private boolean readOneVCard(Uri uri, int vcardType, String charset,
- VCardInterpreter interpreter,
- final int[] possibleVCardVersions) {
- boolean successful = false;
- final int length = possibleVCardVersions.length;
- for (int i = 0; i < length; i++) {
- InputStream is = null;
- final int vcardVersion = possibleVCardVersions[i];
- try {
- if (i > 0 && (interpreter instanceof VCardEntryConstructor)) {
- // Let the object clean up internal temporary objects,
- ((VCardEntryConstructor) interpreter).clear();
- }
-
- is = mResolver.openInputStream(uri);
-
- // 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 (mCanceled) {
- 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 intsead 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.getMessage());
- } 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.getMessage());
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- }
- }
- }
- }
-
- return successful;
- }
-
- public boolean isRunning() {
- return mRunning;
- }
-
- public boolean isCanceled() {
- return mCanceled;
- }
-
- public void cancel() {
- mCanceled = true;
- synchronized (this) {
- if (mVCardParser != null) {
- mVCardParser.cancel();
- }
- }
- }
-
- public synchronized void addRequest(RequestParameter parameter) {
- if (mRunning) {
- mProgressNotifier.addTotalCount(parameter.entryCount);
- mPendingRequests.add(parameter);
- } else {
- Log.w(LOG_TAG, "Request came while the service is not running any more.");
- }
- }
-
- public List<Uri> getCreatedUris() {
- return mCreatedUris;
- }
-
- public List<Uri> getFailedUris() {
- return mFailedUris;
- }
- }
-
- private static class ProgressNotifier implements VCardEntryHandler {
- private Context mContext;
- private NotificationManager mNotificationManager;
-
- private int mCurrentCount;
- private int mTotalCount;
-
- public void init(Context context, NotificationManager notificationManager) {
- mContext = context;
- mNotificationManager = notificationManager;
- }
-
- public void onStart() {
- }
-
- public void onEntryCreated(VCardEntry contactStruct) {
- mCurrentCount++; // 1 origin.
- if (contactStruct.isIgnorable()) {
- return;
- }
-
- // We don't use startEntry() since:
- // - We cannot know name there but here.
- // - There's high probability where name comes soon after the beginning of entry, so
- // we don't need to hurry to show something.
- final String packageName = "com.android.contacts";
- final RemoteViews remoteViews = new RemoteViews(packageName,
- R.layout.status_bar_ongoing_event_progress_bar);
- final String title = mContext.getString(R.string.reading_vcard_title);
- String totalCountString;
- synchronized (this) {
- totalCountString = String.valueOf(mTotalCount);
- }
- final String text = mContext.getString(R.string.progress_notifier_message,
- String.valueOf(mCurrentCount),
- totalCountString,
- contactStruct.getDisplayName());
-
- // TODO: uploading image does not work correctly. (looks like a static image).
- remoteViews.setTextViewText(R.id.description, text);
- remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
- mTotalCount == -1);
- final String percentage =
- mContext.getString(R.string.percentage,
- String.valueOf(mCurrentCount * 100/mTotalCount));
- remoteViews.setTextViewText(R.id.progress_text, percentage);
- remoteViews.setImageViewResource(R.id.appIcon, android.R.drawable.stat_sys_download);
-
- final Notification notification = new Notification();
- notification.icon = android.R.drawable.stat_sys_download;
- notification.flags |= Notification.FLAG_ONGOING_EVENT;
- notification.contentView = remoteViews;
-
- notification.contentIntent =
- PendingIntent.getActivity(mContext, 0,
- new Intent(mContext, ContactsListActivity.class), 0);
- mNotificationManager.notify(MESSAGE_ID, notification);
- }
-
- public synchronized void addTotalCount(int additionalCount) {
- mTotalCount += additionalCount;
- }
-
- public void onEnd() {
- }
- }
-
- private RequestHandler mRequestHandler;
private Messenger mMessenger = new Messenger(new ImportRequestHandler());
- private Thread mThread = null;
@Override
public int onStartCommand(Intent intent, int flags, int id) {
@@ -496,7 +72,6 @@
@Override
public IBinder onBind(Intent intent) {
- Log.d("@@@", "onBind");
return mMessenger.getBinder();
}
}