Buiild emoji2 inline

pull/212/head
M66B 2 years ago
parent 019cab732f
commit 670aaf1970

@ -424,6 +424,9 @@ configurations.all {
} else if (details.requested.group == "org.simplejavamail") {
print("Pinning " + details.requested.group + ":" + details.requested.name + "\n")
details.useVersion "1.7.13"
} else if (details.requested.group == "com.google.flatbuffers") {
print("Pinning " + details.requested.group + ":" + details.requested.name + "\n")
details.useVersion "2.0.0"
}
}
}
@ -436,6 +439,7 @@ dependencies {
def core_version = "1.10.0-rc01" // 1.11.0-alpha01
def appcompat_version = "1.6.1" // 1.7.0-alpha02
def emoji_version = "1.3.0" // 1.4.0-alpha01
def flatbuffers_version = "2.0.0"
def activity_version = "1.7.0"
def fragment_version = "1.5.6" // 1.6.0-alpha08
def windows_version = "1.0.0" // 1.1.0-alpha06
@ -501,7 +505,8 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.fragment/fragment
// https://mvnrepository.com/artifact/androidx.window/window-java
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.emoji2:emoji2:$emoji_version"
//implementation "androidx.emoji2:emoji2:$emoji_version"
implementation "com.google.flatbuffers:flatbuffers-java:$flatbuffers_version"
implementation "androidx.activity:activity:$activity_version"
implementation "androidx.fragment:fragment:$fragment_version"
implementation "androidx.window:window-java:$windows_version"

@ -0,0 +1,108 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Various (internal) helpers for interacting with threads and event loops
*/
class ConcurrencyHelpers {
// It is expected that all callers of this internal API call shutdown on completion, allow 15
// seconds for retry delay before spinning down the thread.
private static final int FONT_LOAD_TIMEOUT_SECONDS = 15 /* seconds */;
private ConcurrencyHelpers() { /* can't instantiate */ }
/**
* Background thread worker with an explicit thread name.
*
* It is expected that callers explicitly shut down the returned ThreadPoolExecutor as soon
* as they have completed font loading.
*
* @param name name of thread
* @return ThreadPoolExecutor limited to one thread with a timeout of 15 seconds.
*/
@SuppressWarnings("ThreadPriorityCheck")
static ThreadPoolExecutor createBackgroundPriorityExecutor(@NonNull String name) {
ThreadFactory threadFactory = runnable -> {
Thread t = new Thread(runnable, name);
t.setPriority(Process.THREAD_PRIORITY_BACKGROUND);
return t;
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(
0 /* corePoolSize */,
1 /* maximumPoolSize */,
FONT_LOAD_TIMEOUT_SECONDS /* keepAliveTime */,
TimeUnit.SECONDS /* keepAliveTime TimeUnit */,
new LinkedBlockingDeque<>() /* unbounded queue*/,
threadFactory
);
executor.allowCoreThreadTimeOut(true);
return executor;
}
/**
* @return Main thread handler, with createAsync if API level supports it.
*/
static Handler mainHandlerAsync() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return Handler28Impl.createAsync(Looper.getMainLooper());
} else {
return new Handler(Looper.getMainLooper());
}
}
/**
* @deprecated Exists only for upgrade path, remove with
* {@link FontRequestEmojiCompatConfig#setHandler(Handler)}
*
* @param handler a background thread handler
* @return an executor that posts all work to that handler
*/
@NonNull
@Deprecated
static Executor convertHandlerToExecutor(@NonNull Handler handler) {
return handler::post;
}
@RequiresApi(28)
static class Handler28Impl {
private Handler28Impl() {
// Non-instantiable.
}
@DoNotInline
public static Handler createAsync(Looper looper) {
return Handler.createAsync(looper);
}
}
}

@ -0,0 +1,343 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.provider.FontRequest;
import androidx.core.util.Preconditions;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* The default config will use downloadable fonts to fetch the emoji compat font file.
*
* <p>It will automatically fetch the emoji compat font from a {@code ContentProvider} that is
* installed on the devices system image, if present.</p>
*
* <p>You should use this if you want the default emoji font from a system installed
* downloadable fonts provider. This is the recommended behavior for all applications unless
* they install a custom emoji font.</p>
*
* <p>You may need to specialize the configuration beyond this default config in some
* situations:</p>
* <ul>
* <li>If you are trying to install a custom emoji font through downloadable fonts use
* {@link FontRequestEmojiCompatConfig} instead of this method.</li>
* <li>If you're trying to bundle an emoji font with your APK use {@code
* BundledEmojiCompatConfig} in the {@code emoji2-bundled} artifact.</li>
* <li>If you are building an APK that will be installed on devices that won't have a
* downloadable fonts provider, use {@code BundledEmojiCompatConfig}.</li>
* </ul>
*
* <p>The downloadable font provider used by {@code DefaultEmojiCompatConfig} always satisfies
* the following contract:</p>
* <ol>
* <li>It <i>MUST</i> provide an intent filter for {@code androidx.content.action.LOAD_EMOJI_FONT}.
* </li>
* <li>It <i>MUST</i> respond to the query {@code emojicompat-emoji-font} with a valid emoji compat
* font file including metadata.</li>
* <li>It <i>MUST</i> provide fonts via the same contract as downloadable fonts.</li>
* <li>It <i>MUST</i> be installed in the system image.</li>
* </ol>
*/
public final class DefaultEmojiCompatConfig {
/**
* This class cannot be instantiated.
*
* @see DefaultEmojiCompatConfig#create
*/
private DefaultEmojiCompatConfig() {
}
/**
* Get the default emoji compat config for this device.
*
* You may further configure the returned config before passing it to {@link EmojiCompat#init}.
*
* Each call to this method will return a new EmojiCompat.Config, so changes to the returned
* object will not modify future return values.
*
* @param context context for lookup
* @return A valid config for downloading the emoji compat font, or null if no font provider
* could be found.
*/
@Nullable
public static FontRequestEmojiCompatConfig create(@NonNull Context context) {
return (FontRequestEmojiCompatConfig) new DefaultEmojiCompatConfigFactory(null)
.create(context);
}
/**
* Actual factory for generating default emoji configs, does service locator lookup internally.
*
* @see DefaultEmojiCompatConfig#create
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static class DefaultEmojiCompatConfigFactory {
private static final @NonNull String TAG = "emoji2.text.DefaultEmojiConfig";
private static final @NonNull String INTENT_LOAD_EMOJI_FONT =
"androidx.content.action.LOAD_EMOJI_FONT";
private static final @NonNull String DEFAULT_EMOJI_QUERY = "emojicompat-emoji-font";
private final DefaultEmojiCompatConfigHelper mHelper;
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public DefaultEmojiCompatConfigFactory(@Nullable DefaultEmojiCompatConfigHelper helper) {
mHelper = helper != null ? helper : getHelperForApi();
}
/**
* @see DefaultEmojiCompatConfig#create
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Nullable
public EmojiCompat.Config create(@NonNull Context context) {
return configOrNull(context, (FontRequest) queryForDefaultFontRequest(context));
}
/**
* Create a new Config if fontRequest is not null
* @param context context for the config
* @param fontRequest optional font request
* @return a new config if fontRequest is not null
*/
@Nullable
private EmojiCompat.Config configOrNull(@NonNull Context context,
@Nullable FontRequest fontRequest) {
if (fontRequest == null) {
return null;
} else {
return new FontRequestEmojiCompatConfig(context, fontRequest);
}
}
/**
* Find the installed font provider and return a FontInfo that describes it.
* @param context context for getting package manager
* @return valid FontRequest, or null if no provider could be found
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Nullable
@VisibleForTesting
FontRequest queryForDefaultFontRequest(@NonNull Context context) {
PackageManager packageManager = context.getPackageManager();
// throw here since the developer has provided an atypical Context
Preconditions.checkNotNull(packageManager,
"Package manager required to locate emoji font provider");
ProviderInfo providerInfo = queryDefaultInstalledContentProvider(packageManager);
if (providerInfo == null) return null;
try {
return generateFontRequestFrom(providerInfo, packageManager);
} catch (PackageManager.NameNotFoundException e) {
Log.wtf(TAG, e);
return null;
}
}
/**
* Look up a ContentProvider that provides emoji fonts that's installed with the system.
*
* @param packageManager package manager from a Context
* @return a ResolveInfo for a system installed content provider, or null if none found
*/
@Nullable
private ProviderInfo queryDefaultInstalledContentProvider(
@NonNull PackageManager packageManager) {
List<ResolveInfo> providers = mHelper.queryIntentContentProviders(packageManager,
new Intent(INTENT_LOAD_EMOJI_FONT), 0);
for (ResolveInfo resolveInfo : providers) {
ProviderInfo providerInfo = mHelper.getProviderInfo(resolveInfo);
if (hasFlagSystem(providerInfo)) {
return providerInfo;
}
}
return null;
}
/**
* @param providerInfo optional ProviderInfo that describes a content provider
* @return true if this provider info is from an application with
* {@link ApplicationInfo#FLAG_SYSTEM}
*/
private boolean hasFlagSystem(@Nullable ProviderInfo providerInfo) {
return providerInfo != null
&& providerInfo.applicationInfo != null
&& (providerInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
== ApplicationInfo.FLAG_SYSTEM;
}
/**
* Generate a full FontRequest from a ResolveInfo that describes a ContentProvider.
*
* @param providerInfo description of content provider to generate a FontRequest from
* @return a valid font request
* @throws NullPointerException if the passed resolveInfo has a null providerInfo.
*/
@NonNull
private FontRequest generateFontRequestFrom(
@NonNull ProviderInfo providerInfo,
@NonNull PackageManager packageManager
) throws PackageManager.NameNotFoundException {
String providerAuthority = providerInfo.authority;
String providerPackage = providerInfo.packageName;
Signature[] signingSignatures = mHelper.getSigningSignatures(packageManager,
providerPackage);
List<List<byte[]>> signatures = convertToByteArray(signingSignatures);
return new FontRequest(providerAuthority, providerPackage, DEFAULT_EMOJI_QUERY,
signatures);
}
/**
* Convert signatures into a form usable by a FontConfig
*/
@NonNull
private List<List<byte[]>> convertToByteArray(@NonNull Signature[] signatures) {
List<byte[]> shaList = new ArrayList<>();
for (Signature signature : signatures) {
shaList.add(signature.toByteArray());
}
return Collections.singletonList(shaList);
}
/**
* @return the right DefaultEmojiCompatConfigHelper for the device API
*/
@NonNull
private static DefaultEmojiCompatConfigHelper getHelperForApi() {
if (Build.VERSION.SDK_INT >= 28) {
return new DefaultEmojiCompatConfigHelper_API28();
} else if (Build.VERSION.SDK_INT >= 19) {
return new DefaultEmojiCompatConfigHelper_API19();
} else {
return new DefaultEmojiCompatConfigHelper();
}
}
}
/**
* Helper to lookup signatures in package manager.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static class DefaultEmojiCompatConfigHelper {
/**
* Get the signing signatures for a package in package manager.
*/
@SuppressWarnings("deprecation") // replaced in API 28
@NonNull
public Signature[] getSigningSignatures(@NonNull PackageManager packageManager,
@NonNull String providerPackage) throws PackageManager.NameNotFoundException {
PackageInfo packageInfoForSignatures = packageManager.getPackageInfo(providerPackage,
PackageManager.GET_SIGNATURES);
return packageInfoForSignatures.signatures;
}
/**
* Get the content provider by intent.
*/
@NonNull
public List<ResolveInfo> queryIntentContentProviders(@NonNull PackageManager packageManager,
@NonNull Intent intent, int flags) {
return Collections.emptyList();
}
/**
* Get a ProviderInfo, if present, from a ResolveInfo
* @param resolveInfo the subject
* @return resolveInfo.providerInfo above API 19
*/
@Nullable
public ProviderInfo getProviderInfo(@NonNull ResolveInfo resolveInfo) {
throw new IllegalStateException("Unable to get provider info prior to API 19");
}
}
/**
* Actually do lookups > API 19
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(19)
public static class DefaultEmojiCompatConfigHelper_API19
extends DefaultEmojiCompatConfigHelper {
@NonNull
@Override
@SuppressWarnings("deprecation")
public List<ResolveInfo> queryIntentContentProviders(@NonNull PackageManager packageManager,
@NonNull Intent intent, int flags) {
return packageManager.queryIntentContentProviders(intent, flags);
}
@Nullable
@Override
public ProviderInfo getProviderInfo(@NonNull ResolveInfo resolveInfo) {
return resolveInfo.providerInfo;
}
}
/**
* Helper to lookup signatures in package manager > API 28
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(28)
public static class DefaultEmojiCompatConfigHelper_API28
extends DefaultEmojiCompatConfigHelper_API19 {
@SuppressWarnings("deprecation") // using deprecated API to match exact behavior in core
@Override
@NonNull
public Signature[] getSigningSignatures(@NonNull PackageManager packageManager,
@NonNull String providerPackage)
throws PackageManager.NameNotFoundException {
// This uses the deprecated GET_SIGNATURES currently to match the behavior in Core.
// When that behavior changes, we will need to update this method.
// Alternatively, you may at that time introduce a new config option that allows
// skipping signature validations to avoid this code sync.
PackageInfo packageInfoForSignatures = packageManager.getPackageInfo(providerPackage,
PackageManager.GET_SIGNATURES);
return packageInfoForSignatures.signatures;
}
}
}

@ -0,0 +1,89 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.os.Build;
import android.text.TextPaint;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.core.graphics.PaintCompat;
/**
* Utility class that checks if the system can render a given glyph.
*
* @hide
*/
@AnyThread
@RestrictTo(LIBRARY)
class DefaultGlyphChecker implements EmojiCompat.GlyphChecker {
/**
* Default text size for {@link #mTextPaint}.
*/
private static final int PAINT_TEXT_SIZE = 10;
/**
* Used to create strings required by
* {@link PaintCompat#hasGlyph(android.graphics.Paint, String)}.
*/
private static final ThreadLocal<StringBuilder> sStringBuilder = new ThreadLocal<>();
/**
* TextPaint used during {@link PaintCompat#hasGlyph(android.graphics.Paint, String)} check.
*/
private final TextPaint mTextPaint;
DefaultGlyphChecker() {
mTextPaint = new TextPaint();
mTextPaint.setTextSize(PAINT_TEXT_SIZE);
}
@Override
public boolean hasGlyph(
@NonNull CharSequence charSequence,
int start,
int end,
int sdkAdded
) {
// For pre M devices, heuristic in PaintCompat can result in false positives. we are
// adding another heuristic using the sdkAdded field. if the emoji was added to OS
// at a later version we assume that the system probably cannot render it.
if (Build.VERSION.SDK_INT < 23 && sdkAdded > Build.VERSION.SDK_INT) {
return false;
}
final StringBuilder builder = getStringBuilder();
builder.setLength(0);
while (start < end) {
builder.append(charSequence.charAt(start));
start++;
}
return PaintCompat.hasGlyph(mTextPaint, builder.toString());
}
private static StringBuilder getStringBuilder() {
if (sStringBuilder.get() == null) {
sStringBuilder.set(new StringBuilder());
}
return sStringBuilder.get();
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,208 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.WorkerThread;
import androidx.core.os.TraceCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleInitializer;
import androidx.startup.AppInitializer;
import androidx.startup.Initializer;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Initializer for configuring EmojiCompat with the system installed downloadable font provider.
*
* <p>This initializer will initialize EmojiCompat immediately then defer loading the font for a
* short delay to avoid delaying application startup. Typically, the font will be loaded shortly
* after the first screen of your application loads, which means users may see system emoji
* briefly prior to the compat font loading.</p>
*
* <p>This is the recommended configuration for all apps that don't need specialized configuration,
* and don't need to control the background thread that initialization runs on. For more information
* see {@link androidx.emoji2.text.DefaultEmojiCompatConfig}.</p>
*
* <p>In addition to the reasons listed in {@code DefaultEmojiCompatConfig} you may wish to disable
* this automatic configuration if you intend to call initialization from an existing background
* thread pool in your application.</p>
*
* <p>This is enabled by default by including the {@code :emoji2:emoji2} gradle artifact. To
* disable the default configuration (and allow manual configuration) add this to your manifest:</p>
*
* <pre>
* &lt;provider
* android:name="androidx.startup.InitializationProvider"
* android:authorities="${applicationId}.androidx-startup"
* android:exported="false"
* tools:node="merge"&gt;
* &lt;meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer"
* tools:node="remove" /&gt;
* &lt;/provider&gt;
* </pre>
*
* This initializer depends on {@link ProcessLifecycleInitializer}.
*
* @see androidx.emoji2.text.DefaultEmojiCompatConfig
*/
public class EmojiCompatInitializer implements Initializer<Boolean> {
private static final long STARTUP_THREAD_CREATION_DELAY_MS = 500L;
private static final String S_INITIALIZER_THREAD_NAME = "EmojiCompatInitializer";
/**
* Initialize EmojiCompat with the app's context.
*
* @param context application context
* @return result of default init
*/
@SuppressWarnings("AutoBoxing")
@NonNull
@Override
public Boolean create(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= 19) {
EmojiCompat.init(new BackgroundDefaultConfig(context));
delayUntilFirstResume(context);
return true;
}
return false;
}
/**
* Wait until the first frame of the application to do anything.
*
* This allows startup code to run before the delay is scheduled.
*/
@RequiresApi(19)
void delayUntilFirstResume(@NonNull Context context) {
// schedule delay after first Activity resumes
AppInitializer appInitializer = AppInitializer.getInstance(context);
LifecycleOwner lifecycleOwner = appInitializer
.initializeComponent(ProcessLifecycleInitializer.class);
Lifecycle lifecycle = lifecycleOwner.getLifecycle();
lifecycle.addObserver(new DefaultLifecycleObserver() {
@Override
public void onResume(@NonNull LifecycleOwner owner) {
loadEmojiCompatAfterDelay();
lifecycle.removeObserver(this);
}
});
}
@RequiresApi(19)
void loadEmojiCompatAfterDelay() {
final Handler mainHandler = ConcurrencyHelpers.mainHandlerAsync();
mainHandler.postDelayed(new LoadEmojiCompatRunnable(), STARTUP_THREAD_CREATION_DELAY_MS);
}
/**
* Dependes on ProcessLifecycleInitializer
*/
@NonNull
@Override
public List<Class<? extends Initializer<?>>> dependencies() {
return Collections.singletonList(ProcessLifecycleInitializer.class);
}
static class LoadEmojiCompatRunnable implements Runnable {
@Override
public void run() {
try {
// this is main thread, so mark what we're doing (this trace includes thread
// start time in BackgroundLoadingLoader.load
TraceCompat.beginSection("EmojiCompat.EmojiCompatInitializer.run");
if (EmojiCompat.isConfigured()) {
EmojiCompat.get().load();
}
} finally {
TraceCompat.endSection();
}
}
}
@RequiresApi(19)
static class BackgroundDefaultConfig extends EmojiCompat.Config {
protected BackgroundDefaultConfig(Context context) {
super(new BackgroundDefaultLoader(context));
setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
}
}
@RequiresApi(19)
static class BackgroundDefaultLoader implements EmojiCompat.MetadataRepoLoader {
private final Context mContext;
BackgroundDefaultLoader(Context context) {
mContext = context.getApplicationContext();
}
@Override
public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
ThreadPoolExecutor executor = ConcurrencyHelpers.createBackgroundPriorityExecutor(
S_INITIALIZER_THREAD_NAME);
executor.execute(() -> doLoad(loaderCallback, executor));
}
@WorkerThread
void doLoad(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback,
@NonNull ThreadPoolExecutor executor) {
try {
FontRequestEmojiCompatConfig config = DefaultEmojiCompatConfig.create(mContext);
if (config == null) {
throw new RuntimeException("EmojiCompat font provider not available on this "
+ "device.");
}
config.setLoadingExecutor(executor);
config.getMetadataRepoLoader().load(new EmojiCompat.MetadataRepoLoaderCallback() {
@Override
public void onLoaded(@NonNull MetadataRepo metadataRepo) {
try {
// main thread is notified before returning, so we can quit now
loaderCallback.onLoaded(metadataRepo);
} finally {
executor.shutdown();
}
}
@Override
public void onFailed(@Nullable Throwable throwable) {
try {
// main thread is notified before returning, so we can quit now
loaderCallback.onFailed(throwable);
} finally {
executor.shutdown();
}
}
});
} catch (Throwable t) {
loaderCallback.onFailed(t);
executor.shutdown();
}
}
}
}

@ -0,0 +1,35 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import androidx.annotation.RestrictTo;
/**
* Defaults for emojicompat
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class EmojiDefaults {
private EmojiDefaults() {}
/**
* Default value for maxEmojiCount if not specified.
*/
public static final int MAX_EMOJI_COUNT = Integer.MAX_VALUE;
}

@ -0,0 +1,89 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.annotation.SuppressLint;
import android.os.Build;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
class EmojiExclusions {
private EmojiExclusions() { /* cannot instantiate */ }
@NonNull
static Set<int[]> getEmojiExclusions() {
if (Build.VERSION.SDK_INT >= 34) {
return EmojiExclusions_Api34.getExclusions();
} else {
return EmojiExclusions_Reflections.getExclusions();
}
}
@RequiresApi(34)
private static class EmojiExclusions_Api34 {
private EmojiExclusions_Api34() { /* cannot instantiate */ }
@NonNull
@DoNotInline
static Set<int[]> getExclusions() {
// TODO: Call directly when API34 is published
return EmojiExclusions_Reflections.getExclusions();
}
}
private static class EmojiExclusions_Reflections {
private EmojiExclusions_Reflections() { /* cannot instantiate */ }
/**
* Attempt to reflectively call EmojiExclusion
*
* If anything goes wrong, return Collections.emptySet.
*/
@SuppressWarnings("unchecked")
// will be checked after platform API for 34 published
@SuppressLint({ "BanUncheckedReflection" })
@NonNull
static Set<int[]> getExclusions() {
try {
Class<?> clazz = Class.forName("android.text.EmojiConsistency");
Method method = clazz.getMethod("getEmojiConsistencySet");
Object result = method.invoke(null);
if (result == null) {
return Collections.emptySet();
}
// validate the result type before exposing it to caller
Set<?> resultList = (Set<?>) result;
for (Object item : resultList) {
if (!(item instanceof int[])) {
return Collections.emptySet();
}
}
return (Set<int[]>) resultList;
} catch (Throwable ignore) {
return Collections.emptySet();
}
}
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,150 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.TESTS;
import android.annotation.SuppressLint;
import android.graphics.Paint;
import android.text.style.ReplacementSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
/**
* Base span class for the emoji replacement. When an emoji is found and needs to be replaced in a
* CharSequence, an instance of this class is added to the CharSequence.
*/
@RequiresApi(19)
public abstract class EmojiSpan extends ReplacementSpan {
/**
* Temporary object to calculate the size of the span.
*/
private final Paint.FontMetricsInt mTmpFontMetrics = new Paint.FontMetricsInt();
/**
* Information about emoji. This is not parcelled since we do not want multiple objects
* representing same emoji to be in memory. When unparcelled, EmojiSpan tries to set it back
* using the singleton EmojiCompat instance.
*/
@NonNull
private final TypefaceEmojiRasterizer mRasterizer;
/**
* Cached width of the span. Width is calculated according to the font metrics.
*/
private short mWidth = -1;
/**
* Cached height of the span. Height is calculated according to the font metrics.
*/
private short mHeight = -1;
/**
* Cached ratio of current font height to emoji image height.
*/
private float mRatio = 1.0f;
/**
* Default constructor.
*
* @param rasterizer information about the emoji, cannot be {@code null}
*
* @hide
*/
@RestrictTo(LIBRARY)
EmojiSpan(@NonNull final TypefaceEmojiRasterizer rasterizer) {
Preconditions.checkNotNull(rasterizer, "rasterizer cannot be null");
mRasterizer = rasterizer;
}
@Override
public int getSize(@NonNull final Paint paint,
@SuppressLint("UnknownNullness") @SuppressWarnings("MissingNullability")
final CharSequence text,
final int start,
final int end,
@Nullable final Paint.FontMetricsInt fm) {
paint.getFontMetricsInt(mTmpFontMetrics);
final int fontHeight = Math.abs(mTmpFontMetrics.descent - mTmpFontMetrics.ascent);
mRatio = fontHeight * 1.0f / mRasterizer.getHeight();
mHeight = (short) (mRasterizer.getHeight() * mRatio);
mWidth = (short) (mRasterizer.getWidth() * mRatio);
if (fm != null) {
fm.ascent = mTmpFontMetrics.ascent;
fm.descent = mTmpFontMetrics.descent;
fm.top = mTmpFontMetrics.top;
fm.bottom = mTmpFontMetrics.bottom;
}
return mWidth;
}
/**
* Get the rasterizer that draws this emoji.
*
* @return rasterizer to draw emoji
*/
@NonNull
public final TypefaceEmojiRasterizer getTypefaceRasterizer() {
return mRasterizer;
}
/**
* @return width of the span
*
* @hide
*/
@RestrictTo(LIBRARY)
final int getWidth() {
return mWidth;
}
/**
* @return height of the span
*
* @hide
*/
@RestrictTo(TESTS)
public final int getHeight() {
return mHeight;
}
/**
* @hide
*/
@RestrictTo(LIBRARY)
final float getRatio() {
return mRatio;
}
/**
* @return unique id for the emoji that this EmojiSpan is used for
*
* @hide
*/
@RestrictTo(TESTS)
public final int getId() {
return getTypefaceRasterizer().getId();
}
}

@ -0,0 +1,444 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.ContentObserver;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.WorkerThread;
import androidx.core.graphics.TypefaceCompatUtil;
import androidx.core.os.TraceCompat;
import androidx.core.provider.FontRequest;
import androidx.core.provider.FontsContractCompat;
import androidx.core.provider.FontsContractCompat.FontFamilyResult;
import androidx.core.util.Preconditions;
import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* {@link EmojiCompat.Config} implementation that asynchronously fetches the required font and the
* metadata using a {@link FontRequest}. FontRequest should be constructed to fetch an EmojiCompat
* compatible emoji font.
* <p/>
*/
public class FontRequestEmojiCompatConfig extends EmojiCompat.Config {
/**
* Retry policy used when the font provider is not ready to give the font file.
*
* To control the thread the retries are handled on, see
* {@link FontRequestEmojiCompatConfig#setLoadingExecutor}.
*/
public abstract static class RetryPolicy {
/**
* Called each time the metadata loading fails.
*
* This is primarily due to a pending download of the font.
* If a value larger than zero is returned, metadata loader will retry after the given
* milliseconds.
* <br />
* If {@code zero} is returned, metadata loader will retry immediately.
* <br/>
* If a value less than 0 is returned, the metadata loader will stop retrying and
* EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
* <p/>
* Note that the retry may happen earlier than you specified if the font provider notifies
* that the download is completed.
*
* @return long milliseconds to wait until next retry
*/
public abstract long getRetryDelay();
}
/**
* A retry policy implementation that doubles the amount of time in between retries.
*
* If downloading hasn't finish within given amount of time, this policy give up and the
* EmojiCompat will get into {@link EmojiCompat#LOAD_STATE_FAILED} state.
*/
public static class ExponentialBackoffRetryPolicy extends RetryPolicy {
private final long mTotalMs;
private long mRetryOrigin;
/**
* @param totalMs A total amount of time to wait in milliseconds.
*/
public ExponentialBackoffRetryPolicy(long totalMs) {
mTotalMs = totalMs;
}
@Override
public long getRetryDelay() {
if (mRetryOrigin == 0) {
mRetryOrigin = SystemClock.uptimeMillis();
// Since download may be completed after getting query result and before registering
// observer, requesting later at the same time.
return 0;
} else {
// Retry periodically since we can't trust notify change event. Some font provider
// may not notify us.
final long elapsedMillis = SystemClock.uptimeMillis() - mRetryOrigin;
if (elapsedMillis > mTotalMs) {
return -1; // Give up since download hasn't finished in 10 min.
}
// Wait until the same amount of the time from the first scheduled time, but adjust
// the minimum request interval is 1 sec and never exceeds 10 min in total.
return Math.min(Math.max(elapsedMillis, 1000), mTotalMs - elapsedMillis);
}
}
}
/**
* @param context Context instance, cannot be {@code null}
* @param request {@link FontRequest} to fetch the font asynchronously, cannot be {@code null}
*/
public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request) {
super(new FontRequestMetadataLoader(context, request, DEFAULT_FONTS_CONTRACT));
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request,
@NonNull FontProviderHelper fontProviderHelper) {
super(new FontRequestMetadataLoader(context, request, fontProviderHelper));
}
/**
* Sets the custom executor to be used for initialization.
*
* Since font loading is too slow for the main thread, the metadata loader will fetch the fonts
* on a background thread. By default, FontRequestEmojiCompatConfig will create its own
* single threaded Executor, which causes a thread to be created.
*
* You can pass your own executor to control which thread the font is loaded on, and avoid an
* extra thread creation.
*
* @param executor background executor for performing font load
*/
@NonNull
public FontRequestEmojiCompatConfig setLoadingExecutor(@NonNull Executor executor) {
((FontRequestMetadataLoader) getMetadataRepoLoader()).setExecutor(executor);
return this;
}
/**
* Please us {@link #setLoadingExecutor(Executor)} instead to set background loading thread.
*
* This was deprecated in emoji2 1.0.0-alpha04.
*
* If migrating from androidx.emoji please prefer to use an existing background executor for
* setLoadingExecutor.
*
* Note: This method will no longer have any effect if passed null, which is a breaking
* change from androidx.emoji.
*
* @deprecated please call setLoadingExecutor instead
*
* @param handler background thread handler to wrap in an Executor, if null this method will
* do nothing
*/
@Deprecated
@NonNull
@SuppressWarnings("deprecation")
public FontRequestEmojiCompatConfig setHandler(@Nullable Handler handler) {
if (handler == null) {
// this is a breaking behavior change from androidx.emoji, we no longer support
// clearing executors
return this;
}
setLoadingExecutor(ConcurrencyHelpers.convertHandlerToExecutor(handler));
return this;
}
/**
* Sets the retry policy.
*
* {@see RetryPolicy}
* @param policy The policy to be used when the font provider is not ready to give the font
* file. Can be {@code null}. In case of {@code null}, the metadata loader never
* retries.
*/
@NonNull
public FontRequestEmojiCompatConfig setRetryPolicy(@Nullable RetryPolicy policy) {
((FontRequestMetadataLoader) getMetadataRepoLoader()).setRetryPolicy(policy);
return this;
}
/**
* MetadataRepoLoader implementation that uses FontsContractCompat and TypefaceCompat to load a
* given FontRequest.
*/
private static class FontRequestMetadataLoader implements EmojiCompat.MetadataRepoLoader {
private static final String S_TRACE_BUILD_TYPEFACE =
"EmojiCompat.FontRequestEmojiCompatConfig.buildTypeface";
@NonNull
private final Context mContext;
@NonNull
private final FontRequest mRequest;
@NonNull
private final FontProviderHelper mFontProviderHelper;
@NonNull
private final Object mLock = new Object();
@GuardedBy("mLock")
@Nullable
private Handler mMainHandler;
@GuardedBy("mLock")
@Nullable
private Executor mExecutor;
@GuardedBy("mLock")
@Nullable
private ThreadPoolExecutor mMyThreadPoolExecutor;
@GuardedBy("mLock")
@Nullable
private RetryPolicy mRetryPolicy;
@GuardedBy("mLock")
@Nullable
EmojiCompat.MetadataRepoLoaderCallback mCallback;
@GuardedBy("mLock")
@Nullable
private ContentObserver mObserver;
@GuardedBy("mLock")
@Nullable
private Runnable mMainHandlerLoadCallback;
FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request,
@NonNull FontProviderHelper fontProviderHelper) {
Preconditions.checkNotNull(context, "Context cannot be null");
Preconditions.checkNotNull(request, "FontRequest cannot be null");
mContext = context.getApplicationContext();
mRequest = request;
mFontProviderHelper = fontProviderHelper;
}
public void setExecutor(@NonNull Executor executor) {
synchronized (mLock) {
mExecutor = executor;
}
}
public void setRetryPolicy(@Nullable RetryPolicy policy) {
synchronized (mLock) {
mRetryPolicy = policy;
}
}
@Override
@RequiresApi(19)
public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
Preconditions.checkNotNull(loaderCallback, "LoaderCallback cannot be null");
synchronized (mLock) {
mCallback = loaderCallback;
}
loadInternal();
}
@RequiresApi(19)
void loadInternal() {
synchronized (mLock) {
if (mCallback == null) {
// do nothing; loading is already complete
return;
}
if (mExecutor == null) {
mMyThreadPoolExecutor = ConcurrencyHelpers.createBackgroundPriorityExecutor(
"emojiCompat");
mExecutor = mMyThreadPoolExecutor;
}
mExecutor.execute(this::createMetadata);
}
}
@WorkerThread
private FontsContractCompat.FontInfo retrieveFontInfo() {
final FontsContractCompat.FontFamilyResult result;
try {
result = mFontProviderHelper.fetchFonts(mContext, mRequest);
} catch (NameNotFoundException e) {
throw new RuntimeException("provider not found", e);
}
if (result.getStatusCode() != FontsContractCompat.FontFamilyResult.STATUS_OK) {
throw new RuntimeException("fetchFonts failed (" + result.getStatusCode() + ")");
}
final FontsContractCompat.FontInfo[] fonts = result.getFonts();
if (fonts == null || fonts.length == 0) {
throw new RuntimeException("fetchFonts failed (empty result)");
}
return fonts[0]; // Assuming the GMS Core provides only one font file.
}
@RequiresApi(19)
@WorkerThread
private void scheduleRetry(Uri uri, long waitMs) {
synchronized (mLock) {
Handler handler = mMainHandler;
if (handler == null) {
handler = ConcurrencyHelpers.mainHandlerAsync();
mMainHandler = handler;
}
if (mObserver == null) {
mObserver = new ContentObserver(handler) {
@Override
public void onChange(boolean selfChange, Uri uri) {
loadInternal();
}
};
mFontProviderHelper.registerObserver(mContext, uri, mObserver);
}
if (mMainHandlerLoadCallback == null) {
mMainHandlerLoadCallback = this::loadInternal;
}
handler.postDelayed(mMainHandlerLoadCallback, waitMs);
}
}
// Must be called on the mHandler.
private void cleanUp() {
synchronized (mLock) {
mCallback = null;
if (mObserver != null) {
mFontProviderHelper.unregisterObserver(mContext, mObserver);
mObserver = null;
}
if (mMainHandler != null) {
mMainHandler.removeCallbacks(mMainHandlerLoadCallback);
}
mMainHandler = null;
if (mMyThreadPoolExecutor != null) {
// if we made the executor, shut it down
mMyThreadPoolExecutor.shutdown();
}
mExecutor = null;
mMyThreadPoolExecutor = null;
}
}
// Must be called on the mHandler.
@RequiresApi(19)
@SuppressWarnings("WeakerAccess") /* synthetic access */
@WorkerThread
void createMetadata() {
synchronized (mLock) {
if (mCallback == null) {
return; // Already handled or cancelled. Do nothing.
}
}
try {
final FontsContractCompat.FontInfo font = retrieveFontInfo();
final int resultCode = font.getResultCode();
if (resultCode == FontsContractCompat.Columns.RESULT_CODE_FONT_UNAVAILABLE) {
// The font provider is now downloading. Ask RetryPolicy for when to retry next.
synchronized (mLock) {
if (mRetryPolicy != null) {
final long delayMs = mRetryPolicy.getRetryDelay();
if (delayMs >= 0) {
scheduleRetry(font.getUri(), delayMs);
return;
}
}
}
}
if (resultCode != FontsContractCompat.Columns.RESULT_CODE_OK) {
throw new RuntimeException("fetchFonts result is not OK. (" + resultCode + ")");
}
final MetadataRepo metadataRepo;
try {
TraceCompat.beginSection(S_TRACE_BUILD_TYPEFACE);
// TODO: Good to add new API to create Typeface from FD not to open FD twice.
final Typeface typeface = mFontProviderHelper.buildTypeface(mContext, font);
final ByteBuffer buffer = TypefaceCompatUtil.mmap(mContext, null,
font.getUri());
if (buffer == null || typeface == null) {
throw new RuntimeException("Unable to open file.");
}
metadataRepo = MetadataRepo.create(typeface, buffer);
} finally {
TraceCompat.endSection();
}
synchronized (mLock) {
if (mCallback != null) {
mCallback.onLoaded(metadataRepo);
}
}
cleanUp();
} catch (Throwable t) {
synchronized (mLock) {
if (mCallback != null) {
mCallback.onFailed(t);
}
}
cleanUp();
}
}
}
/**
* Delegate class for mocking FontsContractCompat.fetchFonts.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static class FontProviderHelper {
/** Calls FontsContractCompat.fetchFonts. */
@NonNull
public FontFamilyResult fetchFonts(@NonNull Context context,
@NonNull FontRequest request) throws NameNotFoundException {
return FontsContractCompat.fetchFonts(context, null /* cancellation signal */, request);
}
/** Calls FontsContractCompat.buildTypeface. */
@Nullable
public Typeface buildTypeface(@NonNull Context context,
@NonNull FontsContractCompat.FontInfo font) throws NameNotFoundException {
return FontsContractCompat.buildTypeface(context, null /* cancellation signal */,
new FontsContractCompat.FontInfo[] { font });
}
/** Calls Context.getContentObserver().registerObserver */
public void registerObserver(@NonNull Context context, @NonNull Uri uri,
@NonNull ContentObserver observer) {
context.getContentResolver().registerContentObserver(
uri, false /* notifyForDescendants */, observer);
}
/** Calls Context.getContentObserver().unregisterObserver */
public void unregisterObserver(@NonNull Context context,
@NonNull ContentObserver observer) {
context.getContentResolver().unregisterContentObserver(observer);
}
}
private static final FontProviderHelper DEFAULT_FONTS_CONTRACT = new FontProviderHelper();
}

@ -0,0 +1,348 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.content.res.AssetManager;
import androidx.annotation.AnyThread;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.emoji2.text.flatbuffer.MetadataList;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Reads the emoji metadata from a given InputStream or ByteBuffer.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@AnyThread
@RequiresApi(19)
class MetadataListReader {
/**
* Meta tag for emoji metadata. This string is used by the font update script to insert the
* emoji meta into the font. This meta table contains the list of all emojis which are stored in
* binary format using FlatBuffers. This flat list is later converted by the system into a trie.
* {@code int} representation for "Emji"
*
* @see MetadataRepo
*/
private static final int EMJI_TAG = 'E' << 24 | 'm' << 16 | 'j' << 8 | 'i';
/**
* Deprecated meta tag name. Do not use, kept for compatibility reasons, will be removed soon.
*/
private static final int EMJI_TAG_DEPRECATED = 'e' << 24 | 'm' << 16 | 'j' << 8 | 'i';
/**
* The name of the meta table in the font. int representation for "meta"
*/
private static final int META_TABLE_NAME = 'm' << 24 | 'e' << 16 | 't' << 8 | 'a';
/**
* Construct MetadataList from an input stream. Does not close the given InputStream, therefore
* it is caller's responsibility to properly close the stream.
*
* @param inputStream InputStream to read emoji metadata from
*/
static MetadataList read(InputStream inputStream) throws IOException {
final OpenTypeReader openTypeReader = new InputStreamOpenTypeReader(inputStream);
final OffsetInfo offsetInfo = findOffsetInfo(openTypeReader);
// skip to where metadata is
openTypeReader.skip((int) (offsetInfo.getStartOffset() - openTypeReader.getPosition()));
// allocate a ByteBuffer and read into it since FlatBuffers can read only from a ByteBuffer
final ByteBuffer buffer = ByteBuffer.allocate((int) offsetInfo.getLength());
final int numRead = inputStream.read(buffer.array());
if (numRead != offsetInfo.getLength()) {
throw new IOException("Needed " + offsetInfo.getLength() + " bytes, got " + numRead);
}
return MetadataList.getRootAsMetadataList(buffer);
}
/**
* Construct MetadataList from a byte buffer.
*
* @param byteBuffer ByteBuffer to read emoji metadata from
*/
static MetadataList read(final ByteBuffer byteBuffer) throws IOException {
final ByteBuffer newBuffer = byteBuffer.duplicate();
final OpenTypeReader reader = new ByteBufferReader(newBuffer);
final OffsetInfo offsetInfo = findOffsetInfo(reader);
// skip to where metadata is
newBuffer.position((int) offsetInfo.getStartOffset());
return MetadataList.getRootAsMetadataList(newBuffer);
}
/**
* Construct MetadataList from an asset.
*
* @param assetManager AssetManager instance
* @param assetPath asset manager path of the file that the Typeface and metadata will be
* created from
*/
static MetadataList read(AssetManager assetManager, String assetPath)
throws IOException {
try (InputStream inputStream = assetManager.open(assetPath)) {
return read(inputStream);
}
}
/**
* Finds the start offset and length of the emoji metadata in the font.
*
* @return OffsetInfo which contains start offset and length of the emoji metadata in the font
*
* @throws IOException
*/
private static OffsetInfo findOffsetInfo(OpenTypeReader reader) throws IOException {
// skip sfnt version
reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
// start of Table Count
final int tableCount = reader.readUnsignedShort();
if (tableCount > 100) {
//something is wrong quit
throw new IOException("Cannot read metadata.");
}
//skip to beginning of tables data
reader.skip(OpenTypeReader.UINT16_BYTE_COUNT * 3);
long metaOffset = -1;
for (int i = 0; i < tableCount; i++) {
final int tag = reader.readTag();
// skip checksum
reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
final long offset = reader.readUnsignedInt();
// skip mLength
reader.skip(OpenTypeReader.UINT32_BYTE_COUNT);
if (META_TABLE_NAME == tag) {
metaOffset = offset;
break;
}
}
if (metaOffset != -1) {
// skip to the beginning of meta tables.
reader.skip((int) (metaOffset - reader.getPosition()));
// skip minorVersion, majorVersion, flags, reserved,
reader.skip(
OpenTypeReader.UINT16_BYTE_COUNT * 2 + OpenTypeReader.UINT32_BYTE_COUNT * 2);
final long mapsCount = reader.readUnsignedInt();
for (int i = 0; i < mapsCount; i++) {
final int tag = reader.readTag();
final long dataOffset = reader.readUnsignedInt();
final long dataLength = reader.readUnsignedInt();
if (EMJI_TAG == tag || EMJI_TAG_DEPRECATED == tag) {
return new OffsetInfo(dataOffset + metaOffset, dataLength);
}
}
}
throw new IOException("Cannot read metadata.");
}
/**
* Start offset and length of the emoji metadata in the font.
*/
private static class OffsetInfo {
private final long mStartOffset;
private final long mLength;
OffsetInfo(long startOffset, long length) {
mStartOffset = startOffset;
mLength = length;
}
long getStartOffset() {
return mStartOffset;
}
long getLength() {
return mLength;
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static int toUnsignedShort(final short value) {
return value & 0xFFFF;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static long toUnsignedInt(final int value) {
return value & 0xFFFFFFFFL;
}
private interface OpenTypeReader {
int UINT16_BYTE_COUNT = 2;
int UINT32_BYTE_COUNT = 4;
/**
* Reads an {@code OpenType uint16}.
*
* @throws IOException
*/
int readUnsignedShort() throws IOException;
/**
* Reads an {@code OpenType uint32}.
*
* @throws IOException
*/
long readUnsignedInt() throws IOException;
/**
* Reads an {@code OpenType Tag}.
*
* @throws IOException
*/
int readTag() throws IOException;
/**
* Skip the given amount of numOfBytes
*
* @throws IOException
*/
void skip(int numOfBytes) throws IOException;
/**
* @return the position of the reader
*/
long getPosition();
}
/**
* Reads {@code OpenType} data from an {@link InputStream}.
*/
private static class InputStreamOpenTypeReader implements OpenTypeReader {
private final @NonNull byte[] mByteArray;
private final @NonNull ByteBuffer mByteBuffer;
private final @NonNull InputStream mInputStream;
private long mPosition = 0;
/**
* Constructs the reader with the given InputStream. Does not close the InputStream, it is
* caller's responsibility to close it.
*
* @param inputStream InputStream to read from
*/
InputStreamOpenTypeReader(@NonNull final InputStream inputStream) {
mInputStream = inputStream;
mByteArray = new byte[UINT32_BYTE_COUNT];
mByteBuffer = ByteBuffer.wrap(mByteArray);
mByteBuffer.order(ByteOrder.BIG_ENDIAN);
}
@Override
public int readUnsignedShort() throws IOException {
mByteBuffer.position(0);
read(UINT16_BYTE_COUNT);
return toUnsignedShort(mByteBuffer.getShort());
}
@Override
public long readUnsignedInt() throws IOException {
mByteBuffer.position(0);
read(UINT32_BYTE_COUNT);
return toUnsignedInt(mByteBuffer.getInt());
}
@Override
public int readTag() throws IOException {
mByteBuffer.position(0);
read(UINT32_BYTE_COUNT);
return mByteBuffer.getInt();
}
@Override
public void skip(int numOfBytes) throws IOException {
while (numOfBytes > 0) {
int skipped = (int) mInputStream.skip(numOfBytes);
if (skipped < 1) {
throw new IOException("Skip didn't move at least 1 byte forward");
}
numOfBytes -= skipped;
mPosition += skipped;
}
}
@Override
public long getPosition() {
return mPosition;
}
private void read(@IntRange(from = 0, to = UINT32_BYTE_COUNT) final int numOfBytes)
throws IOException {
if (mInputStream.read(mByteArray, 0, numOfBytes) != numOfBytes) {
throw new IOException("read failed");
}
mPosition += numOfBytes;
}
}
/**
* Reads OpenType data from a ByteBuffer.
*/
private static class ByteBufferReader implements OpenTypeReader {
private final @NonNull ByteBuffer mByteBuffer;
/**
* Constructs the reader with the given ByteBuffer.
*
* @param byteBuffer ByteBuffer to read from
*/
ByteBufferReader(@NonNull final ByteBuffer byteBuffer) {
mByteBuffer = byteBuffer;
mByteBuffer.order(ByteOrder.BIG_ENDIAN);
}
@Override
public int readUnsignedShort() throws IOException {
return toUnsignedShort(mByteBuffer.getShort());
}
@Override
public long readUnsignedInt() throws IOException {
return toUnsignedInt(mByteBuffer.getInt());
}
@Override
public int readTag() throws IOException {
return mByteBuffer.getInt();
}
@Override
public void skip(final int numOfBytes) throws IOException {
mByteBuffer.position(mByteBuffer.position() + numOfBytes);
}
@Override
public long getPosition() {
return mByteBuffer.position();
}
}
private MetadataListReader() {
}
}

@ -0,0 +1,275 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.content.res.AssetManager;
import android.graphics.Typeface;
import android.util.SparseArray;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.os.TraceCompat;
import androidx.core.util.Preconditions;
import androidx.emoji2.text.flatbuffer.MetadataList;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* Class to hold the emoji metadata required to process and draw emojis.
*/
@AnyThread
@RequiresApi(19)
public final class MetadataRepo {
/**
* The default children size of the root node.
*/
private static final int DEFAULT_ROOT_SIZE = 1024;
private static final String S_TRACE_CREATE_REPO = "EmojiCompat.MetadataRepo.create";
/**
* MetadataList that contains the emoji metadata.
*/
private final @NonNull MetadataList mMetadataList;
/**
* char presentation of all TypefaceEmojiRasterizer's in a single array. All emojis we have are
* mapped to Private Use Area A, in the range U+F0000..U+FFFFD. Therefore each emoji takes 2
* chars.
*/
private final @NonNull char[] mEmojiCharArray;
/**
* Empty root node of the trie.
*/
private final @NonNull Node mRootNode;
/**
* Typeface to be used to render emojis.
*/
private final @NonNull Typeface mTypeface;
/**
* Private constructor that is called by one of {@code create} methods.
*
* @param typeface Typeface to be used to render emojis
* @param metadataList MetadataList that contains the emoji metadata
*/
private MetadataRepo(@NonNull final Typeface typeface,
@NonNull final MetadataList metadataList) {
mTypeface = typeface;
mMetadataList = metadataList;
mRootNode = new Node(DEFAULT_ROOT_SIZE);
mEmojiCharArray = new char[mMetadataList.listLength() * 2];
constructIndex(mMetadataList);
}
/**
* Construct MetadataRepo with empty metadata.
*
* This should only be used from tests.
* @hide
*/
@NonNull
@RestrictTo(RestrictTo.Scope.TESTS)
public static MetadataRepo create(@NonNull final Typeface typeface) {
try {
TraceCompat.beginSection(S_TRACE_CREATE_REPO);
return new MetadataRepo(typeface, new MetadataList());
} finally {
TraceCompat.endSection();
}
}
/**
* Construct MetadataRepo from an input stream. The library does not close the given
* InputStream, therefore it is caller's responsibility to properly close the stream.
*
* @param typeface Typeface to be used to render emojis
* @param inputStream InputStream to read emoji metadata from
*/
@NonNull
public static MetadataRepo create(@NonNull final Typeface typeface,
@NonNull final InputStream inputStream) throws IOException {
try {
TraceCompat.beginSection(S_TRACE_CREATE_REPO);
return new MetadataRepo(typeface, MetadataListReader.read(inputStream));
} finally {
TraceCompat.endSection();
}
}
/**
* Construct MetadataRepo from a byte buffer. The position of the ByteBuffer will change, it is
* caller's responsibility to reposition the buffer if required.
*
* @param typeface Typeface to be used to render emojis
* @param byteBuffer ByteBuffer to read emoji metadata from
*/
@NonNull
public static MetadataRepo create(@NonNull final Typeface typeface,
@NonNull final ByteBuffer byteBuffer) throws IOException {
try {
TraceCompat.beginSection(S_TRACE_CREATE_REPO);
return new MetadataRepo(typeface, MetadataListReader.read(byteBuffer));
} finally {
TraceCompat.endSection();
}
}
/**
* Construct MetadataRepo from an asset.
*
* @param assetManager AssetManager instance
* @param assetPath asset manager path of the file that the Typeface and metadata will be
* created from
*/
@NonNull
public static MetadataRepo create(@NonNull final AssetManager assetManager,
@NonNull final String assetPath) throws IOException {
try {
TraceCompat.beginSection(S_TRACE_CREATE_REPO);
final Typeface typeface = Typeface.createFromAsset(assetManager, assetPath);
return new MetadataRepo(typeface,
MetadataListReader.read(assetManager, assetPath));
} finally {
TraceCompat.endSection();
}
}
/**
* Read emoji metadata list and construct the trie.
*/
private void constructIndex(final MetadataList metadataList) {
int length = metadataList.listLength();
for (int i = 0; i < length; i++) {
final TypefaceEmojiRasterizer metadata = new TypefaceEmojiRasterizer(this, i);
//since all emojis are mapped to a single codepoint in Private Use Area A they are 2
//chars wide
//noinspection ResultOfMethodCallIgnored
Character.toChars(metadata.getId(), mEmojiCharArray, i * 2);
put(metadata);
}
}
/**
* @hide
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
Typeface getTypeface() {
return mTypeface;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
int getMetadataVersion() {
return mMetadataList.version();
}
/**
* @hide
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
Node getRootNode() {
return mRootNode;
}
/**
* @hide
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
public char[] getEmojiCharArray() {
return mEmojiCharArray;
}
/**
* @hide
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
public MetadataList getMetadataList() {
return mMetadataList;
}
/**
* Add a TypefaceEmojiRasterizer to the index.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@VisibleForTesting
void put(@NonNull final TypefaceEmojiRasterizer data) {
Preconditions.checkNotNull(data, "emoji metadata cannot be null");
Preconditions.checkArgument(data.getCodepointsLength() > 0,
"invalid metadata codepoint length");
mRootNode.put(data, 0, data.getCodepointsLength() - 1);
}
/**
* Trie node that holds mapping from emoji codepoint(s) to TypefaceEmojiRasterizer.
*
* A single codepoint emoji is represented by a child of the root node.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
static class Node {
private final SparseArray<Node> mChildren;
private TypefaceEmojiRasterizer mData;
private Node() {
this(1);
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
Node(final int defaultChildrenSize) {
mChildren = new SparseArray<>(defaultChildrenSize);
}
Node get(final int key) {
return mChildren == null ? null : mChildren.get(key);
}
final TypefaceEmojiRasterizer getData() {
return mData;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void put(@NonNull final TypefaceEmojiRasterizer data, final int start, final int end) {
Node node = get(data.getCodepointAt(start));
if (node == null) {
node = new Node();
mChildren.put(data.getCodepointAt(start), node);
}
if (end > start) {
node.put(data, start + 1, end);
} else {
node.mData = data;
}
}
}
}

@ -0,0 +1,462 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.Editable;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* When setSpan functions is called on EmojiSpannableBuilder, it checks if the mObject is instance
* of the DynamicLayout$ChangeWatcher. if so, it wraps it into another listener mObject
* (WatcherWrapper) that implements the same interfaces.
* <p>
* During a span change event WatcherWrappers functions are fired, it checks if the span is an
* EmojiSpan, and prevents the ChangeWatcher being fired for that span. WatcherWrapper informs
* ChangeWatcher only once at the end of the edit. Important point is, the block operation is
* applied only for EmojiSpans. Therefore any other span change operation works the same way as in
* the framework.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SpannableBuilder extends SpannableStringBuilder {
/**
* DynamicLayout$ChangeWatcher class.
*/
private final @NonNull Class<?> mWatcherClass;
/**
* All WatcherWrappers.
*/
private final @NonNull List<WatcherWrapper> mWatchers = new ArrayList<>();
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
SpannableBuilder(@NonNull Class<?> watcherClass) {
Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
mWatcherClass = watcherClass;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text) {
super(text);
Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
mWatcherClass = watcherClass;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text, int start,
int end) {
super(text, start, end);
Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
mWatcherClass = watcherClass;
}
/**
* @hide
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static SpannableBuilder create(@NonNull Class<?> clazz, @NonNull CharSequence text) {
return new SpannableBuilder(clazz, text);
}
/**
* Checks whether the mObject is instance of the DynamicLayout$ChangeWatcher.
*
* @param object mObject to be checked
*
* @return true if mObject is instance of the DynamicLayout$ChangeWatcher.
*/
private boolean isWatcher(@Nullable Object object) {
return object != null && isWatcher(object.getClass());
}
/**
* Checks whether the class is DynamicLayout$ChangeWatcher.
*
* @param clazz class to be checked
*
* @return true if class is DynamicLayout$ChangeWatcher.
*/
private boolean isWatcher(@NonNull Class<?> clazz) {
return mWatcherClass == clazz;
}
@SuppressLint("UnknownNullness")
@Override
public CharSequence subSequence(int start, int end) {
return new SpannableBuilder(mWatcherClass, this, start, end);
}
/**
* If the span being added is instance of DynamicLayout$ChangeWatcher, wrap the watcher in
* another internal watcher that will prevent EmojiSpan events to be fired to DynamicLayout. Set
* this new mObject as the span.
*/
@Override
public void setSpan(@Nullable Object what, int start, int end, int flags) {
if (isWatcher(what)) {
final WatcherWrapper span = new WatcherWrapper(what);
mWatchers.add(span);
what = span;
}
super.setSpan(what, start, end, flags);
}
/**
* If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the
* correct Object that the client has set.
*/
@SuppressLint("UnknownNullness")
@SuppressWarnings("unchecked")
@Override
public <T> T[] getSpans(int queryStart, int queryEnd, @NonNull Class<T> kind) {
if (isWatcher(kind)) {
final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd,
WatcherWrapper.class);
final T[] result = (T[]) Array.newInstance(kind, spans.length);
for (int i = 0; i < spans.length; i++) {
result[i] = (T) spans[i].mObject;
}
return result;
}
return super.getSpans(queryStart, queryEnd, kind);
}
/**
* If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper
* instead.
*/
@Override
public void removeSpan(@Nullable Object what) {
final WatcherWrapper watcher;
if (isWatcher(what)) {
watcher = getWatcherFor(what);
if (watcher != null) {
what = watcher;
}
} else {
watcher = null;
}
super.removeSpan(what);
if (watcher != null) {
mWatchers.remove(watcher);
}
}
/**
* Return the correct start for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int getSpanStart(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
tag = watcher;
}
}
return super.getSpanStart(tag);
}
/**
* Return the correct end for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int getSpanEnd(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
tag = watcher;
}
}
return super.getSpanEnd(tag);
}
/**
* Return the correct flags for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int getSpanFlags(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
tag = watcher;
}
}
return super.getSpanFlags(tag);
}
/**
* Return the correct transition for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int nextSpanTransition(int start, int limit, @Nullable Class type) {
if (type == null || isWatcher(type)) {
type = WatcherWrapper.class;
}
return super.nextSpanTransition(start, limit, type);
}
/**
* Find the WatcherWrapper for a given DynamicLayout$ChangeWatcher.
*
* @param object DynamicLayout$ChangeWatcher mObject
*
* @return WatcherWrapper that wraps the mObject.
*/
private WatcherWrapper getWatcherFor(Object object) {
for (int i = 0; i < mWatchers.size(); i++) {
WatcherWrapper watcher = mWatchers.get(i);
if (watcher.mObject == object) {
return watcher;
}
}
return null;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void beginBatchEdit() {
blockWatchers();
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void endBatchEdit() {
unblockwatchers();
fireWatchers();
}
/**
* Block all watcher wrapper events.
*/
private void blockWatchers() {
for (int i = 0; i < mWatchers.size(); i++) {
mWatchers.get(i).blockCalls();
}
}
/**
* Unblock all watcher wrapper events.
*/
private void unblockwatchers() {
for (int i = 0; i < mWatchers.size(); i++) {
mWatchers.get(i).unblockCalls();
}
}
/**
* Unblock all watcher wrapper events. Called by editing operations, namely
* {@link SpannableStringBuilder#replace(int, int, CharSequence)}.
*/
private void fireWatchers() {
for (int i = 0; i < mWatchers.size(); i++) {
mWatchers.get(i).onTextChanged(this, 0, this.length(), this.length());
}
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
blockWatchers();
super.replace(start, end, tb);
unblockwatchers();
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
int tbend) {
blockWatchers();
super.replace(start, end, tb, tbstart, tbend);
unblockwatchers();
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder insert(int where, CharSequence tb) {
super.insert(where, tb);
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
super.insert(where, tb, start, end);
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder delete(int start, int end) {
super.delete(start, end);
return this;
}
@NonNull
@Override
public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text) {
super.append(text);
return this;
}
@NonNull
@Override
public SpannableStringBuilder append(char text) {
super.append(text);
return this;
}
@NonNull
@Override
public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text,
int start,
int end) {
super.append(text, start, end);
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder append(CharSequence text, Object what, int flags) {
super.append(text, what, flags);
return this;
}
/**
* Wraps a DynamicLayout$ChangeWatcher in order to prevent firing of events to DynamicLayout.
*/
private static class WatcherWrapper implements TextWatcher, SpanWatcher {
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Object mObject;
private final AtomicInteger mBlockCalls = new AtomicInteger(0);
WatcherWrapper(Object object) {
this.mObject = object;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
((TextWatcher) mObject).beforeTextChanged(s, start, count, after);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
((TextWatcher) mObject).onTextChanged(s, start, before, count);
}
@Override
public void afterTextChanged(Editable s) {
((TextWatcher) mObject).afterTextChanged(s);
}
/**
* Prevent the onSpanAdded calls to DynamicLayout$ChangeWatcher if in a replace operation
* (mBlockCalls is set) and the span that is added is an EmojiSpan.
*/
@Override
public void onSpanAdded(Spannable text, Object what, int start, int end) {
if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
return;
}
((SpanWatcher) mObject).onSpanAdded(text, what, start, end);
}
/**
* Prevent the onSpanRemoved calls to DynamicLayout$ChangeWatcher if in a replace operation
* (mBlockCalls is set) and the span that is added is an EmojiSpan.
*/
@Override
public void onSpanRemoved(Spannable text, Object what, int start, int end) {
if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
return;
}
((SpanWatcher) mObject).onSpanRemoved(text, what, start, end);
}
/**
* Prevent the onSpanChanged calls to DynamicLayout$ChangeWatcher if in a replace operation
* (mBlockCalls is set) and the span that is added is an EmojiSpan.
*/
@Override
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart,
int nend) {
if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
return;
}
// workaround for platform bug fixed in Android P
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// b/67926915 start cannot be determined, fallback to reflow from start instead
// of causing an exception.
// emoji2 bug b/216891011
if (ostart > oend) {
ostart = 0;
}
if (nstart > nend) {
nstart = 0;
}
}
((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend);
}
final void blockCalls() {
mBlockCalls.incrementAndGet();
}
final void unblockCalls() {
mBlockCalls.decrementAndGet();
}
private boolean isEmojiSpan(final Object span) {
return span instanceof EmojiSpan;
}
}
}

@ -0,0 +1,331 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.annotation.RestrictTo.Scope.TESTS;
import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import androidx.annotation.AnyThread;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.emoji2.text.flatbuffer.MetadataItem;
import androidx.emoji2.text.flatbuffer.MetadataList;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Information about a single emoji.
*
* To draw this emoji on a canvas using use draw.
*
* To draw this emoji using custom code, use getCodepointAt and getTypeface to access the
* underlying emoji font and look up the glyph.
*
* @see TypefaceEmojiRasterizer#draw
* @see TypefaceEmojiRasterizer#getCodepointAt
* @see TypefaceEmojiRasterizer#getTypeface
*
*/
@AnyThread
@RequiresApi(19)
public class TypefaceEmojiRasterizer {
/**
* Defines whether the system can render the emoji.
* @hide
*/
@IntDef({HAS_GLYPH_UNKNOWN, HAS_GLYPH_ABSENT, HAS_GLYPH_EXISTS})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(LIBRARY_GROUP)
public @interface HasGlyph {
}
/**
* Not calculated on device yet.
* @hide
*/
@RestrictTo(LIBRARY)
static final int HAS_GLYPH_UNKNOWN = 0;
/**
* Device cannot render the emoji.
* @hide
*/
@RestrictTo(LIBRARY)
static final int HAS_GLYPH_ABSENT = 1;
/**
* Device can render the emoji.
* @hide
*/
@RestrictTo(LIBRARY)
static final int HAS_GLYPH_EXISTS = 2;
/**
* @see #getMetadataItem()
*/
private static final ThreadLocal<MetadataItem> sMetadataItem = new ThreadLocal<>();
/**
* Index of the TypefaceEmojiRasterizer in {@link MetadataList}.
*/
private final int mIndex;
/**
* MetadataRepo that holds this instance.
*/
@NonNull
private final MetadataRepo mMetadataRepo;
/**
* Stores hasGlyph as well as exclusion values
*
* mCache & 0b0011 is hasGlyph result
* mCache & 0b0100 is exclusion value
*/
private volatile int mCache = 0;
/**
* @hide
*/
@RestrictTo(LIBRARY)
TypefaceEmojiRasterizer(@NonNull final MetadataRepo metadataRepo,
@IntRange(from = 0) final int index) {
mMetadataRepo = metadataRepo;
mIndex = index;
}
/**
* Draws the emoji onto a canvas with origin at (x,y), using the specified paint.
*
* @param canvas Canvas to be drawn
* @param x x-coordinate of the origin of the emoji being drawn
* @param y y-coordinate of the baseline of the emoji being drawn
* @param paint Paint used for the text (e.g. color, size, style)
*/
public void draw(@NonNull final Canvas canvas, final float x, final float y,
@NonNull final Paint paint) {
final Typeface typeface = mMetadataRepo.getTypeface();
final Typeface oldTypeface = paint.getTypeface();
paint.setTypeface(typeface);
// MetadataRepo.getEmojiCharArray() is a continuous array of chars that is used to store the
// chars for emojis. since all emojis are mapped to a single codepoint, and since it is 2
// chars wide, we assume that the start index of the current emoji is mIndex * 2, and it is
// 2 chars long.
final int charArrayStartIndex = mIndex * 2;
canvas.drawText(mMetadataRepo.getEmojiCharArray(), charArrayStartIndex, 2, x, y, paint);
paint.setTypeface(oldTypeface);
}
/**
* @return return typeface to be used to render
*/
@NonNull
public Typeface getTypeface() {
return mMetadataRepo.getTypeface();
}
/**
* @return a ThreadLocal instance of MetadataItem for this TypefaceEmojiRasterizer
*/
private MetadataItem getMetadataItem() {
MetadataItem result = sMetadataItem.get();
if (result == null) {
result = new MetadataItem();
sMetadataItem.set(result);
}
// MetadataList is a wrapper around the metadata ByteBuffer. MetadataItem is a wrapper with
// an index (pointer) on this ByteBuffer that represents a single emoji. Both are FlatBuffer
// classes that wraps a ByteBuffer and gives access to the information in it. In order not
// to create a wrapper class for each TypefaceEmojiRasterizer, we use mIndex as the index
// of the MetadataItem in the ByteBuffer. We need to reiniitalize the current thread
// local instance by executing the statement below. All the statement does is to set an
// int index in MetadataItem. the same instance is used by all TypefaceEmojiRasterizer
// classes in the same thread.
mMetadataRepo.getMetadataList().list(result, mIndex);
return result;
}
/**
* Unique id for the emoji, as loaded from the font file.
*
* @return unique id for the emoji
* @hide
*/
@RestrictTo(LIBRARY)
public int getId() {
return getMetadataItem().id();
}
/**
* @return width of the emoji image
*/
public int getWidth() {
return getMetadataItem().width();
}
/**
* @return height of the emoji image
*/
public int getHeight() {
return getMetadataItem().height();
}
/**
* @return in which metadata version the emoji was added
* @hide
*/
@RestrictTo(LIBRARY)
public short getCompatAdded() {
return getMetadataItem().compatAdded();
}
/**
* @return first SDK that the support for this emoji was added
* @hide
*/
@RestrictTo(LIBRARY)
public short getSdkAdded() {
return getMetadataItem().sdkAdded();
}
/**
* Returns the value set by setHasGlyph
*
* This is intended to be used as a cache on this emoji to avoid repeatedly calling
* PaintCompat#hasGlyph on the same codepoint sequence, which is expensive.
*
* @see TypefaceEmojiRasterizer#setHasGlyph
* @return the set value of hasGlyph for this metadata item
* @hide
*/
@HasGlyph
@SuppressLint("KotlinPropertyAccess")
@RestrictTo(LIBRARY)
public int getHasGlyph() {
return (int) (mCache & 0b0011);
}
/**
* Reset any cached values of hasGlyph on this metadata.
*
* This is only useful for testing, and will make the next display of this emoji slower.
*
* @hide
*/
@RestrictTo(TESTS)
public void resetHasGlyphCache() {
boolean willExclude = isPreferredSystemRender();
if (willExclude) {
mCache = 0b0100;
} else {
mCache = 0b0000;
}
}
/**
* Set whether the system can render the emoji.
*
* @see PaintCompat#hasGlyph
* @param hasGlyph {@code true} if system can render the emoji
* @hide
*/
@SuppressLint("KotlinPropertyAccess")
@RestrictTo(LIBRARY)
public void setHasGlyph(boolean hasGlyph) {
int newValue = mCache & 0b0100; /* keep the exclusion bit */
if (hasGlyph) {
newValue |= 0b0010;
} else {
newValue |= 0b0001;
}
mCache = newValue;
}
/**
* If this emoji is excluded due to CodepointExclusions.getExcludedCodpoints()
*
* @param exclude if the emoji should never be rendered by emojicompat
* @hide
*/
@RestrictTo(LIBRARY)
public void setExclusion(boolean exclude) {
int hasGlyphBits = getHasGlyph();
if (exclude) {
mCache = hasGlyphBits | 0b0100;
} else {
mCache = hasGlyphBits;
}
}
/**
* If the platform requested that this emoji not be rendered using emojicompat.
*
* @return true if this emoji should be drawn by the system instead of this renderer
*/
public boolean isPreferredSystemRender() {
return (mCache & 0b0100) > 0;
}
/**
* @return whether the emoji is in Emoji Presentation by default (without emoji
* style selector 0xFE0F)
*/
public boolean isDefaultEmoji() {
return getMetadataItem().emojiStyle();
}
/**
* @param index index of the codepoint
*
* @return the codepoint at index
*/
public int getCodepointAt(int index) {
return getMetadataItem().codepoints(index);
}
/**
* @return the length of the codepoints for this emoji
*/
public int getCodepointsLength() {
return getMetadataItem().codepointsLength();
}
@NonNull
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append(super.toString());
builder.append(", id:");
builder.append(Integer.toHexString(getId()));
builder.append(", codepoints:");
final int codepointsLength = getCodepointsLength();
for (int i = 0; i < codepointsLength; i++) {
builder.append(Integer.toHexString(getCodepointAt(i)));
builder.append(" ");
}
return builder.toString();
}
}

@ -0,0 +1,150 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.annotation.SuppressLint;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
/**
* EmojiSpan subclass used to render emojis using Typeface.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(19)
public final class TypefaceEmojiSpan extends EmojiSpan {
/**
* Paint object used to draw a background in debug mode.
*/
private static @Nullable Paint sDebugPaint;
@Nullable
private TextPaint mWorkingPaint;
/**
* Default constructor.
*
* @param metadata metadata representing the emoji that this span will draw
*/
public TypefaceEmojiSpan(final @NonNull TypefaceEmojiRasterizer metadata) {
super(metadata);
}
@Override
public void draw(@NonNull final Canvas canvas,
@SuppressLint("UnknownNullness") final CharSequence text,
@IntRange(from = 0) final int start, @IntRange(from = 0) final int end, final float x,
final int top, final int y, final int bottom, @NonNull final Paint paint) {
@Nullable TextPaint textPaint = applyCharacterSpanStyles(text, start, end, paint);
if (textPaint != null && textPaint.bgColor != 0) {
drawBackground(canvas, textPaint, x, x + getWidth(), top, bottom);
}
if (EmojiCompat.get().isEmojiSpanIndicatorEnabled()) {
canvas.drawRect(x, top , x + getWidth(), bottom, getDebugPaint());
}
getTypefaceRasterizer().draw(canvas, x, y, textPaint != null ? textPaint : paint);
}
// compat behavior with TextLine.java#handleText background drawing
void drawBackground(Canvas c, TextPaint textPaint, float leftX, float rightX, float top,
float bottom) {
int previousColor = textPaint.getColor();
Paint.Style previousStyle = textPaint.getStyle();
textPaint.setColor(textPaint.bgColor);
textPaint.setStyle(Paint.Style.FILL);
c.drawRect(leftX, top, rightX, bottom, textPaint);
textPaint.setStyle(previousStyle);
textPaint.setColor(previousColor);
}
/**
* This applies the CharacterSpanStyles that _would_ have been applied to this character by
* StaticLayout.
*
* StaticLayout applies CharacterSpanStyles _after_ calling ReplacementSpan.draw, which means
* BackgroundSpan will not be applied before draw is called.
*
* If any CharacterSpanStyles would impact _this_ location, apply them to a TextPaint to
* determine if a background needs draw prior to the emoji.
*
* @param text text that this span is part of
* @param start start position to replace
* @param end end position to replace
* @param paint paint (from TextLine)
* @return TextPaint configured
*/
@Nullable
private TextPaint applyCharacterSpanStyles(@Nullable CharSequence text, int start, int end,
Paint paint) {
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
CharacterStyle[] spans = spanned.getSpans(start, end, CharacterStyle.class);
if (spans.length == 0 || (spans.length == 1 && spans[0] == this)) {
if (paint instanceof TextPaint) {
// happy path goes here, retain color and bgColor from caller
return (TextPaint) paint;
} else {
return null;
}
}
// there are some non-TypefaceEmojiSpan character styles to apply, update a working
// paint to apply each span style, like TextLine would have.
TextPaint wp = mWorkingPaint;
if (wp == null) {
wp = new TextPaint();
mWorkingPaint = wp;
}
wp.set(paint);
//noinspection ForLoopReplaceableByForEach
for (int pos = 0; pos < spans.length; pos++) {
spans[pos].updateDrawState(wp);
}
return wp;
} else {
if (paint instanceof TextPaint) {
// retain any color and bgColor from caller
return (TextPaint) paint;
} else {
return null;
}
}
}
@NonNull
private static Paint getDebugPaint() {
if (sDebugPaint == null) {
sDebugPaint = new TextPaint();
sDebugPaint.setColor(EmojiCompat.get().getEmojiSpanIndicatorColor());
sDebugPaint.setStyle(Paint.Style.FILL);
}
return sDebugPaint;
}
}

@ -0,0 +1,182 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.os.Build;
import android.text.PrecomputedText;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.text.PrecomputedTextCompat;
import java.util.stream.IntStream;
/**
* Spannable that will delegate to a passed spannable for all query operations without allocation.
*
* If delegating to a PrecomputedText, the delegate will be swapped for a SpannableString prior
* to any modifications.
*/
class UnprecomputeTextOnModificationSpannable implements Spannable {
/**
* True when mDelegate is safe to write, otherwise mDelegate will need wrapped before mutation
*/
private boolean mSafeToWrite = false;
@NonNull
private Spannable mDelegate;
UnprecomputeTextOnModificationSpannable(@NonNull Spannable delegate) {
mDelegate = delegate;
}
UnprecomputeTextOnModificationSpannable(@NonNull Spanned delegate) {
mDelegate = new SpannableString(delegate);
}
UnprecomputeTextOnModificationSpannable(@NonNull CharSequence delegate) {
mDelegate = new SpannableString(delegate);
}
private void ensureSafeWrites() {
Spannable old = mDelegate;
if (!mSafeToWrite && precomputedTextDetector().isPrecomputedText(old)) {
mDelegate = new SpannableString(old);
}
mSafeToWrite = true;
}
Spannable getUnwrappedSpannable() {
return mDelegate;
}
@Override
public void setSpan(Object o, int i, int i1, int i2) {
ensureSafeWrites();
mDelegate.setSpan(o, i, i1, i2);
}
@Override
public void removeSpan(Object o) {
ensureSafeWrites();
mDelegate.removeSpan(o);
}
@Override
public <T> T[] getSpans(int i, int i1, Class<T> aClass) {
return mDelegate.getSpans(i, i1, aClass);
}
@Override
public int getSpanStart(Object o) {
return mDelegate.getSpanStart(o);
}
@Override
public int getSpanEnd(Object o) {
return mDelegate.getSpanEnd(o);
}
@Override
public int getSpanFlags(Object o) {
return mDelegate.getSpanFlags(o);
}
@Override
public int nextSpanTransition(int i, int i1, Class aClass) {
return mDelegate.nextSpanTransition(i, i1, aClass);
}
@Override
public int length() {
return mDelegate.length();
}
@Override
public char charAt(int i) {
return mDelegate.charAt(i);
}
@NonNull
@Override
public CharSequence subSequence(int i, int i1) {
return mDelegate.subSequence(i, i1);
}
@NonNull
@Override
public String toString() {
return mDelegate.toString();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@NonNull
@Override
public IntStream chars() {
return CharSequenceHelper_API24.chars(mDelegate);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@NonNull
@Override
public IntStream codePoints() {
return CharSequenceHelper_API24.codePoints(mDelegate);
}
@RequiresApi(24)
private static class CharSequenceHelper_API24 {
private CharSequenceHelper_API24() {
// not constructable
}
static IntStream codePoints(CharSequence charSequence) {
return charSequence.codePoints();
}
static IntStream chars(CharSequence charSequence) {
return charSequence.chars();
}
}
static PrecomputedTextDetector precomputedTextDetector() {
return (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
? new PrecomputedTextDetector() : new PrecomputedTextDetector_28();
}
static class PrecomputedTextDetector {
boolean isPrecomputedText(CharSequence text) {
return text instanceof PrecomputedTextCompat;
}
}
@RequiresApi(28)
static class PrecomputedTextDetector_28 extends PrecomputedTextDetector {
@Override
boolean isPrecomputedText(CharSequence text) {
return text instanceof PrecomputedText || text instanceof PrecomputedTextCompat;
}
}
}

@ -0,0 +1,79 @@
// CHECKSTYLE:OFF Generated code
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// automatically generated by the FlatBuffers compiler, do not modify
package androidx.emoji2.text.flatbuffer;
import java.nio.*;
import java.lang.*;
import java.util.*;
import com.google.flatbuffers.*;
@SuppressWarnings("unused")
public final class MetadataItem extends Table {
public static void ValidateVersion() { Constants.FLATBUFFERS_2_0_0(); }
public static MetadataItem getRootAsMetadataItem(ByteBuffer _bb) { return getRootAsMetadataItem(_bb, new MetadataItem()); }
public static MetadataItem getRootAsMetadataItem(ByteBuffer _bb, MetadataItem obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
public void __init(int _i, ByteBuffer _bb) { __reset(_i, _bb); }
public MetadataItem __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
public int id() { int o = __offset(4); return o != 0 ? bb.getInt(o + bb_pos) : 0; }
public boolean emojiStyle() { int o = __offset(6); return o != 0 ? 0!=bb.get(o + bb_pos) : false; }
public short sdkAdded() { int o = __offset(8); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
public short compatAdded() { int o = __offset(10); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
public short width() { int o = __offset(12); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
public short height() { int o = __offset(14); return o != 0 ? bb.getShort(o + bb_pos) : 0; }
public int codepoints(int j) { int o = __offset(16); return o != 0 ? bb.getInt(__vector(o) + j * 4) : 0; }
public int codepointsLength() { int o = __offset(16); return o != 0 ? __vector_len(o) : 0; }
public IntVector codepointsVector() { return codepointsVector(new IntVector()); }
public IntVector codepointsVector(IntVector obj) { int o = __offset(16); return o != 0 ? obj.__assign(__vector(o), bb) : null; }
public ByteBuffer codepointsAsByteBuffer() { return __vector_as_bytebuffer(16, 4); }
public ByteBuffer codepointsInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 16, 4); }
public static int createMetadataItem(FlatBufferBuilder builder,
int id,
boolean emojiStyle,
short sdkAdded,
short compatAdded,
short width,
short height,
int codepointsOffset) {
builder.startTable(7);
MetadataItem.addCodepoints(builder, codepointsOffset);
MetadataItem.addId(builder, id);
MetadataItem.addHeight(builder, height);
MetadataItem.addWidth(builder, width);
MetadataItem.addCompatAdded(builder, compatAdded);
MetadataItem.addSdkAdded(builder, sdkAdded);
MetadataItem.addEmojiStyle(builder, emojiStyle);
return MetadataItem.endMetadataItem(builder);
}
public static void startMetadataItem(FlatBufferBuilder builder) { builder.startTable(7); }
public static void addId(FlatBufferBuilder builder, int id) { builder.addInt(0, id, 0); }
public static void addEmojiStyle(FlatBufferBuilder builder, boolean emojiStyle) { builder.addBoolean(1, emojiStyle, false); }
public static void addSdkAdded(FlatBufferBuilder builder, short sdkAdded) { builder.addShort(2, sdkAdded, 0); }
public static void addCompatAdded(FlatBufferBuilder builder, short compatAdded) { builder.addShort(3, compatAdded, 0); }
public static void addWidth(FlatBufferBuilder builder, short width) { builder.addShort(4, width, 0); }
public static void addHeight(FlatBufferBuilder builder, short height) { builder.addShort(5, height, 0); }
public static void addCodepoints(FlatBufferBuilder builder, int codepointsOffset) { builder.addOffset(6, codepointsOffset, 0); }
public static int createCodepointsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addInt(data[i]); return builder.endVector(); }
public static void startCodepointsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
public static int endMetadataItem(FlatBufferBuilder builder) {
int o = builder.endTable();
return o;
}
public static final class Vector extends BaseVector {
public Vector __assign(int _vector, int _element_size, ByteBuffer _bb) { __reset(_vector, _element_size, _bb); return this; }
public MetadataItem get(int j) { return get(new MetadataItem(), j); }
public MetadataItem get(MetadataItem obj, int j) { return obj.__assign(__indirect(__element(j), bb), bb); }
}
}

@ -0,0 +1,66 @@
// CHECKSTYLE:OFF Generated code
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// automatically generated by the FlatBuffers compiler, do not modify
package androidx.emoji2.text.flatbuffer;
import java.nio.*;
import java.lang.*;
import java.util.*;
import com.google.flatbuffers.*;
@SuppressWarnings("unused")
public final class MetadataList extends Table {
public static void ValidateVersion() { Constants.FLATBUFFERS_2_0_0(); }
public static MetadataList getRootAsMetadataList(ByteBuffer _bb) { return getRootAsMetadataList(_bb, new MetadataList()); }
public static MetadataList getRootAsMetadataList(ByteBuffer _bb, MetadataList obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
public void __init(int _i, ByteBuffer _bb) { __reset(_i, _bb); }
public MetadataList __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
public int version() { int o = __offset(4); return o != 0 ? bb.getInt(o + bb_pos) : 0; }
public androidx.emoji2.text.flatbuffer.MetadataItem list(int j) { return list(new androidx.emoji2.text.flatbuffer.MetadataItem(), j); }
public androidx.emoji2.text.flatbuffer.MetadataItem list(androidx.emoji2.text.flatbuffer.MetadataItem obj, int j) { int o = __offset(6); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
public int listLength() { int o = __offset(6); return o != 0 ? __vector_len(o) : 0; }
public androidx.emoji2.text.flatbuffer.MetadataItem.Vector listVector() { return listVector(new androidx.emoji2.text.flatbuffer.MetadataItem.Vector()); }
public androidx.emoji2.text.flatbuffer.MetadataItem.Vector listVector(androidx.emoji2.text.flatbuffer.MetadataItem.Vector obj) { int o = __offset(6); return o != 0 ? obj.__assign(__vector(o), 4, bb) : null; }
public String sourceSha() { int o = __offset(8); return o != 0 ? __string(o + bb_pos) : null; }
public ByteBuffer sourceShaAsByteBuffer() { return __vector_as_bytebuffer(8, 1); }
public ByteBuffer sourceShaInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 1); }
public static int createMetadataList(FlatBufferBuilder builder,
int version,
int listOffset,
int sourceShaOffset) {
builder.startTable(3);
MetadataList.addSourceSha(builder, sourceShaOffset);
MetadataList.addList(builder, listOffset);
MetadataList.addVersion(builder, version);
return MetadataList.endMetadataList(builder);
}
public static void startMetadataList(FlatBufferBuilder builder) { builder.startTable(3); }
public static void addVersion(FlatBufferBuilder builder, int version) { builder.addInt(0, version, 0); }
public static void addList(FlatBufferBuilder builder, int listOffset) { builder.addOffset(1, listOffset, 0); }
public static int createListVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
public static void startListVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
public static void addSourceSha(FlatBufferBuilder builder, int sourceShaOffset) { builder.addOffset(2, sourceShaOffset, 0); }
public static int endMetadataList(FlatBufferBuilder builder) {
int o = builder.endTable();
return o;
}
public static void finishMetadataListBuffer(FlatBufferBuilder builder, int offset) { builder.finish(offset); }
public static void finishSizePrefixedMetadataListBuffer(FlatBufferBuilder builder, int offset) { builder.finishSizePrefixed(offset); }
public static final class Vector extends BaseVector {
public Vector __assign(int _vector, int _element_size, ByteBuffer _bb) { __reset(_vector, _element_size, _bb); return this; }
public MetadataList get(int j) { return get(new MetadataList(), j); }
public MetadataList get(MetadataList obj, int j) { return obj.__assign(__indirect(__element(j), bb), bb); }
}
}
Loading…
Cancel
Save