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