AttendeesView.java revision 4c5475e6d27497be020d3098c4554fe353d19d38
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        } else {
202            button.setImageResource(R.drawable.ic_menu_remove_field_holo_light);
203        }
204        button.setOnClickListener(this);
205
206        final QuickContactBadge badge = (QuickContactBadge) view.findViewById(R.id.badge);
207        if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_NONE) {
208            item.mBadge.setAlpha(mNoResponsePhotoAlpha);
209        } else {
210            item.mBadge.setAlpha(mDefaultPhotoAlpha);
211        }
212        if (item.mAttendee.mStatus == Attendees.ATTENDEE_STATUS_DECLINED) {
213            item.mBadge.setColorFilter(mGrayscaleFilter);
214        } else {
215            item.mBadge.setColorFilter(null);
216        }
217        badge.setImageDrawable(item.mBadge);
218        badge.assignContactFromEmail(item.mAttendee.mEmail, true);
219        badge.setMaxHeight(60);
220        if (item.mPresence != -1) {
221            final ImageView presence = (ImageView) view.findViewById(R.id.presence);
222            presence.setImageResource(StatusUpdates.getPresenceIconResourceId(item.mPresence));
223            presence.setVisibility(View.VISIBLE);
224        }
225
226        return view;
227    }
228
229    public boolean contains(Attendee attendee) {
230        final int size = getChildCount();
231        for (int i = 0; i < size; i++) {
232            final View view = getChildAt(i);
233            if (view instanceof TextView) { // divider
234                continue;
235            }
236            AttendeeItem attendeeItem = (AttendeeItem) view.getTag();
237            if (TextUtils.equals(attendee.mEmail, attendeeItem.mAttendee.mEmail)) {
238                return true;
239            }
240        }
241        return false;
242    }
243
244
245    private void addOneAttendee(Attendee attendee) {
246        if (contains(attendee)) {
247            return;
248        }
249        final AttendeeItem item = new AttendeeItem(attendee, -1 /* presence */, mDefaultBadge);
250        final int status = attendee.mStatus;
251        final String name = attendee.mName == null ? "" : attendee.mName;
252        final int index;
253        switch (status) {
254            case Attendees.ATTENDEE_STATUS_ACCEPTED: {
255                final int startIndex = 0;
256                updateDividerViewLabel(mDividerForYes, mEntries[1], mYes + 1);
257                if (mYes == 0) {
258                    addView(mDividerForYes, startIndex);
259                }
260                mYes++;
261                index = startIndex + mYes;
262                break;
263            }
264            case Attendees.ATTENDEE_STATUS_DECLINED: {
265                final int startIndex = (mYes == 0 ? 0 : 1 + mYes);
266                updateDividerViewLabel(mDividerForNo, mEntries[3], mNo + 1);
267                if (mNo == 0) {
268                    addView(mDividerForNo, startIndex);
269                }
270                mNo++;
271                index = startIndex + mNo;
272                break;
273            }
274            case Attendees.ATTENDEE_STATUS_TENTATIVE: {
275                final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo);
276                updateDividerViewLabel(mDividerForMaybe, mEntries[2], mMaybe + 1);
277                if (mMaybe == 0) {
278                    addView(mDividerForMaybe, startIndex);
279                }
280                mMaybe++;
281                index = startIndex + mMaybe;
282                break;
283            }
284            default: {
285                final int startIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo)
286                        + (mMaybe == 0 ? 0 : 1 + mMaybe);
287                // We delay adding the divider for "No response".
288                index = startIndex + mNoResponse;
289                mNoResponse++;
290                updateDividerViewLabel(mDividerForNoResponse, mEntries[0], mNoResponse);
291                break;
292            }
293        }
294
295        final View view = constructAttendeeView(item);
296        view.setTag(item);
297        addView(view, index);
298
299        // We want "No Response" divider only when
300        // - someone already answered in some way,
301        // - there is attendees not responding yet, and
302        // - divider isn't in the list yet
303        if (mYes + mNo + mMaybe > 0 && mNoResponse > 0 &&
304                mDividerForNoResponse.getParent() == null) {
305            final int dividerIndex = (mYes == 0 ? 0 : 1 + mYes) + (mNo == 0 ? 0 : 1 + mNo) +
306                    (mMaybe == 0 ? 0 : 1 + mMaybe);
307            addView(mDividerForNoResponse, dividerIndex);
308        }
309
310        mPresenceQueryHandler.startQuery(item.mUpdateCounts + 1, item,
311                CONTACT_DATA_WITH_PRESENCE_URI, PRESENCE_PROJECTION, CONTACT_DATA_SELECTION,
312                new String[] { attendee.mEmail }, null);
313    }
314
315    public void addAttendees(ArrayList<Attendee> attendees) {
316        synchronized (this) {
317            for (final Attendee attendee : attendees) {
318                addOneAttendee(attendee);
319            }
320        }
321    }
322
323    public void addAttendees(HashMap<String, Attendee> attendees) {
324        synchronized (this) {
325            for (final Attendee attendee : attendees.values()) {
326                addOneAttendee(attendee);
327            }
328        }
329    }
330
331    public void addAttendees(String attendees) {
332        final LinkedHashSet<Rfc822Token> addresses =
333                EditEventHelper.getAddressesFromList(attendees, mValidator);
334        synchronized (this) {
335            for (final Rfc822Token address : addresses) {
336                final Attendee attendee = new Attendee(address.getName(), address.getAddress());
337                if (TextUtils.isEmpty(attendee.mName)) {
338                    attendee.mName = attendee.mEmail;
339                }
340                addOneAttendee(attendee);
341            }
342        }
343    }
344
345    /**
346     * Returns true when the attendee at that index is marked as "removed" (the name of
347     * the attendee is shown with a strike through line).
348     */
349    public boolean isMarkAsRemoved(int index) {
350        final View view = getChildAt(index);
351        if (view instanceof TextView) { // divider
352            return false;
353        }
354        return ((AttendeeItem) view.getTag()).mRemoved;
355    }
356
357    // TODO put this into a Loader for auto-requeries
358    private class PresenceQueryHandler extends AsyncQueryHandler {
359        public PresenceQueryHandler(ContentResolver cr) {
360            super(cr);
361        }
362
363        @Override
364        protected void onQueryComplete(int queryIndex, Object cookie, Cursor cursor) {
365            if (cursor == null || cookie == null) {
366                if (DEBUG) {
367                    Log.d(TAG, "onQueryComplete: cursor=" + cursor + ", cookie=" + cookie);
368                }
369                return;
370            }
371
372            final AttendeeItem item = (AttendeeItem)cookie;
373            try {
374                cursor.moveToPosition(-1);
375                boolean found = false;
376                int contactId = 0;
377                int photoId = 0;
378                int presence = 0;
379                while (cursor.moveToNext()) {
380                    String email = cursor.getString(PRESENCE_PROJECTION_EMAIL_INDEX);
381                    int temp = 0;
382                    temp = cursor.getInt(PRESENCE_PROJECTION_PHOTO_ID_INDEX);
383                    // A photo id must be > 0 and we only care about the contact
384                    // ID if there's a photo
385                    if (temp > 0) {
386                        photoId = temp;
387                        contactId = cursor.getInt(PRESENCE_PROJECTION_CONTACT_ID_INDEX);
388                    }
389                    // Take the most available status we can find.
390                    presence = Math.max(
391                            cursor.getInt(PRESENCE_PROJECTION_PRESENCE_INDEX), presence);
392
393                    found = true;
394                    if (DEBUG) {
395                        Log.d(TAG,
396                                "onQueryComplete Id: " + contactId + " PhotoId: " + photoId
397                                        + " Email: " + email + " updateCount:" + item.mUpdateCounts
398                                        + " Presence:" + item.mPresence);
399                    }
400                }
401                if (found) {
402                    item.mPresence = presence;
403
404                    if (photoId > 0 && item.mUpdateCounts < queryIndex) {
405                        item.mUpdateCounts = queryIndex;
406                        final Uri personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI,
407                                contactId);
408                        // Query for this contacts picture
409                        ContactsAsyncHelper.retrieveContactPhotoAsync(
410                                mContext, item, new Runnable() {
411                                    public void run() {
412                                        updateAttendeeView(item);
413                                    }
414                                }, personUri);
415                    }
416                }
417            } finally {
418                cursor.close();
419            }
420        }
421    }
422
423    public Attendee getItem(int index) {
424        final View view = getChildAt(index);
425        if (view instanceof TextView) { // divider
426            return null;
427        }
428        return ((AttendeeItem) view.getTag()).mAttendee;
429    }
430
431    @Override
432    public void onClick(View view) {
433        // Button corresponding to R.id.contact_remove.
434        final AttendeeItem item = (AttendeeItem) view.getTag();
435        item.mRemoved = !item.mRemoved;
436        updateAttendeeView(item);
437    }
438}
439