blob: ae9c64b4800684738fad0bbe8a8eda307f041d81 [file] [log] [blame]
/*
* Copyright (C) 2020 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 com.android.server.graphics.fonts;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.content.Context;
import android.graphics.Typeface;
import android.graphics.fonts.FontFamily;
import android.graphics.fonts.FontManager;
import android.graphics.fonts.FontUpdateRequest;
import android.graphics.fonts.SystemFonts;
import android.os.ParcelFileDescriptor;
import android.os.ResultReceiver;
import android.os.SharedMemory;
import android.os.ShellCallback;
import android.system.ErrnoException;
import android.text.FontConfig;
import android.util.AndroidException;
import android.util.ArrayMap;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.graphics.fonts.IFontManager;
import com.android.internal.security.VerityUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
import com.android.server.LocalServices;
import com.android.server.SystemService;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.DirectByteBuffer;
import java.nio.NioUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** A service for managing system fonts. */
public final class FontManagerService extends IFontManager.Stub {
private static final String TAG = "FontManagerService";
private static final String FONT_FILES_DIR = "/data/fonts/files";
private static final String CONFIG_XML_FILE = "/data/fonts/config/config.xml";
@RequiresPermission(Manifest.permission.UPDATE_FONTS)
@Override
public FontConfig getFontConfig() {
getContext().enforceCallingPermission(Manifest.permission.UPDATE_FONTS,
"UPDATE_FONTS permission required.");
return getSystemFontConfig();
}
@RequiresPermission(Manifest.permission.UPDATE_FONTS)
@Override
public int updateFontFamily(@NonNull List<FontUpdateRequest> requests, int baseVersion) {
try {
Preconditions.checkArgumentNonnegative(baseVersion);
Objects.requireNonNull(requests);
getContext().enforceCallingPermission(Manifest.permission.UPDATE_FONTS,
"UPDATE_FONTS permission required.");
try {
update(baseVersion, requests);
return FontManager.RESULT_SUCCESS;
} catch (SystemFontException e) {
Slog.e(TAG, "Failed to update font family", e);
return e.getErrorCode();
}
} finally {
closeFileDescriptors(requests);
}
}
private static void closeFileDescriptors(@Nullable List<FontUpdateRequest> requests) {
// Make sure we close every passed FD, even if 'requests' is constructed incorrectly and
// some fields are null.
if (requests == null) return;
for (FontUpdateRequest request : requests) {
if (request == null) continue;
ParcelFileDescriptor fd = request.getFd();
if (fd == null) continue;
try {
fd.close();
} catch (IOException e) {
Slog.w(TAG, "Failed to close fd", e);
}
}
}
/* package */ static class SystemFontException extends AndroidException {
private final int mErrorCode;
SystemFontException(@FontManager.ResultCode int errorCode, String msg, Throwable cause) {
super(msg, cause);
mErrorCode = errorCode;
}
SystemFontException(int errorCode, String msg) {
super(msg);
mErrorCode = errorCode;
}
@FontManager.ResultCode
int getErrorCode() {
return mErrorCode;
}
}
/** Class to manage FontManagerService's lifecycle. */
public static final class Lifecycle extends SystemService {
private final FontManagerService mService;
public Lifecycle(@NonNull Context context, boolean safeMode) {
super(context);
mService = new FontManagerService(context, safeMode);
}
@Override
public void onStart() {
LocalServices.addService(FontManagerInternal.class,
new FontManagerInternal() {
@Override
@Nullable
public SharedMemory getSerializedSystemFontMap() {
if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) {
return null;
}
return mService.getCurrentFontMap();
}
});
publishBinderService(Context.FONT_SERVICE, mService);
}
}
private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil {
@Override
public boolean hasFsverity(String filePath) {
return VerityUtils.hasFsverity(filePath);
}
@Override
public void setUpFsverity(String filePath, byte[] pkcs7Signature) throws IOException {
VerityUtils.setUpFsverity(filePath, pkcs7Signature);
}
@Override
public boolean rename(File src, File dest) {
// rename system call preserves fs-verity bit.
return src.renameTo(dest);
}
}
@NonNull
private final Context mContext;
private final Object mUpdatableFontDirLock = new Object();
@GuardedBy("mUpdatableFontDirLock")
@Nullable
private final UpdatableFontDir mUpdatableFontDir;
// mSerializedFontMapLock can be acquired while holding mUpdatableFontDirLock.
// mUpdatableFontDirLock should not be newly acquired while holding mSerializedFontMapLock.
private final Object mSerializedFontMapLock = new Object();
@GuardedBy("mSerializedFontMapLock")
@Nullable
private SharedMemory mSerializedFontMap = null;
private FontManagerService(Context context, boolean safeMode) {
if (safeMode) {
Slog.i(TAG, "Entering safe mode. Deleting all font updates.");
UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE));
}
mContext = context;
mUpdatableFontDir = createUpdatableFontDir(safeMode);
initialize();
}
@Nullable
private static UpdatableFontDir createUpdatableFontDir(boolean safeMode) {
// Never read updatable font files in safe mode.
if (safeMode) return null;
// If apk verity is supported, fs-verity should be available.
if (!VerityUtils.isFsVeritySupported()) return null;
return new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser(),
new FsverityUtilImpl(), new File(CONFIG_XML_FILE));
}
private void initialize() {
synchronized (mUpdatableFontDirLock) {
if (mUpdatableFontDir == null) {
setSerializedFontMap(serializeSystemServerFontMap());
return;
}
mUpdatableFontDir.loadFontFileMap();
updateSerializedFontMap();
}
}
@NonNull
public Context getContext() {
return mContext;
}
@Nullable /* package */ SharedMemory getCurrentFontMap() {
synchronized (mSerializedFontMapLock) {
return mSerializedFontMap;
}
}
/* package */ void update(int baseVersion, List<FontUpdateRequest> requests)
throws SystemFontException {
if (mUpdatableFontDir == null) {
throw new SystemFontException(
FontManager.RESULT_ERROR_FONT_UPDATER_DISABLED,
"The font updater is disabled.");
}
synchronized (mUpdatableFontDirLock) {
// baseVersion == -1 only happens from shell command. This is filtered and treated as
// error from SystemApi call.
if (baseVersion != -1 && mUpdatableFontDir.getConfigVersion() != baseVersion) {
throw new SystemFontException(
FontManager.RESULT_ERROR_VERSION_MISMATCH,
"The base config version is older than current.");
}
mUpdatableFontDir.update(requests);
updateSerializedFontMap();
}
}
/**
* Clears all updates and restarts FontManagerService.
*
* <p>CAUTION: this method is not safe. Existing processes may crash due to missing font files.
* This method is only for {@link FontManagerShellCommand}.
*/
/* package */ void clearUpdates() {
UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE));
initialize();
}
/**
* Restarts FontManagerService, removing not-the-latest font files.
*
* <p>CAUTION: this method is not safe. Existing processes may crash due to missing font files.
* This method is only for {@link FontManagerShellCommand}.
*/
/* package */ void restart() {
initialize();
}
/* package */ Map<String, File> getFontFileMap() {
if (mUpdatableFontDir == null) {
return Collections.emptyMap();
}
synchronized (mUpdatableFontDirLock) {
return mUpdatableFontDir.getPostScriptMap();
}
}
@Override
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter writer,
@Nullable String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
new FontManagerShellCommand(this).dumpAll(new IndentingPrintWriter(writer, " "));
}
@Override
public void onShellCommand(@Nullable FileDescriptor in,
@Nullable FileDescriptor out,
@Nullable FileDescriptor err,
@NonNull String[] args,
@Nullable ShellCallback callback,
@NonNull ResultReceiver result) {
new FontManagerShellCommand(this).exec(this, in, out, err, args, callback, result);
}
/**
* Returns an active system font configuration.
*/
public @NonNull FontConfig getSystemFontConfig() {
if (mUpdatableFontDir == null) {
return SystemFonts.getSystemPreinstalledFontConfig();
}
synchronized (mUpdatableFontDirLock) {
return mUpdatableFontDir.getSystemFontConfig();
}
}
/**
* Makes new serialized font map data and updates mSerializedFontMap.
*/
private void updateSerializedFontMap() {
SharedMemory serializedFontMap = serializeFontMap(getSystemFontConfig());
if (serializedFontMap == null) {
// Fallback to the preloaded config.
serializedFontMap = serializeSystemServerFontMap();
}
setSerializedFontMap(serializedFontMap);
}
@Nullable
private static SharedMemory serializeFontMap(FontConfig fontConfig) {
final ArrayMap<String, ByteBuffer> bufferCache = new ArrayMap<>();
try {
final Map<String, FontFamily[]> fallback =
SystemFonts.buildSystemFallback(fontConfig, bufferCache);
final Map<String, Typeface> typefaceMap =
SystemFonts.buildSystemTypefaces(fontConfig, fallback);
return Typeface.serializeFontMap(typefaceMap);
} catch (IOException | ErrnoException e) {
Slog.w(TAG, "Failed to serialize updatable font map. "
+ "Retrying with system image fonts.", e);
return null;
} finally {
// Unmap buffers promptly, as we map a lot of files and may hit mmap limit before
// GC collects ByteBuffers and unmaps them.
for (ByteBuffer buffer : bufferCache.values()) {
if (buffer instanceof DirectByteBuffer) {
NioUtils.freeDirectBuffer(buffer);
}
}
}
}
@Nullable
private static SharedMemory serializeSystemServerFontMap() {
try {
return Typeface.serializeFontMap(Typeface.getSystemFontMap());
} catch (IOException | ErrnoException e) {
Slog.e(TAG, "Failed to serialize SystemServer system font map", e);
return null;
}
}
private void setSerializedFontMap(SharedMemory serializedFontMap) {
SharedMemory oldFontMap = null;
synchronized (mSerializedFontMapLock) {
oldFontMap = mSerializedFontMap;
mSerializedFontMap = serializedFontMap;
}
if (oldFontMap != null) {
oldFontMap.close();
}
}
}