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 
VCardRequest(final Context context, final VCardRequestDescriptor descriptor)70     VCardRequest(final Context context, final VCardRequestDescriptor descriptor) {
71         mDescriptor = descriptor;
72         mContext = context;
73         mLoadedVCards = new ArrayList<VCardResourceEntry>();
74     }
75 
76     @Override
getKey()77     public String getKey() {
78         return mDescriptor.vCardUri.toString();
79     }
80 
81     @Override
82     @DoesNotRunOnMainThread
loadMediaBlocking(List<MediaRequest<VCardResource>> chainedTask)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
getCacheId()106     public int getCacheId() {
107         return BugleMediaCacheManager.VCARD_CACHE;
108     }
109 
110     @SuppressWarnings("unchecked")
111     @Override
getMediaCache()112     public MediaCache<VCardResource> getMediaCache() {
113         return (MediaCache<VCardResource>) MediaCacheManager.get().getOrCreateMediaCacheById(
114                 getCacheId());
115     }
116 
117     @DoesNotRunOnMainThread
parseVCard(final Uri targetUri, final CountDownLatch signal)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
doActuallyReadOneVCard(final Uri uri, final boolean showEntryParseProgress, final VCardSourceDetector detector, final List<String> errorFileNameList, final CountDownLatch signal)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
readOneVCardFile(final Uri uri, final int vcardType, final VCardInterpreter interpreter, final boolean throwNestedException, final List<String> errorFileNameList)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 
ContactVCardEntryHandler(final CountDownLatch signal)246         public ContactVCardEntryHandler(final CountDownLatch signal) {
247             mSignal = signal;
248         }
249 
250         @Override
onStart()251         public void onStart() {
252         }
253 
254         @Override
255         @DoesNotRunOnMainThread
onEntryCreated(final CustomVCardEntry entry)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
onEnd()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
getRequestType()320     public int getRequestType() {
321         return MediaRequest.REQUEST_LOAD_MEDIA;
322     }
323 
324     @Override
getDescriptor()325     public MediaRequestDescriptor<VCardResource> getDescriptor() {
326         return mDescriptor;
327     }
328 }
329