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