| /* |
| * 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.client.api.JavaParser; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation; |
| import com.android.tools.lint.detector.api.Context; |
| import com.android.tools.lint.detector.api.JavaContext; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Sets; |
| |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import lombok.ast.BinaryExpression; |
| import lombok.ast.BinaryOperator; |
| import lombok.ast.Expression; |
| import lombok.ast.ForwardingAstVisitor; |
| import lombok.ast.Node; |
| import lombok.ast.Select; |
| import lombok.ast.VariableDefinitionEntry; |
| |
| /** |
| * 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 ResolvedAnnotation 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<String> result) { |
| } |
| |
| @Override |
| protected void addRevocablePermissions(@NonNull Set<String> result, |
| @NonNull PermissionHolder revocable) { |
| } |
| |
| @Nullable |
| @Override |
| public BinaryOperator getOperator() { |
| return null; |
| } |
| |
| @NonNull |
| @Override |
| public Iterable<PermissionRequirement> getChildren() { |
| return Collections.emptyList(); |
| } |
| }; |
| |
| private PermissionRequirement(@NonNull ResolvedAnnotation annotation) { |
| this.annotation = annotation; |
| } |
| |
| @NonNull |
| public static PermissionRequirement create( |
| @Nullable Context context, |
| @NonNull ResolvedAnnotation annotation) { |
| String value = (String)annotation.getValue(ATTR_VALUE); |
| if (value != null && !value.isEmpty()) { |
| for (int i = 0, n = value.length(); i < n; i++) { |
| char c = value.charAt(i); |
| // See if it's a complex expression and if so build it up |
| if (c == '&' || c == '|' || c == '^') { |
| return Complex.parse(annotation, context, value); |
| } |
| } |
| |
| return new Single(annotation, value); |
| } |
| |
| Object v = annotation.getValue(ATTR_ANY_OF); |
| if (v != null) { |
| if (v instanceof String[]) { |
| String[] anyOf = (String[])v; |
| if (anyOf.length > 0) { |
| return new Many(annotation, BinaryOperator.LOGICAL_OR, anyOf); |
| } |
| } else if (v instanceof String) { |
| String[] anyOf = new String[] { (String)v }; |
| return new Many(annotation, BinaryOperator.LOGICAL_OR, anyOf); |
| } |
| } |
| |
| v = annotation.getValue(ATTR_ALL_OF); |
| if (v != null) { |
| if (v instanceof String[]) { |
| String[] allOf = (String[])v; |
| if (allOf.length > 0) { |
| return new Many(annotation, BinaryOperator.LOGICAL_AND, allOf); |
| } |
| } else if (v instanceof String) { |
| String[] allOf = new String[] { (String)v }; |
| return new Many(annotation, BinaryOperator.LOGICAL_AND, allOf); |
| } |
| } |
| |
| return NONE; |
| } |
| |
| /** |
| * 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 |
| Object o = annotation.getValue("apis"); |
| if (o instanceof String) { |
| String range = (String)o; |
| // 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. |
| * <p> |
| * 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() { |
| Object o = annotation.getValue(ATTR_CONDITIONAL); |
| if (o instanceof Boolean) { |
| return (Boolean)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<String> getMissingPermissions(@NonNull PermissionHolder available) { |
| Set<String> result = Sets.newHashSet(); |
| addMissingPermissions(available, result); |
| return result; |
| } |
| |
| protected abstract void addMissingPermissions(@NonNull PermissionHolder available, |
| @NonNull Set<String> result); |
| |
| /** Returns the permissions in the requirement that are revocable */ |
| public Set<String> getRevocablePermissions(@NonNull PermissionHolder revocable) { |
| Set<String> result = Sets.newHashSet(); |
| addRevocablePermissions(result, revocable); |
| return result; |
| } |
| |
| protected abstract void addRevocablePermissions(@NonNull Set<String> 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 BinaryOperator getOperator(); |
| |
| /** |
| * Returns nested requirements, combined via {@link #getOperator()} |
| */ |
| @NonNull |
| public abstract Iterable<PermissionRequirement> getChildren(); |
| |
| /** Require a single permission */ |
| private static class Single extends PermissionRequirement { |
| public final String name; |
| |
| public Single(@NonNull ResolvedAnnotation 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 BinaryOperator getOperator() { |
| return null; |
| } |
| |
| @NonNull |
| @Override |
| public Iterable<PermissionRequirement> 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<String> missing) { |
| if (!isSatisfied(available)) { |
| missing.add(name); |
| } |
| } |
| |
| @Override |
| protected void addRevocablePermissions(@NonNull Set<String> result, |
| @NonNull PermissionHolder revocable) { |
| if (isRevocable(revocable)) { |
| result.add(name); |
| } |
| } |
| } |
| |
| protected static void appendOperator(StringBuilder sb, BinaryOperator operator) { |
| sb.append(' '); |
| if (operator == BinaryOperator.LOGICAL_AND) { |
| sb.append("and"); |
| } else if (operator == BinaryOperator.LOGICAL_OR) { |
| sb.append("or"); |
| } else { |
| assert operator == BinaryOperator.BITWISE_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 BinaryOperator operator; |
| public final List<PermissionRequirement> permissions; |
| |
| public Many( |
| @NonNull ResolvedAnnotation annotation, |
| BinaryOperator operator, |
| String[] names) { |
| super(annotation); |
| assert operator == BinaryOperator.LOGICAL_OR |
| || operator == BinaryOperator.LOGICAL_AND : 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 == BinaryOperator.LOGICAL_AND) { |
| for (PermissionRequirement requirement : permissions) { |
| if (!requirement.isSatisfied(available) && requirement.appliesTo(available)) { |
| return false; |
| } |
| } |
| return true; |
| } else { |
| assert operator == BinaryOperator.LOGICAL_OR : 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<String> missing) { |
| for (PermissionRequirement requirement : permissions) { |
| if (!requirement.isSatisfied(available)) { |
| requirement.addMissingPermissions(available, missing); |
| } |
| } |
| } |
| |
| @Override |
| protected void addRevocablePermissions(@NonNull Set<String> 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 BinaryOperator.LOGICAL_OR, 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 BinaryOperator getOperator() { |
| return operator; |
| } |
| |
| @NonNull |
| @Override |
| public Iterable<PermissionRequirement> getChildren() { |
| return permissions; |
| } |
| } |
| |
| /** |
| * Require multiple permissions. This is a group of permissions with some |
| * associated boolean logic, such as "B or (C and (D or E))". |
| */ |
| private static class Complex extends PermissionRequirement { |
| public final BinaryOperator operator; |
| public final PermissionRequirement left; |
| public final PermissionRequirement right; |
| |
| public Complex( |
| @NonNull ResolvedAnnotation annotation, |
| BinaryOperator operator, |
| PermissionRequirement left, |
| PermissionRequirement right) { |
| super(annotation); |
| this.operator = operator; |
| this.left = left; |
| this.right = right; |
| } |
| |
| @Override |
| public boolean isSingle() { |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| |
| boolean needsParentheses = left instanceof Complex && |
| ((Complex) left).operator != BinaryOperator.LOGICAL_AND; |
| if (needsParentheses) { |
| sb.append('('); |
| } |
| sb.append(left.toString()); |
| if (needsParentheses) { |
| sb.append(')'); |
| } |
| |
| appendOperator(sb, operator); |
| |
| needsParentheses = right instanceof Complex && |
| ((Complex) right).operator != BinaryOperator.LOGICAL_AND; |
| if (needsParentheses) { |
| sb.append('('); |
| } |
| sb.append(right.toString()); |
| if (needsParentheses) { |
| sb.append(')'); |
| } |
| |
| return sb.toString(); |
| } |
| |
| @Override |
| public boolean isSatisfied(@NonNull PermissionHolder available) { |
| boolean satisfiedLeft = left.isSatisfied(available) || !left.appliesTo(available); |
| boolean satisfiedRight = right.isSatisfied(available) || !right.appliesTo(available); |
| if (operator == BinaryOperator.LOGICAL_AND) { |
| return satisfiedLeft && satisfiedRight; |
| } else if (operator == BinaryOperator.LOGICAL_OR) { |
| return satisfiedLeft || satisfiedRight; |
| } else { |
| assert operator == BinaryOperator.BITWISE_XOR : operator; |
| return satisfiedLeft ^ satisfiedRight; |
| } |
| } |
| |
| @Override |
| public String describeMissingPermissions(@NonNull PermissionHolder available) { |
| boolean satisfiedLeft = left.isSatisfied(available); |
| boolean satisfiedRight = right.isSatisfied(available); |
| if (operator == BinaryOperator.LOGICAL_AND || operator == BinaryOperator.LOGICAL_OR) { |
| if (satisfiedLeft) { |
| if (satisfiedRight) { |
| return ""; |
| } |
| return right.describeMissingPermissions(available); |
| } else if (satisfiedRight) { |
| return left.describeMissingPermissions(available); |
| } else { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(left.describeMissingPermissions(available)); |
| appendOperator(sb, operator); |
| sb.append(right.describeMissingPermissions(available)); |
| return sb.toString(); |
| } |
| } else { |
| assert operator == BinaryOperator.BITWISE_XOR : operator; |
| return toString(); |
| } |
| } |
| |
| @Override |
| protected void addMissingPermissions(@NonNull PermissionHolder available, |
| @NonNull Set<String> missing) { |
| boolean satisfiedLeft = left.isSatisfied(available); |
| boolean satisfiedRight = right.isSatisfied(available); |
| if (operator == BinaryOperator.LOGICAL_AND || operator == BinaryOperator.LOGICAL_OR) { |
| if (satisfiedLeft) { |
| if (satisfiedRight) { |
| return; |
| } |
| right.addMissingPermissions(available, missing); |
| } else if (satisfiedRight) { |
| left.addMissingPermissions(available, missing); |
| } else { |
| left.addMissingPermissions(available, missing); |
| right.addMissingPermissions(available, missing); |
| } |
| } else { |
| assert operator == BinaryOperator.BITWISE_XOR : operator; |
| left.addMissingPermissions(available, missing); |
| right.addMissingPermissions(available, missing); |
| } |
| } |
| |
| @Override |
| protected void addRevocablePermissions(@NonNull Set<String> result, |
| @NonNull PermissionHolder revocable) { |
| left.addRevocablePermissions(result, revocable); |
| right.addRevocablePermissions(result, revocable); |
| } |
| |
| @Override |
| public boolean isRevocable(@NonNull PermissionHolder revocable) { |
| // TODO: If operator == BinaryOperator.LOGICAL_OR only return |
| // revocable the there isn't a non-revocable term which is also satisfied. |
| return left.isRevocable(revocable) || right.isRevocable(revocable); |
| } |
| |
| @NonNull |
| public static PermissionRequirement parse(@NonNull ResolvedAnnotation annotation, |
| @Nullable Context context, @NonNull final String value) { |
| // Parse an expression of the form (A op1 B op2 C) op3 (D op4 E) etc. |
| // We'll just use the Java parser to handle this to ensure that operator |
| // precedence etc is correct. |
| if (context == null) { |
| return NONE; |
| } |
| JavaParser javaParser = context.getClient().getJavaParser(null); |
| if (javaParser == null) { |
| return NONE; |
| } |
| try { |
| JavaContext javaContext = new JavaContext(context.getDriver(), |
| context.getProject(), context.getMainProject(), context.file, |
| javaParser) { |
| @Nullable |
| @Override |
| public String getContents() { |
| return "" |
| + "class Test { void test() {\n" |
| + "boolean result=" + value |
| + ";\n}\n}"; |
| } |
| }; |
| Node node = javaParser.parseJava(javaContext); |
| if (node != null) { |
| final AtomicReference<Expression> reference = new AtomicReference<Expression>(); |
| node.accept(new ForwardingAstVisitor() { |
| @Override |
| public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { |
| reference.set(node.astInitializer()); |
| return true; |
| } |
| }); |
| Expression expression = reference.get(); |
| if (expression != null) { |
| return parse(annotation, expression); |
| } |
| } |
| |
| return NONE; |
| } finally { |
| javaParser.dispose(); |
| } |
| } |
| |
| private static PermissionRequirement parse( |
| @NonNull ResolvedAnnotation annotation, |
| @NonNull Expression expression) { |
| if (expression instanceof Select) { |
| return new Single(annotation, expression.toString()); |
| } else if (expression instanceof BinaryExpression) { |
| BinaryExpression binaryExpression = (BinaryExpression) expression; |
| BinaryOperator operator = binaryExpression.astOperator(); |
| if (operator == BinaryOperator.LOGICAL_AND |
| || operator == BinaryOperator.LOGICAL_OR |
| || operator == BinaryOperator.BITWISE_XOR) { |
| PermissionRequirement left = parse(annotation, binaryExpression.astLeft()); |
| PermissionRequirement right = parse(annotation, binaryExpression.astRight()); |
| return new Complex(annotation, operator, left, right); |
| } |
| } |
| return NONE; |
| } |
| |
| @Nullable |
| @Override |
| public BinaryOperator getOperator() { |
| return operator; |
| } |
| |
| @NonNull |
| @Override |
| public Iterable<PermissionRequirement> getChildren() { |
| return Arrays.asList(left, right); |
| } |
| } |
| |
| /** |
| * 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.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_SETTINGS", |
| "android.permission.WRITE_PROFILE", |
| "android.permission.WRITE_SOCIAL_STREAM", |
| "com.android.voicemail.permission.ADD_VOICEMAIL", |
| }; |
| } |