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

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

There is a newer version: 25.3.0
Show newest version
/*
 * 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.ATTR_VALUE;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_ALL_OF;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_ANY_OF;
import static com.android.tools.lint.checks.SupportAnnotationDetector.ATTR_CONDITIONAL;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.sdklib.AndroidVersion;
import com.android.tools.lint.detector.api.ConstantEvaluator;
import com.android.tools.lint.detector.api.JavaContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnnotationMemberValue;
import com.intellij.psi.PsiArrayInitializerMemberValue;
import com.intellij.psi.tree.IElementType;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;

/**
 * A permission requirement is a boolean expression of permission names that a
 * caller must satisfy for a given Android API.
 */
public abstract class PermissionRequirement {
    public static final String ATTR_PROTECTION_LEVEL = "protectionLevel"; //$NON-NLS-1$
    public static final String VALUE_DANGEROUS = "dangerous"; //$NON-NLS-1$

    protected final PsiAnnotation annotation;
    private int firstApi;
    private int lastApi;

    @SuppressWarnings("ConstantConditions")
    public static final PermissionRequirement NONE = new PermissionRequirement(null) {
        @Override
        public boolean isSatisfied(@NonNull PermissionHolder available) {
            return true;
        }

        @Override
        public boolean appliesTo(@NonNull PermissionHolder available) {
            return false;
        }

        @Override
        public boolean isConditional() {
            return false;
        }

        @Override
        public boolean isRevocable(@NonNull PermissionHolder revocable) {
            return false;
        }

        @Override
        public String toString() {
            return "None";
        }

        @Override
        protected void addMissingPermissions(@NonNull PermissionHolder available,
          @NonNull Set result) {
        }

        @Override
        protected void addRevocablePermissions(@NonNull Set result,
                @NonNull PermissionHolder revocable) {
        }

        @Nullable
        @Override
        public IElementType getOperator() {
            return null;
        }

        @NonNull
        @Override
        public Iterable getChildren() {
            return Collections.emptyList();
        }
    };

    private PermissionRequirement(@NonNull PsiAnnotation annotation) {
        this.annotation = annotation;
    }

    @NonNull
    public static PermissionRequirement create(
            @NonNull JavaContext context,
            @NonNull PsiAnnotation annotation) {

        String value = getAnnotationStringValue(annotation, ATTR_VALUE);
        if (value != null && !value.isEmpty()) {
            return new Single(annotation, value);
        }

        String[] anyOf = getAnnotationStringValues(annotation, ATTR_ANY_OF);
        if (anyOf != null) {
            if (anyOf.length > 1) {
                return new Many(annotation, JavaTokenType.OROR, anyOf);
            } else if (anyOf.length == 1) {
                return new Single(annotation, anyOf[0]);
            }
        }

        String[] allOf = getAnnotationStringValues(annotation, ATTR_ALL_OF);
        if (allOf != null) {
            if (allOf.length > 1) {
                return new Many(annotation, JavaTokenType.ANDAND, allOf);
            } else if (allOf.length == 1) {
                return new Single(annotation, allOf[0]);
            }
        }

        return NONE;
    }

    @Nullable
    public static Boolean getAnnotationBooleanValue(@Nullable PsiAnnotation annotation,
            @NonNull String name) {
        if (annotation != null) {
            PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
            if (attributeValue == null && ATTR_VALUE.equals(name)) {
                attributeValue = annotation.findDeclaredAttributeValue(null);
            }
            // Use constant evaluator since we want to resolve field references as well
            if (attributeValue != null) {
                Object o = ConstantEvaluator.evaluate(null, attributeValue);
                if (o instanceof Boolean) {
                    return (Boolean) o;
                }
            }
        }

        return null;
    }

    @Nullable
    public static Long getAnnotationLongValue(@Nullable PsiAnnotation annotation,
            @NonNull String name) {
        if (annotation != null) {
            PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
            if (attributeValue == null && ATTR_VALUE.equals(name)) {
                attributeValue = annotation.findDeclaredAttributeValue(null);
            }
            // Use constant evaluator since we want to resolve field references as well
            if (attributeValue != null) {
                Object o = ConstantEvaluator.evaluate(null, attributeValue);
                if (o instanceof Number) {
                    return ((Number)o).longValue();
                }
            }
        }

        return null;
    }

    @Nullable
    public static Double getAnnotationDoubleValue(@Nullable PsiAnnotation annotation,
            @NonNull String name) {
        if (annotation != null) {
            PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
            if (attributeValue == null && ATTR_VALUE.equals(name)) {
                attributeValue = annotation.findDeclaredAttributeValue(null);
            }
            // Use constant evaluator since we want to resolve field references as well
            if (attributeValue != null) {
                Object o = ConstantEvaluator.evaluate(null, attributeValue);
                if (o instanceof Number) {
                    return ((Number)o).doubleValue();
                }
            }
        }

        return null;
    }

    @Nullable
    public static String getAnnotationStringValue(@Nullable PsiAnnotation annotation,
            @NonNull String name) {
        if (annotation != null) {
            PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
            if (attributeValue == null && ATTR_VALUE.equals(name)) {
                attributeValue = annotation.findDeclaredAttributeValue(null);
            }
            // Use constant evaluator since we want to resolve field references as well
            if (attributeValue != null) {
                Object o = ConstantEvaluator.evaluate(null, attributeValue);
                if (o instanceof String) {
                    return (String) o;
                }
            }
        }

        return null;
    }

    @Nullable
    public static String[] getAnnotationStringValues(@Nullable PsiAnnotation annotation,
            @NonNull String name) {
        if (annotation != null) {
            PsiAnnotationMemberValue attributeValue = annotation.findDeclaredAttributeValue(name);
            if (attributeValue == null && ATTR_VALUE.equals(name)) {
                attributeValue = annotation.findDeclaredAttributeValue(null);
            }
            if (attributeValue instanceof PsiArrayInitializerMemberValue) {
                PsiAnnotationMemberValue[] initializers =
                        ((PsiArrayInitializerMemberValue) attributeValue).getInitializers();
                List result = Lists.newArrayListWithCapacity(initializers.length);
                ConstantEvaluator constantEvaluator = new ConstantEvaluator(null);
                for (PsiAnnotationMemberValue element : initializers) {
                    Object o = constantEvaluator.evaluate(element);
                    if (o instanceof String) {
                        result.add((String)o);
                    }
                }
                if (result.isEmpty()) {
                    return null;
                } else {
                    return result.toArray(new String[0]);
                }
            } else {
                // Use constant evaluator since we want to resolve field references as well
                if (attributeValue != null) {
                    Object o = ConstantEvaluator.evaluate(null, attributeValue);
                    if (o instanceof String) {
                        return new String[]{(String) o};
                    } else if (o instanceof String[]) {
                        return (String[])o;
                    } else if (o instanceof Object[]) {
                        Object[] array = (Object[]) o;
                        List strings = Lists.newArrayListWithCapacity(array.length);
                        for (Object element : array) {
                            if (element instanceof String) {
                                strings.add((String) element);
                            }
                        }
                        return strings.toArray(new String[0]);
                    }
                }
            }
        }

        return null;
    }

    /**
     * Returns false if this permission does not apply given the specified minimum and
     * target sdk versions
     *
     * @param minSdkVersion the minimum SDK version
     * @param targetSdkVersion the target SDK version
     * @return true if this permission requirement applies for the given versions
     */
    /**
     * Returns false if this permission does not apply given the specified minimum and target
     * sdk versions
     *
     * @param available   the permission holder which also knows the min and target versions
     * @return true if this permission requirement applies for the given versions
     */
    protected boolean appliesTo(@NonNull PermissionHolder available) {
        if (firstApi == 0) { // initialized?
            firstApi = -1; // initialized, not specified

            // Not initialized
            String range = getAnnotationStringValue(annotation, "apis");
            if (range != null) {
                // Currently only support the syntax "a..b" where a and b are inclusive end points
                // and where "a" and "b" are optional
                int index = range.indexOf("..");
                if (index != -1) {
                    try {
                        if (index > 0) {
                            firstApi = Integer.parseInt(range.substring(0, index));
                        } else {
                            firstApi = 1;
                        }
                        if (index + 2 < range.length()) {
                            lastApi = Integer.parseInt(range.substring(index + 2));
                        } else {
                            lastApi = Integer.MAX_VALUE;
                        }
                    } catch (NumberFormatException ignore) {
                    }
                }
            }
        }

        if (firstApi != -1) {
            AndroidVersion minSdkVersion = available.getMinSdkVersion();
            if (minSdkVersion.getFeatureLevel() > lastApi) {
                return false;
            }

            AndroidVersion targetSdkVersion = available.getTargetSdkVersion();
            if (targetSdkVersion.getFeatureLevel() < firstApi) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns whether this requirement is conditional, meaning that there are
     * some circumstances in which the requirement is not necessary. For
     * example, consider
     * {@code android.app.backup.BackupManager.dataChanged(java.lang.String)} .
     * Here the {@code android.permission.BACKUP} is required but only if the
     * argument is not your own package.
     * 

* This is used to handle permissions differently between the "missing" and * "unused" checks. When checking for missing permissions, we err on the * side of caution: if you are missing a permission, but the permission is * conditional, you may not need it so we may not want to complain. However, * when looking for unused permissions, we don't want to flag the * conditional permissions as unused since they may be required. * * @return true if this requirement is conditional */ public boolean isConditional() { Boolean o = getAnnotationBooleanValue(annotation, ATTR_CONDITIONAL); if (o != null) { return o; } return false; } /** * Returns whether this requirement is for a single permission (rather than * a boolean expression such as one permission or another.) * * @return true if this requirement is just a simple permission name */ public boolean isSingle() { return true; } /** * Whether the permission requirement is satisfied given the set of granted permissions * * @param available the available permissions * @return true if all permissions specified by this requirement are available */ public abstract boolean isSatisfied(@NonNull PermissionHolder available); /** Describes the missing permissions (e.g. "P1, P2 and P3") */ public String describeMissingPermissions(@NonNull PermissionHolder available) { return ""; } /** Returns the missing permissions (e.g. {"P1", "P2", "P3"} */ public Set getMissingPermissions(@NonNull PermissionHolder available) { Set result = Sets.newHashSet(); addMissingPermissions(available, result); return result; } protected abstract void addMissingPermissions(@NonNull PermissionHolder available, @NonNull Set result); /** Returns the permissions in the requirement that are revocable */ public Set getRevocablePermissions(@NonNull PermissionHolder revocable) { Set result = Sets.newHashSet(); addRevocablePermissions(result, revocable); return result; } protected abstract void addRevocablePermissions(@NonNull Set result, @NonNull PermissionHolder revocable); /** * Returns whether this permission is revocable * * @param revocable the set of revocable permissions * @return true if a user can revoke the permission */ public abstract boolean isRevocable(@NonNull PermissionHolder revocable); /** * For permission requirements that combine children, the operator to combine them with; null * for leaf nodes */ @Nullable public abstract IElementType getOperator(); /** * Returns nested requirements, combined via {@link #getOperator()} */ @NonNull public abstract Iterable getChildren(); /** Require a single permission */ private static class Single extends PermissionRequirement { public final String name; public Single(@NonNull PsiAnnotation annotation, @NonNull String name) { super(annotation); this.name = name; } @Override public boolean isRevocable(@NonNull PermissionHolder revocable) { return revocable.isRevocable(name) || isRevocableSystemPermission(name); } @Nullable @Override public IElementType getOperator() { return null; } @NonNull @Override public Iterable getChildren() { return Collections.emptyList(); } @Override public boolean isSingle() { return true; } @Override public String toString() { return name; } @Override public boolean isSatisfied(@NonNull PermissionHolder available) { return available.hasPermission(name) || !appliesTo(available); } @Override public String describeMissingPermissions(@NonNull PermissionHolder available) { return isSatisfied(available) ? "" : name; } @Override protected void addMissingPermissions(@NonNull PermissionHolder available, @NonNull Set missing) { if (!isSatisfied(available)) { missing.add(name); } } @Override protected void addRevocablePermissions(@NonNull Set result, @NonNull PermissionHolder revocable) { if (isRevocable(revocable)) { result.add(name); } } } protected static void appendOperator(StringBuilder sb, IElementType operator) { sb.append(' '); if (operator == JavaTokenType.ANDAND) { sb.append("and"); } else if (operator == JavaTokenType.OROR) { sb.append("or"); } else { assert operator == JavaTokenType.XOR : operator; sb.append("xor"); } sb.append(' '); } /** * Require a series of permissions, all with the same operator. */ private static class Many extends PermissionRequirement { public final IElementType operator; public final List permissions; public Many( @NonNull PsiAnnotation annotation, IElementType operator, String[] names) { super(annotation); assert operator == JavaTokenType.OROR || operator == JavaTokenType.ANDAND : operator; assert names.length >= 2; this.operator = operator; this.permissions = Lists.newArrayListWithExpectedSize(names.length); for (String name : names) { permissions.add(new Single(annotation, name)); } } @Override public boolean isSingle() { return false; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(permissions.get(0)); for (int i = 1; i < permissions.size(); i++) { appendOperator(sb, operator); sb.append(permissions.get(i)); } return sb.toString(); } @Override public boolean isSatisfied(@NonNull PermissionHolder available) { if (operator == JavaTokenType.ANDAND) { for (PermissionRequirement requirement : permissions) { if (!requirement.isSatisfied(available) && requirement.appliesTo(available)) { return false; } } return true; } else { assert operator == JavaTokenType.OROR : operator; for (PermissionRequirement requirement : permissions) { if (requirement.isSatisfied(available) || !requirement.appliesTo(available)) { return true; } } return false; } } @Override public String describeMissingPermissions(@NonNull PermissionHolder available) { StringBuilder sb = new StringBuilder(); boolean first = true; for (PermissionRequirement requirement : permissions) { if (!requirement.isSatisfied(available)) { if (first) { first = false; } else { appendOperator(sb, operator); } sb.append(requirement.describeMissingPermissions(available)); } } return sb.toString(); } @Override protected void addMissingPermissions(@NonNull PermissionHolder available, @NonNull Set missing) { for (PermissionRequirement requirement : permissions) { if (!requirement.isSatisfied(available)) { requirement.addMissingPermissions(available, missing); } } } @Override protected void addRevocablePermissions(@NonNull Set result, @NonNull PermissionHolder revocable) { for (PermissionRequirement requirement : permissions) { requirement.addRevocablePermissions(result, revocable); } } @Override public boolean isRevocable(@NonNull PermissionHolder revocable) { // TODO: Pass in the available set of permissions here, and if // the operator is JavaTokenType.OROR, only return revocable=true // if an unsatisfied permission is also revocable. In other words, // if multiple permissions are allowed, and some of them are satisfied and // not revocable the overall permission requirement is not revocable. for (PermissionRequirement requirement : permissions) { if (requirement.isRevocable(revocable)) { return true; } } return false; } @Nullable @Override public IElementType getOperator() { return operator; } @NonNull @Override public Iterable getChildren() { return permissions; } } /** * Returns true if the given permission name is a revocable permission for * targetSdkVersion ≥ 23 * * @param name permission name * @return true if this is a revocable permission */ public static boolean isRevocableSystemPermission(@NonNull String name) { return Arrays.binarySearch(REVOCABLE_PERMISSION_NAMES, name) >= 0; } @VisibleForTesting static final String[] REVOCABLE_PERMISSION_NAMES = new String[] { "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION", "android.permission.BODY_SENSORS", "android.permission.CALL_PHONE", "android.permission.CAMERA", "android.permission.GET_ACCOUNTS", "android.permission.PROCESS_OUTGOING_CALLS", "android.permission.READ_CALENDAR", "android.permission.READ_CALL_LOG", "android.permission.READ_CELL_BROADCASTS", "android.permission.READ_CONTACTS", "android.permission.READ_EXTERNAL_STORAGE", "android.permission.READ_PHONE_STATE", "android.permission.READ_PROFILE", "android.permission.READ_SMS", "android.permission.READ_SOCIAL_STREAM", "android.permission.RECEIVE_MMS", "android.permission.RECEIVE_SMS", "android.permission.RECEIVE_WAP_PUSH", "android.permission.RECORD_AUDIO", "android.permission.SEND_SMS", "android.permission.USE_FINGERPRINT", "android.permission.USE_SIP", "android.permission.WRITE_CALENDAR", "android.permission.WRITE_CALL_LOG", "android.permission.WRITE_CONTACTS", "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.WRITE_PROFILE", "android.permission.WRITE_SETTINGS", "android.permission.WRITE_SOCIAL_STREAM", "com.android.voicemail.permission.ADD_VOICEMAIL" }; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy