blob: bec08b006dc0393a1e06601b3926b5eb28017832 [file] [log] [blame]
/*
* Copyright (C) 2012 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.checks.ControlFlowGraph.Node;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.ClassScanner;
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.Lint;
import com.android.tools.lint.detector.api.LintFix;
import com.android.tools.lint.detector.api.Location;
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.intellij.psi.PsiMethod;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.jetbrains.uast.UCallExpression;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LdcInsnNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.AnalyzerException;
/**
* Checks for problems with wakelocks (such as failing to release them) which can lead to
* unnecessary battery usage.
*/
public class WakelockDetector extends Detector implements ClassScanner, SourceCodeScanner {
public static final String ANDROID_APP_ACTIVITY = "android/app/Activity";
/** Problems using wakelocks */
public static final Issue ISSUE =
Issue.create(
"Wakelock",
"Incorrect `WakeLock` usage",
"Failing to release a wakelock properly can keep the Android device in "
+ "a high power mode, which reduces battery life. There are several causes "
+ "of this, such as releasing the wake lock in `onDestroy()` instead of in "
+ "`onPause()`, failing to call `release()` in all possible code paths after "
+ "an `acquire()`, and so on.\n"
+ "\n"
+ "NOTE: If you are using the lock just to keep the screen on, you should "
+ "strongly consider using `FLAG_KEEP_SCREEN_ON` instead. This window flag "
+ "will be correctly managed by the platform as the user moves between "
+ "applications and doesn't require a special permission. See "
+ "https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_KEEP_SCREEN_ON.",
Category.PERFORMANCE,
9,
Severity.WARNING,
new Implementation(WakelockDetector.class, Scope.CLASS_FILE_SCOPE))
.setAndroidSpecific(true);
/** Using non-timeout version of wakelock acquire */
public static final Issue TIMEOUT =
Issue.create(
"WakelockTimeout",
"Using wakeLock without timeout",
"Wakelocks have two acquire methods: one with a timeout, and one without. "
+ "You should generally always use the one with a timeout. A typical "
+ "timeout is 10 minutes. If the task takes longer than it is critical "
+ "that it happens (i.e. can't use `JobScheduler`) then maybe they "
+ "should consider a foreground service instead (which is a stronger "
+ "run guarantee and lets the user know something long/important is "
+ "happening).",
Category.PERFORMANCE,
9,
Severity.WARNING,
new Implementation(WakelockDetector.class, Scope.JAVA_FILE_SCOPE))
.setAndroidSpecific(true);
private static final String WAKELOCK_OWNER = "android/os/PowerManager$WakeLock";
private static final String RELEASE_METHOD = "release";
private static final String ACQUIRE_METHOD = "acquire";
private static final String IS_HELD_METHOD = "isHeld";
private static final String POWER_MANAGER = "android/os/PowerManager";
private static final String NEW_WAKE_LOCK_METHOD = "newWakeLock";
/** Constructs a new {@link WakelockDetector} */
public WakelockDetector() {}
@Override
public void afterCheckRootProject(@NonNull Context context) {
if (mHasAcquire && !mHasRelease && context.getDriver().getPhase() == 1) {
// Gather positions of the acquire calls
context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE);
}
}
// ---- Implements ClassScanner ----
/** Whether any {@code acquire()} calls have been encountered */
private boolean mHasAcquire;
/** Whether any {@code release()} calls have been encountered */
private boolean mHasRelease;
@Override
@Nullable
public List<String> getApplicableCallNames() {
return Arrays.asList(ACQUIRE_METHOD, RELEASE_METHOD, NEW_WAKE_LOCK_METHOD);
}
@Override
public void checkCall(
@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull MethodInsnNode call) {
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (call.owner.equals(WAKELOCK_OWNER)) {
String name = call.name;
if (name.equals(ACQUIRE_METHOD)) {
if (call.desc.equals(
"(J)V")) { // acquire(long timeout) does not require a corresponding release
return;
}
mHasAcquire = true;
if (context.getDriver().getPhase() == 2) {
assert !mHasRelease;
context.report(
ISSUE,
method,
call,
context.getLocation(call),
"Found a wakelock `acquire()` but no `release()` calls anywhere");
} else {
assert context.getDriver().getPhase() == 1;
// Perform flow analysis in this method to see if we're
// performing an acquire/release block, where there are code paths
// between the acquire and release which can result in the
// release call not getting reached.
checkFlow(context, classNode, method, call);
}
} else if (name.equals(RELEASE_METHOD)) {
mHasRelease = true;
// See if the release is happening in an onDestroy method, in an
// activity.
if ("onDestroy".equals(method.name)
&& context.getDriver().isSubclassOf(classNode, ANDROID_APP_ACTIVITY)) {
context.report(
ISSUE,
method,
call,
context.getLocation(call),
"Wakelocks should be released in `onPause`, not `onDestroy`");
}
}
} else if (call.owner.equals(POWER_MANAGER)) {
if (call.name.equals(NEW_WAKE_LOCK_METHOD)) {
AbstractInsnNode prev = Lint.getPrevInstruction(call);
if (prev == null) {
return;
}
prev = Lint.getPrevInstruction(prev);
if (prev == null || prev.getOpcode() != Opcodes.LDC) {
return;
}
LdcInsnNode ldc = (LdcInsnNode) prev;
Object constant = ldc.cst;
if (constant instanceof Integer) {
int flag = (Integer) constant;
// Constant values are copied into the bytecode so we have to compare
// values; however, that means the values are part of the API
final int PARTIAL_WAKE_LOCK = 0x00000001;
final int ACQUIRE_CAUSES_WAKEUP = 0x10000000;
final int both = PARTIAL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP;
if ((flag & both) == both) {
context.report(
ISSUE,
method,
call,
context.getLocation(call),
"Should not set both `PARTIAL_WAKE_LOCK` and `ACQUIRE_CAUSES_WAKEUP`. "
+ "If you do not want the screen to turn on, get rid of "
+ "`ACQUIRE_CAUSES_WAKEUP`");
}
}
}
}
}
private static void checkFlow(
@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull MethodInsnNode acquire) {
final InsnList instructions = method.instructions;
MethodInsnNode release = null;
// Find release call
for (int i = 0, n = instructions.size(); i < n; i++) {
AbstractInsnNode instruction = instructions.get(i);
int type = instruction.getType();
if (type == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode call = (MethodInsnNode) instruction;
if (call.name.equals(RELEASE_METHOD) && call.owner.equals(WAKELOCK_OWNER)) {
release = call;
break;
}
}
}
if (release == null) {
// Didn't find both acquire and release in this method; no point in doing
// local flow analysis
return;
}
try {
MyGraph graph = new MyGraph();
ControlFlowGraph.create(graph, classNode, method);
int status = dfs(graph.getNode(acquire));
if ((status & SEEN_RETURN) != 0) {
String message;
if ((status & SEEN_EXCEPTION) != 0) {
message = "The `release()` call is not always reached (via exceptional flow)";
} else {
message = "The `release()` call is not always reached";
}
context.report(ISSUE, method, acquire, context.getLocation(release), message);
}
} catch (AnalyzerException e) {
context.log(e, null);
}
}
private static final int SEEN_TARGET = 1;
private static final int SEEN_BRANCH = 2;
private static final int SEEN_EXCEPTION = 4;
private static final int SEEN_RETURN = 8;
/** TODO RENAME */
private static class MyGraph extends ControlFlowGraph {
@Override
protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) {
if (from.getOpcode() == Opcodes.IFNULL) {
JumpInsnNode jump = (JumpInsnNode) from;
if (jump.label == to) {
// Skip jump targets on null if it's surrounding the release call
//
// if (lock != null) {
// lock.release();
// }
//
// The above shouldn't be considered a scenario where release() may not
// be called.
AbstractInsnNode next = Lint.getNextInstruction(from);
if (next != null && next.getType() == AbstractInsnNode.VAR_INSN) {
next = Lint.getNextInstruction(next);
if (next != null && next.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode method = (MethodInsnNode) next;
if (method.name.equals(RELEASE_METHOD)
&& method.owner.equals(WAKELOCK_OWNER)) {
// This isn't entirely correct; this will also trigger
// for "if (lock == null) { lock.release(); }" but that's
// not likely (and caught by other null checking in tools)
return;
}
}
}
}
} else if (from.getOpcode() == Opcodes.IFEQ) {
JumpInsnNode jump = (JumpInsnNode) from;
if (jump.label == to) {
AbstractInsnNode prev = Lint.getPrevInstruction(from);
if (prev != null && prev.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode method = (MethodInsnNode) prev;
if (method.name.equals(IS_HELD_METHOD)
&& method.owner.equals(WAKELOCK_OWNER)) {
AbstractInsnNode next = Lint.getNextInstruction(from);
if (next != null) {
super.add(from, next);
return;
}
}
}
}
}
super.add(from, to);
}
}
/**
* Search from the given node towards the target; return false if we reach an exit point such as
* a return or a call on the way there that is not within a try/catch clause.
*
* @param node the current node
* @return true if the target was reached XXX RETURN VALUES ARE WRONG AS OF RIGHT NOW
*/
protected static int dfs(ControlFlowGraph.Node node) {
AbstractInsnNode instruction = node.instruction;
if (instruction.getType() == AbstractInsnNode.JUMP_INSN) {
int opcode = instruction.getOpcode();
if (opcode == Opcodes.RETURN
|| opcode == Opcodes.ARETURN
|| opcode == Opcodes.LRETURN
|| opcode == Opcodes.IRETURN
|| opcode == Opcodes.DRETURN
|| opcode == Opcodes.FRETURN
|| opcode == Opcodes.ATHROW) {
return SEEN_RETURN;
}
}
// There are no cycles, so no *NEED* for this, though it does avoid
// researching shared labels. However, it makes debugging harder (no re-entry)
// so this is only done when debugging is off
if (node.visit != 0) {
return 0;
}
node.visit = 1;
// Look for the target. This is any method call node which is a release on the
// lock (later also check it's the same instance, though that's harder).
// This is because finally blocks tend to be inlined so from a single try/catch/finally
// with a release() in the finally, the bytecode can contain multiple repeated
// (inlined) release() calls.
if (instruction.getType() == AbstractInsnNode.METHOD_INSN) {
MethodInsnNode method = (MethodInsnNode) instruction;
if (method.name.equals(RELEASE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
return SEEN_TARGET;
} else if (method.name.equals(ACQUIRE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
// OK
} else if (method.name.equals(IS_HELD_METHOD) && method.owner.equals(WAKELOCK_OWNER)) {
// OK
} else {
// Some non acquire/release method call: if this is not associated with a
// try-catch block, it would mean the exception would exit the method,
// which would be an error
if (node.exceptions.isEmpty()) {
// Look up the corresponding frame, if any
AbstractInsnNode curr = method.getPrevious();
boolean foundFrame = false;
while (curr != null) {
if (curr.getType() == AbstractInsnNode.FRAME) {
foundFrame = true;
break;
}
curr = curr.getPrevious();
}
if (!foundFrame) {
return SEEN_RETURN;
}
}
}
}
// if (node.instruction is a call, and the call is not caught by
// a try/catch block (provided the release is not inside the try/catch block)
// then return false
int status = 0;
boolean implicitReturn = true;
List<Node> successors = node.successors;
List<Node> exceptions = node.exceptions;
if (!exceptions.isEmpty()) {
implicitReturn = false;
}
for (Node successor : exceptions) {
status = dfs(successor) | status;
if ((status & SEEN_RETURN) != 0) {
return status;
}
}
if (status != 0) {
status |= SEEN_EXCEPTION;
}
if (!successors.isEmpty()) {
implicitReturn = false;
if (successors.size() > 1) {
status |= SEEN_BRANCH;
}
}
for (Node successor : successors) {
status = dfs(successor) | status;
if ((status & SEEN_RETURN) != 0) {
return status;
}
}
if (implicitReturn) {
status |= SEEN_RETURN;
}
return status;
}
// Check for the non-timeout version of wakelock acquire
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("acquire");
}
@Override
public void visitMethodCall(
@NonNull JavaContext context,
@NonNull UCallExpression call,
@NonNull PsiMethod method) {
if (call.getValueArgumentCount() > 0) {
return;
}
if (!context.getEvaluator().isMemberInClass(method, "android.os.PowerManager.WakeLock")) {
return;
}
Location location = context.getLocation(call);
LintFix fix =
fix().name("Set timeout to 10 minutes")
.replace()
.pattern("acquire\\(()\\)")
.with("10*60*1000L /*10 minutes*/")
.build();
context.report(
TIMEOUT,
call,
location,
""
+ "Provide a timeout when requesting a wakelock with "
+ "`PowerManager.Wakelock.acquire(long timeout)`. This will ensure the OS will "
+ "cleanup any wakelocks that last longer than you intend, and will save your "
+ "user's battery.",
fix);
}
}