Make ImportVCardService aware of multiple requests from Activity and
unit test friendly.
This change works fine but is not sufficient enough. Need to be
refactored more.
Bug: 2733143
Change-Id: Icea90e68bb61591d67e03a6c22a5046c42e606b8
diff --git a/src/com/android/contacts/ImportVCardActivity.java b/src/com/android/contacts/ImportVCardActivity.java
index 09c4980..e021457 100644
--- a/src/com/android/contacts/ImportVCardActivity.java
+++ b/src/com/android/contacts/ImportVCardActivity.java
@@ -21,23 +21,30 @@
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
+import android.content.ServiceConnection;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
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.os.PowerManager;
+import android.os.RemoteException;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
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;
@@ -48,14 +55,12 @@
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.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
-import java.nio.channels.Channel;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
@@ -88,15 +93,19 @@
/* 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 USE_V30_ARRAY = "use_v30";
+ /* 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;
+
// Run on the UI thread. Must not be null except after onDestroy().
private Handler mHandler = new Handler();
private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
- private String mAccountName;
- private String mAccountType;
+
+ private Account mAccount;
private String mAction;
private Uri mUri;
@@ -111,6 +120,19 @@
private String mErrorMessage;
+ private Messenger mMessenger;
+
+ private final ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mMessenger = new Messenger(service);
+ }
+
+ public void onServiceDisconnected(ComponentName name) {
+ mMessenger = null;
+ finish();
+ }
+ };
+
private static class VCardFile {
private final String mName;
private final String mCanonicalPath;
@@ -180,24 +202,9 @@
private VCardParser mVCardParser;
private final Uri[] mSourceUris;
- // Not using Uri[] since we throw this object to Service via Intent.
- private final String[] mDestUriStrings;
-
- private final int[] mEstimatedVCardTypes;
- private final String[] mEstimatedCharsets;
- private final boolean[] mShouldUseV30;
- private final int[] mEntryCounts;
-
public VCardCacheThread(final Uri[] sourceUris) {
mSourceUris = sourceUris;
final int length = sourceUris.length;
-
- mDestUriStrings = new String[length];
- mEstimatedVCardTypes = new int[length];
- mEstimatedCharsets = new String[length];
- mShouldUseV30 = new boolean[length];
- mEntryCounts = new int[length];
-
final Context context = ImportVCardActivity.this;
PowerManager powerManager = (PowerManager)context.getSystemService(
Context.POWER_SERVICE);
@@ -219,34 +226,48 @@
final ContentResolver resolver = context.getContentResolver();
String errorMessage = null;
mWakeLock.acquire();
-
+ boolean successful = false;
try {
clearOldCache();
+ bindService(new Intent(ImportVCardActivity.this,
+ ImportVCardService.class), mConnection, Context.BIND_AUTO_CREATE);
+ final int length = mSourceUris.length;
+ // 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. This copy is also useful fro the view of stability of the import,
+ // as we are able to restore the procedure even when it is aborted during it.
+ // Imagine the case the importer encountered memory-low situation when
+ // reading 10th entry of a vCard file.
+ //
// 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.
- //
- // Note that Uris given from caller applications may not be opened twice,
- // so we have to use local data after this copy instead of relying on
- // the original source Uris.
- copyVCardToLocal();
- if (mCanceled) {
- return;
+ for (int i = 0; i < length; i++) {
+ final Uri sourceUri = mSourceUris[i];
+ final Uri localDataUri = copyToLocal(sourceUri, i);
+ if (mCanceled) {
+ break;
+ }
+ if (localDataUri == null) {
+ Log.w(LOG_TAG, "destUri is null");
+ break;
+ }
+ final RequestParameter parameter = constructRequestParameter(localDataUri);
+ if (mCanceled) {
+ return;
+ }
+ mMessenger.send(Message.obtain(null,
+ ImportVCardService.IMPORT_REQUEST,
+ parameter));
+
}
- Log.d("@@@", "Caching done. Count the number of vCard entries.");
- if (!collectVCardMetaInfo()) {
- Log.e(LOG_TAG, "Failed to collect vCard meta information");
- runOnUIThread(new DialogDisplayer(
- getString(R.string.fail_reason_failed_to_collect_vcard_meta_info)));
- return;
- }
- if (mCanceled) {
- return;
- }
- for (int i = 0; i < mEntryCounts.length; i++) {
- Log.d("@@@", String.format("length for %s: %d",
- mDestUriStrings[i], mEntryCounts[i]));
- }
+
+ successful = true;
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "RemoteException is thrown when trying to import vCard");
+ runOnUIThread(new DialogDisplayer(getString(R.string.fail_reason_unknown)));
} catch (OutOfMemoryError e) {
Log.e(LOG_TAG, "OutOfMemoryError");
// We should take care of this case since Android devices may have
@@ -255,149 +276,54 @@
runOnUIThread(new DialogDisplayer(
getString(R.string.fail_reason_io_error) +
": " + e.getLocalizedMessage()));
- return;
} catch (IOException e) {
Log.e(LOG_TAG, e.getMessage());
runOnUIThread(new DialogDisplayer(
getString(R.string.fail_reason_io_error) +
": " + e.getLocalizedMessage()));
- return;
} finally {
-
mWakeLock.release();
mProgressDialogForCacheVCard.dismiss();
}
- // TODO(dmiyakawa): do we need runOnUIThread?
- final Intent intent = new Intent(
- ImportVCardActivity.this, ImportVCardService.class);
- intent.putExtra(VCARD_URI_ARRAY, mDestUriStrings);
- intent.putExtra(SelectAccountActivity.ACCOUNT_NAME, mAccountName);
- intent.putExtra(SelectAccountActivity.ACCOUNT_TYPE, mAccountType);
- intent.putExtra(ESTIMATED_VCARD_TYPE_ARRAY, mEstimatedVCardTypes);
- intent.putExtra(ESTIMATED_CHARSET_ARRAY, mEstimatedCharsets);
- intent.putExtra(USE_V30_ARRAY, mShouldUseV30);
- intent.putExtra(ENTRY_COUNT_ARRAY, mEntryCounts);
- startService(intent);
- finish();
- }
-
-
- private boolean collectVCardMetaInfo() {
- final ContentResolver resolver =
- ImportVCardActivity.this.getContentResolver();
- final int length = mDestUriStrings.length;
- try {
- for (int i = 0; i < length; i++) {
- if (mCanceled) {
- return false;
- }
- final Uri uri = Uri.parse(mDestUriStrings[i]);
- boolean shouldUseV30 = false;
- InputStream is;
- VCardEntryCounter counter;
- VCardSourceDetector detector;
- VCardInterpreterCollection interpreter;
-
- is = resolver.openInputStream(uri);
- mVCardParser = new VCardParser_V21();
- try {
- counter = new VCardEntryCounter();
- detector = new VCardSourceDetector();
- interpreter =
- new VCardInterpreterCollection(
- Arrays.asList(counter, detector));
- mVCardParser.parse(is, interpreter);
- } catch (VCardVersionException e1) {
- try {
- is.close();
- } catch (IOException e) {
- }
-
- shouldUseV30 = true;
- is = resolver.openInputStream(uri);
- mVCardParser = new VCardParser_V30();
- try {
- counter = new VCardEntryCounter();
- detector = new VCardSourceDetector();
- interpreter =
- new VCardInterpreterCollection(
- Arrays.asList(counter, detector));
- mVCardParser.parse(is, interpreter);
- } catch (VCardVersionException e2) {
- throw new VCardException("vCard with unspported version.");
- }
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- }
- }
- }
-
- mEstimatedVCardTypes[i] = detector.getEstimatedType();
- mEstimatedCharsets[i] = detector.getEstimatedCharset();
- mShouldUseV30[i] = shouldUseV30;
- mEntryCounts[i] = counter.getCount();
- }
- } catch (IOException e) {
- Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
- return false;
- } catch (VCardNestedException e) {
- Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
- } catch (VCardNotSupportedException e) {
- return false;
- } catch (VCardException e) {
- return false;
+ if (successful) {
+ finish();
+ } else {
+ // finish() should be called via DialogDisplayer().
}
- return true;
}
- private void copyVCardToLocal() throws IOException {
+ /**
+ * Copy the content of sourceUri to local storage.
+ */
+ private Uri copyToLocal(final Uri sourceUri, int i) throws IOException {
final Context context = ImportVCardActivity.this;
final ContentResolver resolver = context.getContentResolver();
ReadableByteChannel inputChannel = null;
WritableByteChannel outputChannel = null;
+ Uri destUri;
try {
- int length = mSourceUris.length;
- for (int i = 0; i < length; i++) {
- if (mCanceled) {
- Log.d(LOG_TAG, "Canceled during import");
- break;
- }
- // XXX: better way to copy stream?
+ // XXX: better way to copy stream?
+ {
inputChannel = Channels.newChannel(resolver.openInputStream(mSourceUris[i]));
final String filename = CACHE_FILE_PREFIX + i + ".vcf";
- mDestUriStrings[i] =
- context.getFileStreamPath(filename).toURI().toString();
- Log.d("@@@", "temporary file: " + filename + ", dest: " + mDestUriStrings[i]);
+ 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 " + mSourceUris[i]);
- break;
- }
- buffer.flip();
- outputChannel.write(buffer);
- buffer.compact();
+ final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
+ while (inputChannel.read(buffer) != -1) {
+ if (mCanceled) {
+ Log.d(LOG_TAG, "Canceled during caching " + mSourceUris[i]);
+ return null;
}
buffer.flip();
- while (buffer.hasRemaining()) {
- outputChannel.write(buffer);
- }
+ outputChannel.write(buffer);
+ buffer.compact();
}
-
- // Avoid double close() in the "finally" block bellow.
- Channel tmp = inputChannel;
- inputChannel = null;
- tmp.close();
- tmp = outputChannel;
- outputChannel = null;
- tmp.close();
+ buffer.flip();
+ while (buffer.hasRemaining()) {
+ outputChannel.write(buffer);
+ }
}
} finally {
if (inputChannel != null) {
@@ -415,6 +341,76 @@
}
}
}
+ return destUri;
+ }
+
+ /**
+ * Reads the Uri once (or twice) and constructs {@link RequestParameter} from
+ * its content.
+ */
+ private RequestParameter constructRequestParameter(final Uri uri) {
+ final ContentResolver resolver =
+ ImportVCardActivity.this.getContentResolver();
+ VCardEntryCounter counter = null;
+ VCardSourceDetector detector = null;
+ VCardInterpreterCollection interpreter = null;
+ int vcardVersion = VCARD_VERSION_V21;
+ try {
+ boolean shouldUseV30 = false;
+ InputStream is;
+
+ is = resolver.openInputStream(uri);
+ mVCardParser = new VCardParser_V21();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ interpreter =
+ new VCardInterpreterCollection(
+ Arrays.asList(counter, detector));
+ mVCardParser.parse(is, interpreter);
+ } catch (VCardVersionException e1) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+
+ shouldUseV30 = true;
+ is = resolver.openInputStream(uri);
+ mVCardParser = new VCardParser_V30();
+ try {
+ counter = new VCardEntryCounter();
+ detector = new VCardSourceDetector();
+ interpreter =
+ new VCardInterpreterCollection(
+ Arrays.asList(counter, detector));
+ mVCardParser.parse(is, interpreter);
+ } 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 returning null.
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, e.getMessage());
+ return null;
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
+ return null;
+ }
+ return new RequestParameter(mAccount, uri,
+ detector.getEstimatedType(),
+ detector.getEstimatedCharset(),
+ vcardVersion, counter.getCount());
}
/**
@@ -666,10 +662,8 @@
importVCard(uriStrings);
}
- private void importVCard(final String uriString) {
- String[] uriStrings = new String[1];
- uriStrings[0] = uriString;
- importVCard(uriStrings);
+ private void importVCard(final Uri uri) {
+ importVCard(new Uri[] {uri});
}
private void importVCard(final String[] uriStrings) {
@@ -678,6 +672,10 @@
for (int i = 0; i < length; i++) {
uris[i] = Uri.parse(uriStrings[i]);
}
+ importVCard(uris);
+ }
+
+ private void importVCard(final Uri[] uris) {
runOnUIThread(new Runnable() {
public void run() {
mVCardCacheThread = new VCardCacheThread(uris);
@@ -745,33 +743,34 @@
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
+ String accountName = null;
+ String accountType = null;
final Intent intent = getIntent();
if (intent != null) {
- mAccountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
- mAccountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+ accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
+ accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
mAction = intent.getAction();
mUri = intent.getData();
} else {
Log.e(LOG_TAG, "intent does not exist");
}
- // The caller may not know account information at all, so we show the UI instead.
- if (TextUtils.isEmpty(mAccountName) || TextUtils.isEmpty(mAccountType)) {
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ mAccount = new Account(accountName, accountType);
+ } else {
final Sources sources = Sources.getInstance(this);
final List<Account> accountList = sources.getAccounts(true);
if (accountList.size() == 0) {
- mAccountName = null;
- mAccountType = null;
+ mAccount = null;
} else if (accountList.size() == 1) {
- final Account account = accountList.get(0);
- mAccountName = account.name;
- mAccountType = account.type;
+ mAccount = accountList.get(0);
} else {
startActivityForResult(new Intent(this, SelectAccountActivity.class),
SELECT_ACCOUNT);
return;
}
}
+
startImport(mAction, mUri);
}
@@ -779,8 +778,9 @@
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == SELECT_ACCOUNT) {
if (resultCode == RESULT_OK) {
- mAccountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
- mAccountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
+ mAccount = new Account(
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
+ intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE));
startImport(mAction, mUri);
} else {
if (resultCode != RESULT_CANCELED) {
@@ -795,7 +795,7 @@
Log.d(LOG_TAG, "action = " + action + " ; path = " + uri);
if (uri != null) {
- importVCard(uri.toString());
+ importVCard(uri);
} else {
doScanExternalStorageAndImportVCard();
}
diff --git a/src/com/android/contacts/ImportVCardService.java b/src/com/android/contacts/ImportVCardService.java
index 293255d..f940761 100644
--- a/src/com/android/contacts/ImportVCardService.java
+++ b/src/com/android/contacts/ImportVCardService.java
@@ -23,24 +23,22 @@
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
-import android.content.DialogInterface;
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.text.TextUtils;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
-import com.android.vcard.VCardConfig;
import com.android.vcard.VCardEntry;
import com.android.vcard.VCardEntryCommitter;
import com.android.vcard.VCardEntryConstructor;
-import com.android.vcard.VCardEntryCounter;
import com.android.vcard.VCardEntryHandler;
import com.android.vcard.VCardInterpreter;
-import com.android.vcard.VCardInterpreterCollection;
import com.android.vcard.VCardParser;
import com.android.vcard.VCardParser_V21;
import com.android.vcard.VCardParser_V30;
@@ -53,7 +51,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
@@ -64,11 +61,379 @@
public class ImportVCardService extends Service {
private final static String LOG_TAG = "ImportVCardService";
- private class ProgressNotifier implements VCardEntryHandler {
- private final int mId;
+ /* package */ static final int IMPORT_REQUEST = 1;
- public ProgressNotifier(int id) {
- mId = id;
+ private static final int MESSAGE_ID = 1000;
+
+ // TODO: Too many static classes. Create separate files for them.
+
+ /**
+ * Class representing one request for reading vCard (as a Uri representation).
+ */
+ /* 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;
+ }
+ }
+
+ public class ImportRequestHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case IMPORT_REQUEST:
+ 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;
+ }
+ mRequestHandler.addRequest(parameter);
+ if (needThreadStart) {
+ mThread = new Thread(new Runnable() {
+ public void run() {
+ mRequestHandler.handleRequests();
+ }
+ });
+ mThread.start();
+ }
+ break;
+ default:
+ Log.e(LOG_TAG, "Unknown request type: " + msg.what);
+ super.hasMessages(msg.what);
+ }
+ }
+ }
+
+ // 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 ProgressNotifier mProgressNotifier;
+
+ private final List<Uri> mFailedUris;
+ private final List<Uri> mCreatedUris;
+
+ 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 RequestHandler() {
+ // We cannot set Service here since Service is not fully ready at this point.
+ // TODO: refactor this class.
+
+ mFailedUris = new ArrayList<Uri>();
+ mCreatedUris = new ArrayList<Uri>();
+ mProgressNotifier = new ProgressNotifier();
+ }
+
+ 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() {
@@ -80,7 +445,6 @@
return;
}
- final Context context = ImportVCardService.this;
// 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
@@ -88,10 +452,14 @@
final String packageName = "com.android.contacts";
final RemoteViews remoteViews = new RemoteViews(packageName,
R.layout.status_bar_ongoing_event_progress_bar);
- final String title = getString(R.string.reading_vcard_title);
- final String text = getString(R.string.progress_notifier_message,
+ 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),
- String.valueOf(mTotalCount),
+ totalCountString,
contactStruct.getDisplayName());
// TODO: uploading image does not work correctly. (looks like a static image).
@@ -99,7 +467,7 @@
remoteViews.setProgressBar(R.id.progress_bar, mTotalCount, mCurrentCount,
mTotalCount == -1);
final String percentage =
- getString(R.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);
@@ -110,359 +478,30 @@
notification.contentView = remoteViews;
notification.contentIntent =
- PendingIntent.getActivity(context, 0,
- new Intent(context, ContactsListActivity.class), 0);
- mNotificationManager.notify(mId, notification);
+ 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 class VCardReadThread extends Thread {
- private final Context mContext;
- private final ContentResolver mResolver;
- private VCardParser mVCardParser;
- private boolean mCanceled;
- private final List<Uri> mErrorUris;
- private final List<Uri> mCreatedUris;
-
- public VCardReadThread() {
- mContext = ImportVCardService.this;
- mResolver = mContext.getContentResolver();
- mErrorUris = new ArrayList<Uri>();
- mCreatedUris = new ArrayList<Uri>();
- }
-
- @Override
- public void run() {
- while (!mCanceled) {
- mErrorUris.clear();
- mCreatedUris.clear();
-
- final Account account;
- final Uri uri;
- final int estimatedType;
- final String estimatedCharset;
- final boolean useV30;
- final int entryCount;
- final int id;
- final boolean needReview;
- synchronized (mContext) {
- if (mPendingInputs.size() == 0) {
- mNowRunning = false;
- break;
- } else {
- final PendingInput pendingInput = mPendingInputs.poll();
- account = pendingInput.account;
- uri = pendingInput.uri;
- estimatedType = pendingInput.estimatedType;
- estimatedCharset = pendingInput.estimatedCharset;
- useV30 = pendingInput.useV30;
- entryCount = pendingInput.entryCount;
- id = pendingInput.id;
- }
- }
- runInternal(account, uri, estimatedType, estimatedCharset,
- useV30, entryCount, id);
- doFinishNotification(id, uri);
- }
- Log.i(LOG_TAG, "Successfully imported. Total: " + mTotalCount);
- stopSelf();
- }
-
- private void runInternal(Account account,
- Uri uri, int estimatedType, String estimatedCharset,
- boolean useV30, int entryCount,
- int id) {
- int totalCount = 0;
- final ArrayList<VCardSourceDetector> detectorList =
- new ArrayList<VCardSourceDetector>();
- if (mCanceled) {
- return;
- }
-
- // First scanning is over. Try to import each vCard, which causes side effects.
- mTotalCount += entryCount;
- mCurrentCount = 0;
-
- if (mCanceled) {
- Log.w(LOG_TAG, "Canceled during importing (with storing data in database)");
- // TODO: implement cancel correctly.
- return;
- }
-
- final VCardEntryConstructor constructor =
- new VCardEntryConstructor(estimatedType, account, estimatedCharset);
- final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
- constructor.addEntryHandler(committer);
- constructor.addEntryHandler(new ProgressNotifier(id));
-
- if (!readOneVCard(uri, estimatedType, estimatedCharset, constructor)) {
- Log.e(LOG_TAG, "Failed to read \"" + uri.toString() + "\" " +
- "while first scan was successful.");
- }
- final List<Uri> createdUris = committer.getCreatedUris();
- if (createdUris != null && createdUris.size() > 0) {
- mCreatedUris.addAll(createdUris);
- } else {
- Log.w(LOG_TAG, "Created Uris is null (src = " + uri.toString() + "\"");
- }
- }
-
- private boolean readOneVCard(Uri uri, int vcardType, String charset,
- VCardInterpreter interpreter) {
- InputStream is;
- try {
- // TODO: use vcardType given from detector and stop trying to read the file twice.
- is = mResolver.openInputStream(uri);
-
- // We need synchronized since we need to handle mCanceled and mVCardParser
- // at once. In the worst case, a user may call cancel() just before recreating
- // mVCardParser.
- synchronized (this) {
- // TODO: ensure this change works fine.
- // mVCardParser = new VCardParser_V21(vcardType, charset);
- mVCardParser = new VCardParser_V21(vcardType);
- if (mCanceled) {
- mVCardParser.cancel();
- }
- }
-
- try {
- mVCardParser.parse(is, interpreter);
- } catch (VCardVersionException e1) {
- try {
- is.close();
- } catch (IOException e) {
- }
- if (interpreter instanceof VCardEntryConstructor) {
- // Let the object clean up internal temporal objects,
- ((VCardEntryConstructor) interpreter).clear();
- }
- is = mResolver.openInputStream(uri);
-
- synchronized (this) {
- // mVCardParser = new VCardParser_V30(vcardType, charset);
- mVCardParser = new VCardParser_V30(vcardType);
- if (mCanceled) {
- mVCardParser.cancel();
- }
- }
-
- try {
- mVCardParser.parse(is, interpreter);
- } catch (VCardVersionException e2) {
- throw new VCardException("vCard with unspported version.");
- }
- } finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- }
- }
- }
- } catch (IOException e) {
- Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage());
- return false;
- } catch (VCardNestedException e) {
- // In the first scan, we may (correctly) encounter this exception.
- // We assume that we were able to detect the type of vCard before
- // the exception being thrown.
- //
- // In the second scan, we may (inappropriately) encounter it.
- // We silently ignore it, since
- // - It is really unusual situation.
- // - We cannot handle it by definition.
- // - Users cannot either.
- // - We should not accept unnecessarily complicated vCard, possibly by wrong manner.
- Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
- } catch (VCardNotSupportedException e) {
- return false;
- } catch (VCardException e) {
- return false;
- }
- return true;
- }
-
- private void doErrorNotification(int id) {
- final Notification notification = new Notification();
- notification.icon = android.R.drawable.stat_sys_download_done;
- final String title = mContext.getString(R.string.reading_vcard_failed_title);
- final PendingIntent intent =
- PendingIntent.getActivity(mContext, 0, new Intent(), 0);
- notification.setLatestEventInfo(mContext, title, "", intent);
- mNotificationManager.notify(id, notification);
- }
-
- private void doFinishNotification(int id, Uri uri) {
- final Notification notification = new Notification();
- notification.icon = android.R.drawable.stat_sys_download_done;
- final String title = mContext.getString(R.string.reading_vcard_finished_title);
-
- final Intent intent;
- final long rawContactId = ContentUris.parseId(mCreatedUris.get(0));
- final Uri contactUri = RawContacts.getContactLookupUri(
- getContentResolver(), ContentUris.withAppendedId(
- RawContacts.CONTENT_URI, rawContactId));
- intent = new Intent(Intent.ACTION_VIEW, contactUri);
-
- final PendingIntent pendingIntent =
- PendingIntent.getActivity(mContext, 0, intent, 0);
- notification.setLatestEventInfo(mContext, title, "", pendingIntent);
- mNotificationManager.notify(id, notification);
- }
-
- // We need synchronized since we need to handle mCanceled and mVCardParser at once.
- public synchronized void cancel() {
- mCanceled = true;
- if (mVCardParser != null) {
- mVCardParser.cancel();
- }
- }
-
- public void onCancel(DialogInterface dialog) {
- cancel();
- }
- }
-
- private static class PendingInput {
- public final Account account;
- public final Uri uri;
- public final int estimatedType;
- public final String estimatedCharset;
- public final boolean useV30;
- public final int entryCount;
- public final int id;
-
- public PendingInput(Account account,
- Uri uri, int estimatedType, String estimatedCharset,
- boolean useV30, int entryCount,
- int id) {
- this.account = account;
- this.uri = uri;
- this.estimatedType = estimatedType;
- this.estimatedCharset = estimatedCharset;
- this.useV30 = useV30;
- this.entryCount = entryCount;
- this.id = id;
- }
- }
-
- // The two classes bellow must be called inside the synchronized block, using this
- // Activity as a Context.
- private boolean mNowRunning;
- private final Queue<PendingInput> mPendingInputs = new LinkedList<PendingInput>();
-
- private NotificationManager mNotificationManager;
- private Thread mThread;
- private int mTotalCount;
- private int mCurrentCount;
-
- private Uri[] tryGetUris(Intent intent) {
- final String[] uriStrings =
- intent.getStringArrayExtra(ImportVCardActivity.VCARD_URI_ARRAY);
- if (uriStrings == null || uriStrings.length == 0) {
- Log.e(LOG_TAG, "Given uri array is empty");
- return null;
- }
-
- final int length = uriStrings.length;
- final Uri[] uris = new Uri[length];
- for (int i = 0; i < length; i++) {
- uris[i] = Uri.parse(uriStrings[i]);
- }
-
- return uris;
- }
-
- private Account tryGetAccount(Intent intent) {
- if (intent == null) {
- Log.w(LOG_TAG, "Intent is null");
- return null;
- }
-
- final String accountName = intent.getStringExtra("account_name");
- final String accountType = intent.getStringExtra("account_type");
- if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
- return new Account(accountName, accountType);
- } else {
- Log.w(LOG_TAG, "Account is not set.");
- return null;
- }
- }
+ private RequestHandler mRequestHandler;
+ private Messenger mMessenger = new Messenger(new ImportRequestHandler());
+ private Thread mThread = null;
@Override
- public int onStartCommand(Intent intent, int flags, int startId) {
- if (mNotificationManager == null) {
- mNotificationManager =
- (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
- }
-
- // TODO: use this.
- final int[] estimatedTypeArray =
- intent.getIntArrayExtra(ImportVCardActivity.ESTIMATED_VCARD_TYPE_ARRAY);
- final String[] estimatedCharsetArray =
- intent.getStringArrayExtra(ImportVCardActivity.ESTIMATED_CHARSET_ARRAY);
- final boolean[] useV30Array =
- intent.getBooleanArrayExtra(ImportVCardActivity.USE_V30_ARRAY);
- final int[] entryCountArray =
- intent.getIntArrayExtra(ImportVCardActivity.ENTRY_COUNT_ARRAY);
-
- final Account account = tryGetAccount(intent);
- final Uri[] uris = tryGetUris(intent);
- if (uris == null) {
- Log.e(LOG_TAG, "Uris are null.");
- Toast.makeText(this, getString(R.string.reading_vcard_failed_title),
- Toast.LENGTH_LONG).show();
- stopSelf();
- return START_NOT_STICKY;
- }
-
- int length = uris.length;
- if (estimatedTypeArray.length < length) {
- Log.w(LOG_TAG, String.format("estimatedTypeArray.length < length (%d, %d)",
- estimatedTypeArray.length, length));
- length = estimatedTypeArray.length;
- }
- if (useV30Array.length < length) {
- Log.w(LOG_TAG, String.format("useV30Array.length < length (%d, %d)",
- useV30Array.length, length));
- length = useV30Array.length;
- }
- if (entryCountArray.length < length) {
- Log.w(LOG_TAG, String.format("entryCountArray.length < length (%d, %d)",
- entryCountArray.length, length));
- length = entryCountArray.length;
- }
-
- synchronized (this) {
- for (int i = 0; i < length; i++) {
- mPendingInputs.add(new PendingInput(account,
- uris[i], estimatedTypeArray[i], estimatedCharsetArray[i],
- useV30Array[i], entryCountArray[i],
- startId));
- }
- if (!mNowRunning) {
- Toast.makeText(this, getString(R.string.vcard_importer_start_message),
- Toast.LENGTH_LONG).show();
- // Assume thread is alredy broken.
- // Even when it still exists, it never scan the PendingInput newly added above.
- mNowRunning = true;
- mThread = new VCardReadThread();
- mThread.start();
- } else {
- Toast.makeText(this, getString(R.string.vcard_importer_will_start_message),
- Toast.LENGTH_LONG).show();
- }
- }
-
- return START_NOT_STICKY;
+ public int onStartCommand(Intent intent, int flags, int id) {
+ return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
- return null;
+ return mMessenger.getBinder();
}
}