1 /* 2 * Copyright (C) 2015 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.messaging.datamodel.media; 17 18 import android.content.ContentResolver; 19 import android.content.Context; 20 import android.net.Uri; 21 22 import com.android.messaging.util.Assert; 23 import com.android.messaging.util.Assert.DoesNotRunOnMainThread; 24 import com.android.messaging.util.AvatarUriUtil; 25 import com.android.messaging.util.LogUtil; 26 import com.android.messaging.util.PhoneUtils; 27 import com.android.messaging.util.UriUtil; 28 import com.android.vcard.VCardConfig; 29 import com.android.vcard.VCardEntry; 30 import com.android.vcard.VCardEntryCounter; 31 import com.android.vcard.VCardInterpreter; 32 import com.android.vcard.VCardParser; 33 import com.android.vcard.VCardParser_V21; 34 import com.android.vcard.VCardParser_V30; 35 import com.android.vcard.VCardSourceDetector; 36 import com.android.vcard.exception.VCardException; 37 import com.android.vcard.exception.VCardNestedException; 38 import com.android.vcard.exception.VCardNotSupportedException; 39 import com.android.vcard.exception.VCardVersionException; 40 41 import java.io.ByteArrayInputStream; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.concurrent.CountDownLatch; 47 import java.util.concurrent.TimeUnit; 48 49 /** 50 * Requests and parses VCard data. In Bugle, we need to display VCard details in the conversation 51 * view such as avatar icon and name, which can be expensive if we parse VCard every time. 52 * Therefore, we'd like to load the vcard once and cache it in our media cache using the 53 * MediaResourceManager component. To load the VCard, we use framework's VCard support to 54 * interpret the VCard content, which gives us information such as phone and email list, which 55 * we'll put in VCardResource object to be cached. 56 * 57 * Some particular attention is needed for the avatar icon. If the VCard contains avatar icon, 58 * it's in byte array form that can't easily be cached/persisted. Therefore, we persist the 59 * image bytes to the scratch directory and generate a content Uri for it, so that ContactIconView 60 * may use this Uri to display and cache the image if needed. 61 */ 62 public class VCardRequest implements MediaRequest<VCardResource> { 63 private final Context mContext; 64 private final VCardRequestDescriptor mDescriptor; 65 private final List<VCardResourceEntry> mLoadedVCards; 66 private VCardResource mLoadedResource; 67 private static final int VCARD_LOADING_TIMEOUT_MILLIS = 10000; // 10s 68 private static final String DEFAULT_VCARD_TYPE = "default"; 69 70 VCardRequest(final Context context, final VCardRequestDescriptor descriptor) { 71 mDescriptor = descriptor; 72 mContext = context; 73 mLoadedVCards = new ArrayList<VCardResourceEntry>(); 74 } 75 76 @Override 77 public String getKey() { 78 return mDescriptor.vCardUri.toString(); 79 } 80 81 @Override 82 @DoesNotRunOnMainThread 83 public VCardResource loadMediaBlocking(List<MediaRequest<VCardResource>> chainedTask) 84 throws Exception { 85 Assert.isNotMainThread(); 86 Assert.isTrue(mLoadedResource == null); 87 Assert.equals(0, mLoadedVCards.size()); 88 89 // The VCard library doesn't support synchronously loading the media resource. Therefore, 90 // We have to burn the thread waiting for the result to come back. 91 final CountDownLatch signal = new CountDownLatch(1); 92 if (!parseVCard(mDescriptor.vCardUri, signal)) { 93 // Directly fail without actually going through the interpreter, return immediately. 94 throw new VCardException("Invalid vcard"); 95 } 96 97 signal.await(VCARD_LOADING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 98 if (mLoadedResource == null) { 99 // Maybe null if failed or timeout. 100 throw new VCardException("Failure or timeout loading vcard"); 101 } 102 return mLoadedResource; 103 } 104 105 @Override 106 public int getCacheId() { 107 return BugleMediaCacheManager.VCARD_CACHE; 108 } 109 110 @SuppressWarnings("unchecked") 111 @Override 112 public MediaCache<VCardResource> getMediaCache() { 113 return (MediaCache<VCardResource>) MediaCacheManager.get().getOrCreateMediaCacheById( 114 getCacheId()); 115 } 116 117 @DoesNotRunOnMainThread 118 private boolean parseVCard(final Uri targetUri, final CountDownLatch signal) { 119 Assert.isNotMainThread(); 120 final VCardEntryCounter counter = new VCardEntryCounter(); 121 final VCardSourceDetector detector = new VCardSourceDetector(); 122 boolean result; 123 try { 124 // We don't know which type should be used to parse the Uri. 125 // It is possible to misinterpret the vCard, but we expect the parser 126 // lets VCardSourceDetector detect the type before the misinterpretation. 127 result = readOneVCardFile(targetUri, VCardConfig.VCARD_TYPE_UNKNOWN, 128 detector, true, null); 129 } catch (final VCardNestedException e) { 130 try { 131 final int estimatedVCardType = detector.getEstimatedType(); 132 // Assume that VCardSourceDetector was able to detect the source. 133 // Try again with the detector. 134 result = readOneVCardFile(targetUri, estimatedVCardType, 135 counter, false, null); 136 } catch (final VCardNestedException e2) { 137 result = false; 138 LogUtil.e(LogUtil.BUGLE_TAG, "Must not reach here. " + e2); 139 } 140 } 141 142 if (!result) { 143 // Load failure. 144 return false; 145 } 146 147 return doActuallyReadOneVCard(targetUri, true, detector, null, signal); 148 } 149 150 @DoesNotRunOnMainThread 151 private boolean doActuallyReadOneVCard(final Uri uri, final boolean showEntryParseProgress, 152 final VCardSourceDetector detector, final List<String> errorFileNameList, 153 final CountDownLatch signal) { 154 Assert.isNotMainThread(); 155 int vcardType = detector.getEstimatedType(); 156 if (vcardType == VCardConfig.VCARD_TYPE_UNKNOWN) { 157 vcardType = VCardConfig.getVCardTypeFromString(DEFAULT_VCARD_TYPE); 158 } 159 final CustomVCardEntryConstructor builder = 160 new CustomVCardEntryConstructor(vcardType, null); 161 builder.addEntryHandler(new ContactVCardEntryHandler(signal)); 162 163 try { 164 if (!readOneVCardFile(uri, vcardType, builder, false, null)) { 165 return false; 166 } 167 } catch (final VCardNestedException e) { 168 LogUtil.e(LogUtil.BUGLE_TAG, "Must not reach here. " + e); 169 return false; 170 } 171 return true; 172 } 173 174 @DoesNotRunOnMainThread 175 private boolean readOneVCardFile(final Uri uri, final int vcardType, 176 final VCardInterpreter interpreter, 177 final boolean throwNestedException, final List<String> errorFileNameList) 178 throws VCardNestedException { 179 Assert.isNotMainThread(); 180 final ContentResolver resolver = mContext.getContentResolver(); 181 VCardParser vCardParser; 182 InputStream is; 183 try { 184 is = resolver.openInputStream(uri); 185 vCardParser = new VCardParser_V21(vcardType); 186 vCardParser.addInterpreter(interpreter); 187 188 try { 189 vCardParser.parse(is); 190 } catch (final VCardVersionException e1) { 191 try { 192 is.close(); 193 } catch (final IOException e) { 194 // Do nothing. 195 } 196 if (interpreter instanceof CustomVCardEntryConstructor) { 197 // Let the object clean up internal temporal objects, 198 ((CustomVCardEntryConstructor) interpreter).clear(); 199 } 200 201 is = resolver.openInputStream(uri); 202 203 try { 204 vCardParser = new VCardParser_V30(vcardType); 205 vCardParser.addInterpreter(interpreter); 206 vCardParser.parse(is); 207 } catch (final VCardVersionException e2) { 208 throw new VCardException("vCard with unspported version."); 209 } 210 } finally { 211 if (is != null) { 212 try { 213 is.close(); 214 } catch (final IOException e) { 215 // Do nothing. 216 } 217 } 218 } 219 } catch (final IOException e) { 220 LogUtil.e(LogUtil.BUGLE_TAG, "IOException was emitted: " + e.getMessage()); 221 222 if (errorFileNameList != null) { 223 errorFileNameList.add(uri.toString()); 224 } 225 return false; 226 } catch (final VCardNotSupportedException e) { 227 if ((e instanceof VCardNestedException) && throwNestedException) { 228 throw (VCardNestedException) e; 229 } 230 if (errorFileNameList != null) { 231 errorFileNameList.add(uri.toString()); 232 } 233 return false; 234 } catch (final VCardException e) { 235 if (errorFileNameList != null) { 236 errorFileNameList.add(uri.toString()); 237 } 238 return false; 239 } 240 return true; 241 } 242 243 class ContactVCardEntryHandler implements CustomVCardEntryConstructor.EntryHandler { 244 final CountDownLatch mSignal; 245 246 public ContactVCardEntryHandler(final CountDownLatch signal) { 247 mSignal = signal; 248 } 249 250 @Override 251 public void onStart() { 252 } 253 254 @Override 255 @DoesNotRunOnMainThread 256 public void onEntryCreated(final CustomVCardEntry entry) { 257 Assert.isNotMainThread(); 258 final String displayName = entry.getDisplayName(); 259 final List<VCardEntry.PhotoData> photos = entry.getPhotoList(); 260 Uri avatarUri = null; 261 if (photos != null && photos.size() > 0) { 262 // The photo data is in bytes form, so we need to persist it in our temp directory 263 // so that ContactIconView can load it and display it later 264 // (and cache it, of course). 265 for (final VCardEntry.PhotoData photo : photos) { 266 final byte[] photoBytes = photo.getBytes(); 267 if (photoBytes != null) { 268 final InputStream inputStream = new ByteArrayInputStream(photoBytes); 269 try { 270 avatarUri = UriUtil.persistContentToScratchSpace(inputStream); 271 if (avatarUri != null) { 272 // Just load the first avatar and be done. Want more? wait for V2. 273 break; 274 } 275 } finally { 276 try { 277 inputStream.close(); 278 } catch (final IOException e) { 279 // Do nothing. 280 } 281 } 282 } 283 } 284 } 285 286 // Fall back to generated avatar. 287 if (avatarUri == null) { 288 String destination = null; 289 final List<VCardEntry.PhoneData> phones = entry.getPhoneList(); 290 if (phones != null && phones.size() > 0) { 291 destination = PhoneUtils.getDefault().getCanonicalBySystemLocale( 292 phones.get(0).getNumber()); 293 } 294 295 if (destination == null) { 296 final List<VCardEntry.EmailData> emails = entry.getEmailList(); 297 if (emails != null && emails.size() > 0) { 298 destination = emails.get(0).getAddress(); 299 } 300 } 301 avatarUri = AvatarUriUtil.createAvatarUri(null, displayName, destination, null); 302 } 303 304 // Add the loaded vcard to the list. 305 mLoadedVCards.add(new VCardResourceEntry(entry, avatarUri)); 306 } 307 308 @Override 309 public void onEnd() { 310 // Finished loading all vCard entries, signal the loading thread to proceed with the 311 // result. 312 if (mLoadedVCards.size() > 0) { 313 mLoadedResource = new VCardResource(getKey(), mLoadedVCards); 314 } 315 mSignal.countDown(); 316 } 317 } 318 319 @Override 320 public int getRequestType() { 321 return MediaRequest.REQUEST_LOAD_MEDIA; 322 } 323 324 @Override 325 public MediaRequestDescriptor<VCardResource> getDescriptor() { 326 return mDescriptor; 327 } 328 } 329