| /* |
| * Copyright (C) 2017 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.tools.lint.checks; |
| |
| import static com.android.SdkConstants.ANDROID_NS_NAME; |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.APPCOMPAT_LIB_ARTIFACT; |
| import static com.android.SdkConstants.APPCOMPAT_LIB_ARTIFACT_ID; |
| import static com.android.SdkConstants.APP_PREFIX; |
| import static com.android.SdkConstants.ATTR_FONT_PROVIDER_AUTHORITY; |
| import static com.android.SdkConstants.ATTR_FONT_PROVIDER_CERTS; |
| import static com.android.SdkConstants.ATTR_FONT_PROVIDER_PACKAGE; |
| import static com.android.SdkConstants.ATTR_FONT_PROVIDER_QUERY; |
| import static com.android.SdkConstants.AUTO_URI; |
| import static com.android.SdkConstants.SUPPORT_LIB_GROUP_ID; |
| import static com.android.SdkConstants.TAG_FONT; |
| import static com.android.SdkConstants.TAG_FONT_FAMILY; |
| import static com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_LOWER; |
| import static com.android.tools.lint.detector.api.Lint.coalesce; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.fonts.FontDetail; |
| import com.android.ide.common.fonts.FontFamily; |
| import com.android.ide.common.fonts.FontLoader; |
| import com.android.ide.common.fonts.FontProvider; |
| import com.android.ide.common.fonts.MutableFontDetail; |
| import com.android.ide.common.fonts.QueryParser; |
| import com.android.ide.common.repository.GradleCoordinate; |
| import com.android.resources.ResourceFolderType; |
| import com.android.sdklib.AndroidVersion; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Context; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.LintFix; |
| import com.android.tools.lint.detector.api.Location; |
| import com.android.tools.lint.detector.api.ResourceXmlDetector; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.XmlContext; |
| import com.android.tools.lint.model.LintModelLibrary; |
| import com.android.tools.lint.model.LintModelMavenName; |
| import com.android.tools.lint.model.LintModelVariant; |
| import com.android.utils.XmlUtils; |
| import com.google.common.base.Joiner; |
| import com.intellij.openapi.util.text.StringUtil; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| |
| public class FontDetector extends ResourceXmlDetector { |
| // TODO: Change this to the API version where we dont have to rely on appcompat for downloadable fonts loading at runtime. |
| public static final int FUTURE_API_VERSION_WHERE_DOWNLOADABLE_FONTS_WORK_IN_FRAMEWORK = |
| Integer.MAX_VALUE - 1; |
| |
| private static final Implementation IMPLEMENTATION = |
| new Implementation(FontDetector.class, Scope.RESOURCE_FILE_SCOPE); |
| |
| public static final Issue FONT_VALIDATION_WARNING = |
| Issue.create( |
| "FontValidationWarning", |
| "Validation of font files", |
| "Look for problems in various font files.", |
| Category.CORRECTNESS, |
| 9, |
| Severity.WARNING, |
| IMPLEMENTATION) |
| .addMoreInfo( |
| "https://developer.android.com/guide/topics/text/downloadable-fonts.html"); |
| |
| public static final Issue FONT_VALIDATION_ERROR = |
| Issue.create( |
| "FontValidationError", |
| "Validation of font files", |
| "Look for problems in various font files.", |
| Category.CORRECTNESS, |
| 8, |
| Severity.ERROR, |
| IMPLEMENTATION) |
| .addMoreInfo( |
| "https://developer.android.com/guide/topics/text/downloadable-fonts.html"); |
| |
| public static final GradleCoordinate MIN_APPSUPPORT_VERSION = |
| new GradleCoordinate(SUPPORT_LIB_GROUP_ID, APPCOMPAT_LIB_ARTIFACT_ID, "26.0.0"); |
| |
| private FontLoader mFontLoader; |
| |
| @Override |
| public boolean appliesTo(@NonNull ResourceFolderType folderType) { |
| return folderType == ResourceFolderType.FONT; |
| } |
| |
| @Override |
| public Collection<String> getApplicableElements() { |
| return Arrays.asList(TAG_FONT_FAMILY, TAG_FONT); |
| } |
| |
| @Override |
| public void beforeCheckRootProject(@NonNull Context context) { |
| if (mFontLoader == null) { |
| mFontLoader = FontLoader.getInstance(context.getClient().getSdkHome()); |
| } |
| } |
| |
| @Override |
| public void visitElement(@NonNull XmlContext context, @NonNull Element element) { |
| Attr authority = element.getAttributeNodeNS(ANDROID_URI, ATTR_FONT_PROVIDER_AUTHORITY); |
| Attr query = element.getAttributeNodeNS(ANDROID_URI, ATTR_FONT_PROVIDER_QUERY); |
| Attr androidPackage = element.getAttributeNodeNS(ANDROID_URI, ATTR_FONT_PROVIDER_PACKAGE); |
| Attr certs = element.getAttributeNodeNS(ANDROID_URI, ATTR_FONT_PROVIDER_CERTS); |
| |
| Attr appAuthority = element.getAttributeNodeNS(AUTO_URI, ATTR_FONT_PROVIDER_AUTHORITY); |
| Attr appQuery = element.getAttributeNodeNS(AUTO_URI, ATTR_FONT_PROVIDER_QUERY); |
| Attr appPackage = element.getAttributeNodeNS(AUTO_URI, ATTR_FONT_PROVIDER_PACKAGE); |
| Attr appCerts = element.getAttributeNodeNS(AUTO_URI, ATTR_FONT_PROVIDER_CERTS); |
| |
| Attr firstAndroidAttribute = coalesce(authority, query, androidPackage, certs); |
| Attr firstAppAttribute = coalesce(appAuthority, appQuery, appPackage, appCerts); |
| List<String> missingAndroidAttributes = |
| findMissingAttributes(authority, query, androidPackage, certs); |
| List<String> missingAppAttributes = |
| findMissingAttributes(appAuthority, appQuery, appPackage, appCerts); |
| |
| Element fontTag = XmlUtils.getFirstSubTagByName(element, TAG_FONT); |
| |
| AndroidVersion minSdk = context.getMainProject().getMinSdkVersion(); |
| boolean downloadableFontFile = coalesce(firstAndroidAttribute, firstAppAttribute) != null; |
| |
| if (downloadableFontFile) { |
| checkSupportLibraryVersion(context, element); |
| if (reportMisplacedFontTag(context, fontTag)) { |
| return; |
| } |
| if (minSdk.getApiLevel() |
| >= FUTURE_API_VERSION_WHERE_DOWNLOADABLE_FONTS_WORK_IN_FRAMEWORK) { |
| reportUnexpectedAttributeNamespace(context, firstAppAttribute, ANDROID_NS_NAME); |
| } else { |
| reportUnexpectedAttributeNamespace(context, firstAndroidAttribute, APP_PREFIX); |
| } |
| FontProvider provider = reportUnknownProvider(context, authority, appAuthority); |
| if (provider != null) { |
| reportUnknownPackage(context, androidPackage, appPackage, provider); |
| reportQueryProblem(context, query, appQuery, provider); |
| } |
| if (minSdk.getFeatureLevel() > AndroidVersion.VersionCodes.O_MR1) { |
| reportMissingAppAttribute( |
| context, |
| firstAndroidAttribute, |
| missingAndroidAttributes, |
| ANDROID_URI, |
| ANDROID_NS_NAME, |
| provider); |
| } else { |
| reportMissingAppAttribute( |
| context, |
| firstAppAttribute, |
| missingAppAttributes, |
| AUTO_URI, |
| APP_PREFIX, |
| provider); |
| } |
| } |
| } |
| |
| @NonNull |
| private static List<String> findMissingAttributes( |
| @Nullable Attr authority, |
| @Nullable Attr query, |
| @Nullable Attr packageName, |
| @Nullable Attr certs) { |
| if (authority != null && query != null && packageName != null && certs != null) { |
| return Collections.emptyList(); |
| } |
| List<String> missing = new ArrayList<>(); |
| if (authority == null) { |
| missing.add(ATTR_FONT_PROVIDER_AUTHORITY); |
| } |
| if (query == null) { |
| missing.add(ATTR_FONT_PROVIDER_QUERY); |
| } |
| if (packageName == null) { |
| missing.add(ATTR_FONT_PROVIDER_PACKAGE); |
| } |
| if (certs == null) { |
| missing.add(ATTR_FONT_PROVIDER_CERTS); |
| } |
| return missing; |
| } |
| |
| private static void checkSupportLibraryVersion( |
| @NonNull XmlContext context, @NonNull Element element) { |
| LintModelVariant variant = context.getMainProject().getBuildVariant(); |
| if (variant == null) { |
| return; |
| } |
| LintModelLibrary library = |
| variant.getMainArtifact().findCompileDependency(APPCOMPAT_LIB_ARTIFACT); |
| if (library == null) { |
| return; |
| } |
| |
| LintModelMavenName rc = library.getResolvedCoordinates(); |
| GradleCoordinate version = |
| new GradleCoordinate( |
| SUPPORT_LIB_GROUP_ID, APPCOMPAT_LIB_ARTIFACT_ID, rc.getVersion()); |
| if (COMPARE_PLUS_LOWER.compare(version, MIN_APPSUPPORT_VERSION) < 0) { |
| String message = |
| "Using version " |
| + version.getRevision() |
| + " of the " |
| + APPCOMPAT_LIB_ARTIFACT_ID |
| + " library. Required version for using downloadable fonts: " |
| + MIN_APPSUPPORT_VERSION.getRevision() |
| + " or higher."; |
| LintFix fix = LintFix.create().data(APPCOMPAT_LIB_ARTIFACT_ID); |
| reportError(context, element, message, context.getNameLocation(element), fix); |
| } |
| } |
| |
| private static boolean reportMisplacedFontTag( |
| @NonNull XmlContext context, @Nullable Element fontTag) { |
| if (fontTag == null) { |
| return false; |
| } |
| LintFix fix = |
| LintFix.create().replace().with("").range(context.getLocation(fontTag)).build(); |
| reportError( |
| context, |
| fontTag, |
| "A downloadable font cannot have a `<font>` sub tag", |
| context.getElementLocation(fontTag), |
| fix); |
| return true; |
| } |
| |
| private static void reportUnexpectedAttributeNamespace( |
| @NonNull XmlContext context, @Nullable Attr first, @NonNull String namespace) { |
| if (first != null) { |
| AndroidVersion minSdk = context.getMainProject().getMinSdkVersion(); |
| String message = |
| String.format( |
| "For `minSdkVersion`=%1$d only `%2$s:` attributes should be used", |
| minSdk.getApiLevel(), namespace); |
| LintFix fix = |
| LintFix.create().unset(first.getNamespaceURI(), first.getLocalName()).build(); |
| reportWarning(context, first, message, context.getLocation(first), fix); |
| } |
| } |
| |
| private void reportMissingAppAttribute( |
| @NonNull XmlContext context, |
| @Nullable Attr firstFontAttribute, |
| @NonNull List<String> missingAttributes, |
| @NonNull String namespaceUri, |
| @NonNull String namespacePrefix, |
| @Nullable FontProvider provider) { |
| if (firstFontAttribute != null && !missingAttributes.isEmpty()) { |
| String message = |
| String.format( |
| "Missing required %1$s: %2$s:%3$s", |
| StringUtil.pluralize("attribute", missingAttributes.size()), |
| namespacePrefix, |
| Joiner.on(", " + namespacePrefix + ":").join(missingAttributes)); |
| LintFix fix = makeMissingAttributeFix(missingAttributes, namespaceUri, provider); |
| Element element = firstFontAttribute.getOwnerElement(); |
| reportError(context, element, message, context.getElementLocation(element), fix); |
| } |
| } |
| |
| private LintFix makeMissingAttributeFix( |
| @NonNull List<String> missingAttributes, |
| @NonNull String namespaceUri, |
| @Nullable FontProvider provider) { |
| if (provider == null) { |
| provider = mFontLoader.findOnlyKnownProvider(); |
| } |
| |
| LintFix.GroupBuilder fix = fix().composite(); |
| for (String missingAttribute : missingAttributes) { |
| String value = generateNewValue(missingAttribute, provider); |
| if (value == null) { |
| fix.add(fix().set().todo(namespaceUri, missingAttribute).build()); |
| } else { |
| fix.add(fix().set(namespaceUri, missingAttribute, value).build()); |
| } |
| } |
| return fix.build(); |
| } |
| |
| @Nullable |
| private static String generateNewValue( |
| @NonNull String missingAttribute, @Nullable FontProvider provider) { |
| if (provider == null) { |
| return null; |
| } |
| switch (missingAttribute) { |
| case ATTR_FONT_PROVIDER_AUTHORITY: |
| return provider.getAuthority(); |
| case ATTR_FONT_PROVIDER_PACKAGE: |
| return provider.getPackageName(); |
| case ATTR_FONT_PROVIDER_CERTS: |
| return "@array/" + provider.getCertificateResourceName(); |
| default: |
| return null; |
| } |
| } |
| |
| @Nullable |
| private FontProvider reportUnknownProvider( |
| @NonNull XmlContext context, |
| @Nullable Attr attrAuthority, |
| @Nullable Attr attrAppAuthority) { |
| String authority = attrAuthority != null ? attrAuthority.getValue() : null; |
| String appAuthority = attrAppAuthority != null ? attrAppAuthority.getValue() : null; |
| FontProvider provider = null; |
| if (authority != null) { |
| provider = reportUnknownProvider(context, attrAuthority, authority); |
| } else if (appAuthority != null) { |
| provider = reportUnknownProvider(context, attrAppAuthority, appAuthority); |
| } |
| return provider; |
| } |
| |
| private FontProvider reportUnknownProvider( |
| @NonNull XmlContext context, @NonNull Attr attrAuthority, @NonNull String authority) { |
| FontProvider provider = mFontLoader.findProvider(authority); |
| if (provider != null) { |
| return provider; |
| } |
| LintFix fix = null; |
| FontProvider onlyKnownProvider = mFontLoader.findOnlyKnownProvider(); |
| if (onlyKnownProvider != null) { |
| fix = |
| fix().name("Replace with " + onlyKnownProvider.getAuthority()) |
| .replace() |
| .text(authority) |
| .with(onlyKnownProvider.getAuthority()) |
| .build(); |
| } |
| reportError( |
| context, |
| attrAuthority, |
| "Unknown font provider authority", |
| context.getValueLocation(attrAuthority), |
| fix); |
| return null; |
| } |
| |
| private static void reportUnknownPackage( |
| @NonNull XmlContext context, |
| @Nullable Attr attrAndroidPackage, |
| @Nullable Attr attrAppPackage, |
| @NonNull FontProvider provider) { |
| String androidPackage = attrAndroidPackage != null ? attrAndroidPackage.getValue() : null; |
| String appPackage = attrAppPackage != null ? attrAppPackage.getValue() : null; |
| if (androidPackage != null && !androidPackage.equals(provider.getPackageName())) { |
| reportUnknownPackage(context, attrAndroidPackage, androidPackage, provider); |
| } else if (appPackage != null && !appPackage.equals(provider.getPackageName())) { |
| reportUnknownPackage(context, attrAppPackage, appPackage, provider); |
| } |
| } |
| |
| private static void reportUnknownPackage( |
| @NonNull XmlContext context, |
| @NonNull Attr attrPackage, |
| @NonNull String packageName, |
| @NonNull FontProvider provider) { |
| if (provider.getPackageName().equals(packageName)) { |
| return; |
| } |
| LintFix fix = |
| LintFix.create() |
| .name("Replace with " + provider.getPackageName()) |
| .replace() |
| .text(packageName) |
| .with(provider.getPackageName()) |
| .build(); |
| reportError( |
| context, |
| attrPackage, |
| "Unexpected font provider package", |
| context.getValueLocation(attrPackage), |
| fix); |
| } |
| |
| private void reportQueryProblem( |
| @NonNull XmlContext context, |
| @Nullable Attr androidQueryAttr, |
| @Nullable Attr appQueryAttr, |
| @NonNull FontProvider provider) { |
| String androidQuery = androidQueryAttr != null ? androidQueryAttr.getValue() : null; |
| String appQuery = appQueryAttr != null ? appQueryAttr.getValue() : null; |
| if (androidQuery != null) { |
| reportQueryProblem(context, androidQueryAttr, androidQuery, provider); |
| } else if (appQuery != null) { |
| reportQueryProblem(context, appQueryAttr, appQuery, provider); |
| } |
| } |
| |
| private void reportQueryProblem( |
| @NonNull XmlContext context, |
| @NonNull Attr queryAttr, |
| @NonNull String query, |
| @NonNull FontProvider provider) { |
| if (query.isEmpty()) { |
| LintFix fix = |
| fix().set().todo(queryAttr.getNamespaceURI(), queryAttr.getLocalName()).build(); |
| reportError( |
| context, |
| queryAttr, |
| "Missing provider query", |
| context.getLocation(queryAttr), |
| fix); |
| return; |
| } |
| try { |
| QueryParser.DownloadableParseResult result = |
| QueryParser.parseDownloadableFont( |
| provider.getAuthority(), XmlUtils.fromXmlAttributeValue(query)); |
| if (!mFontLoader.fontsLoaded()) { |
| return; |
| } |
| for (String fontName : result.getFonts().keySet()) { |
| FontFamily family = mFontLoader.findFont(provider, fontName); |
| if (family == null) { |
| reportError( |
| context, |
| queryAttr, |
| "Unknown font: " + fontName, |
| context.getValueLocation(queryAttr), |
| null); |
| } else { |
| for (MutableFontDetail detail : result.getFonts().get(fontName)) { |
| FontDetail best = detail.findBestMatch(family.getFonts()); |
| if (best != null && detail.match(best) != 0) { |
| LintFix fix = null; |
| if (result.getFonts().size() == 1) { |
| String better = best.generateQuery(detail.getExact()); |
| |
| fix = |
| fix().name("Replace with closest font: " + better) |
| .set( |
| queryAttr.getNamespaceURI(), |
| queryAttr.getLocalName(), |
| better) |
| .build(); |
| } |
| if (detail.getExact()) { |
| reportError( |
| context, |
| queryAttr, |
| "No exact match found for: " + fontName, |
| context.getValueLocation(queryAttr), |
| fix); |
| } else { |
| reportWarning( |
| context, |
| queryAttr, |
| "No exact match found for: " + fontName, |
| context.getValueLocation(queryAttr), |
| fix); |
| } |
| } |
| } |
| } |
| } |
| } catch (QueryParser.FontQueryParserError ex) { |
| reportError( |
| context, queryAttr, ex.getMessage(), context.getValueLocation(queryAttr), null); |
| } |
| } |
| |
| private static void reportError( |
| @NonNull XmlContext context, |
| @NonNull Node node, |
| @NonNull String message, |
| @NonNull Location location, |
| @Nullable LintFix fix) { |
| if (!context.isEnabled(FONT_VALIDATION_ERROR)) { |
| return; |
| } |
| context.report(FONT_VALIDATION_ERROR, node, location, message, fix); |
| } |
| |
| private static void reportWarning( |
| @NonNull XmlContext context, |
| @NonNull Node node, |
| @NonNull String message, |
| @NonNull Location location, |
| @Nullable LintFix fix) { |
| if (!context.isEnabled(FONT_VALIDATION_WARNING)) { |
| return; |
| } |
| context.report(FONT_VALIDATION_WARNING, node, location, message, fix); |
| } |
| } |