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