1 /*
2  * Copyright 2018 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 androidx.core.app;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 
21 import android.os.Bundle;
22 import android.os.PersistableBundle;
23 
24 import androidx.annotation.RequiresApi;
25 import androidx.annotation.RestrictTo;
26 import androidx.core.graphics.drawable.IconCompat;
27 
28 import org.jspecify.annotations.NonNull;
29 import org.jspecify.annotations.Nullable;
30 
31 import java.util.Objects;
32 
33 /**
34  * Provides an immutable reference to an entity that appears repeatedly on different surfaces of the
35  * platform. For example, this could represent the sender of a message.
36  */
37 public class Person {
38     private static final String NAME_KEY = "name";
39     private static final String ICON_KEY = "icon";
40     private static final String URI_KEY = "uri";
41     private static final String KEY_KEY = "key";
42     private static final String IS_BOT_KEY = "isBot";
43     private static final String IS_IMPORTANT_KEY = "isImportant";
44 
45     /**
46      * Extracts and returns the {@link Person} written to the {@code bundle}. A bundle can be
47      * created from a {@link Person} using {@link #toBundle()}.
48      */
fromBundle(@onNull Bundle bundle)49     public static @NonNull Person fromBundle(@NonNull Bundle bundle) {
50         Bundle iconBundle = bundle.getBundle(ICON_KEY);
51         return new Builder()
52                 .setName(bundle.getCharSequence(NAME_KEY))
53                 .setIcon(iconBundle != null ? IconCompat.createFromBundle(iconBundle) : null)
54                 .setUri(bundle.getString(URI_KEY))
55                 .setKey(bundle.getString(KEY_KEY))
56                 .setBot(bundle.getBoolean(IS_BOT_KEY))
57                 .setImportant(bundle.getBoolean(IS_IMPORTANT_KEY))
58                 .build();
59     }
60 
61     /**
62      * Extracts and returns the {@link Person} written to the {@code bundle}. A persistable bundle
63      * can be created from a {@link Person} using {@link #toPersistableBundle()}. The Icon of the
64      * Person will not be extracted from the PersistableBundle.
65      *
66      */
67     @RestrictTo(LIBRARY_GROUP_PREFIX)
68     @RequiresApi(22)
fromPersistableBundle(@onNull PersistableBundle bundle)69     public static @NonNull Person fromPersistableBundle(@NonNull PersistableBundle bundle) {
70         return Api22Impl.fromPersistableBundle(bundle);
71     }
72 
73     /**
74      * Converts an Android framework {@link android.app.Person} to a compat {@link Person}.
75      *
76      */
77     @RestrictTo(LIBRARY_GROUP_PREFIX)
78     @RequiresApi(28)
fromAndroidPerson(android.app.@onNull Person person)79     public static @NonNull Person fromAndroidPerson(android.app.@NonNull Person person) {
80         return Api28Impl.fromAndroidPerson(person);
81     }
82 
83     @SuppressWarnings("WeakerAccess") /* synthetic access */
84 @Nullable CharSequence mName;
85     @SuppressWarnings("WeakerAccess") /* synthetic access */
86 @Nullable IconCompat mIcon;
87     @SuppressWarnings("WeakerAccess") /* synthetic access */
88 @Nullable String mUri;
89     @SuppressWarnings("WeakerAccess") /* synthetic access */
90 @Nullable String mKey;
91     @SuppressWarnings("WeakerAccess") /* synthetic access */
92     boolean mIsBot;
93     @SuppressWarnings("WeakerAccess") /* synthetic access */
94     boolean mIsImportant;
95 
96     @SuppressWarnings("WeakerAccess") /* synthetic access */
Person(Builder builder)97     Person(Builder builder) {
98         mName = builder.mName;
99         mIcon = builder.mIcon;
100         mUri = builder.mUri;
101         mKey = builder.mKey;
102         mIsBot = builder.mIsBot;
103         mIsImportant = builder.mIsImportant;
104     }
105 
106     /**
107      * Writes and returns a new {@link Bundle} that represents this {@link Person}. This bundle can
108      * be converted back by using {@link #fromBundle(Bundle)}.
109      */
toBundle()110     public @NonNull Bundle toBundle() {
111         Bundle result = new Bundle();
112         result.putCharSequence(NAME_KEY, mName);
113         result.putBundle(ICON_KEY, mIcon != null ? mIcon.toBundle() : null);
114         result.putString(URI_KEY, mUri);
115         result.putString(KEY_KEY, mKey);
116         result.putBoolean(IS_BOT_KEY, mIsBot);
117         result.putBoolean(IS_IMPORTANT_KEY, mIsImportant);
118         return result;
119     }
120 
121     /**
122      * Writes and returns a new {@link PersistableBundle} that represents this {@link Person}. This
123      * bundle can be converted back by using {@link #fromPersistableBundle(PersistableBundle)}. The
124      * Icon of the Person will not be included in the resulting PersistableBundle.
125      *
126      */
127     @RestrictTo(LIBRARY_GROUP_PREFIX)
128     @RequiresApi(22)
toPersistableBundle()129     public @NonNull PersistableBundle toPersistableBundle() {
130         return Api22Impl.toPersistableBundle(this);
131     }
132 
133     /** Creates and returns a new {@link Builder} initialized with this Person's data. */
toBuilder()134     public @NonNull Builder toBuilder() {
135         return new Builder(this);
136     }
137 
138     /**
139      * Converts this compat {@link Person} to the base Android framework {@link android.app.Person}.
140      *
141      */
142     @RestrictTo(LIBRARY_GROUP_PREFIX)
143     @RequiresApi(28)
toAndroidPerson()144     public android.app.@NonNull Person toAndroidPerson() {
145         return Api28Impl.toAndroidPerson(this);
146     }
147 
148     /**
149      * Returns the name for this {@link Person} or {@code null} if no name was provided. This could
150      * be a full name, nickname, username, etc.
151      */
getName()152     public @Nullable CharSequence getName() {
153         return mName;
154     }
155 
156     /** Returns the icon for this {@link Person} or {@code null} if no icon was provided. */
getIcon()157     public @Nullable IconCompat getIcon() {
158         return mIcon;
159     }
160 
161     /**
162      * Returns the raw URI for this {@link Person} or {@code null} if no URI was provided. A URI can
163      * be any of the following:
164      * <ul>
165      *     <li>The {@code String} representation of a
166      *     {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}</li>
167      *     <li>A {@code mailto:} schema*</li>
168      *     <li>A {@code tel:} schema*</li>
169      * </ul>
170      *
171      * <p>*Note for these schemas, the path portion of the URI must exist in the contacts
172      * database in their appropriate column, otherwise the reference should be discarded.
173      */
getUri()174     public @Nullable String getUri() {
175         return mUri;
176     }
177 
178     /**
179      * Returns the key for this {@link Person} or {@code null} if no key was provided. This is
180      * provided as a unique identifier between other {@link Person}s.
181      */
getKey()182     public @Nullable String getKey() {
183         return mKey;
184     }
185 
186     /**
187      * Returns whether or not this {@link Person} is a machine rather than a human. Used primarily
188      * to identify automated tooling.
189      */
isBot()190     public boolean isBot() {
191         return mIsBot;
192     }
193 
194     /**
195      * Returns whether or not this {@link Person} is important to the user of this device with
196      * regards to how frequently they interact.
197      */
isImportant()198     public boolean isImportant() {
199         return mIsImportant;
200     }
201 
202     /**
203      * @return the URI associated with this person, or "name:mName" otherwise
204      */
205     @RestrictTo(LIBRARY_GROUP_PREFIX)
resolveToLegacyUri()206     public @NonNull String resolveToLegacyUri() {
207         if (mUri != null) {
208             return mUri;
209         }
210         if (mName != null) {
211             return "name:" + mName;
212         }
213         return "";
214     }
215 
216     @Override
equals(@ullable Object otherObject)217     public boolean equals(@Nullable Object otherObject) {
218         if (otherObject == null) {
219             return false;
220         }
221 
222         if (!(otherObject instanceof Person)) {
223             return false;
224         }
225 
226         Person otherPerson = (Person) otherObject;
227 
228         // If a unique ID was provided, use it
229         String key1 = getKey();
230         String key2 = otherPerson.getKey();
231         if (key1 != null || key2 != null) {
232             return Objects.equals(key1, key2);
233         }
234 
235         // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
236         String name1 = Objects.toString(getName());
237         String name2 = Objects.toString(otherPerson.getName());
238 
239         // Fallback: Compare field-by-field
240         return
241                 Objects.equals(name1, name2)
242                         && Objects.equals(getUri(), otherPerson.getUri())
243                         && Objects.equals(isBot(), otherPerson.isBot())
244                         && Objects.equals(isImportant(), otherPerson.isImportant());
245     }
246 
247     @Override
hashCode()248     public int hashCode() {
249         // If a unique ID was provided, use it
250         String key = getKey();
251         if (key != null) {
252             return key.hashCode();
253         }
254 
255         // Fallback: Use hash code for individual fields
256         return Objects.hash(getName(), getUri(), isBot(), isImportant());
257     }
258 
259     /** Builder for the immutable {@link Person} class. */
260     public static class Builder {
261         @Nullable CharSequence mName;
262         @Nullable IconCompat mIcon;
263         @Nullable String mUri;
264         @Nullable String mKey;
265         boolean mIsBot;
266         boolean mIsImportant;
267 
268         /** Creates a new, empty {@link Builder}. */
Builder()269         public Builder() { }
270 
Builder(Person person)271         Builder(Person person) {
272             mName = person.mName;
273             mIcon = person.mIcon;
274             mUri = person.mUri;
275             mKey = person.mKey;
276             mIsBot = person.mIsBot;
277             mIsImportant = person.mIsImportant;
278         }
279 
280         /**
281          * Give this {@link Person} a name to use for display. This can be, for example, a full
282          * name, nickname, username, etc.
283          */
setName(@ullable CharSequence name)284         public @NonNull Builder setName(@Nullable CharSequence name) {
285             mName = name;
286             return this;
287         }
288 
289         /**
290          * Set an icon for this {@link Person}.
291          *
292          * <p>The system will prefer this icon over any images that are resolved from
293          * {@link #setUri(String)}.
294          */
setIcon(@ullable IconCompat icon)295         public @NonNull Builder setIcon(@Nullable IconCompat icon) {
296             mIcon = icon;
297             return this;
298         }
299 
300         /**
301          * Set a URI for this {@link Person} which can be any of the following:
302          * <ul>
303          *     <li>The {@code String} representation of a
304          *     {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}</li>
305          *     <li>A {@code mailto:} schema*</li>
306          *     <li>A {@code tel:} schema*</li>
307          * </ul>
308          *
309          * <p>*Note for these schemas, the path portion of the URI must exist in the contacts
310          * database in their appropriate column, otherwise the reference will be discarded.
311          */
setUri(@ullable String uri)312         public @NonNull Builder setUri(@Nullable String uri) {
313             mUri = uri;
314             return this;
315         }
316 
317         /**
318          * Set a unique identifier for this {@link Person}. This is especially useful if the
319          * {@link #setName(CharSequence)} value isn't unique. This value is preferred for
320          * identification, but if it's not provided, the person's name will be used in its place.
321          */
setKey(@ullable String key)322         public @NonNull Builder setKey(@Nullable String key) {
323             mKey = key;
324             return this;
325         }
326 
327         /**
328          * Sets whether or not this {@link Person} represents a machine rather than a human. This is
329          * used primarily for testing and automated tooling.
330          */
setBot(boolean bot)331         public @NonNull Builder setBot(boolean bot) {
332             mIsBot = bot;
333             return this;
334         }
335 
336         /**
337          * Sets whether this is an important person. Use this method to denote users who frequently
338          * interact with the user of this device when {@link #setUri(String)} isn't provided with
339          * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}, and instead with
340          * the {@code mailto:} or {@code tel:} schemas.
341          */
setImportant(boolean important)342         public @NonNull Builder setImportant(boolean important) {
343             mIsImportant = important;
344             return this;
345         }
346 
347         /** Creates and returns the {@link Person} this builder represents. */
build()348         public @NonNull Person build() {
349             return new Person(this);
350         }
351     }
352 
353     @RequiresApi(22)
354     static class Api22Impl {
Api22Impl()355         private Api22Impl() {
356             // This class is not instantiable.
357         }
358 
fromPersistableBundle(PersistableBundle bundle)359         static Person fromPersistableBundle(PersistableBundle bundle) {
360             return new Builder()
361                     .setName(bundle.getString(NAME_KEY))
362                     .setUri(bundle.getString(URI_KEY))
363                     .setKey(bundle.getString(KEY_KEY))
364                     .setBot(bundle.getBoolean(IS_BOT_KEY))
365                     .setImportant(bundle.getBoolean(IS_IMPORTANT_KEY))
366                     .build();
367         }
368 
toPersistableBundle(Person person)369         static PersistableBundle toPersistableBundle(Person person) {
370             PersistableBundle result = new PersistableBundle();
371             result.putString(NAME_KEY, person.mName != null ? person.mName.toString() : null);
372             result.putString(URI_KEY, person.mUri);
373             result.putString(KEY_KEY, person.mKey);
374             result.putBoolean(IS_BOT_KEY, person.mIsBot);
375             result.putBoolean(IS_IMPORTANT_KEY, person.mIsImportant);
376             return result;
377         }
378     }
379 
380     @RequiresApi(28)
381     static class Api28Impl {
Api28Impl()382         private Api28Impl() {
383             // This class is not instantiable.
384         }
385 
fromAndroidPerson(android.app.Person person)386         static Person fromAndroidPerson(android.app.Person person) {
387             return new Builder()
388                     .setName(person.getName())
389                     .setIcon(
390                             (person.getIcon() != null)
391                                     ? IconCompat.createFromIcon(person.getIcon())
392                                     : null)
393                     .setUri(person.getUri())
394                     .setKey(person.getKey())
395                     .setBot(person.isBot())
396                     .setImportant(person.isImportant())
397                     .build();
398         }
399 
400         @SuppressWarnings("deprecation")
toAndroidPerson(Person person)401         static android.app.Person toAndroidPerson(Person person) {
402             return new android.app.Person.Builder()
403                     .setName(person.getName())
404                     .setIcon((person.getIcon() != null) ? person.getIcon().toIcon() : null)
405                     .setUri(person.getUri())
406                     .setKey(person.getKey())
407                     .setBot(person.isBot())
408                     .setImportant(person.isImportant())
409                     .build();
410         }
411     }
412 }
413