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