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