1/* 2 * Copyright (C) 2017 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 */ 16package android.support.text.emoji; 17 18import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20import android.graphics.Color; 21import android.os.Build; 22import android.os.Handler; 23import android.os.Looper; 24import android.support.annotation.AnyThread; 25import android.support.annotation.CheckResult; 26import android.support.annotation.ColorInt; 27import android.support.annotation.GuardedBy; 28import android.support.annotation.IntDef; 29import android.support.annotation.IntRange; 30import android.support.annotation.NonNull; 31import android.support.annotation.Nullable; 32import android.support.annotation.RequiresApi; 33import android.support.annotation.RestrictTo; 34import android.support.annotation.VisibleForTesting; 35import android.support.v4.util.ArraySet; 36import android.support.v4.util.Preconditions; 37import android.text.Editable; 38import android.text.method.KeyListener; 39import android.view.KeyEvent; 40import android.view.inputmethod.EditorInfo; 41import android.view.inputmethod.InputConnection; 42 43import java.lang.annotation.Retention; 44import java.lang.annotation.RetentionPolicy; 45import java.util.ArrayList; 46import java.util.Arrays; 47import java.util.Collection; 48import java.util.List; 49import java.util.Set; 50import java.util.concurrent.locks.ReadWriteLock; 51import java.util.concurrent.locks.ReentrantReadWriteLock; 52 53/** 54 * Main class to keep Android devices up to date with the newest emojis by adding {@link EmojiSpan}s 55 * to a given {@link CharSequence}. It is a singleton class that can be configured using a {@link 56 * EmojiCompat.Config} instance. 57 * <p/> 58 * EmojiCompat has to be initialized using {@link #init(EmojiCompat.Config)} function before it can 59 * process a {@link CharSequence}. 60 * <pre><code>EmojiCompat.init(/* a config instance */);</code></pre> 61 * <p/> 62 * It is suggested to make the initialization as early as possible in your app. Please check {@link 63 * EmojiCompat.Config} for more configuration parameters. 64 * <p/> 65 * During initialization information about emojis is loaded on a background thread. Before the 66 * EmojiCompat instance is initialized, calls to functions such as {@link 67 * EmojiCompat#process(CharSequence)} will throw an exception. You can use the {@link InitCallback} 68 * class to be informed about the state of initialization. 69 * <p/> 70 * After initialization the {@link #get()} function can be used to get the configured instance and 71 * the {@link #process(CharSequence)} function can be used to update a CharSequence with emoji 72 * EmojiSpans. 73 * <p/> 74 * <pre><code>CharSequence processedSequence = EmojiCompat.get().process("some string")</pre> 75 */ 76@AnyThread 77public class EmojiCompat { 78 /** 79 * Key in {@link EditorInfo#extras} that represents the emoji metadata version used by the 80 * widget. The existence of the value means that the widget is using EmojiCompat. 81 * <p/> 82 * If exists, the value for the key is an {@code int} and can be used to query EmojiCompat to 83 * see whether the widget has the ability to display a certain emoji using 84 * {@link #hasEmojiGlyph(CharSequence, int)}. 85 */ 86 public static final String EDITOR_INFO_METAVERSION_KEY = 87 "android.support.text.emoji.emojiCompat_metadataVersion"; 88 89 /** 90 * Key in {@link EditorInfo#extras} that represents {@link 91 * EmojiCompat.Config#setReplaceAll(boolean)} configuration parameter. The key is added only if 92 * EmojiCompat is used by the widget. If exists, the value is a boolean. 93 */ 94 public static final String EDITOR_INFO_REPLACE_ALL_KEY = 95 "android.support.text.emoji.emojiCompat_replaceAll"; 96 97 /** 98 * EmojiCompat is initializing. 99 */ 100 public static final int LOAD_STATE_LOADING = 0; 101 102 /** 103 * EmojiCompat successfully initialized. 104 */ 105 public static final int LOAD_STATE_SUCCEEDED = 1; 106 107 /** 108 * @deprecated Use {@link #LOAD_STATE_SUCCEEDED} instead. 109 */ 110 @Deprecated 111 public static final int LOAD_STATE_SUCCESS = 1; 112 113 /** 114 * An unrecoverable error occurred during initialization of EmojiCompat. Calls to functions 115 * such as {@link #process(CharSequence)} will fail. 116 */ 117 public static final int LOAD_STATE_FAILED = 2; 118 119 /** 120 * @deprecated Use {@link #LOAD_STATE_FAILED} instead. 121 */ 122 @Deprecated 123 public static final int LOAD_STATE_FAILURE = 2; 124 125 /** 126 * @hide 127 */ 128 @RestrictTo(LIBRARY_GROUP) 129 @IntDef({LOAD_STATE_LOADING, LOAD_STATE_SUCCEEDED, LOAD_STATE_FAILED}) 130 @Retention(RetentionPolicy.SOURCE) 131 public @interface LoadState { 132 } 133 134 /** 135 * Replace strategy that uses the value given in {@link EmojiCompat.Config}. 136 */ 137 public static final int REPLACE_STRATEGY_DEFAULT = 0; 138 139 /** 140 * Replace strategy to add {@link EmojiSpan}s for all emoji that were found. 141 */ 142 public static final int REPLACE_STRATEGY_ALL = 1; 143 144 /** 145 * Replace strategy to add {@link EmojiSpan}s only for emoji that do not exist in the system. 146 */ 147 public static final int REPLACE_STRATEGY_NON_EXISTENT = 2; 148 149 /** 150 * @hide 151 */ 152 @RestrictTo(LIBRARY_GROUP) 153 @IntDef({REPLACE_STRATEGY_DEFAULT, REPLACE_STRATEGY_NON_EXISTENT, REPLACE_STRATEGY_ALL}) 154 @Retention(RetentionPolicy.SOURCE) 155 public @interface ReplaceStrategy { 156 } 157 158 private static final Object sInstanceLock = new Object(); 159 160 @GuardedBy("sInstanceLock") 161 private static volatile EmojiCompat sInstance; 162 163 private final ReadWriteLock mInitLock; 164 165 @GuardedBy("mInitLock") 166 private final Set<InitCallback> mInitCallbacks; 167 168 @GuardedBy("mInitLock") 169 @LoadState 170 private int mLoadState; 171 172 /** 173 * Handler with main looper to run the callbacks on. 174 */ 175 private final Handler mMainHandler; 176 177 /** 178 * Helper class for pre 19 compatibility. 179 */ 180 private final CompatInternal mHelper; 181 182 /** 183 * MetadataLoader instance given in the Config instance. 184 */ 185 private final MetadataLoader mMetadataLoader; 186 187 /** 188 * @see Config#setReplaceAll(boolean) 189 */ 190 private final boolean mReplaceAll; 191 192 /** 193 * @see Config#setEmojiSpanIndicatorEnabled(boolean) 194 */ 195 private final boolean mEmojiSpanIndicatorEnabled; 196 197 /** 198 * @see Config#setEmojiSpanIndicatorColor(int) 199 */ 200 private final int mEmojiSpanIndicatorColor; 201 202 /** 203 * Private constructor for singleton instance. 204 * 205 * @see #init(Config) 206 */ 207 private EmojiCompat(@NonNull final Config config) { 208 mInitLock = new ReentrantReadWriteLock(); 209 mReplaceAll = config.mReplaceAll; 210 mEmojiSpanIndicatorEnabled = config.mEmojiSpanIndicatorEnabled; 211 mEmojiSpanIndicatorColor = config.mEmojiSpanIndicatorColor; 212 mMetadataLoader = config.mMetadataLoader; 213 mMainHandler = new Handler(Looper.getMainLooper()); 214 mInitCallbacks = new ArraySet<>(); 215 if (config.mInitCallbacks != null && !config.mInitCallbacks.isEmpty()) { 216 mInitCallbacks.addAll(config.mInitCallbacks); 217 } 218 mHelper = Build.VERSION.SDK_INT < 19 ? new CompatInternal(this) : new CompatInternal19( 219 this); 220 loadMetadata(); 221 } 222 223 /** 224 * Initialize the singleton instance with a configuration. When used on devices running API 18 225 * or below, the singleton instance is immediately moved into {@link #LOAD_STATE_SUCCEEDED} 226 * state without loading any metadata. 227 * 228 * @see EmojiCompat.Config 229 */ 230 public static EmojiCompat init(@NonNull final Config config) { 231 if (sInstance == null) { 232 synchronized (sInstanceLock) { 233 if (sInstance == null) { 234 sInstance = new EmojiCompat(config); 235 } 236 } 237 } 238 return sInstance; 239 } 240 241 /** 242 * Used by the tests to reset EmojiCompat with a new configuration. Every time it is called a 243 * new instance is created with the new configuration. 244 * 245 * @hide 246 */ 247 @RestrictTo(LIBRARY_GROUP) 248 @VisibleForTesting 249 public static EmojiCompat reset(@NonNull final Config config) { 250 synchronized (sInstanceLock) { 251 sInstance = new EmojiCompat(config); 252 } 253 return sInstance; 254 } 255 256 /** 257 * Used by the tests to reset EmojiCompat with a new singleton instance. 258 * 259 * @hide 260 */ 261 @RestrictTo(LIBRARY_GROUP) 262 @VisibleForTesting 263 public static EmojiCompat reset(final EmojiCompat emojiCompat) { 264 synchronized (sInstanceLock) { 265 sInstance = emojiCompat; 266 } 267 return sInstance; 268 } 269 270 /** 271 * Used by the tests to set GlyphChecker for EmojiProcessor. 272 * 273 * @hide 274 */ 275 @RestrictTo(LIBRARY_GROUP) 276 @VisibleForTesting 277 void setGlyphChecker(@NonNull final EmojiProcessor.GlyphChecker glyphChecker) { 278 mHelper.setGlyphChecker(glyphChecker); 279 } 280 281 /** 282 * Return singleton EmojiCompat instance. Should be called after 283 * {@link #init(EmojiCompat.Config)} is called to initialize the singleton instance. 284 * 285 * @return EmojiCompat instance 286 * 287 * @throws IllegalStateException if called before {@link #init(EmojiCompat.Config)} 288 */ 289 public static EmojiCompat get() { 290 synchronized (sInstanceLock) { 291 Preconditions.checkState(sInstance != null, 292 "EmojiCompat is not initialized. Please call EmojiCompat.init() first"); 293 return sInstance; 294 } 295 } 296 297 private void loadMetadata() { 298 mInitLock.writeLock().lock(); 299 try { 300 mLoadState = LOAD_STATE_LOADING; 301 } finally { 302 mInitLock.writeLock().unlock(); 303 } 304 305 mHelper.loadMetadata(); 306 } 307 308 private void onMetadataLoadSuccess() { 309 final Collection<InitCallback> initCallbacks = new ArrayList<>(); 310 mInitLock.writeLock().lock(); 311 try { 312 mLoadState = LOAD_STATE_SUCCEEDED; 313 initCallbacks.addAll(mInitCallbacks); 314 mInitCallbacks.clear(); 315 } finally { 316 mInitLock.writeLock().unlock(); 317 } 318 319 mMainHandler.post(new ListenerDispatcher(initCallbacks, mLoadState)); 320 } 321 322 private void onMetadataLoadFailed(@Nullable final Throwable throwable) { 323 final Collection<InitCallback> initCallbacks = new ArrayList<>(); 324 mInitLock.writeLock().lock(); 325 try { 326 mLoadState = LOAD_STATE_FAILED; 327 initCallbacks.addAll(mInitCallbacks); 328 mInitCallbacks.clear(); 329 } finally { 330 mInitLock.writeLock().unlock(); 331 } 332 mMainHandler.post(new ListenerDispatcher(initCallbacks, mLoadState, throwable)); 333 } 334 335 /** 336 * Registers an initialization callback. If the initialization is already completed by the time 337 * the listener is added, the callback functions are called immediately. Callbacks are called on 338 * the main looper. 339 * <p/> 340 * When used on devices running API 18 or below, {@link InitCallback#onInitialized()} is called 341 * without loading any metadata. In such cases {@link InitCallback#onFailed(Throwable)} is never 342 * called. 343 * 344 * @param initCallback the initialization callback to register, cannot be {@code null} 345 * 346 * @see #unregisterInitCallback(InitCallback) 347 */ 348 public void registerInitCallback(@NonNull InitCallback initCallback) { 349 Preconditions.checkNotNull(initCallback, "initCallback cannot be null"); 350 351 mInitLock.writeLock().lock(); 352 try { 353 if (mLoadState == LOAD_STATE_SUCCEEDED || mLoadState == LOAD_STATE_FAILED) { 354 mMainHandler.post(new ListenerDispatcher(initCallback, mLoadState)); 355 } else { 356 mInitCallbacks.add(initCallback); 357 } 358 } finally { 359 mInitLock.writeLock().unlock(); 360 } 361 } 362 363 /** 364 * Unregisters a callback that was added before. 365 * 366 * @param initCallback the callback to be removed, cannot be {@code null} 367 */ 368 public void unregisterInitCallback(@NonNull InitCallback initCallback) { 369 Preconditions.checkNotNull(initCallback, "initCallback cannot be null"); 370 mInitLock.writeLock().lock(); 371 try { 372 mInitCallbacks.remove(initCallback); 373 } finally { 374 mInitLock.writeLock().unlock(); 375 } 376 } 377 378 /** 379 * Returns loading state of the EmojiCompat instance. When used on devices running API 18 or 380 * below always returns {@link #LOAD_STATE_SUCCEEDED}. 381 * 382 * @return one of {@link #LOAD_STATE_LOADING}, {@link #LOAD_STATE_SUCCEEDED}, 383 * {@link #LOAD_STATE_FAILED} 384 */ 385 public @LoadState int getLoadState() { 386 mInitLock.readLock().lock(); 387 try { 388 return mLoadState; 389 } finally { 390 mInitLock.readLock().unlock(); 391 } 392 } 393 394 /** 395 * @return {@code true} if EmojiCompat is successfully initialized 396 */ 397 private boolean isInitialized() { 398 return getLoadState() == LOAD_STATE_SUCCEEDED; 399 } 400 401 /** 402 * @return whether a background should be drawn for the emoji. 403 * @hide 404 */ 405 @RestrictTo(LIBRARY_GROUP) 406 boolean isEmojiSpanIndicatorEnabled() { 407 return mEmojiSpanIndicatorEnabled; 408 } 409 410 /** 411 * @return whether a background should be drawn for the emoji. 412 * @hide 413 */ 414 @RestrictTo(LIBRARY_GROUP) 415 @ColorInt int getEmojiSpanIndicatorColor() { 416 return mEmojiSpanIndicatorColor; 417 } 418 419 /** 420 * Handles onKeyDown commands from a {@link KeyListener} and if {@code keyCode} is one of 421 * {@link KeyEvent#KEYCODE_DEL} or {@link KeyEvent#KEYCODE_FORWARD_DEL} it tries to delete an 422 * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is 423 * deleted with the characters it covers. 424 * <p/> 425 * If there is a selection where selection start is not equal to selection end, does not 426 * delete. 427 * <p/> 428 * When used on devices running API 18 or below, always returns {@code false}. 429 * 430 * @param editable Editable instance passed to {@link KeyListener#onKeyDown(android.view.View, 431 * Editable, int, KeyEvent)} 432 * @param keyCode keyCode passed to {@link KeyListener#onKeyDown(android.view.View, Editable, 433 * int, KeyEvent)} 434 * @param event KeyEvent passed to {@link KeyListener#onKeyDown(android.view.View, Editable, 435 * int, KeyEvent)} 436 * 437 * @return {@code true} if an {@link EmojiSpan} is deleted 438 */ 439 public static boolean handleOnKeyDown(@NonNull final Editable editable, final int keyCode, 440 final KeyEvent event) { 441 if (Build.VERSION.SDK_INT >= 19) { 442 return EmojiProcessor.handleOnKeyDown(editable, keyCode, event); 443 } else { 444 return false; 445 } 446 } 447 448 /** 449 * Handles deleteSurroundingText commands from {@link InputConnection} and tries to delete an 450 * {@link EmojiSpan} from an {@link Editable}. Returns {@code true} if an {@link EmojiSpan} is 451 * deleted. 452 * <p/> 453 * If there is a selection where selection start is not equal to selection end, does not 454 * delete. 455 * <p/> 456 * When used on devices running API 18 or below, always returns {@code false}. 457 * 458 * @param inputConnection InputConnection instance 459 * @param editable TextView.Editable instance 460 * @param beforeLength the number of characters before the cursor to be deleted 461 * @param afterLength the number of characters after the cursor to be deleted 462 * @param inCodePoints {@code true} if length parameters are in codepoints 463 * 464 * @return {@code true} if an {@link EmojiSpan} is deleted 465 */ 466 public static boolean handleDeleteSurroundingText( 467 @NonNull final InputConnection inputConnection, @NonNull final Editable editable, 468 @IntRange(from = 0) final int beforeLength, @IntRange(from = 0) final int afterLength, 469 final boolean inCodePoints) { 470 if (Build.VERSION.SDK_INT >= 19) { 471 return EmojiProcessor.handleDeleteSurroundingText(inputConnection, editable, 472 beforeLength, afterLength, inCodePoints); 473 } else { 474 return false; 475 } 476 } 477 478 /** 479 * Returns {@code true} if EmojiCompat is capable of rendering an emoji. When used on devices 480 * running API 18 or below, always returns {@code false}. 481 * 482 * @param sequence CharSequence representing the emoji 483 * 484 * @return {@code true} if EmojiCompat can render given emoji, cannot be {@code null} 485 * 486 * @throws IllegalStateException if not initialized yet 487 */ 488 public boolean hasEmojiGlyph(@NonNull final CharSequence sequence) { 489 Preconditions.checkState(isInitialized(), "Not initialized yet"); 490 Preconditions.checkNotNull(sequence, "sequence cannot be null"); 491 return mHelper.hasEmojiGlyph(sequence); 492 } 493 494 /** 495 * Returns {@code true} if EmojiCompat is capable of rendering an emoji at the given metadata 496 * version. When used on devices running API 18 or below, always returns {@code false}. 497 * 498 * @param sequence CharSequence representing the emoji 499 * @param metadataVersion the metadata version to check against, should be greater than or 500 * equal to {@code 0}, 501 * 502 * @return {@code true} if EmojiCompat can render given emoji, cannot be {@code null} 503 * 504 * @throws IllegalStateException if not initialized yet 505 */ 506 public boolean hasEmojiGlyph(@NonNull final CharSequence sequence, 507 @IntRange(from = 0) final int metadataVersion) { 508 Preconditions.checkState(isInitialized(), "Not initialized yet"); 509 Preconditions.checkNotNull(sequence, "sequence cannot be null"); 510 return mHelper.hasEmojiGlyph(sequence, metadataVersion); 511 } 512 513 /** 514 * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. When 515 * used on devices running API 18 or below, returns the given {@code charSequence} without 516 * processing it. 517 * 518 * @param charSequence CharSequence to add the EmojiSpans 519 * 520 * @throws IllegalStateException if not initialized yet 521 * @see #process(CharSequence, int, int) 522 */ 523 @CheckResult 524 public CharSequence process(@NonNull final CharSequence charSequence) { 525 // since charSequence might be null here we have to check it. Passing through here to the 526 // main function so that it can do all the checks including isInitialized. It will also 527 // be the main point that decides what to return. 528 @IntRange(from = 0) final int length = charSequence == null ? 0 : charSequence.length(); 529 return process(charSequence, 0, length); 530 } 531 532 /** 533 * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. 534 * <p> 535 * <ul> 536 * <li>If no emojis are found, {@code charSequence} given as the input is returned without 537 * any changes. i.e. charSequence is a String, and no emojis are found, the same String is 538 * returned.</li> 539 * <li>If the given input is not a Spannable (such as String), and at least one emoji is found 540 * a new {@link android.text.Spannable} instance is returned. </li> 541 * <li>If the given input is a Spannable, the same instance is returned. </li> 542 * </ul> 543 * When used on devices running API 18 or below, returns the given {@code charSequence} without 544 * processing it. 545 * 546 * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null} 547 * @param start start index in the charSequence to look for emojis, should be greater than or 548 * equal to {@code 0}, also less than {@code charSequence.length()} 549 * @param end end index in the charSequence to look for emojis, should be greater than or 550 * equal to {@code start} parameter, also less than {@code charSequence.length()} 551 * 552 * @throws IllegalStateException if not initialized yet 553 * @throws IllegalArgumentException in the following cases: 554 * {@code start < 0}, {@code end < 0}, {@code end < start}, 555 * {@code start > charSequence.length()}, 556 * {@code end > charSequence.length()} 557 */ 558 @CheckResult 559 public CharSequence process(@NonNull final CharSequence charSequence, 560 @IntRange(from = 0) final int start, @IntRange(from = 0) final int end) { 561 return process(charSequence, start, end, EmojiProcessor.EMOJI_COUNT_UNLIMITED); 562 } 563 564 /** 565 * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. 566 * <p> 567 * <ul> 568 * <li>If no emojis are found, {@code charSequence} given as the input is returned without 569 * any changes. i.e. charSequence is a String, and no emojis are found, the same String is 570 * returned.</li> 571 * <li>If the given input is not a Spannable (such as String), and at least one emoji is found 572 * a new {@link android.text.Spannable} instance is returned. </li> 573 * <li>If the given input is a Spannable, the same instance is returned. </li> 574 * </ul> 575 * When used on devices running API 18 or below, returns the given {@code charSequence} without 576 * processing it. 577 * 578 * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null} 579 * @param start start index in the charSequence to look for emojis, should be greater than or 580 * equal to {@code 0}, also less than {@code charSequence.length()} 581 * @param end end index in the charSequence to look for emojis, should be greater than or 582 * equal to {@code start} parameter, also less than {@code charSequence.length()} 583 * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater 584 * than or equal to {@code 0} 585 * 586 * @throws IllegalStateException if not initialized yet 587 * @throws IllegalArgumentException in the following cases: 588 * {@code start < 0}, {@code end < 0}, {@code end < start}, 589 * {@code start > charSequence.length()}, 590 * {@code end > charSequence.length()} 591 * {@code maxEmojiCount < 0} 592 */ 593 @CheckResult 594 public CharSequence process(@NonNull final CharSequence charSequence, 595 @IntRange(from = 0) final int start, @IntRange(from = 0) final int end, 596 @IntRange(from = 0) final int maxEmojiCount) { 597 return process(charSequence, start, end, maxEmojiCount, REPLACE_STRATEGY_DEFAULT); 598 } 599 600 /** 601 * Checks a given CharSequence for emojis, and adds EmojiSpans if any emojis are found. 602 * <p> 603 * <ul> 604 * <li>If no emojis are found, {@code charSequence} given as the input is returned without 605 * any changes. i.e. charSequence is a String, and no emojis are found, the same String is 606 * returned.</li> 607 * <li>If the given input is not a Spannable (such as String), and at least one emoji is found 608 * a new {@link android.text.Spannable} instance is returned. </li> 609 * <li>If the given input is a Spannable, the same instance is returned. </li> 610 * </ul> 611 * When used on devices running API 18 or below, returns the given {@code charSequence} without 612 * processing it. 613 * 614 * @param charSequence CharSequence to add the EmojiSpans, cannot be {@code null} 615 * @param start start index in the charSequence to look for emojis, should be greater than or 616 * equal to {@code 0}, also less than {@code charSequence.length()} 617 * @param end end index in the charSequence to look for emojis, should be greater than or 618 * equal to {@code start} parameter, also less than {@code charSequence.length()} 619 * @param maxEmojiCount maximum number of emojis in the {@code charSequence}, should be greater 620 * than or equal to {@code 0} 621 * @param replaceStrategy whether to replace all emoji with {@link EmojiSpan}s, should be one of 622 * {@link #REPLACE_STRATEGY_DEFAULT}, 623 * {@link #REPLACE_STRATEGY_NON_EXISTENT}, 624 * {@link #REPLACE_STRATEGY_ALL} 625 * 626 * @throws IllegalStateException if not initialized yet 627 * @throws IllegalArgumentException in the following cases: 628 * {@code start < 0}, {@code end < 0}, {@code end < start}, 629 * {@code start > charSequence.length()}, 630 * {@code end > charSequence.length()} 631 * {@code maxEmojiCount < 0} 632 */ 633 @CheckResult 634 public CharSequence process(@NonNull final CharSequence charSequence, 635 @IntRange(from = 0) final int start, @IntRange(from = 0) final int end, 636 @IntRange(from = 0) final int maxEmojiCount, @ReplaceStrategy int replaceStrategy) { 637 Preconditions.checkState(isInitialized(), "Not initialized yet"); 638 Preconditions.checkArgumentNonnegative(start, "start cannot be negative"); 639 Preconditions.checkArgumentNonnegative(end, "end cannot be negative"); 640 Preconditions.checkArgumentNonnegative(maxEmojiCount, "maxEmojiCount cannot be negative"); 641 Preconditions.checkArgument(start <= end, "start should be <= than end"); 642 643 // early return since there is nothing to do 644 if (charSequence == null) { 645 return charSequence; 646 } 647 648 Preconditions.checkArgument(start <= charSequence.length(), 649 "start should be < than charSequence length"); 650 Preconditions.checkArgument(end <= charSequence.length(), 651 "end should be < than charSequence length"); 652 653 // early return since there is nothing to do 654 if (charSequence.length() == 0 || start == end) { 655 return charSequence; 656 } 657 658 final boolean replaceAll; 659 switch (replaceStrategy) { 660 case REPLACE_STRATEGY_ALL: 661 replaceAll = true; 662 break; 663 case REPLACE_STRATEGY_NON_EXISTENT: 664 replaceAll = false; 665 break; 666 case REPLACE_STRATEGY_DEFAULT: 667 default: 668 replaceAll = mReplaceAll; 669 break; 670 } 671 672 return mHelper.process(charSequence, start, end, maxEmojiCount, replaceAll); 673 } 674 675 /** 676 * Updates the EditorInfo attributes in order to communicate information to Keyboards. When 677 * used on devices running API 18 or below, does not update EditorInfo attributes. 678 * 679 * @param outAttrs EditorInfo instance passed to 680 * {@link android.widget.TextView#onCreateInputConnection(EditorInfo)} 681 * 682 * @see #EDITOR_INFO_METAVERSION_KEY 683 * @see #EDITOR_INFO_REPLACE_ALL_KEY 684 * 685 * @hide 686 */ 687 @RestrictTo(LIBRARY_GROUP) 688 public void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) { 689 if (isInitialized() && outAttrs != null && outAttrs.extras != null) { 690 mHelper.updateEditorInfoAttrs(outAttrs); 691 } 692 } 693 694 /** 695 * Factory class that creates the EmojiSpans. By default it creates {@link TypefaceEmojiSpan}. 696 * 697 * @hide 698 */ 699 @RestrictTo(LIBRARY_GROUP) 700 @RequiresApi(19) 701 static class SpanFactory { 702 /** 703 * Create EmojiSpan instance. 704 * 705 * @param metadata EmojiMetadata instance 706 * 707 * @return EmojiSpan instance 708 */ 709 EmojiSpan createSpan(@NonNull final EmojiMetadata metadata) { 710 return new TypefaceEmojiSpan(metadata); 711 } 712 } 713 714 /** 715 * Listener class for the initialization of the EmojiCompat. 716 */ 717 public abstract static class InitCallback { 718 /** 719 * Called when EmojiCompat is initialized and the emoji data is loaded. When used on devices 720 * running API 18 or below, this function is always called. 721 */ 722 public void onInitialized() { 723 } 724 725 /** 726 * Called when an unrecoverable error occurs during EmojiCompat initialization. When used on 727 * devices running API 18 or below, this function is never called. 728 */ 729 public void onFailed(@Nullable Throwable throwable) { 730 } 731 } 732 733 /** 734 * Interface to load emoji metadata. 735 */ 736 public interface MetadataLoader { 737 /** 738 * Start loading the metadata. When the loading operation is finished {@link 739 * LoaderCallback#onLoaded(MetadataRepo)} or {@link LoaderCallback#onFailed(Throwable)} 740 * should be called. When used on devices running API 18 or below, this function is never 741 * called. 742 * 743 * @param loaderCallback callback to signal the loading state 744 */ 745 void load(@NonNull LoaderCallback loaderCallback); 746 } 747 748 /** 749 * Callback to inform EmojiCompat about the state of the metadata load. Passed to MetadataLoader 750 * during {@link MetadataLoader#load(LoaderCallback)} call. 751 */ 752 public abstract static class LoaderCallback { 753 /** 754 * Called by {@link MetadataLoader} when metadata is loaded successfully. 755 * 756 * @param metadataRepo MetadataRepo instance, cannot be {@code null} 757 */ 758 public abstract void onLoaded(@NonNull MetadataRepo metadataRepo); 759 760 /** 761 * Called by {@link MetadataLoader} if an error occurs while loading the metadata. 762 * 763 * @param throwable the exception that caused the failure, {@code nullable} 764 */ 765 public abstract void onFailed(@Nullable Throwable throwable); 766 } 767 768 /** 769 * Configuration class for EmojiCompat. Changes to the values will be ignored after 770 * {@link #init(Config)} is called. 771 * 772 * @see #init(EmojiCompat.Config) 773 */ 774 public abstract static class Config { 775 private final MetadataLoader mMetadataLoader; 776 private boolean mReplaceAll; 777 private Set<InitCallback> mInitCallbacks; 778 private boolean mEmojiSpanIndicatorEnabled; 779 private int mEmojiSpanIndicatorColor = Color.GREEN; 780 781 /** 782 * Default constructor. 783 * 784 * @param metadataLoader MetadataLoader instance, cannot be {@code null} 785 */ 786 protected Config(@NonNull final MetadataLoader metadataLoader) { 787 Preconditions.checkNotNull(metadataLoader, "metadataLoader cannot be null."); 788 mMetadataLoader = metadataLoader; 789 } 790 791 /** 792 * Registers an initialization callback. 793 * 794 * @param initCallback the initialization callback to register, cannot be {@code null} 795 * 796 * @return EmojiCompat.Config instance 797 */ 798 public Config registerInitCallback(@NonNull InitCallback initCallback) { 799 Preconditions.checkNotNull(initCallback, "initCallback cannot be null"); 800 if (mInitCallbacks == null) { 801 mInitCallbacks = new ArraySet<>(); 802 } 803 804 mInitCallbacks.add(initCallback); 805 806 return this; 807 } 808 809 /** 810 * Unregisters a callback that was added before. 811 * 812 * @param initCallback the initialization callback to be removed, cannot be {@code null} 813 * 814 * @return EmojiCompat.Config instance 815 */ 816 public Config unregisterInitCallback(@NonNull InitCallback initCallback) { 817 Preconditions.checkNotNull(initCallback, "initCallback cannot be null"); 818 if (mInitCallbacks != null) { 819 mInitCallbacks.remove(initCallback); 820 } 821 return this; 822 } 823 824 /** 825 * Determines whether EmojiCompat should replace all the emojis it finds with the 826 * EmojiSpans. By default EmojiCompat tries its best to understand if the system already 827 * can render an emoji and do not replace those emojis. 828 * 829 * @param replaceAll replace all emojis found with EmojiSpans 830 * 831 * @return EmojiCompat.Config instance 832 */ 833 public Config setReplaceAll(final boolean replaceAll) { 834 mReplaceAll = replaceAll; 835 return this; 836 } 837 838 /** 839 * Determines whether a background will be drawn for the emojis that are found and 840 * replaced by EmojiCompat. Should be used only for debugging purposes. The indicator color 841 * can be set using {@link #setEmojiSpanIndicatorColor(int)}. 842 * 843 * @param emojiSpanIndicatorEnabled when {@code true} a background is drawn for each emoji 844 * that is replaced 845 */ 846 public Config setEmojiSpanIndicatorEnabled(boolean emojiSpanIndicatorEnabled) { 847 mEmojiSpanIndicatorEnabled = emojiSpanIndicatorEnabled; 848 return this; 849 } 850 851 /** 852 * Sets the color used as emoji span indicator. The default value is 853 * {@link Color#GREEN Color.GREEN}. 854 * 855 * @see #setEmojiSpanIndicatorEnabled(boolean) 856 */ 857 public Config setEmojiSpanIndicatorColor(@ColorInt int color) { 858 mEmojiSpanIndicatorColor = color; 859 return this; 860 } 861 862 /** 863 * Returns the {@link MetadataLoader}. 864 * @hide 865 */ 866 @RestrictTo(LIBRARY_GROUP) 867 public final MetadataLoader getMetadataLoader() { 868 return mMetadataLoader; 869 } 870 } 871 872 /** 873 * Runnable to call success/failure case for the listeners. 874 */ 875 private static class ListenerDispatcher implements Runnable { 876 private final List<InitCallback> mInitCallbacks; 877 private final Throwable mThrowable; 878 private final int mLoadState; 879 880 ListenerDispatcher(@NonNull final InitCallback initCallback, 881 @LoadState final int loadState) { 882 this(Arrays.asList(Preconditions.checkNotNull(initCallback, 883 "initCallback cannot be null")), loadState, null); 884 } 885 886 ListenerDispatcher(@NonNull final Collection<InitCallback> initCallbacks, 887 @LoadState final int loadState) { 888 this(initCallbacks, loadState, null); 889 } 890 891 ListenerDispatcher(@NonNull final Collection<InitCallback> initCallbacks, 892 @LoadState final int loadState, 893 @Nullable final Throwable throwable) { 894 Preconditions.checkNotNull(initCallbacks, "initCallbacks cannot be null"); 895 mInitCallbacks = new ArrayList<>(initCallbacks); 896 mLoadState = loadState; 897 mThrowable = throwable; 898 } 899 900 @Override 901 public void run() { 902 final int size = mInitCallbacks.size(); 903 switch (mLoadState) { 904 case LOAD_STATE_SUCCEEDED: 905 for (int i = 0; i < size; i++) { 906 mInitCallbacks.get(i).onInitialized(); 907 } 908 break; 909 case LOAD_STATE_FAILED: 910 default: 911 for (int i = 0; i < size; i++) { 912 mInitCallbacks.get(i).onFailed(mThrowable); 913 } 914 break; 915 } 916 } 917 } 918 919 /** 920 * Internal helper class to behave no-op for certain functions. 921 */ 922 private static class CompatInternal { 923 protected final EmojiCompat mEmojiCompat; 924 925 CompatInternal(EmojiCompat emojiCompat) { 926 mEmojiCompat = emojiCompat; 927 } 928 929 void loadMetadata() { 930 // Moves into LOAD_STATE_SUCCESS state immediately. 931 mEmojiCompat.onMetadataLoadSuccess(); 932 } 933 934 boolean hasEmojiGlyph(@NonNull final CharSequence sequence) { 935 // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis. 936 return false; 937 } 938 939 boolean hasEmojiGlyph(@NonNull final CharSequence sequence, final int metadataVersion) { 940 // Since no metadata is loaded, EmojiCompat cannot detect or render any emojis. 941 return false; 942 } 943 944 CharSequence process(@NonNull final CharSequence charSequence, 945 @IntRange(from = 0) final int start, @IntRange(from = 0) final int end, 946 @IntRange(from = 0) final int maxEmojiCount, boolean replaceAll) { 947 // Returns the given charSequence as it is. 948 return charSequence; 949 } 950 951 void updateEditorInfoAttrs(@NonNull final EditorInfo outAttrs) { 952 // Does not add any EditorInfo attributes. 953 } 954 955 void setGlyphChecker(@NonNull EmojiProcessor.GlyphChecker glyphChecker) { 956 // intentionally empty 957 } 958 } 959 960 @RequiresApi(19) 961 private static class CompatInternal19 extends CompatInternal { 962 /** 963 * Responsible to process a CharSequence and add the spans. @{code Null} until the time the 964 * metadata is loaded. 965 */ 966 private volatile EmojiProcessor mProcessor; 967 968 /** 969 * Keeps the information about emojis. Null until the time the data is loaded. 970 */ 971 private volatile MetadataRepo mMetadataRepo; 972 973 974 CompatInternal19(EmojiCompat emojiCompat) { 975 super(emojiCompat); 976 } 977 978 @Override 979 void loadMetadata() { 980 try { 981 mEmojiCompat.mMetadataLoader.load(new LoaderCallback() { 982 @Override 983 public void onLoaded(@NonNull MetadataRepo metadataRepo) { 984 onMetadataLoadSuccess(metadataRepo); 985 } 986 987 @Override 988 public void onFailed(@Nullable Throwable throwable) { 989 mEmojiCompat.onMetadataLoadFailed(throwable); 990 } 991 }); 992 } catch (Throwable t) { 993 mEmojiCompat.onMetadataLoadFailed(t); 994 } 995 } 996 997 private void onMetadataLoadSuccess(@NonNull final MetadataRepo metadataRepo) { 998 if (metadataRepo == null) { 999 mEmojiCompat.onMetadataLoadFailed( 1000 new IllegalArgumentException("metadataRepo cannot be null")); 1001 return; 1002 } 1003 1004 mMetadataRepo = metadataRepo; 1005 mProcessor = new EmojiProcessor(mMetadataRepo, new SpanFactory()); 1006 1007 mEmojiCompat.onMetadataLoadSuccess(); 1008 } 1009 1010 @Override 1011 boolean hasEmojiGlyph(@NonNull CharSequence sequence) { 1012 return mProcessor.getEmojiMetadata(sequence) != null; 1013 } 1014 1015 @Override 1016 boolean hasEmojiGlyph(@NonNull CharSequence sequence, int metadataVersion) { 1017 final EmojiMetadata emojiMetadata = mProcessor.getEmojiMetadata(sequence); 1018 return emojiMetadata != null && emojiMetadata.getCompatAdded() <= metadataVersion; 1019 } 1020 1021 @Override 1022 CharSequence process(@NonNull CharSequence charSequence, int start, int end, 1023 int maxEmojiCount, boolean replaceAll) { 1024 return mProcessor.process(charSequence, start, end, maxEmojiCount, replaceAll); 1025 } 1026 1027 @Override 1028 void updateEditorInfoAttrs(@NonNull EditorInfo outAttrs) { 1029 outAttrs.extras.putInt(EDITOR_INFO_METAVERSION_KEY, mMetadataRepo.getMetadataVersion()); 1030 outAttrs.extras.putBoolean(EDITOR_INFO_REPLACE_ALL_KEY, mEmojiCompat.mReplaceAll); 1031 } 1032 1033 @Override 1034 void setGlyphChecker(@NonNull EmojiProcessor.GlyphChecker glyphChecker) { 1035 mProcessor.setGlyphChecker(glyphChecker); 1036 } 1037 } 1038} 1039