Home | History | Annotate | Download | only in media
      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