mirror of https://github.com/M66B/FairEmail.git
parent
019cab732f
commit
670aaf1970
@ -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>
|
||||
* <provider
|
||||
* android:name="androidx.startup.InitializationProvider"
|
||||
* android:authorities="${applicationId}.androidx-startup"
|
||||
* android:exported="false"
|
||||
* tools:node="merge">
|
||||
* <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer"
|
||||
* tools:node="remove" />
|
||||
* </provider>
|
||||
* </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,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…
Reference in new issue