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