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
17package androidx.emoji.text;
18
19import static android.content.res.AssetManager.ACCESS_BUFFER;
20
21import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_FONT_NOT_FOUND;
22import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_FONT_UNAVAILABLE;
23import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_MALFORMED_QUERY;
24import static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_OK;
25import static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_OK;
26import static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_WRONG_CERTIFICATES;
27
28import static org.hamcrest.CoreMatchers.containsString;
29import static org.junit.Assert.assertThat;
30import static org.junit.Assert.fail;
31import static org.mockito.Matchers.any;
32import static org.mockito.Matchers.eq;
33import static org.mockito.Matchers.same;
34import static org.mockito.Mockito.atLeastOnce;
35import static org.mockito.Mockito.doReturn;
36import static org.mockito.Mockito.doThrow;
37import static org.mockito.Mockito.mock;
38import static org.mockito.Mockito.never;
39import static org.mockito.Mockito.spy;
40import static org.mockito.Mockito.times;
41import static org.mockito.Mockito.verify;
42
43import android.content.Context;
44import android.content.pm.PackageManager.NameNotFoundException;
45import android.database.ContentObserver;
46import android.net.Uri;
47import android.os.Handler;
48import android.os.HandlerThread;
49import android.support.test.InstrumentationRegistry;
50import android.support.test.filters.SdkSuppress;
51import android.support.test.filters.SmallTest;
52import android.support.test.runner.AndroidJUnit4;
53
54import androidx.annotation.GuardedBy;
55import androidx.annotation.NonNull;
56import androidx.annotation.Nullable;
57import androidx.core.provider.FontRequest;
58import androidx.core.provider.FontsContractCompat.FontFamilyResult;
59import androidx.core.provider.FontsContractCompat.FontInfo;
60
61import org.junit.Before;
62import org.junit.Test;
63import org.junit.runner.RunWith;
64import org.mockito.ArgumentCaptor;
65
66import java.io.File;
67import java.io.FileOutputStream;
68import java.io.IOException;
69import java.io.InputStream;
70import java.util.ArrayList;
71import java.util.List;
72import java.util.concurrent.CountDownLatch;
73import java.util.concurrent.TimeUnit;
74
75@SmallTest
76@RunWith(AndroidJUnit4.class)
77public class FontRequestEmojiCompatConfigTest {
78    private static final int DEFAULT_TIMEOUT_MILLIS = 3000;
79    private Context mContext;
80    private FontRequest mFontRequest;
81    private FontRequestEmojiCompatConfig.FontProviderHelper mFontProviderHelper;
82
83    @Before
84    public void setup() {
85        mContext = InstrumentationRegistry.getContext();
86        mFontRequest = new FontRequest("authority", "package", "query",
87                new ArrayList<List<byte[]>>());
88        mFontProviderHelper = mock(FontRequestEmojiCompatConfig.FontProviderHelper.class);
89    }
90
91    @Test(expected = NullPointerException.class)
92    public void testConstructor_withNullContext() {
93        new FontRequestEmojiCompatConfig(null, mFontRequest);
94    }
95
96    @Test(expected = NullPointerException.class)
97    public void testConstructor_withNullFontRequest() {
98        new FontRequestEmojiCompatConfig(mContext, null);
99    }
100
101    @Test
102    @SdkSuppress(minSdkVersion = 19)
103    public void testLoad_whenGetFontThrowsException() throws NameNotFoundException {
104        final Exception exception = new RuntimeException();
105        doThrow(exception).when(mFontProviderHelper).fetchFonts(
106                any(Context.class), any(FontRequest.class));
107        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
108        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
109                mFontProviderHelper);
110
111        config.getMetadataRepoLoader().load(callback);
112        callback.await(DEFAULT_TIMEOUT_MILLIS);
113        verify(callback, times(1)).onFailed(same(exception));
114    }
115
116    @Test
117    @SdkSuppress(minSdkVersion = 19)
118    public void testLoad_providerNotFound() throws NameNotFoundException {
119        doThrow(new NameNotFoundException()).when(mFontProviderHelper).fetchFonts(
120                any(Context.class), any(FontRequest.class));
121        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
122        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
123                mFontRequest, mFontProviderHelper);
124
125        config.getMetadataRepoLoader().load(callback);
126        callback.await(DEFAULT_TIMEOUT_MILLIS);
127
128        final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
129        verify(callback, times(1)).onFailed(argumentCaptor.capture());
130        assertThat(argumentCaptor.getValue().getMessage(), containsString("provider not found"));
131    }
132
133    @Test
134    @SdkSuppress(minSdkVersion = 19)
135    public void testLoad_wrongCertificate() throws NameNotFoundException {
136        verifyLoaderOnFailedCalled(STATUS_WRONG_CERTIFICATES, null /* fonts */,
137                "fetchFonts failed (" + STATUS_WRONG_CERTIFICATES + ")");
138    }
139
140    @Test
141    @SdkSuppress(minSdkVersion = 19)
142    public void testLoad_fontNotFound() throws NameNotFoundException {
143        verifyLoaderOnFailedCalled(STATUS_OK,
144                getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_NOT_FOUND),
145                "fetchFonts result is not OK. (" + RESULT_CODE_FONT_NOT_FOUND + ")");
146    }
147
148    @Test
149    @SdkSuppress(minSdkVersion = 19)
150    public void testLoad_fontUnavailable() throws NameNotFoundException {
151        verifyLoaderOnFailedCalled(STATUS_OK,
152                getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_UNAVAILABLE),
153                "fetchFonts result is not OK. (" + RESULT_CODE_FONT_UNAVAILABLE + ")");
154    }
155
156    @Test
157    @SdkSuppress(minSdkVersion = 19)
158    public void testLoad_malformedQuery() throws NameNotFoundException {
159        verifyLoaderOnFailedCalled(STATUS_OK,
160                getTestFontInfoWithInvalidPath(RESULT_CODE_MALFORMED_QUERY),
161                "fetchFonts result is not OK. (" + RESULT_CODE_MALFORMED_QUERY + ")");
162    }
163
164    @Test
165    @SdkSuppress(minSdkVersion = 19)
166    public void testLoad_resultNotFound() throws NameNotFoundException {
167        verifyLoaderOnFailedCalled(STATUS_OK, new FontInfo[] {},
168                "fetchFonts failed (empty result)");
169    }
170
171    @Test
172    @SdkSuppress(minSdkVersion = 19)
173    public void testLoad_nullFontInfo() throws NameNotFoundException {
174        verifyLoaderOnFailedCalled(STATUS_OK, null /* fonts */,
175                "fetchFonts failed (empty result)");
176    }
177
178    @Test
179    @SdkSuppress(minSdkVersion = 19)
180    public void testLoad_cannotLoadTypeface() throws NameNotFoundException {
181        // getTestFontInfoWithInvalidPath returns FontInfo with invalid path to file.
182        verifyLoaderOnFailedCalled(STATUS_OK,
183                getTestFontInfoWithInvalidPath(RESULT_CODE_OK),
184                "Unable to open file.");
185    }
186
187    @Test
188    @SdkSuppress(minSdkVersion = 19)
189    public void testLoad_success() throws IOException, NameNotFoundException {
190        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
191        final FontInfo[] fonts =  new FontInfo[] {
192                new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
193                        false /* italic */, RESULT_CODE_OK)
194        };
195        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
196                any(Context.class), any(FontRequest.class));
197        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
198        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
199                mFontRequest, mFontProviderHelper);
200
201        config.getMetadataRepoLoader().load(callback);
202        callback.await(DEFAULT_TIMEOUT_MILLIS);
203        verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
204    }
205
206    @Test
207    @SdkSuppress(minSdkVersion = 19)
208    public void testLoad_retryPolicy() throws IOException, NameNotFoundException {
209        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
210        final FontInfo[] fonts =  new FontInfo[] {
211                new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
212                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
213        };
214        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
215                any(Context.class), any(FontRequest.class));
216        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
217        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(-1, 1));
218        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
219                mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
220
221        config.getMetadataRepoLoader().load(callback);
222        callback.await(DEFAULT_TIMEOUT_MILLIS);
223        verify(callback, never()).onLoaded(any(MetadataRepo.class));
224        verify(callback, times(1)).onFailed(any(Throwable.class));
225        verify(retryPolicy, times(1)).getRetryDelay();
226    }
227
228    @Test
229    @SdkSuppress(minSdkVersion = 19)
230    public void testLoad_keepRetryingAndGiveUp() throws IOException, NameNotFoundException {
231        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
232        final FontInfo[] fonts =  new FontInfo[] {
233                new FontInfo(Uri.fromFile(file), 0 /* ttc index */, 400 /* weight */,
234                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
235        };
236        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
237                any(Context.class), any(FontRequest.class));
238        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
239        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
240        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
241                mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
242
243        config.getMetadataRepoLoader().load(callback);
244        retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
245        verify(callback, never()).onLoaded(any(MetadataRepo.class));
246        verify(callback, never()).onFailed(any(Throwable.class));
247        verify(retryPolicy, atLeastOnce()).getRetryDelay();
248        retryPolicy.changeReturnValue(-1);
249        callback.await(DEFAULT_TIMEOUT_MILLIS);
250        verify(callback, never()).onLoaded(any(MetadataRepo.class));
251        verify(callback, times(1)).onFailed(any(Throwable.class));
252    }
253
254    @Test
255    @SdkSuppress(minSdkVersion = 19)
256    public void testLoad_keepRetryingAndFail() throws IOException, NameNotFoundException {
257        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
258        final Uri uri = Uri.fromFile(file);
259
260        final FontInfo[] fonts = new FontInfo[] {
261                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
262                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
263        };
264        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
265                any(Context.class), any(FontRequest.class));
266        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
267        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
268
269        HandlerThread thread = new HandlerThread("testThread");
270        thread.start();
271        try {
272            Handler handler = new Handler(thread.getLooper());
273
274            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
275                    mFontRequest, mFontProviderHelper).setHandler(handler)
276                    .setRetryPolicy(retryPolicy);
277
278            config.getMetadataRepoLoader().load(callback);
279            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
280            verify(callback, never()).onLoaded(any(MetadataRepo.class));
281            verify(callback, never()).onFailed(any(Throwable.class));
282            verify(retryPolicy, atLeastOnce()).getRetryDelay();
283
284            // To avoid race condition, change the fetchFonts result on the handler thread.
285            handler.post(new Runnable() {
286                @Override
287                public void run() {
288                    try {
289                        final FontInfo[] fontsSuccess = new FontInfo[] {
290                                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
291                                        false /* italic */, RESULT_CODE_FONT_NOT_FOUND)
292                        };
293
294                        doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
295                                mFontProviderHelper).fetchFonts(any(Context.class),
296                                any(FontRequest.class));
297                    } catch (NameNotFoundException e) {
298                        throw new RuntimeException(e);
299                    }
300                }
301            });
302
303            callback.await(DEFAULT_TIMEOUT_MILLIS);
304            verify(callback, never()).onLoaded(any(MetadataRepo.class));
305            verify(callback, times(1)).onFailed(any(Throwable.class));
306        } finally {
307            thread.quit();
308        }
309    }
310
311    @Test
312    @SdkSuppress(minSdkVersion = 19)
313    public void testLoad_keepRetryingAndSuccess() throws IOException, NameNotFoundException {
314        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
315        final Uri uri = Uri.fromFile(file);
316
317        final FontInfo[] fonts = new FontInfo[]{
318                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
319                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
320        };
321        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
322                any(Context.class), any(FontRequest.class));
323        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
324        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
325
326        HandlerThread thread = new HandlerThread("testThread");
327        thread.start();
328        try {
329            Handler handler = new Handler(thread.getLooper());
330
331            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
332                    mFontRequest, mFontProviderHelper).setHandler(handler)
333                    .setRetryPolicy(retryPolicy);
334
335            config.getMetadataRepoLoader().load(callback);
336            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
337            verify(callback, never()).onLoaded(any(MetadataRepo.class));
338            verify(callback, never()).onFailed(any(Throwable.class));
339            verify(retryPolicy, atLeastOnce()).getRetryDelay();
340
341            final FontInfo[] fontsSuccess = new FontInfo[]{
342                    new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
343                            false /* italic */, RESULT_CODE_OK)
344            };
345
346            // To avoid race condition, change the fetchFonts result on the handler thread.
347            handler.post(new Runnable() {
348                @Override
349                public void run() {
350                    try {
351                        doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
352                                mFontProviderHelper).fetchFonts(any(Context.class),
353                                any(FontRequest.class));
354                    } catch (NameNotFoundException e) {
355                        throw new RuntimeException(e);
356                    }
357                }
358            });
359
360            callback.await(DEFAULT_TIMEOUT_MILLIS);
361            verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
362            verify(callback, never()).onFailed(any(Throwable.class));
363        } finally {
364            thread.quit();
365        }
366    }
367
368    @Test
369    @SdkSuppress(minSdkVersion = 19)
370    public void testLoad_ObserverNotifyAndSuccess() throws IOException, NameNotFoundException {
371        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
372        final Uri uri = Uri.fromFile(file);
373        final FontInfo[] fonts = new FontInfo[]{
374                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
375                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
376        };
377        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
378                any(Context.class), any(FontRequest.class));
379        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
380        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 2));
381
382        HandlerThread thread = new HandlerThread("testThread");
383        thread.start();
384        try {
385            Handler handler = new Handler(thread.getLooper());
386            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
387                    mFontRequest, mFontProviderHelper).setHandler(handler)
388                    .setRetryPolicy(retryPolicy);
389
390            ArgumentCaptor<ContentObserver> observerCaptor =
391                    ArgumentCaptor.forClass(ContentObserver.class);
392
393            config.getMetadataRepoLoader().load(callback);
394            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
395            verify(callback, never()).onLoaded(any(MetadataRepo.class));
396            verify(callback, never()).onFailed(any(Throwable.class));
397            verify(retryPolicy, atLeastOnce()).getRetryDelay();
398            verify(mFontProviderHelper, times(1)).registerObserver(
399                    any(Context.class), eq(uri), observerCaptor.capture());
400
401            final FontInfo[] fontsSuccess = new FontInfo[]{
402                    new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
403                            false /* italic */, RESULT_CODE_OK)
404            };
405            doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
406                    mFontProviderHelper).fetchFonts(any(Context.class), any(FontRequest.class));
407
408            final ContentObserver observer = observerCaptor.getValue();
409            handler.post(new Runnable() {
410                @Override
411                public void run() {
412                    observer.onChange(false /* self change */, uri);
413                }
414            });
415
416            callback.await(DEFAULT_TIMEOUT_MILLIS);
417            verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
418            verify(callback, never()).onFailed(any(Throwable.class));
419        } finally {
420            thread.quit();
421        }
422    }
423
424    @Test
425    @SdkSuppress(minSdkVersion = 19)
426    public void testLoad_ObserverNotifyAndFail() throws IOException, NameNotFoundException {
427        final File file = loadFont(mContext, "NotoColorEmojiCompat.ttf");
428        final Uri uri = Uri.fromFile(file);
429        final FontInfo[] fonts = new FontInfo[]{
430                new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
431                        false /* italic */, RESULT_CODE_FONT_UNAVAILABLE)
432        };
433        doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
434                any(Context.class), any(FontRequest.class));
435        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
436        final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 2));
437
438        HandlerThread thread = new HandlerThread("testThread");
439        thread.start();
440        try {
441            Handler handler = new Handler(thread.getLooper());
442            final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
443                    mFontRequest, mFontProviderHelper).setHandler(handler)
444                    .setRetryPolicy(retryPolicy);
445
446            ArgumentCaptor<ContentObserver> observerCaptor =
447                    ArgumentCaptor.forClass(ContentObserver.class);
448
449            config.getMetadataRepoLoader().load(callback);
450            retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
451            verify(callback, never()).onLoaded(any(MetadataRepo.class));
452            verify(callback, never()).onFailed(any(Throwable.class));
453            verify(retryPolicy, atLeastOnce()).getRetryDelay();
454            verify(mFontProviderHelper, times(1)).registerObserver(
455                    any(Context.class), eq(uri), observerCaptor.capture());
456
457            final FontInfo[] fontsSuccess = new FontInfo[]{
458                    new FontInfo(uri, 0 /* ttc index */, 400 /* weight */,
459                            false /* italic */, RESULT_CODE_FONT_NOT_FOUND)
460            };
461            doReturn(new FontFamilyResult(STATUS_OK, fontsSuccess)).when(
462                    mFontProviderHelper).fetchFonts(any(Context.class), any(FontRequest.class));
463
464            final ContentObserver observer = observerCaptor.getValue();
465            handler.post(new Runnable() {
466                @Override
467                public void run() {
468                    observer.onChange(false /* self change */, uri);
469                }
470            });
471
472            callback.await(DEFAULT_TIMEOUT_MILLIS);
473            verify(callback, never()).onLoaded(any(MetadataRepo.class));
474            verify(callback, times(1)).onFailed(any(Throwable.class));
475        } finally {
476            thread.quit();
477        }
478    }
479
480    private void verifyLoaderOnFailedCalled(final int statusCode,
481            final FontInfo[] fonts, String exceptionMessage) throws NameNotFoundException {
482        doReturn(new FontFamilyResult(statusCode, fonts)).when(mFontProviderHelper).fetchFonts(
483                any(Context.class), any(FontRequest.class));
484        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
485        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
486                mFontProviderHelper);
487
488        config.getMetadataRepoLoader().load(callback);
489        callback.await(DEFAULT_TIMEOUT_MILLIS);
490
491        final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
492        verify(callback, times(1)).onFailed(argumentCaptor.capture());
493        assertThat(argumentCaptor.getValue().getMessage(), containsString(exceptionMessage));
494    }
495
496    public static class WaitingRetryPolicy extends FontRequestEmojiCompatConfig.RetryPolicy {
497        private final CountDownLatch mLatch;
498        private final Object mLock = new Object();
499        @GuardedBy("mLock")
500        private long mReturnValue;
501
502        public WaitingRetryPolicy(long returnValue, int callCount) {
503            mLatch = new CountDownLatch(callCount);
504            synchronized (mLock) {
505                mReturnValue = returnValue;
506            }
507        }
508
509        @Override
510        public long getRetryDelay() {
511            mLatch.countDown();
512            synchronized (mLock) {
513                return mReturnValue;
514            }
515        }
516
517        public void changeReturnValue(long value) {
518            synchronized (mLock) {
519                mReturnValue = value;
520            }
521        }
522
523        public void await(long timeoutMillis) {
524            try {
525                mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
526            } catch (InterruptedException e) {
527                throw new RuntimeException(e);
528            }
529        }
530    }
531
532    public static class WaitingLoaderCallback extends EmojiCompat.MetadataRepoLoaderCallback {
533        final CountDownLatch mLatch;
534
535        public WaitingLoaderCallback() {
536            mLatch = new CountDownLatch(1);
537        }
538
539        @Override
540        public void onLoaded(@NonNull MetadataRepo metadataRepo) {
541            mLatch.countDown();
542        }
543
544        @Override
545        public void onFailed(@Nullable Throwable throwable) {
546            mLatch.countDown();
547        }
548
549        public void await(long timeoutMillis) {
550            try {
551                mLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
552            } catch (InterruptedException e) {
553                throw new RuntimeException(e);
554            }
555        }
556    }
557
558    public static File loadFont(Context context, String fileName) {
559        File cacheFile = new File(context.getCacheDir(), fileName);
560        try {
561            copyToCacheFile(context, fileName, cacheFile);
562            return cacheFile;
563        } catch (IOException e) {
564            fail();
565        }
566        return null;
567    }
568
569    private static void copyToCacheFile(final Context context, final String assetPath,
570            final File cacheFile) throws IOException {
571        try (InputStream is = context.getAssets().open(assetPath, ACCESS_BUFFER);
572             FileOutputStream fos = new FileOutputStream(cacheFile, false)) {
573            byte[] buffer = new byte[1024];
574            int readLen;
575            while ((readLen = is.read(buffer)) != -1) {
576                fos.write(buffer, 0, readLen);
577            }
578        }
579    }
580
581    private FontInfo[] getTestFontInfoWithInvalidPath(int resultCode) {
582        return new FontInfo[] { new FontInfo(Uri.parse("file:///some/dummy/file"),
583                0 /* ttc index */, 400 /* weight */, false /* italic */, resultCode) };
584    }
585}
586