Merge "Recycling bitmaps in TransitionAnimationView."
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ddc1537..fdcc6a7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -758,12 +758,14 @@
is rarely needed in the actual vCard...). -->
<string name="fail_reason_vcard_not_supported_error">Failed to parse vCard though it seems in valid format, since the current implementation does not support it</string>
- <!-- The failed reason shown when the system could not find any vCard file
- (with extension ".vcf" in SDCard.) [CHAR LIMIT=NONE] -->
- <string name="fail_reason_no_vcard_file" product="nosdcard">No vCard file found in the USB storage</string>
- <!-- The failed reason shown when the system could not find any vCard file
- (with extension ".vcf" in SDCard.) -->
- <string name="fail_reason_no_vcard_file" product="default">No vCard file found on the SD card</string>
+ <!-- The failure message shown when the system could not find any vCard file.
+ (with extension ".vcf" in USB storage.)
+ [CHAR LIMIT=128] -->
+ <string name="import_failure_no_vcard_file" product="nosdcard">No vCard file found in the USB storage</string>
+ <!-- The failure message shown when the system could not find any vCard file.
+ (with extension ".vcf" in SDCard.)
+ [CHAR LIMIT=128] -->
+ <string name="import_failure_no_vcard_file" product="default">No vCard file found on the SD card</string>
<!-- Fail reason shown when vCard importer failed to look over meta information stored in vCard file(s). -->
<string name="fail_reason_failed_to_collect_vcard_meta_info">Failed to collect meta information of given vCard file(s).</string>
diff --git a/src/com/android/contacts/vcard/ExportProcessor.java b/src/com/android/contacts/vcard/ExportProcessor.java
index 028bcc7..c5f293b 100644
--- a/src/com/android/contacts/vcard/ExportProcessor.java
+++ b/src/com/android/contacts/vcard/ExportProcessor.java
@@ -41,6 +41,7 @@
*/
public class ExportProcessor extends ProcessorBase {
private static final String LOG_TAG = "VCardExport";
+ private static final boolean DEBUG = VCardService.DEBUG;
private final VCardService mService;
private final ContentResolver mResolver;
@@ -88,7 +89,7 @@
}
private void runInternal() {
- Log.i(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
+ if (DEBUG) Log.d(LOG_TAG, String.format("vCard export (id: %d) has started.", mJobId));
final ExportRequest request = mExportRequest;
VCardComposer composer = null;
boolean successful = false;
@@ -108,7 +109,6 @@
final String errorReason =
mService.getString(R.string.fail_reason_could_not_open_file,
uri, e.getMessage());
- Log.i(LOG_TAG, "failed to export (could not open output stream)");
doFinishNotification(errorReason, "");
return;
}
@@ -132,7 +132,6 @@
composer.addHandler(composer.new HandlerForOutputStream(outputStream));
if (!composer.init()) {
- Log.w(LOG_TAG, "vCard composer init failed");
final String errorReason = composer.getErrorReason();
Log.e(LOG_TAG, "initialization of vCard composer failed: " + errorReason);
final String translatedErrorReason =
@@ -219,6 +218,7 @@
}
private void doCancelNotification() {
+ if (DEBUG) Log.d(LOG_TAG, "send cancel notification");
final String description = mService.getString(R.string.exporting_vcard_canceled_title,
mExportRequest.destUri.getLastPathSegment());
final Notification notification =
@@ -227,15 +227,16 @@
}
private void doFinishNotification(final String title, final String description) {
+ if (DEBUG) Log.d(LOG_TAG, "send finish notification: " + title + ", " + description);
final Intent intent = new Intent(mService, ContactBrowserActivity.class);
final Notification notification =
- VCardService.constructFinishNotification(mService, description, intent);
+ VCardService.constructFinishNotification(mService, title, description, intent);
mNotificationManager.notify(mJobId, notification);
}
@Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
- Log.i(LOG_TAG, "received cancel request");
+ if (DEBUG) Log.d(LOG_TAG, "received cancel request");
if (mDone || mCanceled) {
return false;
}
@@ -252,4 +253,8 @@
public synchronized boolean isDone() {
return mDone;
}
+
+ public ExportRequest getRequest() {
+ return mExportRequest;
+ }
}
diff --git a/src/com/android/contacts/vcard/ExportVCardActivity.java b/src/com/android/contacts/vcard/ExportVCardActivity.java
index d53d5b2..9211aec 100644
--- a/src/com/android/contacts/vcard/ExportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ExportVCardActivity.java
@@ -15,6 +15,9 @@
*/
package com.android.contacts.vcard;
+import com.android.contacts.R;
+import com.android.vcard.VCardComposer;
+
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
@@ -23,9 +26,9 @@
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
-import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
@@ -33,35 +36,82 @@
import android.text.TextUtils;
import android.util.Log;
-import com.android.contacts.R;
-import com.android.vcard.VCardComposer;
-
import java.io.File;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.Queue;
-import java.util.Set;
/**
- * Class for exporting vCard.
+ * Shows a dialog confirming the export and asks actual vCard export to {@link VCardService}
*
- * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
- * finished (with the method {@link Activity#finish()}) after the export and never reuse
- * any Dialog in the instance. So this code is careless about the management around managed
- * dialogs stuffs (like how onCreateDialog() is used).
+ * This Activity first connects to VCardService and ask an available file name and shows it to
+ * a user. After the user's confirmation, it send export request with the file name, assuming the
+ * file name is not reserved yet.
*/
-public class ExportVCardActivity extends Activity {
+public class ExportVCardActivity extends Activity implements ServiceConnection,
+ DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
private static final String LOG_TAG = "VCardExport";
+ private static final boolean DEBUG = VCardService.DEBUG;
- // If true, VCardExporter is able to emits files longer than 8.3 format.
- private static final boolean ALLOW_LONG_FILE_NAME = false;
- private String mTargetDirectory;
- private String mFileNamePrefix;
- private String mFileNameSuffix;
- private int mFileIndexMinimum;
- private int mFileIndexMaximum;
- private String mFileNameExtension;
- private Set<String> mExtensionsToConsider;
+ /**
+ * Handler used when some Message has come from {@link VCardService}.
+ */
+ private class IncomingHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ if (DEBUG) Log.d(LOG_TAG, "IncomingHandler received message.");
+
+ if (msg.arg1 != 0) {
+ Log.i(LOG_TAG, "Message returned from vCard server contains error code.");
+ if (msg.obj != null) {
+ mErrorReason = (String)msg.obj;
+ }
+ showDialog(msg.arg1);
+ return;
+ }
+
+ switch (msg.what) {
+ case VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION:
+ if (msg.obj == null) {
+ Log.w(LOG_TAG, "Message returned from vCard server doesn't contain valid path");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ } else {
+ mTargetFileName = (String)msg.obj;
+ if (TextUtils.isEmpty(mTargetFileName)) {
+ Log.w(LOG_TAG, "Destination file name coming from vCard service is empty.");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ } else {
+ if (DEBUG) {
+ Log.d(LOG_TAG,
+ String.format("Target file name is set (%s). " +
+ "Show confirmation dialog", mTargetFileName));
+ }
+ showDialog(R.id.dialog_export_confirmation);
+ }
+ }
+ break;
+ default:
+ Log.w(LOG_TAG, "Unknown message type: " + msg.what);
+ super.handleMessage(msg);
+ }
+ }
+ }
+
+ /**
+ * True when this Activity is connected to {@link VCardService}.
+ *
+ * Should be touched inside synchronized block.
+ */
+ private boolean mConnected;
+
+ /**
+ * True when users need to do something and this Activity should not disconnect from
+ * VCardService. False when all necessary procedures are done (including sending export request)
+ * or there's some error occured.
+ */
+ private volatile boolean mProcessOngoing = true;
+
+ private Messenger mOutgoingMessenger;
+ private final Messenger mIncomingMessenger = new Messenger(new IncomingHandler());
// Used temporarily when asking users to confirm the file name
private String mTargetFileName;
@@ -69,190 +119,128 @@
// String for storing error reason temporarily.
private String mErrorReason;
-
- private class CustomConnection implements ServiceConnection {
- private Messenger mMessenger;
- private Queue<ExportRequest> mPendingRequests = new LinkedList<ExportRequest>();
-
- public void doBindService() {
-
- }
-
- public synchronized void requestSend(final ExportRequest parameter) {
- if (mMessenger != null) {
- sendMessage(parameter);
- } else {
- mPendingRequests.add(parameter);
- }
- }
-
- private void sendMessage(final ExportRequest request) {
- try {
- mMessenger.send(Message.obtain(null,
- VCardService.MSG_EXPORT_REQUEST,
- request));
- } catch (RemoteException e) {
- Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
- runOnUiThread(new ErrorReasonDisplayer(
- getString(R.string.fail_reason_unknown)));
- }
- }
-
- public void onServiceConnected(ComponentName name, IBinder service) {
- synchronized (this) {
- mMessenger = new Messenger(service);
- // Send pending requests thrown from this Activity before an actual connection
- // is established.
- while (!mPendingRequests.isEmpty()) {
- final ExportRequest parameter = mPendingRequests.poll();
- if (parameter == null) {
- throw new NullPointerException();
- }
- sendMessage(parameter);
- }
-
- unbindService(this);
- finish();
- }
- }
-
- public void onServiceDisconnected(ComponentName name) {
- synchronized (this) {
- if (!mPendingRequests.isEmpty()) {
- Log.w(LOG_TAG, "Some request(s) are dropped.");
- }
- // Set to null so that we can detect inappropriate re-connection toward
- // the Service via NullPointerException;
- mPendingRequests = null;
- mMessenger = null;
- }
- }
- }
-
- private final CustomConnection mConnection = new CustomConnection();
-
- private class CancelListener
- implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
- public void onClick(DialogInterface dialog, int which) {
- finish();
- }
- public void onCancel(DialogInterface dialog) {
- finish();
- }
- }
-
- private CancelListener mCancelListener = new CancelListener();
-
- private class ErrorReasonDisplayer implements Runnable {
- private final int mResId;
- public ErrorReasonDisplayer(int resId) {
- mResId = resId;
- }
- public ErrorReasonDisplayer(String errorReason) {
- mResId = R.id.dialog_fail_to_export_with_reason;
- mErrorReason = errorReason;
- }
- public void run() {
- // Show the Dialog only when the parent Activity is still alive.
- if (!ExportVCardActivity.this.isFinishing()) {
- showDialog(mResId);
- }
- }
- }
-
private class ExportConfirmationListener implements DialogInterface.OnClickListener {
- private final Uri mDestUri;
+ private final Uri mDestinationUri;
- public ExportConfirmationListener(String fileName) {
- this(Uri.parse("file://" + fileName));
+ public ExportConfirmationListener(String path) {
+ this(Uri.parse("file://" + path));
}
public ExportConfirmationListener(Uri uri) {
- mDestUri = uri;
+ mDestinationUri = uri;
}
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
- // We don't want the service finishes itself just after this connection.
- startService(new Intent(ExportVCardActivity.this, VCardService.class));
- bindService(new Intent(ExportVCardActivity.this, VCardService.class),
- mConnection, Context.BIND_AUTO_CREATE);
-
- final ExportRequest request = new ExportRequest(mDestUri);
-
+ if (DEBUG) {
+ Log.d(LOG_TAG,
+ String.format("Try sending export request (uri: %s)", mDestinationUri));
+ }
+ final ExportRequest request = new ExportRequest(mDestinationUri);
// The connection object will call finish().
- mConnection.requestSend(request);
+ if (trySend(Message.obtain(null, VCardService.MSG_EXPORT_REQUEST, request))) {
+ Log.i(LOG_TAG, "Successfully sent export request. Finish itself");
+ unbindAndFinish();
+ }
}
}
}
- private String translateComposerError(String errorMessage) {
- Resources resources = getResources();
- if (VCardComposer.FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO.equals(errorMessage)) {
- return resources.getString(R.string.composer_failed_to_get_database_infomation);
- } else if (VCardComposer.FAILURE_REASON_NO_ENTRY.equals(errorMessage)) {
- return resources.getString(R.string.composer_has_no_exportable_contact);
- } else if (VCardComposer.FAILURE_REASON_NOT_INITIALIZED.equals(errorMessage)) {
- return resources.getString(R.string.composer_not_initialized);
- } else {
- return errorMessage;
- }
- }
-
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
- mTargetDirectory = getString(R.string.config_export_dir);
- mFileNamePrefix = getString(R.string.config_export_file_prefix);
- mFileNameSuffix = getString(R.string.config_export_file_suffix);
- mFileNameExtension = getString(R.string.config_export_file_extension);
-
- mExtensionsToConsider = new HashSet<String>();
- mExtensionsToConsider.add(mFileNameExtension);
-
- final String additionalExtensions =
- getString(R.string.config_export_extensions_to_consider);
- if (!TextUtils.isEmpty(additionalExtensions)) {
- for (String extension : additionalExtensions.split(",")) {
- String trimed = extension.trim();
- if (trimed.length() > 0) {
- mExtensionsToConsider.add(trimed);
- }
- }
+ // Check directory is available.
+ final File targetDirectory = new File(getString(R.string.config_export_dir));
+ if (!(targetDirectory.exists() &&
+ targetDirectory.isDirectory() &&
+ targetDirectory.canRead()) &&
+ !targetDirectory.mkdirs()) {
+ showDialog(R.id.dialog_sdcard_not_found);
+ return;
}
- final Resources resources = getResources();
- mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
- mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
+ if (startService(new Intent(this, VCardService.class)) == null) {
+ Log.e(LOG_TAG, "Failed to start vCard service");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ return;
+ }
- startExportVCardToSdCard();
+ if (!bindService(new Intent(this, VCardService.class), this, Context.BIND_AUTO_CREATE)) {
+ Log.e(LOG_TAG, "Failed to connect to vCard service.");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ }
+ // Continued to onServiceConnected()
+ }
+
+ @Override
+ public synchronized void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) Log.d(LOG_TAG, "connected to service, requesting a destination file name");
+ mConnected = true;
+ mOutgoingMessenger = new Messenger(service);
+ final Message message =
+ Message.obtain(null, VCardService.MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION);
+ message.replyTo = mIncomingMessenger;
+ trySend(message);
+ // Wait until MSG_SET_AVAILABLE_EXPORT_DESTINATION message is available.
+ }
+
+ // Use synchronized since we don't want to call unbindAndFinish() just after this call.
+ @Override
+ public synchronized void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Log.d(LOG_TAG, "onServiceDisconnected()");
+ mOutgoingMessenger = null;
+ mConnected = false;
+ if (mProcessOngoing) {
+ // Unexpected disconnect event.
+ Log.w(LOG_TAG, "Disconnected from service during the process ongoing.");
+ mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ }
}
@Override
protected Dialog onCreateDialog(int id, Bundle bundle) {
switch (id) {
case R.id.dialog_export_confirmation: {
- return getExportConfirmationDialog();
+ return new AlertDialog.Builder(this)
+ .setTitle(R.string.confirm_export_title)
+ .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
+ .setPositiveButton(android.R.string.ok,
+ new ExportConfirmationListener(mTargetFileName))
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
}
case R.string.fail_reason_too_many_vcard: {
+ mProcessOngoing = false;
return new AlertDialog.Builder(this)
- .setTitle(R.string.exporting_contact_failed_title)
- .setMessage(getString(R.string.exporting_contact_failed_message,
+ .setTitle(R.string.exporting_contact_failed_title)
+ .setMessage(getString(R.string.exporting_contact_failed_message,
getString(R.string.fail_reason_too_many_vcard)))
- .setPositiveButton(android.R.string.ok, mCancelListener)
- .create();
+ .setPositiveButton(android.R.string.ok, this)
+ .create();
}
case R.id.dialog_fail_to_export_with_reason: {
- return getErrorDialogWithReason();
+ mProcessOngoing = false;
+ return new AlertDialog.Builder(this)
+ .setTitle(R.string.exporting_contact_failed_title)
+ .setMessage(getString(R.string.exporting_contact_failed_message,
+ mErrorReason != null ? mErrorReason :
+ getString(R.string.fail_reason_unknown)))
+ .setPositiveButton(android.R.string.ok, this)
+ .setOnCancelListener(this)
+ .create();
}
case R.id.dialog_sdcard_not_found: {
- AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.no_sdcard_title)
- .setIcon(android.R.drawable.ic_dialog_alert)
- .setMessage(R.string.no_sdcard_message)
- .setPositiveButton(android.R.string.ok, mCancelListener);
- return builder.create();
+ mProcessOngoing = false;
+ return new AlertDialog.Builder(this)
+ .setTitle(R.string.no_sdcard_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.no_sdcard_message)
+ .setPositiveButton(android.R.string.ok, this).create();
}
}
return super.onCreateDialog(id, bundle);
@@ -261,7 +249,7 @@
@Override
protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
if (id == R.id.dialog_fail_to_export_with_reason) {
- ((AlertDialog)dialog).setMessage(getErrorReason());
+ ((AlertDialog)dialog).setMessage(mErrorReason);
} else if (id == R.id.dialog_export_confirmation) {
((AlertDialog)dialog).setMessage(
getString(R.string.confirm_export_message, mTargetFileName));
@@ -275,119 +263,47 @@
super.onStop();
if (!isFinishing()) {
- finish();
+ unbindAndFinish();
}
}
- /**
- * Tries to start exporting VCard. If there's no SDCard available,
- * an error dialog is shown.
- */
- public void startExportVCardToSdCard() {
- File targetDirectory = new File(mTargetDirectory);
-
- if (!(targetDirectory.exists() &&
- targetDirectory.isDirectory() &&
- targetDirectory.canRead()) &&
- !targetDirectory.mkdirs()) {
- showDialog(R.id.dialog_sdcard_not_found);
- } else {
- mTargetFileName = getAppropriateFileName(mTargetDirectory);
- if (TextUtils.isEmpty(mTargetFileName)) {
- mTargetFileName = null;
- // finish() is called via the error dialog. Do not call the method here.
- return;
- }
-
- showDialog(R.id.dialog_export_confirmation);
- }
- }
-
- /**
- * Tries to get an appropriate filename. Returns null if it fails.
- */
- private String getAppropriateFileName(final String destDirectory) {
- int fileNumberStringLength = 0;
- {
- // Calling Math.Log10() is costly.
- int tmp;
- for (fileNumberStringLength = 0, tmp = mFileIndexMaximum; tmp > 0;
- fileNumberStringLength++, tmp /= 10) {
- }
- }
- String bodyFormat = "%s%0" + fileNumberStringLength + "d%s";
-
- if (!ALLOW_LONG_FILE_NAME) {
- String possibleBody = String.format(bodyFormat,mFileNamePrefix, 1, mFileNameSuffix);
- if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
- Log.e(LOG_TAG, "This code does not allow any long file name.");
- mErrorReason = getString(R.string.fail_reason_too_long_filename,
- String.format("%s.%s", possibleBody, mFileNameExtension));
- showDialog(R.id.dialog_fail_to_export_with_reason);
- // finish() is called via the error dialog. Do not call the method here.
- return null;
- }
- }
-
- // Note that this logic assumes that the target directory is case insensitive.
- // As of 2009-07-16, it is true since the external storage is only sdcard, and
- // it is formated as FAT/VFAT.
- // TODO: fix this.
- for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
- boolean numberIsAvailable = true;
- // SD Association's specification seems to require this feature, though we cannot
- // have the specification since it is proprietary...
- String body = null;
- for (String possibleExtension : mExtensionsToConsider) {
- body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
- File file = new File(String.format("%s/%s.%s",
- destDirectory, body, possibleExtension));
- if (file.exists()) {
- numberIsAvailable = false;
- break;
- }
- }
- if (numberIsAvailable) {
- return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
- }
- }
- showDialog(R.string.fail_reason_too_many_vcard);
- return null;
- }
-
- public Dialog getExportConfirmationDialog() {
- if (TextUtils.isEmpty(mTargetFileName)) {
- Log.e(LOG_TAG, "Target file name is empty, which must not be!");
- // This situation is not acceptable (probably a bug!), but we don't have no reason to
- // show...
- mErrorReason = null;
- return getErrorDialogWithReason();
- }
-
- return new AlertDialog.Builder(this)
- .setTitle(R.string.confirm_export_title)
- .setMessage(getString(R.string.confirm_export_message, mTargetFileName))
- .setPositiveButton(android.R.string.ok,
- new ExportConfirmationListener(mTargetFileName))
- .setNegativeButton(android.R.string.cancel, mCancelListener)
- .setOnCancelListener(mCancelListener)
- .create();
- }
-
- public Dialog getErrorDialogWithReason() {
- if (mErrorReason == null) {
- Log.e(LOG_TAG, "Error reason must have been set.");
+ private boolean trySend(Message message) {
+ try {
+ mOutgoingMessenger.send(message);
+ return true;
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "RemoteException is thrown when trying to send request");
+ unbindService(this);
mErrorReason = getString(R.string.fail_reason_unknown);
+ showDialog(R.id.dialog_fail_to_export_with_reason);
+ return false;
}
- return new AlertDialog.Builder(this)
- .setTitle(R.string.exporting_contact_failed_title)
- .setMessage(getString(R.string.exporting_contact_failed_message, mErrorReason))
- .setPositiveButton(android.R.string.ok, mCancelListener)
- .setOnCancelListener(mCancelListener)
- .create();
}
- public String getErrorReason() {
- return mErrorReason;
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onClick() is called");
+ unbindAndFinish();
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (DEBUG) Log.d(LOG_TAG, "ExportVCardActivity#onCancel() is called");
+ mProcessOngoing = false;
+ unbindAndFinish();
+ }
+
+ @Override
+ public void unbindService(ServiceConnection conn) {
+ mProcessOngoing = false;
+ super.unbindService(conn);
+ }
+
+ private synchronized void unbindAndFinish() {
+ if (mConnected) {
+ unbindService(this);
+ mConnected = false;
+ }
+ finish();
}
}
\ No newline at end of file
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
index 5b5ae79..1b70025 100644
--- a/src/com/android/contacts/vcard/ImportProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -30,7 +30,6 @@
import android.accounts.Account;
import android.app.Notification;
import android.app.NotificationManager;
-import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
@@ -38,7 +37,6 @@
import android.net.Uri;
import android.provider.ContactsContract.RawContacts;
import android.util.Log;
-import android.widget.RemoteViews;
import java.io.IOException;
import java.io.InputStream;
@@ -51,6 +49,7 @@
*/
public class ImportProcessor extends ProcessorBase {
private static final String LOG_TAG = "VCardImport";
+ private static final boolean DEBUG = VCardService.DEBUG;
private final VCardService mService;
private final ContentResolver mResolver;
@@ -160,6 +159,7 @@
Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri);
List<Uri> uris = committer.getCreatedUris();
if (uris != null && uris.size() > 0) {
+ // TODO: construct intent showing a list of imported contact list.
doFinishNotification(uris.get(0));
} else {
// Not critical, but suspicious.
@@ -197,7 +197,8 @@
intent = null;
}
final Notification notification =
- VCardService.constructFinishNotification(mService, description, intent);
+ VCardService.constructFinishNotification(mService,
+ description, description, intent);
mNotificationManager.notify(mJobId, notification);
}
@@ -272,7 +273,7 @@
@Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
- Log.i(LOG_TAG, "ImportProcessor received cancel request");
+ if (DEBUG) Log.d(LOG_TAG, "ImportProcessor received cancel request");
if (mDone || mCanceled) {
return false;
}
diff --git a/src/com/android/contacts/vcard/ImportVCardActivity.java b/src/com/android/contacts/vcard/ImportVCardActivity.java
index 948b465..b375804 100644
--- a/src/com/android/contacts/vcard/ImportVCardActivity.java
+++ b/src/com/android/contacts/vcard/ImportVCardActivity.java
@@ -844,13 +844,12 @@
return builder.create();
}
case R.id.dialog_vcard_not_found: {
- String message = (getString(R.string.scanning_sdcard_failed_message,
- getString(R.string.fail_reason_no_vcard_file)));
+ final String message = getString(R.string.import_failure_no_vcard_file);
AlertDialog.Builder builder = new AlertDialog.Builder(this)
- .setTitle(R.string.scanning_sdcard_failed_title)
- .setMessage(message)
- .setOnCancelListener(mCancelListener)
- .setPositiveButton(android.R.string.ok, mCancelListener);
+ .setTitle(R.string.scanning_sdcard_failed_title)
+ .setMessage(message)
+ .setOnCancelListener(mCancelListener)
+ .setPositiveButton(android.R.string.ok, mCancelListener);
return builder.create();
}
case R.id.dialog_select_import_type: {
diff --git a/src/com/android/contacts/vcard/VCardService.java b/src/com/android/contacts/vcard/VCardService.java
index cad9fc6..7639c18 100644
--- a/src/com/android/contacts/vcard/VCardService.java
+++ b/src/com/android/contacts/vcard/VCardService.java
@@ -23,17 +23,23 @@
import android.app.Service;
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
+import android.os.RemoteException;
+import android.text.TextUtils;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
+import java.io.File;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
@@ -49,10 +55,13 @@
// works fine enough. Investigate the feasibility.
public class VCardService extends Service {
private final static String LOG_TAG = "VCardService";
+ /* package */ final static boolean DEBUG = true;
/* package */ static final int MSG_IMPORT_REQUEST = 1;
/* package */ static final int MSG_EXPORT_REQUEST = 2;
/* package */ static final int MSG_CANCEL_REQUEST = 3;
+ /* package */ static final int MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION = 4;
+ /* package */ static final int MSG_SET_AVAILABLE_EXPORT_DESTINATION = 5;
/**
* Specifies the type of operation. Used when constructing a {@link Notification}, canceling
@@ -79,6 +88,10 @@
handleCancelRequest((CancelRequest)msg.obj);
break;
}
+ case MSG_REQUEST_AVAILABLE_EXPORT_DESTINATION: {
+ handleRequestAvailableExportDestination(msg);
+ break;
+ }
// TODO: add cancel capability for export..
default: {
Log.w(LOG_TAG, "Received unknown request, ignoring it.");
@@ -101,10 +114,53 @@
private final Map<Integer, ProcessorBase> mRunningJobMap =
new HashMap<Integer, ProcessorBase>();
+ /* ** vCard exporter params ** */
+ // If true, VCardExporter is able to emits files longer than 8.3 format.
+ private static final boolean ALLOW_LONG_FILE_NAME = false;
+ private String mTargetDirectory;
+ private String mFileNamePrefix;
+ private String mFileNameSuffix;
+ private int mFileIndexMinimum;
+ private int mFileIndexMaximum;
+ private String mFileNameExtension;
+ private Set<String> mExtensionsToConsider;
+ private String mErrorReason;
+
+ // File names currently reserved by some export job.
+ private final Set<String> mReservedDestination = new HashSet<String>();
+ /* ** end of vCard exporter params ** */
+
@Override
public void onCreate() {
super.onCreate();
+ if (DEBUG) Log.d(LOG_TAG, "vCard Service is being created.");
mNotificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+ initExporterParams();
+ }
+
+ private void initExporterParams() {
+ mTargetDirectory = getString(R.string.config_export_dir);
+ mFileNamePrefix = getString(R.string.config_export_file_prefix);
+ mFileNameSuffix = getString(R.string.config_export_file_suffix);
+ mFileNameExtension = getString(R.string.config_export_file_extension);
+
+ mExtensionsToConsider = new HashSet<String>();
+ mExtensionsToConsider.add(mFileNameExtension);
+
+ final String additionalExtensions =
+ getString(R.string.config_export_extensions_to_consider);
+ if (!TextUtils.isEmpty(additionalExtensions)) {
+ for (String extension : additionalExtensions.split(",")) {
+ String trimed = extension.trim();
+ if (trimed.length() > 0) {
+ mExtensionsToConsider.add(trimed);
+ }
+ }
+ }
+
+ final Resources resources = getResources();
+ mFileIndexMinimum = resources.getInteger(R.integer.config_export_file_min_index);
+ mFileIndexMaximum = resources.getInteger(R.integer.config_export_file_max_index);
}
@Override
@@ -119,13 +175,18 @@
@Override
public void onDestroy() {
- Log.i(LOG_TAG, "VCardService is being destroyed.");
+ if (DEBUG) Log.d(LOG_TAG, "VCardService is being destroyed.");
cancelAllRequestsAndShutdown();
clearCache();
super.onDestroy();
}
private synchronized void handleImportRequest(ImportRequest request) {
+ if (DEBUG) {
+ Log.d(LOG_TAG,
+ String.format("received import request (uri: %s, originalUri: %s)",
+ request.uri, request.originalUri));
+ }
if (tryExecute(new ImportProcessor(this, request, mCurrentJobId))) {
final String displayName = request.originalUri.getLastPathSegment();
final String message = getString(R.string.vcard_import_will_start_message,
@@ -153,6 +214,18 @@
final String displayName = request.destUri.getLastPathSegment();
final String message = getString(R.string.vcard_export_will_start_message,
displayName);
+
+ final String path = request.destUri.getEncodedPath();
+ if (DEBUG) Log.d(LOG_TAG, "Reserve the path " + path);
+ if (!mReservedDestination.add(path)) {
+ Log.w(LOG_TAG,
+ String.format("The path %s is already reserved. Reject export request",
+ path));
+ Toast.makeText(this, getString(R.string.vcard_export_request_rejected_message),
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
final Notification notification =
constructProgressNotification(this, TYPE_EXPORT, message, message,
@@ -180,9 +253,9 @@
}
}
- private void handleCancelRequest(CancelRequest request) {
+ private synchronized void handleCancelRequest(CancelRequest request) {
final int jobId = request.jobId;
- Log.i(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
+ if (DEBUG) Log.d(LOG_TAG, String.format("Received cancel request. (id: %d)", jobId));
final ProcessorBase processor = mRunningJobMap.remove(jobId);
if (processor != null) {
@@ -192,12 +265,41 @@
getString(R.string.exporting_vcard_canceled_title, request.displayName);
final Notification notification = constructCancelNotification(this, description);
mNotificationManager.notify(jobId, notification);
+ if (processor.getType() == TYPE_EXPORT) {
+ final String path =
+ ((ExportProcessor)processor).getRequest().destUri.getEncodedPath();
+ Log.i(LOG_TAG,
+ String.format("Cancel reservation for the path %s if appropriate", path));
+ if (!mReservedDestination.remove(path)) {
+ Log.w(LOG_TAG, "Not reserved.");
+ }
+ }
} else {
Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
}
stopServiceWhenNoJob();
}
+ private synchronized void handleRequestAvailableExportDestination(Message msg) {
+ if (DEBUG) Log.d(LOG_TAG, "Received available export destination request.");
+ final Messenger messenger = msg.replyTo;
+ final String path = getAppropriateDestination(mTargetDirectory);
+ final Message message;
+ if (path != null) {
+ message = Message.obtain(null,
+ VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION, 0, 0, path);
+ } else {
+ message = Message.obtain(null,
+ VCardService.MSG_SET_AVAILABLE_EXPORT_DESTINATION,
+ R.id.dialog_fail_to_export_with_reason, 0, mErrorReason);
+ }
+ try {
+ messenger.send(message);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, "Failed to send reply for available export destination request.", e);
+ }
+ }
+
/**
* Checks job list and call {@link #stopSelf()} when there's no job now.
* A new job cannot be submitted any more after this call.
@@ -223,8 +325,10 @@
/* package */ synchronized void handleFinishImportNotification(
int jobId, boolean successful) {
- Log.v(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
- + "Result: %b", jobId, (successful ? "success" : "failure")));
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("Received vCard import finish notification (id: %d). "
+ + "Result: %b", jobId, (successful ? "success" : "failure")));
+ }
if (mRunningJobMap.remove(jobId) == null) {
Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
}
@@ -233,11 +337,22 @@
/* package */ synchronized void handleFinishExportNotification(
int jobId, boolean successful) {
- Log.v(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
- + "Result: %b", jobId, (successful ? "success" : "failure")));
- if (mRunningJobMap.remove(jobId) == null) {
- Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("Received vCard export finish notification (id: %d). "
+ + "Result: %b", jobId, (successful ? "success" : "failure")));
}
+ final ProcessorBase job = mRunningJobMap.remove(jobId);
+ if (job == null) {
+ Log.w(LOG_TAG, String.format("Tried to remove unknown job (id: %d)", jobId));
+ } else if (!(job instanceof ExportProcessor)) {
+ Log.w(LOG_TAG,
+ String.format("Removed job (id: %s) isn't ExportProcessor", jobId));
+ } else {
+ final String path = ((ExportProcessor)job).getRequest().destUri.getEncodedPath();
+ if (DEBUG) Log.d(LOG_TAG, "Remove reserved path " + path);
+ mReservedDestination.remove(path);
+ }
+
stopServiceWhenNoJob();
}
@@ -339,12 +454,13 @@
*/
/* package */ static Notification constructCancelNotification(
Context context, String description) {
- final Notification notification = new Notification();
- notification.flags |= Notification.FLAG_AUTO_CANCEL;
- notification.icon = android.R.drawable.stat_notify_error;
- notification.setLatestEventInfo(context, description, description,
- PendingIntent.getActivity(context, 0, null, 0));
- return notification;
+ return new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setSmallIcon(android.R.drawable.stat_notify_error)
+ .setContentTitle(description)
+ .setContentText(description)
+ .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(), 0))
+ .getNotification();
}
/**
@@ -355,12 +471,94 @@
* @param intent Intent to be launched when the Notification is clicked. Can be null.
*/
/* package */ static Notification constructFinishNotification(
- Context context, String description, Intent intent) {
- final Notification notification = new Notification();
- notification.flags |= Notification.FLAG_AUTO_CANCEL;
- notification.icon = android.R.drawable.stat_sys_download_done;
- notification.setLatestEventInfo(context, description, description,
- PendingIntent.getActivity(context, 0, intent, 0));
- return notification;
+ Context context, String title, String description, Intent intent) {
+ return new Notification.Builder(context)
+ .setAutoCancel(true)
+ .setSmallIcon(android.R.drawable.stat_sys_download_done)
+ .setContentTitle(title)
+ .setContentText(description)
+ .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0))
+ .getNotification();
+ }
+
+ /**
+ * Returns an appropriate file name for vCard export. Returns null when impossible.
+ *
+ * @return destination path for a vCard file to be exported. null on error and mErrorReason
+ * is correctly set.
+ */
+ private String getAppropriateDestination(final String destDirectory) {
+ /*
+ * Here, file names have 5 parts: directory, prefix, index, suffix, and extension.
+ * e.g. "/mnt/sdcard/prfx00001sfx.vcf" -> "/mnt/sdcard", "prfx", "00001", "sfx", and ".vcf"
+ * (In default, prefix and suffix is empty, so usually the destination would be
+ * /mnt/sdcard/00001.vcf.)
+ *
+ * This method increments "index" part from 1 to maximum, and checks whether any file name
+ * following naming rule is available. If there's no file named /mnt/sdcard/00001.vcf, the
+ * name will be returned to a caller. If there are 00001.vcf 00002.vcf, 00003.vcf is
+ * returned.
+ *
+ * There may not be any appropriate file name. If there are 99999 vCard files in the
+ * storage, for example, there's no appropriate name, so this method returns
+ * null.
+ */
+
+ // Count the number of digits of mFileIndexMaximum
+ // e.g. When mFileIndexMaximum is 99999, fileIndexDigit becomes 5, as we will count the
+ int fileIndexDigit = 0;
+ {
+ // Calling Math.Log10() is costly.
+ int tmp;
+ for (fileIndexDigit = 0, tmp = mFileIndexMaximum; tmp > 0;
+ fileIndexDigit++, tmp /= 10) {
+ }
+ }
+
+ // %s05d%s (e.g. "p00001s")
+ final String bodyFormat = "%s%0" + fileIndexDigit + "d%s";
+
+ if (!ALLOW_LONG_FILE_NAME) {
+ final String possibleBody =
+ String.format(bodyFormat, mFileNamePrefix, 1, mFileNameSuffix);
+ if (possibleBody.length() > 8 || mFileNameExtension.length() > 3) {
+ Log.e(LOG_TAG, "This code does not allow any long file name.");
+ mErrorReason = getString(R.string.fail_reason_too_long_filename,
+ String.format("%s.%s", possibleBody, mFileNameExtension));
+ Log.w(LOG_TAG, "File name becomes too long.");
+ return null;
+ }
+ }
+
+ for (int i = mFileIndexMinimum; i <= mFileIndexMaximum; i++) {
+ boolean numberIsAvailable = true;
+ String body = null;
+ for (String possibleExtension : mExtensionsToConsider) {
+ body = String.format(bodyFormat, mFileNamePrefix, i, mFileNameSuffix);
+ final String path =
+ String.format("%s/%s.%s", destDirectory, body, possibleExtension);
+ synchronized (this) {
+ if (mReservedDestination.contains(path)) {
+ if (DEBUG) {
+ Log.d(LOG_TAG, String.format("The path %s is reserved.", path));
+ }
+ numberIsAvailable = false;
+ break;
+ }
+ }
+ final File file = new File(path);
+ if (file.exists()) {
+ numberIsAvailable = false;
+ break;
+ }
+ }
+ if (numberIsAvailable) {
+ return String.format("%s/%s.%s", destDirectory, body, mFileNameExtension);
+ }
+ }
+
+ Log.w(LOG_TAG, "Reached vCard number limit. Maybe there are too many vCard in the storage");
+ mErrorReason = getString(R.string.fail_reason_too_many_vcard);
+ return null;
}
}