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.Utils;
23import com.android.calendar.event.EditEventHelper.AttendeeItem;
24import com.android.common.Rfc822Validator;
25
26import android.content.AsyncQueryHandler;
27import android.content.ContentResolver;
28import android.content.ContentUris;
29import android.content.Context;
30import android.content.res.Resources;
31import android.database.Cursor;
32import android.graphics.ColorMatrix;
33import android.graphics.ColorMatrixColorFilter;
34import android.graphics.Paint;
35import android.graphics.drawable.Drawable;
36import android.net.Uri;
37import android.provider.CalendarContract.Attendees;
38import android.provider.ContactsContract.CommonDataKinds.Email;
39import android.provider.ContactsContract.CommonDataKinds.Identity;
40import android.provider.ContactsContract.Contacts;
41import android.provider.ContactsContract.Data;
42import android.provider.ContactsContract.RawContacts;
43import android.text.TextUtils;
44import android.text.util.Rfc822Token;
45import android.util.AttributeSet;
46import android.util.Log;
47import android.view.LayoutInflater;
48import android.view.View;
49import android.widget.ImageButton;
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 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
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
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
143    public void setRfc822Validator(Rfc822Validator validator) {
144        mValidator = validator;
145    }
146
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
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     */
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     */
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
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
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
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
369    public void addAttendees(ArrayList<Attendee> attendees) {
370        synchronized (this) {
371            for (final Attendee attendee : attendees) {
372                addOneAttendee(attendee);
373            }
374        }
375    }
376
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
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     */
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 {
413        public PresenceQueryHandler(ContentResolver cr) {
414            super(cr);
415        }
416
417        @Override
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
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
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