1 /*
2  * Copyright (C) 2021 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 android.annotation.IntDef;
20 import android.annotation.IntRange;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.os.Parcel;
24 import android.os.ParcelFileDescriptor;
25 import android.os.Parcelable;
26 import android.text.FontConfig;
27 
28 import com.android.modules.utils.TypedXmlSerializer;
29 
30 import org.xmlpull.v1.XmlPullParser;
31 import org.xmlpull.v1.XmlPullParserException;
32 
33 import java.io.IOException;
34 import java.lang.annotation.Retention;
35 import java.lang.annotation.RetentionPolicy;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Objects;
39 
40 /**
41  * Represents a font update request. Currently only font install request is supported.
42  * @hide
43  */
44 public final class FontUpdateRequest implements Parcelable {
45 
46     public static final int TYPE_UPDATE_FONT_FILE = 0;
47     public static final int TYPE_UPDATE_FONT_FAMILY = 1;
48 
49     @IntDef(prefix = "TYPE_", value = {
50             TYPE_UPDATE_FONT_FILE,
51             TYPE_UPDATE_FONT_FAMILY,
52     })
53     @Retention(RetentionPolicy.SOURCE)
54     public @interface Type {}
55 
56     /**
57      * Font object used for update.
58      *
59      * Here is an example of Family/Font XML.
60      * <family name="my-sans">
61      *   <font name="MySans" weight="400" slant="0" axis="'wght' 400 'ital' 0" index="0" />
62      *   <font name="MySans" weight="400" slant="0" axis="'wght' 400 'ital' 1" index="0" />
63      *   <font name="MySans" weight="400" slant="0" axis="'wght' 700 'ital' 0" index="0" />
64      *   <font name="MySans" weight="400" slant="0" axis="'wght' 700 'ital' 1" index="0" />
65      * </family>
66      *
67      * @see Font#readFromXml(XmlPullParser)
68      * @see Font#writeToXml(TypedXmlSerializer, Font)
69      * @see Family#readFromXml(XmlPullParser)
70      * @see Family#writeFamilyToXml(TypedXmlSerializer, Family)
71      */
72     public static final class Font implements Parcelable {
73         private static final String ATTR_INDEX = "index";
74         private static final String ATTR_WEIGHT = "weight";
75         private static final String ATTR_SLANT = "slant";
76         private static final String ATTR_AXIS = "axis";
77         private static final String ATTR_POSTSCRIPT_NAME = "name";
78 
79         private final @NonNull String mPostScriptName;
80         private final @NonNull FontStyle mFontStyle;
81         private final @IntRange(from = 0) int mIndex;
82         private final @NonNull String mFontVariationSettings;
83 
Font(@onNull String postScriptName, @NonNull FontStyle fontStyle, @IntRange(from = 0) int index, @NonNull String fontVariationSettings)84         public Font(@NonNull String postScriptName, @NonNull FontStyle fontStyle,
85                 @IntRange(from = 0) int index, @NonNull String fontVariationSettings) {
86             mPostScriptName = postScriptName;
87             mFontStyle = fontStyle;
88             mIndex = index;
89             mFontVariationSettings = fontVariationSettings;
90         }
91 
92         @Override
describeContents()93         public int describeContents() {
94             return 0;
95         }
96 
97         @Override
writeToParcel(Parcel dest, int flags)98         public void writeToParcel(Parcel dest, int flags) {
99             dest.writeString8(mPostScriptName);
100             dest.writeInt(mFontStyle.getWeight());
101             dest.writeInt(mFontStyle.getSlant());
102             dest.writeInt(mIndex);
103             dest.writeString8(mFontVariationSettings);
104         }
105 
106         public static final @NonNull Creator<Font> CREATOR = new Creator<Font>() {
107             @Override
108             public Font createFromParcel(Parcel source) {
109                 String fontName = source.readString8();
110                 int weight = source.readInt();
111                 int slant = source.readInt();
112                 int index = source.readInt();
113                 String varSettings = source.readString8();
114                 return new Font(fontName, new FontStyle(weight, slant), index, varSettings);
115             }
116 
117             @Override
118             public Font[] newArray(int size) {
119                 return new Font[size];
120             }
121         };
122 
123         /**
124          * Write {@link Font} instance to XML file.
125          *
126          * For the XML format, see {@link Font} class comment.
127          *
128          * @param out output XML serializer
129          * @param font a Font instance to be written.
130          */
writeToXml(TypedXmlSerializer out, Font font)131         public static void writeToXml(TypedXmlSerializer out, Font font) throws IOException {
132             out.attribute(null, ATTR_POSTSCRIPT_NAME, font.getPostScriptName());
133             out.attributeInt(null, ATTR_INDEX, font.getIndex());
134             out.attributeInt(null, ATTR_WEIGHT, font.getFontStyle().getWeight());
135             out.attributeInt(null, ATTR_SLANT, font.getFontStyle().getSlant());
136             out.attribute(null, ATTR_AXIS, font.getFontVariationSettings());
137         }
138 
139         /**
140          * Read {@link Font} instance from &lt;font&gt; element in XML
141          *
142          * For the XML format, see {@link Font} class comment.
143          *
144          * @param parser a parser that point &lt;font&gt; element.
145          * @return a font instance
146          * @throws IOException if font element is invalid.
147          */
readFromXml(XmlPullParser parser)148         public static Font readFromXml(XmlPullParser parser) throws IOException {
149             String psName = parser.getAttributeValue(null, ATTR_POSTSCRIPT_NAME);
150             if (psName == null) {
151                 throw new IOException("name attribute is missing in font tag.");
152             }
153             int index = getAttributeValueInt(parser, ATTR_INDEX, 0);
154             int weight = getAttributeValueInt(parser, ATTR_WEIGHT, FontStyle.FONT_WEIGHT_NORMAL);
155             int slant = getAttributeValueInt(parser, ATTR_SLANT, FontStyle.FONT_SLANT_UPRIGHT);
156             String varSettings = parser.getAttributeValue(null, ATTR_AXIS);
157             if (varSettings == null) {
158                 varSettings = "";
159             }
160             return new Font(psName, new FontStyle(weight, slant), index, varSettings);
161         }
162 
getPostScriptName()163         public @NonNull String getPostScriptName() {
164             return mPostScriptName;
165         }
166 
getFontStyle()167         public @NonNull FontStyle getFontStyle() {
168             return mFontStyle;
169         }
170 
getIndex()171         public @IntRange(from = 0) int getIndex() {
172             return mIndex;
173         }
174 
getFontVariationSettings()175         public @NonNull String getFontVariationSettings() {
176             return mFontVariationSettings;
177         }
178 
179         @Override
equals(Object o)180         public boolean equals(Object o) {
181             if (this == o) return true;
182             if (o == null || getClass() != o.getClass()) return false;
183             Font font = (Font) o;
184             return mIndex == font.mIndex
185                     && mPostScriptName.equals(font.mPostScriptName)
186                     && mFontStyle.equals(font.mFontStyle)
187                     && mFontVariationSettings.equals(font.mFontVariationSettings);
188         }
189 
190         @Override
hashCode()191         public int hashCode() {
192             return Objects.hash(mPostScriptName, mFontStyle, mIndex, mFontVariationSettings);
193         }
194 
195         @Override
toString()196         public String toString() {
197             return "Font{"
198                     + "mPostScriptName='" + mPostScriptName + '\''
199                     + ", mFontStyle=" + mFontStyle
200                     + ", mIndex=" + mIndex
201                     + ", mFontVariationSettings='" + mFontVariationSettings + '\''
202                     + '}';
203         }
204     }
205 
206     /**
207      * Font Family object used for update request.
208      */
209     public static final class Family implements Parcelable {
210         private static final String TAG_FAMILY = "family";
211         private static final String ATTR_NAME = "name";
212         private static final String TAG_FONT = "font";
213 
214         private final @NonNull String mName;
215         private final @NonNull List<Font> mFonts;
216 
Family(String name, List<Font> fonts)217         public Family(String name, List<Font> fonts) {
218             mName = name;
219             mFonts = fonts;
220         }
221 
222         @Override
describeContents()223         public int describeContents() {
224             return 0;
225         }
226 
227         @Override
writeToParcel(Parcel dest, int flags)228         public void writeToParcel(Parcel dest, int flags) {
229             dest.writeString8(mName);
230             dest.writeParcelableList(mFonts, flags);
231         }
232 
233         public static final @NonNull Creator<Family> CREATOR = new Creator<Family>() {
234 
235             @Override
236             public Family createFromParcel(Parcel source) {
237                 String familyName = source.readString8();
238                 List<Font> fonts = source.readParcelableList(
239                         new ArrayList<>(), Font.class.getClassLoader(), android.graphics.fonts.FontUpdateRequest.Font.class);
240                 return new Family(familyName, fonts);
241             }
242 
243             @Override
244             public Family[] newArray(int size) {
245                 return new Family[size];
246             }
247         };
248 
249         /**
250          * Write {@link Family} instance to XML.
251          *
252          * For the XML format, see {@link Font} class comment.
253          *
254          * @param out an output XML serializer
255          * @param family a {@link Family} instance to be written
256          */
writeFamilyToXml(@onNull TypedXmlSerializer out, @NonNull Family family)257         public static void writeFamilyToXml(@NonNull TypedXmlSerializer out, @NonNull Family family)
258                 throws IOException {
259             out.attribute(null, ATTR_NAME, family.getName());
260             List<Font> fonts = family.getFonts();
261             for (int i = 0; i < fonts.size(); ++i) {
262                 Font font = fonts.get(i);
263                 out.startTag(null, TAG_FONT);
264                 Font.writeToXml(out, font);
265                 out.endTag(null, TAG_FONT);
266             }
267         }
268 
269         /**
270          * Read a {@link Family} instance from &lt;family&gt; element in XML
271          *
272          * For the XML format, see {@link Font} class comment.
273          *
274          * @param parser an XML parser that points &lt;family&gt; element.
275          * @return an {@link Family} instance
276          */
readFromXml(@onNull XmlPullParser parser)277         public static @NonNull Family readFromXml(@NonNull XmlPullParser parser)
278                 throws XmlPullParserException, IOException {
279             List<Font> fonts = new ArrayList<>();
280             if (parser.getEventType() != XmlPullParser.START_TAG
281                     || !parser.getName().equals(TAG_FAMILY)) {
282                 throw new IOException("Unexpected parser state: must be START_TAG with family");
283             }
284             String name = parser.getAttributeValue(null, ATTR_NAME);
285             if (name == null) {
286                 throw new IOException("name attribute is missing in family tag.");
287             }
288             int type = 0;
289             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
290                 if (type == XmlPullParser.START_TAG && parser.getName().equals(TAG_FONT)) {
291                     fonts.add(Font.readFromXml(parser));
292                 } else if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_FAMILY)) {
293                     break;
294                 }
295             }
296             return new Family(name, fonts);
297         }
298 
getName()299         public @NonNull String getName() {
300             return mName;
301         }
302 
getFonts()303         public @NonNull List<Font> getFonts() {
304             return mFonts;
305         }
306 
307         @Override
equals(Object o)308         public boolean equals(Object o) {
309             if (this == o) return true;
310             if (o == null || getClass() != o.getClass()) return false;
311             Family family = (Family) o;
312             return mName.equals(family.mName) && mFonts.equals(family.mFonts);
313         }
314 
315         @Override
hashCode()316         public int hashCode() {
317             return Objects.hash(mName, mFonts);
318         }
319 
320         @Override
toString()321         public String toString() {
322             return "Family{mName='" + mName + '\'' + ", mFonts=" + mFonts + '}';
323         }
324     }
325 
326     public static final Creator<FontUpdateRequest> CREATOR = new Creator<FontUpdateRequest>() {
327         @Override
328         public FontUpdateRequest createFromParcel(Parcel in) {
329             return new FontUpdateRequest(in);
330         }
331 
332         @Override
333         public FontUpdateRequest[] newArray(int size) {
334             return new FontUpdateRequest[size];
335         }
336     };
337 
338     private final @Type int mType;
339     // NonNull if mType == TYPE_UPDATE_FONT_FILE.
340     @Nullable
341     private final ParcelFileDescriptor mFd;
342     // NonNull if mType == TYPE_UPDATE_FONT_FILE.
343     @Nullable
344     private final byte[] mSignature;
345     // NonNull if mType == TYPE_UPDATE_FONT_FAMILY.
346     @Nullable
347     private final Family mFontFamily;
348 
FontUpdateRequest(@onNull ParcelFileDescriptor fd, @NonNull byte[] signature)349     public FontUpdateRequest(@NonNull ParcelFileDescriptor fd, @NonNull byte[] signature) {
350         mType = TYPE_UPDATE_FONT_FILE;
351         mFd = fd;
352         mSignature = signature;
353         mFontFamily = null;
354     }
355 
FontUpdateRequest(@onNull Family fontFamily)356     public FontUpdateRequest(@NonNull Family fontFamily) {
357         mType = TYPE_UPDATE_FONT_FAMILY;
358         mFd = null;
359         mSignature = null;
360         mFontFamily = fontFamily;
361     }
362 
FontUpdateRequest(@onNull String familyName, @NonNull List<FontFamilyUpdateRequest.Font> variations)363     public FontUpdateRequest(@NonNull String familyName,
364             @NonNull List<FontFamilyUpdateRequest.Font> variations) {
365         this(createFontFamily(familyName, variations));
366     }
367 
createFontFamily(@onNull String familyName, @NonNull List<FontFamilyUpdateRequest.Font> fonts)368     private static Family createFontFamily(@NonNull String familyName,
369             @NonNull List<FontFamilyUpdateRequest.Font> fonts) {
370         List<Font> updateFonts = new ArrayList<>(fonts.size());
371         for (FontFamilyUpdateRequest.Font font : fonts) {
372             updateFonts.add(new Font(
373                     font.getPostScriptName(),
374                     font.getStyle(),
375                     font.getIndex(),
376                     FontVariationAxis.toFontVariationSettings(font.getAxes())));
377         }
378         return new Family(familyName, updateFonts);
379     }
380 
FontUpdateRequest(Parcel in)381     protected FontUpdateRequest(Parcel in) {
382         mType = in.readInt();
383         mFd = in.readParcelable(ParcelFileDescriptor.class.getClassLoader(), android.os.ParcelFileDescriptor.class);
384         mSignature = in.readBlob();
385         mFontFamily = in.readParcelable(FontConfig.FontFamily.class.getClassLoader(), android.graphics.fonts.FontUpdateRequest.Family.class);
386     }
387 
getType()388     public @Type int getType() {
389         return mType;
390     }
391 
392     @Nullable
getFd()393     public ParcelFileDescriptor getFd() {
394         return mFd;
395     }
396 
397     @Nullable
getSignature()398     public byte[] getSignature() {
399         return mSignature;
400     }
401 
402     @Nullable
getFontFamily()403     public Family getFontFamily() {
404         return mFontFamily;
405     }
406 
407     @Override
describeContents()408     public int describeContents() {
409         return mFd != null ? mFd.describeContents() : 0;
410     }
411 
412     @Override
writeToParcel(Parcel dest, int flags)413     public void writeToParcel(Parcel dest, int flags) {
414         dest.writeInt(mType);
415         dest.writeParcelable(mFd, flags);
416         dest.writeBlob(mSignature);
417         dest.writeParcelable(mFontFamily, flags);
418     }
419 
420     // Utility functions
getAttributeValueInt(XmlPullParser parser, String name, int defaultValue)421     private static int getAttributeValueInt(XmlPullParser parser, String name, int defaultValue) {
422         try {
423             String value = parser.getAttributeValue(null, name);
424             if (value == null) {
425                 return defaultValue;
426             }
427             return Integer.parseInt(value);
428         } catch (NumberFormatException e) {
429             return defaultValue;
430         }
431     }
432 }
433