1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.graphics.fonts;
18 
19 import static android.text.FontConfig.Alias;
20 import static android.text.FontConfig.NamedFamilyList;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.graphics.FontListParser;
25 import android.text.FontConfig;
26 import android.util.Xml;
27 
28 import org.xmlpull.v1.XmlPullParser;
29 import org.xmlpull.v1.XmlPullParserException;
30 
31 import java.io.File;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.Map;
40 
41 /**
42  * Parser for font customization
43  *
44  * @hide
45  */
46 public class FontCustomizationParser {
47     private static final String TAG = "FontCustomizationParser";
48 
49     /**
50      * Represents a customization XML
51      */
52     public static class Result {
53         private final Map<String, NamedFamilyList> mAdditionalNamedFamilies;
54 
55         private final List<Alias> mAdditionalAliases;
56 
57         private final List<FontConfig.Customization.LocaleFallback> mLocaleFamilyCustomizations;
58 
Result()59         public Result() {
60             mAdditionalNamedFamilies = Collections.emptyMap();
61             mLocaleFamilyCustomizations = Collections.emptyList();
62             mAdditionalAliases = Collections.emptyList();
63         }
64 
Result(Map<String, NamedFamilyList> additionalNamedFamilies, List<FontConfig.Customization.LocaleFallback> localeFamilyCustomizations, List<Alias> additionalAliases)65         public Result(Map<String, NamedFamilyList> additionalNamedFamilies,
66                 List<FontConfig.Customization.LocaleFallback> localeFamilyCustomizations,
67                 List<Alias> additionalAliases) {
68             mAdditionalNamedFamilies = additionalNamedFamilies;
69             mLocaleFamilyCustomizations = localeFamilyCustomizations;
70             mAdditionalAliases = additionalAliases;
71         }
72 
getAdditionalNamedFamilies()73         public Map<String, NamedFamilyList> getAdditionalNamedFamilies() {
74             return mAdditionalNamedFamilies;
75         }
76 
getAdditionalAliases()77         public List<Alias> getAdditionalAliases() {
78             return mAdditionalAliases;
79         }
80 
getLocaleFamilyCustomizations()81         public List<FontConfig.Customization.LocaleFallback> getLocaleFamilyCustomizations() {
82             return mLocaleFamilyCustomizations;
83         }
84     }
85 
86     /**
87      * Parses the customization XML
88      *
89      * Caller must close the input stream
90      */
parse( @onNull InputStream in, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap )91     public static Result parse(
92             @NonNull InputStream in,
93             @NonNull String fontDir,
94             @Nullable Map<String, File> updatableFontMap
95     ) throws XmlPullParserException, IOException {
96         XmlPullParser parser = Xml.newPullParser();
97         parser.setInput(in, null);
98         parser.nextTag();
99         return readFamilies(parser, fontDir, updatableFontMap);
100     }
101 
validateAndTransformToResult( List<NamedFamilyList> families, List<FontConfig.Customization.LocaleFallback> outLocaleFamilies, List<Alias> aliases)102     private static Result validateAndTransformToResult(
103             List<NamedFamilyList> families,
104             List<FontConfig.Customization.LocaleFallback> outLocaleFamilies,
105             List<Alias> aliases) {
106         HashMap<String, NamedFamilyList> namedFamily = new HashMap<>();
107         for (int i = 0; i < families.size(); ++i) {
108             final NamedFamilyList family = families.get(i);
109             final String name = family.getName();
110             if (name != null) {
111                 if (namedFamily.put(name, family) != null) {
112                     throw new IllegalArgumentException(
113                             "new-named-family requires unique name attribute");
114                 }
115             } else {
116                 throw new IllegalArgumentException(
117                         "new-named-family requires name attribute or new-default-fallback-family"
118                                 + "requires fallackTarget attribute");
119             }
120         }
121         return new Result(namedFamily, outLocaleFamilies, aliases);
122     }
123 
readFamilies( @onNull XmlPullParser parser, @NonNull String fontDir, @Nullable Map<String, File> updatableFontMap )124     private static Result readFamilies(
125             @NonNull XmlPullParser parser,
126             @NonNull String fontDir,
127             @Nullable Map<String, File> updatableFontMap
128     ) throws XmlPullParserException, IOException {
129         List<NamedFamilyList> families = new ArrayList<>();
130         List<Alias> aliases = new ArrayList<>();
131         List<FontConfig.Customization.LocaleFallback> outLocaleFamilies = new ArrayList<>();
132         parser.require(XmlPullParser.START_TAG, null, "fonts-modification");
133         while (parser.next() != XmlPullParser.END_TAG) {
134             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
135             String tag = parser.getName();
136             if (tag.equals("family")) {
137                 readFamily(parser, fontDir, families, outLocaleFamilies, updatableFontMap);
138             } else if (tag.equals("family-list")) {
139                 readFamilyList(parser, fontDir, families, updatableFontMap);
140             } else if (tag.equals("alias")) {
141                 aliases.add(FontListParser.readAlias(parser));
142             } else {
143                 FontListParser.skip(parser);
144             }
145         }
146         return validateAndTransformToResult(families, outLocaleFamilies, aliases);
147     }
148 
readFamily( @onNull XmlPullParser parser, @NonNull String fontDir, @NonNull List<NamedFamilyList> out, @NonNull List<FontConfig.Customization.LocaleFallback> outCustomization, @Nullable Map<String, File> updatableFontMap)149     private static void readFamily(
150             @NonNull XmlPullParser parser,
151             @NonNull String fontDir,
152             @NonNull List<NamedFamilyList> out,
153             @NonNull List<FontConfig.Customization.LocaleFallback> outCustomization,
154             @Nullable Map<String, File> updatableFontMap)
155             throws XmlPullParserException, IOException {
156         final String customizationType = parser.getAttributeValue(null, "customizationType");
157         if (customizationType == null) {
158             throw new IllegalArgumentException("customizationType must be specified");
159         }
160         if (customizationType.equals("new-named-family")) {
161             NamedFamilyList fontFamily = FontListParser.readNamedFamily(
162                     parser, fontDir, updatableFontMap, false);
163             if (fontFamily != null) {
164                 out.add(fontFamily);
165             }
166         } else if (customizationType.equals("new-locale-family")) {
167             final String lang = parser.getAttributeValue(null, "lang");
168             final String op = parser.getAttributeValue(null, "operation");
169             final int intOp;
170             if (op.equals("append")) {
171                 intOp = FontConfig.Customization.LocaleFallback.OPERATION_APPEND;
172             } else if (op.equals("prepend")) {
173                 intOp = FontConfig.Customization.LocaleFallback.OPERATION_PREPEND;
174             } else if (op.equals("replace")) {
175                 intOp = FontConfig.Customization.LocaleFallback.OPERATION_REPLACE;
176             } else {
177                 throw new IllegalArgumentException("Unknown operation=" + op);
178             }
179 
180             final FontConfig.FontFamily family = FontListParser.readFamily(
181                     parser, fontDir, updatableFontMap, false);
182 
183             // For ignoring the customization, consume the new-locale-family element but don't
184             // register any customizations.
185             if (com.android.text.flags.Flags.vendorCustomLocaleFallback()) {
186                 outCustomization.add(new FontConfig.Customization.LocaleFallback(
187                         Locale.forLanguageTag(lang), intOp, family));
188             }
189         } else {
190             throw new IllegalArgumentException("Unknown customizationType=" + customizationType);
191         }
192     }
193 
readFamilyList( @onNull XmlPullParser parser, @NonNull String fontDir, @NonNull List<NamedFamilyList> out, @Nullable Map<String, File> updatableFontMap)194     private static void readFamilyList(
195             @NonNull XmlPullParser parser,
196             @NonNull String fontDir,
197             @NonNull List<NamedFamilyList> out,
198             @Nullable Map<String, File> updatableFontMap)
199             throws XmlPullParserException, IOException {
200         final String customizationType = parser.getAttributeValue(null, "customizationType");
201         if (customizationType == null) {
202             throw new IllegalArgumentException("customizationType must be specified");
203         }
204         if (customizationType.equals("new-named-family")) {
205             NamedFamilyList fontFamily = FontListParser.readNamedFamilyList(
206                     parser, fontDir, updatableFontMap, false);
207             if (fontFamily != null) {
208                 out.add(fontFamily);
209             }
210         } else {
211             throw new IllegalArgumentException("Unknown customizationType=" + customizationType);
212         }
213     }
214 }
215