Home | History | Annotate | Download | only in text
      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)
     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      */
     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      */
    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      */
    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      */
    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      */
    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)
    152     Typeface getTypeface() {
    153         return mTypeface;
    154     }
    155 
    156     /**
    157      * @hide
    158      */
    159     @RestrictTo(LIBRARY_GROUP)
    160     int getMetadataVersion() {
    161         return mMetadataList.version();
    162     }
    163 
    164     /**
    165      * @hide
    166      */
    167     @RestrictTo(LIBRARY_GROUP)
    168     Node getRootNode() {
    169         return mRootNode;
    170     }
    171 
    172     /**
    173      * @hide
    174      */
    175     @RestrictTo(LIBRARY_GROUP)
    176     public char[] getEmojiCharArray() {
    177         return mEmojiCharArray;
    178     }
    179 
    180     /**
    181      * @hide
    182      */
    183     @RestrictTo(LIBRARY_GROUP)
    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
    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 
    214         private Node() {
    215             this(1);
    216         }
    217 
    218         private Node(final int defaultChildrenSize) {
    219             mChildren = new SparseArray<>(defaultChildrenSize);
    220         }
    221 
    222         Node get(final int key) {
    223             return mChildren == null ? null : mChildren.get(key);
    224         }
    225 
    226         final EmojiMetadata getData() {
    227             return mData;
    228         }
    229 
    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