1/*
2 * Copyright (C) 2009 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 android.widget;
18
19import com.android.internal.R;
20
21import android.content.AsyncQueryHandler;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.TypedArray;
26import android.database.Cursor;
27import android.graphics.Canvas;
28import android.graphics.drawable.Drawable;
29import android.net.Uri;
30import android.os.Bundle;
31import android.provider.ContactsContract.CommonDataKinds.Email;
32import android.provider.ContactsContract.Contacts;
33import android.provider.ContactsContract.Intents;
34import android.provider.ContactsContract.PhoneLookup;
35import android.provider.ContactsContract.QuickContact;
36import android.provider.ContactsContract.RawContacts;
37import android.util.AttributeSet;
38import android.view.View;
39import android.view.View.OnClickListener;
40import android.view.accessibility.AccessibilityEvent;
41import android.view.accessibility.AccessibilityNodeInfo;
42
43/**
44 * Widget used to show an image with the standard QuickContact badge
45 * and on-click behavior.
46 */
47public class QuickContactBadge extends ImageView implements OnClickListener {
48    private Uri mContactUri;
49    private String mContactEmail;
50    private String mContactPhone;
51    private Drawable mOverlay;
52    private QueryHandler mQueryHandler;
53    private Drawable mDefaultAvatar;
54    private Bundle mExtras = null;
55
56    protected String[] mExcludeMimes = null;
57
58    static final private int TOKEN_EMAIL_LOOKUP = 0;
59    static final private int TOKEN_PHONE_LOOKUP = 1;
60    static final private int TOKEN_EMAIL_LOOKUP_AND_TRIGGER = 2;
61    static final private int TOKEN_PHONE_LOOKUP_AND_TRIGGER = 3;
62
63    static final private String EXTRA_URI_CONTENT = "uri_content";
64
65    static final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
66        RawContacts.CONTACT_ID,
67        Contacts.LOOKUP_KEY,
68    };
69    static final int EMAIL_ID_COLUMN_INDEX = 0;
70    static final int EMAIL_LOOKUP_STRING_COLUMN_INDEX = 1;
71
72    static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
73        PhoneLookup._ID,
74        PhoneLookup.LOOKUP_KEY,
75    };
76    static final int PHONE_ID_COLUMN_INDEX = 0;
77    static final int PHONE_LOOKUP_STRING_COLUMN_INDEX = 1;
78
79    public QuickContactBadge(Context context) {
80        this(context, null);
81    }
82
83    public QuickContactBadge(Context context, AttributeSet attrs) {
84        this(context, attrs, 0);
85    }
86
87    public QuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) {
88        this(context, attrs, defStyleAttr, 0);
89    }
90
91    public QuickContactBadge(
92            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
93        super(context, attrs, defStyleAttr, defStyleRes);
94
95        TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
96        mOverlay = styledAttributes.getDrawable(
97                com.android.internal.R.styleable.Theme_quickContactBadgeOverlay);
98        styledAttributes.recycle();
99
100        if (!isInEditMode()) {
101            mQueryHandler = new QueryHandler(mContext.getContentResolver());
102        }
103        setOnClickListener(this);
104    }
105
106    @Override
107    protected void drawableStateChanged() {
108        super.drawableStateChanged();
109        if (mOverlay != null && mOverlay.isStateful()) {
110            mOverlay.setState(getDrawableState());
111            invalidate();
112        }
113    }
114
115    @Override
116    public void drawableHotspotChanged(float x, float y) {
117        super.drawableHotspotChanged(x, y);
118
119        if (mOverlay != null) {
120            mOverlay.setHotspot(x, y);
121        }
122    }
123
124    /** This call has no effect anymore, as there is only one QuickContact mode */
125    @SuppressWarnings("unused")
126    public void setMode(int size) {
127    }
128
129    @Override
130    protected void onDraw(Canvas canvas) {
131        super.onDraw(canvas);
132
133        if (!isEnabled()) {
134            // not clickable? don't show triangle
135            return;
136        }
137
138        if (mOverlay == null || mOverlay.getIntrinsicWidth() == 0 ||
139                mOverlay.getIntrinsicHeight() == 0) {
140            // nothing to draw
141            return;
142        }
143
144        mOverlay.setBounds(0, 0, getWidth(), getHeight());
145
146        if (mPaddingTop == 0 && mPaddingLeft == 0) {
147            mOverlay.draw(canvas);
148        } else {
149            int saveCount = canvas.getSaveCount();
150            canvas.save();
151            canvas.translate(mPaddingLeft, mPaddingTop);
152            mOverlay.draw(canvas);
153            canvas.restoreToCount(saveCount);
154        }
155    }
156
157    /** True if a contact, an email address or a phone number has been assigned */
158    private boolean isAssigned() {
159        return mContactUri != null || mContactEmail != null || mContactPhone != null;
160    }
161
162    /**
163     * Resets the contact photo to the default state.
164     */
165    public void setImageToDefault() {
166        if (mDefaultAvatar == null) {
167            mDefaultAvatar = mContext.getDrawable(R.drawable.ic_contact_picture);
168        }
169        setImageDrawable(mDefaultAvatar);
170    }
171
172    /**
173     * Assign the contact uri that this QuickContactBadge should be associated
174     * with. Note that this is only used for displaying the QuickContact window and
175     * won't bind the contact's photo for you. Call {@link #setImageDrawable(Drawable)} to set the
176     * photo.
177     *
178     * @param contactUri Either a {@link Contacts#CONTENT_URI} or
179     *            {@link Contacts#CONTENT_LOOKUP_URI} style URI.
180     */
181    public void assignContactUri(Uri contactUri) {
182        mContactUri = contactUri;
183        mContactEmail = null;
184        mContactPhone = null;
185        onContactUriChanged();
186    }
187
188    /**
189     * Assign a contact based on an email address. This should only be used when
190     * the contact's URI is not available, as an extra query will have to be
191     * performed to lookup the URI based on the email.
192     *
193     * @param emailAddress The email address of the contact.
194     * @param lazyLookup If this is true, the lookup query will not be performed
195     * until this view is clicked.
196     */
197    public void assignContactFromEmail(String emailAddress, boolean lazyLookup) {
198        assignContactFromEmail(emailAddress, lazyLookup, null);
199    }
200
201    /**
202     * Assign a contact based on an email address. This should only be used when
203     * the contact's URI is not available, as an extra query will have to be
204     * performed to lookup the URI based on the email.
205
206     @param emailAddress The email address of the contact.
207     @param lazyLookup If this is true, the lookup query will not be performed
208     until this view is clicked.
209     @param extras A bundle of extras to populate the contact edit page with if the contact
210     is not found and the user chooses to add the email address to an existing contact or
211     create a new contact. Uses the same string constants as those found in
212     {@link android.provider.ContactsContract.Intents.Insert}
213    */
214
215    public void assignContactFromEmail(String emailAddress, boolean lazyLookup, Bundle extras) {
216        mContactEmail = emailAddress;
217        mExtras = extras;
218        if (!lazyLookup && mQueryHandler != null) {
219            mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, null,
220                    Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
221                    EMAIL_LOOKUP_PROJECTION, null, null, null);
222        } else {
223            mContactUri = null;
224            onContactUriChanged();
225        }
226    }
227
228
229    /**
230     * Assign a contact based on a phone number. This should only be used when
231     * the contact's URI is not available, as an extra query will have to be
232     * performed to lookup the URI based on the phone number.
233     *
234     * @param phoneNumber The phone number of the contact.
235     * @param lazyLookup If this is true, the lookup query will not be performed
236     * until this view is clicked.
237     */
238    public void assignContactFromPhone(String phoneNumber, boolean lazyLookup) {
239        assignContactFromPhone(phoneNumber, lazyLookup, new Bundle());
240    }
241
242    /**
243     * Assign a contact based on a phone number. This should only be used when
244     * the contact's URI is not available, as an extra query will have to be
245     * performed to lookup the URI based on the phone number.
246     *
247     * @param phoneNumber The phone number of the contact.
248     * @param lazyLookup If this is true, the lookup query will not be performed
249     * until this view is clicked.
250     * @param extras A bundle of extras to populate the contact edit page with if the contact
251     * is not found and the user chooses to add the phone number to an existing contact or
252     * create a new contact. Uses the same string constants as those found in
253     * {@link android.provider.ContactsContract.Intents.Insert}
254     */
255    public void assignContactFromPhone(String phoneNumber, boolean lazyLookup, Bundle extras) {
256        mContactPhone = phoneNumber;
257        mExtras = extras;
258        if (!lazyLookup && mQueryHandler != null) {
259            mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, null,
260                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
261                    PHONE_LOOKUP_PROJECTION, null, null, null);
262        } else {
263            mContactUri = null;
264            onContactUriChanged();
265        }
266    }
267
268    /**
269     * Assigns the drawable that is to be drawn on top of the assigned contact photo.
270     *
271     * @param overlay Drawable to be drawn over the assigned contact photo. Must have a non-zero
272     *         instrinsic width and height.
273     */
274    public void setOverlay(Drawable overlay) {
275        mOverlay = overlay;
276    }
277
278    private void onContactUriChanged() {
279        setEnabled(isAssigned());
280    }
281
282    @Override
283    public void onClick(View v) {
284        // If contact has been assigned, mExtras should no longer be null, but do a null check
285        // anyway just in case assignContactFromPhone or Email was called with a null bundle or
286        // wasn't assigned previously.
287        final Bundle extras = (mExtras == null) ? new Bundle() : mExtras;
288        if (mContactUri != null) {
289            QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
290                    QuickContact.MODE_LARGE, mExcludeMimes);
291        } else if (mContactEmail != null && mQueryHandler != null) {
292            extras.putString(EXTRA_URI_CONTENT, mContactEmail);
293            mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, extras,
294                    Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
295                    EMAIL_LOOKUP_PROJECTION, null, null, null);
296        } else if (mContactPhone != null && mQueryHandler != null) {
297            extras.putString(EXTRA_URI_CONTENT, mContactPhone);
298            mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, extras,
299                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
300                    PHONE_LOOKUP_PROJECTION, null, null, null);
301        } else {
302            // If a contact hasn't been assigned, don't react to click.
303            return;
304        }
305    }
306
307    @Override
308    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
309        super.onInitializeAccessibilityEvent(event);
310        event.setClassName(QuickContactBadge.class.getName());
311    }
312
313    @Override
314    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
315        super.onInitializeAccessibilityNodeInfo(info);
316        info.setClassName(QuickContactBadge.class.getName());
317    }
318
319    /**
320     * Set a list of specific MIME-types to exclude and not display. For
321     * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
322     * profile icon.
323     */
324    public void setExcludeMimes(String[] excludeMimes) {
325        mExcludeMimes = excludeMimes;
326    }
327
328    private class QueryHandler extends AsyncQueryHandler {
329
330        public QueryHandler(ContentResolver cr) {
331            super(cr);
332        }
333
334        @Override
335        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
336            Uri lookupUri = null;
337            Uri createUri = null;
338            boolean trigger = false;
339            Bundle extras = (cookie != null) ? (Bundle) cookie : new Bundle();
340            try {
341                switch(token) {
342                    case TOKEN_PHONE_LOOKUP_AND_TRIGGER:
343                        trigger = true;
344                        createUri = Uri.fromParts("tel", extras.getString(EXTRA_URI_CONTENT), null);
345
346                        //$FALL-THROUGH$
347                    case TOKEN_PHONE_LOOKUP: {
348                        if (cursor != null && cursor.moveToFirst()) {
349                            long contactId = cursor.getLong(PHONE_ID_COLUMN_INDEX);
350                            String lookupKey = cursor.getString(PHONE_LOOKUP_STRING_COLUMN_INDEX);
351                            lookupUri = Contacts.getLookupUri(contactId, lookupKey);
352                        }
353
354                        break;
355                    }
356                    case TOKEN_EMAIL_LOOKUP_AND_TRIGGER:
357                        trigger = true;
358                        createUri = Uri.fromParts("mailto",
359                                extras.getString(EXTRA_URI_CONTENT), null);
360
361                        //$FALL-THROUGH$
362                    case TOKEN_EMAIL_LOOKUP: {
363                        if (cursor != null && cursor.moveToFirst()) {
364                            long contactId = cursor.getLong(EMAIL_ID_COLUMN_INDEX);
365                            String lookupKey = cursor.getString(EMAIL_LOOKUP_STRING_COLUMN_INDEX);
366                            lookupUri = Contacts.getLookupUri(contactId, lookupKey);
367                        }
368                        break;
369                    }
370                }
371            } finally {
372                if (cursor != null) {
373                    cursor.close();
374                }
375            }
376
377            mContactUri = lookupUri;
378            onContactUriChanged();
379
380            if (trigger && lookupUri != null) {
381                // Found contact, so trigger QuickContact
382                QuickContact.showQuickContact(getContext(), QuickContactBadge.this, lookupUri,
383                        QuickContact.MODE_LARGE, mExcludeMimes);
384            } else if (createUri != null) {
385                // Prompt user to add this person to contacts
386                final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, createUri);
387                if (extras != null) {
388                    extras.remove(EXTRA_URI_CONTENT);
389                    intent.putExtras(extras);
390                }
391                getContext().startActivity(intent);
392            }
393        }
394    }
395}
396