blob: 7b5ed40ca01e6c18f83f0173aa258e60aab46c47 [file] [log] [blame]
/*
* 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_URI;
import static com.android.SdkConstants.ATTR_PERMISSION;
import static com.android.SdkConstants.CLASS_SERVICE;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TAG_SERVICE;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.detector.api.Category;
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.Lint;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.android.tools.lint.detector.api.UastLintUtils;
import com.android.utils.XmlUtils;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiVariable;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UClassLiteralExpression;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UReferenceExpression;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/** Checks looking for issues related to the JobScheduler API */
public class JobSchedulerDetector extends Detector implements SourceCodeScanner {
@SuppressWarnings("unchecked")
public static final Implementation IMPLEMENTATION =
new Implementation(JobSchedulerDetector.class, Scope.JAVA_FILE_SCOPE);
/** Issues that negatively affect battery life */
public static final Issue ISSUE =
Issue.create(
"JobSchedulerService",
"JobScheduler problems",
"This check looks for various common mistakes in using the "
+ "JobScheduler API: the service class must extend `JobService`, "
+ "the service must be registered in the manifest and the registration "
+ "must require the permission `android.permission.BIND_JOB_SERVICE`.",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("https://developer.android.com/topic/performance/scheduling.html")
.setAndroidSpecific(true);
private static final String CLASS_JOB_SERVICE = "android.app.job.JobService";
/** Constructs a new {@link JobSchedulerDetector} */
public JobSchedulerDetector() {}
@Nullable
@Override
public List<String> getApplicableConstructorTypes() {
return Collections.singletonList("android.app.job.JobInfo.Builder");
}
@Override
public void visitConstructor(
@NonNull JavaContext context,
@NonNull UCallExpression node,
@NonNull PsiMethod constructor) {
List<UExpression> arguments = node.getValueArguments();
if (arguments.size() < 2) {
return;
}
UExpression componentName = arguments.get(1);
if (componentName instanceof UReferenceExpression) {
PsiElement resolved = ((UReferenceExpression) componentName).resolve();
if (resolved instanceof PsiVariable) {
componentName =
UastLintUtils.findLastAssignment((PsiVariable) resolved, componentName);
}
}
if (!(componentName instanceof UCallExpression)) {
return;
}
UCallExpression call = (UCallExpression) componentName;
arguments = call.getValueArguments();
if (arguments.size() < 2) {
return;
}
UExpression typeReference = arguments.get(1);
if (!(typeReference instanceof UClassLiteralExpression)) {
return;
}
UClassLiteralExpression classRef = (UClassLiteralExpression) typeReference;
PsiType serviceType = classRef.getType();
if (!(serviceType instanceof PsiClassType)) {
return;
}
PsiClass serviceClass = ((PsiClassType) serviceType).resolve();
if (serviceClass == null) {
return;
}
JavaEvaluator evaluator = context.getEvaluator();
if (evaluator.inheritsFrom(serviceClass, CLASS_SERVICE, false)
&& !evaluator.inheritsFrom(serviceClass, CLASS_JOB_SERVICE, false)) {
String message =
String.format(
"Scheduled job class %1$s must extend android.app.job.JobService",
serviceClass.getName());
context.report(ISSUE, componentName, context.getLocation(componentName), message);
} else {
ensureBindServicePermission(context, serviceType.getCanonicalText(), classRef);
}
}
private static void ensureBindServicePermission(
@NonNull JavaContext context,
@NonNull String fqcn,
@NonNull UClassLiteralExpression typeReference) {
// Make sure the app has
// android:permission="android.permission.BIND_JOB_SERVICE"
// as well.
Project project = context.getMainProject();
Document mergedManifest = project.getMergedManifest();
if (mergedManifest == null) {
return;
}
Element manifest = mergedManifest.getDocumentElement();
if (manifest == null) {
return;
}
Element application = XmlUtils.getFirstSubTagByName(manifest, TAG_APPLICATION);
if (application == null) {
return;
}
Element service = XmlUtils.getFirstSubTagByName(application, TAG_SERVICE);
while (service != null) {
String name = Lint.resolveManifestName(service).replace('$', '.');
if (fqcn.equals(name)) {
// Check that it has the desired permission
String permission = service.getAttributeNS(ANDROID_URI, ATTR_PERMISSION);
if (!"android.permission.BIND_JOB_SERVICE".equals(permission)) {
Location location = context.getLocation(typeReference);
// Also report the manifest location, if possible
LintClient client = context.getClient();
Location secondary = client.findManifestSourceLocation(service);
if (secondary != null) {
location =
location.withSecondary(
secondary, "Service declaration here", false);
}
context.report(
ISSUE,
typeReference,
location,
"The manifest registration for this service does not declare "
+ "`android:permission=\"android.permission.BIND_JOB_SERVICE\"`");
}
return;
}
service = XmlUtils.getNextTagByName(service, TAG_SERVICE);
}
// Service not found in the manifest; flag it
context.report(
ISSUE,
typeReference,
context.getLocation(typeReference),
"Did not find a manifest registration for this service");
}
}