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