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.calendar.event;
18 
19 import com.android.calendar.CalendarEventModel.Attendee;
20 import com.android.calendar.ContactsAsyncHelper;
21 import com.android.calendar.R;
22 import com.android.calendar.Utils;
23 import com.android.calendar.event.EditEventHelper.AttendeeItem;
24 import com.android.common.Rfc822Validator;
25 
26 import android.content.AsyncQueryHandler;
27 import android.content.ContentResolver;
28 import android.content.ContentUris;
29 import android.content.Context;
30 import android.content.res.Resources;
31 import android.database.Cursor;
32 import android.graphics.ColorMatrix;
33 import android.graphics.ColorMatrixColorFilter;
34 import android.graphics.Paint;
35 import android.graphics.drawable.Drawable;
36 import android.net.Uri;
37 import android.provider.CalendarContract.Attendees;
38 import android.provider.ContactsContract.CommonDataKinds.Email;
39 import android.provider.ContactsContract.CommonDataKinds.Identity;
40 import android.provider.ContactsContract.Contacts;
41 import android.provider.ContactsContract.Data;
42 import android.provider.ContactsContract.RawContacts;
43 import android.text.TextUtils;
44 import android.text.util.Rfc822Token;
45 import android.util.AttributeSet;
46 import android.util.Log;
47 import android.view.LayoutInflater;
48 import android.view.View;
49 import android.widget.ImageButton;
50 import android.widget.LinearLayout;
51 import android.widget.QuickContactBadge;
52 import android.widget.TextView;
53 
54 import java.util.ArrayList;
55 import java.util.HashMap;
56 import java.util.LinkedHashSet;
57 
58 public class AttendeesView extends LinearLayout implements View.OnClickListener {
59     private static final String TAG = "AttendeesView";
60     private static final boolean DEBUG = false;
61 
62     private static final int EMAIL_PROJECTION_CONTACT_ID_INDEX = 0;
63     private static final int EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX = 1;
64     private static final int EMAIL_PROJECTION_PHOTO_ID_INDEX = 2;
65 
66     private static final String[] PROJECTION = new String[] {
67         RawContacts.CONTACT_ID,     // 0
68         Contacts.LOOKUP_KEY,        // 1
69         Contacts.PHOTO_ID,          // 2
70     };
71 
72     private final Context mContext;
73     private final LayoutInflater mInflater;
74     private final PresenceQueryHandler mPresenceQueryHandler;
75     private final Drawable mDefaultBadge;
76     private final ColorMatrixColorFilter mGrayscaleFilter;
77 
78     // TextView shown at the top of each type of attendees
79     // e.g.
80     // Yes  <-- divider
81     // example_for_yes <exampleyes@example.com>
82     // No <-- divider
83     // example_for_no <exampleno@example.com>
84     private final CharSequence[] mEntries;
85     private final View mDividerForYes;
86     private final View mDividerForNo;
87     private final View mDividerForMaybe;
88     private final View mDividerForNoResponse;
89     private final int mNoResponsePhotoAlpha;
90     private final int mDefaultPhotoAlpha;
91     private Rfc822Validator mValidator;
92 
93     // Number of attendees responding or not responding.
94     private int mYes;
95     private int mNo;
96     private int mMaybe;
97     private int mNoResponse;
98 
99     // Cache for loaded photos
100     HashMap<String, Drawable> mRecycledPhotos;
101 
AttendeesView(Context context, AttributeSet attrs)102     public AttendeesView(Context context, AttributeSet attrs) {
103         super(context, attrs);
104         mContext = context;
105         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
106         mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver());
107 
108         final Resources resources = context.getResources();
109         mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture);
110         mNoResponsePhotoAlpha =
111             resources.getInteger(R.integer.noresponse_attendee_photo_alpha_level);
112         mDefaultPhotoAlpha = resources.getInteger(R.integer.default_attendee_photo_alpha_level);
113 
114         // Create dividers between groups of attendees (accepted, declined, etc...)
115         mEntries = resources.getTextArray(R.array.response_labels1);
116         mDividerForYes = constructDividerView(mEntries[1]);
117         mDividerForNo = constructDividerView(mEntries[3]);
118         mDividerForMaybe = constructDividerView(mEntries[2]);
119         mDividerForNoResponse = constructDividerView(mEntries[0]);
120 
121         // Create a filter to convert photos of declined attendees to grayscale.
122         ColorMatrix matrix = new ColorMatrix();
123         matrix.setSaturation(0);
124         mGrayscaleFilter = new ColorMatrixColorFilter(matrix);
125 
126     }
127 
128     // Disable/enable removal of attendings
129     @Override
setEnabled(boolean enabled)130     public void setEnabled(boolean enabled) {
131         super.setEnabled(enabled);
132         int visibility = isEnabled() ? View.VISIBLE : View.GONE;
133         int count = getChildCount();
134         for (int i = 0; i < count; i++) {
135             View child = getChildAt(i);
136             View minusButton = child.findViewById(R.id.contact_remove);
137             if (minusButton != null) {
138                 minusButton.setVisibility(visibility);
139             }
140         }
141     }
142 
setRfc822Validator(Rfc822Validator validator)143     public void setRfc822Validator(Rfc822Validator validator) {
144         mValidator = validator;
145     }
146 
constructDividerView(CharSequence label)147     private View constructDividerView(CharSequence label) {
148         final TextView textView =
149             (TextView)mInflater.inflate(R.layout.event_info_label, this, false);
150         textView.setText(label);
151         textView.setClickable(false);
152         return textView;
153     }
154 
155     // Add the number of attendees in the specific status (corresponding to the divider) in
156     // parenthesis next to the label
updateDividerViewLabel(View divider, CharSequence label, int count)157     private void updateDividerViewLabel(View divider, CharSequence label, int count) {
158         if (count <= 0) {
159             ((TextView)divider).setText(label);
160         }
161         else {
162             ((TextView)divider).setText(label + " (" + count + ")");
163         }
164     }
165 
166 
167     /**
168      * Inflates a layout for a given attendee view and set up each element in it, and returns
169      * the constructed View object. The object is also stored in {@link AttendeeItem#mView}.
170      */
constructAttendeeView(AttendeeItem item)171     private View constructAttendeeView(AttendeeItem item) {
172         item.mView = mInflater.inflate(R.layout.contact_item, null);
173         return updateAttendeeView(item);
174     }
175 
176     /**
177      * Set up each element in {@link AttendeeItem#mView} using the latest information. View
178      * object is reused.
179      */
updateAttendeeView(AttendeeItem item)180     private View updateAttendeeView(AttendeeItem item) {
181         final Attendee attendee = item.mAttendee;
182         final View view = item.mView;
183         final TextView nameView = (TextView) view.findViewById(R.id.name);
184         nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName);
185         if (item.mRemoved) {
186             nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags());
187         } else {
188             nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags());
189         }
190 
191         // Set up the Image button even if the view is disabled
192         // Everything will be ready when the view is enabled later
193         final ImageButton button = (ImageButton) view.findViewById(R.id.contact_remove);
194         button.setVisibility(isEnabled() ? View.VISIBLE : View.GONE);
195         button.setTag(item);
196         if (item.mRemoved) {
197             button.setImageResource(R.drawable.ic_menu_add_field_holo_light);
198             button.setContentDescription(mContext.getString(R.string.accessibility_add_attendee));
199         } else {
200             button.setImageResource(R.drawable.ic_menu_remove_field_holo_light);
201             button.setContentDescription(mContext.
202                     getString(R.string.accessibility_remove_attendee));
203         }
204         button.setOnClickListener(this);
205 
206         final QuickContactBadge badgeView = (QuickContactBadge) view.findViewById(R.id.badge);
207 
208         Drawable badge = null;
209         // Search for photo in recycled photos
210         if (mRecycledPhotos != null) {
211             badge = mRecycledPhotos.get(item.mAttendee.mEmail);
212         }
213         if (badge != null) {
214             item.mBadge = badge;
215         }
216         badgeView.setImageDrawable(item.mBadge);
217 
218         if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) {
219             item.mBadge.setAlpha(mNoResponsePhotoAlpha);
220         } else {
221             item.mBadge.setAlpha(mDefaultPhotoAlpha);
222         }
223         if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
224             item.mBadge.setColorFilter(mGrayscaleFilter);
225         } else {
226             item.mBadge.setColorFilter(null);
227         }
228 
229         // If we know the lookup-uri of the contact, it is a good idea to set this here. This
230         // allows QuickContact to be started without an extra database lookup. If we don't know
231         // the lookup uri (yet), we can set Email and QuickContact will lookup once tapped.
232         if (item.mContactLookupUri != null) {
233             badgeView.assignContactUri(item.mContactLookupUri);
234         } else {
235             badgeView.assignContactFromEmail(item.mAttendee.mEmail, true);
236         }
237         badgeView.setMaxHeight(60);
238 
239         return view;
240     }
241 
contains(Attendee attendee)242     public boolean contains(Attendee attendee) {
243         final int size = getChildCount();
244         for (int i = 0; i < size; i++) {
245             final View view = getChildAt(i);
246             if (view instanceof TextView) { // divider
247                 continue;
248             }
249             AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
250             if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) {
251                 return true;
252             }
253         }
254         return false;
255     }
256 
clearAttendees()257     public void clearAttendees() {
258 
259         // Before clearing the views, save all the badges. The updateAtendeeView will use the saved
260         // photo instead of the default badge thus prevent switching between the two while the
261         // most current photo is loaded in the background.
262         mRecycledPhotos = new HashMap<String, Drawable>  ();
263         final int size = getChildCount();
264         for (int i = 0; i < size; i++) {
265             final View view = getChildAt(i);
266             if (view instanceof TextView) { // divider
267                 continue;
268             }
269             AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
270             mRecycledPhotos.put(attendeeItem.mAttendee.mEmail, attendeeItem.mBadge);
271         }
272 
273         removeAllViews();
274         mYes = 0;
275         mNo = 0;
276         mMaybe = 0;
277         mNoResponse = 0;
278     }
279 
addOneAttendee(Attendee attendee)280     private void addOneAttendee(Attendee attendee) {
281         if (contains(attendee)) {
282             return;
283         }
284         final AttendeeItem item = new AttendeeItem(attendee, mDefaultBadge);
285         final int status = attendee.mStatus;
286         final int index;
287         boolean firstAttendeeInCategory = false;
288         switch (status) {
289             case Attendees.ATTENDEE_STATUS_ACCEPTED: {
290                 final int startIndex = 0;
291                 updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1);
292                 if (mYes == 0) {
293                     addView(mDividerForYes, startIndex);
294                     firstAttendeeInCategory = true;
295                 }
296                 mYes++;
297                 index = startIndex + mYes;
298                 break;
299             }
300             case Attendees.ATTENDEE_STATUS_DECLINED: {
301                 final int startIndex = (mYes == 0 ? 0 : 1 + mYes);
302                 updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1);
303                 if (mNo == 0) {
304                     addView(mDividerForNo, startIndex);
305                     firstAttendeeInCategory = true;
306                 }
307                 mNo++;
308                 index = startIndex + mNo;
309                 break;
310             }
311             case Attendees.ATTENDEE_STATUS_TENTATIVE: {
312                 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo);
313                 updateDividerViewLabel(mDividerForMaybe, mEntries[2], mMaybe + 1);
314                 if (mMaybe == 0) {
315                     addView(mDividerForMaybe, startIndex);
316                     firstAttendeeInCategory = true;
317                 }
318                 mMaybe++;
319                 index = startIndex + mMaybe;
320                 break;
321             }
322             default: {
323                 final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo)
324                         + (mMaybe == 0 ? 0 : 1 + mMaybe);
325                 updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse + 1);
326                 if (mNoResponse == 0) {
327                     addView(mDividerForNoResponse, startIndex);
328                     firstAttendeeInCategory = true;
329                 }
330                 mNoResponse++;
331                 index = startIndex + mNoResponse;
332                 break;
333             }
334         }
335 
336         final View view = constructAttendeeView(item);
337         view.setTag(item);
338         addView(view, index);
339         // Show separator between Attendees
340         if (!firstAttendeeInCategory) {
341             View prevItem = getChildAt(index - 1);
342             if (prevItem != null) {
343                 View Separator = prevItem.findViewById(R.id.contact_separator);
344                 if (Separator != null) {
345                     Separator.setVisibility(View.VISIBLE);
346                 }
347             }
348         }
349 
350         Uri uri;
351         String selection = null;
352         String[] selectionArgs = null;
353         if (attendee.mIdentity != null && attendee.mIdNamespace != null) {
354             // Query by identity + namespace
355             uri = Data.CONTENT_URI;
356             selection = Data.MIMETYPE + "=? AND " + Identity.IDENTITY + "=? AND " +
357                     Identity.NAMESPACE + "=?";
358             selectionArgs = new String[] {Identity.CONTENT_ITEM_TYPE, attendee.mIdentity,
359                     attendee.mIdNamespace};
360         } else {
361             // Query by email
362             uri = Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(attendee.mEmail));
363         }
364 
365         mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item, uri, PROJECTION, selection,
366                 selectionArgs, null);
367     }
368 
addAttendees(ArrayList<Attendee> attendees)369     public void addAttendees(ArrayList<Attendee> attendees) {
370         synchronized (this) {
371             for (final Attendee attendee : attendees) {
372                 addOneAttendee(attendee);
373             }
374         }
375     }
376 
addAttendees(HashMap<String, Attendee> attendees)377     public void addAttendees(HashMap<String, Attendee> attendees) {
378         synchronized (this) {
379             for (final Attendee attendee : attendees.values()) {
380                 addOneAttendee(attendee);
381             }
382         }
383     }
384 
addAttendees(String attendees)385     public void addAttendees(String attendees) {
386         final LinkedHashSet<Rfc822Token> addresses =
387                 EditEventHelper.getAddressesFromList(attendees, mValidator);
388         synchronized (this) {
389             for (final Rfc822Token address : addresses) {
390                 final Attendee attendee = new Attendee(address.getName(), address.getAddress());
391                 if (TextUtils.isEmpty(attendee.mName)) {
392                     attendee.mName = attendee.mEmail;
393                 }
394                 addOneAttendee(attendee);
395             }
396         }
397     }
398 
399     /**
400      * Returns true when the attendee at that index is marked as "removed" (the name of
401      * the attendee is shown with a strike through line).
402      */
isMarkAsRemoved(int index)403     public boolean isMarkAsRemoved(int index) {
404         final View view = getChildAt(index);
405         if (view instanceof TextView) { // divider
406             return false;
407         }
408         return ((AttendeeItem) view.getTag()).mRemoved;
409     }
410 
411     // TODO put this into a Loader for auto-requeries
412     private class PresenceQueryHandler extends AsyncQueryHandler {
PresenceQueryHandler(ContentResolver cr)413         public PresenceQueryHandler(ContentResolver cr) {
414             super(cr);
415         }
416 
417         @Override
onQueryComplete(int queryIndex, Object cookie, Cursor cursor)418         protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
419             if (cursor == null || cookie == null) {
420                 if (DEBUG) {
421                     Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie);
422                 }
423                 return;
424             }
425 
426             final AttendeeItem item = (AttendeeItem)cookie;
427             try {
428                 if (item.mUpdateCounts < queryIndex) {
429                     item.mUpdateCounts = queryIndex;
430                     if (cursor.moveToFirst()) {
431                         final long contactId = cursor.getLong(EMAIL_PROJECTION_CONTACT_ID_INDEX);
432                         final Uri contactUri =
433                                 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
434 
435                         final String lookupKey =
436                                 cursor.getString(EMAIL_PROJECTION_CONTACT_LOOKUP_INDEX);
437                         item.mContactLookupUri = Contacts.getLookupUri(contactId, lookupKey);
438 
439                         final long photoId = cursor.getLong(EMAIL_PROJECTION_PHOTO_ID_INDEX);
440                         // If we found a picture, start the async loading
441                         if (photoId > 0) {
442                             // Query for this contacts picture
443                             ContactsAsyncHelper.retrieveContactPhotoAsync(
444                                     mContext, item, new Runnable() {
445                                         @Override
446                                         public void run() {
447                                             updateAttendeeView(item);
448                                         }
449                                     }, contactUri);
450                         } else {
451                             // call update view to make sure that the lookup key gets set in
452                             // the QuickContactBadge
453                             updateAttendeeView(item);
454                         }
455                     } else {
456                         // Contact not found.  For real emails, keep the QuickContactBadge with
457                         // its Email address set, so that the user can create a contact by tapping.
458                         item.mContactLookupUri = null;
459                         if (!Utils.isValidEmail(item.mAttendee.mEmail)) {
460                             item.mAttendee.mEmail = null;
461                             updateAttendeeView(item);
462                         }
463                     }
464                 }
465             } finally {
466                 cursor.close();
467             }
468         }
469     }
470 
getItem(int index)471     public Attendee getItem(int index) {
472         final View view = getChildAt(index);
473         if (view instanceof TextView) { // divider
474             return null;
475         }
476         return ((AttendeeItem) view.getTag()).mAttendee;
477     }
478 
479     @Override
onClick(View view)480     public void onClick(View view) {
481         // Button corresponding to R.id.contact_remove.
482         final AttendeeItem item = (AttendeeItem) view.getTag();
483         item.mRemoved = !item.mRemoved;
484         updateAttendeeView(item);
485     }
486 }
487