1 /* 2 * Copyright (C) 2010 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 package com.android.contacts.vcard; 17 18 import android.accounts.Account; 19 import android.app.Notification; 20 import android.content.ContentResolver; 21 import android.net.Uri; 22 import android.util.Log; 23 24 import com.android.contactsbind.FeedbackHelper; 25 import com.android.vcard.VCardEntry; 26 import com.android.vcard.VCardEntryCommitter; 27 import com.android.vcard.VCardEntryConstructor; 28 import com.android.vcard.VCardEntryHandler; 29 import com.android.vcard.VCardInterpreter; 30 import com.android.vcard.VCardParser; 31 import com.android.vcard.VCardParser_V21; 32 import com.android.vcard.VCardParser_V30; 33 import com.android.vcard.exception.VCardException; 34 import com.android.vcard.exception.VCardNotSupportedException; 35 import com.android.vcard.exception.VCardVersionException; 36 37 import java.io.ByteArrayInputStream; 38 import java.io.IOException; 39 import java.io.InputStream; 40 import java.util.ArrayList; 41 import java.util.List; 42 43 /** 44 * Class for processing one import request from a user. Dropped after importing requested Uri(s). 45 * {@link VCardService} will create another object when there is another import request. 46 */ 47 public class ImportProcessor extends ProcessorBase implements VCardEntryHandler { 48 private static final String LOG_TAG = "VCardImport"; 49 private static final boolean DEBUG = VCardService.DEBUG; 50 51 private final VCardService mService; 52 private final ContentResolver mResolver; 53 private final ImportRequest mImportRequest; 54 private final int mJobId; 55 private final VCardImportExportListener mListener; 56 57 // TODO: remove and show appropriate message instead. 58 private final List<Uri> mFailedUris = new ArrayList<Uri>(); 59 60 private VCardParser mVCardParser; 61 62 private volatile boolean mCanceled; 63 private volatile boolean mDone; 64 65 private int mCurrentCount = 0; 66 private int mTotalCount = 0; 67 ImportProcessor(final VCardService service, final VCardImportExportListener listener, final ImportRequest request, final int jobId)68 public ImportProcessor(final VCardService service, final VCardImportExportListener listener, 69 final ImportRequest request, final int jobId) { 70 mService = service; 71 mResolver = mService.getContentResolver(); 72 mListener = listener; 73 74 mImportRequest = request; 75 mJobId = jobId; 76 } 77 78 @Override onStart()79 public void onStart() { 80 // do nothing 81 } 82 83 @Override onEnd()84 public void onEnd() { 85 // do nothing 86 } 87 88 @Override onEntryCreated(VCardEntry entry)89 public void onEntryCreated(VCardEntry entry) { 90 mCurrentCount++; 91 if (mListener != null) { 92 final Notification notification = mListener.onImportParsed(mImportRequest, mJobId, 93 entry, mCurrentCount, mTotalCount); 94 if (notification != null) { 95 mService.startForeground(mJobId, notification); 96 } 97 } 98 } 99 100 @Override getType()101 public final int getType() { 102 return VCardService.TYPE_IMPORT; 103 } 104 105 @Override run()106 public void run() { 107 // ExecutorService ignores RuntimeException, so we need to show it here. 108 try { 109 runInternal(); 110 111 if (isCancelled() && mListener != null) { 112 mListener.onImportCanceled(mImportRequest, mJobId); 113 } 114 } catch (OutOfMemoryError|RuntimeException e) { 115 FeedbackHelper.sendFeedback(mService, LOG_TAG, "Vcard import failed", e); 116 } finally { 117 synchronized (this) { 118 mDone = true; 119 } 120 } 121 } 122 runInternal()123 private void runInternal() { 124 Log.i(LOG_TAG, String.format("vCard import (id: %d) has started.", mJobId)); 125 final ImportRequest request = mImportRequest; 126 if (isCancelled()) { 127 Log.i(LOG_TAG, "Canceled before actually handling parameter (" + request.uri + ")"); 128 return; 129 } 130 final int[] possibleVCardVersions; 131 if (request.vcardVersion == ImportVCardActivity.VCARD_VERSION_AUTO_DETECT) { 132 /** 133 * Note: this code assumes that a given Uri is able to be opened more than once, 134 * which may not be true in certain conditions. 135 */ 136 possibleVCardVersions = new int[] { 137 ImportVCardActivity.VCARD_VERSION_V21, 138 ImportVCardActivity.VCARD_VERSION_V30 139 }; 140 } else { 141 possibleVCardVersions = new int[] { 142 request.vcardVersion 143 }; 144 } 145 146 final Uri uri = request.uri; 147 final Account account = request.account; 148 final int estimatedVCardType = request.estimatedVCardType; 149 final String estimatedCharset = request.estimatedCharset; 150 final int entryCount = request.entryCount; 151 mTotalCount += entryCount; 152 153 final VCardEntryConstructor constructor = 154 new VCardEntryConstructor(estimatedVCardType, account, estimatedCharset); 155 final VCardEntryCommitter committer = new VCardEntryCommitter(mResolver); 156 constructor.addEntryHandler(committer); 157 constructor.addEntryHandler(this); 158 159 InputStream is = null; 160 boolean successful = false; 161 try { 162 if (uri != null) { 163 Log.i(LOG_TAG, "start importing one vCard (Uri: " + uri + ")"); 164 is = mResolver.openInputStream(uri); 165 } else if (request.data != null){ 166 Log.i(LOG_TAG, "start importing one vCard (byte[])"); 167 is = new ByteArrayInputStream(request.data); 168 } 169 170 if (is != null) { 171 successful = readOneVCard(is, estimatedVCardType, estimatedCharset, constructor, 172 possibleVCardVersions); 173 } 174 } catch (IOException e) { 175 successful = false; 176 } finally { 177 if (is != null) { 178 try { 179 is.close(); 180 } catch (Exception e) { 181 // ignore 182 } 183 } 184 } 185 186 mService.handleFinishImportNotification(mJobId, successful); 187 188 if (successful) { 189 // TODO: successful becomes true even when cancelled. Should return more appropriate 190 // value 191 if (isCancelled()) { 192 Log.i(LOG_TAG, "vCard import has been canceled (uri: " + uri + ")"); 193 // Cancel notification will be done outside this method. 194 } else { 195 Log.i(LOG_TAG, "Successfully finished importing one vCard file: " + uri); 196 List<Uri> uris = committer.getCreatedUris(); 197 if (mListener != null) { 198 if (uris != null && uris.size() == 1) { 199 mListener.onImportFinished(mImportRequest, mJobId, uris.get(0)); 200 } else { 201 if (uris == null || uris.size() == 0) { 202 // Not critical, but suspicious. 203 Log.w(LOG_TAG, "Created Uris is null or 0 length " + 204 "though the creation itself is successful."); 205 } 206 mListener.onImportFinished(mImportRequest, mJobId, null); 207 } 208 } 209 } 210 } else { 211 Log.w(LOG_TAG, "Failed to read one vCard file: " + uri); 212 mFailedUris.add(uri); 213 } 214 } 215 readOneVCard(InputStream is, int vcardType, String charset, final VCardInterpreter interpreter, final int[] possibleVCardVersions)216 private boolean readOneVCard(InputStream is, int vcardType, String charset, 217 final VCardInterpreter interpreter, 218 final int[] possibleVCardVersions) { 219 boolean successful = false; 220 final int length = possibleVCardVersions.length; 221 for (int i = 0; i < length; i++) { 222 final int vcardVersion = possibleVCardVersions[i]; 223 try { 224 if (i > 0 && (interpreter instanceof VCardEntryConstructor)) { 225 // Let the object clean up internal temporary objects, 226 ((VCardEntryConstructor) interpreter).clear(); 227 } 228 229 // We need synchronized block here, 230 // since we need to handle mCanceled and mVCardParser at once. 231 // In the worst case, a user may call cancel() just before creating 232 // mVCardParser. 233 synchronized (this) { 234 mVCardParser = (vcardVersion == ImportVCardActivity.VCARD_VERSION_V30 ? 235 new VCardParser_V30(vcardType) : 236 new VCardParser_V21(vcardType)); 237 if (isCancelled()) { 238 Log.i(LOG_TAG, "ImportProcessor already recieves cancel request, so " + 239 "send cancel request to vCard parser too."); 240 mVCardParser.cancel(); 241 } 242 } 243 mVCardParser.parse(is, interpreter); 244 245 successful = true; 246 break; 247 } catch (IOException|VCardNotSupportedException e) { 248 // VCardNestedException (a subclass of VCardNotSupportedException) should 249 // not be thrown here. We should instead handle it 250 // in the preprocessing session in ImportVCardActivity, as we don't try 251 // to detect the type of given vCard here. 252 // 253 // TODO: Handle this case appropriately, which should mean we have to have 254 // code trying to auto-detect the type of given vCard twice (both in 255 // ImportVCardActivity and ImportVCardService). 256 FeedbackHelper.sendFeedback(mService, LOG_TAG, "Failed to read vcard", e); 257 } catch (VCardVersionException e) { 258 if (i == length - 1) { 259 Log.e(LOG_TAG, "Appropriate version for this vCard is not found."); 260 } else { 261 // We'll try the other (v30) version. 262 } 263 } catch (VCardException e) { 264 Log.e(LOG_TAG, e.toString()); 265 } finally { 266 if (is != null) { 267 try { 268 is.close(); 269 } catch (IOException e) { 270 } 271 } 272 } 273 } 274 275 return successful; 276 } 277 278 @Override cancel(boolean mayInterruptIfRunning)279 public synchronized boolean cancel(boolean mayInterruptIfRunning) { 280 if (DEBUG) Log.d(LOG_TAG, "ImportProcessor received cancel request"); 281 if (mDone || mCanceled) { 282 return false; 283 } 284 mCanceled = true; 285 synchronized (this) { 286 if (mVCardParser != null) { 287 mVCardParser.cancel(); 288 } 289 } 290 return true; 291 } 292 293 @Override isCancelled()294 public synchronized boolean isCancelled() { 295 return mCanceled; 296 } 297 298 299 @Override isDone()300 public synchronized boolean isDone() { 301 return mDone; 302 } 303 } 304