From 0d253e46aa0b4cb2ea56e220812aeab92de64ae1 Mon Sep 17 00:00:00 2001 From: Seigo Nonaka Date: Thu, 29 Jun 2017 17:50:33 -0700 Subject: [PATCH] Implement family fallback. Introduce new attribute "fallbackFor" to font element. By specifying name of the family to this attribute, that font is used when the developer specifies the font family. For example, if fonts.xml has the following family entry, NotoSerifJP-Regular.ttf NotoSansJP-Regular.ttf the Japanese text is rendered by NotoSansJP-Regular.ttf by default. Then, if developer specifies fontFamily="serif" in TextView, the Japanese text is rendered by NotoSerifJP-Regular.ttf. Bug: 37328609 Bug: 31491668 Test: bit FrameworksCoreTests:android.graphics.TypefaceSystemFallbackTest Change-Id: I2744db7384c8056795e841c88b387545434131f4 --- core/java/android/text/FontConfig.java | 8 +- core/tests/coretests/assets/fonts/a3em.ttf | Bin 0 -> 1784 bytes core/tests/coretests/assets/fonts/a3em.ttx | 187 +++++++ core/tests/coretests/assets/fonts/all2em.ttf | Bin 0 -> 1780 bytes core/tests/coretests/assets/fonts/all2em.ttx | 184 +++++++ core/tests/coretests/assets/fonts/b3em.ttf | Bin 0 -> 1784 bytes core/tests/coretests/assets/fonts/b3em.ttx | 187 +++++++ core/tests/coretests/assets/fonts/c3em.ttf | Bin 0 -> 1784 bytes core/tests/coretests/assets/fonts/c3em.ttx | 187 +++++++ .../coretests/assets/fonts/no_coverage.ttf | Bin 0 -> 1768 bytes .../coretests/assets/fonts/no_coverage.ttx | 180 +++++++ .../graphics/TypefaceSystemFallbackTest.java | 469 +++++++++++++++++ .../java/android/graphics/FontListParser.java | 3 +- graphics/java/android/graphics/Typeface.java | 490 ++++++++++-------- libs/hwui/hwui/Typeface.cpp | 2 +- 15 files changed, 1688 insertions(+), 209 deletions(-) create mode 100644 core/tests/coretests/assets/fonts/a3em.ttf create mode 100644 core/tests/coretests/assets/fonts/a3em.ttx create mode 100644 core/tests/coretests/assets/fonts/all2em.ttf create mode 100644 core/tests/coretests/assets/fonts/all2em.ttx create mode 100644 core/tests/coretests/assets/fonts/b3em.ttf create mode 100644 core/tests/coretests/assets/fonts/b3em.ttx create mode 100644 core/tests/coretests/assets/fonts/c3em.ttf create mode 100644 core/tests/coretests/assets/fonts/c3em.ttx create mode 100644 core/tests/coretests/assets/fonts/no_coverage.ttf create mode 100644 core/tests/coretests/assets/fonts/no_coverage.ttx create mode 100644 core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java index ed583907123b6..029f66e1d670a 100644 --- a/core/java/android/text/FontConfig.java +++ b/core/java/android/text/FontConfig.java @@ -64,17 +64,19 @@ public final class FontConfig { private final int mWeight; private final boolean mIsItalic; private Uri mUri; + private final String mFallbackFor; /** * @hide */ public Font(@NonNull String fontName, int ttcIndex, @NonNull FontVariationAxis[] axes, - int weight, boolean isItalic) { + int weight, boolean isItalic, String fallbackFor) { mFontName = fontName; mTtcIndex = ttcIndex; mAxes = axes; mWeight = weight; mIsItalic = isItalic; + mFallbackFor = fallbackFor; } /** @@ -125,6 +127,10 @@ public final class FontConfig { public void setUri(@NonNull Uri uri) { mUri = uri; } + + public String getFallbackFor() { + return mFallbackFor; + } } /** diff --git a/core/tests/coretests/assets/fonts/a3em.ttf b/core/tests/coretests/assets/fonts/a3em.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a601ce2ed932468ecc3232eecb75bf167fdc9ab8 GIT binary patch literal 1784 zcmai#O>Yxd6o%jNI1nfw>V|HwA{Kza&@5;LT|g;_G$^j(gp^7FCeFk*^;c~NCx`_S z8^o#${s3D9%SuRPU9spABz^!3#I91hcAs-+CSeSyb2ayTpL_0m&e#eh?bSVna^=d& z>H78BV<|X-_GYyebv4j4{KHtPw+R}WGmX}s_5lOcGr$tar4dnACvqO#croR7(LgtwCj!8 zWwr9Y`0LN=jf3C)t1x_L(XakFXYqP{xBsir^x3UK$`KR38+|K%@q9;<{2qAh3h0xCVf)#wU+8F1+=FJ7H{k;vLQ438 z4(LwG7eCPN(qTPF_4~sobT;unpoQX9T~J3|ZK|h+YN|6GPDjulg-+{~zScKz3s_?4 zd9^vOM@$3iR3m2{>ny{R@=;TuahC46 z57wr?uj5`3=sD`>5N{P(ZAmo^c&9Z^N^Tx&!`E-$phIi8Rf$WmX^^Sy?M_3iR?W_> z@)ecFs#zq`&glCSdvL!|NrQVPIIZ{^BYI$6Fcc%Lq*WRILr&^=6eD_KQAykYZC2?J zy;Ws(*JMccv96&RlEGr~2(9AT$7)e4R5M6(eyV5r+u4bR+~+db{5X%6(-bpVL7HKo zf`0F1lk(Lp;jy60Oza}mX$il1)_Kh#&*-AhQbyyL{xaGhe8i0B?S!%}v77%j=qy(A zF&tv*O=idD(7N39cqH9OaBXaN*^nLYUxlb_YK)HKv6glxo6tTd*tga2(^VzHx@dh_ zht_n0?iKG*`g(0v(y7Cl_@h{X)~<^3fmm|MBtjAH_iv(*OVf literal 0 HcmV?d00001 diff --git a/core/tests/coretests/assets/fonts/a3em.ttx b/core/tests/coretests/assets/fonts/a3em.ttx new file mode 100644 index 0000000000000..d3b9e16037641 --- /dev/null +++ b/core/tests/coretests/assets/fonts/a3em.ttx @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (C) 2017 The Android Open Source Project + + + Sample Font + + + Regular + + + Sample Font + + + SampleFont-Regular + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + + + + + + + + + + + diff --git a/core/tests/coretests/assets/fonts/all2em.ttf b/core/tests/coretests/assets/fonts/all2em.ttf new file mode 100644 index 0000000000000000000000000000000000000000..482f7552f5109ab5afa4a5da62b4611fccf11b78 GIT binary patch literal 1780 zcmai#zi$**5XZlJdj=B$>8PI^3W(rHTtEaVAOwVC9i0}_Pwd40Fh{o?MNvk?Un zLPDwd7bs{@QbvlR`~j9|D2zlwNrC7hnfmgXx4SlXfU-~f=GS-T&6}CkDvmANDd6LbwqGsH9_28#8{zS3U?)8U%X&UI3 z*=4!%Zk>Mq^ye?Xf3Glni$+`jo;82Hyw`hZG<)`{ka75gZ-?L6zN^V$o)&!U96w^p zgs?DA6(%%U_^oga-E2yOTbd4z8D=5_LM+dnXK{9EPG=P<4jnmclEWrOj|sL5|G+!M zZQaCj(_2D1BJEdEUqg#5L&w7*d*t^3oyEu(!p^f-H>he*+=pg`H{}BzLQ4694(e{k z7eCPN(P2Hx^asLcbT!2v)I#y9&a17CHq=$3n(FG4_Yt&5q0{gx> zW)V+2qvucT!rdX0gnOoSTJ||c^uW4cRgAQpWo7jra8lb>F`_5tmHPG3I`Ee10KH{p zQcYIL9@bSf10tBuXoXfl_OP1QGTHRAn4jv|=DRVXO`@ z`KGgDb7)=e_Nb8#HblEC9-oFZ7`P3L4ug7ZEoqR%joM7Kp;-{;Mhjr2VvIedB zST%fph3leEd*-}XmrSg_`4?c7Hs6)8irJhPvX9leZdEtz|M#!N + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (C) 2017 The Android Open Source Project + + + Sample Font + + + Regular + + + Sample Font + + + SampleFont-Regular + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + + + + + + + + + + + diff --git a/core/tests/coretests/assets/fonts/b3em.ttf b/core/tests/coretests/assets/fonts/b3em.ttf new file mode 100644 index 0000000000000000000000000000000000000000..63948a22c11370a55b4f3d842fbe183cfb730785 GIT binary patch literal 1784 zcmai#O>Yxd6o%jNI1r$Kx+x1bR}l-Sz_eLV1S}FLh%_j!;)Jx7LYX)ddn$inJ9UD( zsKf@bsJh?}bc+x>B&6~KvgndZ{D3Y{ca_q$`o_p>&XKV$M4(n%y za^=eT>H3}8Z&Gj??cHiK>Zqq@$d8dP)Ee6xP@dqG?e#cXKlEVtKI~K83w0FX--mbM z|KdGWZ}zu7ejJ>@{~_{6jdnGH@&tddO*W&gj*4EMWOuEo88==(_$rM*QS7vP{oxBu zO9x(?U6w2F%bnj(et-PRzY4=QhJN+WjN$d?PrYA^X3s$tG7e1mW_VFL@?u{za`QCw zvBR7$FNB4~;8#s(vhb+z4W{{&26r?aoHf^peSgby?^&K-S7?53&Y-XP~Xwfpm+$)I&aDc z`UENE3p%Fz8DD%$e?X`7Fw-9mpU~OV|CpAFw{=Zzb+oOn64g{!pWIKQJp-NAXZl=U zz%5~kp|fgnUKdOPbt;36(K%JnZDOf$K)`jOc-N!KxT(Ez8R4Kj5UcuVO?`43*;g zXdV8R=>WZDWl~L6$sX1XGy@_SCSRd-JbPFTwMI7mEaw09EPp#Y(JJ@82sS^?SIcRN znXDkqu}?v-dfB9WHY<26=_V7q40T$;Z;>^t1>`wh_fg7d9MfM$`=v+BdEQPa>jt~| ze+!+*YCcwnn0(XOu{pFZcYHn4ZlrZh_PT799q(TS)M#pqj@M%&>&|FGdz@h3R^q3t z3c|W*ec6Q8e5@LnAK|*_)1Enx>XM1ock~5Vr4742Rxz6s^Xy}_u3Od3_rv`wF}ZzK zN0)S-HSnuMoc%AeMQyLqkp+5kUFWhdH2W + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (C) 2017 The Android Open Source Project + + + Sample Font + + + Regular + + + Sample Font + + + SampleFont-Regular + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + + + + + + + + + + + diff --git a/core/tests/coretests/assets/fonts/c3em.ttf b/core/tests/coretests/assets/fonts/c3em.ttf new file mode 100644 index 0000000000000000000000000000000000000000..badc3e29868fbd30d6f8e24a63165ce89b7b0242 GIT binary patch literal 1784 zcmai#yKfX%6vn^VS%ZN9N(1ecML{B1hzk-03f{so@*=N|LnM=o*W=wq`(k&^+9*XN zH;5_)QfgWRWh5l1BZ`zn;t!xebV#P&{LY;j%q)qR(cJSs_uTKCyQ@IbUj3#}u3R}W zUB6j-CU?20Hs-p)N%ooWQ-3I1l8Y(*Pg6}>#k>e^8&Zob<3A;~{c>~{Kt(Nj%JyIz@H zRx980zn<;;`QVxN3d1)R{qo&ei`PHD_Wv-NKD$*&Iby;$qwl1BPj@t#o2Q}A9p-d- zAuP-XziUF1h5LmcG0i44xT)#jn7K~u_*k1hOfg;S>l{_>MX;Q@=;TuahmSA zkKRpxUdOp2&~wz$A>Jyo+LCG-@J?&&l$<=)hOgi5phIi8Rf$WmX^^Sy?M_3iR?W_- z@)ecFs#zq`_UQW^dvLd@q`^58oK}2|5k0Uj7>bcr(y9#qAv<+EiV;1rs3dNHHmh`q z-m0>?YceGJSl7@D$zU;ggjVtFW3{Lisu`p?|Ep*D)7g%O-2Y{;`EedCrzvK#f;7WA z1wHR&lk(Lp;jy4AOza}mX$il1-gBBmp3x!>;O**ro!}k% sw?v+un_2QFs$os$F}u}$G*`j)0v(y7Czo_Qy`kxE`RGsk|M>Lz7u5|D(*OVf literal 0 HcmV?d00001 diff --git a/core/tests/coretests/assets/fonts/c3em.ttx b/core/tests/coretests/assets/fonts/c3em.ttx new file mode 100644 index 0000000000000..f5ed8e556332b --- /dev/null +++ b/core/tests/coretests/assets/fonts/c3em.ttx @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (C) 2017 The Android Open Source Project + + + Sample Font + + + Regular + + + Sample Font + + + SampleFont-Regular + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + + + + + + + + + + + diff --git a/core/tests/coretests/assets/fonts/no_coverage.ttf b/core/tests/coretests/assets/fonts/no_coverage.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c884881c5026713114077979f52e202bc7d1b2e1 GIT binary patch literal 1768 zcmai#&rcIk5XWcR@-u=r&o6`nCQuX;e;+^uMF>^O4>dtuN?F$Qmu;b7JeY7Hp8PwE zR})PP>d(>pz1-#slHt75c{S@UcWnJU!}2BX*;NejZ%&f+SKi*$e03vs zOBTUzqqpXY85hZL-ZHi3-Hj6as@~3I3T{4|`?Pf<$lsDE6)V;HYZ(H%qkd7Yyq~|k ze{=u%)fcxI!f!o1qFV z(jI#jyNzu!kj_0Ba{3k15&{v*^I*(PE=m zlL5Y4W)t%=Dn%*Drj*4OPgbQ0qdu%BkwbDyPRlt~vv{(|5h<`=nHV2wBt>i%n~^lO zH9Q%MyJ;~GS!c+Y7FY5hbJjNwvVe64%_Amh<5Cct^DR$NT1*tIW{%e*`mUu(P zh+~P2nNVwt=?6DYY?%zc<)Noca!x&Ul%Yo&qk5>uB|2t~Je&5shI6@8&%mQdyfRX5 z0UMvSkrj3dPM(zw%fDoiJ~0Np7}mxoQ^ER;)QC|who@>}lx$)%N2Kmiu{)MoJ%WkP zIfFVaS&l9}&@N~dU0DiQY5i;LRJ2ue>4`=KaaF7ayfUp}S5~TdqLr-RUBOZ#gT^#g zXc=S$uSPAwrW)q_ThAI#r#oup{*^)RkFnJ<(!-rhqfIdPAg@?mwMI4zAZFz{H+Bwb zWC7d^&xlN;Pso_flEPxR{VA+hZN!9K>kcUyXIA_jwxe`pnx2fwK=_5){}Fy4U+MqFpW`>V&lb`E literal 0 HcmV?d00001 diff --git a/core/tests/coretests/assets/fonts/no_coverage.ttx b/core/tests/coretests/assets/fonts/no_coverage.ttx new file mode 100644 index 0000000000000..3be5f8626bf14 --- /dev/null +++ b/core/tests/coretests/assets/fonts/no_coverage.ttx @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright (C) 2017 The Android Open Source Project + + + Sample Font + + + Regular + + + Sample Font + + + SampleFont-Regular + + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + + + + + + + + + + + diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java new file mode 100644 index 0000000000000..ca4f7d43caf42 --- /dev/null +++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.content.res.AssetManager; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.ArrayMap; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class TypefaceSystemFallbackTest { + private static final String SYSTEM_FONT_DIR = "/system/fonts/"; + private static final String SYSTEM_FONTS_XML = "/system/etc/fonts.xml"; + + private static final String[] TEST_FONT_FILES = { + "a3em.ttf", // Supports "a","b","c". The width of "a" is 3em, others are 1em. + "b3em.ttf", // Supports "a","b","c". The width of "b" is 3em, others are 1em. + "c3em.ttf", // Supports "a","b","c". The width of "c" is 3em, others are 1em. + "all2em.ttf", // Supports "a,","b","c". All of them have the same width of 2em. + "no_coverage.ttf", // This font doesn't support any characters. + }; + private static final String TEST_FONTS_XML; + private static final String TEST_FONT_DIR; + + private static final float GLYPH_1EM_WIDTH; + private static final float GLYPH_2EM_WIDTH; + private static final float GLYPH_3EM_WIDTH; + + static { + final Context targetCtx = InstrumentationRegistry.getInstrumentation().getTargetContext(); + final File cacheDir = new File(targetCtx.getCacheDir(), "TypefaceSystemFallbackTest"); + if (!cacheDir.isDirectory()) { + cacheDir.mkdirs(); + } + TEST_FONT_DIR = cacheDir.getAbsolutePath() + "/"; + TEST_FONTS_XML = new File(cacheDir, "fonts.xml").getAbsolutePath(); + + final AssetManager am = + InstrumentationRegistry.getInstrumentation().getContext().getAssets(); + final Paint paint = new Paint(); + paint.setTypeface(new Typeface.Builder(am, "fonts/a3em.ttf").build()); + GLYPH_3EM_WIDTH = paint.measureText("a"); + GLYPH_1EM_WIDTH = paint.measureText("b"); + + paint.setTypeface(new Typeface.Builder(am, "fonts/all2em.ttf").build()); + GLYPH_2EM_WIDTH = paint.measureText("a"); + } + + @Before + public void setUp() { + final AssetManager am = + InstrumentationRegistry.getInstrumentation().getContext().getAssets(); + for (final String fontFile : TEST_FONT_FILES) { + final String sourceInAsset = "fonts/" + fontFile; + final File outInCache = new File(TEST_FONT_DIR, fontFile); + try (InputStream is = am.open(sourceInAsset)) { + Files.copy(is, outInCache.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + @After + public void tearDown() { + for (final String fontFile : TEST_FONT_FILES) { + final File outInCache = new File(TEST_FONT_DIR, fontFile); + outInCache.delete(); + } + } + + private static void buildSystemFallback(String xml, + ArrayMap fontMap, ArrayMap fallbackMap) { + try (FileOutputStream fos = new FileOutputStream(TEST_FONTS_XML)) { + fos.write(xml.getBytes(Charset.forName("UTF-8"))); + } catch (IOException e) { + throw new RuntimeException(e); + } + Typeface.buildSystemFallback(TEST_FONTS_XML, TEST_FONT_DIR, fontMap, fallbackMap); + } + + @Test + public void testBuildSystemFallback() { + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + Typeface.buildSystemFallback(SYSTEM_FONTS_XML, SYSTEM_FONT_DIR, fontMap, fallbackMap); + + assertFalse(fontMap.isEmpty()); + assertFalse(fallbackMap.isEmpty()); + } + + @Test + public void testBuildSystemFallback_NonExistentFontShouldBeIgnored() { + final String xml = "" + + "" + + " " + + " a3em.ttf" + + " NoSuchFont.ttf" + + " " + + " " + + " NoSuchFont.ttf" + + " " + + " " + + " NoSuchFont.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + assertEquals(1, fontMap.size()); + assertTrue(fontMap.containsKey("sans-serif")); + assertEquals(1, fallbackMap.size()); + assertTrue(fallbackMap.containsKey("sans-serif")); + } + + @Test + public void testBuildSystemFallback_NamedFamily() { + final String xml = "" + + "" + + " " + + " a3em.ttf" + + " " + + " " + + " b3em.ttf" + + " " + + " " + + " c3em.ttf" + + " " + + " " + + " all2em.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + final Paint paint = new Paint(); + + final Typeface sansSerifTypeface = fontMap.get("sans-serif"); + assertNotNull(sansSerifTypeface); + paint.setTypeface(sansSerifTypeface); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface testTypeface = fontMap.get("test"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface test2Typeface = fontMap.get("test2"); + assertNotNull(test2Typeface); + paint.setTypeface(test2Typeface); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + } + + @Test + public void testBuildSystemFallback_defaultFallback() { + final String xml = "" + + "" + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " a3em.ttf" + + " " + + " " + + " all2em.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + final Paint paint = new Paint(); + + final Typeface sansSerifTypeface = fontMap.get("sans-serif"); + assertNotNull(sansSerifTypeface); + paint.setTypeface(sansSerifTypeface); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface testTypeface = fontMap.get("test"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + } + + @Test + public void testBuildSystemFallback_namedFallbackFamily() { + final String xml = "" + + "" + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " a3em.ttf" + + " " + + " " + + " b3em.ttf" + + " " + + " " + + " all2em.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + final Paint paint = new Paint(); + + final Typeface sansSerifTypeface = fontMap.get("sans-serif"); + assertNotNull(sansSerifTypeface); + paint.setTypeface(sansSerifTypeface); + assertEquals(GLYPH_2EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_2EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_2EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface testTypeface = fontMap.get("test"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface test2Typeface = fontMap.get("test2"); + assertNotNull(test2Typeface); + paint.setTypeface(test2Typeface); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + } + + @Test + public void testBuildSystemFallback_namedFallbackFamily2() { + final String xml = "" + + "" + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " a3em.ttf" + + " b3em.ttf" + + " " + + " " + + " all2em.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + final Paint paint = new Paint(); + + final Typeface sansSerifTypeface = fontMap.get("sans-serif"); + assertNotNull(sansSerifTypeface); + paint.setTypeface(sansSerifTypeface); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface testTypeface = fontMap.get("test"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface test2Typeface = fontMap.get("test2"); + assertNotNull(test2Typeface); + paint.setTypeface(test2Typeface); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + } + + @Test + public void testBuildSystemFallback_ImplicitSansSerifFallback() { + final String xml = "" + + "" + + " " + + " a3em.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " all2em.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + final Paint paint = new Paint(); + + final Typeface testTypeface = fontMap.get("test"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + final Typeface test2Typeface = fontMap.get("test2"); + assertNotNull(test2Typeface); + paint.setTypeface(test2Typeface); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + } + + @Test + public void testBuildSystemFallback_ElegantFallback() { + final String xml = "" + + "" + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " a3em.ttf" + + " " + + " " + + " b3em.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + final Paint paint = new Paint(); + + final Typeface testTypeface = fontMap.get("serif"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + paint.setElegantTextHeight(true); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + paint.setElegantTextHeight(false); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + } + + @Test + public void testBuildSystemFallback_ElegantFallback_customFallback() { + final String xml = "" + + "" + + " " + + " no_coverage.ttf" + + " " + + " " + + " no_coverage.ttf" + + " " + + " " + + " a3em.ttf" + + " b3em.ttf" + + " " + + " " + + " c3em.ttf" + + " " + + ""; + final ArrayMap fontMap = new ArrayMap<>(); + final ArrayMap fallbackMap = new ArrayMap<>(); + + buildSystemFallback(xml, fontMap, fallbackMap); + + final Paint paint = new Paint(); + + Typeface testTypeface = fontMap.get("serif"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + paint.setElegantTextHeight(true); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + paint.setElegantTextHeight(false); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + + testTypeface = fontMap.get("sans-serif"); + assertNotNull(testTypeface); + paint.setTypeface(testTypeface); + paint.setElegantTextHeight(true); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + + paint.setElegantTextHeight(false); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + } +} diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index 7c07a302dfe99..80a9324d04f3c 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -111,6 +111,7 @@ public class FontListParser { String weightStr = parser.getAttributeValue(null, "weight"); int weight = weightStr == null ? 400 : Integer.parseInt(weightStr); boolean isItalic = "italic".equals(parser.getAttributeValue(null, "style")); + String fallbackFor = parser.getAttributeValue(null, "fallbackFor"); StringBuilder filename = new StringBuilder(); while (parser.next() != XmlPullParser.END_TAG) { if (parser.getEventType() == XmlPullParser.TEXT) { @@ -126,7 +127,7 @@ public class FontListParser { } String sanitizedName = FILENAME_WHITESPACE_PATTERN.matcher(filename).replaceAll(""); return new FontConfig.Font(sanitizedName, index, - axes.toArray(new FontVariationAxis[axes.size()]), weight, isItalic); + axes.toArray(new FontVariationAxis[axes.size()]), weight, isItalic, fallbackFor); } private static FontVariationAxis readAxis(XmlPullParser parser) diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index c4b56c333c646..1d8b5830aa926 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -38,6 +38,7 @@ import android.os.ResultReceiver; import android.provider.FontRequest; import android.provider.FontsContract; import android.text.FontConfig; +import android.util.ArrayMap; import android.util.Base64; import android.util.Log; import android.util.LongSparseArray; @@ -45,6 +46,7 @@ import android.util.LruCache; import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import libcore.io.IoUtils; @@ -105,12 +107,10 @@ public class Typeface { private static final LruCache sDynamicTypefaceCache = new LruCache<>(16); static Typeface sDefaultTypeface; - static Map sSystemFontMap; - static FontFamily[] sFallbackFonts; + static final Map sSystemFontMap; + static final Map sSystemFallbackMap; private static final Object sLock = new Object(); - static final String FONTS_CONFIG = "fonts.xml"; - /** * @hide */ @@ -129,6 +129,7 @@ public class Typeface { // Must be the same as the C++ constant in core/jni/android/graphics/FontFamily.cpp /** @hide */ public static final int RESOLVE_BY_FONT_TABLE = -1; + private static final String DEFAULT_FAMILY = "sans-serif"; // Style value for building typeface. private static final int STYLE_NORMAL = 0; @@ -163,28 +164,27 @@ public class Typeface { */ @Nullable public static Typeface createFromResources(AssetManager mgr, String path, int cookie) { - if (sFallbackFonts != null) { - synchronized (sDynamicTypefaceCache) { - final String key = Builder.createAssetUid( - mgr, path, 0 /* ttcIndex */, null /* axes */, - RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */); - Typeface typeface = sDynamicTypefaceCache.get(key); - if (typeface != null) return typeface; + synchronized (sDynamicTypefaceCache) { + final String key = Builder.createAssetUid( + mgr, path, 0 /* ttcIndex */, null /* axes */, + RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */, + DEFAULT_FAMILY); + Typeface typeface = sDynamicTypefaceCache.get(key); + if (typeface != null) return typeface; - FontFamily fontFamily = new FontFamily(); - // TODO: introduce ttc index and variation settings to resource type font. - if (fontFamily.addFontFromAssetManager(mgr, path, cookie, false /* isAsset */, - 0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */, - RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) { - if (!fontFamily.freeze()) { - return null; - } - FontFamily[] families = {fontFamily}; - typeface = createFromFamiliesWithDefault(families, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - sDynamicTypefaceCache.put(key, typeface); - return typeface; + FontFamily fontFamily = new FontFamily(); + // TODO: introduce ttc index and variation settings to resource type font. + if (fontFamily.addFontFromAssetManager(mgr, path, cookie, false /* isAsset */, + 0 /* ttcIndex */, RESOLVE_BY_FONT_TABLE /* weight */, + RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) { + if (!fontFamily.freeze()) { + return null; } + FontFamily[] families = {fontFamily}; + typeface = createFromFamiliesWithDefault(families, DEFAULT_FAMILY, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); + sDynamicTypefaceCache.put(key, typeface); + return typeface; } } return null; @@ -197,61 +197,57 @@ public class Typeface { @Nullable public static Typeface createFromResources( FamilyResourceEntry entry, AssetManager mgr, String path) { - if (sFallbackFonts != null) { - if (entry instanceof ProviderResourceEntry) { - final ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry; - // Downloadable font - List> givenCerts = providerEntry.getCerts(); - List> certs = new ArrayList<>(); - if (givenCerts != null) { - for (int i = 0; i < givenCerts.size(); i++) { - List certSet = givenCerts.get(i); - List byteArraySet = new ArrayList<>(); - for (int j = 0; j < certSet.size(); j++) { - byteArraySet.add(Base64.decode(certSet.get(j), Base64.DEFAULT)); - } - certs.add(byteArraySet); + if (entry instanceof ProviderResourceEntry) { + final ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry; + // Downloadable font + List> givenCerts = providerEntry.getCerts(); + List> certs = new ArrayList<>(); + if (givenCerts != null) { + for (int i = 0; i < givenCerts.size(); i++) { + List certSet = givenCerts.get(i); + List byteArraySet = new ArrayList<>(); + for (int j = 0; j < certSet.size(); j++) { + byteArraySet.add(Base64.decode(certSet.get(j), Base64.DEFAULT)); } - } - // Downloaded font and it wasn't cached, request it again and return a - // default font instead (nothing we can do now). - FontRequest request = new FontRequest(providerEntry.getAuthority(), - providerEntry.getPackage(), providerEntry.getQuery(), certs); - Typeface typeface = FontsContract.getFontSync(request); - return typeface == null ? DEFAULT : typeface; - } - - Typeface typeface = findFromCache(mgr, path); - if (typeface != null) return typeface; - - // family is FontFamilyFilesResourceEntry - final FontFamilyFilesResourceEntry filesEntry = - (FontFamilyFilesResourceEntry) entry; - - FontFamily fontFamily = new FontFamily(); - for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { - // TODO: Add ttc and variation font support. (b/37853920) - if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(), - 0 /* resourceCookie */, false /* isAsset */, 0 /* ttcIndex */, - fontFile.getWeight(), fontFile.getItalic(), null /* axes */)) { - return null; + certs.add(byteArraySet); } } - if (!fontFamily.freeze()) { + // Downloaded font and it wasn't cached, request it again and return a + // default font instead (nothing we can do now). + FontRequest request = new FontRequest(providerEntry.getAuthority(), + providerEntry.getPackage(), providerEntry.getQuery(), certs); + Typeface typeface = FontsContract.getFontSync(request); + return typeface == null ? DEFAULT : typeface; + } + + Typeface typeface = findFromCache(mgr, path); + if (typeface != null) return typeface; + + // family is FontFamilyFilesResourceEntry + final FontFamilyFilesResourceEntry filesEntry = (FontFamilyFilesResourceEntry) entry; + + FontFamily fontFamily = new FontFamily(); + for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) { + // TODO: Add ttc and variation font support. (b/37853920) + if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(), + 0 /* resourceCookie */, false /* isAsset */, 0 /* ttcIndex */, + fontFile.getWeight(), fontFile.getItalic(), null /* axes */)) { return null; } - FontFamily[] familyChain = { fontFamily }; - typeface = createFromFamiliesWithDefault(familyChain, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - synchronized (sDynamicTypefaceCache) { - final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, - null /* axes */, RESOLVE_BY_FONT_TABLE /* weight */, - RESOLVE_BY_FONT_TABLE /* italic */); - sDynamicTypefaceCache.put(key, typeface); - } - return typeface; } - return null; + if (!fontFamily.freeze()) { + return null; + } + FontFamily[] familyChain = { fontFamily }; + typeface = createFromFamiliesWithDefault(familyChain, DEFAULT_FAMILY, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); + synchronized (sDynamicTypefaceCache) { + final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, + null /* axes */, RESOLVE_BY_FONT_TABLE /* weight */, + RESOLVE_BY_FONT_TABLE /* italic */, DEFAULT_FAMILY); + sDynamicTypefaceCache.put(key, typeface); + } + return typeface; } /** @@ -261,7 +257,8 @@ public class Typeface { public static Typeface findFromCache(AssetManager mgr, String path) { synchronized (sDynamicTypefaceCache) { final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, null /* axes */, - RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */); + RESOLVE_BY_FONT_TABLE /* weight */, RESOLVE_BY_FONT_TABLE /* italic */, + DEFAULT_FAMILY); Typeface typeface = sDynamicTypefaceCache.get(key); if (typeface != null) { return typeface; @@ -498,7 +495,7 @@ public class Typeface { * @return Unique id for a given AssetManager and asset path. */ private static String createAssetUid(final AssetManager mgr, String path, int ttcIndex, - @Nullable FontVariationAxis[] axes, int weight, int italic) { + @Nullable FontVariationAxis[] axes, int weight, int italic, String fallback) { final SparseArray pkgs = mgr.getAssignedPackageIdentifiers(); final StringBuilder builder = new StringBuilder(); final int size = pkgs.size(); @@ -513,7 +510,11 @@ public class Typeface { builder.append(Integer.toString(weight)); builder.append("-"); builder.append(Integer.toString(italic)); - builder.append("-"); + // Family name may contain hyphen. Use double hyphen for avoiding key conflicts before + // and after appending falblack name. + builder.append("--"); + builder.append(fallback); + builder.append("--"); if (axes != null) { for (FontVariationAxis axis : axes) { builder.append(axis.getTag()); @@ -593,13 +594,15 @@ public class Typeface { return resolveFallbackTypeface(); } FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, mWeight, mItalic); + return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight, + mItalic); } catch (IOException e) { return resolveFallbackTypeface(); } } else if (mAssetManager != null) { // Builder is created with asset manager. final String key = createAssetUid( - mAssetManager, mPath, mTtcIndex, mAxes, mWeight, mItalic); + mAssetManager, mPath, mTtcIndex, mAxes, mWeight, mItalic, + mFallbackFamilyName); synchronized (sLock) { Typeface typeface = sDynamicTypefaceCache.get(key); if (typeface != null) return typeface; @@ -613,7 +616,8 @@ public class Typeface { return resolveFallbackTypeface(); } FontFamily[] families = { fontFamily }; - typeface = createFromFamiliesWithDefault(families, mWeight, mItalic); + typeface = createFromFamiliesWithDefault(families, mFallbackFamilyName, + mWeight, mItalic); sDynamicTypefaceCache.put(key, typeface); return typeface; } @@ -627,7 +631,8 @@ public class Typeface { return resolveFallbackTypeface(); } FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, mWeight, mItalic); + return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight, + mItalic); } else if (mFonts != null) { final FontFamily fontFamily = new FontFamily(); boolean atLeastOneFont = false; @@ -653,7 +658,8 @@ public class Typeface { } fontFamily.freeze(); FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, mWeight, mItalic); + return createFromFamiliesWithDefault(families, mFallbackFamilyName, mWeight, + mItalic); } // Must not reach here. @@ -673,10 +679,7 @@ public class Typeface { * @return The best matching typeface. */ public static Typeface create(String familyName, int style) { - if (sSystemFontMap != null) { - return create(sSystemFontMap.get(familyName), style); - } - return null; + return create(sSystemFontMap.get(familyName), style); } /** @@ -751,34 +754,33 @@ public class Typeface { if (path == null) { throw new NullPointerException(); // for backward compatibility } - if (sFallbackFonts != null) { - synchronized (sLock) { - Typeface typeface = new Builder(mgr, path).build(); - if (typeface != null) return typeface; + synchronized (sLock) { + Typeface typeface = new Builder(mgr, path).build(); + if (typeface != null) return typeface; - final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, - null /* axes */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - typeface = sDynamicTypefaceCache.get(key); - if (typeface != null) return typeface; + final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, + null /* axes */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, + DEFAULT_FAMILY); + typeface = sDynamicTypefaceCache.get(key); + if (typeface != null) return typeface; - final FontFamily fontFamily = new FontFamily(); - if (fontFamily.addFontFromAssetManager(mgr, path, 0, true /* isAsset */, - 0 /* ttc index */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, - null /* axes */)) { - // Due to backward compatibility, even if the font is not supported by our font - // stack, we need to place the empty font at the first place. The typeface with - // empty font behaves different from default typeface especially in fallback - // font selection. - fontFamily.allowUnsupportedFont(); - fontFamily.freeze(); - final FontFamily[] families = { fontFamily }; - typeface = createFromFamiliesWithDefault(families, RESOLVE_BY_FONT_TABLE, - RESOLVE_BY_FONT_TABLE); - sDynamicTypefaceCache.put(key, typeface); - return typeface; - } else { - fontFamily.abortCreation(); - } + final FontFamily fontFamily = new FontFamily(); + if (fontFamily.addFontFromAssetManager(mgr, path, 0, true /* isAsset */, + 0 /* ttc index */, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, + null /* axes */)) { + // Due to backward compatibility, even if the font is not supported by our font + // stack, we need to place the empty font at the first place. The typeface with + // empty font behaves different from default typeface especially in fallback + // font selection. + fontFamily.allowUnsupportedFont(); + fontFamily.freeze(); + final FontFamily[] families = { fontFamily }; + typeface = createFromFamiliesWithDefault(families, DEFAULT_FAMILY, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); + sDynamicTypefaceCache.put(key, typeface); + return typeface; + } else { + fontFamily.abortCreation(); } } throw new RuntimeException("Font asset not found " + path); @@ -815,22 +817,20 @@ public class Typeface { * @return The new typeface. */ public static Typeface createFromFile(@Nullable String path) { - if (sFallbackFonts != null) { - final FontFamily fontFamily = new FontFamily(); - if (fontFamily.addFont(path, 0 /* ttcIndex */, null /* axes */, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)) { - // Due to backward compatibility, even if the font is not supported by our font - // stack, we need to place the empty font at the first place. The typeface with - // empty font behaves different from default typeface especially in fallback font - // selection. - fontFamily.allowUnsupportedFont(); - fontFamily.freeze(); - FontFamily[] families = { fontFamily }; - return createFromFamiliesWithDefault(families, RESOLVE_BY_FONT_TABLE, - RESOLVE_BY_FONT_TABLE); - } else { - fontFamily.abortCreation(); - } + final FontFamily fontFamily = new FontFamily(); + if (fontFamily.addFont(path, 0 /* ttcIndex */, null /* axes */, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE)) { + // Due to backward compatibility, even if the font is not supported by our font + // stack, we need to place the empty font at the first place. The typeface with + // empty font behaves different from default typeface especially in fallback font + // selection. + fontFamily.allowUnsupportedFont(); + fontFamily.freeze(); + FontFamily[] families = { fontFamily }; + return createFromFamiliesWithDefault(families, DEFAULT_FAMILY, + RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); + } else { + fontFamily.abortCreation(); } throw new RuntimeException("Font not found " + path); } @@ -852,6 +852,8 @@ public class Typeface { /** * Create a new typeface from an array of font families, including * also the font families in the fallback list. + * @param fallbackName the family name. If given families don't support characters, the + * characters will be rendered with this family. * @param weight the weight for this family. {@link RESOLVE_BY_FONT_TABLE} can be used. In that * case, the table information in the first family's font is used. If the first * family has multiple fonts, the closest to the regular weight and upright font @@ -863,13 +865,17 @@ public class Typeface { * @param families array of font families */ private static Typeface createFromFamiliesWithDefault(FontFamily[] families, - int weight, int italic) { - long[] ptrArray = new long[families.length + sFallbackFonts.length]; + String fallbackName, int weight, int italic) { + FontFamily[] fallback = sSystemFallbackMap.get(fallbackName); + if (fallback == null) { + fallback = sSystemFallbackMap.get(DEFAULT_FAMILY); + } + long[] ptrArray = new long[families.length + fallback.length]; for (int i = 0; i < families.length; i++) { ptrArray[i] = families[i].mNativePtr; } - for (int i = 0; i < sFallbackFonts.length; i++) { - ptrArray[i + families.length] = sFallbackFonts[i].mNativePtr; + for (int i = 0; i < fallback.length; i++) { + ptrArray[i + families.length] = fallback[i].mNativePtr; } return new Typeface(nativeCreateFromArray(ptrArray, weight, italic)); } @@ -885,113 +891,189 @@ public class Typeface { mWeight = nativeGetWeight(ni); } - private static FontFamily makeFamilyFromParsed(FontConfig.Family family, - Map bufferForPath) { - FontFamily fontFamily = new FontFamily(family.getLanguage(), family.getVariant()); - for (FontConfig.Font font : family.getFonts()) { - String fullPathName = "/system/fonts/" + font.getFontName(); - ByteBuffer fontBuffer = bufferForPath.get(fullPathName); - if (fontBuffer == null) { - try (FileInputStream file = new FileInputStream(fullPathName)) { - FileChannel fileChannel = file.getChannel(); - long fontSize = fileChannel.size(); - fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); - bufferForPath.put(fullPathName, fontBuffer); - } catch (IOException e) { - Log.e(TAG, "Error mapping font file " + fullPathName); + private static @Nullable ByteBuffer mmap(String fullPath) { + try (FileInputStream file = new FileInputStream(fullPath)) { + final FileChannel fileChannel = file.getChannel(); + final long fontSize = fileChannel.size(); + return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); + } catch (IOException e) { + Log.e(TAG, "Error mapping font file " + fullPath); + return null; + } + } + + private static @Nullable FontFamily createFontFamily( + String familyName, List fonts, String languageTag, int variant, + Map cache, String fontDir) { + final FontFamily family = new FontFamily(languageTag, variant); + for (int i = 0; i < fonts.size(); i++) { + final FontConfig.Font font = fonts.get(i); + final String fullPath = fontDir + font.getFontName(); + ByteBuffer buffer = cache.get(fullPath); + if (buffer == null) { + if (cache.containsKey(fullPath)) { + continue; // Already failed to mmap. Skip it. + } + buffer = mmap(fullPath); + cache.put(fullPath, buffer); + if (buffer == null) { continue; } } - if (!fontFamily.addFontFromBuffer(fontBuffer, font.getTtcIndex(), font.getAxes(), + if (!family.addFontFromBuffer(buffer, font.getTtcIndex(), font.getAxes(), font.getWeight(), font.isItalic() ? STYLE_ITALIC : STYLE_NORMAL)) { - Log.e(TAG, "Error creating font " + fullPathName + "#" + font.getTtcIndex()); + Log.e(TAG, "Error creating font " + fullPath + "#" + font.getTtcIndex()); } } - if (!fontFamily.freeze()) { - // Treat as system error since reaching here means that a system pre-installed font - // can't be used by our font stack. - Log.e(TAG, "Unable to load Family: " + family.getName() + ":" + family.getLanguage()); + if (!family.freeze()) { + Log.e(TAG, "Unable to load Family: " + familyName + " : " + languageTag); return null; } - return fontFamily; + return family; } - /* - * (non-Javadoc) + private static void pushFamilyToFallback(FontConfig.Family xmlFamily, + ArrayMap> fallbackMap, + Map cache, + String fontDir) { + + final String languageTag = xmlFamily.getLanguage(); + final int variant = xmlFamily.getVariant(); + + final ArrayList defaultFonts = new ArrayList<>(); + final ArrayMap> specificFallbackFonts = new ArrayMap<>(); + + // Collect default fallback and specific fallback fonts. + for (final FontConfig.Font font : xmlFamily.getFonts()) { + final String fallbackName = font.getFallbackFor(); + if (fallbackName == null) { + defaultFonts.add(font); + } else { + ArrayList fallback = specificFallbackFonts.get(fallbackName); + if (fallback == null) { + fallback = new ArrayList<>(); + specificFallbackFonts.put(fallbackName, fallback); + } + fallback.add(font); + } + } + + final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( + xmlFamily.getName(), defaultFonts, languageTag, variant, cache, fontDir); + + // Insert family into fallback map. + for (int i = 0; i < fallbackMap.size(); i++) { + final ArrayList fallback = + specificFallbackFonts.get(fallbackMap.keyAt(i)); + if (fallback == null) { + if (defaultFamily != null) { + fallbackMap.valueAt(i).add(defaultFamily); + } + } else { + final FontFamily family = createFontFamily( + xmlFamily.getName(), fallback, languageTag, variant, cache, fontDir); + if (family != null) { + fallbackMap.valueAt(i).add(family); + } + } + } + } + + /** + * Build the system fallback from xml file. * - * This should only be called once, from the static class initializer block. + * @param xmlPath A full path string to the fonts.xml file. + * @param fontDir A full path string to the system font directory. This must end with + * slash('/'). + * @param fontMap An output system font map. Caller must pass empty map. + * @param fallbackMap An output system fallback map. Caller must pass empty map. + * @hide */ - private static void init() { - // Load font config and initialize Minikin state - File systemFontConfigLocation = getSystemFontConfigLocation(); - File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG); + @VisibleForTesting + public static void buildSystemFallback(String xmlPath, String fontDir, + ArrayMap fontMap, ArrayMap fallbackMap) { try { - FileInputStream fontsIn = new FileInputStream(configFilename); - FontConfig fontConfig = FontListParser.parse(fontsIn); + final FileInputStream fontsIn = new FileInputStream(xmlPath); + final FontConfig fontConfig = FontListParser.parse(fontsIn); - Map bufferForPath = new HashMap(); + final HashMap bufferCache = new HashMap(); + final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies(); - List familyList = new ArrayList(); - // Note that the default typeface is always present in the fallback list; - // this is an enhancement from pre-Minikin behavior. - for (int i = 0; i < fontConfig.getFamilies().length; i++) { - FontConfig.Family f = fontConfig.getFamilies()[i]; - if (i == 0 || f.getName() == null) { - FontFamily family = makeFamilyFromParsed(f, bufferForPath); - if (family != null) { - familyList.add(family); - } + final ArrayMap> fallbackListMap = new ArrayMap<>(); + // First traverse families which have a 'name' attribute to create fallback map. + for (final FontConfig.Family xmlFamily : xmlFamilies) { + final String familyName = xmlFamily.getName(); + if (familyName == null) { + continue; + } + final FontFamily family = createFontFamily( + xmlFamily.getName(), Arrays.asList(xmlFamily.getFonts()), + xmlFamily.getLanguage(), xmlFamily.getVariant(), bufferCache, fontDir); + if (family == null) { + continue; + } + final ArrayList fallback = new ArrayList<>(); + fallback.add(family); + fallbackListMap.put(familyName, fallback); + } + + // Then, add fallback fonts to the each fallback map. + for (int i = 0; i < xmlFamilies.length; i++) { + final FontConfig.Family xmlFamily = xmlFamilies[i]; + // The first family (usually the sans-serif family) is always placed immediately + // after the primary family in the fallback. + if (i == 0 || xmlFamily.getName() == null) { + pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, fontDir); } } - sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]); - setDefault(Typeface.createFromFamilies(sFallbackFonts)); - Map systemFonts = new HashMap(); - for (int i = 0; i < fontConfig.getFamilies().length; i++) { - Typeface typeface; - FontConfig.Family f = fontConfig.getFamilies()[i]; - if (f.getName() != null) { - if (i == 0) { - // The first entry is the default typeface; no sense in - // duplicating the corresponding FontFamily. - typeface = sDefaultTypeface; - } else { - FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath); - if (fontFamily == null) { - continue; - } - FontFamily[] families = { fontFamily }; - typeface = Typeface.createFromFamiliesWithDefault(families, - RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE); - } - systemFonts.put(f.getName(), typeface); + // Build the font map and fallback map. + for (int i = 0; i < fallbackListMap.size(); i++) { + final String fallbackName = fallbackListMap.keyAt(i); + final List familyList = fallbackListMap.valueAt(i); + final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]); + + fallbackMap.put(fallbackName, families); + final long[] ptrArray = new long[families.length]; + for (int j = 0; j < families.length; j++) { + ptrArray[j] = families[j].mNativePtr; } + fontMap.put(fallbackName, new Typeface(nativeCreateFromArray( + ptrArray, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE))); } - for (FontConfig.Alias alias : fontConfig.getAliases()) { - Typeface base = systemFonts.get(alias.getToName()); + + // Insert alias to font maps. + for (final FontConfig.Alias alias : fontConfig.getAliases()) { + Typeface base = fontMap.get(alias.getToName()); Typeface newFace = base; int weight = alias.getWeight(); if (weight != 400) { newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight)); } - systemFonts.put(alias.getName(), newFace); + fontMap.put(alias.getName(), newFace); } - sSystemFontMap = systemFonts; - } catch (RuntimeException e) { Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e); // TODO: normal in non-Minikin case, remove or make error when Minikin-only } catch (FileNotFoundException e) { - Log.e(TAG, "Error opening " + configFilename, e); + Log.e(TAG, "Error opening " + xmlPath, e); } catch (IOException e) { - Log.e(TAG, "Error reading " + configFilename, e); + Log.e(TAG, "Error reading " + xmlPath, e); } catch (XmlPullParserException e) { - Log.e(TAG, "XML parse exception for " + configFilename, e); + Log.e(TAG, "XML parse exception for " + xmlPath, e); } } static { - init(); + final ArrayMap systemFontMap = new ArrayMap<>(); + final ArrayMap systemFallbackMap = new ArrayMap<>(); + buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", systemFontMap, + systemFallbackMap); + sSystemFontMap = Collections.unmodifiableMap(systemFontMap); + sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap); + + setDefault(sSystemFontMap.get(DEFAULT_FAMILY)); + // Set up defaults and typefaces exposed in public API DEFAULT = create((String) null, 0); DEFAULT_BOLD = create((String) null, Typeface.BOLD); @@ -1008,10 +1090,6 @@ public class Typeface { } - private static File getSystemFontConfigLocation() { - return new File("/system/etc/"); - } - @Override protected void finalize() throws Throwable { try { diff --git a/libs/hwui/hwui/Typeface.cpp b/libs/hwui/hwui/Typeface.cpp index f66bb045373c2..d96e376b0b706 100644 --- a/libs/hwui/hwui/Typeface.cpp +++ b/libs/hwui/hwui/Typeface.cpp @@ -68,7 +68,7 @@ static minikin::FontStyle computeRelativeStyle(int baseWeight, SkTypeface::Style Typeface* gDefaultTypeface = NULL; Typeface* Typeface::resolveDefault(Typeface* src) { - LOG_ALWAYS_FATAL_IF(gDefaultTypeface == nullptr); + LOG_ALWAYS_FATAL_IF(src == nullptr && gDefaultTypeface == nullptr); return src == nullptr ? gDefaultTypeface : src; }