All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.ServiceCastDetector Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2013 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.CLASS_ACTIVITY;
import static com.android.SdkConstants.CLASS_APPLICATION;
import static com.android.SdkConstants.CLASS_CONTEXT;
import static com.android.SdkConstants.CLASS_VIEW;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiAssignmentExpression;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassType;
import com.intellij.psi.PsiDeclarationStatement;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionStatement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiLocalVariable;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiStatement;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeCastExpression;
import com.intellij.psi.util.PsiTreeUtil;
import java.util.Collections;
import java.util.List;

/**
 * Detector looking for casts on th result of context.getSystemService which are suspect
 */
public class ServiceCastDetector extends Detector implements JavaPsiScanner {
    public static final Implementation IMPLEMENTATION = new Implementation(
        ServiceCastDetector.class,
        Scope.JAVA_FILE_SCOPE);

    /** Invalid cast to a type from the service constant */
    public static final Issue ISSUE = Issue.create(
            "ServiceCast",
            "Wrong system service casts",

            "When you call `Context#getSystemService()`, the result is typically cast to " +
            "a specific interface. This lint check ensures that the cast is compatible with " +
            "the expected type of the return value.",

            Category.CORRECTNESS,
            6,
            Severity.FATAL,
            IMPLEMENTATION);

    /** Using wifi manager from the wrong context */
    public static final Issue WIFI_MANAGER = Issue.create(
            "WifiManagerLeak",
            "WifiManager Leak",

            "On versions prior to Android N (24), initializing the `WifiManager` via " +
            "`Context#getSystemService` can cause a memory leak if the context is not " +
            "the application context. Change `context.getSystemService(...)` to " +
            "`context.getApplicationContext().getSystemService(...)`.",

            Category.CORRECTNESS,
            6,
            Severity.FATAL,
            IMPLEMENTATION);

    /** Using wifi manager from the wrong context: unknown Context origin */
    public static final Issue WIFI_MANAGER_UNCERTAIN = Issue.create(
            "WifiManagerPotentialLeak",
            "WifiManager Potential Leak",

            "On versions prior to Android N (24), initializing the `WifiManager` via " +
            "`Context#getSystemService` can cause a memory leak if the context is not " +
            "the application context.\n" +
            "\n" +
            "In many cases, it's not obvious from the code where the `Context` is " +
            "coming from (e.g. it might be a parameter to a method, or a field initialized " +
            "from various method calls.)  It's possible that the context being passed in " +
            "is the application context, but to be on the safe side, you should consider " +
            "changing `context.getSystemService(...)` to " +
            "`context.getApplicationContext().getSystemService(...)`.",

            Category.CORRECTNESS,
            6,
            Severity.WARNING,
            IMPLEMENTATION);

    /** Constructs a new {@link ServiceCastDetector} check */
    public ServiceCastDetector() {
    }

    // ---- Implements JavaScanner ----

    @Override
    public List getApplicableMethodNames() {
        return Collections.singletonList("getSystemService");
    }

    @Override
    public void visitMethod(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
            @NonNull PsiMethodCallExpression call, @NonNull PsiMethod method) {
        PsiExpression[] args = call.getArgumentList().getExpressions();
        if (args.length == 1 && args[0] instanceof PsiReferenceExpression) {
            String name = ((PsiReferenceExpression)args[0]).getReferenceName();

            // Check WIFI_SERVICE context origin
            if ("WIFI_SERVICE".equals(name)) {
                checkWifiService(context, call);
            }

            // Check cast
            PsiElement parent = LintUtils.skipParentheses(call.getParent());
            if (parent instanceof PsiTypeCastExpression) {
                PsiTypeCastExpression cast = (PsiTypeCastExpression) parent;
                String expectedClass = getExpectedType(name);
                if (expectedClass != null && cast.getCastType() != null) {
                    String castType = cast.getCastType().getType().getCanonicalText();
                    if (castType.indexOf('.') == -1) {
                        expectedClass = stripPackage(expectedClass);
                    }
                    if (!castType.equals(expectedClass)) {
                        // It's okay to mix and match
                        // android.content.ClipboardManager and android.text.ClipboardManager
                        if (isClipboard(castType) && isClipboard(expectedClass)) {
                            return;
                        }

                        String message = String.format(
                                "Suspicious cast to `%1$s` for a `%2$s`: expected `%3$s`",
                                stripPackage(castType), name, stripPackage(expectedClass));
                        context.report(ISSUE, call, context.getLocation(cast), message);
                    }
                }
            }
        }
    }

    /**
     * Checks that the given call to {@code Context#getSystemService(WIFI_SERVICE)} is
     * using the application context
     */
    private static void checkWifiService(@NonNull JavaContext context,
            @NonNull PsiMethodCallExpression call) {
        JavaEvaluator evaluator = context.getEvaluator();
        PsiReferenceExpression methodExpression = call.getMethodExpression();
        PsiExpression qualifier = methodExpression.getQualifierExpression();

        PsiMethod resolvedMethod = call.resolveMethod();
        if (resolvedMethod != null &&
                (evaluator.isMemberInSubClassOf(resolvedMethod, CLASS_ACTIVITY, false) ||
                        (evaluator.isMemberInSubClassOf(resolvedMethod, CLASS_VIEW, false)))) {
            reportWifiServiceLeak(WIFI_MANAGER, context, call);
            return;
        }
        if (qualifier == null) {
            // Implicit: check surrounding class
            PsiMethod currentMethod = PsiTreeUtil.getParentOfType(call, PsiMethod.class, true);
            if (currentMethod != null
                    && !evaluator.isMemberInSubClassOf(currentMethod, CLASS_APPLICATION, true)) {
                reportWifiServiceLeak(WIFI_MANAGER, context, call);
            }
        } else {
            checkContextReference(context, qualifier, call);
        }
    }

    /**
     * Given a reference to a context, check to see if the context is an application
     * context (in which case, return quietly), or known to not be an application context
     * (in which case, report an error), or is of an unknown context type (in which case,
     * report a warning).
     *
     * @param context the lint analysis context
     * @param element the reference to be checked
     * @param call    the original getSystemService call to report an error against
     */
    private static boolean checkContextReference(
            @NonNull JavaContext context,
            @Nullable PsiElement element,
            @NonNull PsiMethodCallExpression call) {
        if (element == null) {
            return false;
        }
        if (element instanceof PsiMethodCallExpression) {
            PsiMethod resolvedMethod = ((PsiMethodCallExpression) element).resolveMethod();
            if (resolvedMethod != null && !"getApplicationContext".equals(resolvedMethod.getName())) {
                reportWifiServiceLeak(WIFI_MANAGER, context, call);
                return true;
            }
        } else if (element instanceof PsiReferenceExpression) {
            // Check variable references backwards
            PsiElement resolved = ((PsiReferenceExpression) element).resolve();
            if (resolved instanceof PsiField) {
                PsiType type = ((PsiField) resolved).getType();
                return checkWifiContextType(context, call, type, true);
            } else if (resolved instanceof PsiParameter) {
                // Parameter: is the parameter type something other than just "Context"
                // or some subclass of Application?
                PsiType type = ((PsiParameter) resolved).getType();
                return checkWifiContextType(context, call, type, true);
            } else if (resolved instanceof PsiLocalVariable) {
                PsiLocalVariable variable = (PsiLocalVariable) resolved;
                PsiType type = variable.getType();
                if (checkWifiContextType(context, call, type, false)) {
                    return true;
                }

                // Walk backwards through assignments to find the most recent initialization
                // of this variable
                PsiStatement statement = PsiTreeUtil.getParentOfType(element, PsiStatement.class,
                        false);
                if (statement != null) {
                    PsiStatement prev = PsiTreeUtil.getPrevSiblingOfType(statement,
                            PsiStatement.class);
                    String targetName = variable.getName();
                    if (targetName == null) {
                        return false;
                    }
                    while (prev != null) {
                        if (prev instanceof PsiDeclarationStatement) {
                            for (PsiElement st :
                                    ((PsiDeclarationStatement) prev).getDeclaredElements()) {
                                if (variable.equals(st)) {
                                    return checkContextReference(context,
                                            variable.getInitializer(), call);
                                }
                            }
                        } else if (prev instanceof PsiExpressionStatement) {
                            PsiExpression expression = ((PsiExpressionStatement) prev)
                                    .getExpression();
                            if (expression instanceof PsiAssignmentExpression) {
                                PsiAssignmentExpression assign
                                        = (PsiAssignmentExpression) expression;
                                PsiExpression lhs = assign.getLExpression();
                                if (lhs instanceof PsiReferenceExpression) {
                                    PsiReferenceExpression reference = (PsiReferenceExpression) lhs;
                                    if (targetName.equals(reference.getReferenceName()) &&
                                            reference.getQualifier() == null) {
                                        return checkContextReference(context,
                                                assign.getRExpression(), call);
                                    }
                                }
                            }
                        }
                        prev = PsiTreeUtil.getPrevSiblingOfType(prev,
                                PsiStatement.class);
                    }
                }
            }
        }

        return false;
    }

    /**
     * Given a context type (of a parameter or field), check to see if that type implies
     * that the context is not the application context (for example because it's an Activity
     * rather than a plain context).
     * 

* Returns true if it finds and reports a problem. */ private static boolean checkWifiContextType(@NonNull JavaContext context, @NonNull PsiMethodCallExpression call, @NonNull PsiType type, boolean flagPlainContext) { JavaEvaluator evaluator = context.getEvaluator(); if (type instanceof PsiClassType) { PsiClass psiClass = ((PsiClassType) type).resolve(); if (evaluator.extendsClass(psiClass, CLASS_APPLICATION, false)) { return false; } } if (evaluator.typeMatches(type, CLASS_CONTEXT)) { if (flagPlainContext) { reportWifiServiceLeak(WIFI_MANAGER_UNCERTAIN, context, call); return true; } return false; } reportWifiServiceLeak(WIFI_MANAGER, context, call); return true; } private static void reportWifiServiceLeak(@NonNull Issue issue, @NonNull JavaContext context, @NonNull PsiMethodCallExpression call) { if (context.getMainProject().getMinSdk() >= 24) { // Bug is fixed in Nougat return; } String qualifier = ""; if (call.getMethodExpression().getQualifierExpression() != null) { qualifier = call.getMethodExpression().getQualifierExpression().getText(); } String message = String.format("The WIFI_SERVICE must be looked up on the " + "Application context or memory will leak on devices < Android N. " + "Try changing `%1$s` to `%1$s.getApplicationContext()` ", qualifier); context.report(issue, call, context.getLocation(call), message); } private static boolean isClipboard(@NonNull String cls) { return cls.equals("android.content.ClipboardManager") || cls.equals("android.text.ClipboardManager"); } private static String stripPackage(@NonNull String fqcn) { int index = fqcn.lastIndexOf('.'); if (index != -1) { fqcn = fqcn.substring(index + 1); } return fqcn; } @Nullable private static String getExpectedType(@Nullable String value) { if (value == null) { return null; } switch (value) { case "ACCESSIBILITY_SERVICE": return "android.view.accessibility.AccessibilityManager"; case "ACCOUNT_SERVICE": return "android.accounts.AccountManager"; case "ACTIVITY_SERVICE": return "android.app.ActivityManager"; case "ALARM_SERVICE": return "android.app.AlarmManager"; case "APPWIDGET_SERVICE": return "android.appwidget.AppWidgetManager"; case "APP_OPS_SERVICE": return "android.app.AppOpsManager"; case "AUDIO_SERVICE": return "android.media.AudioManager"; case "BATTERY_SERVICE": return "android.os.BatteryManager"; case "BLUETOOTH_SERVICE": return "android.bluetooth.BluetoothManager"; case "CAMERA_SERVICE": return "android.hardware.camera2.CameraManager"; case "CAPTIONING_SERVICE": return "android.view.accessibility.CaptioningManager"; case "CARRIER_CONFIG_SERVICE": return "android.telephony.CarrierConfigManager"; // also allow @Deprecated android.content.ClipboardManager, see isClipboard case "CLIPBOARD_SERVICE": return "android.text.ClipboardManager"; case "CONNECTIVITY_SERVICE": return "android.net.ConnectivityManager"; case "CONSUMER_IR_SERVICE": return "android.hardware.ConsumerIrManager"; case "DEVICE_POLICY_SERVICE": return "android.app.admin.DevicePolicyManager"; case "DISPLAY_SERVICE": return "android.hardware.display.DisplayManager"; case "DOWNLOAD_SERVICE": return "android.app.DownloadManager"; case "DROPBOX_SERVICE": return "android.os.DropBoxManager"; case "FINGERPRINT_SERVICE": return "android.hardware.fingerprint.FingerprintManager"; case "HARDWARE_PROPERTIES_SERVICE": return "android.os.HardwarePropertiesManager"; case "INPUT_METHOD_SERVICE": return "android.view.inputmethod.InputMethodManager"; case "INPUT_SERVICE": return "android.hardware.input.InputManager"; case "JOB_SCHEDULER_SERVICE": return "android.app.job.JobScheduler"; case "KEYGUARD_SERVICE": return "android.app.KeyguardManager"; case "LAUNCHER_APPS_SERVICE": return "android.content.pm.LauncherApps"; case "LAYOUT_INFLATER_SERVICE": return "android.view.LayoutInflater"; case "LOCATION_SERVICE": return "android.location.LocationManager"; case "MEDIA_PROJECTION_SERVICE": return "android.media.projection.MediaProjectionManager"; case "MEDIA_ROUTER_SERVICE": return "android.media.MediaRouter"; case "MEDIA_SESSION_SERVICE": return "android.media.session.MediaSessionManager"; case "MIDI_SERVICE": return "android.media.midi.MidiManager"; case "NETWORK_STATS_SERVICE": return "android.app.usage.NetworkStatsManager"; case "NFC_SERVICE": return "android.nfc.NfcManager"; case "NOTIFICATION_SERVICE": return "android.app.NotificationManager"; case "NSD_SERVICE": return "android.net.nsd.NsdManager"; case "POWER_SERVICE": return "android.os.PowerManager"; case "PRINT_SERVICE": return "android.print.PrintManager"; case "RESTRICTIONS_SERVICE": return "android.content.RestrictionsManager"; case "SEARCH_SERVICE": return "android.app.SearchManager"; case "SENSOR_SERVICE": return "android.hardware.SensorManager"; case "STORAGE_SERVICE": return "android.os.storage.StorageManager"; case "SYSTEM_HEALTH_SERVICE": return "android.os.health.SystemHealthManager"; case "TELEPHONY_SERVICE": return "android.telephony.TelephonyManager"; case "TELEPHONY_SUBSCRIPTION_SERVICE": return "android.telephony.SubscriptionManager"; case "TEXT_SERVICES_MANAGER_SERVICE": return "android.view.textservice.TextServicesManager"; case "TV_INPUT_SERVICE": return "android.media.tv.TvInputManager"; case "UI_MODE_SERVICE": return "android.app.UiModeManager"; case "USAGE_STATS_SERVICE": return "android.app.usage.UsageStatsManager"; case "USB_SERVICE": return "android.hardware.usb.UsbManager"; case "USER_SERVICE": return "android.os.UserManager"; case "VIBRATOR_SERVICE": return "android.os.Vibrator"; case "WALLPAPER_SERVICE": return "android.service.wallpaper.WallpaperService"; case "WIFI_P2P_SERVICE": return "android.net.wifi.p2p.WifiP2pManager"; case "WIFI_SERVICE": return "android.net.wifi.WifiManager"; case "WINDOW_SERVICE": return "android.view.WindowManager"; case "SHORTCUT_SERVICE": return "android.content.pm.ShortcutManager"; default: return null; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy