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