ImportVCardActivity.java revision 0e38be03c78137194d4bc9d3df8b7c2a63f72d69
1a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang/* 2a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * Copyright (C) 2009 The Android Open Source Project 3a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * 4a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * Licensed under the Apache License, Version 2.0 (the "License"); 5a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * you may not use this file except in compliance with the License. 6a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * You may obtain a copy of the License at 7a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * 8a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * http://www.apache.org/licenses/LICENSE-2.0 9a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * 10a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * Unless required by applicable law or agreed to in writing, software 11a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * distributed under the License is distributed on an "AS IS" BASIS, 12a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * See the License for the specific language governing permissions and 14a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * limitations under the License. 15a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang */ 16a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang 17a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangpackage com.android.contacts.vcard; 18a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang 19a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.contacts.ContactsActivity; 20a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.contacts.R; 213737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport com.android.contacts.model.AccountTypeManager; 223737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport com.android.contacts.util.AccountSelectionUtil; 23a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.vcard.VCardEntryCounter; 24a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.vcard.VCardParser; 25a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.vcard.VCardParser_V21; 26a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.vcard.VCardParser_V30; 27a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.vcard.VCardSourceDetector; 28a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.vcard.exception.VCardException; 29a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport com.android.vcard.exception.VCardNestedException; 303737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport com.android.vcard.exception.VCardVersionException; 31a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang 32a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.accounts.Account; 33a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.app.Activity; 34a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.app.AlertDialog; 35a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.app.Dialog; 36a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.app.Notification; 37a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.app.NotificationManager; 38a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.app.ProgressDialog; 39a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.ComponentName; 40a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.ContentResolver; 41a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.Context; 42a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.DialogInterface; 43a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.DialogInterface.OnCancelListener; 44a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.DialogInterface.OnClickListener; 45a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.Intent; 46a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.content.ServiceConnection; 473737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.content.res.Configuration; 483737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.net.Uri; 493737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.os.Bundle; 503737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.os.Environment; 513737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.os.Handler; 523737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.os.IBinder; 53a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.os.Message; 54a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.os.Messenger; 55a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.os.PowerManager; 56a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.os.RemoteException; 57a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.text.SpannableStringBuilder; 58a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.text.Spanned; 59a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhangimport android.text.TextUtils; 603737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.text.style.RelativeSizeSpan; 613737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.util.Log; 623737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport android.widget.Toast; 633737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhang 643737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.io.File; 653737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.io.IOException; 663737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.io.InputStream; 673737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.nio.ByteBuffer; 683737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.nio.channels.Channels; 693737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.nio.channels.ReadableByteChannel; 703737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.nio.channels.WritableByteChannel; 713737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.text.DateFormat; 723737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.text.SimpleDateFormat; 733737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.util.ArrayList; 743737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.util.Arrays; 753737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.util.Date; 763737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.util.HashSet; 773737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.util.List; 783737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.util.Set; 793737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhangimport java.util.Vector; 803737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhang 813737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhang/** 823737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhang * The class letting users to import vCard. This includes the UI part for letting them select 833737fabda48c5fefc0eb9758e73202dd9f4ebc57Fan Zhang * an Account and posssibly a file if there's no Uri is given from its caller Activity. 84a44b1efbdce7dc50d6e3576150a47bbc72bfed96Fan Zhang * 85 * Note that this Activity assumes that the instance is a "one-shot Activity", which will be 86 * finished (with the method {@link Activity#finish()}) after the import and never reuse 87 * any Dialog in the instance. So this code is careless about the management around managed 88 * dialogs stuffs (like how onCreateDialog() is used). 89 */ 90public class ImportVCardActivity extends ContactsActivity { 91 private static final String LOG_TAG = "VCardImport"; 92 93 private static final int SELECT_ACCOUNT = 0; 94 95 /* package */ static final String VCARD_URI_ARRAY = "vcard_uri"; 96 /* package */ static final String ESTIMATED_VCARD_TYPE_ARRAY = "estimated_vcard_type"; 97 /* package */ static final String ESTIMATED_CHARSET_ARRAY = "estimated_charset"; 98 /* package */ static final String VCARD_VERSION_ARRAY = "vcard_version"; 99 /* package */ static final String ENTRY_COUNT_ARRAY = "entry_count"; 100 101 /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0; 102 /* package */ final static int VCARD_VERSION_V21 = 1; 103 /* package */ final static int VCARD_VERSION_V30 = 2; 104 105 private static final String SECURE_DIRECTORY_NAME = ".android_secure"; 106 107 /** 108 * Notification id used when error happened before sending an import request to VCardServer. 109 */ 110 private static final int DEFAULT_NOTIFICATION_ID = 1000; 111 112 final static String CACHED_URIS = "cached_uris"; 113 114 private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener; 115 116 private Account mAccount; 117 118 private String mAction; 119 private Uri mUri; 120 121 private ProgressDialog mProgressDialogForScanVCard; 122 private ProgressDialog mProgressDialogForCachingVCard; 123 124 private List<VCardFile> mAllVCardFileList; 125 private VCardScanThread mVCardScanThread; 126 127 private VCardCacheThread mVCardCacheThread; 128 private ImportRequestConnection mConnection; 129 130 private String mErrorMessage; 131 132 private Handler mHandler = new Handler(); 133 134 private static class VCardFile { 135 private final String mName; 136 private final String mCanonicalPath; 137 private final long mLastModified; 138 139 public VCardFile(String name, String canonicalPath, long lastModified) { 140 mName = name; 141 mCanonicalPath = canonicalPath; 142 mLastModified = lastModified; 143 } 144 145 public String getName() { 146 return mName; 147 } 148 149 public String getCanonicalPath() { 150 return mCanonicalPath; 151 } 152 153 public long getLastModified() { 154 return mLastModified; 155 } 156 } 157 158 // Runs on the UI thread. 159 private class DialogDisplayer implements Runnable { 160 private final int mResId; 161 public DialogDisplayer(int resId) { 162 mResId = resId; 163 } 164 public DialogDisplayer(String errorMessage) { 165 mResId = R.id.dialog_error_with_message; 166 mErrorMessage = errorMessage; 167 } 168 @Override 169 public void run() { 170 showDialog(mResId); 171 } 172 } 173 174 private class CancelListener 175 implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { 176 @Override 177 public void onClick(DialogInterface dialog, int which) { 178 finish(); 179 } 180 @Override 181 public void onCancel(DialogInterface dialog) { 182 finish(); 183 } 184 } 185 186 private CancelListener mCancelListener = new CancelListener(); 187 188 private class ImportRequestConnection implements ServiceConnection { 189 private Messenger mMessenger; 190 191 public void sendImportRequest(final ImportRequest request) { 192 Log.i(LOG_TAG, String.format("Send an import request (Uri: %s)", request.uri)); 193 try { 194 mMessenger.send(Message.obtain(null, 195 VCardService.MSG_IMPORT_REQUEST, 196 request)); 197 } catch (RemoteException e) { 198 Log.e(LOG_TAG, "RemoteException is thrown when trying to send request"); 199 runOnUiThread(new DialogDisplayer(getString(R.string.fail_reason_unknown))); 200 } 201 } 202 203 @Override 204 public void onServiceConnected(ComponentName name, IBinder service) { 205 mMessenger = new Messenger(service); 206 Log.i(LOG_TAG, 207 String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)", 208 Arrays.toString(mVCardCacheThread.getSourceUris()))); 209 mVCardCacheThread.start(); 210 } 211 212 @Override 213 public void onServiceDisconnected(ComponentName name) { 214 Log.i(LOG_TAG, "Disconnected from VCardService"); 215 } 216 } 217 218 /** 219 * Caches given vCard files into a local directory, and sends actual import request to 220 * {@link VCardService}. 221 * 222 * We need to cache given files into local storage. One of reasons is that some data (as Uri) 223 * may have special permissions. Callers may allow only this Activity to access that content, 224 * not what this Activity launched (like {@link VCardService}). 225 */ 226 private class VCardCacheThread extends Thread 227 implements DialogInterface.OnCancelListener { 228 private boolean mCanceled; 229 private PowerManager.WakeLock mWakeLock; 230 private VCardParser mVCardParser; 231 private final Uri[] mSourceUris; // Given from a caller. 232 233 public VCardCacheThread(final Uri[] sourceUris) { 234 mSourceUris = sourceUris; 235 final Context context = ImportVCardActivity.this; 236 final PowerManager powerManager = 237 (PowerManager)context.getSystemService(Context.POWER_SERVICE); 238 mWakeLock = powerManager.newWakeLock( 239 PowerManager.SCREEN_DIM_WAKE_LOCK | 240 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 241 } 242 243 @Override 244 public void finalize() { 245 if (mWakeLock != null && mWakeLock.isHeld()) { 246 Log.w(LOG_TAG, "WakeLock is being held."); 247 mWakeLock.release(); 248 } 249 } 250 251 @Override 252 public void run() { 253 Log.i(LOG_TAG, "vCard cache thread starts running."); 254 if (mConnection == null) { 255 throw new NullPointerException("vCard cache thread must be launched " 256 + "after a service connection is established"); 257 } 258 259 mWakeLock.acquire(); 260 try { 261 if (mCanceled == true) { 262 Log.i(LOG_TAG, "vCard cache operation is canceled."); 263 return; 264 } 265 266 final Context context = ImportVCardActivity.this; 267 // Uris given from caller applications may not be opened twice: consider when 268 // it is not from local storage (e.g. "file:///...") but from some special 269 // provider (e.g. "content://..."). 270 // Thus we have to once copy the content of Uri into local storage, and read 271 // it after it. 272 // 273 // We may be able to read content of each vCard file during copying them 274 // to local storage, but currently vCard code does not allow us to do so. 275 int cache_index = 0; 276 for (Uri sourceUri : mSourceUris) { 277 String filename = null; 278 // Note: caches are removed by VCardService. 279 while (true) { 280 filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf"; 281 final File file = context.getFileStreamPath(filename); 282 if (!file.exists()) { 283 break; 284 } else { 285 if (cache_index == Integer.MAX_VALUE) { 286 throw new RuntimeException("Exceeded cache limit"); 287 } 288 cache_index++; 289 } 290 } 291 final Uri localDataUri = copyTo(sourceUri, filename); 292 if (mCanceled) { 293 Log.i(LOG_TAG, "vCard cache operation is canceled."); 294 break; 295 } 296 if (localDataUri == null) { 297 Log.w(LOG_TAG, "destUri is null"); 298 break; 299 } 300 final ImportRequest parameter; 301 try { 302 parameter = constructImportRequest(localDataUri, sourceUri); 303 } catch (VCardException e) { 304 Log.e(LOG_TAG, "Maybe the file is in wrong format", e); 305 showFailureNotification(R.string.fail_reason_not_supported); 306 return; 307 } catch (IOException e) { 308 Log.e(LOG_TAG, "Unexpected IOException", e); 309 showFailureNotification(R.string.fail_reason_io_error); 310 return; 311 } 312 if (mCanceled) { 313 Log.i(LOG_TAG, "vCard cache operation is canceled."); 314 return; 315 } 316 mConnection.sendImportRequest(parameter); 317 } 318 } catch (OutOfMemoryError e) { 319 Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard"); 320 System.gc(); 321 runOnUiThread(new DialogDisplayer( 322 getString(R.string.fail_reason_low_memory_during_import))); 323 } catch (IOException e) { 324 Log.e(LOG_TAG, "IOException during caching vCard", e); 325 runOnUiThread(new DialogDisplayer( 326 getString(R.string.fail_reason_io_error))); 327 } finally { 328 Log.i(LOG_TAG, "Finished caching vCard."); 329 mWakeLock.release(); 330 unbindService(mConnection); 331 mProgressDialogForCachingVCard.dismiss(); 332 mProgressDialogForCachingVCard = null; 333 finish(); 334 } 335 } 336 337 /** 338 * Copy the content of sourceUri to the destination. 339 */ 340 private Uri copyTo(final Uri sourceUri, String filename) throws IOException { 341 Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)", 342 sourceUri, filename)); 343 final Context context = ImportVCardActivity.this; 344 final ContentResolver resolver = context.getContentResolver(); 345 ReadableByteChannel inputChannel = null; 346 WritableByteChannel outputChannel = null; 347 Uri destUri = null; 348 try { 349 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri)); 350 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString()); 351 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel(); 352 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192); 353 while (inputChannel.read(buffer) != -1) { 354 if (mCanceled) { 355 Log.d(LOG_TAG, "Canceled during caching " + sourceUri); 356 return null; 357 } 358 buffer.flip(); 359 outputChannel.write(buffer); 360 buffer.compact(); 361 } 362 buffer.flip(); 363 while (buffer.hasRemaining()) { 364 outputChannel.write(buffer); 365 } 366 } finally { 367 if (inputChannel != null) { 368 try { 369 inputChannel.close(); 370 } catch (IOException e) { 371 Log.w(LOG_TAG, "Failed to close inputChannel."); 372 } 373 } 374 if (outputChannel != null) { 375 try { 376 outputChannel.close(); 377 } catch(IOException e) { 378 Log.w(LOG_TAG, "Failed to close outputChannel"); 379 } 380 } 381 } 382 return destUri; 383 } 384 385 /** 386 * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from 387 * its content. 388 * 389 * @arg localDataUri Uri actually used for the import. Should be stored in 390 * app local storage, as we cannot guarantee other types of Uris can be read 391 * multiple times. This variable populates {@link ImportRequest#uri}. 392 * @arg originalUri Uri requested to be imported. Used mainly for displaying 393 * information. This variable populates {@link ImportRequest#originalUri}. 394 */ 395 private ImportRequest constructImportRequest( 396 final Uri localDataUri, final Uri originalUri) 397 throws IOException, VCardException { 398 final ContentResolver resolver = ImportVCardActivity.this.getContentResolver(); 399 VCardEntryCounter counter = null; 400 VCardSourceDetector detector = null; 401 int vcardVersion = VCARD_VERSION_V21; 402 try { 403 boolean shouldUseV30 = false; 404 InputStream is = resolver.openInputStream(localDataUri); 405 mVCardParser = new VCardParser_V21(); 406 try { 407 counter = new VCardEntryCounter(); 408 detector = new VCardSourceDetector(); 409 mVCardParser.addInterpreter(counter); 410 mVCardParser.addInterpreter(detector); 411 mVCardParser.parse(is); 412 } catch (VCardVersionException e1) { 413 try { 414 is.close(); 415 } catch (IOException e) { 416 } 417 418 shouldUseV30 = true; 419 is = resolver.openInputStream(localDataUri); 420 mVCardParser = new VCardParser_V30(); 421 try { 422 counter = new VCardEntryCounter(); 423 detector = new VCardSourceDetector(); 424 mVCardParser.addInterpreter(counter); 425 mVCardParser.addInterpreter(detector); 426 mVCardParser.parse(is); 427 } catch (VCardVersionException e2) { 428 throw new VCardException("vCard with unspported version."); 429 } 430 } finally { 431 if (is != null) { 432 try { 433 is.close(); 434 } catch (IOException e) { 435 } 436 } 437 } 438 439 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21; 440 } catch (VCardNestedException e) { 441 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive)."); 442 // Go through without throwing the Exception, as we may be able to detect the 443 // version before it 444 } 445 return new ImportRequest(mAccount, 446 localDataUri, originalUri, 447 detector.getEstimatedType(), 448 detector.getEstimatedCharset(), 449 vcardVersion, counter.getCount()); 450 } 451 452 public Uri[] getSourceUris() { 453 return mSourceUris; 454 } 455 456 public void cancel() { 457 mCanceled = true; 458 if (mVCardParser != null) { 459 mVCardParser.cancel(); 460 } 461 } 462 463 @Override 464 public void onCancel(DialogInterface dialog) { 465 Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard."); 466 cancel(); 467 } 468 } 469 470 private class ImportTypeSelectedListener implements 471 DialogInterface.OnClickListener { 472 public static final int IMPORT_ONE = 0; 473 public static final int IMPORT_MULTIPLE = 1; 474 public static final int IMPORT_ALL = 2; 475 public static final int IMPORT_TYPE_SIZE = 3; 476 477 private int mCurrentIndex; 478 479 public void onClick(DialogInterface dialog, int which) { 480 if (which == DialogInterface.BUTTON_POSITIVE) { 481 switch (mCurrentIndex) { 482 case IMPORT_ALL: 483 importVCardFromSDCard(mAllVCardFileList); 484 break; 485 case IMPORT_MULTIPLE: 486 showDialog(R.id.dialog_select_multiple_vcard); 487 break; 488 default: 489 showDialog(R.id.dialog_select_one_vcard); 490 break; 491 } 492 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 493 finish(); 494 } else { 495 mCurrentIndex = which; 496 } 497 } 498 } 499 500 private class VCardSelectedListener implements 501 DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener { 502 private int mCurrentIndex; 503 private Set<Integer> mSelectedIndexSet; 504 505 public VCardSelectedListener(boolean multipleSelect) { 506 mCurrentIndex = 0; 507 if (multipleSelect) { 508 mSelectedIndexSet = new HashSet<Integer>(); 509 } 510 } 511 512 public void onClick(DialogInterface dialog, int which) { 513 if (which == DialogInterface.BUTTON_POSITIVE) { 514 if (mSelectedIndexSet != null) { 515 List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>(); 516 final int size = mAllVCardFileList.size(); 517 // We'd like to sort the files by its index, so we do not use Set iterator. 518 for (int i = 0; i < size; i++) { 519 if (mSelectedIndexSet.contains(i)) { 520 selectedVCardFileList.add(mAllVCardFileList.get(i)); 521 } 522 } 523 importVCardFromSDCard(selectedVCardFileList); 524 } else { 525 importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex)); 526 } 527 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 528 finish(); 529 } else { 530 // Some file is selected. 531 mCurrentIndex = which; 532 if (mSelectedIndexSet != null) { 533 if (mSelectedIndexSet.contains(which)) { 534 mSelectedIndexSet.remove(which); 535 } else { 536 mSelectedIndexSet.add(which); 537 } 538 } 539 } 540 } 541 542 public void onClick(DialogInterface dialog, int which, boolean isChecked) { 543 if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) { 544 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which, 545 mAllVCardFileList.get(which).getCanonicalPath())); 546 } else { 547 onClick(dialog, which); 548 } 549 } 550 } 551 552 /** 553 * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select 554 * a vCard file is shown. After the choice, VCardReadThread starts running. 555 */ 556 private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener { 557 private boolean mCanceled; 558 private boolean mGotIOException; 559 private File mRootDirectory; 560 561 // To avoid recursive link. 562 private Set<String> mCheckedPaths; 563 private PowerManager.WakeLock mWakeLock; 564 565 private class CanceledException extends Exception { 566 } 567 568 public VCardScanThread(File sdcardDirectory) { 569 mCanceled = false; 570 mGotIOException = false; 571 mRootDirectory = sdcardDirectory; 572 mCheckedPaths = new HashSet<String>(); 573 PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService( 574 Context.POWER_SERVICE); 575 mWakeLock = powerManager.newWakeLock( 576 PowerManager.SCREEN_DIM_WAKE_LOCK | 577 PowerManager.ON_AFTER_RELEASE, LOG_TAG); 578 } 579 580 @Override 581 public void run() { 582 mAllVCardFileList = new Vector<VCardFile>(); 583 try { 584 mWakeLock.acquire(); 585 getVCardFileRecursively(mRootDirectory); 586 } catch (CanceledException e) { 587 mCanceled = true; 588 } catch (IOException e) { 589 mGotIOException = true; 590 } finally { 591 mWakeLock.release(); 592 } 593 594 if (mCanceled) { 595 mAllVCardFileList = null; 596 } 597 598 mProgressDialogForScanVCard.dismiss(); 599 mProgressDialogForScanVCard = null; 600 601 if (mGotIOException) { 602 runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception)); 603 } else if (mCanceled) { 604 finish(); 605 } else { 606 int size = mAllVCardFileList.size(); 607 final Context context = ImportVCardActivity.this; 608 if (size == 0) { 609 runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found)); 610 } else { 611 startVCardSelectAndImport(); 612 } 613 } 614 } 615 616 private void getVCardFileRecursively(File directory) 617 throws CanceledException, IOException { 618 if (mCanceled) { 619 throw new CanceledException(); 620 } 621 622 // e.g. secured directory may return null toward listFiles(). 623 final File[] files = directory.listFiles(); 624 if (files == null) { 625 final String currentDirectoryPath = directory.getCanonicalPath(); 626 final String secureDirectoryPath = 627 mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME); 628 if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) { 629 Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")"); 630 } 631 return; 632 } 633 for (File file : directory.listFiles()) { 634 if (mCanceled) { 635 throw new CanceledException(); 636 } 637 String canonicalPath = file.getCanonicalPath(); 638 if (mCheckedPaths.contains(canonicalPath)) { 639 continue; 640 } 641 642 mCheckedPaths.add(canonicalPath); 643 644 if (file.isDirectory()) { 645 getVCardFileRecursively(file); 646 } else if (canonicalPath.toLowerCase().endsWith(".vcf") && 647 file.canRead()){ 648 String fileName = file.getName(); 649 VCardFile vcardFile = new VCardFile( 650 fileName, canonicalPath, file.lastModified()); 651 mAllVCardFileList.add(vcardFile); 652 } 653 } 654 } 655 656 public void onCancel(DialogInterface dialog) { 657 mCanceled = true; 658 } 659 660 public void onClick(DialogInterface dialog, int which) { 661 if (which == DialogInterface.BUTTON_NEGATIVE) { 662 mCanceled = true; 663 } 664 } 665 } 666 667 private void startVCardSelectAndImport() { 668 int size = mAllVCardFileList.size(); 669 if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) || 670 size == 1) { 671 importVCardFromSDCard(mAllVCardFileList); 672 } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) { 673 runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type)); 674 } else { 675 runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard)); 676 } 677 } 678 679 private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) { 680 final int size = selectedVCardFileList.size(); 681 String[] uriStrings = new String[size]; 682 int i = 0; 683 for (VCardFile vcardFile : selectedVCardFileList) { 684 uriStrings[i] = "file://" + vcardFile.getCanonicalPath(); 685 i++; 686 } 687 importVCard(uriStrings); 688 } 689 690 private void importVCardFromSDCard(final VCardFile vcardFile) { 691 importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())}); 692 } 693 694 private void importVCard(final Uri uri) { 695 importVCard(new Uri[] {uri}); 696 } 697 698 private void importVCard(final String[] uriStrings) { 699 final int length = uriStrings.length; 700 final Uri[] uris = new Uri[length]; 701 for (int i = 0; i < length; i++) { 702 uris[i] = Uri.parse(uriStrings[i]); 703 } 704 importVCard(uris); 705 } 706 707 private void importVCard(final Uri[] uris) { 708 runOnUiThread(new Runnable() { 709 public void run() { 710 mVCardCacheThread = new VCardCacheThread(uris); 711 showDialog(R.id.dialog_cache_vcard); 712 } 713 }); 714 } 715 716 private Dialog getSelectImportTypeDialog() { 717 final DialogInterface.OnClickListener listener = new ImportTypeSelectedListener(); 718 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 719 .setTitle(R.string.select_vcard_title) 720 .setPositiveButton(android.R.string.ok, listener) 721 .setOnCancelListener(mCancelListener) 722 .setNegativeButton(android.R.string.cancel, mCancelListener); 723 724 final String[] items = new String[ImportTypeSelectedListener.IMPORT_TYPE_SIZE]; 725 items[ImportTypeSelectedListener.IMPORT_ONE] = 726 getString(R.string.import_one_vcard_string); 727 items[ImportTypeSelectedListener.IMPORT_MULTIPLE] = 728 getString(R.string.import_multiple_vcard_string); 729 items[ImportTypeSelectedListener.IMPORT_ALL] = 730 getString(R.string.import_all_vcard_string); 731 builder.setSingleChoiceItems(items, ImportTypeSelectedListener.IMPORT_ONE, listener); 732 return builder.create(); 733 } 734 735 private Dialog getVCardFileSelectDialog(boolean multipleSelect) { 736 final int size = mAllVCardFileList.size(); 737 final VCardSelectedListener listener = new VCardSelectedListener(multipleSelect); 738 final AlertDialog.Builder builder = 739 new AlertDialog.Builder(this) 740 .setTitle(R.string.select_vcard_title) 741 .setPositiveButton(android.R.string.ok, listener) 742 .setOnCancelListener(mCancelListener) 743 .setNegativeButton(android.R.string.cancel, mCancelListener); 744 745 CharSequence[] items = new CharSequence[size]; 746 DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 747 for (int i = 0; i < size; i++) { 748 VCardFile vcardFile = mAllVCardFileList.get(i); 749 SpannableStringBuilder stringBuilder = new SpannableStringBuilder(); 750 stringBuilder.append(vcardFile.getName()); 751 stringBuilder.append('\n'); 752 int indexToBeSpanned = stringBuilder.length(); 753 // Smaller date text looks better, since each file name becomes easier to read. 754 // The value set to RelativeSizeSpan is arbitrary. You can change it to any other 755 // value (but the value bigger than 1.0f would not make nice appearance :) 756 stringBuilder.append( 757 "(" + dateFormat.format(new Date(vcardFile.getLastModified())) + ")"); 758 stringBuilder.setSpan( 759 new RelativeSizeSpan(0.7f), indexToBeSpanned, stringBuilder.length(), 760 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 761 items[i] = stringBuilder; 762 } 763 if (multipleSelect) { 764 builder.setMultiChoiceItems(items, (boolean[])null, listener); 765 } else { 766 builder.setSingleChoiceItems(items, 0, listener); 767 } 768 return builder.create(); 769 } 770 771 @Override 772 protected void onCreate(Bundle bundle) { 773 super.onCreate(bundle); 774 775 String accountName = null; 776 String accountType = null; 777 final Intent intent = getIntent(); 778 if (intent != null) { 779 accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME); 780 accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE); 781 mAction = intent.getAction(); 782 mUri = intent.getData(); 783 } else { 784 Log.e(LOG_TAG, "intent does not exist"); 785 } 786 787 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) { 788 mAccount = new Account(accountName, accountType); 789 } else { 790 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this); 791 final List<Account> accountList = accountTypes.getAccounts(true); 792 if (accountList.size() == 0) { 793 mAccount = null; 794 } else if (accountList.size() == 1) { 795 mAccount = accountList.get(0); 796 } else { 797 startActivityForResult(new Intent(this, SelectAccountActivity.class), 798 SELECT_ACCOUNT); 799 return; 800 } 801 } 802 803 startImport(mAction, mUri); 804 } 805 806 @Override 807 public void onActivityResult(int requestCode, int resultCode, Intent intent) { 808 if (requestCode == SELECT_ACCOUNT) { 809 if (resultCode == RESULT_OK) { 810 mAccount = new Account( 811 intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME), 812 intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE)); 813 startImport(mAction, mUri); 814 } else { 815 if (resultCode != RESULT_CANCELED) { 816 Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode); 817 } 818 finish(); 819 } 820 } 821 } 822 823 private void startImport(String action, Uri uri) { 824 if (uri != null) { 825 Log.i(LOG_TAG, "Starting vCard import using Uri " + uri); 826 importVCard(uri); 827 } else { 828 Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually."); 829 doScanExternalStorageAndImportVCard(); 830 } 831 } 832 833 @Override 834 protected Dialog onCreateDialog(int resId, Bundle bundle) { 835 switch (resId) { 836 case R.string.import_from_sdcard: { 837 if (mAccountSelectionListener == null) { 838 throw new NullPointerException( 839 "mAccountSelectionListener must not be null."); 840 } 841 return AccountSelectionUtil.getSelectAccountDialog(this, resId, 842 mAccountSelectionListener, mCancelListener); 843 } 844 case R.id.dialog_searching_vcard: { 845 if (mProgressDialogForScanVCard == null) { 846 String title = getString(R.string.searching_vcard_title); 847 String message = getString(R.string.searching_vcard_message); 848 mProgressDialogForScanVCard = 849 ProgressDialog.show(this, title, message, true, false); 850 mProgressDialogForScanVCard.setOnCancelListener(mVCardScanThread); 851 mVCardScanThread.start(); 852 } 853 return mProgressDialogForScanVCard; 854 } 855 case R.id.dialog_sdcard_not_found: { 856 AlertDialog.Builder builder = new AlertDialog.Builder(this) 857 .setTitle(R.string.no_sdcard_title) 858 .setIconAttribute(android.R.attr.alertDialogIcon) 859 .setMessage(R.string.no_sdcard_message) 860 .setOnCancelListener(mCancelListener) 861 .setPositiveButton(android.R.string.ok, mCancelListener); 862 return builder.create(); 863 } 864 case R.id.dialog_vcard_not_found: { 865 final String message = getString(R.string.import_failure_no_vcard_file); 866 AlertDialog.Builder builder = new AlertDialog.Builder(this) 867 .setTitle(R.string.scanning_sdcard_failed_title) 868 .setMessage(message) 869 .setOnCancelListener(mCancelListener) 870 .setPositiveButton(android.R.string.ok, mCancelListener); 871 return builder.create(); 872 } 873 case R.id.dialog_select_import_type: { 874 return getSelectImportTypeDialog(); 875 } 876 case R.id.dialog_select_multiple_vcard: { 877 return getVCardFileSelectDialog(true); 878 } 879 case R.id.dialog_select_one_vcard: { 880 return getVCardFileSelectDialog(false); 881 } 882 case R.id.dialog_cache_vcard: { 883 if (mProgressDialogForCachingVCard == null) { 884 final String title = getString(R.string.caching_vcard_title); 885 final String message = getString(R.string.caching_vcard_message); 886 mProgressDialogForCachingVCard = new ProgressDialog(this); 887 mProgressDialogForCachingVCard.setTitle(title); 888 mProgressDialogForCachingVCard.setMessage(message); 889 mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER); 890 mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread); 891 mConnection = new ImportRequestConnection(); 892 893 Log.i(LOG_TAG, "Bind to VCardService."); 894 // We don't want the service finishes itself just after this connection. 895 startService(new Intent(this, VCardService.class)); 896 bindService(new Intent(this, VCardService.class), 897 mConnection, Context.BIND_AUTO_CREATE); 898 } 899 return mProgressDialogForCachingVCard; 900 } 901 case R.id.dialog_io_exception: { 902 String message = (getString(R.string.scanning_sdcard_failed_message, 903 getString(R.string.fail_reason_io_error))); 904 AlertDialog.Builder builder = new AlertDialog.Builder(this) 905 .setTitle(R.string.scanning_sdcard_failed_title) 906 .setIconAttribute(android.R.attr.alertDialogIcon) 907 .setMessage(message) 908 .setOnCancelListener(mCancelListener) 909 .setPositiveButton(android.R.string.ok, mCancelListener); 910 return builder.create(); 911 } 912 case R.id.dialog_error_with_message: { 913 String message = mErrorMessage; 914 if (TextUtils.isEmpty(message)) { 915 Log.e(LOG_TAG, "Error message is null while it must not."); 916 message = getString(R.string.fail_reason_unknown); 917 } 918 final AlertDialog.Builder builder = new AlertDialog.Builder(this) 919 .setTitle(getString(R.string.reading_vcard_failed_title)) 920 .setIconAttribute(android.R.attr.alertDialogIcon) 921 .setMessage(message) 922 .setOnCancelListener(mCancelListener) 923 .setPositiveButton(android.R.string.ok, mCancelListener); 924 return builder.create(); 925 } 926 } 927 928 return super.onCreateDialog(resId, bundle); 929 } 930 931 @Override 932 protected void onRestoreInstanceState(Bundle savedInstanceState) { 933 super.onRestoreInstanceState(savedInstanceState); 934 if (mProgressDialogForCachingVCard != null) { 935 Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again."); 936 showDialog(R.id.dialog_cache_vcard); 937 } 938 } 939 940 @Override 941 public void onConfigurationChanged(Configuration newConfig) { 942 super.onConfigurationChanged(newConfig); 943 // This Activity should finish itself on orientation change, and give the main screen back 944 // to the caller Activity. 945 finish(); 946 } 947 948 /** 949 * Scans vCard in external storage (typically SDCard) and tries to import it. 950 * - When there's no SDCard available, an error dialog is shown. 951 * - When multiple vCard files are available, asks a user to select one. 952 */ 953 private void doScanExternalStorageAndImportVCard() { 954 // TODO: should use getExternalStorageState(). 955 final File file = Environment.getExternalStorageDirectory(); 956 if (!file.exists() || !file.isDirectory() || !file.canRead()) { 957 showDialog(R.id.dialog_sdcard_not_found); 958 } else { 959 mVCardScanThread = new VCardScanThread(file); 960 showDialog(R.id.dialog_searching_vcard); 961 } 962 } 963 964 private void showFailureNotification(int reasonId) { 965 final NotificationManager notificationManager = 966 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); 967 final Notification notification = 968 VCardService.constructImportFailureNotification( 969 ImportVCardActivity.this, 970 getString(reasonId)); 971 notificationManager.notify(DEFAULT_NOTIFICATION_ID, notification); 972 mHandler.post(new Runnable() { 973 @Override 974 public void run() { 975 Toast.makeText(ImportVCardActivity.this, 976 getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show(); 977 } 978 }); 979 } 980} 981