AttendeesView.java revision 9ceed1f3df98c5fc85441da0c6e7e5d45cf17a1e
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
17package com.android.calendar.event;
18
19import com.android.calendar.CalendarEventModel.Attendee;
20import com.android.calendar.ContactsAsyncHelper;
21import com.android.calendar.R;
22import com.android.calendar.event.EditEventHelper.AttendeeItem;
23import com.android.common.Rfc822Validator;
24
25import android.content.AsyncQueryHandler;
26import android.content.ContentResolver;
27import android.content.ContentUris;
28import android.content.Context;
29import android.content.res.Resources;
30import android.database.Cursor;
31import android.graphics.Color;
32import android.graphics.ColorMatrix;
33import android.graphics.ColorMatrixColorFilter;
34import android.graphics.Paint;
35import android.graphics.drawable.Drawable;
36import android.net.Uri;
37import android.provider.Calendar.Attendees;
38import android.provider.ContactsContract.CommonDataKinds.Email;
39import android.provider.ContactsContract.Contacts;
40import android.provider.ContactsContract.Data;
41import android.provider.ContactsContract.StatusUpdates;
42import android.text.TextUtils;
43import android.text.util.Rfc822Token;
44import android.util.AttributeSet;
45import android.util.Log;
46import android.view.LayoutInflater;
47import android.view.View;
48import android.widget.ImageButton;
49import android.widget.ImageView;
50import android.widget.LinearLayout;
51import android.widget.QuickContactBadge;
52import android.widget.TextView;
53
54import java.util.ArrayList;
55import java.util.HashMap;
56import java.util.LinkedHashSet;
57
58public 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 PRESENCE_PROJECTION_CONTACT_ID_INDEX = 0;
63    private static final int PRESENCE_PROJECTION_PRESENCE_INDEX = 1;
64    private static final int PRESENCE_PROJECTION_EMAIL_INDEX = 2;
65    private static final int PRESENCE_PROJECTION_PHOTO_ID_INDEX = 3;
66
67    private static final String[] PRESENCE_PROJECTION = new String[] {
68        Email.CONTACT_ID,           // 0
69        Email.CONTACT_PRESENCE,     // 1
70        Email.DATA,                 // 2
71        Email.PHOTO_ID,             // 3
72    };
73
74    private static final Uri CONTACT_DATA_WITH_PRESENCE_URI = Data.CONTENT_URI;
75    private static final String CONTACT_DATA_SELECTION = Email.DATA + " IN (?)";
76
77    private final Context mContext;
78    private final LayoutInflater mInflater;
79    private final PresenceQueryHandler mPresenceQueryHandler;
80    private final Drawable mDefaultBadge;
81    private final ColorMatrixColorFilter mGrayscaleFilter;
82
83    // TextView shown at the top of each type of attendees
84    // e.g.
85    // Yes  <-- divider
86    // example_for_yes <exampleyes@example.com>
87    // No <-- divider
88    // example_for_no <exampleno@example.com>
89    private final CharSequence[] mEntries;
90    private final View mDividerForYes;
91    private final View mDividerForNo;
92    private final View mDividerForMaybe;
93    private final View mDividerForNoResponse;
94    private final int mNoResponsePhotoAlpha;
95    private final int mDefaultPhotoAlpha;
96    private Rfc822Validator mValidator;
97
98    // Number of attendees responding or not responding.
99    private int mYes;
100    private int mNo;
101    private int mMaybe;
102    private int mNoResponse;
103
104    public AttendeesView(Context context, AttributeSet attrs) {
105        super(context, attrs);
106        mContext = context;
107        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
108        mPresenceQueryHandler = new PresenceQueryHandler(context.getContentResolver());
109
110        final Resources resources = context.getResources();
111        mDefaultBadge = resources.getDrawable(R.drawable.ic_contact_picture);
112        mNoResponsePhotoAlpha =
113            resources.getInteger(R.integer.noresponse_attendee_photo_alpha_level);
114        mDefaultPhotoAlpha = resources.getInteger(R.integer.default_attendee_photo_alpha_level);
115
116        // Create dividers between groups of attendees (accepted, declined, etc...)
117        mEntries = resources.getTextArray(R.array.response_labels1);
118        mDividerForYes = constructDividerView(mEntries[1]);
119        mDividerForNo = constructDividerView(mEntries[3]);
120        mDividerForMaybe = constructDividerView(mEntries[2]);
121        mDividerForNoResponse = constructDividerView(mEntries[0]);
122
123        // Create a filter to convert photos of declined attendees to grayscale.
124        ColorMatrix matrix = new ColorMatrix();
125        matrix.setSaturation(0);
126        mGrayscaleFilter = new ColorMatrixColorFilter(matrix);
127
128    }
129
130    // Disable/enable removal of attendings
131    @Override
132    public void setEnabled(boolean enabled) {
133        super.setEnabled(enabled);
134        int visibility = isEnabled() ? View.VISIBLE : View.GONE;
135        int count = getChildCount();
136        for (int i = 0; i < count; i++) {
137            View child = getChildAt(i);
138            View minusButton = child.findViewById(R.id.contact_remove);
139            if (minusButton != null) {
140                minusButton.setVisibility(visibility);
141            }
142        }
143    }
144
145    public void setRfc822Validator(Rfc822Validator validator) {
146        mValidator = validator;
147    }
148
149    private View constructDividerView(CharSequence label) {
150        final TextView textView = new TextView(mContext);
151        textView.setText(label);
152        textView.setTextAppearance(mContext, R.style.TextAppearance_EventInfo_Label);
153        textView.setTextColor(Color.BLACK);
154        textView.setClickable(false);
155        return textView;
156    }
157
158    // Add the number of attendees in the specific status (corresponding to the divider) in
159    // parenthesis next to the label
160    private void updateDividerViewLabel(View divider, CharSequence label, int count) {
161        if (count <= 0) {
162            ((TextView)divider).setText(label);
163        }
164        else {
165            ((TextView)divider).setText(label + " (" + count + ")");
166        }
167    }
168
169
170    /**
171     * Inflates a layout for a given attendee view and set up each element in it, and returns
172     * the constructed View object. The object is also stored in {@link AttendeeItem#mView}.
173     */
174    private View constructAttendeeView(AttendeeItem item) {
175        item.mView = mInflater.inflate(R.layout.contact_item, null);
176        return updateAttendeeView(item);
177    }
178
179    /**
180     * Set up each element in {@link AttendeeItem#mView} using the latest information. View
181     * object is reused.
182     */
183    private View updateAttendeeView(AttendeeItem item) {
184        final Attendee attendee = item.mAttendee;
185        final View view = item.mView;
186        final TextView nameView = (TextView) view.findViewById(R.id.name);
187        nameView.setText(TextUtils.isEmpty(attendee.mName) ? attendee.mEmail : attendee.mName);
188        if (item.mRemoved) {
189            nameView.setPaintFlags(Paint.STRIKE_THRU_TEXT_FLAG | nameView.getPaintFlags());
190        } else {
191            nameView.setPaintFlags((~Paint.STRIKE_THRU_TEXT_FLAG) & nameView.getPaintFlags());
192        }
193
194        // Set up the Image button even if the view is disabled
195        // Everything will be ready when the view is enabled later
196        final ImageButton button = (ImageButton) view.findViewById(R.id.contact_remove);
197        button.setVisibility(isEnabled() ? View.VISIBLE : View.GONE);
198        button.setTag(item);
199        if (item.mRemoved) {
200            button.setImageResource(R.drawable.ic_menu_add_field_holo_light);
201            button.setContentDescription(mContext.getString(R.string.accessibility_add_attendee));
202        } else {
203            button.setImageResource(R.drawable.ic_menu_remove_field_holo_light);
204            button.setContentDescription(mContext.
205                    getString(R.string.accessibility_remove_attendee));
206        }
207        button.setOnClickListener(this);
208
209        final QuickContactBadge badge = (QuickContactBadge) view.findViewById(R.id.badge);
210        if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) {
211            item.mBadge.setAlpha(mNoResponsePhotoAlpha);
212        } else {
213            item.mBadge.setAlpha(mDefaultPhotoAlpha);
214        }
215        if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
216            item.mBadge.setColorFilter(mGrayscaleFilter);
217        } else {
218            item.mBadge.setColorFilter(null);
219        }
220        badge.setImageDrawable(item.mBadge);
221        badge.assignContactFromEmail(item.mAttendee.mEmail, true);
222        badge.setMaxHeight(60);
223        if (item.mPresence != -1) {
224            final ImageView presence = (ImageView) view.findViewById(R.id.presence);
225            presence.setImageResource(StatusUpdates.getPresenceIconResourceId(item.mPresence));
226            presence.setVisibility(View.VISIBLE);
227        }
228
229        return view;
230    }
231
232    public boolean contains(Attendee attendee) {
233        final int size = getChildCount();
234        for (int i = 0; i < size; i++) {
235            final View view = getChildAt(i);
236            if (view instanceof TextView) { // divider
237                continue;
238            }
239            AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
240            if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) {
241                return true;
242            }
243        }
244        return false;
245    }
246
247
248    private void addOneAttendee(Attendee attendee) {
249        if (contains(attendee)) {
250            return;
251        }
252        final AttendeeItem item = new AttendeeItem(attendee, -1 /* presence */, mDefaultBadge);
253        final int status = attendee.mStatus;
254        final String name = attendee.mName == null ? "" : attendee.mName;
255        final int index;
256        switch (status) {
257            case Attendees.ATTENDEE_STATUS_ACCEPTED: {
258                final int startIndex = 0;
259                updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1);
260                if (mYes == 0) {
261                    addView(mDividerForYes, startIndex);
262                }
263                mYes++;
264                index = startIndex + mYes;
265                break;
266            }
267            case Attendees.ATTENDEE_STATUS_DECLINED: {
268                final int startIndex = (mYes == 0 ? 0 : 1 + mYes);
269                updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1);
270                if (mNo == 0) {
271                    addView(mDividerForNo, startIndex);
272                }
273                mNo++;
274                index = startIndex + mNo;
275                break;
276            }
277            case Attendees.ATTENDEE_STATUS_TENTATIVE: {
278                final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo);
279                updateDividerViewLabel(mDividerForMaybe, mEntries[2], mMaybe + 1);
280                if (mMaybe == 0) {
281                    addView(mDividerForMaybe, startIndex);
282                }
283                mMaybe++;
284                index = startIndex + mMaybe;
285                break;
286            }
287            default: {
288                final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo)
289                        + (mMaybe == 0 ? 0 : 1 + mMaybe);
290                // We delay adding the divider for "No response".
291                index = startIndex + mNoResponse;
292                mNoResponse++;
293                updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse);
294                break;
295            }
296        }
297
298        final View view = constructAttendeeView(item);
299        view.setTag(item);
300        addView(view, index);
301
302        // We want "No Response" divider only when
303        // - someone already answered in some way,
304        // - there is attendees not responding yet, and
305        // - divider isn't in the list yet
306        if (mYes + mNo + mMaybe > 0 && mNoResponse > 0 &&
307                mDividerForNoResponse.getParent() == null) {
308            final int dividerIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) +
309                    (mMaybe == 0 ? 0 : 1 + mMaybe);
310            addView(mDividerForNoResponse, dividerIndex);
311        }
312
313        mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item,
314                CONTACT_DATA_WITH_PRESENCE_URI, PRESENCE_PROJECTION, CONTACT_DATA_SELECTION,
315                new String[] { attendee.mEmail }, null);
316    }
317
318    public void addAttendees(ArrayList<Attendee> attendees) {
319        synchronized (this) {
320            for (final Attendee attendee : attendees) {
321                addOneAttendee(attendee);
322            }
323        }
324    }
325
326    public void addAttendees(HashMap<String, Attendee> attendees) {
327        synchronized (this) {
328            for (final Attendee attendee : attendees.values()) {
329                addOneAttendee(attendee);
330            }
331        }
332    }
333
334    public void addAttendees(String attendees) {
335        final LinkedHashSet<Rfc822Token> addresses =
336                EditEventHelper.getAddressesFromList(attendees, mValidator);
337        synchronized (this) {
338            for (final Rfc822Token address : addresses) {
339                final Attendee attendee = new Attendee(address.getName(), address.getAddress());
340                if (TextUtils.isEmpty(attendee.mName)) {
341                    attendee.mName = attendee.mEmail;
342                }
343                addOneAttendee(attendee);
344            }
345        }
346    }
347
348    /**
349     * Returns true when the attendee at that index is marked as "removed" (the name of
350     * the attendee is shown with a strike through line).
351     */
352    public boolean isMarkAsRemoved(int index) {
353        final View view = getChildAt(index);
354        if (view instanceof TextView) { // divider
355            return false;
356        }
357        return ((AttendeeItem) view.getTag()).mRemoved;
358    }
359
360    // TODO put this into a Loader for auto-requeries
361    private class PresenceQueryHandler extends AsyncQueryHandler {
362        public PresenceQueryHandler(ContentResolver cr) {
363            super(cr);
364        }
365
366        @Override
367        protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
368            if (cursor == null || cookie == null) {
369                if (DEBUG) {
370                    Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie);
371                }
372                return;
373            }
374
375            final AttendeeItem item = (AttendeeItem)cookie;
376            try {
377                cursor.moveToPosition(-1);
378                boolean found = false;
379                int contactId = 0;
380                int photoId = 0;
381                int presence = 0;
382                while (cursor.moveToNext()) {
383                    String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
384                    int temp = 0;
385                    temp = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX);
386                    // A photo id must be > 0 and we only care about the contact
387                    // ID if there's a photo
388                    if (temp > 0) {
389                        photoId = temp;
390                        contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX);
391                    }
392                    // Take the most available status we can find.
393                    presence = Math.max(
394                            cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX), presence);
395
396                    found = true;
397                    if (DEBUG) {
398                        Log.d(TAG,
399                                "onQueryComplete Id: " + contactId + " PhotoId: " + photoId
400                                        + " Email: " + email + " updateCount:" + item.mUpdateCounts
401                                        + " Presence:" + item.mPresence);
402                    }
403                }
404                if (found) {
405                    item.mPresence = presence;
406
407                    if (photoId > 0 && item.mUpdateCounts < queryIndex) {
408                        item.mUpdateCounts = queryIndex;
409                        final Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
410                                contactId);
411                        // Query for this contacts picture
412                        ContactsAsyncHelper.retrieveContactPhotoAsync(
413                                mContext, item, new Runnable() {
414                                    public void run() {
415                                        updateAttendeeView(item);
416                                    }
417                                }, personUri);
418                    }
419                }
420            } finally {
421                cursor.close();
422            }
423        }
424    }
425
426    public Attendee getItem(int index) {
427        final View view = getChildAt(index);
428        if (view instanceof TextView) { // divider
429            return null;
430        }
431        return ((AttendeeItem) view.getTag()).mAttendee;
432    }
433
434    @Override
435    public void onClick(View view) {
436        // Button corresponding to R.id.contact_remove.
437        final AttendeeItem item = (AttendeeItem) view.getTag();
438        item.mRemoved = !item.mRemoved;
439        updateAttendeeView(item);
440    }
441}
442