1 /* 2 * Copyright (C) 2023 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.adservices.service.common; 18 19 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; 20 import static org.xmlpull.v1.XmlPullParser.END_TAG; 21 import static org.xmlpull.v1.XmlPullParser.START_TAG; 22 23 import android.annotation.NonNull; 24 import android.content.res.Resources; 25 import android.content.res.XmlResourceParser; 26 27 import androidx.annotation.Nullable; 28 29 import com.android.modules.utils.build.SdkLevel; 30 31 import org.xmlpull.v1.XmlPullParserException; 32 33 import java.io.IOException; 34 import java.util.NoSuchElementException; 35 import java.util.Objects; 36 import java.util.Optional; 37 38 /** Parsing utils for AndroidManifest XML. */ 39 public final class AndroidManifestConfigParser { 40 private static final String APPLICATION_TAG = "application"; 41 private static final String PROPERTY_TAG = "property"; 42 private static final String NAME_ATTRIBUTE = "name"; 43 private static final String RESOURCE_ATTRIBUTE = "resource"; 44 private static final String RESOLVED_RESOURCE_ID_PREFIX = "@"; 45 // The <property> is strictly expected to reside inside <application>, which must be defined 46 // inside <manifest>. Therefore, the expected depth for the AdServices config property is 3. 47 private static final int AD_SERVICES_CONFIG_PROPERTY_DEPTH = 3; 48 AndroidManifestConfigParser()49 private AndroidManifestConfigParser() { 50 // Prevent instantiation. 51 } 52 53 /** 54 * Parses an app's compiled AndroidManifest XML resource to obtain AdServices config resource 55 * ID. The property is expected to be defined strictly inside <application>, which can only be 56 * defined inside <manifest> at an overall element depth of 3 in the XML file. The parser is 57 * intentionally meant to be lightweight and doesn't attempt to validate anything in the XML 58 * file apart from the AdServices config property itself. Returns {@code null} if AdServices 59 * config's resource ID cannot be detected in the XML. 60 * 61 * @param parser the XmlResourceParser representing the app's AndroidManifest compiled XML file. 62 * @param resources the resources belonging to the app. 63 */ 64 @Nullable getAdServicesConfigResourceId( @onNull XmlResourceParser parser, @NonNull Resources resources)65 public static Integer getAdServicesConfigResourceId( 66 @NonNull XmlResourceParser parser, @NonNull Resources resources) 67 throws XmlPullParserException, IOException { 68 if (SdkLevel.isAtLeastS()) { 69 throw new IllegalStateException( 70 "Attempt to custom parse AndroidManifest on Android S+. Use " 71 + "PackageManager::getProperty to parse property instead!"); 72 } 73 Objects.requireNonNull(parser); 74 Objects.requireNonNull(resources); 75 76 boolean isInsideApplication = false; 77 int eventType = parser.next(); 78 while (eventType != END_DOCUMENT && !hasReachedEndOfApplication(parser)) { 79 if (eventType == START_TAG) { 80 if (!isInsideApplication && isTagType(parser, APPLICATION_TAG)) { 81 isInsideApplication = true; 82 } else if (isInsideApplication 83 && parser.getDepth() == AD_SERVICES_CONFIG_PROPERTY_DEPTH 84 && isTagType(parser, PROPERTY_TAG)) { 85 Optional<Integer> maybeResourceId = 86 getResourceIdIfAdServicesProperty(parser, resources); 87 if (maybeResourceId.isPresent()) return maybeResourceId.get(); 88 } 89 } 90 eventType = parser.next(); 91 } 92 93 return null; 94 } 95 hasReachedEndOfApplication(@onNull XmlResourceParser parser)96 private static boolean hasReachedEndOfApplication(@NonNull XmlResourceParser parser) 97 throws XmlPullParserException { 98 return parser.getEventType() == END_TAG && isTagType(parser, APPLICATION_TAG); 99 } 100 isTagType(@onNull XmlResourceParser parser, @NonNull String tag)101 private static boolean isTagType(@NonNull XmlResourceParser parser, @NonNull String tag) { 102 return tag.equals(parser.getName()); 103 } 104 getResourceIdIfAdServicesProperty( @onNull XmlResourceParser parser, @NonNull Resources resources)105 private static Optional<Integer> getResourceIdIfAdServicesProperty( 106 @NonNull XmlResourceParser parser, @NonNull Resources resources) { 107 Optional<String> resourceValue = Optional.empty(); 108 boolean isAdServicesProperty = false; 109 final int numAttributes = parser.getAttributeCount(); 110 for (int attrIndex = 0; attrIndex < numAttributes; attrIndex++) { 111 if (isAttributeType(parser, NAME_ATTRIBUTE, attrIndex)) { 112 final String attributeVal = parser.getAttributeValue(attrIndex); 113 isAdServicesProperty = isAdServicesPropertyName(attributeVal, resources); 114 if (!isAdServicesProperty) break; 115 } else if (isAttributeType(parser, RESOURCE_ATTRIBUTE, attrIndex)) { 116 final String attributeVal = parser.getAttributeValue(attrIndex); 117 resourceValue = Optional.ofNullable(attributeVal); 118 } 119 120 // Already extracted raw resource value belonging to AdServices config property. 121 // No need to check other attributes. 122 if (isAdServicesProperty && resourceValue.isPresent()) break; 123 } 124 125 if (!isAdServicesProperty) return Optional.empty(); 126 127 if (resourceValue.isEmpty()) { 128 throw new NoSuchElementException( 129 "Missing resource attribute in AdServices config property!"); 130 } 131 132 final String rawResourceVal = resourceValue.get(); 133 Objects.requireNonNull(rawResourceVal); 134 final Optional<Integer> resId = maybeGetResolvedResourceId(rawResourceVal.strip()); 135 if (resId.isEmpty()) { 136 throw new IllegalStateException( 137 "AdServices config property resource not resolved to a resource ID!"); 138 } 139 140 return resId; 141 } 142 isAttributeType( @onNull XmlResourceParser parser, @NonNull String attr, int attrIndex)143 private static boolean isAttributeType( 144 @NonNull XmlResourceParser parser, @NonNull String attr, int attrIndex) { 145 return attr.equals(parser.getAttributeName(attrIndex)); 146 } 147 isAdServicesPropertyName( @ullable String name, @NonNull Resources resources)148 private static boolean isAdServicesPropertyName( 149 @Nullable String name, @NonNull Resources resources) { 150 if (name == null) return false; 151 final String resolvedName = resolvePropertyName(name.strip(), resources); 152 return AppManifestConfigHelper.AD_SERVICES_CONFIG_PROPERTY.equals(resolvedName); 153 } 154 155 /** 156 * The android:name value can be defined in one of two ways: <property 157 * android:name="@string/property_name_ref" .. /> OR <property android:name="property_name" ../> 158 * 159 * <p>For option 1, property_name_ref is expected to be resolved to a resource ID in the form of 160 * "@<resourceId>" where resourceId is an integer. This will require an additional step in 161 * resolving the resourceId to the property_name itself. 162 * 163 * <p>In the case of option 2, we can return property_name as-is. 164 * 165 * @param name value of android:name to be processed. 166 * @param resources resources for the app. 167 */ resolvePropertyName(@onNull String name, @NonNull Resources resources)168 private static String resolvePropertyName(@NonNull String name, @NonNull Resources resources) { 169 final Optional<Integer> resId = maybeGetResolvedResourceId(name); 170 return resId.isPresent() ? resources.getString(resId.get()) : name; 171 } 172 maybeGetResolvedResourceId(@onNull String value)173 private static Optional<Integer> maybeGetResolvedResourceId(@NonNull String value) { 174 try { 175 // Values that are resolved to resource IDs in the compiled XML will look like 176 // "@902323". Ensure raw value can be converted into an int after omitting "@". 177 if (value.length() <= 1 || !value.startsWith(RESOLVED_RESOURCE_ID_PREFIX)) { 178 return Optional.empty(); 179 } 180 return Optional.of(Integer.parseInt(value.substring(1))); 181 } catch (Exception e) { 182 return Optional.empty(); 183 } 184 } 185 } 186