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