blob: fbbf4c89f553b6dd96c6ae872907ad573a5e5dbc [file] [log] [blame]
/*
* Copyright 2018 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.emoji.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
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.util.Preconditions;
import androidx.text.emoji.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;
/**
* MetadataList that contains the emoji metadata.
*/
private final MetadataList mMetadataList;
/**
* char presentation of all EmojiMetadata'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 char[] mEmojiCharArray;
/**
* Empty root node of the trie.
*/
private final Node mRootNode;
/**
* Typeface to be used to render emojis.
*/
private final Typeface mTypeface;
/**
* Constructor used for tests.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
MetadataRepo() {
mTypeface = null;
mMetadataList = null;
mRootNode = new Node(DEFAULT_ROOT_SIZE);
mEmojiCharArray = new char[0];
}
/**
* 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 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
*/
public static MetadataRepo create(@NonNull final Typeface typeface,
@NonNull final InputStream inputStream) throws IOException {
return new MetadataRepo(typeface, MetadataListReader.read(inputStream));
}
/**
* 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
*/
public static MetadataRepo create(@NonNull final Typeface typeface,
@NonNull final ByteBuffer byteBuffer) throws IOException {
return new MetadataRepo(typeface, MetadataListReader.read(byteBuffer));
}
/**
* 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
*/
public static MetadataRepo create(@NonNull final AssetManager assetManager,
final String assetPath) throws IOException {
final Typeface typeface = Typeface.createFromAsset(assetManager, assetPath);
return new MetadataRepo(typeface, MetadataListReader.read(assetManager, assetPath));
}
/**
* 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 EmojiMetadata metadata = new EmojiMetadata(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
*/
@RestrictTo(LIBRARY_GROUP)
Typeface getTypeface() {
return mTypeface;
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
int getMetadataVersion() {
return mMetadataList.version();
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
Node getRootNode() {
return mRootNode;
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public char[] getEmojiCharArray() {
return mEmojiCharArray;
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
public MetadataList getMetadataList() {
return mMetadataList;
}
/**
* Add an EmojiMetadata to the index.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@VisibleForTesting
void put(@NonNull final EmojiMetadata 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 EmojiMetadata. A single codepoint
* emoji is represented by a child of the root node.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
static class Node {
private final SparseArray<Node> mChildren;
private EmojiMetadata mData;
private Node() {
this(1);
}
private Node(final int defaultChildrenSize) {
mChildren = new SparseArray<>(defaultChildrenSize);
}
Node get(final int key) {
return mChildren == null ? null : mChildren.get(key);
}
final EmojiMetadata getData() {
return mData;
}
private void put(@NonNull final EmojiMetadata 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;
}
}
}
}