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