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