blob: 7ec48e1f8f25a5c950b23ca5d6d42fc22d2e1df6 [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 static com.android.SdkConstants.ANDROID_APP_ACTIVITY;
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.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
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;
import java.util.Arrays;
import java.util.List;
/**
* 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 {
/** Problems using wakelocks */
public static final Issue ISSUE = Issue.create(
"Wakelock", //$NON-NLS-1$
"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 " +
"http://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));
private static final String WAKELOCK_OWNER = "android/os/PowerManager$WakeLock"; //$NON-NLS-1$
private static final String RELEASE_METHOD = "release"; //$NON-NLS-1$
private static final String ACQUIRE_METHOD = "acquire"; //$NON-NLS-1$
private static final String IS_HELD_METHOD = "isHeld"; //$NON-NLS-1$
private static final String POWER_MANAGER = "android/os/PowerManager"; //$NON-NLS-1$
private static final String NEW_WAKE_LOCK_METHOD = "newWakeLock"; //$NON-NLS-1$
/** Print diagnostics during analysis (display flow control graph etc).
* Make sure you add the asm-debug or asm-util jars to the runtime classpath
* as well since the opcode integer to string mapping display routine looks for
* it via reflection. */
private static final boolean DEBUG = false;
/** Constructs a new {@link WakelockDetector} */
public WakelockDetector() {
}
@Override
public void afterCheckProject(@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) //$NON-NLS-1$
&& 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 = LintUtils.getPrevInstruction(call);
if (prev == null) {
return;
}
prev = LintUtils.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).intValue();
// 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) {
// Track allocations such that we know whether the type of the call
// is on a SecureRandom rather than a Random
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);
if (DEBUG) {
// Requires util package
//ClassNode clazz = classNode;
//clazz.accept(new TraceClassVisitor(new PrintWriter(System.out)));
System.out.println(graph.toString(graph.getNode(acquire)));
}
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 = LintUtils.getNextInstruction(from);
if (next != null && next.getType() == AbstractInsnNode.VAR_INSN) {
next = LintUtils.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 = LintUtils.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 = LintUtils.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) {
if (DEBUG) {
System.out.println("Found exit via explicit return: " //$NON-NLS-1$
+ node.toString(false));
}
return SEEN_RETURN;
}
}
if (!DEBUG) {
// 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 == null || 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) {
if (DEBUG) {
System.out.println("Found exit via unguarded method call: " //$NON-NLS-1$
+ node.toString(false));
}
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 != null) {
if (!exceptions.isEmpty()) {
implicitReturn = false;
}
for (Node successor : exceptions) {
status = dfs(successor) | status;
if ((status & SEEN_RETURN) != 0) {
if (DEBUG) {
System.out.println("Found exit via exception: " //$NON-NLS-1$
+ node.toString(false));
}
return status;
}
}
if (status != 0) {
status |= SEEN_EXCEPTION;
}
}
if (successors != null) {
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) {
if (DEBUG) {
System.out.println("Found exit via branches: " //$NON-NLS-1$
+ node.toString(false));
}
return status;
}
}
}
if (implicitReturn) {
status |= SEEN_RETURN;
if (DEBUG) {
System.out.println("Found exit: via implicit return: " //$NON-NLS-1$
+ node.toString(false));
}
}
return status;
}
}