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 */
16package com.android.messaging.datamodel.media;
17
18import android.content.ContentResolver;
19import android.content.Context;
20import android.net.Uri;
21
22import com.android.messaging.util.Assert;
23import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
24import com.android.messaging.util.AvatarUriUtil;
25import com.android.messaging.util.LogUtil;
26import com.android.messaging.util.PhoneUtils;
27import com.android.messaging.util.UriUtil;
28import com.android.vcard.VCardConfig;
29import com.android.vcard.VCardEntry;
30import com.android.vcard.VCardEntryCounter;
31import com.android.vcard.VCardInterpreter;
32import com.android.vcard.VCardParser;
33import com.android.vcard.VCardParser_V21;
34import com.android.vcard.VCardParser_V30;
35import com.android.vcard.VCardSourceDetector;
36import com.android.vcard.exception.VCardException;
37import com.android.vcard.exception.VCardNestedException;
38import com.android.vcard.exception.VCardNotSupportedException;
39import com.android.vcard.exception.VCardVersionException;
40
41import java.io.ByteArrayInputStream;
42import java.io.IOException;
43import java.io.InputStream;
44import java.util.ArrayList;
45import java.util.List;
46import java.util.concurrent.CountDownLatch;
47import 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 */
62public 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