Hand merge from cupcake_dcm from donut, part 4.
Make Contacts app use vCard importer.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 71a90eb..f515434 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -25,6 +25,7 @@
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.mail" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
<application
android:label="@string/contactsList"
diff --git a/res/values/config.xml b/res/values/config.xml
new file mode 100644
index 0000000..9747fd1
--- /dev/null
+++ b/res/values/config.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<resources>
+ <!-- Flag indicating whether Contacts app is allowed to import contacts from SDCard -->
+ <bool name="config_allow_import_from_sd_card">false</bool>
+ <!-- If true, all vcard files are imported from SDCard without asking a user.
+ If not, dialog shows to let the user to select whether all vcard files are imported or not.
+ If the user selects "not", then the application ask the user to select a file.-->
+ <bool name="config_import_all_vcard_from_sdcard_automatically">false</bool>
+ <!-- If true, vcard importer shows a dialog which asks the user whether the user wants
+ to import all vcard files in SDCard or select one vcard file. If false, the dialog is
+ skipped and the importer asks the user to choose one vcard file.
+ If config_import_all_vcard_from_sdcard_automatically is set true, this configuration
+ is ignored. -->
+ <bool name="config_allow_users_select_all_vcard_import">false</bool>
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3da39d9..c147479 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -582,4 +582,75 @@
-->
<string name="description_image_button_pound">pound</string>
+ <!-- Dialog title shown when SD Card does not exist -->
+ <string name="no_sdcard_title">No SD Card</string>
+
+ <!-- Dialog message shown when SDcard does not exist -->
+ <string name="no_sdcard_message">No SD Card detected</string>
+
+ <!-- Dialog title shown when searching VCard data from SD Card -->
+ <string name="searching_vcard_title">Searching for VCard</string>
+
+ <!-- Dialog title shown when asking a user whether import contact data from SIM or SD Card -->
+ <string name="select_import_type_title">Where would you like to import contacts from?</string>
+
+ <!-- Action string for selecting SIM for importing contacts -->
+ <string name="import_from_sim">SIM Card</string>
+
+ <!-- Action string for selecting SD Card for importing contacts -->
+ <string name="import_from_sdcard">SD Card</string>
+
+ <!-- "Import all VCard files" -->
+ <string name="import_all_vcard_string">Import all VCard files</string>
+
+ <!-- "Import one VCard file" -->
+ <string name="import_one_vcard_string">Import one VCard file</string>
+
+ <!-- Dialog message shown when searching VCard data from SD Card -->
+ <string name="searching_vcard_message">Searching for VCard data on VCard</string>
+
+ <!-- Dialog title shown when searching VCard data failed. -->
+ <string name="scanning_sdcard_failed_title">Scanning SD Card failed</string>
+
+ <!-- Dialog message shown when searching VCard data failed. -->
+ <string name="scanning_sdcard_failed_message">Scanning SD Card failed</string>
+
+ <!-- The failed reason: "I/O Error" -->
+ <string name="fail_reason_io_error">I/O Error</string>
+
+ <!-- The failed reason: "Failed to parse VCard data" -->
+ <string name="fail_reason_vcard_parse_error">Failed to parse VCard</string>
+
+ <!-- The failed reason: "There is no VCard file" -->
+ <string name="fail_reason_no_vcard_file">No VCard file found on SD Card</string>
+
+ <!-- The failed reason: "There is no valid VCard entry in the file(s)" -->
+ <string name="fail_reason_no_vcard_entry">No valid VCard entry found for your selection</string>
+
+ <!-- Dialog title shown when a user is asked to select VCard file -->
+ <string name="select_vcard_title">Select VCard file</string>
+
+ <!-- Dialog message shown when a user is asked to choose VCard file -->
+ <string name="select_vcard_message">Please select a VCard file to import</string>
+
+ <!-- Dialog title shown when reading VCard data -->
+ <string name="reading_vcard_title">Reading VCard</string>
+
+ <!-- Dialog message shown when reading a VCard file -->
+ <string name="reading_vcard_message">Reading VCard file(s)</string>
+
+ <!-- Dialog message shown when importing VCard data into local database -->
+ <string name="importing_vcard_message">Importing VCard data</string>
+
+ <!-- Dialog title shown when reading VCard data failed -->
+ <string name="reading_vcard_failed_title">Reading of VCard data has failed</string>
+
+ <!-- Dialog message shown when reading VCard data failed -->
+ <string name="reading_vcard_failed_message">VCard data could not be read\nReason for failure: \"<xliff:g id="fail_reason">%s</xliff:g>\"</string>
+
+ <!-- Message while reading one vCard file "(current number) of (total number) contacts" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
+ <string name="reading_vcard_contacts"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> contacts</string>
+
+ <!-- Message while reading multiple vCard files "(current number) of (total number) files" The order of "current number" and "total number" cannot be changed (like "total: (total number), current: (current number)")-->
+ <string name="reading_vcard_files"><xliff:g id="current_number">%s</xliff:g> of <xliff:g id="total_number">%s</xliff:g> files</string>
</resources>
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 7dbc76c..271a6b3 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -16,8 +16,6 @@
package com.android.contacts;
-import static com.android.contacts.ShowOrCreateActivity.QUERY_KIND_EMAIL_OR_IM;
-
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListActivity;
@@ -34,11 +32,11 @@
import android.content.SharedPreferences;
import android.database.CharArrayBuffer;
import android.database.Cursor;
-import android.database.CursorWrapper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
import android.os.Parcelable;
import android.os.RemoteException;
import android.preference.PreferenceManager;
@@ -101,6 +99,7 @@
public static final int MENU_DIALER = 9;
public static final int MENU_NEW_CONTACT = 10;
public static final int MENU_DISPLAY_GROUP = 11;
+ public static final int MENU_IMPORT_CONTACTS = 12;
private static final int SUBACTIVITY_NEW_CONTACT = 1;
@@ -310,6 +309,34 @@
*/
private String mQueryData;
+ private Handler mHandler = new Handler();
+
+ private class ImportTypeSelectedListener implements DialogInterface.OnClickListener {
+ public static final int IMPORT_FROM_SIM = 0;
+ public static final int IMPORT_FROM_SDCARD = 1;
+
+ private int mIndex;
+
+ public ImportTypeSelectedListener() {
+ mIndex = IMPORT_FROM_SIM;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (mIndex == IMPORT_FROM_SIM) {
+ doImportFromSim();
+ } else {
+ VCardImporter importer = new VCardImporter(ContactsListActivity.this, mHandler);
+ importer.startImportVCardFromSdCard();
+ }
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+
+ } else {
+ mIndex = which;
+ }
+ }
+ }
+
private class DeleteClickListener implements DialogInterface.OnClickListener {
private Uri mUri;
@@ -746,13 +773,9 @@
.setIntent(syncIntent);
}
- // SIM import
- Intent importIntent = new Intent(Intent.ACTION_VIEW);
- importIntent.setType("vnd.android.cursor.item/sim-contact");
- importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
- menu.add(0, 0, 0, R.string.importFromSim)
- .setIcon(R.drawable.ic_menu_import_contact)
- .setIntent(importIntent);
+ // Contacts import (SIM/SDCard)
+ menu.add(0, MENU_IMPORT_CONTACTS, 0, R.string.importFromSim)
+ .setIcon(R.drawable.ic_menu_import_contact);
return super.onCreateOptionsMenu(menu);
}
@@ -816,10 +839,36 @@
case MENU_SEARCH:
startSearch(null, false, null, false);
return true;
+
+ case MENU_IMPORT_CONTACTS:
+ if (getResources().getBoolean(R.bool.config_allow_import_from_sd_card)) {
+ ImportTypeSelectedListener listener =
+ new ImportTypeSelectedListener();
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this)
+ .setTitle(R.string.select_import_type_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setNegativeButton(android.R.string.cancel, null);
+ dialogBuilder.setSingleChoiceItems(new String[] {
+ getString(R.string.import_from_sim),
+ getString(R.string.import_from_sdcard)},
+ ImportTypeSelectedListener.IMPORT_FROM_SIM, listener);
+ dialogBuilder.show();
+ } else {
+ doImportFromSim();
+ }
+ return true;
+
}
return false;
}
+ private void doImportFromSim() {
+ Intent importIntent = new Intent(Intent.ACTION_VIEW);
+ importIntent.setType("vnd.android.cursor.item/sim-contact");
+ importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
+ startActivity(importIntent);
+ }
+
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
@@ -1461,7 +1510,7 @@
}
private SectionIndexer getNewIndexer(Cursor cursor) {
- if (Locale.getDefault().equals(Locale.JAPAN)) {
+ if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) {
return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX);
} else {
return new AlphabetIndexer(cursor, NAME_COLUMN_INDEX, mAlphabet);
diff --git a/src/com/android/contacts/VCardImporter.java b/src/com/android/contacts/VCardImporter.java
new file mode 100644
index 0000000..e987e85
--- /dev/null
+++ b/src/com/android/contacts/VCardImporter.java
@@ -0,0 +1,647 @@
+/*
+ * 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;
+
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.syncml.pim.VBuilder;
+import android.syncml.pim.VBuilderCollection;
+import android.syncml.pim.VParser;
+import android.syncml.pim.vcard.VCardDataBuilder;
+import android.syncml.pim.vcard.VCardEntryCounter;
+import android.syncml.pim.vcard.VCardException;
+import android.syncml.pim.vcard.VCardNestedException;
+import android.syncml.pim.vcard.VCardParser_V21;
+import android.syncml.pim.vcard.VCardParser_V30;
+import android.syncml.pim.vcard.VCardSourceDetector;
+import android.syncml.pim.vcard.VCardVersionException;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.RelativeSizeSpan;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+
+class VCardFile {
+ private String mName;
+ private String mCanonicalPath;
+ private 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;
+ }
+}
+
+/**
+ * Class for importing vCard. Several user interaction will be required while reading
+ * (selecting a file, waiting a moment, etc.)
+ */
+public class VCardImporter {
+ private static final String LOG_TAG = "VCardImporter";
+ private static final boolean DO_PERFORMANCE_PROFILE = false;
+
+ private ProgressDialog mProgressDialog;
+ private Context mParentContext;
+ private Handler mParentHandler;
+ private boolean mLastNameComesBeforeFirstName;
+
+ private class ErrorDisplayer implements Runnable {
+ private String mErrorMessage;
+
+ public ErrorDisplayer(String errorMessage) {
+ mErrorMessage = errorMessage;
+ }
+
+ public void run() {
+ String message =
+ getString(R.string.reading_vcard_failed_message, mErrorMessage);
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(mParentContext)
+ .setTitle(getString(R.string.reading_vcard_failed_title))
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ }
+ }
+
+ private class VCardReadThread extends Thread
+ implements DialogInterface.OnCancelListener {
+ private String mCanonicalPath;
+ private List<VCardFile> mVCardFileList;
+ private ContentResolver mResolver;
+ private VCardParser_V21 mVCardParser;
+ private boolean mCanceled;
+ private PowerManager.WakeLock mWakeLock;
+
+ public VCardReadThread(String canonicalPath) {
+ mCanonicalPath = canonicalPath;
+ mVCardFileList = null;
+ init();
+ }
+
+ public VCardReadThread(List<VCardFile> vcardFileList) {
+ mCanonicalPath = null;
+ mVCardFileList = vcardFileList;
+ init();
+ }
+
+ private void init() {
+ mResolver = mParentContext.getContentResolver();
+ PowerManager powerManager = (PowerManager)mParentContext.getSystemService(
+ Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK |
+ PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ }
+
+ @Override
+ public void finalize() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+
+ @Override
+ public void run() {
+ mWakeLock.acquire();
+ // Some malicious vCard data may make this thread broken
+ // (e.g. OutOfMemoryError).
+ // Even in such cases, some should be done.
+ try {
+ if (mCanonicalPath != null) {
+ mProgressDialog.setProgressNumberFormat("");
+ mProgressDialog.setProgress(0);
+
+ // Count the number of VCard entries
+ mProgressDialog.setIndeterminate(true);
+ long start;
+ if (DO_PERFORMANCE_PROFILE) {
+ start = System.currentTimeMillis();
+ }
+ VCardEntryCounter counter = new VCardEntryCounter();
+ VCardSourceDetector detector = new VCardSourceDetector();
+ VBuilderCollection builderCollection = new VBuilderCollection(
+ Arrays.asList(counter, detector));
+ boolean result;
+ try {
+ result = readOneVCard(mCanonicalPath,
+ VParser.DEFAULT_CHARSET, builderCollection, null, true);
+ } catch (VCardNestedException e) {
+ try {
+ // Assume that VCardSourceDetector was able to detect the source.
+ // Try again with the detector.
+ result = readOneVCard(mCanonicalPath,
+ VParser.DEFAULT_CHARSET, counter, detector, false);
+ } catch (VCardNestedException e2) {
+ result = false;
+ Log.e(LOG_TAG, "Must not reach here. " + e2);
+ }
+ }
+ if (DO_PERFORMANCE_PROFILE) {
+ long time = System.currentTimeMillis() - start;
+ Log.d(LOG_TAG, "time for counting the number of vCard entries: " +
+ time + " ms");
+ }
+ if (!result) {
+ return;
+ }
+
+ mProgressDialog.setProgressNumberFormat(
+ getString(R.string.reading_vcard_contacts));
+ mProgressDialog.setIndeterminate(false);
+ mProgressDialog.setMax(counter.getCount());
+ String charset = detector.getEstimatedCharset();
+ doActuallyReadOneVCard(charset, true, detector);
+ } else {
+ mProgressDialog.setProgressNumberFormat(
+ getString(R.string.reading_vcard_files));
+ mProgressDialog.setMax(mVCardFileList.size());
+ mProgressDialog.setProgress(0);
+ for (VCardFile vcardFile : mVCardFileList) {
+ if (mCanceled) {
+ return;
+ }
+ String canonicalPath = vcardFile.getCanonicalPath();
+
+ VCardSourceDetector detector = new VCardSourceDetector();
+ try {
+ if (!readOneVCard(canonicalPath, VParser.DEFAULT_CHARSET, detector,
+ null, true)) {
+ continue;
+ }
+ } catch (VCardNestedException e) {
+ // Assume that VCardSourceDetector was able to detect the source.
+ }
+ String charset = detector.getEstimatedCharset();
+ doActuallyReadOneVCard(charset, false, detector);
+ mProgressDialog.incrementProgressBy(1);
+ }
+ }
+ } finally {
+ mWakeLock.release();
+ mProgressDialog.dismiss();
+ }
+ }
+
+ private void doActuallyReadOneVCard(String charset, boolean doIncrementProgress,
+ VCardSourceDetector detector) {
+ VCardDataBuilder builder;
+ if (charset != null) {
+ builder = new VCardDataBuilder(mResolver,
+ mProgressDialog,
+ mParentContext.getString(R.string.reading_vcard_message),
+ mParentHandler,
+ charset,
+ charset,
+ false,
+ mLastNameComesBeforeFirstName);
+ } else {
+ builder = new VCardDataBuilder(mResolver,
+ mProgressDialog,
+ mParentContext.getString(R.string.reading_vcard_message),
+ mParentHandler,
+ null,
+ null,
+ false,
+ mLastNameComesBeforeFirstName);
+ charset = VParser.DEFAULT_CHARSET;
+ }
+ if (doIncrementProgress) {
+ builder.setOnProgressRunnable(new Runnable() {
+ public void run() {
+ mProgressDialog.incrementProgressBy(1);
+ }
+ });
+ }
+ try {
+ readOneVCard(mCanonicalPath, charset, builder, detector, false);
+ } catch (VCardNestedException e) {
+ Log.e(LOG_TAG, "Must not reach here.");
+ }
+ builder.showDebugInfo();
+ }
+
+ private boolean readOneVCard(String canonicalPath, String charset, VBuilder builder,
+ VCardSourceDetector detector, boolean throwNestedException)
+ throws VCardNestedException {
+ FileInputStream is;
+ try {
+ is = new FileInputStream(canonicalPath);
+ mVCardParser = new VCardParser_V21(detector);
+
+ try {
+ mVCardParser.parse(is, charset, builder, mCanceled);
+ } catch (VCardVersionException e1) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ is = new FileInputStream(canonicalPath);
+
+ try {
+ mVCardParser = new VCardParser_V30();
+ mVCardParser.parse(is, charset, builder, mCanceled);
+ } catch (VCardVersionException e2) {
+ throw new VCardException("vCard with unspported version.");
+ }
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ mVCardParser.showDebugInfo();
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "IOException was emitted: " + e);
+
+ mProgressDialog.dismiss();
+
+ mParentHandler.post(new ErrorDisplayer(
+ getString(R.string.fail_reason_io_error) +
+ " (" + e.getMessage() + ")"));
+ return false;
+ } catch (VCardNestedException e) {
+ if (throwNestedException) {
+ throw e;
+ } else {
+ Log.e(LOG_TAG, "VCardNestedException was emitted: " + e);
+ mParentHandler.post(new ErrorDisplayer(
+ getString(R.string.fail_reason_vcard_parse_error) +
+ " (" + e.getMessage() + ")"));
+ return false;
+ }
+ } catch (VCardException e) {
+ Log.e(LOG_TAG, "VCardException was emitted: " + e);
+
+ mParentHandler.post(new ErrorDisplayer(
+ getString(R.string.fail_reason_vcard_parse_error) +
+ " (" + e.getMessage() + ")"));
+ return false;
+ }
+ return true;
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ mCanceled = true;
+ if (mVCardParser != null) {
+ mVCardParser.cancel();
+ }
+ }
+ }
+
+ private class ImportTypeSelectedListener implements
+ DialogInterface.OnClickListener {
+ public static final int IMPORT_ALL = 0;
+ public static final int IMPORT_ONE = 1;
+
+ private List<VCardFile> mVCardFileList;
+ private int mCurrentIndex;
+
+ public ImportTypeSelectedListener(List<VCardFile> vcardFileList) {
+ mVCardFileList = vcardFileList;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ if (mCurrentIndex == IMPORT_ALL) {
+ importAllVCardFromSDCard(mVCardFileList);
+ } else {
+ showVCardFileSelectDialog(mVCardFileList);
+ }
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+
+ } else {
+ mCurrentIndex = which;
+ }
+ }
+ }
+
+ private class VCardSelectedListener implements DialogInterface.OnClickListener {
+ private List<VCardFile> mVCardFileList;
+ private int mCurrentIndex;
+
+ public VCardSelectedListener(List<VCardFile> vcardFileList) {
+ mVCardFileList = vcardFileList;
+ mCurrentIndex = 0;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ importOneVCardFromSDCard(mVCardFileList.get(mCurrentIndex).getCanonicalPath());
+ } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+
+ } else {
+ // Some file is selected.
+ mCurrentIndex = 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;
+
+ // null when search operation is canceled.
+ private List<VCardFile> mVCardFiles;
+
+ // 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>();
+ mVCardFiles = new Vector<VCardFile>();
+ PowerManager powerManager = (PowerManager)mParentContext.getSystemService(
+ Context.POWER_SERVICE);
+ mWakeLock = powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK |
+ PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ }
+
+ @Override
+ public void run() {
+ try {
+ mWakeLock.acquire();
+ getVCardFileRecursively(mRootDirectory);
+ } catch (CanceledException e) {
+ mCanceled = true;
+ } catch (IOException e) {
+ mGotIOException = true;
+ } finally {
+ mWakeLock.release();
+ }
+
+ if (mCanceled) {
+ mVCardFiles = null;
+ }
+
+ mProgressDialog.dismiss();
+
+ if (mGotIOException) {
+ mParentHandler.post(new Runnable() {
+ public void run() {
+ String message = (getString(R.string.scanning_sdcard_failed_message,
+ getString(R.string.fail_reason_io_error)));
+
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(mParentContext)
+ .setTitle(R.string.scanning_sdcard_failed_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ }
+ });
+ } else if (mCanceled) {
+ return;
+ } else {
+ mParentHandler.post(new Runnable() {
+ public void run() {
+ int size = mVCardFiles.size();
+ if (size == 0) {
+ String message = (getString(R.string.scanning_sdcard_failed_message,
+ getString(R.string.fail_reason_no_vcard_file)));
+
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(mParentContext)
+ .setTitle(R.string.scanning_sdcard_failed_title)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ return;
+ } else if (mParentContext.getResources().getBoolean(
+ R.bool.config_import_all_vcard_from_sdcard_automatically)) {
+ importAllVCardFromSDCard(mVCardFiles);
+ } else if (size == 1) {
+ importOneVCardFromSDCard(mVCardFiles.get(0).getCanonicalPath());
+ } else if (mParentContext.getResources().getBoolean(
+ R.bool.config_allow_users_select_all_vcard_import)) {
+ showSelectImportTypeDialog(mVCardFiles);
+ } else {
+ showVCardFileSelectDialog(mVCardFiles);
+ }
+ }
+ });
+ }
+ }
+
+ private void getVCardFileRecursively(File directory)
+ throws CanceledException, IOException {
+ if (mCanceled) {
+ throw new CanceledException();
+ }
+
+ 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());
+ mVCardFiles.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 importOneVCardFromSDCard(String canonicalPath) {
+ VCardReadThread thread = new VCardReadThread(canonicalPath);
+ showReadingVCardDialog(thread);
+ thread.start();
+ }
+
+ private void importAllVCardFromSDCard(List<VCardFile> vcardFileList) {
+ VCardReadThread thread = new VCardReadThread(vcardFileList);
+ showReadingVCardDialog(thread);
+ thread.start();
+ }
+
+ private void showSelectImportTypeDialog(List<VCardFile> vcardFileList) {
+ DialogInterface.OnClickListener listener =
+ new ImportTypeSelectedListener(vcardFileList);
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(mParentContext)
+ .setTitle(R.string.select_vcard_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setNegativeButton(android.R.string.cancel, null);
+
+ String[] items = new String[2];
+ items[ImportTypeSelectedListener.IMPORT_ALL] =
+ getString(R.string.import_all_vcard_string);
+ items[ImportTypeSelectedListener.IMPORT_ONE] =
+ getString(R.string.import_one_vcard_string);
+ builder.setSingleChoiceItems(items,
+ ImportTypeSelectedListener.IMPORT_ALL, listener);
+ builder.show();
+ }
+
+ private void showVCardFileSelectDialog(List<VCardFile> vcardFileList) {
+ int size = vcardFileList.size();
+ DialogInterface.OnClickListener listener =
+ new VCardSelectedListener(vcardFileList);
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(mParentContext)
+ .setTitle(R.string.select_vcard_title)
+ .setPositiveButton(android.R.string.ok, listener)
+ .setNegativeButton(android.R.string.cancel, null);
+
+ CharSequence[] items = new CharSequence[size];
+ DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ for (int i = 0; i < size; i++) {
+ VCardFile vcardFile = vcardFileList.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;
+ }
+ builder.setSingleChoiceItems(items, 0, listener);
+ builder.show();
+ }
+
+ private void showReadingVCardDialog(DialogInterface.OnCancelListener listener) {
+ String title = getString(R.string.reading_vcard_title);
+ String message = getString(R.string.reading_vcard_message);
+ mProgressDialog = new ProgressDialog(mParentContext);
+ mProgressDialog.setTitle(title);
+ mProgressDialog.setMessage(message);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setOnCancelListener(listener);
+ mProgressDialog.show();
+ }
+
+ private String getString(int resId, Object... formatArgs) {
+ return mParentContext.getString(resId, formatArgs);
+ }
+
+ private String getString(int resId) {
+ return mParentContext.getString(resId);
+ }
+
+ /**
+ * @param parentContext must not be null
+ * @param parentHandler must not be null
+ */
+ public VCardImporter(Context parentContext, Handler parentHandler) {
+ mParentContext = parentContext;
+ mParentHandler = parentHandler;
+ mLastNameComesBeforeFirstName = parentContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_lastname_comes_before_firstname);
+ }
+
+ /**
+ * Tries to start importing VCard. If there's no SDCard available,
+ * an error dialog is shown. If there is, start scanning using another thread
+ * and shows a progress dialog. Several interactions will occur.
+ * This method should be called from a thread with a looper (like Activity).
+ */
+ public void startImportVCardFromSdCard() {
+ File file = new File("/sdcard");
+ if (!file.exists() || !file.isDirectory() || !file.canRead()) {
+ new AlertDialog.Builder(mParentContext)
+ .setTitle(R.string.no_sdcard_title)
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setMessage(R.string.no_sdcard_message)
+ .setPositiveButton(android.R.string.ok, null)
+ .show();
+ } else {
+ String title = getString(R.string.searching_vcard_title);
+ String message = getString(R.string.searching_vcard_message);
+
+ mProgressDialog = ProgressDialog.show(mParentContext,
+ title, message, true, false);
+ VCardScanThread thread = new VCardScanThread(file);
+ mProgressDialog.setOnCancelListener(thread);
+ thread.start();
+ }
+ }
+}