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