1feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk/*
2feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * Copyright 2018 The Android Open Source Project
3feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk *
4feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * Licensed under the Apache License, Version 2.0 (the "License");
5feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * you may not use this file except in compliance with the License.
6feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * You may obtain a copy of the License at
7feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk *
8feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk *      http://www.apache.org/licenses/LICENSE-2.0
9feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk *
10feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * Unless required by applicable law or agreed to in writing, software
11feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * distributed under the License is distributed on an "AS IS" BASIS,
12feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * See the License for the specific language governing permissions and
14feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk * limitations under the License.
15feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk */
16feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
17feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkpackage androidx.emoji.text;
18feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
195d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvalaimport static android.content.res.AssetManager.ACCESS_BUFFER;
20feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
21feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_FONT_NOT_FOUND;
22feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_FONT_UNAVAILABLE;
23feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_MALFORMED_QUERY;
24feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static androidx.core.provider.FontsContractCompat.Columns.RESULT_CODE_OK;
25feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_OK;
26feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static androidx.core.provider.FontsContractCompat.FontFamilyResult.STATUS_WRONG_CERTIFICATES;
27feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
28feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.hamcrest.CoreMatchers.containsString;
29feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.junit.Assert.assertThat;
30feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.junit.Assert.fail;
31feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Matchers.any;
32feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Matchers.eq;
33feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Matchers.same;
34feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Mockito.atLeastOnce;
35feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Mockito.doReturn;
36feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Mockito.doThrow;
37feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Mockito.mock;
38feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Mockito.never;
39feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Mockito.spy;
40feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport static org.mockito.Mockito.times;
410fd198ad89ec9c600bb1761b10d938146c28bb98Ruben Brunkimport static org.mockito.Mockito.verify;
42feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
43234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.content.Context;
44234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.content.pm.PackageManager.NameNotFoundException;
45234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.database.ContentObserver;
46234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.net.Uri;
47234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.os.Handler;
48234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.os.HandlerThread;
49234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.support.test.InstrumentationRegistry;
50234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.support.test.filters.SdkSuppress;
51234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.support.test.filters.SmallTest;
52234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport android.support.test.runner.AndroidJUnit4;
53234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wang
54234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport androidx.annotation.GuardedBy;
55234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport androidx.annotation.NonNull;
56234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport androidx.annotation.Nullable;
57234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport androidx.core.provider.FontRequest;
58234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport androidx.core.provider.FontsContractCompat.FontFamilyResult;
59234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport androidx.core.provider.FontsContractCompat.FontInfo;
60234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wang
61234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport org.junit.Before;
62234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport org.junit.Test;
63234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport org.junit.runner.RunWith;
64234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangimport org.mockito.ArgumentCaptor;
650fd198ad89ec9c600bb1761b10d938146c28bb98Ruben Brunk
660fd198ad89ec9c600bb1761b10d938146c28bb98Ruben Brunkimport java.io.File;
670fd198ad89ec9c600bb1761b10d938146c28bb98Ruben Brunkimport java.io.FileOutputStream;
68feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport java.io.IOException;
69feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport java.io.InputStream;
70feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport java.util.ArrayList;
71feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport java.util.List;
72feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport java.util.concurrent.CountDownLatch;
73feb50af361e4305a25758966b6b5df2738c00259Ruben Brunkimport java.util.concurrent.TimeUnit;
74234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wang
75234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wang@SmallTest
76feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk@RunWith(AndroidJUnit4.class)
77234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wangpublic class FontRequestEmojiCompatConfigTest {
78feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    private static final int DEFAULT_TIMEOUT_MILLIS = 3000;
79234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wang    private Context mContext;
80feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    private FontRequest mFontRequest;
81feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    private FontRequestEmojiCompatConfig.FontProviderHelper mFontProviderHelper;
82feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
83feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Before
84feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    public void setup() {
85feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        mContext = InstrumentationRegistry.getContext();
86feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        mFontRequest = new FontRequest("authority", "package", "query",
87feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk                new ArrayList<List<byte[]>>());
88feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        mFontProviderHelper = mock(FontRequestEmojiCompatConfig.FontProviderHelper.class);
89feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
90234ba3ef752742e2f87094e67896a8bde5709d12Shuzhen Wang
91feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test(expected = NullPointerException.class)
92feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    public void testConstructor_withNullContext() {
93feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        new FontRequestEmojiCompatConfig(null, mFontRequest);
94feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
95feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
96feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test(expected = NullPointerException.class)
97feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    public void testConstructor_withNullFontRequest() {
98feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        new FontRequestEmojiCompatConfig(mContext, null);
99feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
100feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
101feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test
102feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @SdkSuppress(minSdkVersion = 19)
103feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    public void testLoad_whenGetFontThrowsException() throws NameNotFoundException {
104feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        final Exception exception = new RuntimeException();
105feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        doThrow(exception).when(mFontProviderHelper).fetchFonts(
10653284d5816f065b2de20dcb019fa1096b148eee4Ruben Brunk                any(Context.class), any(FontRequest.class));
10753284d5816f065b2de20dcb019fa1096b148eee4Ruben Brunk        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
108feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
109e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk                mFontProviderHelper);
110feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
111feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        config.getMetadataRepoLoader().load(callback);
112feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        callback.await(DEFAULT_TIMEOUT_MILLIS);
113feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        verify(callback, times(1)).onFailed(same(exception));
114feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
115feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
116feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test
117e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk    @SdkSuppress(minSdkVersion = 19)
118e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk    public void testLoad_providerNotFound() throws NameNotFoundException {
119e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        doThrow(new NameNotFoundException()).when(mFontProviderHelper).fetchFonts(
120e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk                any(Context.class), any(FontRequest.class));
121e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
122e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
123e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk                mFontRequest, mFontProviderHelper);
124e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk
125e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        config.getMetadataRepoLoader().load(callback);
126e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        callback.await(DEFAULT_TIMEOUT_MILLIS);
127e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk
128e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
129e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        verify(callback, times(1)).onFailed(argumentCaptor.capture());
130e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk        assertThat(argumentCaptor.getValue().getMessage(), containsString("provider not found"));
131feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
132feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
133feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test
134feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @SdkSuppress(minSdkVersion = 19)
135feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    public void testLoad_wrongCertificate() throws NameNotFoundException {
136feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        verifyLoaderOnFailedCalled(STATUS_WRONG_CERTIFICATES, null /* fonts */,
1375d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala                "fetchFonts failed (" + STATUS_WRONG_CERTIFICATES + ")");
1385d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala    }
1395d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala
1405d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala    @Test
141feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @SdkSuppress(minSdkVersion = 19)
1425d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala    public void testLoad_fontNotFound() throws NameNotFoundException {
143feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        verifyLoaderOnFailedCalled(STATUS_OK,
1440fd198ad89ec9c600bb1761b10d938146c28bb98Ruben Brunk                getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_NOT_FOUND),
1455d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala                "fetchFonts result is not OK. (" + RESULT_CODE_FONT_NOT_FOUND + ")");
146feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
147e663cb77281c4c76241b820f6126543f1c2d859fRuben Brunk
148feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test
1495d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala    @SdkSuppress(minSdkVersion = 19)
15053284d5816f065b2de20dcb019fa1096b148eee4Ruben Brunk    public void testLoad_fontUnavailable() throws NameNotFoundException {
151feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        verifyLoaderOnFailedCalled(STATUS_OK,
152feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk                getTestFontInfoWithInvalidPath(RESULT_CODE_FONT_UNAVAILABLE),
153feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk                "fetchFonts result is not OK. (" + RESULT_CODE_FONT_UNAVAILABLE + ")");
154feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
155feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
1565d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala    @Test
157feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @SdkSuppress(minSdkVersion = 19)
1585d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala    public void testLoad_malformedQuery() throws NameNotFoundException {
1595d2d7788f1759b0f3d2c057af0b3ea61b0354feeEino-Ville Talvala        verifyLoaderOnFailedCalled(STATUS_OK,
160feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk                getTestFontInfoWithInvalidPath(RESULT_CODE_MALFORMED_QUERY),
161feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk                "fetchFonts result is not OK. (" + RESULT_CODE_MALFORMED_QUERY + ")");
162feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
163feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
164feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test
165feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @SdkSuppress(minSdkVersion = 19)
166feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    public void testLoad_resultNotFound() throws NameNotFoundException {
16791b9aabc9fa0c058ecc4a8b3f486540c28fe1cc0Ruben Brunk        verifyLoaderOnFailedCalled(STATUS_OK, new FontInfo[] {},
168feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk                "fetchFonts failed (empty result)");
169feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    }
170feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk
171feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @Test
172feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    @SdkSuppress(minSdkVersion = 19)
173feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk    public void testLoad_nullFontInfo() throws NameNotFoundException {
174feb50af361e4305a25758966b6b5df2738c00259Ruben Brunk        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