| /* |
| * 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.accounts.Account; |
| import android.app.Activity; |
| import android.app.AlertDialog; |
| import android.app.Dialog; |
| import android.app.ProgressDialog; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.DialogInterface.OnCancelListener; |
| import android.content.DialogInterface.OnClickListener; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.os.Handler; |
| import android.os.PowerManager; |
| import android.pim.vcard.VCardConfig; |
| import android.pim.vcard.VCardEntryCommitter; |
| import android.pim.vcard.VCardEntryConstructor; |
| import android.pim.vcard.VCardEntryCounter; |
| import android.pim.vcard.VCardInterpreter; |
| import android.pim.vcard.VCardInterpreterCollection; |
| import android.pim.vcard.VCardParser_V21; |
| import android.pim.vcard.VCardParser_V30; |
| import android.pim.vcard.VCardSourceDetector; |
| import android.pim.vcard.exception.VCardException; |
| import android.pim.vcard.exception.VCardNestedException; |
| import android.pim.vcard.exception.VCardNotSupportedException; |
| import android.pim.vcard.exception.VCardVersionException; |
| import android.provider.ContactsContract.RawContacts; |
| import android.text.SpannableStringBuilder; |
| import android.text.Spanned; |
| import android.text.TextUtils; |
| import android.text.style.RelativeSizeSpan; |
| import android.util.Log; |
| |
| import com.android.contacts.model.Sources; |
| import com.android.contacts.util.AccountSelectionUtil; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| 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.Locale; |
| 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.) |
| * |
| * 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 = "ImportVCardActivity"; |
| private static final boolean DO_PERFORMANCE_PROFILE = false; |
| |
| // Run on the UI thread. Must not be null except after onDestroy(). |
| private Handler mHandler = new Handler(); |
| |
| private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener; |
| private Account mAccount; |
| |
| private ProgressDialog mProgressDialogForScanVCard; |
| |
| private List<VCardFile> mAllVCardFileList; |
| private VCardScanThread mVCardScanThread; |
| private VCardReadThread mVCardReadThread; |
| private ProgressDialog mProgressDialogForReadVCard; |
| |
| private String mErrorMessage; |
| |
| private boolean mNeedReview = false; |
| |
| // 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; |
| } |
| public void run() { |
| showDialog(mResId); |
| } |
| } |
| |
| 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 VCardReadThread extends Thread |
| implements DialogInterface.OnCancelListener { |
| private ContentResolver mResolver; |
| private VCardParser_V21 mVCardParser; |
| private boolean mCanceled; |
| private PowerManager.WakeLock mWakeLock; |
| private Uri mUri; |
| private File mTempFile; |
| |
| private List<VCardFile> mSelectedVCardFileList; |
| private List<String> mErrorFileNameList; |
| |
| public VCardReadThread(Uri uri) { |
| mUri = uri; |
| init(); |
| } |
| |
| public VCardReadThread(final List<VCardFile> selectedVCardFileList) { |
| mSelectedVCardFileList = selectedVCardFileList; |
| mErrorFileNameList = new ArrayList<String>(); |
| init(); |
| } |
| |
| private void init() { |
| Context context = ImportVCardActivity.this; |
| mResolver = context.getContentResolver(); |
| PowerManager powerManager = (PowerManager)context.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() { |
| boolean shouldCallFinish = true; |
| mWakeLock.acquire(); |
| Uri createdUri = null; |
| mTempFile = null; |
| // Some malicious vCard data may make this thread broken |
| // (e.g. OutOfMemoryError). |
| // Even in such cases, some should be done. |
| try { |
| if (mUri != null) { // Read one vCard expressed by mUri |
| final Uri targetUri = mUri; |
| mProgressDialogForReadVCard.setProgressNumberFormat(""); |
| mProgressDialogForReadVCard.setProgress(0); |
| |
| // Count the number of VCard entries |
| mProgressDialogForReadVCard.setIndeterminate(true); |
| long start; |
| if (DO_PERFORMANCE_PROFILE) { |
| start = System.currentTimeMillis(); |
| } |
| VCardEntryCounter counter = new VCardEntryCounter(); |
| VCardSourceDetector detector = new VCardSourceDetector(); |
| VCardInterpreterCollection builderCollection = new VCardInterpreterCollection( |
| Arrays.asList(counter, detector)); |
| boolean result; |
| try { |
| result = readOneVCardFile(targetUri, |
| VCardConfig.DEFAULT_CHARSET, builderCollection, null, true, null); |
| } catch (VCardNestedException e) { |
| try { |
| // Assume that VCardSourceDetector was able to detect the source. |
| // Try again with the detector. |
| result = readOneVCardFile(targetUri, |
| VCardConfig.DEFAULT_CHARSET, counter, detector, false, null); |
| } 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) { |
| shouldCallFinish = false; |
| return; |
| } |
| |
| mProgressDialogForReadVCard.setProgressNumberFormat( |
| getString(R.string.reading_vcard_contacts)); |
| mProgressDialogForReadVCard.setIndeterminate(false); |
| mProgressDialogForReadVCard.setMax(counter.getCount()); |
| String charset = detector.getEstimatedCharset(); |
| createdUri = doActuallyReadOneVCard(targetUri, null, charset, true, detector, |
| mErrorFileNameList); |
| } else { // Read multiple files. |
| mProgressDialogForReadVCard.setProgressNumberFormat( |
| getString(R.string.reading_vcard_files)); |
| mProgressDialogForReadVCard.setMax(mSelectedVCardFileList.size()); |
| mProgressDialogForReadVCard.setProgress(0); |
| |
| for (VCardFile vcardFile : mSelectedVCardFileList) { |
| if (mCanceled) { |
| return; |
| } |
| // TODO: detect scheme! |
| final Uri targetUri = |
| Uri.parse("file://" + vcardFile.getCanonicalPath()); |
| |
| VCardSourceDetector detector = new VCardSourceDetector(); |
| try { |
| if (!readOneVCardFile(targetUri, VCardConfig.DEFAULT_CHARSET, |
| detector, null, true, mErrorFileNameList)) { |
| continue; |
| } |
| } catch (VCardNestedException e) { |
| // Assume that VCardSourceDetector was able to detect the source. |
| } |
| String charset = detector.getEstimatedCharset(); |
| doActuallyReadOneVCard(targetUri, mAccount, |
| charset, false, detector, mErrorFileNameList); |
| mProgressDialogForReadVCard.incrementProgressBy(1); |
| } |
| } |
| } finally { |
| mWakeLock.release(); |
| mProgressDialogForReadVCard.dismiss(); |
| if (mTempFile != null) { |
| if (!mTempFile.delete()) { |
| Log.w(LOG_TAG, "Failed to delete a cache file."); |
| } |
| mTempFile = null; |
| } |
| // finish() is called via mCancelListener, which is used in DialogDisplayer. |
| if (shouldCallFinish && !isFinishing()) { |
| if (mErrorFileNameList == null || mErrorFileNameList.isEmpty()) { |
| finish(); |
| if (mNeedReview) { |
| mNeedReview = false; |
| Log.v("importVCardActivity", "Prepare to review the imported contact"); |
| |
| if (createdUri != null) { |
| // get contact_id of this raw_contact |
| final long rawContactId = ContentUris.parseId(createdUri); |
| Uri contactUri = RawContacts.getContactLookupUri( |
| getContentResolver(), ContentUris.withAppendedId( |
| RawContacts.CONTENT_URI, rawContactId)); |
| |
| Intent viewIntent = new Intent(Intent.ACTION_VIEW, contactUri); |
| startActivity(viewIntent); |
| } |
| } |
| } else { |
| StringBuilder builder = new StringBuilder(); |
| boolean first = true; |
| for (String fileName : mErrorFileNameList) { |
| if (first) { |
| first = false; |
| } else { |
| builder.append(", "); |
| } |
| builder.append(fileName); |
| } |
| |
| runOnUIThread(new DialogDisplayer( |
| getString(R.string.fail_reason_failed_to_read_files, |
| builder.toString()))); |
| } |
| } |
| } |
| } |
| |
| private Uri doActuallyReadOneVCard(Uri uri, Account account, |
| String charset, boolean showEntryParseProgress, |
| VCardSourceDetector detector, List<String> errorFileNameList) { |
| final Context context = ImportVCardActivity.this; |
| VCardEntryConstructor builder; |
| final String currentLanguage = Locale.getDefault().getLanguage(); |
| int vcardType = VCardConfig.getVCardTypeFromString( |
| context.getString(R.string.config_import_vcard_type)); |
| if (charset != null) { |
| builder = new VCardEntryConstructor(charset, charset, false, vcardType, mAccount); |
| } else { |
| charset = VCardConfig.DEFAULT_CHARSET; |
| builder = new VCardEntryConstructor(null, null, false, vcardType, mAccount); |
| } |
| VCardEntryCommitter committer = new VCardEntryCommitter(mResolver); |
| builder.addEntryHandler(committer); |
| if (showEntryParseProgress) { |
| builder.addEntryHandler(new ProgressShower(mProgressDialogForReadVCard, |
| context.getString(R.string.reading_vcard_message), |
| ImportVCardActivity.this, |
| mHandler)); |
| } |
| |
| try { |
| if (!readOneVCardFile(uri, charset, builder, detector, false, null)) { |
| return null; |
| } |
| } catch (VCardNestedException e) { |
| Log.e(LOG_TAG, "Never reach here."); |
| } |
| final ArrayList<Uri> createdUris = committer.getCreatedUris(); |
| return (createdUris == null || createdUris.size() != 1) ? null : createdUris.get(0); |
| } |
| |
| private boolean readOneVCardFile(Uri uri, String charset, |
| VCardInterpreter builder, VCardSourceDetector detector, |
| boolean throwNestedException, List<String> errorFileNameList) |
| throws VCardNestedException { |
| InputStream is; |
| try { |
| is = mResolver.openInputStream(uri); |
| mVCardParser = new VCardParser_V21(detector); |
| |
| try { |
| mVCardParser.parse(is, charset, builder, mCanceled); |
| } catch (VCardVersionException e1) { |
| try { |
| is.close(); |
| } catch (IOException e) { |
| } |
| if (builder instanceof VCardEntryConstructor) { |
| // Let the object clean up internal temporal objects, |
| ((VCardEntryConstructor)builder).clear(); |
| } |
| is = mResolver.openInputStream(uri); |
| |
| 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) { |
| } |
| } |
| } |
| } catch (IOException e) { |
| Log.e(LOG_TAG, "IOException was emitted: " + e.getMessage()); |
| |
| mProgressDialogForReadVCard.dismiss(); |
| |
| if (errorFileNameList != null) { |
| errorFileNameList.add(uri.toString()); |
| } else { |
| runOnUIThread(new DialogDisplayer( |
| getString(R.string.fail_reason_io_error) + |
| ": " + e.getLocalizedMessage())); |
| } |
| return false; |
| } catch (VCardNotSupportedException e) { |
| if ((e instanceof VCardNestedException) && throwNestedException) { |
| throw (VCardNestedException)e; |
| } |
| if (errorFileNameList != null) { |
| errorFileNameList.add(uri.toString()); |
| } else { |
| runOnUIThread(new DialogDisplayer( |
| getString(R.string.fail_reason_vcard_not_supported_error) + |
| " (" + e.getMessage() + ")")); |
| } |
| return false; |
| } catch (VCardException e) { |
| if (errorFileNameList != null) { |
| errorFileNameList.add(uri.toString()); |
| } else { |
| runOnUIThread(new DialogDisplayer( |
| getString(R.string.fail_reason_vcard_parse_error) + |
| " (" + e.getMessage() + ")")); |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| public void cancel() { |
| mCanceled = true; |
| if (mVCardParser != null) { |
| mVCardParser.cancel(); |
| } |
| } |
| |
| public void onCancel(DialogInterface dialog) { |
| 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: |
| importMultipleVCardFromSDCard(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>(); |
| 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)); |
| } |
| } |
| importMultipleVCardFromSDCard(selectedVCardFileList); |
| } else { |
| String canonicalPath = mAllVCardFileList.get(mCurrentIndex).getCanonicalPath(); |
| final Uri uri = Uri.parse("file://" + canonicalPath); |
| importOneVCardFromSDCard(uri); |
| } |
| } 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) { |
| 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)) { |
| importMultipleVCardFromSDCard(mAllVCardFileList); |
| } else if (size == 1) { |
| String canonicalPath = mAllVCardFileList.get(0).getCanonicalPath(); |
| Uri uri = Uri.parse("file://" + canonicalPath); |
| importOneVCardFromSDCard(uri); |
| } 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 importMultipleVCardFromSDCard(final List<VCardFile> selectedVCardFileList) { |
| runOnUIThread(new Runnable() { |
| public void run() { |
| mVCardReadThread = new VCardReadThread(selectedVCardFileList); |
| showDialog(R.id.dialog_reading_vcard); |
| } |
| }); |
| } |
| |
| private void importOneVCardFromSDCard(final Uri uri) { |
| runOnUIThread(new Runnable() { |
| public void run() { |
| mVCardReadThread = new VCardReadThread(uri); |
| showDialog(R.id.dialog_reading_vcard); |
| } |
| }); |
| } |
| |
| private Dialog getSelectImportTypeDialog() { |
| DialogInterface.OnClickListener listener = |
| new ImportTypeSelectedListener(); |
| 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); |
| |
| 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) { |
| int size = mAllVCardFileList.size(); |
| VCardSelectedListener listener = new VCardSelectedListener(multipleSelect); |
| 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); |
| |
| final Intent intent = getIntent(); |
| if (intent != null) { |
| final String accountName = intent.getStringExtra("account_name"); |
| final String accountType = intent.getStringExtra("account_type"); |
| if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { |
| mAccount = new Account(accountName, accountType); |
| } |
| } else { |
| Log.e(LOG_TAG, "intent does not exist"); |
| } |
| |
| // The caller often does not know account information at all, so we show the UI instead. |
| if (mAccount == null) { |
| // There's three possibilities: |
| // - more than one accounts -> ask the user |
| // - just one account -> use the account without asking the user |
| // - no account -> use phone-local storage without asking the user |
| final Sources sources = Sources.getInstance(this); |
| final List<Account> accountList = sources.getAccounts(true); |
| final int size = accountList.size(); |
| if (size > 1) { |
| final int resId = R.string.import_from_sdcard; |
| mAccountSelectionListener = |
| new AccountSelectionUtil.AccountSelectedListener( |
| this, accountList, resId) { |
| @Override |
| public void onClick(DialogInterface dialog, int which) { |
| dialog.dismiss(); |
| mAccount = mAccountList.get(which); |
| // Instead of using Intent mechanism, call the relevant private method, |
| // to avoid throwing an Intent to itself again. |
| startImport(); |
| } |
| }; |
| showDialog(resId); |
| return; |
| } else { |
| mAccount = size > 0 ? accountList.get(0) : null; |
| } |
| } |
| |
| startImport(); |
| } |
| |
| private void startImport() { |
| Intent intent = getIntent(); |
| final String action = intent.getAction(); |
| final Uri uri = intent.getData(); |
| Log.v(LOG_TAG, "action = " + action + " ; path = " + uri); |
| if (Intent.ACTION_VIEW.equals(action)) { |
| // Import the file directly and then go to EDIT screen |
| mNeedReview = true; |
| } |
| |
| if (uri != null) { |
| importOneVCardFromSDCard(uri); |
| } else { |
| doScanExternalStorageAndImportVCard(); |
| } |
| } |
| |
| @Override |
| protected Dialog onCreateDialog(int resId) { |
| 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, |
| new CancelListener()); |
| } |
| case R.id.dialog_searching_vcard: { |
| if (mProgressDialogForScanVCard == null) { |
| String title = getString(R.string.searching_vcard_title); |
| String message = getString(R.string.searching_vcard_message); |
| mProgressDialogForScanVCard = |
| ProgressDialog.show(this, title, message, true, false); |
| mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread); |
| mVCardScanThread.start(); |
| } |
| return mProgressDialogForScanVCard; |
| } |
| 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) |
| .setOnCancelListener(mCancelListener) |
| .setPositiveButton(android.R.string.ok, mCancelListener); |
| 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))); |
| AlertDialog.Builder builder = new AlertDialog.Builder(this) |
| .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: { |
| 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_reading_vcard: { |
| if (mProgressDialogForReadVCard == null) { |
| String title = getString(R.string.reading_vcard_title); |
| String message = getString(R.string.reading_vcard_message); |
| mProgressDialogForReadVCard = new ProgressDialog(this); |
| mProgressDialogForReadVCard.setTitle(title); |
| mProgressDialogForReadVCard.setMessage(message); |
| mProgressDialogForReadVCard.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); |
| mProgressDialogForReadVCard.setOnCancelListener(mVCardReadThread); |
| mVCardReadThread.start(); |
| } |
| return mProgressDialogForReadVCard; |
| } |
| 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) |
| .setTitle(R.string.scanning_sdcard_failed_title) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .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); |
| } |
| AlertDialog.Builder builder = new AlertDialog.Builder(this) |
| .setTitle(getString(R.string.reading_vcard_failed_title)) |
| .setIcon(android.R.drawable.ic_dialog_alert) |
| .setMessage(message) |
| .setOnCancelListener(mCancelListener) |
| .setPositiveButton(android.R.string.ok, mCancelListener); |
| return builder.create(); |
| } |
| } |
| |
| return super.onCreateDialog(resId); |
| } |
| |
| @Override |
| protected void onPause() { |
| super.onPause(); |
| if (mVCardReadThread != null) { |
| // The Activity is no longer visible. Stop the thread. |
| mVCardReadThread.cancel(); |
| mVCardReadThread = null; |
| } |
| |
| // ImportVCardActivity should not be persistent. In other words, if there's some |
| // event calling onPause(), this Activity should finish its work and give the main |
| // screen back to the caller Activity. |
| if (!isFinishing()) { |
| finish(); |
| } |
| } |
| |
| @Override |
| protected void onDestroy() { |
| // The code assumes the handler runs on the UI thread. If not, |
| // clearing the message queue is not enough, one would have to |
| // make sure that the handler does not run any callback when |
| // this activity isFinishing(). |
| |
| // Need to make sure any worker thread is done before we flush and |
| // nullify the message handler. |
| if (mVCardReadThread != null) { |
| Log.w(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!"); |
| mVCardReadThread.cancel(); |
| int attempts = 0; |
| while (mVCardReadThread.isAlive() && attempts < 10) { |
| try { |
| Thread.currentThread().sleep(20); |
| } catch (InterruptedException ie) { |
| // Keep on going until max attempts is reached. |
| } |
| attempts++; |
| } |
| if (mVCardReadThread.isAlive()) { |
| // Find out why the thread did not exit in a timely |
| // fashion. Last resort: increase the sleep duration |
| // and/or the number of attempts. |
| Log.e(LOG_TAG, "VCardReadThread is still alive after max attempts."); |
| } |
| mVCardReadThread = null; |
| } |
| |
| // Callbacks messages have what == 0. |
| if (mHandler.hasMessages(0)) { |
| mHandler.removeMessages(0); |
| } |
| |
| mHandler = null; // Prevents memory leaks by breaking any circular dependency. |
| super.onDestroy(); |
| } |
| |
| /** |
| * Tries to run a given Runnable object when the UI thread can. Ignore it otherwise |
| */ |
| private void runOnUIThread(Runnable runnable) { |
| if (mHandler == null) { |
| Log.w(LOG_TAG, "Handler object is null. No dialog is shown."); |
| } else { |
| mHandler.post(runnable); |
| } |
| } |
| |
| @Override |
| public void finalize() { |
| // TODO: This should not be needed. Throw exception instead. |
| if (mVCardReadThread != null) { |
| // Not sure this procedure is really needed, but just in case... |
| Log.e(LOG_TAG, "VCardReadThread exists while this Activity is now being killed!"); |
| mVCardReadThread.cancel(); |
| mVCardReadThread = null; |
| } |
| } |
| |
| /** |
| * 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() { |
| // TODO: should use getExternalStorageState(). |
| final File file = Environment.getExternalStorageDirectory(); |
| if (!file.exists() || !file.isDirectory() || !file.canRead()) { |
| showDialog(R.id.dialog_sdcard_not_found); |
| } else { |
| mVCardScanThread = new VCardScanThread(file); |
| showDialog(R.id.dialog_searching_vcard); |
| } |
| } |
| } |