blob: f36e09639d45470d704fbde6d4d20b66c0ef5463 [file] [log] [blame]
/*
* 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.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
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;
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.Arrays;
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";
private class ProgressNotifier implements VCardEntryHandler {
private final int mId;
public ProgressNotifier(int id) {
mId = id;
}
public void onStart() {
}
public void onEntryCreated(VCardEntry contactStruct) {
mCurrentCount++; // 1 origin.
if (contactStruct.isIgnorable()) {
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
// 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 = getString(R.string.reading_vcard_title);
final String text = getString(R.string.progress_notifier_message,
String.valueOf(mCurrentCount),
String.valueOf(mTotalCount),
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 =
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(context, 0,
new Intent(context, ContactsListActivity.class), 0);
mNotificationManager.notify(mId, notification);
}
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[] uris;
final int id;
final boolean needReview;
synchronized (mContext) {
if (mPendingInputs.size() == 0) {
mNowRunning = false;
break;
} else {
final PendingInput pendingInput = mPendingInputs.poll();
account = pendingInput.account;
uris = pendingInput.uris;
id = pendingInput.id;
}
}
runInternal(account, uris, id);
doFinishNotification(id, uris);
}
Log.i(LOG_TAG, "Successfully imported. Total: " + mTotalCount);
stopSelf();
}
private void runInternal(Account account, Uri[] uris, int id) {
int totalCount = 0;
final ArrayList<VCardSourceDetector> detectorList =
new ArrayList<VCardSourceDetector>();
// First scan all Uris with a default charset and try to understand an exact
// charset to be used to each Uri. Note that detector would return null when
// it does not know an appropriate charset, so stick to use the default
// at that time.
// TODO: notification for first scanning?
for (Uri uri : uris) {
if (mCanceled) {
return;
}
final VCardEntryCounter counter = new VCardEntryCounter();
final VCardSourceDetector detector = new VCardSourceDetector();
final VCardInterpreterCollection interpreterCollection =
new VCardInterpreterCollection(Arrays.asList(counter, detector));
if (!readOneVCard(uri, VCardConfig.VCARD_TYPE_UNKNOWN, null,
interpreterCollection)) {
mErrorUris.add(uri);
}
totalCount += counter.getCount();
detectorList.add(detector);
}
if (mErrorUris.size() > 0) {
final StringBuilder builder = new StringBuilder();
builder.append("Error happened on ");
for (Uri errorUri : mErrorUris) {
builder.append("\"");
builder.append(errorUri.toString());
builder.append("\"");
}
Log.e(LOG_TAG, builder.toString());
doErrorNotification(id);
return;
}
if (uris.length != detectorList.size()) {
Log.e(LOG_TAG,
"The number of Uris to be imported is different from that of " +
"charset to be used.");
doErrorNotification(id);
return;
}
// First scanning is over. Try to import each vCard, which causes side effects.
mTotalCount = totalCount;
mCurrentCount = 0;
for (int i = 0; i < uris.length; i++) {
if (mCanceled) {
Log.w(LOG_TAG, "Canceled during importing (with storing data in database)");
// TODO: implement cancel correctly.
return;
}
final Uri uri = uris[i];
final VCardSourceDetector detector = detectorList.get(i);
final int vcardType = detector.getEstimatedType();
final String charset = detector.getEstimatedCharset(); // May be null.
final VCardEntryConstructor constructor =
new VCardEntryConstructor(vcardType, account, charset);
final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver);
constructor.addEntryHandler(committer);
constructor.addEntryHandler(new ProgressNotifier(id));
if (!readOneVCard(uri, vcardType, charset, 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) {
mVCardParser = new VCardParser_V21(vcardType, charset);
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);
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[] uris) {
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 String text = ((uris.length == 1) ? uris[0].getPath() : "");
final PendingIntent pendingIntent =
PendingIntent.getActivity(mContext, 0, intent, 0);
notification.setLatestEventInfo(mContext, title, text, 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[] uris;
public final int id;
public PendingInput(Account account, Uri[] uris, int id) {
this.account = account;
this.uris = uris;
this.id = id;
}
}
// The two classes bellow must be called inside the synchronized block, using this 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;
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (mNotificationManager == null) {
mNotificationManager =
(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
}
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;
}
synchronized (this) {
mPendingInputs.add(new PendingInput(account, uris, 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;
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}