Add support for VCard parsing. Allows VCard records to actually
[android/platform/packages/apps/Tag.git] / src / com / android / apps / tag / record / VCardRecord.java
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
17 package com.android.apps.tag.record;
18
19 import com.android.apps.tag.R;
20 import com.android.vcard.VCardConfig;
21 import com.android.vcard.VCardEntry;
22 import com.android.vcard.VCardEntryConstructor;
23 import com.android.vcard.VCardEntryHandler;
24 import com.android.vcard.VCardParser;
25 import com.android.vcard.VCardParser_V21;
26 import com.android.vcard.exception.VCardException;
27 import com.google.common.base.Preconditions;
28
29 import android.app.Activity;
30 import android.content.ActivityNotFoundException;
31 import android.content.ContentUris;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.pm.PackageManager;
35 import android.content.pm.ResolveInfo;
36 import android.content.res.AssetFileDescriptor;
37 import android.database.Cursor;
38 import android.graphics.BitmapFactory;
39 import android.graphics.drawable.BitmapDrawable;
40 import android.graphics.drawable.Drawable;
41 import android.net.Uri;
42 import android.nfc.NdefRecord;
43 import android.os.AsyncTask;
44 import android.os.Parcel;
45 import android.os.Parcelable;
46 import android.provider.ContactsContract;
47 import android.util.Log;
48 import android.view.LayoutInflater;
49 import android.view.View;
50 import android.view.View.OnClickListener;
51 import android.view.ViewGroup;
52 import android.widget.ImageView;
53 import android.widget.TextView;
54
55 import java.io.ByteArrayInputStream;
56 import java.io.FileInputStream;
57 import java.io.FileNotFoundException;
58 import java.io.IOException;
59 import java.io.InputStream;
60 import java.lang.ref.WeakReference;
61 import java.util.List;
62 import java.util.Locale;
63
64 /**
65  * VCard Ndef Record object
66  */
67 public class VCardRecord extends ParsedNdefRecord implements OnClickListener {
68     private static final String TAG = VCardRecord.class.getSimpleName();
69
70     public static final String RECORD_TYPE = "vcard";
71
72     private final byte[] mVCard;
73
74     private VCardRecord(byte[] content) {
75         mVCard = content;
76     }
77
78     @Override
79     public View getView(Activity activity, LayoutInflater inflater, ViewGroup parent, int offset) {
80
81         Uri uri = activity.getIntent().getData();
82         uri = Uri.withAppendedPath(uri, Integer.toString(offset));
83         uri = Uri.withAppendedPath(uri, "mime");
84
85         // TODO: parse content and display something nicer.
86         Intent intent = new Intent(Intent.ACTION_VIEW, uri);
87         return RecordUtils.getViewsForIntent(activity, inflater, parent, this, intent,
88                 activity.getString(R.string.import_vcard));
89     }
90
91     @Override
92     public String getSnippet(Context context, Locale locale) {
93         return "text/x-vCard";
94     }
95
96     private static Intent getPickContactIntent() {
97         Intent intent = new Intent(Intent.ACTION_PICK);
98         intent.setType(ContactsContract.Contacts.CONTENT_TYPE);
99         return intent;
100     }
101
102     /**
103      * Returns a view in a list of record types for adding new records to a message.
104      */
105     public static View getAddView(Context context, LayoutInflater inflater, ViewGroup parent) {
106         ViewGroup root = (ViewGroup) inflater.inflate(
107                 R.layout.tag_add_record_list_item, parent, false);
108
109         Intent intent = getPickContactIntent();
110         PackageManager pm = context.getPackageManager();
111         List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
112         if (activities.isEmpty()) {
113             return null;
114         }
115
116         ResolveInfo info = activities.get(0);
117         ((ImageView) root.findViewById(R.id.image)).setImageDrawable(info.loadIcon(pm));
118         ((TextView) root.findViewById(R.id.text)).setText(context.getString(R.string.contact));
119
120         root.setTag(new VCardRecordEditInfo());
121         return root;
122     }
123
124     @Override
125     public RecordEditInfo getEditInfo(Activity host) {
126         return new VCardRecordEditInfo(mVCard);
127     }
128
129     public static VCardRecord parse(NdefRecord record) {
130         MimeRecord underlyingRecord = MimeRecord.parse(record);
131
132         // TODO: Add support for other vcard mime types.
133         Preconditions.checkArgument("text/x-vCard".equals(underlyingRecord.getMimeType()));
134         return new VCardRecord(underlyingRecord.getContent());
135     }
136
137     public static NdefRecord newVCardRecord(byte[] data) {
138         return MimeRecord.newMimeRecord("text/x-vCard", data);
139     }
140
141     @Override
142     public void onClick(View view) {
143         RecordUtils.ClickInfo info = (RecordUtils.ClickInfo) view.getTag();
144         try {
145             info.activity.startActivity(info.intent);
146             info.activity.finish();
147         } catch (ActivityNotFoundException e) {
148             // The activity wansn't found for some reason. Don't crash, but don't do anything.
149             Log.e(TAG, "Failed to launch activity for intent " + info.intent, e);
150         }
151     }
152
153     public static boolean isVCard(NdefRecord record) {
154         try {
155             parse(record);
156             return true;
157         } catch (IllegalArgumentException e) {
158             return false;
159         }
160     }
161
162     private static class VCardRecordEditInfo extends RecordEditInfo {
163         /**
164          * The lookup {@link Uri} if the data is to be pulled from the contact provider.
165          * Can be null if {@link #mValue} is filled in by other means externally.
166          * @see ContactsContract
167          */
168         private Uri mLookupUri;
169
170         /**
171          * The raw VCard bytes.
172          */
173         private byte[] mValue;
174
175         private WeakReference<View> mActiveView = null;
176
177         private String mCachedName = null;
178         private Drawable mCachedPhoto = null;
179
180         /**
181          * Data
182          */
183         private static final class CacheData {
184             public String mName;
185             public Drawable mPhoto;
186             public byte[] mVcard;
187         }
188
189         public VCardRecordEditInfo() {
190             super(RECORD_TYPE);
191         }
192
193         public VCardRecordEditInfo(Uri lookupUri) {
194             super(RECORD_TYPE);
195             mLookupUri = lookupUri;
196             mValue = null;
197         }
198
199         public VCardRecordEditInfo(byte[] value) {
200             super(RECORD_TYPE);
201             mLookupUri = null;
202             mValue = Preconditions.checkNotNull(value);
203         }
204
205         protected VCardRecordEditInfo(Parcel parcel) {
206             super(RECORD_TYPE);
207             mLookupUri = parcel.readParcelable(null);
208             int valueLength = parcel.readInt();
209             if (valueLength > 0) {
210                 mValue = new byte[valueLength];
211                 parcel.readByteArray(mValue);
212             }
213         }
214
215         @Override
216         public Intent getPickIntent() {
217             return getPickContactIntent();
218         }
219
220         private void extractValuesFromBytes(final Context context) {
221             byte[] bytes = Preconditions.checkNotNull(mValue);
222
223             final int type = VCardConfig.VCARD_TYPE_UNKNOWN;
224             final VCardEntryConstructor constructor = new VCardEntryConstructor(type);
225             constructor.addEntryHandler(new VCardEntryHandler() {
226                 @Override public void onStart() {}
227                 @Override public void onEnd() {}
228
229                 @Override
230                 public void onEntryCreated(VCardEntry entry) {
231                     mCachedName = entry.getDisplayName();
232                     List<VCardEntry.PhotoData> photoList = entry.getPhotoList();
233                     if (!photoList.isEmpty()) {
234                         byte[] rawData = photoList.get(0).photoBytes;
235                         mCachedPhoto = new BitmapDrawable(
236                                 context.getResources(),
237                                 BitmapFactory.decodeByteArray(rawData, 0, rawData.length));
238                     }
239                     bindView();
240                 }
241             });
242
243             VCardParser parser = new VCardParser_V21(type);
244             try {
245                 parser.parse(new ByteArrayInputStream(bytes), constructor);
246             } catch (IOException e) {
247             } catch (VCardException e) {
248             }
249         }
250
251         private void fetchValuesFromProvider(final Context context) {
252             if (mValue != null) {
253                 bindView();
254                 return;
255             }
256
257             new AsyncTask<Uri, Void, CacheData>() {
258                 @Override
259                 protected CacheData doInBackground(Uri... params) {
260                     Cursor cursor = null;
261                     long id;
262                     String lookupKey = null;
263                     Uri lookupUri = params[0];
264                     CacheData result = new CacheData();
265                     try {
266                         String[] projection = {
267                                 ContactsContract.Contacts._ID,
268                                 ContactsContract.Contacts.LOOKUP_KEY,
269                                 ContactsContract.Contacts.DISPLAY_NAME
270                         };
271                         cursor = context.getContentResolver().query(
272                                 lookupUri, projection, null, null, null);
273                         cursor.moveToFirst();
274                         id = cursor.getLong(0);
275                         lookupKey = cursor.getString(1);
276                         result.mName = cursor.getString(2);
277
278                     } finally {
279                         if (cursor != null) {
280                             cursor.close();
281                             cursor = null;
282                         }
283                     }
284
285                     if (lookupKey == null) {
286                         // TODO: handle errors.
287                         return null;
288                     }
289
290                     // Note: the lookup key should already encoded.
291                     Uri vcardUri = Uri.withAppendedPath(
292                             ContactsContract.Contacts.CONTENT_VCARD_URI,
293                             lookupKey);
294
295                     AssetFileDescriptor descriptor;
296                     FileInputStream in = null;
297                     try {
298                         descriptor =  context.getContentResolver().openAssetFileDescriptor(
299                                 vcardUri, "r");
300                         result.mVcard = new byte[(int) descriptor.getLength()];
301
302                         in = descriptor.createInputStream();
303                         in.read(result.mVcard);
304                         in.close();
305                     } catch (FileNotFoundException e) {
306                         return null;
307                     } catch (IOException e) {
308                         return null;
309                     }
310
311                     Uri contactUri = ContentUris.withAppendedId(
312                             ContactsContract.Contacts.CONTENT_URI, id);
313                     InputStream photoIn = ContactsContract.Contacts.openContactPhotoInputStream(
314                             context.getContentResolver(), contactUri);
315                     if (photoIn != null) {
316                         result.mPhoto = Drawable.createFromStream(photoIn, contactUri.toString());
317                     }
318                     return result;
319                 }
320
321                 @Override
322                 protected void onPostExecute(CacheData data) {
323                     if (data == null) {
324                         return;
325                     }
326
327                     mCachedName = data.mName;
328                     mValue = data.mVcard;
329                     mCachedPhoto = data.mPhoto;
330                     bindView();
331                 }
332             }.execute(mLookupUri);
333         }
334
335         @Override
336         public NdefRecord getValue() {
337             return (mValue == null) ? null : VCardRecord.newVCardRecord(mValue);
338         }
339
340         @Override
341         public void handlePickResult(Context context, Intent data) {
342             mLookupUri = data.getData();
343             mValue = null;
344             mCachedName = null;
345             mCachedPhoto = null;
346         }
347
348         private void bindView() {
349             View view = (mActiveView == null) ? null : mActiveView.get();
350             if (view == null) {
351                 return;
352             }
353
354             if (mCachedPhoto != null) {
355                 ((ImageView) view.findViewById(R.id.photo)).setImageDrawable(mCachedPhoto);
356             }
357
358             if (mCachedName != null) {
359                 ((TextView) view.findViewById(R.id.display_name)).setText(mCachedName);
360             }
361         }
362
363         @Override
364         public View getEditView(
365                 Activity activity, LayoutInflater inflater,
366                 ViewGroup parent, EditCallbacks callbacks) {
367             View result = buildEditView(
368                     activity, inflater, R.layout.tag_edit_vcard, parent, callbacks);
369
370             mActiveView = new WeakReference<View>(result);
371             result.setOnClickListener(this);
372
373             // Show default contact photo until the data loads.
374             ((ImageView) result.findViewById(R.id.photo)).setImageDrawable(
375                     activity.getResources().getDrawable(R.drawable.default_contact_photo));
376
377             if (mLookupUri != null) {
378                 fetchValuesFromProvider(activity);
379             } else if (mValue != null) {
380                 extractValuesFromBytes(activity);
381             }
382             return result;
383         }
384
385         @Override
386         public void writeToParcel(Parcel out, int flags) {
387             super.writeToParcel(out, flags);
388             out.writeParcelable(mLookupUri, flags);
389             out.writeInt(mValue == null ? 0 : mValue.length);
390             out.writeByteArray(mValue);
391         }
392
393         @SuppressWarnings("unused")
394         public static final Parcelable.Creator<VCardRecordEditInfo> CREATOR =
395                 new Parcelable.Creator<VCardRecordEditInfo>() {
396             @Override
397             public VCardRecordEditInfo createFromParcel(Parcel in) {
398                 return new VCardRecordEditInfo(in);
399             }
400
401             @Override
402             public VCardRecordEditInfo[] newArray(int size) {
403                 return new VCardRecordEditInfo[size];
404             }
405         };
406
407         @Override
408         public int describeContents() {
409             return 0;
410         }
411
412         @Override
413         public void onClick(View target) {
414             if (this == target.getTag()) {
415                 mCallbacks.startPickForRecord(this, getPickIntent());
416             } else {
417                 super.onClick(target);
418             }
419         }
420     }
421 }