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 com.android.server.graphics.fonts;
18 
19 import static com.android.server.graphics.fonts.FontManagerService.SystemFontException;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.graphics.fonts.Font;
25 import android.graphics.fonts.FontFamily;
26 import android.graphics.fonts.FontManager;
27 import android.graphics.fonts.FontUpdateRequest;
28 import android.graphics.fonts.FontVariationAxis;
29 import android.graphics.fonts.SystemFonts;
30 import android.os.Binder;
31 import android.os.Build;
32 import android.os.ParcelFileDescriptor;
33 import android.os.Process;
34 import android.os.ShellCommand;
35 import android.text.FontConfig;
36 import android.util.IndentingPrintWriter;
37 import android.util.Slog;
38 import android.util.Xml;
39 
40 import com.android.internal.util.DumpUtils;
41 import com.android.modules.utils.TypedXmlPullParser;
42 
43 import org.xmlpull.v1.XmlPullParser;
44 import org.xmlpull.v1.XmlPullParserException;
45 
46 import java.io.File;
47 import java.io.FileInputStream;
48 import java.io.IOException;
49 import java.io.InputStream;
50 import java.io.PrintWriter;
51 import java.time.LocalDateTime;
52 import java.time.ZoneOffset;
53 import java.time.format.DateTimeFormatter;
54 import java.util.ArrayList;
55 import java.util.Arrays;
56 import java.util.Collections;
57 import java.util.List;
58 import java.util.Map;
59 
60 /**
61  * A shell command implementation of font service.
62  */
63 public class FontManagerShellCommand extends ShellCommand {
64     private static final String TAG = "FontManagerShellCommand";
65 
66     /**
67      * The maximum size of signature file.  This is just to avoid potential abuse.
68      *
69      * This is copied from VerityUtils.java.
70      */
71     private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192;
72 
73     @NonNull private final FontManagerService mService;
74 
FontManagerShellCommand(@onNull FontManagerService service)75     FontManagerShellCommand(@NonNull FontManagerService service) {
76         mService = service;
77     }
78 
79     @Override
onCommand(String cmd)80     public int onCommand(String cmd) {
81         final int callingUid = Binder.getCallingUid();
82         if (callingUid != Process.ROOT_UID && callingUid != Process.SHELL_UID) {
83             // Do not change this string since this string is expected in the CTS.
84             getErrPrintWriter().println("Only shell or root user can execute font command.");
85             return 1;
86         }
87         return execCommand(this, cmd);
88     }
89 
90     @Override
onHelp()91     public void onHelp() {
92         PrintWriter w = getOutPrintWriter();
93         w.println("Font service (font) commands");
94         w.println("help");
95         w.println("    Print this help text.");
96         w.println();
97         w.println("dump [family name]");
98         w.println("    Dump all font files in the specified family name.");
99         w.println("    Dump current system font configuration if no family name was specified.");
100         w.println();
101         w.println("update [font file path] [signature file path]");
102         w.println("    Update installed font files with new font file.");
103         w.println();
104         w.println("update-family [family definition XML path]");
105         w.println("    Update font families with the new definitions.");
106         w.println();
107         w.println("install-debug-cert [cert file path]");
108         w.println("    Install debug certificate file. This command can be used only on");
109         w.println("    debuggable device with root user.");
110         w.println();
111         w.println("clear");
112         w.println("    Remove all installed font files and reset to the initial state.");
113         w.println();
114         w.println("restart");
115         w.println("    Restart FontManagerService emulating device reboot.");
116         w.println("    WARNING: this is not a safe operation. Other processes may misbehave if");
117         w.println("    they are using fonts updated by FontManagerService.");
118         w.println("    This command exists merely for testing.");
119         w.println();
120         w.println("status");
121         w.println("    Prints status of current system font configuration.");
122     }
123 
dumpAll(@onNull IndentingPrintWriter w)124     /* package */ void dumpAll(@NonNull IndentingPrintWriter w) {
125         FontConfig fontConfig = mService.getSystemFontConfig();
126         dumpFontConfig(w, fontConfig);
127     }
128 
dumpSingleFontConfig( @onNull IndentingPrintWriter w, @NonNull FontConfig.Font font )129     private void dumpSingleFontConfig(
130             @NonNull IndentingPrintWriter w,
131             @NonNull FontConfig.Font font
132     ) {
133         StringBuilder sb = new StringBuilder();
134         sb.append("style = ");
135         sb.append(font.getStyle());
136         sb.append(", path = ");
137         sb.append(font.getFile().getAbsolutePath());
138         if (font.getTtcIndex() != 0) {
139             sb.append(", index = ");
140             sb.append(font.getTtcIndex());
141         }
142         if (!font.getFontVariationSettings().isEmpty()) {
143             sb.append(", axes = ");
144             sb.append(font.getFontVariationSettings());
145         }
146         if (font.getFontFamilyName() != null) {
147             sb.append(", fallback = ");
148             sb.append(font.getFontFamilyName());
149         }
150         w.println(sb.toString());
151 
152         if (font.getOriginalFile() != null) {
153             w.increaseIndent();
154             w.println("Font is updated from " + font.getOriginalFile());
155             w.decreaseIndent();
156         }
157     }
158 
dumpFontConfig( @onNull IndentingPrintWriter w, @NonNull FontConfig fontConfig )159     private void dumpFontConfig(
160             @NonNull IndentingPrintWriter w,
161             @NonNull FontConfig fontConfig
162     ) {
163         // Dump named font family first.
164         List<FontConfig.FontFamily> families = fontConfig.getFontFamilies();
165 
166         // Dump FontFamilyList
167         w.println("Named Family List");
168         w.increaseIndent();
169         List<FontConfig.NamedFamilyList> namedFamilyLists = fontConfig.getNamedFamilyLists();
170         for (int i = 0; i < namedFamilyLists.size(); ++i) {
171             final FontConfig.NamedFamilyList namedFamilyList = namedFamilyLists.get(i);
172             w.println("Named Family (" + namedFamilyList.getName() + ")");
173             w.increaseIndent();
174             final List<FontConfig.FontFamily> namedFamilies = namedFamilyList.getFamilies();
175             for (int j = 0; j < namedFamilies.size(); ++j) {
176                 final FontConfig.FontFamily family = namedFamilies.get(j);
177 
178                 w.println("Family");
179                 final List<FontConfig.Font> fonts = family.getFontList();
180                 w.increaseIndent();
181                 for (int k = 0; k < fonts.size(); ++k) {
182                     dumpSingleFontConfig(w, fonts.get(k));
183                 }
184                 w.decreaseIndent();
185             }
186             w.decreaseIndent();
187         }
188         w.decreaseIndent();
189 
190         // Dump Fallback fonts.
191         w.println("Dump Fallback Families");
192         w.increaseIndent();
193         int c = 0;
194         for (int i = 0; i < families.size(); ++i) {
195             final FontConfig.FontFamily family = families.get(i);
196 
197             // Skip named font family since they are already dumped.
198             if (family.getName() != null) continue;
199 
200             StringBuilder sb = new StringBuilder("Fallback Family [");
201             sb.append(c++);
202             sb.append("]: lang=\"");
203             sb.append(family.getLocaleList().toLanguageTags());
204             sb.append("\"");
205             if (family.getVariant() != FontConfig.FontFamily.VARIANT_DEFAULT) {
206                 sb.append(", variant=");
207                 switch (family.getVariant()) {
208                     case FontConfig.FontFamily.VARIANT_COMPACT:
209                         sb.append("Compact");
210                         break;
211                     case FontConfig.FontFamily.VARIANT_ELEGANT:
212                         sb.append("Elegant");
213                         break;
214                     default:
215                         sb.append("Unknown");
216                         break;
217                 }
218             }
219             w.println(sb.toString());
220 
221             final List<FontConfig.Font> fonts = family.getFontList();
222             w.increaseIndent();
223             for (int j = 0; j < fonts.size(); ++j) {
224                 dumpSingleFontConfig(w, fonts.get(j));
225             }
226             w.decreaseIndent();
227         }
228         w.decreaseIndent();
229 
230         // Dump aliases
231         w.println("Dump Family Aliases");
232         w.increaseIndent();
233         List<FontConfig.Alias> aliases = fontConfig.getAliases();
234         for (int i = 0; i < aliases.size(); ++i) {
235             final FontConfig.Alias alias = aliases.get(i);
236             w.println("alias = " + alias.getName() + ", reference = " + alias.getOriginal()
237                     + ", width = " + alias.getWeight());
238         }
239         w.decreaseIndent();
240     }
241 
dumpFallback(@onNull IndentingPrintWriter writer, @NonNull FontFamily[] families)242     private void dumpFallback(@NonNull IndentingPrintWriter writer,
243             @NonNull FontFamily[] families) {
244         for (FontFamily family : families) {
245             dumpFamily(writer, family);
246         }
247     }
248 
dumpFamily(@onNull IndentingPrintWriter writer, @NonNull FontFamily family)249     private void dumpFamily(@NonNull IndentingPrintWriter writer, @NonNull FontFamily family) {
250         StringBuilder sb = new StringBuilder("Family:");
251         if (family.getLangTags() != null) {
252             sb.append(" langTag = ");
253             sb.append(family.getLangTags());
254         }
255         if (family.getVariant() != FontConfig.FontFamily.VARIANT_DEFAULT) {
256             sb.append(" variant = ");
257             switch (family.getVariant()) {
258                 case FontConfig.FontFamily.VARIANT_COMPACT:
259                     sb.append("Compact");
260                     break;
261                 case FontConfig.FontFamily.VARIANT_ELEGANT:
262                     sb.append("Elegant");
263                     break;
264                 default:
265                     sb.append("UNKNOWN");
266                     break;
267             }
268 
269         }
270         writer.println(sb.toString());
271         for (int i = 0; i < family.getSize(); ++i) {
272             writer.increaseIndent();
273             try {
274                 dumpFont(writer, family.getFont(i));
275             } finally {
276                 writer.decreaseIndent();
277             }
278         }
279     }
280 
dumpFont(@onNull IndentingPrintWriter writer, @NonNull Font font)281     private void dumpFont(@NonNull IndentingPrintWriter writer, @NonNull Font font) {
282         File file = font.getFile();
283         StringBuilder sb = new StringBuilder();
284         sb.append(font.getStyle());
285         sb.append(", path = ");
286         sb.append(file == null ? "[Not a file]" : file.getAbsolutePath());
287         if (font.getTtcIndex() != 0) {
288             sb.append(", index = ");
289             sb.append(font.getTtcIndex());
290         }
291         FontVariationAxis[] axes = font.getAxes();
292         if (axes != null && axes.length != 0) {
293             sb.append(", axes = \"");
294             sb.append(FontVariationAxis.toFontVariationSettings(axes));
295             sb.append("\"");
296         }
297         writer.println(sb.toString());
298     }
299 
writeCommandResult(ShellCommand shell, SystemFontException e)300     private void writeCommandResult(ShellCommand shell, SystemFontException e) {
301         // Print short summary to the stderr.
302         PrintWriter pw = shell.getErrPrintWriter();
303         pw.println(e.getErrorCode());
304         pw.println(e.getMessage());
305 
306         // Dump full stack trace to logcat.
307 
308         Slog.e(TAG, "Command failed: " + Arrays.toString(shell.getAllArgs()), e);
309     }
310 
dump(ShellCommand shell)311     private int dump(ShellCommand shell) {
312         final Context ctx = mService.getContext();
313 
314         if (!DumpUtils.checkDumpPermission(ctx, TAG, shell.getErrPrintWriter())) {
315             return 1;
316         }
317         final IndentingPrintWriter writer =
318                 new IndentingPrintWriter(shell.getOutPrintWriter(), "  ");
319         String nextArg = shell.getNextArg();
320         FontConfig fontConfig = mService.getSystemFontConfig();
321         if (nextArg == null) {
322             dumpFontConfig(writer, fontConfig);
323         } else {
324             final Map<String, FontFamily[]> fallbackMap =
325                     SystemFonts.buildSystemFallback(fontConfig);
326             FontFamily[] families = fallbackMap.get(nextArg);
327             if (families == null) {
328                 writer.println("Font Family \"" + nextArg + "\" not found");
329             } else {
330                 dumpFallback(writer, families);
331             }
332         }
333         return 0;
334     }
335 
installCert(ShellCommand shell)336     private int installCert(ShellCommand shell) throws SystemFontException {
337         if (!Build.IS_DEBUGGABLE) {
338             throw new SecurityException("Only debuggable device can add debug certificate");
339         }
340         if (Binder.getCallingUid() != Process.ROOT_UID) {
341             throw new SecurityException("Only root can add debug certificate");
342         }
343 
344         String certPath = shell.getNextArg();
345         if (certPath == null) {
346             throw new SystemFontException(
347                     FontManager.RESULT_ERROR_INVALID_DEBUG_CERTIFICATE,
348                     "Cert file path argument is required.");
349         }
350         File file = new File(certPath);
351         if (!file.isFile()) {
352             throw new SystemFontException(
353                     FontManager.RESULT_ERROR_INVALID_DEBUG_CERTIFICATE,
354                     "Cert file (" + file + ") is not found");
355         }
356 
357         mService.addDebugCertificate(certPath);
358         mService.restart();
359         shell.getOutPrintWriter().println("Success");
360         return 0;
361     }
362 
update(ShellCommand shell)363     private int update(ShellCommand shell) throws SystemFontException {
364         String fontPath = shell.getNextArg();
365         if (fontPath == null) {
366             throw new SystemFontException(
367                     FontManager.RESULT_ERROR_INVALID_SHELL_ARGUMENT,
368                     "Font file path argument is required.");
369         }
370         String signaturePath = shell.getNextArg();
371         if (signaturePath == null) {
372             throw new SystemFontException(
373                     FontManager.RESULT_ERROR_INVALID_SHELL_ARGUMENT,
374                     "Signature file argument is required.");
375         }
376 
377         try (ParcelFileDescriptor fontFd = shell.openFileForSystem(fontPath, "r");
378              ParcelFileDescriptor sigFd = shell.openFileForSystem(signaturePath, "r")) {
379             if (fontFd == null) {
380                 throw new SystemFontException(
381                         FontManager.RESULT_ERROR_FAILED_TO_OPEN_FONT_FILE,
382                         "Failed to open font file");
383             }
384 
385             if (sigFd == null) {
386                 throw new SystemFontException(
387                         FontManager.RESULT_ERROR_FAILED_TO_OPEN_SIGNATURE_FILE,
388                         "Failed to open signature file");
389             }
390 
391             byte[] signature;
392             try (FileInputStream sigFis = new FileInputStream(sigFd.getFileDescriptor())) {
393                 int len = sigFis.available();
394                 if (len > MAX_SIGNATURE_FILE_SIZE_BYTES) {
395                     throw new SystemFontException(
396                             FontManager.RESULT_ERROR_SIGNATURE_TOO_LARGE,
397                             "Signature file is too large");
398                 }
399                 signature = new byte[len];
400                 if (sigFis.read(signature, 0, len) != len) {
401                     throw new SystemFontException(
402                             FontManager.RESULT_ERROR_INVALID_SIGNATURE_FILE,
403                             "Invalid read length");
404                 }
405             } catch (IOException e) {
406                 throw new SystemFontException(
407                         FontManager.RESULT_ERROR_INVALID_SIGNATURE_FILE,
408                         "Failed to read signature file.", e);
409             }
410             mService.update(
411                     -1, Collections.singletonList(new FontUpdateRequest(fontFd, signature)));
412         } catch (IOException e) {
413             // We should reach here only when close() threw IOException.
414             // shell.openFileForSystem() and FontManagerService.update() don't throw IOException.
415             Slog.w(TAG, "Error while closing files", e);
416         }
417 
418         shell.getOutPrintWriter().println("Success");  // TODO: Output more details.
419         return 0;
420     }
421 
updateFamily(ShellCommand shell)422     private int updateFamily(ShellCommand shell) throws SystemFontException {
423         String xmlPath = shell.getNextArg();
424         if (xmlPath == null) {
425             throw new SystemFontException(
426                     FontManager.RESULT_ERROR_INVALID_SHELL_ARGUMENT,
427                     "XML file path argument is required.");
428         }
429 
430         List<FontUpdateRequest> requests;
431         try (ParcelFileDescriptor xmlFd = shell.openFileForSystem(xmlPath, "r")) {
432             requests = parseFontFamilyUpdateXml(new FileInputStream(xmlFd.getFileDescriptor()));
433         } catch (IOException e) {
434             throw new SystemFontException(
435                     FontManager.RESULT_ERROR_FAILED_TO_OPEN_XML_FILE,
436                     "Failed to open XML file.", e);
437         }
438         mService.update(-1, requests);
439         shell.getOutPrintWriter().println("Success");
440         return 0;
441     }
442 
443     /**
444      * Parses XML representing {@link android.graphics.fonts.FontFamilyUpdateRequest}.
445      *
446      * <p>The format is like:
447      * <pre>{@code
448      *   <fontFamilyUpdateRequest>
449      *       <family name="family-name">
450      *           <font name="postScriptName"/>
451      *       </family>
452      *   </fontFamilyUpdateRequest>
453      * }</pre>
454      */
parseFontFamilyUpdateXml(InputStream inputStream)455     private static List<FontUpdateRequest> parseFontFamilyUpdateXml(InputStream inputStream)
456             throws SystemFontException {
457         try {
458             TypedXmlPullParser parser = Xml.resolvePullParser(inputStream);
459             List<FontUpdateRequest> requests = new ArrayList<>();
460             int type;
461             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
462                 if (type != XmlPullParser.START_TAG) {
463                     continue;
464                 }
465                 final int depth = parser.getDepth();
466                 final String tag = parser.getName();
467                 if (depth == 1) {
468                     if (!"fontFamilyUpdateRequest".equals(tag)) {
469                         throw new SystemFontException(FontManager.RESULT_ERROR_INVALID_XML,
470                                 "Expected <fontFamilyUpdateRequest> but got: " + tag);
471                     }
472                 } else if (depth == 2) {
473                     // TODO: Support including FontFileUpdateRequest
474                     if ("family".equals(tag)) {
475                         requests.add(new FontUpdateRequest(
476                                 FontUpdateRequest.Family.readFromXml(parser)));
477                     } else {
478                         throw new SystemFontException(FontManager.RESULT_ERROR_INVALID_XML,
479                                 "Expected <family> but got: " + tag);
480                     }
481                 }
482             }
483             return requests;
484         } catch (IOException | XmlPullParserException e) {
485             throw new SystemFontException(0, "Failed to parse xml", e);
486         }
487     }
488 
clear(ShellCommand shell)489     private int clear(ShellCommand shell) {
490         mService.clearUpdates();
491         shell.getOutPrintWriter().println("Success");
492         return 0;
493     }
494 
restart(ShellCommand shell)495     private int restart(ShellCommand shell) {
496         mService.restart();
497         shell.getOutPrintWriter().println("Success");
498         return 0;
499     }
500 
status(ShellCommand shell)501     private int status(ShellCommand shell) {
502         final IndentingPrintWriter writer =
503                 new IndentingPrintWriter(shell.getOutPrintWriter(), "  ");
504         FontConfig config = mService.getSystemFontConfig();
505 
506         writer.println("Current Version: " + config.getConfigVersion());
507         LocalDateTime dt = LocalDateTime.ofEpochSecond(config.getLastModifiedTimeMillis(), 0,
508                 ZoneOffset.UTC);
509         writer.println("Last Modified Date: " + dt.format(DateTimeFormatter.ISO_DATE_TIME));
510 
511         Map<String, File> fontFileMap = mService.getFontFileMap();
512         writer.println("Number of updated font files: " + fontFileMap.size());
513         return 0;
514     }
515 
execCommand(@onNull ShellCommand shell, @Nullable String cmd)516     private int execCommand(@NonNull ShellCommand shell, @Nullable String cmd) {
517         if (cmd == null) {
518             return shell.handleDefaultCommands(null);
519         }
520 
521         try {
522             switch (cmd) {
523                 case "dump":
524                     return dump(shell);
525                 case "update":
526                     return update(shell);
527                 case "update-family":
528                     return updateFamily(shell);
529                 case "clear":
530                     return clear(shell);
531                 case "restart":
532                     return restart(shell);
533                 case "status":
534                     return status(shell);
535                 case "install-debug-cert":
536                     return installCert(shell);
537                 default:
538                     return shell.handleDefaultCommands(cmd);
539             }
540         } catch (SystemFontException e) {
541             writeCommandResult(shell, e);
542             return 1;
543         }
544     }
545 }
546