| /* |
| * 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.internal.content.om; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.pm.PackagePartitions; |
| import android.os.Build; |
| import android.os.Trace; |
| import android.util.ArrayMap; |
| import android.util.IndentingPrintWriter; |
| import android.util.Log; |
| |
| import com.android.apex.ApexInfo; |
| import com.android.apex.XmlParser; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.content.om.OverlayConfigParser.OverlayPartition; |
| import com.android.internal.content.om.OverlayConfigParser.ParsedConfiguration; |
| import com.android.internal.content.om.OverlayScanner.ParsedOverlayInfo; |
| import com.android.internal.util.Preconditions; |
| import com.android.internal.util.function.TriConsumer; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.SAXException; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.Supplier; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| |
| /** |
| * Responsible for reading overlay configuration files and handling queries of overlay mutability, |
| * default-enabled state, and priority. |
| * |
| * @see OverlayConfigParser |
| */ |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) |
| public class OverlayConfig { |
| static final String TAG = "OverlayConfig"; |
| |
| // The default priority of an overlay that has not been configured. Overlays with default |
| // priority have a higher precedence than configured overlays. |
| @VisibleForTesting |
| public static final int DEFAULT_PRIORITY = Integer.MAX_VALUE; |
| |
| public static final String PARTITION_ORDER_FILE_PATH = "/product/overlay/partition_order.xml"; |
| |
| @VisibleForTesting |
| public static final class Configuration { |
| @Nullable |
| public final ParsedConfiguration parsedConfig; |
| |
| public final int configIndex; |
| |
| public Configuration(@Nullable ParsedConfiguration parsedConfig, int configIndex) { |
| this.parsedConfig = parsedConfig; |
| this.configIndex = configIndex; |
| } |
| } |
| |
| /** |
| * Interface for providing information on scanned packages. |
| * TODO(147840005): Remove this when android:isStatic and android:priority are fully deprecated |
| */ |
| public interface PackageProvider { |
| |
| /** Performs the given action for each package. */ |
| void forEachPackage(TriConsumer<Package, Boolean, File> p); |
| |
| interface Package { |
| |
| String getBaseApkPath(); |
| |
| int getOverlayPriority(); |
| |
| String getOverlayTarget(); |
| |
| String getPackageName(); |
| |
| int getTargetSdkVersion(); |
| |
| boolean isOverlayIsStatic(); |
| } |
| } |
| |
| private static final Comparator<ParsedConfiguration> sStaticOverlayComparator = (c1, c2) -> { |
| final ParsedOverlayInfo o1 = c1.parsedInfo; |
| final ParsedOverlayInfo o2 = c2.parsedInfo; |
| Preconditions.checkArgument(o1.isStatic && o2.isStatic, |
| "attempted to sort non-static overlay"); |
| |
| if (!o1.targetPackageName.equals(o2.targetPackageName)) { |
| return o1.targetPackageName.compareTo(o2.targetPackageName); |
| } |
| |
| final int comparedPriority = o1.priority - o2.priority; |
| return comparedPriority == 0 ? o1.path.compareTo(o2.path) : comparedPriority; |
| }; |
| |
| // Map of overlay package name to configured overlay settings |
| private final ArrayMap<String, Configuration> mConfigurations = new ArrayMap<>(); |
| |
| // Singleton instance only assigned in system server |
| private static OverlayConfig sInstance; |
| |
| private final String mPartitionOrder; |
| |
| private final boolean mIsDefaultPartitionOrder; |
| |
| @VisibleForTesting |
| public OverlayConfig(@Nullable File rootDirectory, |
| @Nullable Supplier<OverlayScanner> scannerFactory, |
| @Nullable PackageProvider packageProvider) { |
| Preconditions.checkArgument((scannerFactory == null) != (packageProvider == null), |
| "scannerFactory and packageProvider cannot be both null or both non-null"); |
| |
| final ArrayList<OverlayPartition> partitions; |
| if (rootDirectory == null) { |
| partitions = new ArrayList<>( |
| PackagePartitions.getOrderedPartitions(OverlayPartition::new)); |
| } else { |
| // Rebase the system partitions and settings file on the specified root directory. |
| partitions = new ArrayList<>(PackagePartitions.getOrderedPartitions( |
| p -> new OverlayPartition( |
| new File(rootDirectory, p.getNonConicalFolder().getPath()), |
| p))); |
| } |
| mIsDefaultPartitionOrder = !sortPartitions(PARTITION_ORDER_FILE_PATH, partitions); |
| mPartitionOrder = generatePartitionOrderString(partitions); |
| |
| ArrayMap<Integer, List<String>> activeApexesPerPartition = getActiveApexes(partitions); |
| |
| final Map<String, ParsedOverlayInfo> packageManagerOverlayInfos = |
| packageProvider == null ? null : getOverlayPackageInfos(packageProvider); |
| |
| final ArrayList<ParsedConfiguration> overlays = new ArrayList<>(); |
| for (int i = 0, n = partitions.size(); i < n; i++) { |
| final OverlayPartition partition = partitions.get(i); |
| final OverlayScanner scanner = (scannerFactory == null) ? null : scannerFactory.get(); |
| final ArrayList<ParsedConfiguration> partitionOverlays = |
| OverlayConfigParser.getConfigurations(partition, scanner, |
| packageManagerOverlayInfos, |
| activeApexesPerPartition.getOrDefault(partition.type, |
| Collections.emptyList())); |
| if (partitionOverlays != null) { |
| overlays.addAll(partitionOverlays); |
| continue; |
| } |
| |
| // If the configuration file is not present, then use android:isStatic and |
| // android:priority to configure the overlays in the partition. |
| // TODO(147840005): Remove converting static overlays to immutable, default-enabled |
| // overlays when android:siStatic and android:priority are fully deprecated. |
| final ArrayList<ParsedOverlayInfo> partitionOverlayInfos; |
| if (scannerFactory != null) { |
| partitionOverlayInfos = new ArrayList<>(scanner.getAllParsedInfos()); |
| } else { |
| // Filter out overlays not present in the partition. |
| partitionOverlayInfos = new ArrayList<>(packageManagerOverlayInfos.values()); |
| for (int j = partitionOverlayInfos.size() - 1; j >= 0; j--) { |
| if (!partition.containsFile(partitionOverlayInfos.get(j) |
| .getOriginalPartitionPath())) { |
| partitionOverlayInfos.remove(j); |
| } |
| } |
| } |
| |
| // Static overlays are configured as immutable, default-enabled overlays. |
| final ArrayList<ParsedConfiguration> partitionConfigs = new ArrayList<>(); |
| for (int j = 0, m = partitionOverlayInfos.size(); j < m; j++) { |
| final ParsedOverlayInfo p = partitionOverlayInfos.get(j); |
| if (p.isStatic) { |
| partitionConfigs.add(new ParsedConfiguration(p.packageName, |
| true /* enabled */, false /* mutable */, partition.policy, p, null)); |
| } |
| } |
| |
| partitionConfigs.sort(sStaticOverlayComparator); |
| overlays.addAll(partitionConfigs); |
| } |
| |
| for (int i = 0, n = overlays.size(); i < n; i++) { |
| // Add the configurations to a map so definitions of an overlay in an earlier |
| // partition can be replaced by an overlay with the same package name in a later |
| // partition. |
| final ParsedConfiguration config = overlays.get(i); |
| mConfigurations.put(config.packageName, new Configuration(config, i)); |
| } |
| } |
| |
| private static String generatePartitionOrderString(List<OverlayPartition> partitions) { |
| if (partitions == null || partitions.size() == 0) { |
| return ""; |
| } |
| StringBuilder partitionOrder = new StringBuilder(); |
| partitionOrder.append(partitions.get(0).getName()); |
| for (int i = 1; i < partitions.size(); i++) { |
| partitionOrder.append(", ").append(partitions.get(i).getName()); |
| } |
| return partitionOrder.toString(); |
| } |
| |
| private static boolean parseAndValidatePartitionsOrderXml(String partitionOrderFilePath, |
| Map<String, Integer> orderMap, List<OverlayPartition> partitions) { |
| try { |
| File file = new File(partitionOrderFilePath); |
| if (!file.exists()) { |
| Log.w(TAG, "partition_order.xml does not exist."); |
| return false; |
| } |
| var dbFactory = DocumentBuilderFactory.newInstance(); |
| DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); |
| Document doc = dBuilder.parse(file); |
| doc.getDocumentElement().normalize(); |
| |
| Element root = doc.getDocumentElement(); |
| if (!root.getNodeName().equals("partition-order")) { |
| Log.w(TAG, "Invalid partition_order.xml, " |
| + "xml root element is not partition-order"); |
| return false; |
| } |
| |
| NodeList partitionList = doc.getElementsByTagName("partition"); |
| for (int order = 0; order < partitionList.getLength(); order++) { |
| Node partitionNode = partitionList.item(order); |
| if (partitionNode.getNodeType() == Node.ELEMENT_NODE) { |
| Element partitionElement = (Element) partitionNode; |
| String partitionName = partitionElement.getAttribute("name"); |
| if (orderMap.containsKey(partitionName)) { |
| Log.w(TAG, "Invalid partition_order.xml, " |
| + "it has duplicate partition: " + partitionName); |
| return false; |
| } |
| orderMap.put(partitionName, order); |
| } |
| } |
| |
| if (orderMap.keySet().size() != partitions.size()) { |
| Log.w(TAG, "Invalid partition_order.xml, partition_order.xml has " |
| + orderMap.keySet().size() + " partitions, " |
| + "which is different from SYSTEM_PARTITIONS"); |
| return false; |
| } |
| for (int i = 0; i < partitions.size(); i++) { |
| if (!orderMap.keySet().contains(partitions.get(i).getName())) { |
| Log.w(TAG, "Invalid Parsing partition_order.xml, " |
| + "partition_order.xml does not have partition: " |
| + partitions.get(i).getName()); |
| return false; |
| } |
| } |
| } catch (ParserConfigurationException | SAXException | IOException e) { |
| Log.w(TAG, "Parsing or validating partition_order.xml failed, " |
| + "exception thrown: " + e.getMessage()); |
| return false; |
| } |
| Log.i(TAG, "Sorting partitions in the specified order from partitions_order.xml"); |
| return true; |
| } |
| |
| /** |
| * Sort partitions by order in partition_order.xml if the file exists. |
| * |
| * @hide |
| */ |
| @VisibleForTesting |
| public static boolean sortPartitions(String partitionOrderFilePath, |
| List<OverlayPartition> partitions) { |
| Map<String, Integer> orderMap = new HashMap<>(); |
| if (!parseAndValidatePartitionsOrderXml(partitionOrderFilePath, orderMap, partitions)) { |
| return false; |
| } |
| |
| Comparator<OverlayPartition> partitionComparator = Comparator.comparingInt( |
| o -> orderMap.get(o.getName())); |
| Collections.sort(partitions, partitionComparator); |
| |
| return true; |
| } |
| |
| /** |
| * Creates an instance of OverlayConfig for use in the zygote process. |
| * This instance will not include information of static overlays existing outside of a partition |
| * overlay directory. |
| */ |
| @NonNull |
| public static OverlayConfig getZygoteInstance() { |
| Trace.traceBegin(Trace.TRACE_TAG_RRO, "OverlayConfig#getZygoteInstance"); |
| try { |
| return new OverlayConfig(null /* rootDirectory */, OverlayScanner::new, |
| null /* packageProvider */); |
| } finally { |
| Trace.traceEnd(Trace.TRACE_TAG_RRO); |
| } |
| } |
| |
| /** |
| * Initializes a singleton instance for use in the system process. |
| * Can only be called once. This instance is cached so future invocations of |
| * {@link #getSystemInstance()} will return the initialized instance. |
| */ |
| @NonNull |
| public static OverlayConfig initializeSystemInstance(PackageProvider packageProvider) { |
| Trace.traceBegin(Trace.TRACE_TAG_RRO, "OverlayConfig#initializeSystemInstance"); |
| try { |
| sInstance = new OverlayConfig(null, null, packageProvider); |
| } finally { |
| Trace.traceEnd(Trace.TRACE_TAG_RRO); |
| } |
| return sInstance; |
| } |
| |
| /** |
| * Retrieves the singleton instance initialized by |
| * {@link #initializeSystemInstance(PackageProvider)}. |
| */ |
| @NonNull |
| public static OverlayConfig getSystemInstance() { |
| if (sInstance == null) { |
| throw new IllegalStateException("System instance not initialized"); |
| } |
| |
| return sInstance; |
| } |
| |
| @VisibleForTesting |
| @Nullable |
| public Configuration getConfiguration(@NonNull String packageName) { |
| return mConfigurations.get(packageName); |
| } |
| |
| /** |
| * Returns whether the overlay is enabled by default. |
| * Overlays that are not configured are disabled by default. |
| * |
| * If an immutable overlay has its enabled state change, the new enabled state is applied to the |
| * overlay. |
| * |
| * When a mutable is first seen by the OverlayManagerService, the default-enabled state will be |
| * applied to the overlay. If the configured default-enabled state changes in a subsequent boot, |
| * the default-enabled state will not be applied to the overlay. |
| * |
| * The configured enabled state will only be applied when: |
| * <ul> |
| * <li> The device is factory reset |
| * <li> The overlay is removed from the device and added back to the device in a future OTA |
| * <li> The overlay changes its package name |
| * <li> The overlay changes its target package name or target overlayable name |
| * <li> An immutable overlay becomes mutable |
| * </ul> |
| */ |
| public boolean isEnabled(String packageName) { |
| final Configuration config = mConfigurations.get(packageName); |
| return config == null? OverlayConfigParser.DEFAULT_ENABLED_STATE |
| : config.parsedConfig.enabled; |
| } |
| |
| /** |
| * Returns whether the overlay is mutable and can have its enabled state changed dynamically. |
| * Overlays that are not configured are mutable. |
| */ |
| public boolean isMutable(String packageName) { |
| final Configuration config = mConfigurations.get(packageName); |
| return config == null ? OverlayConfigParser.DEFAULT_MUTABILITY |
| : config.parsedConfig.mutable; |
| } |
| |
| /** |
| * Returns an integer corresponding to the priority of the overlay. |
| * When multiple overlays override the same resource, the overlay with the highest priority will |
| * will have its value chosen. Overlays that are not configured have a priority of |
| * {@link Integer#MAX_VALUE}. |
| */ |
| public int getPriority(String packageName) { |
| final Configuration config = mConfigurations.get(packageName); |
| return config == null ? DEFAULT_PRIORITY : config.configIndex; |
| } |
| |
| @NonNull |
| private ArrayList<Configuration> getSortedOverlays() { |
| final ArrayList<Configuration> sortedOverlays = new ArrayList<>(); |
| for (int i = 0, n = mConfigurations.size(); i < n; i++) { |
| sortedOverlays.add(mConfigurations.valueAt(i)); |
| } |
| sortedOverlays.sort(Comparator.comparingInt(o -> o.configIndex)); |
| return sortedOverlays; |
| } |
| |
| @NonNull |
| private static Map<String, ParsedOverlayInfo> getOverlayPackageInfos( |
| @NonNull PackageProvider packageManager) { |
| final HashMap<String, ParsedOverlayInfo> overlays = new HashMap<>(); |
| packageManager.forEachPackage((PackageProvider.Package p, Boolean isSystem, |
| @Nullable File preInstalledApexPath) -> { |
| if (p.getOverlayTarget() != null && isSystem) { |
| overlays.put(p.getPackageName(), new ParsedOverlayInfo(p.getPackageName(), |
| p.getOverlayTarget(), p.getTargetSdkVersion(), p.isOverlayIsStatic(), |
| p.getOverlayPriority(), new File(p.getBaseApkPath()), |
| preInstalledApexPath)); |
| } |
| }); |
| return overlays; |
| } |
| |
| /** Returns a map of PartitionType to List of active APEX module names. */ |
| @NonNull |
| private static ArrayMap<Integer, List<String>> getActiveApexes( |
| @NonNull List<OverlayPartition> partitions) { |
| // An Overlay in an APEX, which is an update of an APEX in a given partition, |
| // is considered as belonging to that partition. |
| ArrayMap<Integer, List<String>> result = new ArrayMap<>(); |
| for (OverlayPartition partition : partitions) { |
| result.put(partition.type, new ArrayList<String>()); |
| } |
| // Read from apex-info-list because ApexManager is not accessible to zygote. |
| File apexInfoList = new File("/apex/apex-info-list.xml"); |
| if (apexInfoList.exists() && apexInfoList.canRead()) { |
| try (FileInputStream stream = new FileInputStream(apexInfoList)) { |
| List<ApexInfo> apexInfos = XmlParser.readApexInfoList(stream).getApexInfo(); |
| for (ApexInfo info : apexInfos) { |
| if (info.getIsActive()) { |
| for (OverlayPartition partition : partitions) { |
| if (partition.containsPath(info.getPreinstalledModulePath())) { |
| result.get(partition.type).add(info.getModuleName()); |
| break; |
| } |
| } |
| } |
| } |
| } catch (Exception e) { |
| Log.w(TAG, "Error reading apex-info-list: " + e); |
| } |
| } |
| return result; |
| } |
| |
| /** Represents a single call to idmap create-multiple. */ |
| @VisibleForTesting |
| public static class IdmapInvocation { |
| public final boolean enforceOverlayable; |
| public final String policy; |
| public final ArrayList<String> overlayPaths = new ArrayList<>(); |
| |
| IdmapInvocation(boolean enforceOverlayable, @NonNull String policy) { |
| this.enforceOverlayable = enforceOverlayable; |
| this.policy = policy; |
| } |
| |
| @Override |
| public String toString() { |
| return getClass().getSimpleName() + String.format("{enforceOverlayable=%s, policy=%s" |
| + ", overlayPaths=[%s]}", enforceOverlayable, policy, |
| String.join(", ", overlayPaths)); |
| } |
| } |
| |
| /** |
| * Retrieves a list of immutable framework overlays in order of least precedence to greatest |
| * precedence. |
| */ |
| @VisibleForTesting |
| public ArrayList<IdmapInvocation> getImmutableFrameworkOverlayIdmapInvocations() { |
| final ArrayList<IdmapInvocation> idmapInvocations = new ArrayList<>(); |
| final ArrayList<Configuration> sortedConfigs = getSortedOverlays(); |
| for (int i = 0, n = sortedConfigs.size(); i < n; i++) { |
| final Configuration overlay = sortedConfigs.get(i); |
| if (overlay.parsedConfig.mutable || !overlay.parsedConfig.enabled |
| || !"android".equals(overlay.parsedConfig.parsedInfo.targetPackageName)) { |
| continue; |
| } |
| |
| // Only enforce that overlays targeting packages with overlayable declarations abide by |
| // those declarations if the target sdk of the overlay is at least Q (when overlayable |
| // was introduced). |
| final boolean enforceOverlayable = overlay.parsedConfig.parsedInfo.targetSdkVersion |
| >= Build.VERSION_CODES.Q; |
| |
| // Determine if the idmap for the current overlay can be generated in the last idmap |
| // create-multiple invocation. |
| IdmapInvocation invocation = null; |
| if (!idmapInvocations.isEmpty()) { |
| final IdmapInvocation last = idmapInvocations.get(idmapInvocations.size() - 1); |
| if (last.enforceOverlayable == enforceOverlayable |
| && last.policy.equals(overlay.parsedConfig.policy)) { |
| invocation = last; |
| } |
| } |
| |
| if (invocation == null) { |
| invocation = new IdmapInvocation(enforceOverlayable, overlay.parsedConfig.policy); |
| idmapInvocations.add(invocation); |
| } |
| |
| invocation.overlayPaths.add(overlay.parsedConfig.parsedInfo.path.getAbsolutePath()); |
| } |
| return idmapInvocations; |
| } |
| |
| /** |
| * Creates idmap files for immutable overlays targeting the framework packages. Currently the |
| * android package is the only preloaded system package. Only the zygote can invoke this method. |
| * |
| * @return the paths of the created idmap files |
| */ |
| @NonNull |
| public String[] createImmutableFrameworkIdmapsInZygote() { |
| final String targetPath = "/system/framework/framework-res.apk"; |
| final ArrayList<String> idmapPaths = new ArrayList<>(); |
| final ArrayList<IdmapInvocation> idmapInvocations = |
| getImmutableFrameworkOverlayIdmapInvocations(); |
| |
| for (int i = 0, n = idmapInvocations.size(); i < n; i++) { |
| final IdmapInvocation invocation = idmapInvocations.get(i); |
| final String[] idmaps = createIdmap(targetPath, |
| invocation.overlayPaths.toArray(new String[0]), |
| new String[]{OverlayConfigParser.OverlayPartition.POLICY_PUBLIC, |
| invocation.policy}, |
| invocation.enforceOverlayable); |
| |
| if (idmaps == null) { |
| Log.w(TAG, "'idmap2 create-multiple' failed: no mutable=\"false\" overlays" |
| + " targeting \"android\" will be loaded"); |
| return new String[0]; |
| } |
| |
| idmapPaths.addAll(Arrays.asList(idmaps)); |
| } |
| |
| return idmapPaths.toArray(new String[0]); |
| } |
| |
| /** Dump all overlay configurations to the Printer. */ |
| public void dump(@NonNull PrintWriter writer) { |
| final IndentingPrintWriter ipw = new IndentingPrintWriter(writer); |
| ipw.println("Overlay configurations:"); |
| ipw.increaseIndent(); |
| |
| final ArrayList<Configuration> configurations = new ArrayList<>(mConfigurations.values()); |
| configurations.sort(Comparator.comparingInt(o -> o.configIndex)); |
| for (int i = 0; i < configurations.size(); i++) { |
| final Configuration configuration = configurations.get(i); |
| ipw.print(configuration.configIndex); |
| ipw.print(", "); |
| ipw.print(configuration.parsedConfig); |
| ipw.println(); |
| } |
| ipw.decreaseIndent(); |
| ipw.println(); |
| } |
| |
| /** |
| * For each overlay APK, this creates the idmap file that allows the overlay to override the |
| * target package. |
| * |
| * @return the paths of the created idmap |
| */ |
| private static native String[] createIdmap(@NonNull String targetPath, |
| @NonNull String[] overlayPath, @NonNull String[] policies, boolean enforceOverlayable); |
| |
| /** |
| * @hide |
| */ |
| public boolean isDefaultPartitionOrder() { |
| return mIsDefaultPartitionOrder; |
| } |
| |
| /** |
| * @hide |
| */ |
| public String getPartitionOrder() { |
| return mPartitionOrder; |
| } |
| |
| } |