1 /*
2  * Copyright (C) 2011 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 com.android.ex.chips;
18 
19 import android.net.Uri;
20 import android.provider.ContactsContract.CommonDataKinds.Email;
21 import android.provider.ContactsContract.DisplayNameSources;
22 import androidx.annotation.DrawableRes;
23 import android.text.util.Rfc822Token;
24 import android.text.util.Rfc822Tokenizer;
25 
26 /**
27  * Represents one entry inside recipient auto-complete list.
28  */
29 public class RecipientEntry {
30     /* package */ static final int INVALID_CONTACT = -1;
31     /**
32      * A GENERATED_CONTACT is one that was created based entirely on
33      * information passed in to the RecipientEntry from an external source
34      * that is not a real contact.
35      */
36     /* package */ static final int GENERATED_CONTACT = -2;
37 
38     /** Used when {@link #mDestinationType} is invalid and thus shouldn't be used for display. */
39     public static final int INVALID_DESTINATION_TYPE = -1;
40 
41     public static final int ENTRY_TYPE_PERSON = 0;
42 
43     /**
44      * Entry of this type represents the item in auto-complete that asks user to grant permissions
45      * to the app. This permission model is introduced in M platform.
46      *
47      * <p>Entries of this type should have {@link #mPermissions} set as well.
48      */
49     public static final int ENTRY_TYPE_PERMISSION_REQUEST = 1;
50 
51     public static final int ENTRY_TYPE_SIZE = 2;
52 
53     private final int mEntryType;
54 
55     /**
56      * True when this entry is the first entry in a group, which should have a photo and display
57      * name, while the second or later entries won't.
58      */
59     private boolean mIsFirstLevel;
60     private final String mDisplayName;
61 
62     /** Destination for this contact entry. Would be an email address or a phone number. */
63     private final String mDestination;
64     /** Type of the destination like {@link Email#TYPE_HOME} */
65     private final int mDestinationType;
66     /**
67      * Label of the destination which will be used when type was {@link Email#TYPE_CUSTOM}.
68      * Can be null when {@link #mDestinationType} is {@link #INVALID_DESTINATION_TYPE}.
69      */
70     private final String mDestinationLabel;
71     /** ID for the person */
72     private final long mContactId;
73     /** ID for the directory this contact came from, or <code>null</code> */
74     private final Long mDirectoryId;
75     /** ID for the destination */
76     private final long mDataId;
77 
78     private final Uri mPhotoThumbnailUri;
79     /** Configures showing the icon in the chip */
80     private final boolean mShouldDisplayIcon;
81 
82     private boolean mIsValid;
83     /**
84      * This can be updated after this object being constructed, when the photo is fetched
85      * from remote directories.
86      */
87     private byte[] mPhotoBytes;
88 
89     @DrawableRes private int mIndicatorIconId;
90     private String mIndicatorText;
91 
92     /** See {@link android.provider.ContactsContract.ContactsColumns#LOOKUP_KEY} */
93     private final String mLookupKey;
94 
95     /** Should be used when type is {@link #ENTRY_TYPE_PERMISSION_REQUEST}. */
96     private final String[] mPermissions;
97 
98     /** Whether RecipientEntry is in a replaced chip or not. */
99     private boolean mInReplacedChip;
100 
RecipientEntry(int entryType, String displayName, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid, String lookupKey, String[] permissions)101     protected RecipientEntry(int entryType, String displayName, String destination,
102         int destinationType, String destinationLabel, long contactId, Long directoryId,
103         long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid,
104         String lookupKey, String[] permissions) {
105         this(entryType, displayName, destination, destinationType,
106             destinationLabel, contactId, directoryId, dataId, photoThumbnailUri,
107             true /* shouldDisplayIcon */, isFirstLevel, isValid, lookupKey, permissions);
108     }
109 
RecipientEntry(int entryType, String displayName, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, Uri photoThumbnailUri, boolean shouldDisplayIcon, boolean isFirstLevel, boolean isValid, String lookupKey, String[] permissions)110     protected RecipientEntry(int entryType, String displayName, String destination,
111             int destinationType, String destinationLabel, long contactId, Long directoryId,
112             long dataId, Uri photoThumbnailUri, boolean shouldDisplayIcon,
113             boolean isFirstLevel, boolean isValid, String lookupKey, String[] permissions) {
114         mEntryType = entryType;
115         mIsFirstLevel = isFirstLevel;
116         mDisplayName = displayName;
117         mDestination = destination;
118         mDestinationType = destinationType;
119         mDestinationLabel = destinationLabel;
120         mContactId = contactId;
121         mDirectoryId = directoryId;
122         mDataId = dataId;
123         mPhotoThumbnailUri = photoThumbnailUri;
124         mShouldDisplayIcon = shouldDisplayIcon;
125         mPhotoBytes = null;
126         mIsValid = isValid;
127         mLookupKey = lookupKey;
128         mIndicatorIconId = 0;
129         mIndicatorText = null;
130         mPermissions = permissions;
131     }
132 
RecipientEntry(int entryType, String displayName, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid, String lookupKey)133     protected RecipientEntry(int entryType, String displayName, String destination,
134             int destinationType, String destinationLabel, long contactId, Long directoryId,
135             long dataId, Uri photoThumbnailUri, boolean isFirstLevel, boolean isValid,
136             String lookupKey) {
137         this(entryType, displayName, destination, destinationType, destinationLabel,
138                 contactId, directoryId, dataId, photoThumbnailUri, isFirstLevel, isValid,
139                 lookupKey, null);
140     }
141 
isValid()142     public boolean isValid() {
143         return mIsValid;
144     }
145 
146     /**
147      * Determine if this was a RecipientEntry created from recipient info or
148      * an entry from contacts.
149      */
isCreatedRecipient(long id)150     public static boolean isCreatedRecipient(long id) {
151         return id == RecipientEntry.INVALID_CONTACT || id == RecipientEntry.GENERATED_CONTACT;
152     }
153 
154     /**
155      * Construct a RecipientEntry from just an address that has been entered.
156      * This address has not been resolved to a contact and therefore does not
157      * have a contact id or photo.
158      */
constructFakeEntry(final String address, final boolean isValid)159     public static RecipientEntry constructFakeEntry(final String address, final boolean isValid) {
160         final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
161         final String tokenizedAddress = tokens.length > 0 ? tokens[0].getAddress() : address;
162 
163         return new RecipientEntry(ENTRY_TYPE_PERSON, tokenizedAddress, tokenizedAddress,
164                 INVALID_DESTINATION_TYPE, null, INVALID_CONTACT, null /* directoryId */,
165                 INVALID_CONTACT, null, true, isValid, null /* lookupKey */, null /* permissions */);
166     }
167 
168     /**
169      * Construct a RecipientEntry from just a phone number.
170      */
constructFakePhoneEntry(final String phoneNumber, final boolean isValid)171     public static RecipientEntry constructFakePhoneEntry(final String phoneNumber,
172             final boolean isValid) {
173         return new RecipientEntry(ENTRY_TYPE_PERSON, phoneNumber, phoneNumber,
174                 INVALID_DESTINATION_TYPE, null, INVALID_CONTACT, null /* directoryId */,
175                 INVALID_CONTACT, null, true, isValid, null /* lookupKey */, null /* permissions */);
176     }
177 
178     /**
179      * Construct a RecipientEntry from just an address that has been entered
180      * with both an associated display name. This address has not been resolved
181      * to a contact and therefore does not have a contact id or photo.
182      */
constructGeneratedEntry(String display, String address, boolean isValid)183     public static RecipientEntry constructGeneratedEntry(String display, String address,
184             boolean isValid) {
185         return new RecipientEntry(ENTRY_TYPE_PERSON, display, address, INVALID_DESTINATION_TYPE,
186                 null, GENERATED_CONTACT, null /* directoryId */, GENERATED_CONTACT, null, true,
187                 isValid, null /* lookupKey */, null /* permissions */);
188     }
189 
constructTopLevelEntry(String displayName, int displayNameSource, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, Uri photoThumbnailUri, boolean isValid, String lookupKey)190     public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource,
191             String destination, int destinationType, String destinationLabel, long contactId,
192             Long directoryId, long dataId, Uri photoThumbnailUri, boolean isValid,
193             String lookupKey) {
194         return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
195                 displayName, destination), destination, destinationType, destinationLabel,
196                 contactId, directoryId, dataId, photoThumbnailUri, true, isValid, lookupKey,
197                 null /* permissions */);
198     }
199 
constructTopLevelEntry(String displayName, int displayNameSource, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, String thumbnailUriAsString, boolean isValid, String lookupKey)200     public static RecipientEntry constructTopLevelEntry(String displayName, int displayNameSource,
201             String destination, int destinationType, String destinationLabel, long contactId,
202             Long directoryId, long dataId, String thumbnailUriAsString, boolean isValid,
203             String lookupKey) {
204         return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
205                 displayName, destination), destination, destinationType, destinationLabel,
206                 contactId, directoryId, dataId, (thumbnailUriAsString != null
207                 ? Uri.parse(thumbnailUriAsString) : null), true, isValid, lookupKey,
208                 null /* permissions */);
209     }
210 
constructSecondLevelEntry(String displayName, int displayNameSource, String destination, int destinationType, String destinationLabel, long contactId, Long directoryId, long dataId, String thumbnailUriAsString, boolean isValid, String lookupKey)211     public static RecipientEntry constructSecondLevelEntry(String displayName,
212             int displayNameSource, String destination, int destinationType,
213             String destinationLabel, long contactId, Long directoryId, long dataId,
214             String thumbnailUriAsString, boolean isValid, String lookupKey) {
215         return new RecipientEntry(ENTRY_TYPE_PERSON, pickDisplayName(displayNameSource,
216                 displayName, destination), destination, destinationType, destinationLabel,
217                 contactId, directoryId, dataId, (thumbnailUriAsString != null
218                 ? Uri.parse(thumbnailUriAsString) : null), false, isValid, lookupKey,
219                 null /* permissions */);
220     }
221 
constructPermissionEntry(String[] permissions)222     public static RecipientEntry constructPermissionEntry(String[] permissions) {
223         return new RecipientEntry(
224                 ENTRY_TYPE_PERMISSION_REQUEST,
225                 "" /* displayName */,
226                 "" /* destination */,
227                 Email.TYPE_CUSTOM,
228                 "" /* destinationLabel */,
229                 INVALID_CONTACT,
230                 null /* directoryId */,
231                 INVALID_CONTACT,
232                 null /* photoThumbnailUri */,
233                 true /* isFirstLevel*/,
234                 false /* isValid */,
235                 null /* lookupKey */,
236                 permissions);
237     }
238 
239     /**
240      * @return the display name for the entry.  If the display name source is larger than
241      * {@link DisplayNameSources#PHONE} we use the contact's display name, but if not,
242      * i.e. the display name came from an email address or a phone number, we don't use it
243      * to avoid confusion and just use the destination instead.
244      */
pickDisplayName(int displayNameSource, String displayName, String destination)245     private static String pickDisplayName(int displayNameSource, String displayName,
246             String destination) {
247         return (displayNameSource > DisplayNameSources.PHONE) ? displayName : destination;
248     }
249 
getEntryType()250     public int getEntryType() {
251         return mEntryType;
252     }
253 
getDisplayName()254     public String getDisplayName() {
255         return mDisplayName;
256     }
257 
getDestination()258     public String getDestination() {
259         return mDestination;
260     }
261 
getDestinationType()262     public int getDestinationType() {
263         return mDestinationType;
264     }
265 
getDestinationLabel()266     public String getDestinationLabel() {
267         return mDestinationLabel;
268     }
269 
getContactId()270     public long getContactId() {
271         return mContactId;
272     }
273 
getDirectoryId()274     public Long getDirectoryId() {
275         return mDirectoryId;
276     }
277 
getDataId()278     public long getDataId() {
279         return mDataId;
280     }
281 
isFirstLevel()282     public boolean isFirstLevel() {
283         return mIsFirstLevel;
284     }
285 
getPhotoThumbnailUri()286     public Uri getPhotoThumbnailUri() {
287         return mPhotoThumbnailUri;
288     }
289 
290     /** Indicates whether the icon in the chip is displayed or not. */
shouldDisplayIcon()291     public boolean shouldDisplayIcon() {
292         return mShouldDisplayIcon;
293     }
294 
295     /** This can be called outside main Looper thread. */
setPhotoBytes(byte[] photoBytes)296     public synchronized void setPhotoBytes(byte[] photoBytes) {
297         mPhotoBytes = photoBytes;
298     }
299 
300     /** This can be called outside main Looper thread. */
getPhotoBytes()301     public synchronized byte[] getPhotoBytes() {
302         return mPhotoBytes;
303     }
304 
305     /**
306      * Used together with {@link #ENTRY_TYPE_PERMISSION_REQUEST} and indicates what permissions we
307      * need to ask user to grant.
308      */
getPermissions()309     public String[] getPermissions() {
310         return mPermissions;
311     }
312 
getLookupKey()313     public String getLookupKey() {
314         return mLookupKey;
315     }
316 
isSelectable()317     public boolean isSelectable() {
318         return mEntryType == ENTRY_TYPE_PERSON || mEntryType == ENTRY_TYPE_PERMISSION_REQUEST;
319     }
320 
321     @Override
toString()322     public String toString() {
323         return mDisplayName + " <" + mDestination + ">, isValid=" + mIsValid;
324     }
325 
326     /**
327      * Returns if entry represents the same person as this instance. The default implementation
328      * checks whether the contact ids are the same, and subclasses may opt to override this.
329      */
isSamePerson(final RecipientEntry entry)330     public boolean isSamePerson(final RecipientEntry entry) {
331         return entry != null && mContactId == entry.mContactId;
332     }
333 
334     /**
335      * Returns the resource ID for the indicator icon, or 0 if no icon should be displayed.
336      */
337     @DrawableRes
getIndicatorIconId()338     public int getIndicatorIconId() {
339         return mIndicatorIconId;
340     }
341 
342     /**
343      * Sets the indicator icon to the given resource ID.  Set to 0 to display no icon.
344      */
setIndicatorIconId(@rawableRes int indicatorIconId)345     public void setIndicatorIconId(@DrawableRes int indicatorIconId) {
346         mIndicatorIconId = indicatorIconId;
347     }
348 
349     /**
350      * Get the indicator text, or null if no text should be displayed.
351      */
getIndicatorText()352     public String getIndicatorText() {
353         return mIndicatorText;
354     }
355 
356     /**
357      * Set the indicator text.  Set to null for no text to be displayed.
358      */
setIndicatorText(String indicatorText)359     public void setIndicatorText(String indicatorText) {
360         mIndicatorText = indicatorText;
361     }
362 
363     /**
364      * Get whether this RecipientEntry is in a replaced chip or not. Replaced chip only occurs
365      * if {@link RecipientEditTextView} uses a replacement chip for the entry.
366      */
getInReplacedChip()367     public boolean getInReplacedChip() {
368         return mInReplacedChip;
369     }
370 
371     /**
372      * Sets {@link #mInReplacedChip} to {@param inReplacedChip}.
373      */
setInReplacedChip(boolean inReplacedChip)374     public void setInReplacedChip(boolean inReplacedChip) {
375         mInReplacedChip = inReplacedChip;
376     }
377 }
378