blob: 0e21235fed3299b7a7b06b8cc64df3101fd80bdd [file] [log] [blame]
/*
* Copyright (C) 2009 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.contacts.common.vcard;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.ProgressDialog;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.RelativeSizeSpan;
import android.util.Log;
import android.widget.Toast;
import com.android.contacts.common.R;
import com.android.contacts.common.model.AccountTypeManager;
import com.android.contacts.common.model.account.AccountWithDataSet;
import com.android.contacts.common.util.AccountSelectionUtil;
import com.android.vcard.VCardEntryCounter;
import com.android.vcard.VCardParser;
import com.android.vcard.VCardParser_V21;
import com.android.vcard.VCardParser_V30;
import com.android.vcard.VCardSourceDetector;
import com.android.vcard.exception.VCardException;
import com.android.vcard.exception.VCardNestedException;
import com.android.vcard.exception.VCardVersionException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Vector;
/**
* The class letting users to import vCard. This includes the UI part for letting them select
* an Account and posssibly a file if there's no Uri is given from its caller Activity.
*
* Note that this Activity assumes that the instance is a "one-shot Activity", which will be
* finished (with the method {@link Activity#finish()}) after the import and never reuse
* any Dialog in the instance. So this code is careless about the management around managed
* dialogs stuffs (like how onCreateDialog() is used).
*/
public class ImportVCardActivity extends Activity {
private static final String LOG_TAG = "VCardImport";
private static final int SELECT_ACCOUNT = 0;
/* package */ static final String VCARD_URI_ARRAY = "vcard_uri";
/* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type";
/* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset";
/* package */ static final String VCARD_VERSION_ARRAY = "vcard_version";
/* package */ static final String ENTRY_COUNT_ARRAY = "entry_count";
/* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
/* package */ final static int VCARD_VERSION_V21 = 1;
/* package */ final static int VCARD_VERSION_V30 = 2;
private static final String SECURE_DIRECTORY_NAME = ".android_secure";
/**
* Notification id used when error happened before sending an import request to VCardServer.
*/
private static final int FAILURE_NOTIFICATION_ID = 1;
final static String CACHED_URIS = "cached_uris";
private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
private AccountWithDataSet mAccount;
private ProgressDialog mProgressDialogForScanVCard;
private ProgressDialog mProgressDialogForCachingVCard;
private List<VCardFile> mAllVCardFileList;
private VCardScanThread mVCardScanThread;
private VCardCacheThread mVCardCacheThread;
private ImportRequestConnection mConnection;
/* package */ VCardImportExportListener mListener;
private String mErrorMessage;
private Handler mHandler = new Handler();
private static class VCardFile {
private final String mName;
private final String mCanonicalPath;
private final long mLastModified;
public VCardFile(String name, String canonicalPath, long lastModified) {
mName = name;
mCanonicalPath = canonicalPath;
mLastModified = lastModified;
}
public String getName() {
return mName;
}
public String getCanonicalPath() {
return mCanonicalPath;
}
public long getLastModified() {
return mLastModified;
}
}
// Runs on the UI thread.
private class DialogDisplayer implements Runnable {
private final int mResId;
public DialogDisplayer(int resId) {
mResId = resId;
}
public DialogDisplayer(String errorMessage) {
mResId = R.id.dialog_error_with_message;
mErrorMessage = errorMessage;
}
@Override
public void run() {
if (!isFinishing()) {
showDialog(mResId);
}
}
}
private class CancelListener
implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
@Override
public void onClick(DialogInterface dialog, int which) {
finish();
}
@Override
public void onCancel(DialogInterface dialog) {
finish();
}
}
private CancelListener mCancelListener = new CancelListener();
private class ImportRequestConnection implements ServiceConnection {
private VCardService mService;
public void sendImportRequest(final List<ImportRequest> requests) {
Log.i(LOG_TAG, "Send an import request");
mService.handleImportRequest(requests, mListener);
}
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
mService = ((VCardService.MyBinder) binder).getService();
Log.i(LOG_TAG,
String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
Arrays.toString(mVCardCacheThread.getSourceUris())));
mVCardCacheThread.start();
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.i(LOG_TAG, "Disconnected from VCardService");
}
}
/**
* Caches given vCard files into a local directory, and sends actual import request to
* {@link VCardService}.
*
* We need to cache given files into local storage. One of reasons is that some data (as Uri)
* may have special permissions. Callers may allow only this Activity to access that content,
* not what this Activity launched (like {@link VCardService}).
*/
private class VCardCacheThread extends Thread
implements DialogInterface.OnCancelListener {
private boolean mCanceled;
private PowerManager.WakeLock mWakeLock;
private VCardParser mVCardParser;
private final Uri[] mSourceUris; // Given from a caller.
private final byte[] mSource;
private final String mDisplayName;
public VCardCacheThread(final Uri[] sourceUris) {
mSourceUris = sourceUris;
mSource = null;
final Context context = ImportVCardActivity.this;
final PowerManager powerManager =
(PowerManager)context.getSystemService(Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_DIM_WAKE_LOCK |
PowerManager.ON_AFTER_RELEASE, LOG_TAG);
mDisplayName = null;
}
@Override
public void finalize() {
if (mWakeLock != null && mWakeLock.isHeld()) {
Log.w(LOG_TAG, "WakeLock is being held.");
mWakeLock.release();
}
}
@Override
public void run() {
Log.i(LOG_TAG, "vCard cache thread starts running.");
if (mConnection == null) {
throw new NullPointerException("vCard cache thread must be launched "
+ "after a service connection is established");
}
mWakeLock.acquire();
try {
if (mCanceled == true) {
Log.i(LOG_TAG, "vCard cache operation is canceled.");
return;
}
final Context context = ImportVCardActivity.this;
// Uris given from caller applications may not be opened twice: consider when
// it is not from local storage (e.g. "file:///...") but from some special
// provider (e.g. "content://...").
// Thus we have to once copy the content of Uri into local storage, and read
// it after it.
//
// We may be able to read content of each vCard file during copying them
// to local storage, but currently vCard code does not allow us to do so.
int cache_index = 0;
ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
if (mSource != null) {
try {
requests.add(constructImportRequest(mSource, null, mDisplayName));
} catch (VCardException e) {
Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
showFailureNotification(R.string.fail_reason_not_supported);
return;
}
} else {
final ContentResolver resolver =
ImportVCardActivity.this.getContentResolver();
for (Uri sourceUri : mSourceUris) {
String filename = null;
// Note: caches are removed by VCardService.
while (true) {
filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
final File file = context.getFileStreamPath(filename);
if (!file.exists()) {
break;
} else {
if (cache_index == Integer.MAX_VALUE) {
throw new RuntimeException("Exceeded cache limit");
}
cache_index++;
}
}
final Uri localDataUri = copyTo(sourceUri, filename);
if (mCanceled) {
Log.i(LOG_TAG, "vCard cache operation is canceled.");
break;
}
if (localDataUri == null) {
Log.w(LOG_TAG, "destUri is null");
break;
}
String displayName = null;
Cursor cursor = null;
// Try to get a display name from the given Uri. If it fails, we just
// pick up the last part of the Uri.
try {
cursor = resolver.query(sourceUri,
new String[] { OpenableColumns.DISPLAY_NAME },
null, null, null);
if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
if (cursor.getCount() > 1) {
Log.w(LOG_TAG, "Unexpected multiple rows: "
+ cursor.getCount());
}
int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (index >= 0) {
displayName = cursor.getString(index);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (TextUtils.isEmpty(displayName)){
displayName = sourceUri.getLastPathSegment();
}
final ImportRequest request;
try {
request = constructImportRequest(null, localDataUri, displayName);
} catch (VCardException e) {
Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
showFailureNotification(R.string.fail_reason_not_supported);
return;
} catch (IOException e) {
Log.e(LOG_TAG, "Unexpected IOException", e);
showFailureNotification(R.string.fail_reason_io_error);
return;
}
if (mCanceled) {
Log.i(LOG_TAG, "vCard cache operation is canceled.");
return;
}
requests.add(request);
}
}
if (!requests.isEmpty()) {
mConnection.sendImportRequest(requests);
} else {
Log.w(LOG_TAG, "Empty import requests. Ignore it.");
}
} catch (OutOfMemoryError e) {
Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
System.gc();
runOnUiThread(new DialogDisplayer(
getString(R.string.fail_reason_low_memory_during_import)));
} catch (IOException e) {
Log.e(LOG_TAG, "IOException during caching vCard", e);
runOnUiThread(new DialogDisplayer(
getString(R.string.fail_reason_io_error)));
} finally {
Log.i(LOG_TAG, "Finished caching vCard.");
mWakeLock.release();
unbindService(mConnection);
mProgressDialogForCachingVCard.dismiss();
mProgressDialogForCachingVCard = null;
finish();
}
}
/**
* Copy the content of sourceUri to the destination.
*/
private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
sourceUri, filename));
final Context context = ImportVCardActivity.this;
final ContentResolver resolver = context.getContentResolver();
ReadableByteChannel inputChannel = null;
WritableByteChannel outputChannel = null;
Uri destUri = null;
try {
inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
while (inputChannel.read(buffer) != -1) {
if (mCanceled) {
Log.d(LOG_TAG, "Canceled during caching " + sourceUri);
return null;
}
buffer.flip();
outputChannel.write(buffer);
buffer.compact();
}
buffer.flip();
while (buffer.hasRemaining()) {
outputChannel.write(buffer);
}
} finally {
if (inputChannel != null) {
try {
inputChannel.close();
} catch (IOException e) {
Log.w(LOG_TAG, "Failed to close inputChannel.");
}
}
if (outputChannel != null) {
try {
outputChannel.close();
} catch(IOException e) {
Log.w(LOG_TAG, "Failed to close outputChannel");
}
}
}
return destUri;
}
/**
* Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
* its content.
*
* @arg localDataUri Uri actually used for the import. Should be stored in
* app local storage, as we cannot guarantee other types of Uris can be read
* multiple times. This variable populates {@link ImportRequest#uri}.
* @arg displayName Used for displaying information to the user. This variable populates
* {@link ImportRequest#displayName}.
*/
private ImportRequest constructImportRequest(final byte[] data,
final Uri localDataUri, final String displayName)
throws IOException, VCardException {
final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
VCardEntryCounter counter = null;
VCardSourceDetector detector = null;
int vcardVersion = VCARD_VERSION_V21;
try {
boolean shouldUseV30 = false;
InputStream is;
if (data != null) {
is = new ByteArrayInputStream(data);
} else {
is = resolver.openInputStream(localDataUri);
}
mVCardParser = new VCardParser_V21();
try {
counter = new VCardEntryCounter();
detector = new VCardSourceDetector();
mVCardParser.addInterpreter(counter);
mVCardParser.addInterpreter(detector);
mVCardParser.parse(is);
} catch (VCardVersionException e1) {
try {
is.close();
} catch (IOException e) {
}
shouldUseV30 = true;
if (data != null) {
is = new ByteArrayInputStream(data);
} else {
is = resolver.openInputStream(localDataUri);
}
mVCardParser = new VCardParser_V30();
try {
counter = new VCardEntryCounter();
detector = new VCardSourceDetector();
mVCardParser.addInterpreter(counter);
mVCardParser.addInterpreter(detector);
mVCardParser.parse(is);
} catch (VCardVersionException e2) {
throw new VCardException("vCard with unspported version.");
}
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
}
}
}
vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
} catch (VCardNestedException e) {
Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
// Go through without throwing the Exception, as we may be able to detect the
// version before it
}
return new ImportRequest(mAccount,
data, localDataUri, displayName,
detector.getEstimatedType(),
detector.getEstimatedCharset(),
vcardVersion, counter.getCount());
}
public Uri[] getSourceUris() {
return mSourceUris;
}
public void cancel() {
mCanceled = true;
if (mVCardParser != null) {
mVCardParser.cancel();
}
}
@Override
public void onCancel(DialogInterface dialog) {
Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
cancel();
}
}
private class ImportTypeSelectedListener implements
DialogInterface.OnClickListener {
public static final int IMPORT_ONE = 0;
public static final int IMPORT_MULTIPLE = 1;
public static final int IMPORT_ALL = 2;
public static final int IMPORT_TYPE_SIZE = 3;
private int mCurrentIndex;
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
switch (mCurrentIndex) {
case IMPORT_ALL:
importVCardFromSDCard(mAllVCardFileList);
break;
case IMPORT_MULTIPLE:
showDialog(R.id.dialog_select_multiple_vcard);
break;
default:
showDialog(R.id.dialog_select_one_vcard);
break;
}
} else if (which == DialogInterface.BUTTON_NEGATIVE) {
finish();
} else {
mCurrentIndex = which;
}
}
}
private class VCardSelectedListener implements
DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
private int mCurrentIndex;
private Set<Integer> mSelectedIndexSet;
public VCardSelectedListener(boolean multipleSelect) {
mCurrentIndex = 0;
if (multipleSelect) {
mSelectedIndexSet = new HashSet<Integer>();
}
}
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
if (mSelectedIndexSet != null) {
List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
final int size = mAllVCardFileList.size();
// We'd like to sort the files by its index, so we do not use Set iterator.
for (int i = 0; i < size; i++) {
if (mSelectedIndexSet.contains(i)) {
selectedVCardFileList.add(mAllVCardFileList.get(i));
}
}
importVCardFromSDCard(selectedVCardFileList);
} else {
importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
}
} else if (which == DialogInterface.BUTTON_NEGATIVE) {
finish();
} else {
// Some file is selected.
mCurrentIndex = which;
if (mSelectedIndexSet != null) {
if (mSelectedIndexSet.contains(which)) {
mSelectedIndexSet.remove(which);
} else {
mSelectedIndexSet.add(which);
}
}
}
}
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
mAllVCardFileList.get(which).getCanonicalPath()));
} else {
onClick(dialog, which);
}
}
}
/**
* Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
* a vCard file is shown. After the choice, VCardReadThread starts running.
*/
private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
private boolean mCanceled;
private boolean mGotIOException;
private File mRootDirectory;
// To avoid recursive link.
private Set<String> mCheckedPaths;
private PowerManager.WakeLock mWakeLock;
private class CanceledException extends Exception {
}
public VCardScanThread(File sdcardDirectory) {
mCanceled = false;
mGotIOException = false;
mRootDirectory = sdcardDirectory;
mCheckedPaths = new HashSet<String>();
PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
Context.POWER_SERVICE);
mWakeLock = powerManager.newWakeLock(
PowerManager.SCREEN_DIM_WAKE_LOCK |
PowerManager.ON_AFTER_RELEASE, LOG_TAG);
}
@Override
public void run() {
mAllVCardFileList = new Vector<VCardFile>();
try {
mWakeLock.acquire();
getVCardFileRecursively(mRootDirectory);
} catch (CanceledException e) {
mCanceled = true;
} catch (IOException e) {
mGotIOException = true;
} finally {
mWakeLock.release();
}
if (mCanceled) {
mAllVCardFileList = null;
}
mProgressDialogForScanVCard.dismiss();
mProgressDialogForScanVCard = null;
if (mGotIOException) {
runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
} else if (mCanceled) {
finish();
} else {
int size = mAllVCardFileList.size();
final Context context = ImportVCardActivity.this;
if (size == 0) {
runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
} else {
startVCardSelectAndImport();
}
}
}
private void getVCardFileRecursively(File directory)
throws CanceledException, IOException {
if (mCanceled) {
throw new CanceledException();
}
// e.g. secured directory may return null toward listFiles().
final File[] files = directory.listFiles();
if (files == null) {
final String currentDirectoryPath = directory.getCanonicalPath();
final String secureDirectoryPath =
mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME);
if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) {
Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
}
return;
}
for (File file : directory.listFiles()) {
if (mCanceled) {
throw new CanceledException();
}
String canonicalPath = file.getCanonicalPath();
if (mCheckedPaths.contains(canonicalPath)) {
continue;
}
mCheckedPaths.add(canonicalPath);
if (file.isDirectory()) {
getVCardFileRecursively(file);
} else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
file.canRead()){
String fileName = file.getName();
VCardFile vcardFile = new VCardFile(
fileName, canonicalPath, file.lastModified());
mAllVCardFileList.add(vcardFile);
}
}
}
public void onCancel(DialogInterface dialog) {
mCanceled = true;
}
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_NEGATIVE) {
mCanceled = true;
}
}
}
private void startVCardSelectAndImport() {
int size = mAllVCardFileList.size();
if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
size == 1) {
importVCardFromSDCard(mAllVCardFileList);
} else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type));
} else {
runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
}
}
private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
final int size = selectedVCardFileList.size();
String[] uriStrings = new String[size];
int i = 0;
for (VCardFile vcardFile : selectedVCardFileList) {
uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
i++;
}
importVCard(uriStrings);
}
private void importVCardFromSDCard(final VCardFile vcardFile) {
importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())});
}
private void importVCard(final Uri uri) {
importVCard(new Uri[] {uri});
}
private void importVCard(final String[] uriStrings) {
final int length = uriStrings.length;
final Uri[] uris = new Uri[length];
for (int i = 0; i < length; i++) {
uris[i] = Uri.parse(uriStrings[i]);
}
importVCard(uris);
}
private void importVCard(final Uri[] uris) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isFinishing()) {
mVCardCacheThread = new VCardCacheThread(uris);
mListener = new NotificationImportExportListener(ImportVCardActivity.this);
showDialog(R.id.dialog_cache_vcard);
}
}
});
}
private Dialog getSelectImportTypeDialog() {
final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener();
final AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle(R.string.select_vcard_title)
.setPositiveButton(android.R.string.ok, listener)
.setOnCancelListener(mCancelListener)
.setNegativeButton(android.R.string.cancel, mCancelListener);
final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE];
items[ImportTypeSelectedListener.IMPORT_ONE] =
getString(R.string.import_one_vcard_string);
items[ImportTypeSelectedListener.IMPORT_MULTIPLE] =
getString(R.string.import_multiple_vcard_string);
items[ImportTypeSelectedListener.IMPORT_ALL] =
getString(R.string.import_all_vcard_string);
builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener);
return builder.create();
}
private Dialog getVCardFileSelectDialog(boolean multipleSelect) {
final int size = mAllVCardFileList.size();
final VCardSelectedListener listener = new VCardSelectedListener(multipleSelect);
final AlertDialog.Builder builder =
new AlertDialog.Builder(this)
.setTitle(R.string.select_vcard_title)
.setPositiveButton(android.R.string.ok, listener)
.setOnCancelListener(mCancelListener)
.setNegativeButton(android.R.string.cancel, mCancelListener);
CharSequence[] items = new CharSequence[size];
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < size; i++) {
VCardFile vcardFile = mAllVCardFileList.get(i);
SpannableStringBuilder stringBuilder = new SpannableStringBuilder();
stringBuilder.append(vcardFile.getName());
stringBuilder.append('\n');
int indexToBeSpanned = stringBuilder.length();
// Smaller date text looks better, since each file name becomes easier to read.
// The value set to RelativeSizeSpan is arbitrary. You can change it to any other
// value (but the value bigger than 1.0f would not make nice appearance :)
stringBuilder.append(
"(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")");
stringBuilder.setSpan(
new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
items[i] = stringBuilder;
}
if (multipleSelect) {
builder.setMultiChoiceItems(items, (boolean[])null, listener);
} else {
builder.setSingleChoiceItems(items, 0, listener);
}
return builder.create();
}
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
String accountName = null;
String accountType = null;
String dataSet = null;
final Intent intent = getIntent();
if (intent != null) {
accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
} else {
Log.e(LOG_TAG, "intent does not exist");
}
if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
mAccount = new AccountWithDataSet(accountName, accountType, dataSet);
} else {
final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
if (accountList.size() == 0) {
mAccount = null;
} else if (accountList.size() == 1) {
mAccount = accountList.get(0);
} else {
startActivityForResult(new Intent(this, SelectAccountActivity.class),
SELECT_ACCOUNT);
return;
}
}
startImport();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == SELECT_ACCOUNT) {
if (resultCode == Activity.RESULT_OK) {
mAccount = new AccountWithDataSet(
intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
intent.getStringExtra(SelectAccountActivity.DATA_SET));
startImport();
} else {
if (resultCode != Activity.RESULT_CANCELED) {
Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
}
finish();
}
}
}
private void startImport() {
Intent intent = getIntent();
// Handle inbound files
Uri uri = intent.getData();
if (uri != null) {
Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
importVCard(uri);
} else {
Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
doScanExternalStorageAndImportVCard();
}
}
@Override
protected Dialog onCreateDialog(int resId, Bundle bundle) {
switch (resId) {
case R.string.import_from_sdcard: {
if (mAccountSelectionListener == null) {
throw new NullPointerException(
"mAccountSelectionListener must not be null.");
}
return AccountSelectionUtil.getSelectAccountDialog(this, resId,
mAccountSelectionListener, mCancelListener);
}
case R.id.dialog_searching_vcard: {
if (mProgressDialogForScanVCard == null) {
String message = getString(R.string.searching_vcard_message);
mProgressDialogForScanVCard =
ProgressDialog.show(this, "", message, true, false);
mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread);
mVCardScanThread.start();
}
return mProgressDialogForScanVCard;
}
case R.id.dialog_sdcard_not_found: {
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setMessage(R.string.no_sdcard_message)
.setOnCancelListener(mCancelListener)
.setPositiveButton(android.R.string.ok, mCancelListener);
return builder.create();
}
case R.id.dialog_vcard_not_found: {
final String message = getString(R.string.import_failure_no_vcard_file);
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setMessage(message)
.setOnCancelListener(mCancelListener)
.setPositiveButton(android.R.string.ok, mCancelListener);
return builder.create();
}
case R.id.dialog_select_import_type: {
return getSelectImportTypeDialog();
}
case R.id.dialog_select_multiple_vcard: {
return getVCardFileSelectDialog(true);
}
case R.id.dialog_select_one_vcard: {
return getVCardFileSelectDialog(false);
}
case R.id.dialog_cache_vcard: {
if (mProgressDialogForCachingVCard == null) {
final String title = getString(R.string.caching_vcard_title);
final String message = getString(R.string.caching_vcard_message);
mProgressDialogForCachingVCard = new ProgressDialog(this);
mProgressDialogForCachingVCard.setTitle(title);
mProgressDialogForCachingVCard.setMessage(message);
mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
startVCardService();
}
return mProgressDialogForCachingVCard;
}
case R.id.dialog_io_exception: {
String message = (getString(R.string.scanning_sdcard_failed_message,
getString(R.string.fail_reason_io_error)));
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setMessage(message)
.setOnCancelListener(mCancelListener)
.setPositiveButton(android.R.string.ok, mCancelListener);
return builder.create();
}
case R.id.dialog_error_with_message: {
String message = mErrorMessage;
if (TextUtils.isEmpty(message)) {
Log.e(LOG_TAG, "Error message is null while it must not.");
message = getString(R.string.fail_reason_unknown);
}
final AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle(getString(R.string.reading_vcard_failed_title))
.setIconAttribute(android.R.attr.alertDialogIcon)
.setMessage(message)
.setOnCancelListener(mCancelListener)
.setPositiveButton(android.R.string.ok, mCancelListener);
return builder.create();
}
}
return super.onCreateDialog(resId, bundle);
}
/* package */ void startVCardService() {
mConnection = new ImportRequestConnection();
Log.i(LOG_TAG, "Bind to VCardService.");
// We don't want the service finishes itself just after this connection.
Intent intent = new Intent(this, VCardService.class);
startService(intent);
bindService(new Intent(this, VCardService.class),
mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (mProgressDialogForCachingVCard != null) {
Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
showDialog(R.id.dialog_cache_vcard);
}
}
/**
* Scans vCard in external storage (typically SDCard) and tries to import it.
* - When there's no SDCard available, an error dialog is shown.
* - When multiple vCard files are available, asks a user to select one.
*/
private void doScanExternalStorageAndImportVCard() {
final File file = Environment.getSecondaryStorageDirectory();
if ((file == null)
|| (!file.exists() || !file.isDirectory() || !file.canRead())) {
showDialog(R.id.dialog_sdcard_not_found);
} else {
mVCardScanThread = new VCardScanThread(file);
showDialog(R.id.dialog_searching_vcard);
}
}
/* package */ void showFailureNotification(int reasonId) {
final NotificationManager notificationManager =
(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
final Notification notification =
NotificationImportExportListener.constructImportFailureNotification(
ImportVCardActivity.this,
getString(reasonId));
notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
FAILURE_NOTIFICATION_ID, notification);
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(ImportVCardActivity.this,
getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
}
});
}
}