• 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.vcard;
18 
19 import com.android.contacts.ContactsActivity;
20 import com.android.contacts.R;
21 import com.android.contacts.model.AccountTypeManager;
22 import com.android.contacts.model.AccountWithDataSet;
23 import com.android.contacts.util.AccountSelectionUtil;
24 import com.android.vcard.VCardEntryCounter;
25 import com.android.vcard.VCardParser;
26 import com.android.vcard.VCardParser_V21;
27 import com.android.vcard.VCardParser_V30;
28 import com.android.vcard.VCardSourceDetector;
29 import com.android.vcard.exception.VCardException;
30 import com.android.vcard.exception.VCardNestedException;
31 import com.android.vcard.exception.VCardVersionException;
32 
33 import android.app.Activity;
34 import android.app.AlertDialog;
35 import android.app.Dialog;
36 import android.app.Notification;
37 import android.app.NotificationManager;
38 import android.app.ProgressDialog;
39 import android.content.ComponentName;
40 import android.content.ContentResolver;
41 import android.content.Context;
42 import android.content.DialogInterface;
43 import android.content.DialogInterface.OnCancelListener;
44 import android.content.DialogInterface.OnClickListener;
45 import android.content.Intent;
46 import android.content.ServiceConnection;
47 import android.content.res.Configuration;
48 import android.database.Cursor;
49 import android.net.Uri;
50 import android.os.Bundle;
51 import android.os.Environment;
52 import android.os.Handler;
53 import android.os.IBinder;
54 import android.os.PowerManager;
55 import android.provider.OpenableColumns;
56 import android.text.SpannableStringBuilder;
57 import android.text.Spanned;
58 import android.text.TextUtils;
59 import android.text.style.RelativeSizeSpan;
60 import android.util.Log;
61 import android.widget.Toast;
62 
63 import java.io.ByteArrayInputStream;
64 import java.io.File;
65 import java.io.IOException;
66 import java.io.InputStream;
67 import java.nio.ByteBuffer;
68 import java.nio.channels.Channels;
69 import java.nio.channels.ReadableByteChannel;
70 import java.nio.channels.WritableByteChannel;
71 import java.text.DateFormat;
72 import java.text.SimpleDateFormat;
73 import java.util.ArrayList;
74 import java.util.Arrays;
75 import java.util.Date;
76 import java.util.HashSet;
77 import java.util.List;
78 import java.util.Set;
79 import java.util.Vector;
80 
81 /**
82  * The class letting users to import vCard. This includes the UI part for letting them select
83  * an Account and posssibly a file if there's no Uri is given from its caller Activity.
84  *
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  */
90 public 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 FAILURE_NOTIFICATION_ID = 1;
111 
112     final static String CACHED_URIS = "cached_uris";
113 
114     private AccountSelectionUtil.AccountSelectedListener mAccountSelectionListener;
115 
116     private AccountWithDataSet mAccount;
117 
118     private ProgressDialog mProgressDialogForScanVCard;
119     private ProgressDialog mProgressDialogForCachingVCard;
120 
121     private List<VCardFile> mAllVCardFileList;
122     private VCardScanThread mVCardScanThread;
123 
124     private VCardCacheThread mVCardCacheThread;
125     private ImportRequestConnection mConnection;
126     /* package */ VCardImportExportListener mListener;
127 
128     private String mErrorMessage;
129 
130     private Handler mHandler = new Handler();
131 
132     private static class VCardFile {
133         private final String mName;
134         private final String mCanonicalPath;
135         private final long mLastModified;
136 
VCardFile(String name, String canonicalPath, long lastModified)137         public VCardFile(String name, String canonicalPath, long lastModified) {
138             mName = name;
139             mCanonicalPath = canonicalPath;
140             mLastModified = lastModified;
141         }
142 
getName()143         public String getName() {
144             return mName;
145         }
146 
getCanonicalPath()147         public String getCanonicalPath() {
148             return mCanonicalPath;
149         }
150 
getLastModified()151         public long getLastModified() {
152             return mLastModified;
153         }
154     }
155 
156     // Runs on the UI thread.
157     private class DialogDisplayer implements Runnable {
158         private final int mResId;
DialogDisplayer(int resId)159         public DialogDisplayer(int resId) {
160             mResId = resId;
161         }
DialogDisplayer(String errorMessage)162         public DialogDisplayer(String errorMessage) {
163             mResId = R.id.dialog_error_with_message;
164             mErrorMessage = errorMessage;
165         }
166         @Override
run()167         public void run() {
168             if (!isFinishing()) {
169                 showDialog(mResId);
170             }
171         }
172     }
173 
174     private class CancelListener
175         implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
176         @Override
onClick(DialogInterface dialog, int which)177         public void onClick(DialogInterface dialog, int which) {
178             finish();
179         }
180         @Override
onCancel(DialogInterface dialog)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 VCardService mService;
190 
sendImportRequest(final List<ImportRequest> requests)191         public void sendImportRequest(final List<ImportRequest> requests) {
192             Log.i(LOG_TAG, "Send an import request");
193             mService.handleImportRequest(requests, mListener);
194         }
195 
196         @Override
onServiceConnected(ComponentName name, IBinder binder)197         public void onServiceConnected(ComponentName name, IBinder binder) {
198             mService = ((VCardService.MyBinder) binder).getService();
199             Log.i(LOG_TAG,
200                     String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
201                             Arrays.toString(mVCardCacheThread.getSourceUris())));
202             mVCardCacheThread.start();
203         }
204 
205         @Override
onServiceDisconnected(ComponentName name)206         public void onServiceDisconnected(ComponentName name) {
207             Log.i(LOG_TAG, "Disconnected from VCardService");
208         }
209     }
210 
211     /**
212      * Caches given vCard files into a local directory, and sends actual import request to
213      * {@link VCardService}.
214      *
215      * We need to cache given files into local storage. One of reasons is that some data (as Uri)
216      * may have special permissions. Callers may allow only this Activity to access that content,
217      * not what this Activity launched (like {@link VCardService}).
218      */
219     private class VCardCacheThread extends Thread
220             implements DialogInterface.OnCancelListener {
221         private boolean mCanceled;
222         private PowerManager.WakeLock mWakeLock;
223         private VCardParser mVCardParser;
224         private final Uri[] mSourceUris;  // Given from a caller.
225         private final byte[] mSource;
226         private final String mDisplayName;
227 
VCardCacheThread(final Uri[] sourceUris)228         public VCardCacheThread(final Uri[] sourceUris) {
229             mSourceUris = sourceUris;
230             mSource = null;
231             final Context context = ImportVCardActivity.this;
232             final PowerManager powerManager =
233                     (PowerManager)context.getSystemService(Context.POWER_SERVICE);
234             mWakeLock = powerManager.newWakeLock(
235                     PowerManager.SCREEN_DIM_WAKE_LOCK |
236                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
237             mDisplayName = null;
238         }
239 
240         @Override
finalize()241         public void finalize() {
242             if (mWakeLock != null && mWakeLock.isHeld()) {
243                 Log.w(LOG_TAG, "WakeLock is being held.");
244                 mWakeLock.release();
245             }
246         }
247 
248         @Override
run()249         public void run() {
250             Log.i(LOG_TAG, "vCard cache thread starts running.");
251             if (mConnection == null) {
252                 throw new NullPointerException("vCard cache thread must be launched "
253                         + "after a service connection is established");
254             }
255 
256             mWakeLock.acquire();
257             try {
258                 if (mCanceled == true) {
259                     Log.i(LOG_TAG, "vCard cache operation is canceled.");
260                     return;
261                 }
262 
263                 final Context context = ImportVCardActivity.this;
264                 // Uris given from caller applications may not be opened twice: consider when
265                 // it is not from local storage (e.g. "file:///...") but from some special
266                 // provider (e.g. "content://...").
267                 // Thus we have to once copy the content of Uri into local storage, and read
268                 // it after it.
269                 //
270                 // We may be able to read content of each vCard file during copying them
271                 // to local storage, but currently vCard code does not allow us to do so.
272                 int cache_index = 0;
273                 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
274                 if (mSource != null) {
275                     try {
276                         requests.add(constructImportRequest(mSource, null, mDisplayName));
277                     } catch (VCardException e) {
278                         Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
279                         showFailureNotification(R.string.fail_reason_not_supported);
280                         return;
281                     }
282                 } else {
283                     final ContentResolver resolver =
284                             ImportVCardActivity.this.getContentResolver();
285                     for (Uri sourceUri : mSourceUris) {
286                         String filename = null;
287                         // Note: caches are removed by VCardService.
288                         while (true) {
289                             filename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
290                             final File file = context.getFileStreamPath(filename);
291                             if (!file.exists()) {
292                                 break;
293                             } else {
294                                 if (cache_index == Integer.MAX_VALUE) {
295                                     throw new RuntimeException("Exceeded cache limit");
296                                 }
297                                 cache_index++;
298                             }
299                         }
300                         final Uri localDataUri = copyTo(sourceUri, filename);
301                         if (mCanceled) {
302                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
303                             break;
304                         }
305                         if (localDataUri == null) {
306                             Log.w(LOG_TAG, "destUri is null");
307                             break;
308                         }
309 
310                         String displayName = null;
311                         Cursor cursor = null;
312                         // Try to get a display name from the given Uri. If it fails, we just
313                         // pick up the last part of the Uri.
314                         try {
315                             cursor = resolver.query(sourceUri,
316                                     new String[] { OpenableColumns.DISPLAY_NAME },
317                                     null, null, null);
318                             if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
319                                 if (cursor.getCount() > 1) {
320                                     Log.w(LOG_TAG, "Unexpected multiple rows: "
321                                             + cursor.getCount());
322                                 }
323                                 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
324                                 if (index >= 0) {
325                                     displayName = cursor.getString(index);
326                                 }
327                             }
328                         } finally {
329                             if (cursor != null) {
330                                 cursor.close();
331                             }
332                         }
333                         if (TextUtils.isEmpty(displayName)){
334                             displayName = sourceUri.getLastPathSegment();
335                         }
336 
337                         final ImportRequest request;
338                         try {
339                             request = constructImportRequest(null, localDataUri, displayName);
340                         } catch (VCardException e) {
341                             Log.e(LOG_TAG, "Maybe the file is in wrong format", e);
342                             showFailureNotification(R.string.fail_reason_not_supported);
343                             return;
344                         } catch (IOException e) {
345                             Log.e(LOG_TAG, "Unexpected IOException", e);
346                             showFailureNotification(R.string.fail_reason_io_error);
347                             return;
348                         }
349                         if (mCanceled) {
350                             Log.i(LOG_TAG, "vCard cache operation is canceled.");
351                             return;
352                         }
353                         requests.add(request);
354                     }
355                 }
356                 if (!requests.isEmpty()) {
357                     mConnection.sendImportRequest(requests);
358                 } else {
359                     Log.w(LOG_TAG, "Empty import requests. Ignore it.");
360                 }
361             } catch (OutOfMemoryError e) {
362                 Log.e(LOG_TAG, "OutOfMemoryError occured during caching vCard");
363                 System.gc();
364                 runOnUiThread(new DialogDisplayer(
365                         getString(R.string.fail_reason_low_memory_during_import)));
366             } catch (IOException e) {
367                 Log.e(LOG_TAG, "IOException during caching vCard", e);
368                 runOnUiThread(new DialogDisplayer(
369                         getString(R.string.fail_reason_io_error)));
370             } finally {
371                 Log.i(LOG_TAG, "Finished caching vCard.");
372                 mWakeLock.release();
373                 unbindService(mConnection);
374                 mProgressDialogForCachingVCard.dismiss();
375                 mProgressDialogForCachingVCard = null;
376                 finish();
377             }
378         }
379 
380         /**
381          * Copy the content of sourceUri to the destination.
382          */
copyTo(final Uri sourceUri, String filename)383         private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
384             Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
385                     sourceUri, filename));
386             final Context context = ImportVCardActivity.this;
387             final ContentResolver resolver = context.getContentResolver();
388             ReadableByteChannel inputChannel = null;
389             WritableByteChannel outputChannel = null;
390             Uri destUri = null;
391             try {
392                 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
393                 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
394                 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
395                 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
396                 while (inputChannel.read(buffer) != -1) {
397                     if (mCanceled) {
398                         Log.d(LOG_TAG, "Canceled during caching " + sourceUri);
399                         return null;
400                     }
401                     buffer.flip();
402                     outputChannel.write(buffer);
403                     buffer.compact();
404                 }
405                 buffer.flip();
406                 while (buffer.hasRemaining()) {
407                     outputChannel.write(buffer);
408                 }
409             } finally {
410                 if (inputChannel != null) {
411                     try {
412                         inputChannel.close();
413                     } catch (IOException e) {
414                         Log.w(LOG_TAG, "Failed to close inputChannel.");
415                     }
416                 }
417                 if (outputChannel != null) {
418                     try {
419                         outputChannel.close();
420                     } catch(IOException e) {
421                         Log.w(LOG_TAG, "Failed to close outputChannel");
422                     }
423                 }
424             }
425             return destUri;
426         }
427 
428         /**
429          * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
430          * its content.
431          *
432          * @arg localDataUri Uri actually used for the import. Should be stored in
433          * app local storage, as we cannot guarantee other types of Uris can be read
434          * multiple times. This variable populates {@link ImportRequest#uri}.
435          * @arg displayName Used for displaying information to the user. This variable populates
436          * {@link ImportRequest#displayName}.
437          */
constructImportRequest(final byte[] data, final Uri localDataUri, final String displayName)438         private ImportRequest constructImportRequest(final byte[] data,
439                 final Uri localDataUri, final String displayName)
440                 throws IOException, VCardException {
441             final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
442             VCardEntryCounter counter = null;
443             VCardSourceDetector detector = null;
444             int vcardVersion = VCARD_VERSION_V21;
445             try {
446                 boolean shouldUseV30 = false;
447                 InputStream is;
448                 if (data != null) {
449                     is = new ByteArrayInputStream(data);
450                 } else {
451                     is = resolver.openInputStream(localDataUri);
452                 }
453                 mVCardParser = new VCardParser_V21();
454                 try {
455                     counter = new VCardEntryCounter();
456                     detector = new VCardSourceDetector();
457                     mVCardParser.addInterpreter(counter);
458                     mVCardParser.addInterpreter(detector);
459                     mVCardParser.parse(is);
460                 } catch (VCardVersionException e1) {
461                     try {
462                         is.close();
463                     } catch (IOException e) {
464                     }
465 
466                     shouldUseV30 = true;
467                     if (data != null) {
468                         is = new ByteArrayInputStream(data);
469                     } else {
470                         is = resolver.openInputStream(localDataUri);
471                     }
472                     mVCardParser = new VCardParser_V30();
473                     try {
474                         counter = new VCardEntryCounter();
475                         detector = new VCardSourceDetector();
476                         mVCardParser.addInterpreter(counter);
477                         mVCardParser.addInterpreter(detector);
478                         mVCardParser.parse(is);
479                     } catch (VCardVersionException e2) {
480                         throw new VCardException("vCard with unspported version.");
481                     }
482                 } finally {
483                     if (is != null) {
484                         try {
485                             is.close();
486                         } catch (IOException e) {
487                         }
488                     }
489                 }
490 
491                 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
492             } catch (VCardNestedException e) {
493                 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
494                 // Go through without throwing the Exception, as we may be able to detect the
495                 // version before it
496             }
497             return new ImportRequest(mAccount,
498                     data, localDataUri, displayName,
499                     detector.getEstimatedType(),
500                     detector.getEstimatedCharset(),
501                     vcardVersion, counter.getCount());
502         }
503 
getSourceUris()504         public Uri[] getSourceUris() {
505             return mSourceUris;
506         }
507 
cancel()508         public void cancel() {
509             mCanceled = true;
510             if (mVCardParser != null) {
511                 mVCardParser.cancel();
512             }
513         }
514 
515         @Override
onCancel(DialogInterface dialog)516         public void onCancel(DialogInterface dialog) {
517             Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
518             cancel();
519         }
520     }
521 
522     private class ImportTypeSelectedListener implements
523             DialogInterface.OnClickListener {
524         public static final int IMPORT_ONE = 0;
525         public static final int IMPORT_MULTIPLE = 1;
526         public static final int IMPORT_ALL = 2;
527         public static final int IMPORT_TYPE_SIZE = 3;
528 
529         private int mCurrentIndex;
530 
onClick(DialogInterface dialog, int which)531         public void onClick(DialogInterface dialog, int which) {
532             if (which == DialogInterface.BUTTON_POSITIVE) {
533                 switch (mCurrentIndex) {
534                 case IMPORT_ALL:
535                     importVCardFromSDCard(mAllVCardFileList);
536                     break;
537                 case IMPORT_MULTIPLE:
538                     showDialog(R.id.dialog_select_multiple_vcard);
539                     break;
540                 default:
541                     showDialog(R.id.dialog_select_one_vcard);
542                     break;
543                 }
544             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
545                 finish();
546             } else {
547                 mCurrentIndex = which;
548             }
549         }
550     }
551 
552     private class VCardSelectedListener implements
553             DialogInterface.OnClickListener, DialogInterface.OnMultiChoiceClickListener {
554         private int mCurrentIndex;
555         private Set<Integer> mSelectedIndexSet;
556 
VCardSelectedListener(boolean multipleSelect)557         public VCardSelectedListener(boolean multipleSelect) {
558             mCurrentIndex = 0;
559             if (multipleSelect) {
560                 mSelectedIndexSet = new HashSet<Integer>();
561             }
562         }
563 
onClick(DialogInterface dialog, int which)564         public void onClick(DialogInterface dialog, int which) {
565             if (which == DialogInterface.BUTTON_POSITIVE) {
566                 if (mSelectedIndexSet != null) {
567                     List<VCardFile> selectedVCardFileList = new ArrayList<VCardFile>();
568                     final int size = mAllVCardFileList.size();
569                     // We'd like to sort the files by its index, so we do not use Set iterator.
570                     for (int i = 0; i < size; i++) {
571                         if (mSelectedIndexSet.contains(i)) {
572                             selectedVCardFileList.add(mAllVCardFileList.get(i));
573                         }
574                     }
575                     importVCardFromSDCard(selectedVCardFileList);
576                 } else {
577                     importVCardFromSDCard(mAllVCardFileList.get(mCurrentIndex));
578                 }
579             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
580                 finish();
581             } else {
582                 // Some file is selected.
583                 mCurrentIndex = which;
584                 if (mSelectedIndexSet != null) {
585                     if (mSelectedIndexSet.contains(which)) {
586                         mSelectedIndexSet.remove(which);
587                     } else {
588                         mSelectedIndexSet.add(which);
589                     }
590                 }
591             }
592         }
593 
onClick(DialogInterface dialog, int which, boolean isChecked)594         public void onClick(DialogInterface dialog, int which, boolean isChecked) {
595             if (mSelectedIndexSet == null || (mSelectedIndexSet.contains(which) == isChecked)) {
596                 Log.e(LOG_TAG, String.format("Inconsist state in index %d (%s)", which,
597                         mAllVCardFileList.get(which).getCanonicalPath()));
598             } else {
599                 onClick(dialog, which);
600             }
601         }
602     }
603 
604     /**
605      * Thread scanning VCard from SDCard. After scanning, the dialog which lets a user select
606      * a vCard file is shown. After the choice, VCardReadThread starts running.
607      */
608     private class VCardScanThread extends Thread implements OnCancelListener, OnClickListener {
609         private boolean mCanceled;
610         private boolean mGotIOException;
611         private File mRootDirectory;
612 
613         // To avoid recursive link.
614         private Set<String> mCheckedPaths;
615         private PowerManager.WakeLock mWakeLock;
616 
617         private class CanceledException extends Exception {
618         }
619 
VCardScanThread(File sdcardDirectory)620         public VCardScanThread(File sdcardDirectory) {
621             mCanceled = false;
622             mGotIOException = false;
623             mRootDirectory = sdcardDirectory;
624             mCheckedPaths = new HashSet<String>();
625             PowerManager powerManager = (PowerManager)ImportVCardActivity.this.getSystemService(
626                     Context.POWER_SERVICE);
627             mWakeLock = powerManager.newWakeLock(
628                     PowerManager.SCREEN_DIM_WAKE_LOCK |
629                     PowerManager.ON_AFTER_RELEASE, LOG_TAG);
630         }
631 
632         @Override
run()633         public void run() {
634             mAllVCardFileList = new Vector<VCardFile>();
635             try {
636                 mWakeLock.acquire();
637                 getVCardFileRecursively(mRootDirectory);
638             } catch (CanceledException e) {
639                 mCanceled = true;
640             } catch (IOException e) {
641                 mGotIOException = true;
642             } finally {
643                 mWakeLock.release();
644             }
645 
646             if (mCanceled) {
647                 mAllVCardFileList = null;
648             }
649 
650             mProgressDialogForScanVCard.dismiss();
651             mProgressDialogForScanVCard = null;
652 
653             if (mGotIOException) {
654                 runOnUiThread(new DialogDisplayer(R.id.dialog_io_exception));
655             } else if (mCanceled) {
656                 finish();
657             } else {
658                 int size = mAllVCardFileList.size();
659                 final Context context = ImportVCardActivity.this;
660                 if (size == 0) {
661                     runOnUiThread(new DialogDisplayer(R.id.dialog_vcard_not_found));
662                 } else {
663                     startVCardSelectAndImport();
664                 }
665             }
666         }
667 
getVCardFileRecursively(File directory)668         private void getVCardFileRecursively(File directory)
669                 throws CanceledException, IOException {
670             if (mCanceled) {
671                 throw new CanceledException();
672             }
673 
674             // e.g. secured directory may return null toward listFiles().
675             final File[] files = directory.listFiles();
676             if (files == null) {
677                 final String currentDirectoryPath = directory.getCanonicalPath();
678                 final String secureDirectoryPath =
679                         mRootDirectory.getCanonicalPath().concat(SECURE_DIRECTORY_NAME);
680                 if (!TextUtils.equals(currentDirectoryPath, secureDirectoryPath)) {
681                     Log.w(LOG_TAG, "listFiles() returned null (directory: " + directory + ")");
682                 }
683                 return;
684             }
685             for (File file : directory.listFiles()) {
686                 if (mCanceled) {
687                     throw new CanceledException();
688                 }
689                 String canonicalPath = file.getCanonicalPath();
690                 if (mCheckedPaths.contains(canonicalPath)) {
691                     continue;
692                 }
693 
694                 mCheckedPaths.add(canonicalPath);
695 
696                 if (file.isDirectory()) {
697                     getVCardFileRecursively(file);
698                 } else if (canonicalPath.toLowerCase().endsWith(".vcf") &&
699                         file.canRead()){
700                     String fileName = file.getName();
701                     VCardFile vcardFile = new VCardFile(
702                             fileName, canonicalPath, file.lastModified());
703                     mAllVCardFileList.add(vcardFile);
704                 }
705             }
706         }
707 
onCancel(DialogInterface dialog)708         public void onCancel(DialogInterface dialog) {
709             mCanceled = true;
710         }
711 
onClick(DialogInterface dialog, int which)712         public void onClick(DialogInterface dialog, int which) {
713             if (which == DialogInterface.BUTTON_NEGATIVE) {
714                 mCanceled = true;
715             }
716         }
717     }
718 
startVCardSelectAndImport()719     private void startVCardSelectAndImport() {
720         int size = mAllVCardFileList.size();
721         if (getResources().getBoolean(R.bool.config_import_all_vcard_from_sdcard_automatically) ||
722                 size == 1) {
723             importVCardFromSDCard(mAllVCardFileList);
724         } else if (getResources().getBoolean(R.bool.config_allow_users_select_all_vcard_import)) {
725             runOnUiThread(new DialogDisplayer(R.id.dialog_select_import_type));
726         } else {
727             runOnUiThread(new DialogDisplayer(R.id.dialog_select_one_vcard));
728         }
729     }
730 
importVCardFromSDCard(final List<VCardFile> selectedVCardFileList)731     private void importVCardFromSDCard(final List<VCardFile> selectedVCardFileList) {
732         final int size = selectedVCardFileList.size();
733         String[] uriStrings = new String[size];
734         int i = 0;
735         for (VCardFile vcardFile : selectedVCardFileList) {
736             uriStrings[i] = "file://" + vcardFile.getCanonicalPath();
737             i++;
738         }
739         importVCard(uriStrings);
740     }
741 
importVCardFromSDCard(final VCardFile vcardFile)742     private void importVCardFromSDCard(final VCardFile vcardFile) {
743         importVCard(new Uri[] {Uri.parse("file://" + vcardFile.getCanonicalPath())});
744     }
745 
importVCard(final Uri uri)746     private void importVCard(final Uri uri) {
747         importVCard(new Uri[] {uri});
748     }
749 
importVCard(final String[] uriStrings)750     private void importVCard(final String[] uriStrings) {
751         final int length = uriStrings.length;
752         final Uri[] uris = new Uri[length];
753         for (int i = 0; i < length; i++) {
754             uris[i] = Uri.parse(uriStrings[i]);
755         }
756         importVCard(uris);
757     }
758 
importVCard(final Uri[] uris)759     private void importVCard(final Uri[] uris) {
760         runOnUiThread(new Runnable() {
761             @Override
762             public void run() {
763                 mVCardCacheThread = new VCardCacheThread(uris);
764                 mListener = new NotificationImportExportListener(ImportVCardActivity.this);
765                 showDialog(R.id.dialog_cache_vcard);
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 == 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 != 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     @Override
onConfigurationChanged(Configuration newConfig)1000     public void onConfigurationChanged(Configuration newConfig) {
1001         super.onConfigurationChanged(newConfig);
1002         // This Activity should finish itself on orientation change, and give the main screen back
1003         // to the caller Activity.
1004         finish();
1005     }
1006 
1007     /**
1008      * Scans vCard in external storage (typically SDCard) and tries to import it.
1009      * - When there's no SDCard available, an error dialog is shown.
1010      * - When multiple vCard files are available, asks a user to select one.
1011      */
doScanExternalStorageAndImportVCard()1012     private void doScanExternalStorageAndImportVCard() {
1013         // TODO: should use getExternalStorageState().
1014         final File file = Environment.getExternalStorageDirectory();
1015         if (!file.exists() || !file.isDirectory() || !file.canRead()) {
1016             showDialog(R.id.dialog_sdcard_not_found);
1017         } else {
1018             mVCardScanThread = new VCardScanThread(file);
1019             showDialog(R.id.dialog_searching_vcard);
1020         }
1021     }
1022 
showFailureNotification(int reasonId)1023     /* package */ void showFailureNotification(int reasonId) {
1024         final NotificationManager notificationManager =
1025                 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
1026         final Notification notification =
1027                 NotificationImportExportListener.constructImportFailureNotification(
1028                         ImportVCardActivity.this,
1029                         getString(reasonId));
1030         notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
1031                 FAILURE_NOTIFICATION_ID, notification);
1032         mHandler.post(new Runnable() {
1033             @Override
1034             public void run() {
1035                 Toast.makeText(ImportVCardActivity.this,
1036                         getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
1037             }
1038         });
1039     }
1040 }
1041