com.android.tools.lint.checks.AndroidAutoDetector Maven / Gradle / Ivy
/*
* Copyright (C) 2015 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_URI;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.TAG_INTENT_FILTER;
import static com.android.SdkConstants.TAG_SERVICE;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import static com.android.xml.AndroidManifest.NODE_APPLICATION;
import static com.android.xml.AndroidManifest.NODE_METADATA;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.client.api.JavaParser;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
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.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import lombok.ast.ClassDeclaration;
import lombok.ast.ForwardingAstVisitor;
import lombok.ast.MethodDeclaration;
import lombok.ast.Node;
/**
* Detector for Android Auto issues.
* Uses a {@code } tag with a {@code name="com.google.android.gms.car.application"}
* as a trigger for validating Automotive specific issues.
*/
public class AndroidAutoDetector extends ResourceXmlDetector
implements Detector.XmlScanner, Detector.JavaScanner {
public static final Implementation IMPL = new Implementation(
AndroidAutoDetector.class,
EnumSet.of(Scope.RESOURCE_FILE, Scope.MANIFEST, Scope.JAVA_FILE),
Scope.RESOURCE_FILE_SCOPE);
/** Invalid attribute for uses tag.*/
public static final Issue INVALID_USES_TAG_ISSUE = Issue.create(
"InvalidUsesTagAttribute", //$NON-NLS-1$
"Invalid `name` attribute for `uses` element.",
"The element in `` should contain a " +
"valid value for the `name` attribute.\n" +
"Valid values are `media` or `notification`.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/start/index.html#auto-metadata");
/** Missing MediaBrowserService action */
public static final Issue MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE = Issue.create(
"MissingMediaBrowserServiceIntentFilter", //$NON-NLS-1$
"Missing intent-filter with action `android.media.browse.MediaBrowserService`.",
"An Automotive Media App requires an exported service that extends " +
"`android.service.media.MediaBrowserService` with an " +
"`intent-filter` for the action `android.media.browse.MediaBrowserService` " +
"to be able to browse and play media.\n" +
"To do this, add\n" +
"``\n" +
" ` `\n" +
"` `\n to the service that extends " +
"`android.service.media.MediaBrowserService`",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/audio/index.html#config_manifest");
/** Missing intent-filter for Media Search. */
public static final Issue MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH = Issue.create(
"MissingIntentFilterForMediaSearch", //$NON-NLS-1$
"Missing intent-filter with action `android.media.action.MEDIA_PLAY_FROM_SEARCH`",
"To support voice searches on Android Auto, you should also register an " +
"`intent-filter` for the action `android.media.action.MEDIA_PLAY_FROM_SEARCH`" +
".\nTo do this, add\n" +
"``\n" +
" ` `\n" +
"` `\n" +
"to your `` or ``.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/audio/index.html#support_voice");
/** Missing implementation of MediaSession.Callback#onPlayFromSearch*/
public static final Issue MISSING_ON_PLAY_FROM_SEARCH = Issue.create(
"MissingOnPlayFromSearch", //$NON-NLS-1$
"Missing `onPlayFromSearch`.",
"To support voice searches on Android Auto, in addition to adding an " +
"`intent-filter` for the action `onPlayFromSearch`," +
" you also need to override and implement " +
"`onPlayFromSearch(String query, Bundle bundle)`",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPL).addMoreInfo(
"https://developer.android.com/training/auto/audio/index.html#support_voice");
private static final String CAR_APPLICATION_METADATA_NAME =
"com.google.android.gms.car.application"; //$NON-NLS-1$
private static final String VAL_NAME_MEDIA = "media"; //$NON-NLS-1$
private static final String VAL_NAME_NOTIFICATION = "notification"; //$NON-NLS-1$
private static final String TAG_AUTOMOTIVE_APP = "automotiveApp"; //$NON-NLS-1$
private static final String ATTR_RESOURCE = "resource"; //$NON-NLS-1$
private static final String TAG_USES = "uses"; //$NON-NLS-1$
private static final String ACTION_MEDIA_BROWSER_SERVICE =
"android.media.browse.MediaBrowserService"; //$NON-NLS-1$
private static final String ACTION_MEDIA_PLAY_FROM_SEARCH =
"android.media.action.MEDIA_PLAY_FROM_SEARCH"; //$NON-NLS-1$
private static final String CLASS_MEDIA_SESSION_CALLBACK =
"android.media.session.MediaSession.Callback"; //$NON-NLS-1$
private static final String CLASS_V4MEDIA_SESSION_COMPAT_CALLBACK =
"android.support.v4.media.session.MediaSessionCompat.Callback"; //$NON-NLS-1$
private static final String METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH =
"onPlayFromSearch"; //$NON-NLS-1$
private static final String STRING_ARG = "java.lang.String"; //$NON-NLS-1$
private static final String BUNDLE_ARG = "android.os.Bundle"; //$NON-NLS-1$
/**
* Indicates whether we identified that the current app is an automotive app and
* that we should validate all the automotive specific issues.
*/
private boolean mDoAutomotiveAppCheck;
/** Indicates that a {@link #ACTION_MEDIA_BROWSER_SERVICE} intent-filter action was found. */
private boolean mMediaIntentFilterFound;
/** Indicates that a {@link #ACTION_MEDIA_PLAY_FROM_SEARCH} intent-filter action was found. */
private boolean mMediaSearchIntentFilterFound;
/** The resource file name deduced by the meta-data resource value */
private String mAutomotiveResourceFileName;
/** Indicates whether this app is an automotive Media App. */
private boolean mIsAutomotiveMediaApp;
/** {@link Location.Handle} to the application element */
private Location.Handle mMainApplicationHandle;
/** Constructs a new {@link AndroidAutoDetector} check */
public AndroidAutoDetector() {
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
// We only need to check the meta data resource file in res/xml if any.
return folderType == ResourceFolderType.XML;
}
@Override
public Collection getApplicableElements() {
return Arrays.asList(
TAG_AUTOMOTIVE_APP, // Root element of a declared automotive descriptor.
NODE_METADATA, // meta-data from AndroidManifest.xml
TAG_SERVICE, // service from AndroidManifest.xml
TAG_INTENT_FILTER, // Any declared intent-filter from AndroidManifest.xml
NODE_APPLICATION // Used for storing the application element/location.
);
}
@Override
public void beforeCheckProject(@NonNull Context context) {
mIsAutomotiveMediaApp = false;
mAutomotiveResourceFileName = null;
mMediaIntentFilterFound = false;
mMediaSearchIntentFilterFound = false;
}
@Override
@NonNull
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tagName = element.getTagName();
if (NODE_METADATA.equals(tagName) && !mDoAutomotiveAppCheck) {
checkAutoMetadataTag(element);
} else if (TAG_AUTOMOTIVE_APP.equals(tagName)) {
checkAutomotiveAppElement(context, element);
} else if (NODE_APPLICATION.equals(tagName)) {
// Disable reporting the error if the Issue was suppressed at
// the application level.
if (context.getMainProject() == context.getProject()
&& !context.getProject().isLibrary()) {
mMainApplicationHandle = context.createLocationHandle(element);
mMainApplicationHandle.setClientData(element);
}
} else if (TAG_SERVICE.equals(tagName)) {
checkServiceForBrowserServiceIntentFilter(element);
} else if (TAG_INTENT_FILTER.equals(tagName)) {
checkForMediaSearchIntentFilter(element);
}
}
private void checkAutoMetadataTag(Element element) {
String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (CAR_APPLICATION_METADATA_NAME.equals(name)) {
String autoFileName = element.getAttributeNS(ANDROID_URI, ATTR_RESOURCE);
if (autoFileName != null && autoFileName.startsWith("@xml/")) { //$NON-NLS-1$
// Store the fact that we need to check all the auto issues.
mDoAutomotiveAppCheck = true;
mAutomotiveResourceFileName =
autoFileName.substring("@xml/".length()) + DOT_XML; //$NON-NLS-1$
}
}
}
private void checkAutomotiveAppElement(XmlContext context, Element element) {
// Indicates whether the current file matches the resource that was registered
// in AndroidManifest.xml.
boolean isMetadataResource =
mAutomotiveResourceFileName != null
&& mAutomotiveResourceFileName.equals(context.file.getName());
for (Element child : LintUtils.getChildren(element)) {
if (TAG_USES.equals(child.getTagName())) {
String attrValue = child.getAttribute(ATTR_NAME);
if (VAL_NAME_MEDIA.equals(attrValue)) {
mIsAutomotiveMediaApp |= isMetadataResource;
} else if (!VAL_NAME_NOTIFICATION.equals(attrValue)
&& context.isEnabled(INVALID_USES_TAG_ISSUE)) {
// Error invalid value for attribute.
Attr node = child.getAttributeNode(ATTR_NAME);
if (node == null) {
// no name specified
continue;
}
context.report(INVALID_USES_TAG_ISSUE, node,
context.getLocation(node),
"Expecting one of `" + VAL_NAME_MEDIA + "` or `" +
VAL_NAME_NOTIFICATION + "` for the name " +
"attribute in " + TAG_USES + " tag.");
}
}
}
// Report any errors that we have collected that can be shown to the user
// once we determine that this is an Automotive Media App.
if (mIsAutomotiveMediaApp
&& !context.getProject().isLibrary()
&& mMainApplicationHandle != null
&& mDoAutomotiveAppCheck) {
Element node = (Element) mMainApplicationHandle.getClientData();
if (!mMediaIntentFilterFound
&& context.isEnabled(MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE)) {
context.report(MISSING_MEDIA_BROWSER_SERVICE_ACTION_ISSUE, node,
mMainApplicationHandle.resolve(),
"Missing `intent-filter` for action " +
"`android.media.browse.MediaBrowserService` that is required for " +
"android auto support");
}
if (!mMediaSearchIntentFilterFound
&& context.isEnabled(MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH)) {
context.report(MISSING_INTENT_FILTER_FOR_MEDIA_SEARCH, node,
mMainApplicationHandle.resolve(),
"Missing `intent-filter` for action " +
"`android.media.action.MEDIA_PLAY_FROM_SEARCH`.");
}
}
}
private void checkServiceForBrowserServiceIntentFilter(Element element) {
if (TAG_SERVICE.equals(element.getTagName())
&& !mMediaIntentFilterFound) {
for (Element child : LintUtils.getChildren(element)) {
String tagName = child.getTagName();
if (TAG_INTENT_FILTER.equals(tagName)) {
for (Element filterChild : LintUtils.getChildren(child)) {
if (NODE_ACTION.equals(filterChild.getTagName())) {
String actionValue = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (ACTION_MEDIA_BROWSER_SERVICE.equals(actionValue)) {
mMediaIntentFilterFound = true;
return;
}
}
}
}
}
}
}
private void checkForMediaSearchIntentFilter(Element element) {
if (!mMediaSearchIntentFilterFound) {
for (Element filterChild : LintUtils.getChildren(element)) {
if (NODE_ACTION.equals(filterChild.getTagName())) {
String actionValue = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (ACTION_MEDIA_PLAY_FROM_SEARCH.equals(actionValue)) {
mMediaSearchIntentFilterFound = true;
break;
}
}
}
}
}
// Implementation of the JavaScanner
@Override
@Nullable
public List applicableSuperClasses() {
// We currently enable scanning only for media apps.
return mIsAutomotiveMediaApp ?
Arrays.asList(CLASS_MEDIA_SESSION_CALLBACK,
CLASS_V4MEDIA_SESSION_COMPAT_CALLBACK)
: null;
}
@Override
public void checkClass(@NonNull JavaContext context, @Nullable ClassDeclaration declaration,
@NonNull Node node, @NonNull JavaParser.ResolvedClass resolvedClass) {
// Only check classes that are not declared abstract.
if (declaration != null && (resolvedClass.getModifiers() & Modifier.ABSTRACT) == 0) {
MediaSessionCallbackVisitor visitor = new MediaSessionCallbackVisitor(context);
declaration.accept(visitor);
if (!visitor.isPlayFromSearchMethodFound()
&& context.isEnabled(MISSING_ON_PLAY_FROM_SEARCH)) {
context.report(MISSING_ON_PLAY_FROM_SEARCH, declaration.astName(),
context.getLocation(declaration.astName()),
"This class does not override `" +
METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH + "` from `MediaSession.Callback`" +
" The method should be overridden and implemented to support " +
"Voice search on Android Auto.");
}
}
}
@Override
@Nullable
public List getApplicableMethodNames() {
return Collections.singletonList(METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH);
}
/**
* A Visitor class to search for {@code MediaSession.Callback#onPlayFromSearch(..)}
* method declaration.
*/
private static class MediaSessionCallbackVisitor extends ForwardingAstVisitor {
private final JavaContext mContext;
private boolean mOnPlayFromSearchFound;
public MediaSessionCallbackVisitor(JavaContext context) {
this.mContext = context;
}
public boolean isPlayFromSearchMethodFound() {
return mOnPlayFromSearchFound;
}
@Override
public boolean visitMethodDeclaration(MethodDeclaration node) {
JavaParser.ResolvedNode result = mContext.resolve(node);
if (result != null
&& METHOD_MEDIA_SESSION_PLAY_FROM_SEARCH.equals(result.getName())
&& result instanceof JavaParser.ResolvedMethod) {
JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) result;
if (method.getArgumentCount() == 2) {
JavaParser.TypeDescriptor firstArg = method.getArgumentType(0);
JavaParser.TypeDescriptor secondArg = method.getArgumentType(1);
if (firstArg.getTypeClass() != null
&& firstArg.getTypeClass().matches(STRING_ARG)
&& secondArg.getTypeClass() != null
&& secondArg.getTypeClass().matches(BUNDLE_ARG)) {
mOnPlayFromSearchFound = true;
}
}
}
return super.visitMethodDeclaration(node);
}
}
// Used by AS to show errors.
@NonNull
public static String[] getAllowedAutomotiveAppTypes() {
return new String[]{VAL_NAME_MEDIA, VAL_NAME_NOTIFICATION};
}
}