1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.emoji.text; 17 18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 19 20 import android.content.res.AssetManager; 21 import android.graphics.Typeface; 22 import android.util.SparseArray; 23 24 import androidx.annotation.AnyThread; 25 import androidx.annotation.NonNull; 26 import androidx.annotation.RequiresApi; 27 import androidx.annotation.RestrictTo; 28 import androidx.annotation.VisibleForTesting; 29 import androidx.core.util.Preconditions; 30 import androidx.text.emoji.flatbuffer.MetadataList; 31 32 import java.io.IOException; 33 import java.io.InputStream; 34 import java.nio.ByteBuffer; 35 36 /** 37 * Class to hold the emoji metadata required to process and draw emojis. 38 */ 39 @AnyThread 40 @RequiresApi(19) 41 public final class MetadataRepo { 42 /** 43 * The default children size of the root node. 44 */ 45 private static final int DEFAULT_ROOT_SIZE = 1024; 46 47 /** 48 * MetadataList that contains the emoji metadata. 49 */ 50 private final MetadataList mMetadataList; 51 52 /** 53 * char presentation of all EmojiMetadata's in a single array. All emojis we have are mapped to 54 * Private Use Area A, in the range U+F0000..U+FFFFD. Therefore each emoji takes 2 chars. 55 */ 56 private final char[] mEmojiCharArray; 57 58 /** 59 * Empty root node of the trie. 60 */ 61 private final Node mRootNode; 62 63 /** 64 * Typeface to be used to render emojis. 65 */ 66 private final Typeface mTypeface; 67 68 /** 69 * Constructor used for tests. 70 * 71 * @hide 72 */ 73 @RestrictTo(LIBRARY_GROUP) MetadataRepo()74 MetadataRepo() { 75 mTypeface = null; 76 mMetadataList = null; 77 mRootNode = new Node(DEFAULT_ROOT_SIZE); 78 mEmojiCharArray = new char[0]; 79 } 80 81 /** 82 * Private constructor that is called by one of {@code create} methods. 83 * 84 * @param typeface Typeface to be used to render emojis 85 * @param metadataList MetadataList that contains the emoji metadata 86 */ MetadataRepo(@onNull final Typeface typeface, @NonNull final MetadataList metadataList)87 private MetadataRepo(@NonNull final Typeface typeface, 88 @NonNull final MetadataList metadataList) { 89 mTypeface = typeface; 90 mMetadataList = metadataList; 91 mRootNode = new Node(DEFAULT_ROOT_SIZE); 92 mEmojiCharArray = new char[mMetadataList.listLength() * 2]; 93 constructIndex(mMetadataList); 94 } 95 96 /** 97 * Construct MetadataRepo from an input stream. The library does not close the given 98 * InputStream, therefore it is caller's responsibility to properly close the stream. 99 * 100 * @param typeface Typeface to be used to render emojis 101 * @param inputStream InputStream to read emoji metadata from 102 */ create(@onNull final Typeface typeface, @NonNull final InputStream inputStream)103 public static MetadataRepo create(@NonNull final Typeface typeface, 104 @NonNull final InputStream inputStream) throws IOException { 105 return new MetadataRepo(typeface, MetadataListReader.read(inputStream)); 106 } 107 108 /** 109 * Construct MetadataRepo from a byte buffer. The position of the ByteBuffer will change, it is 110 * caller's responsibility to reposition the buffer if required. 111 * 112 * @param typeface Typeface to be used to render emojis 113 * @param byteBuffer ByteBuffer to read emoji metadata from 114 */ create(@onNull final Typeface typeface, @NonNull final ByteBuffer byteBuffer)115 public static MetadataRepo create(@NonNull final Typeface typeface, 116 @NonNull final ByteBuffer byteBuffer) throws IOException { 117 return new MetadataRepo(typeface, MetadataListReader.read(byteBuffer)); 118 } 119 120 /** 121 * Construct MetadataRepo from an asset. 122 * 123 * @param assetManager AssetManager instance 124 * @param assetPath asset manager path of the file that the Typeface and metadata will be 125 * created from 126 */ create(@onNull final AssetManager assetManager, final String assetPath)127 public static MetadataRepo create(@NonNull final AssetManager assetManager, 128 final String assetPath) throws IOException { 129 final Typeface typeface = Typeface.createFromAsset(assetManager, assetPath); 130 return new MetadataRepo(typeface, MetadataListReader.read(assetManager, assetPath)); 131 } 132 133 /** 134 * Read emoji metadata list and construct the trie. 135 */ constructIndex(final MetadataList metadataList)136 private void constructIndex(final MetadataList metadataList) { 137 int length = metadataList.listLength(); 138 for (int i = 0; i < length; i++) { 139 final EmojiMetadata metadata = new EmojiMetadata(this, i); 140 //since all emojis are mapped to a single codepoint in Private Use Area A they are 2 141 //chars wide 142 //noinspection ResultOfMethodCallIgnored 143 Character.toChars(metadata.getId(), mEmojiCharArray, i * 2); 144 put(metadata); 145 } 146 } 147 148 /** 149 * @hide 150 */ 151 @RestrictTo(LIBRARY_GROUP) getTypeface()152 Typeface getTypeface() { 153 return mTypeface; 154 } 155 156 /** 157 * @hide 158 */ 159 @RestrictTo(LIBRARY_GROUP) getMetadataVersion()160 int getMetadataVersion() { 161 return mMetadataList.version(); 162 } 163 164 /** 165 * @hide 166 */ 167 @RestrictTo(LIBRARY_GROUP) getRootNode()168 Node getRootNode() { 169 return mRootNode; 170 } 171 172 /** 173 * @hide 174 */ 175 @RestrictTo(LIBRARY_GROUP) getEmojiCharArray()176 public char[] getEmojiCharArray() { 177 return mEmojiCharArray; 178 } 179 180 /** 181 * @hide 182 */ 183 @RestrictTo(LIBRARY_GROUP) getMetadataList()184 public MetadataList getMetadataList() { 185 return mMetadataList; 186 } 187 188 /** 189 * Add an EmojiMetadata to the index. 190 * 191 * @hide 192 */ 193 @RestrictTo(LIBRARY_GROUP) 194 @VisibleForTesting put(@onNull final EmojiMetadata data)195 void put(@NonNull final EmojiMetadata data) { 196 Preconditions.checkNotNull(data, "emoji metadata cannot be null"); 197 Preconditions.checkArgument(data.getCodepointsLength() > 0, 198 "invalid metadata codepoint length"); 199 200 mRootNode.put(data, 0, data.getCodepointsLength() - 1); 201 } 202 203 /** 204 * Trie node that holds mapping from emoji codepoint(s) to EmojiMetadata. A single codepoint 205 * emoji is represented by a child of the root node. 206 * 207 * @hide 208 */ 209 @RestrictTo(LIBRARY_GROUP) 210 static class Node { 211 private final SparseArray<Node> mChildren; 212 private EmojiMetadata mData; 213 Node()214 private Node() { 215 this(1); 216 } 217 Node(final int defaultChildrenSize)218 private Node(final int defaultChildrenSize) { 219 mChildren = new SparseArray<>(defaultChildrenSize); 220 } 221 get(final int key)222 Node get(final int key) { 223 return mChildren == null ? null : mChildren.get(key); 224 } 225 getData()226 final EmojiMetadata getData() { 227 return mData; 228 } 229 put(@onNull final EmojiMetadata data, final int start, final int end)230 private void put(@NonNull final EmojiMetadata data, final int start, final int end) { 231 Node node = get(data.getCodepointAt(start)); 232 if (node == null) { 233 node = new Node(); 234 mChildren.put(data.getCodepointAt(start), node); 235 } 236 237 if (end > start) { 238 node.put(data, start + 1, end); 239 } else { 240 node.mData = data; 241 } 242 } 243 } 244 } 245