blob: 6d486e3ee28fa1cc95886cd1fe4f0c1ed43df38b [file] [log] [blame]
Chiao Chengd80c4342012-12-03 17:15:58 -08001/*
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
Gary Mai69c182a2016-12-05 13:07:03 -080017package com.android.contacts.vcard;
Chiao Chengd80c4342012-12-03 17:15:58 -080018
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.Notification;
23import android.app.NotificationManager;
24import android.app.ProgressDialog;
Walter Jang6321e5a2015-07-14 10:56:03 -070025import android.content.ClipData;
Chiao Chengd80c4342012-12-03 17:15:58 -080026import android.content.ComponentName;
27import android.content.ContentResolver;
28import android.content.Context;
29import android.content.DialogInterface;
Chiao Chengd80c4342012-12-03 17:15:58 -080030import android.content.Intent;
31import android.content.ServiceConnection;
32import android.database.Cursor;
33import android.net.Uri;
34import android.os.Bundle;
Chiao Chengd80c4342012-12-03 17:15:58 -080035import android.os.Handler;
36import android.os.IBinder;
37import android.os.PowerManager;
38import android.provider.OpenableColumns;
Chiao Chengd80c4342012-12-03 17:15:58 -080039import android.text.TextUtils;
Chiao Chengd80c4342012-12-03 17:15:58 -080040import android.util.Log;
41import android.widget.Toast;
42
Arthur Wang3f6a2442016-12-05 14:51:59 -080043import com.android.contacts.R;
Gary Mai0a49afa2016-12-05 15:53:58 -080044import com.android.contacts.activities.RequestImportVCardPermissionsActivity;
Gary Mai69c182a2016-12-05 13:07:03 -080045import com.android.contacts.model.AccountTypeManager;
46import com.android.contacts.model.account.AccountWithDataSet;
Walter Jang3a0b4832016-10-12 11:02:54 -070047import com.android.contactsbind.FeedbackHelper;
Chiao Chengd80c4342012-12-03 17:15:58 -080048import com.android.vcard.VCardEntryCounter;
49import com.android.vcard.VCardParser;
50import com.android.vcard.VCardParser_V21;
51import com.android.vcard.VCardParser_V30;
52import com.android.vcard.VCardSourceDetector;
53import com.android.vcard.exception.VCardException;
54import com.android.vcard.exception.VCardNestedException;
55import com.android.vcard.exception.VCardVersionException;
56
57import java.io.ByteArrayInputStream;
58import java.io.File;
59import java.io.IOException;
60import java.io.InputStream;
61import java.nio.ByteBuffer;
62import java.nio.channels.Channels;
63import java.nio.channels.ReadableByteChannel;
64import java.nio.channels.WritableByteChannel;
Chiao Chengd80c4342012-12-03 17:15:58 -080065import java.util.ArrayList;
66import java.util.Arrays;
Chiao Chengd80c4342012-12-03 17:15:58 -080067import java.util.List;
Chiao Chengd80c4342012-12-03 17:15:58 -080068
69/**
70 * The class letting users to import vCard. This includes the UI part for letting them select
71 * an Account and posssibly a file if there's no Uri is given from its caller Activity.
72 *
73 * Note that this Activity assumes that the instance is a "one-shot Activity", which will be
74 * finished (with the method {@link Activity#finish()}) after the import and never reuse
75 * any Dialog in the instance. So this code is careless about the management around managed
76 * dialogs stuffs (like how onCreateDialog() is used).
77 */
Walter Jang915ccdd2016-10-24 12:18:20 -070078public class ImportVCardActivity extends Activity implements ImportVCardDialogFragment.Listener {
Chiao Chengd80c4342012-12-03 17:15:58 -080079 private static final String LOG_TAG = "VCardImport";
80
81 private static final int SELECT_ACCOUNT = 0;
82
Chiao Chengd80c4342012-12-03 17:15:58 -080083 /* package */ final static int VCARD_VERSION_AUTO_DETECT = 0;
84 /* package */ final static int VCARD_VERSION_V21 = 1;
85 /* package */ final static int VCARD_VERSION_V30 = 2;
86
Walter Jang6321e5a2015-07-14 10:56:03 -070087 private static final int REQUEST_OPEN_DOCUMENT = 100;
Chiao Chengd80c4342012-12-03 17:15:58 -080088
89 /**
90 * Notification id used when error happened before sending an import request to VCardServer.
91 */
92 private static final int FAILURE_NOTIFICATION_ID = 1;
93
Tingting Wang1fdb8902015-09-08 16:34:09 -070094 private static final String LOCAL_TMP_FILE_NAME_EXTRA =
Gary Mai69c182a2016-12-05 13:07:03 -080095 "com.android.contacts.vcard.LOCAL_TMP_FILE_NAME";
Tingting Wang1fdb8902015-09-08 16:34:09 -070096
Tingting Wang7e0d93c2015-09-16 10:47:57 -070097 private static final String SOURCE_URI_DISPLAY_NAME =
Gary Mai69c182a2016-12-05 13:07:03 -080098 "com.android.contacts.vcard.SOURCE_URI_DISPLAY_NAME";
Tingting Wang7e0d93c2015-09-16 10:47:57 -070099
Tingting Wang683d7702016-02-04 15:54:50 -0800100 private static final String STORAGE_VCARD_URI_PREFIX = "file:///storage";
Tingting Wanga6139f92015-11-30 12:44:27 -0800101
Chiao Chengd80c4342012-12-03 17:15:58 -0800102 private AccountWithDataSet mAccount;
103
Chiao Chengd80c4342012-12-03 17:15:58 -0800104 private ProgressDialog mProgressDialogForCachingVCard;
105
Chiao Chengd80c4342012-12-03 17:15:58 -0800106 private VCardCacheThread mVCardCacheThread;
107 private ImportRequestConnection mConnection;
108 /* package */ VCardImportExportListener mListener;
109
110 private String mErrorMessage;
111
112 private Handler mHandler = new Handler();
113
Chiao Chengd80c4342012-12-03 17:15:58 -0800114 // Runs on the UI thread.
115 private class DialogDisplayer implements Runnable {
116 private final int mResId;
117 public DialogDisplayer(int resId) {
118 mResId = resId;
119 }
120 public DialogDisplayer(String errorMessage) {
121 mResId = R.id.dialog_error_with_message;
122 mErrorMessage = errorMessage;
123 }
124 @Override
125 public void run() {
126 if (!isFinishing()) {
127 showDialog(mResId);
128 }
129 }
130 }
131
132 private class CancelListener
133 implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
134 @Override
135 public void onClick(DialogInterface dialog, int which) {
136 finish();
137 }
138 @Override
139 public void onCancel(DialogInterface dialog) {
140 finish();
141 }
142 }
143
144 private CancelListener mCancelListener = new CancelListener();
145
146 private class ImportRequestConnection implements ServiceConnection {
147 private VCardService mService;
148
149 public void sendImportRequest(final List<ImportRequest> requests) {
150 Log.i(LOG_TAG, "Send an import request");
151 mService.handleImportRequest(requests, mListener);
152 }
153
154 @Override
155 public void onServiceConnected(ComponentName name, IBinder binder) {
156 mService = ((VCardService.MyBinder) binder).getService();
157 Log.i(LOG_TAG,
158 String.format("Connected to VCardService. Kick a vCard cache thread (uri: %s)",
159 Arrays.toString(mVCardCacheThread.getSourceUris())));
160 mVCardCacheThread.start();
161 }
162
163 @Override
164 public void onServiceDisconnected(ComponentName name) {
165 Log.i(LOG_TAG, "Disconnected from VCardService");
166 }
167 }
168
169 /**
170 * Caches given vCard files into a local directory, and sends actual import request to
171 * {@link VCardService}.
172 *
173 * We need to cache given files into local storage. One of reasons is that some data (as Uri)
174 * may have special permissions. Callers may allow only this Activity to access that content,
175 * not what this Activity launched (like {@link VCardService}).
176 */
177 private class VCardCacheThread extends Thread
178 implements DialogInterface.OnCancelListener {
179 private boolean mCanceled;
180 private PowerManager.WakeLock mWakeLock;
181 private VCardParser mVCardParser;
182 private final Uri[] mSourceUris; // Given from a caller.
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700183 private final String[] mSourceDisplayNames; // Display names for each Uri in mSourceUris.
Chiao Chengd80c4342012-12-03 17:15:58 -0800184 private final byte[] mSource;
185 private final String mDisplayName;
186
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700187 public VCardCacheThread(final Uri[] sourceUris, String[] sourceDisplayNames) {
Chiao Chengd80c4342012-12-03 17:15:58 -0800188 mSourceUris = sourceUris;
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700189 mSourceDisplayNames = sourceDisplayNames;
Chiao Chengd80c4342012-12-03 17:15:58 -0800190 mSource = null;
191 final Context context = ImportVCardActivity.this;
192 final PowerManager powerManager =
193 (PowerManager)context.getSystemService(Context.POWER_SERVICE);
194 mWakeLock = powerManager.newWakeLock(
195 PowerManager.SCREEN_DIM_WAKE_LOCK |
196 PowerManager.ON_AFTER_RELEASE, LOG_TAG);
197 mDisplayName = null;
198 }
199
200 @Override
201 public void finalize() {
202 if (mWakeLock != null && mWakeLock.isHeld()) {
203 Log.w(LOG_TAG, "WakeLock is being held.");
204 mWakeLock.release();
205 }
206 }
207
208 @Override
209 public void run() {
210 Log.i(LOG_TAG, "vCard cache thread starts running.");
211 if (mConnection == null) {
212 throw new NullPointerException("vCard cache thread must be launched "
213 + "after a service connection is established");
214 }
215
216 mWakeLock.acquire();
217 try {
218 if (mCanceled == true) {
219 Log.i(LOG_TAG, "vCard cache operation is canceled.");
220 return;
221 }
222
223 final Context context = ImportVCardActivity.this;
224 // Uris given from caller applications may not be opened twice: consider when
225 // it is not from local storage (e.g. "file:///...") but from some special
226 // provider (e.g. "content://...").
227 // Thus we have to once copy the content of Uri into local storage, and read
228 // it after it.
229 //
230 // We may be able to read content of each vCard file during copying them
231 // to local storage, but currently vCard code does not allow us to do so.
232 int cache_index = 0;
233 ArrayList<ImportRequest> requests = new ArrayList<ImportRequest>();
234 if (mSource != null) {
235 try {
236 requests.add(constructImportRequest(mSource, null, mDisplayName));
237 } catch (VCardException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700238 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG,
239 "Failed to cache vcard", e);
Chiao Chengd80c4342012-12-03 17:15:58 -0800240 showFailureNotification(R.string.fail_reason_not_supported);
241 return;
242 }
243 } else {
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700244 int i = 0;
Chiao Chengd80c4342012-12-03 17:15:58 -0800245 for (Uri sourceUri : mSourceUris) {
Chiao Chengd80c4342012-12-03 17:15:58 -0800246 if (mCanceled) {
247 Log.i(LOG_TAG, "vCard cache operation is canceled.");
248 break;
249 }
Chiao Chengd80c4342012-12-03 17:15:58 -0800250
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700251 String sourceDisplayName = mSourceDisplayNames[i++];
Chiao Chengd80c4342012-12-03 17:15:58 -0800252
253 final ImportRequest request;
254 try {
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700255 request = constructImportRequest(null, sourceUri, sourceDisplayName);
Chiao Chengd80c4342012-12-03 17:15:58 -0800256 } catch (VCardException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700257 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG,
258 "Failed to cache vcard", e);
Chiao Chengd80c4342012-12-03 17:15:58 -0800259 showFailureNotification(R.string.fail_reason_not_supported);
260 return;
261 } catch (IOException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700262 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG,
263 "Failed to cache vcard", e);
Chiao Chengd80c4342012-12-03 17:15:58 -0800264 showFailureNotification(R.string.fail_reason_io_error);
265 return;
266 }
267 if (mCanceled) {
268 Log.i(LOG_TAG, "vCard cache operation is canceled.");
269 return;
270 }
271 requests.add(request);
272 }
273 }
274 if (!requests.isEmpty()) {
275 mConnection.sendImportRequest(requests);
276 } else {
277 Log.w(LOG_TAG, "Empty import requests. Ignore it.");
278 }
279 } catch (OutOfMemoryError e) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700280 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG,
281 "OutOfMemoryError occured during caching vCard", e);
Chiao Chengd80c4342012-12-03 17:15:58 -0800282 System.gc();
283 runOnUiThread(new DialogDisplayer(
284 getString(R.string.fail_reason_low_memory_during_import)));
285 } catch (IOException e) {
Walter Jang3a0b4832016-10-12 11:02:54 -0700286 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG,
287 "IOException during caching vCard", e);
Chiao Chengd80c4342012-12-03 17:15:58 -0800288 runOnUiThread(new DialogDisplayer(
289 getString(R.string.fail_reason_io_error)));
290 } finally {
291 Log.i(LOG_TAG, "Finished caching vCard.");
292 mWakeLock.release();
yaolu455d0822016-11-16 14:56:11 -0800293 try {
294 unbindService(mConnection);
295 } catch (IllegalArgumentException e) {
296 FeedbackHelper.sendFeedback(ImportVCardActivity.this, LOG_TAG,
297 "Cannot unbind service connection", e);
298 }
Chiao Chengd80c4342012-12-03 17:15:58 -0800299 mProgressDialogForCachingVCard.dismiss();
300 mProgressDialogForCachingVCard = null;
301 finish();
302 }
303 }
304
305 /**
Chiao Chengd80c4342012-12-03 17:15:58 -0800306 * Reads localDataUri (possibly multiple times) and constructs {@link ImportRequest} from
307 * its content.
308 *
309 * @arg localDataUri Uri actually used for the import. Should be stored in
310 * app local storage, as we cannot guarantee other types of Uris can be read
311 * multiple times. This variable populates {@link ImportRequest#uri}.
312 * @arg displayName Used for displaying information to the user. This variable populates
313 * {@link ImportRequest#displayName}.
314 */
315 private ImportRequest constructImportRequest(final byte[] data,
316 final Uri localDataUri, final String displayName)
317 throws IOException, VCardException {
318 final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
319 VCardEntryCounter counter = null;
320 VCardSourceDetector detector = null;
321 int vcardVersion = VCARD_VERSION_V21;
322 try {
323 boolean shouldUseV30 = false;
324 InputStream is;
325 if (data != null) {
326 is = new ByteArrayInputStream(data);
327 } else {
328 is = resolver.openInputStream(localDataUri);
329 }
330 mVCardParser = new VCardParser_V21();
331 try {
332 counter = new VCardEntryCounter();
333 detector = new VCardSourceDetector();
334 mVCardParser.addInterpreter(counter);
335 mVCardParser.addInterpreter(detector);
336 mVCardParser.parse(is);
337 } catch (VCardVersionException e1) {
338 try {
339 is.close();
340 } catch (IOException e) {
341 }
342
343 shouldUseV30 = true;
344 if (data != null) {
345 is = new ByteArrayInputStream(data);
346 } else {
347 is = resolver.openInputStream(localDataUri);
348 }
349 mVCardParser = new VCardParser_V30();
350 try {
351 counter = new VCardEntryCounter();
352 detector = new VCardSourceDetector();
353 mVCardParser.addInterpreter(counter);
354 mVCardParser.addInterpreter(detector);
355 mVCardParser.parse(is);
356 } catch (VCardVersionException e2) {
357 throw new VCardException("vCard with unspported version.");
358 }
359 } finally {
360 if (is != null) {
361 try {
362 is.close();
363 } catch (IOException e) {
364 }
365 }
366 }
367
368 vcardVersion = shouldUseV30 ? VCARD_VERSION_V30 : VCARD_VERSION_V21;
369 } catch (VCardNestedException e) {
370 Log.w(LOG_TAG, "Nested Exception is found (it may be false-positive).");
371 // Go through without throwing the Exception, as we may be able to detect the
372 // version before it
373 }
374 return new ImportRequest(mAccount,
375 data, localDataUri, displayName,
376 detector.getEstimatedType(),
377 detector.getEstimatedCharset(),
378 vcardVersion, counter.getCount());
379 }
380
381 public Uri[] getSourceUris() {
382 return mSourceUris;
383 }
384
385 public void cancel() {
386 mCanceled = true;
387 if (mVCardParser != null) {
388 mVCardParser.cancel();
389 }
390 }
391
392 @Override
393 public void onCancel(DialogInterface dialog) {
394 Log.i(LOG_TAG, "Cancel request has come. Abort caching vCard.");
395 cancel();
396 }
397 }
398
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700399 private void importVCard(final Uri uri, final String sourceDisplayName) {
400 importVCard(new Uri[] {uri}, new String[] {sourceDisplayName});
Chiao Chengd80c4342012-12-03 17:15:58 -0800401 }
402
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700403 private void importVCard(final Uri[] uris, final String[] sourceDisplayNames) {
Chiao Chengd80c4342012-12-03 17:15:58 -0800404 runOnUiThread(new Runnable() {
405 @Override
406 public void run() {
407 if (!isFinishing()) {
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700408 mVCardCacheThread = new VCardCacheThread(uris, sourceDisplayNames);
Chiao Chengd80c4342012-12-03 17:15:58 -0800409 mListener = new NotificationImportExportListener(ImportVCardActivity.this);
410 showDialog(R.id.dialog_cache_vcard);
411 }
412 }
413 });
414 }
415
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700416 private String getDisplayName(Uri sourceUri) {
Tingting Wang0d533522015-12-14 11:53:01 -0800417 if (sourceUri == null) {
418 return null;
419 }
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700420 final ContentResolver resolver = ImportVCardActivity.this.getContentResolver();
421 String displayName = null;
422 Cursor cursor = null;
423 // Try to get a display name from the given Uri. If it fails, we just
424 // pick up the last part of the Uri.
425 try {
426 cursor = resolver.query(sourceUri,
427 new String[] { OpenableColumns.DISPLAY_NAME },
428 null, null, null);
429 if (cursor != null && cursor.getCount() > 0 && cursor.moveToFirst()) {
430 if (cursor.getCount() > 1) {
431 Log.w(LOG_TAG, "Unexpected multiple rows: "
432 + cursor.getCount());
433 }
434 int index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
435 if (index >= 0) {
436 displayName = cursor.getString(index);
437 }
438 }
439 } finally {
440 if (cursor != null) {
441 cursor.close();
442 }
443 }
444 if (TextUtils.isEmpty(displayName)){
445 displayName = sourceUri.getLastPathSegment();
446 }
447 return displayName;
448 }
449
Tingting Wang1fdb8902015-09-08 16:34:09 -0700450 /**
451 * Copy the content of sourceUri to the destination.
452 */
453 private Uri copyTo(final Uri sourceUri, String filename) throws IOException {
454 Log.i(LOG_TAG, String.format("Copy a Uri to app local storage (%s -> %s)",
455 sourceUri, filename));
456 final Context context = ImportVCardActivity.this;
457 final ContentResolver resolver = context.getContentResolver();
458 ReadableByteChannel inputChannel = null;
459 WritableByteChannel outputChannel = null;
460 Uri destUri = null;
461 try {
462 inputChannel = Channels.newChannel(resolver.openInputStream(sourceUri));
463 destUri = Uri.parse(context.getFileStreamPath(filename).toURI().toString());
464 outputChannel = context.openFileOutput(filename, Context.MODE_PRIVATE).getChannel();
465 final ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
466 while (inputChannel.read(buffer) != -1) {
467 buffer.flip();
468 outputChannel.write(buffer);
469 buffer.compact();
470 }
471 buffer.flip();
472 while (buffer.hasRemaining()) {
473 outputChannel.write(buffer);
474 }
475 } finally {
476 if (inputChannel != null) {
477 try {
478 inputChannel.close();
479 } catch (IOException e) {
480 Log.w(LOG_TAG, "Failed to close inputChannel.");
481 }
482 }
483 if (outputChannel != null) {
484 try {
485 outputChannel.close();
486 } catch(IOException e) {
487 Log.w(LOG_TAG, "Failed to close outputChannel");
488 }
489 }
490 }
491 return destUri;
492 }
493
494 /**
495 * Reads the file from {@param sourceUri} and copies it to local cache file.
496 * Returns the local file name which stores the file from sourceUri.
497 */
498 private String readUriToLocalFile(Uri sourceUri) {
499 // Read the uri to local first.
500 int cache_index = 0;
501 String localFilename = null;
502 // Note: caches are removed by VCardService.
503 while (true) {
504 localFilename = VCardService.CACHE_FILE_PREFIX + cache_index + ".vcf";
505 final File file = getFileStreamPath(localFilename);
506 if (!file.exists()) {
507 break;
508 } else {
509 if (cache_index == Integer.MAX_VALUE) {
510 throw new RuntimeException("Exceeded cache limit");
511 }
512 cache_index++;
513 }
514 }
515 try {
516 copyTo(sourceUri, localFilename);
Walter Jang3a0b4832016-10-12 11:02:54 -0700517 } catch (IOException|SecurityException e) {
518 FeedbackHelper.sendFeedback(this, LOG_TAG, "Failed to copy vcard to local file", e);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700519 showFailureNotification(R.string.fail_reason_io_error);
520 return null;
521 }
522
523 if (localFilename == null) {
524 Log.e(LOG_TAG, "Cannot load uri to local storage.");
525 showFailureNotification(R.string.fail_reason_io_error);
526 return null;
527 }
528
529 return localFilename;
530 }
531
532 private Uri readUriToLocalUri(Uri sourceUri) {
533 final String fileName = readUriToLocalFile(sourceUri);
Jay Shrauner68909452015-12-14 11:09:31 -0800534 if (fileName == null) {
535 return null;
536 }
Tingting Wang1fdb8902015-09-08 16:34:09 -0700537 return Uri.parse(getFileStreamPath(fileName).toURI().toString());
538 }
539
Tingting Wang683d7702016-02-04 15:54:50 -0800540 // Returns true if uri is from Storage.
541 private boolean isStorageUri(Uri uri) {
542 return uri != null && uri.toString().startsWith(STORAGE_VCARD_URI_PREFIX);
Tingting Wanga6139f92015-11-30 12:44:27 -0800543 }
544
Chiao Chengd80c4342012-12-03 17:15:58 -0800545 @Override
546 protected void onCreate(Bundle bundle) {
547 super.onCreate(bundle);
548
Tingting Wang1fdb8902015-09-08 16:34:09 -0700549 Uri sourceUri = getIntent().getData();
Tingting Wang683d7702016-02-04 15:54:50 -0800550
551 // Reading uris from non-storage needs the permission granted from the source intent,
Tingting Wanga6139f92015-11-30 12:44:27 -0800552 // instead of permissions from RequestImportVCardPermissionActivity. So skipping requesting
Tingting Wang683d7702016-02-04 15:54:50 -0800553 // permissions from RequestImportVCardPermissionActivity for uris from non-storage source.
Walter Jang915ccdd2016-10-24 12:18:20 -0700554 if (isStorageUri(sourceUri) && RequestImportVCardPermissionsActivity
555 .startPermissionActivity(this, isCallerSelf(this))) {
Tingting Wang5da0bdf2015-11-17 21:53:13 -0800556 return;
557 }
558
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700559 String sourceDisplayName = null;
Tingting Wang1fdb8902015-09-08 16:34:09 -0700560 if (sourceUri != null) {
561 // Read the uri to local first.
562 String localTmpFileName = getIntent().getStringExtra(LOCAL_TMP_FILE_NAME_EXTRA);
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700563 sourceDisplayName = getIntent().getStringExtra(SOURCE_URI_DISPLAY_NAME);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700564 if (TextUtils.isEmpty(localTmpFileName)) {
565 localTmpFileName = readUriToLocalFile(sourceUri);
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700566 sourceDisplayName = getDisplayName(sourceUri);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700567 if (localTmpFileName == null) {
568 Log.e(LOG_TAG, "Cannot load uri to local storage.");
569 showFailureNotification(R.string.fail_reason_io_error);
570 return;
571 }
572 getIntent().putExtra(LOCAL_TMP_FILE_NAME_EXTRA, localTmpFileName);
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700573 getIntent().putExtra(SOURCE_URI_DISPLAY_NAME, sourceDisplayName);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700574 }
575 sourceUri = Uri.parse(getFileStreamPath(localTmpFileName).toURI().toString());
576 }
577
Tingting Wang683d7702016-02-04 15:54:50 -0800578 // Always request required permission for contacts before importing the vcard.
Walter Jang915ccdd2016-10-24 12:18:20 -0700579 if (RequestImportVCardPermissionsActivity.startPermissionActivity(this,
580 isCallerSelf(this))) {
Tingting Wang683d7702016-02-04 15:54:50 -0800581 return;
582 }
583
Chiao Chengd80c4342012-12-03 17:15:58 -0800584 String accountName = null;
585 String accountType = null;
586 String dataSet = null;
587 final Intent intent = getIntent();
588 if (intent != null) {
589 accountName = intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME);
590 accountType = intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE);
591 dataSet = intent.getStringExtra(SelectAccountActivity.DATA_SET);
592 } else {
593 Log.e(LOG_TAG, "intent does not exist");
594 }
595
596 if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
597 mAccount = new AccountWithDataSet(accountName, accountType, dataSet);
598 } else {
599 final AccountTypeManager accountTypes = AccountTypeManager.getInstance(this);
600 final List<AccountWithDataSet> accountList = accountTypes.getAccounts(true);
601 if (accountList.size() == 0) {
602 mAccount = null;
603 } else if (accountList.size() == 1) {
604 mAccount = accountList.get(0);
605 } else {
606 startActivityForResult(new Intent(this, SelectAccountActivity.class),
607 SELECT_ACCOUNT);
608 return;
609 }
610 }
611
Walter Jang915ccdd2016-10-24 12:18:20 -0700612 if (isCallerSelf(this)) {
613 startImport(sourceUri, sourceDisplayName);
614 } else {
615 ImportVCardDialogFragment.show(this, sourceUri, sourceDisplayName);
616 }
617 }
618
619 private static boolean isCallerSelf(Activity activity) {
620 // {@link Activity#getCallingActivity()} is a safer alternative to
621 // {@link Activity#getCallingPackage()} that works around a
622 // framework bug where getCallingPackage() can sometimes return null even when the
623 // current activity *was* in fact launched via a startActivityForResult() call.
624 //
625 // (The bug happens if the task stack needs to be re-created by the framework after
626 // having been killed due to memory pressure or by the "Don't keep activities"
627 // developer option; see bug 7494866 for the full details.)
628 //
629 // Turns out that {@link Activity#getCallingActivity()} *does* return correct info
630 // even in the case where getCallingPackage() is broken, so the workaround is simply
631 // to get the package name from getCallingActivity().getPackageName() instead.
632 final ComponentName callingActivity = activity.getCallingActivity();
633 if (callingActivity == null) return false;
634 final String packageName = callingActivity.getPackageName();
635 if (packageName == null) return false;
636 return packageName.equals(activity.getApplicationContext().getPackageName());
637 }
638
639 @Override
640 public void onImportVCardConfirmed(Uri sourceUri, String sourceDisplayName) {
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700641 startImport(sourceUri, sourceDisplayName);
Chiao Chengd80c4342012-12-03 17:15:58 -0800642 }
643
644 @Override
Walter Jang915ccdd2016-10-24 12:18:20 -0700645 public void onImportVCardDenied() {
646 finish();
647 }
648
649 @Override
Chiao Chengd80c4342012-12-03 17:15:58 -0800650 public void onActivityResult(int requestCode, int resultCode, Intent intent) {
651 if (requestCode == SELECT_ACCOUNT) {
652 if (resultCode == Activity.RESULT_OK) {
653 mAccount = new AccountWithDataSet(
654 intent.getStringExtra(SelectAccountActivity.ACCOUNT_NAME),
655 intent.getStringExtra(SelectAccountActivity.ACCOUNT_TYPE),
656 intent.getStringExtra(SelectAccountActivity.DATA_SET));
Tingting Wang1fdb8902015-09-08 16:34:09 -0700657 final Uri sourceUri = getIntent().getData();
658 if (sourceUri == null) {
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700659 startImport(sourceUri, /* sourceDisplayName =*/ null);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700660 } else {
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700661 final String sourceDisplayName = getIntent().getStringExtra(
662 SOURCE_URI_DISPLAY_NAME);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700663 final String localFileName = getIntent().getStringExtra(
664 LOCAL_TMP_FILE_NAME_EXTRA);
665 final Uri localUri = Uri.parse(
666 getFileStreamPath(localFileName).toURI().toString());
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700667 startImport(localUri, sourceDisplayName);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700668 }
Chiao Chengd80c4342012-12-03 17:15:58 -0800669 } else {
670 if (resultCode != Activity.RESULT_CANCELED) {
671 Log.w(LOG_TAG, "Result code was not OK nor CANCELED: " + resultCode);
672 }
673 finish();
674 }
Walter Jang6321e5a2015-07-14 10:56:03 -0700675 } else if (requestCode == REQUEST_OPEN_DOCUMENT) {
676 if (resultCode == Activity.RESULT_OK) {
677 final ClipData clipData = intent.getClipData();
678 if (clipData != null) {
679 final ArrayList<Uri> uris = new ArrayList<>();
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700680 final ArrayList<String> sourceDisplayNames = new ArrayList<>();
Walter Jang6321e5a2015-07-14 10:56:03 -0700681 for (int i = 0; i < clipData.getItemCount(); i++) {
682 ClipData.Item item = clipData.getItemAt(i);
683 final Uri uri = item.getUri();
684 if (uri != null) {
Tingting Wang1fdb8902015-09-08 16:34:09 -0700685 final Uri localUri = readUriToLocalUri(uri);
Jay Shrauner68909452015-12-14 11:09:31 -0800686 if (localUri != null) {
687 final String sourceDisplayName = getDisplayName(uri);
688 uris.add(localUri);
689 sourceDisplayNames.add(sourceDisplayName);
690 }
Walter Jang6321e5a2015-07-14 10:56:03 -0700691 }
692 }
693 if (uris.isEmpty()) {
694 Log.w(LOG_TAG, "No vCard was selected for import");
695 finish();
696 } else {
697 Log.i(LOG_TAG, "Multiple vCards selected for import: " + uris);
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700698 importVCard(uris.toArray(new Uri[0]),
699 sourceDisplayNames.toArray(new String[0]));
Walter Jang6321e5a2015-07-14 10:56:03 -0700700 }
701 } else {
702 final Uri uri = intent.getData();
703 if (uri != null) {
704 Log.i(LOG_TAG, "vCard selected for import: " + uri);
Tingting Wang1fdb8902015-09-08 16:34:09 -0700705 final Uri localUri = readUriToLocalUri(uri);
Jay Shrauner68909452015-12-14 11:09:31 -0800706 if (localUri != null) {
Tingting Wang0d533522015-12-14 11:53:01 -0800707 final String sourceDisplayName = getDisplayName(uri);
Jay Shrauner68909452015-12-14 11:09:31 -0800708 importVCard(localUri, sourceDisplayName);
709 } else {
710 Log.w(LOG_TAG, "No local URI for vCard import");
711 finish();
712 }
Walter Jang6321e5a2015-07-14 10:56:03 -0700713 } else {
714 Log.w(LOG_TAG, "No vCard was selected for import");
715 finish();
716 }
717 }
718 } else {
719 if (resultCode != Activity.RESULT_CANCELED) {
720 Log.w(LOG_TAG, "Result code was not OK nor CANCELED" + resultCode);
721 }
722 finish();
723 }
Chiao Chengd80c4342012-12-03 17:15:58 -0800724 }
725 }
726
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700727 private void startImport(Uri uri, String sourceDisplayName) {
Chiao Chengd80c4342012-12-03 17:15:58 -0800728 // Handle inbound files
Chiao Chengd80c4342012-12-03 17:15:58 -0800729 if (uri != null) {
730 Log.i(LOG_TAG, "Starting vCard import using Uri " + uri);
Tingting Wang7e0d93c2015-09-16 10:47:57 -0700731 importVCard(uri, sourceDisplayName);
Chiao Chengd80c4342012-12-03 17:15:58 -0800732 } else {
733 Log.i(LOG_TAG, "Start vCard without Uri. The user will select vCard manually.");
Walter Jang54434262015-08-11 09:18:35 -0700734 final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
Walter Jang6321e5a2015-07-14 10:56:03 -0700735 intent.addCategory(Intent.CATEGORY_OPENABLE);
Walter Jang54434262015-08-11 09:18:35 -0700736 intent.setType(VCardService.X_VCARD_MIME_TYPE);
Walter Jang6321e5a2015-07-14 10:56:03 -0700737 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
Walter Jang6321e5a2015-07-14 10:56:03 -0700738 startActivityForResult(intent, REQUEST_OPEN_DOCUMENT);
Chiao Chengd80c4342012-12-03 17:15:58 -0800739 }
740 }
741
742 @Override
743 protected Dialog onCreateDialog(int resId, Bundle bundle) {
Sailesh Nepalb4a522e2016-02-20 21:17:32 -0800744 if (resId == R.id.dialog_cache_vcard) {
745 if (mProgressDialogForCachingVCard == null) {
746 final String title = getString(R.string.caching_vcard_title);
747 final String message = getString(R.string.caching_vcard_message);
748 mProgressDialogForCachingVCard = new ProgressDialog(this);
749 mProgressDialogForCachingVCard.setTitle(title);
750 mProgressDialogForCachingVCard.setMessage(message);
751 mProgressDialogForCachingVCard.setProgressStyle(ProgressDialog.STYLE_SPINNER);
752 mProgressDialogForCachingVCard.setOnCancelListener(mVCardCacheThread);
753 startVCardService();
Chiao Chengd80c4342012-12-03 17:15:58 -0800754 }
Sailesh Nepalb4a522e2016-02-20 21:17:32 -0800755 return mProgressDialogForCachingVCard;
756 } else if (resId == R.id.dialog_error_with_message) {
757 String message = mErrorMessage;
758 if (TextUtils.isEmpty(message)) {
759 Log.e(LOG_TAG, "Error message is null while it must not.");
760 message = getString(R.string.fail_reason_unknown);
Chiao Chengd80c4342012-12-03 17:15:58 -0800761 }
Sailesh Nepalb4a522e2016-02-20 21:17:32 -0800762 final AlertDialog.Builder builder = new AlertDialog.Builder(this)
763 .setTitle(getString(R.string.reading_vcard_failed_title))
764 .setIconAttribute(android.R.attr.alertDialogIcon)
765 .setMessage(message)
766 .setOnCancelListener(mCancelListener)
767 .setPositiveButton(android.R.string.ok, mCancelListener);
768 return builder.create();
Chiao Chengd80c4342012-12-03 17:15:58 -0800769 }
770
771 return super.onCreateDialog(resId, bundle);
772 }
773
774 /* package */ void startVCardService() {
775 mConnection = new ImportRequestConnection();
776
777 Log.i(LOG_TAG, "Bind to VCardService.");
778 // We don't want the service finishes itself just after this connection.
779 Intent intent = new Intent(this, VCardService.class);
780 startService(intent);
781 bindService(new Intent(this, VCardService.class),
782 mConnection, Context.BIND_AUTO_CREATE);
783 }
784
785 @Override
786 protected void onRestoreInstanceState(Bundle savedInstanceState) {
787 super.onRestoreInstanceState(savedInstanceState);
788 if (mProgressDialogForCachingVCard != null) {
789 Log.i(LOG_TAG, "Cache thread is still running. Show progress dialog again.");
790 showDialog(R.id.dialog_cache_vcard);
791 }
792 }
793
Chiao Chengd80c4342012-12-03 17:15:58 -0800794 /* package */ void showFailureNotification(int reasonId) {
795 final NotificationManager notificationManager =
796 (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
797 final Notification notification =
798 NotificationImportExportListener.constructImportFailureNotification(
799 ImportVCardActivity.this,
800 getString(reasonId));
801 notificationManager.notify(NotificationImportExportListener.FAILURE_NOTIFICATION_TAG,
802 FAILURE_NOTIFICATION_ID, notification);
803 mHandler.post(new Runnable() {
804 @Override
805 public void run() {
806 Toast.makeText(ImportVCardActivity.this,
807 getString(R.string.vcard_import_failed), Toast.LENGTH_LONG).show();
808 }
809 });
810 }
811}