FontRequestEmojiCompatConfig.java revision f69ef36b9ff270c87e41177551ef4692f9aff965
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 */
16
17package android.support.text.emoji;
18
19import android.content.Context;
20import android.graphics.Typeface;
21import android.os.Bundle;
22import android.os.ParcelFileDescriptor;
23import android.support.annotation.NonNull;
24import android.support.annotation.RestrictTo;
25import android.support.v4.graphics.TypefaceCompat;
26import android.support.v4.graphics.TypefaceCompat.FontRequestCallback;
27import android.support.v4.graphics.fonts.FontResult;
28import android.support.v4.os.ResultReceiver;
29import android.support.v4.provider.FontRequest;
30import android.support.v4.provider.FontsContractCompat;
31import android.support.v4.provider.FontsContractInternal;
32import android.support.v4.util.Preconditions;
33
34import java.io.FileInputStream;
35import java.io.IOException;
36import java.nio.ByteBuffer;
37import java.nio.channels.FileChannel;
38import java.util.Arrays;
39import java.util.List;
40
41/**
42 * {@link EmojiCompat.Config} implementation that asynchronously fetches the required font and the
43 * metadata using a {@link FontRequest}. FontRequest should be constructed to fetch an EmojiCompat
44 * compatible emoji font.
45 * <p/>
46 * See {@link FontsContractCompat.FontRequestCallback#onTypefaceRequestFailed(int)} for more
47 * information about the cases where the font loading can fail.
48 */
49public class FontRequestEmojiCompatConfig extends EmojiCompat.Config {
50
51    /**
52     * @param context Context instance, cannot be {@code null}
53     * @param request {@link FontRequest} to fetch the font asynchronously, cannot be {@code null}
54     */
55    public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request) {
56        super(new FontRequestMetadataLoader(context, request));
57    }
58
59    /**
60     * @hide
61     */
62    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
63    public FontRequestEmojiCompatConfig(@NonNull Context context, @NonNull FontRequest request,
64            @NonNull FontsContractInternal fontsContract) {
65        super(new FontRequestMetadataLoader(context, request, fontsContract));
66    }
67
68
69    /**
70     * MetadataLoader implementation that uses FontsContractInternal and TypefaceCompat to load a
71     * given FontRequest.
72     */
73    private static class FontRequestMetadataLoader implements EmojiCompat.MetadataLoader {
74        private final Context mContext;
75        private final FontRequest mRequest;
76        private final FontsContractInternal mFontsContract;
77
78        FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request) {
79            this(context, request, new FontsContractInternal(context));
80        }
81
82        FontRequestMetadataLoader(@NonNull Context context, @NonNull FontRequest request,
83                @NonNull FontsContractInternal fontsContract) {
84            Preconditions.checkNotNull(context, "Context cannot be null");
85            Preconditions.checkNotNull(request, "FontRequest cannot be null");
86            mContext = context.getApplicationContext();
87            mRequest = request;
88            mFontsContract = fontsContract;
89        }
90
91        @Override
92        public void load(@NonNull final EmojiCompat.LoaderCallback loaderCallback) {
93            Preconditions.checkNotNull(loaderCallback, "LoaderCallback cannot be null");
94            final ResultReceiver receiver = new ResultReceiver(null) {
95                @Override
96                public void onReceiveResult(final int resultCode, final Bundle resultData) {
97                    receiveResult(loaderCallback, resultCode, resultData);
98                }
99            };
100            try {
101                mFontsContract.getFont(mRequest, receiver);
102            } catch (Throwable throwable) {
103                loaderCallback.onFailed(throwable);
104            }
105        }
106
107        private void receiveResult(final EmojiCompat.LoaderCallback loaderCallback,
108                final int resultCode, final Bundle resultData) {
109            try {
110                if (resultCode != FontsContractCompat.Columns.RESULT_CODE_OK) {
111                    throwException(resultCode);
112                }
113
114                if (resultData == null) {
115                    throwException(FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND);
116                }
117
118                final List<FontResult> fontResults = resultData.getParcelableArrayList(
119                        FontsContractInternal.PARCEL_FONT_RESULTS);
120                if (fontResults == null || fontResults.isEmpty()) {
121                    throwException(FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND);
122                }
123
124                final InitRunnable runnable = new InitRunnable(mContext, fontResults.get(0),
125                        loaderCallback);
126                final Thread thread = new Thread(runnable);
127                thread.setDaemon(false);
128                thread.start();
129            } catch (Throwable t) {
130                loaderCallback.onFailed(t);
131            }
132        }
133    }
134
135    /**
136     * Runnable used to create the Typeface and MetadataRepo from a given FontResult.
137     */
138    private static class InitRunnable implements Runnable {
139        private final EmojiCompat.LoaderCallback mLoaderCallback;
140        private final Context mContext;
141        private final FontResult mFontResult;
142
143        private InitRunnable(final Context context,
144                final FontResult fontResult,
145                final EmojiCompat.LoaderCallback loaderCallback) {
146            mContext = context;
147            mFontResult = fontResult;
148            mLoaderCallback = loaderCallback;
149        }
150
151        @Override
152        public void run() {
153            try {
154                final ParcelFileDescriptor dupFd = mFontResult.getFileDescriptor().dup();
155                // this one will close fd that is in mFontResult
156                final Typeface typeface = TypefaceCompat.createTypeface(mContext,
157                        Arrays.asList(mFontResult)).getTypeface();
158                if (typeface == null) {
159                    throwException(FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
160                }
161                // this one will close dupFd
162                final MetadataRepo metadataRepo = createMetadataRepo(typeface, dupFd);
163                mLoaderCallback.onLoaded(metadataRepo);
164            } catch (Throwable t) {
165                mLoaderCallback.onFailed(t);
166            }
167        }
168
169        private MetadataRepo createMetadataRepo(final Typeface typeface,
170                final ParcelFileDescriptor parcelFileDescriptor) throws IOException {
171            try (ParcelFileDescriptor pfd = parcelFileDescriptor;
172                 FileInputStream inputStream = new FileInputStream(pfd.getFileDescriptor())) {
173                final FileChannel fileChannel = inputStream.getChannel();
174                final ByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0,
175                        fileChannel.size());
176                final MetadataRepo metadataRepo = MetadataRepo.create(typeface, buffer);
177                return metadataRepo;
178            }
179        }
180    }
181
182    private static void throwException(int code) {
183        throw new RuntimeException("Cannot load metadata, error code:" + Integer.toString(code));
184    }
185}
186