blob: 29b0be70c32f5502a0c70bcbbf4d981faaeeb9d3 [file] [log] [blame]
/*
* 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.idea.model;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidLibrary;
import com.android.sdklib.AndroidVersion;
import com.android.tools.lint.checks.PermissionHolder;
import com.android.utils.XmlUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.IdeaSourceProvider;
import org.jetbrains.annotations.NotNull;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.android.SdkConstants.*;
import static com.android.tools.lint.checks.PermissionRequirement.ATTR_PROTECTION_LEVEL;
import static com.android.tools.lint.checks.PermissionRequirement.VALUE_DANGEROUS;
/**
* A database which records (and responds to queries about) which permissions
* a given module or library depends on.
*/
public class DeclaredPermissionsLookup implements ProjectComponent {
/** Number of milliseconds we'll wait before checking file stamps again */
private static final int CACHE_MS = 1000;
private final Project myProject;
public DeclaredPermissionsLookup(Project project) {
myProject = project;
}
/** Returns the {@link DeclaredPermissionsLookup} project component */
public static DeclaredPermissionsLookup getInstance(Project project) {
return project.getComponent(DeclaredPermissionsLookup.class);
}
/** Returns a {@link PermissionHolder} for the given module (and its dependencies) */
@NonNull
public static PermissionHolder getPermissionHolder(@NonNull Module module) {
return getInstance(module.getProject()).getModulePermissions(module);
}
/** Resets any cached state */
public void reset() {
if (myLibraryPermissions != null) {
myLibraryPermissions.clear();
}
if (myManifestPermissionsMap != null) {
myModulePermissionsMap.clear();
}
if (myManifestPermissionsMap != null) {
myManifestPermissionsMap.clear();
}
}
private Map<AndroidLibrary, LibraryPermissions> myLibraryPermissions;
private Map<Module, ModulePermissions> myModulePermissionsMap;
private Map<VirtualFile, ManifestPermissions> myManifestPermissionsMap;
@NonNull
private LibraryPermissions getLibraryPermissions(@NonNull AndroidLibrary library) {
if (myLibraryPermissions == null) {
myLibraryPermissions = Maps.newIdentityHashMap();
}
LibraryPermissions libraryPermissions = myLibraryPermissions.get(library);
if (libraryPermissions == null) {
libraryPermissions = new LibraryPermissions(library);
myLibraryPermissions.put(library, libraryPermissions);
}
return libraryPermissions;
}
@NonNull
private ModulePermissions getModulePermissions(@NonNull Module module) {
if (myModulePermissionsMap == null) {
myModulePermissionsMap = Maps.newIdentityHashMap();
}
ModulePermissions modulePermissions = myModulePermissionsMap.get(module);
if (modulePermissions == null) {
modulePermissions = new ModulePermissions(module);
myModulePermissionsMap.put(module, modulePermissions);
}
return modulePermissions;
}
private ManifestPermissions getManifestPermissions(VirtualFile manifest) {
if (myManifestPermissionsMap == null) {
myManifestPermissionsMap = Maps.newIdentityHashMap();
}
ManifestPermissions manifestPermissions = myManifestPermissionsMap.get(manifest);
if (manifestPermissions == null) {
manifestPermissions = new ManifestVirtualFilePermissions(myProject, manifest);
myManifestPermissionsMap.put(manifest, manifestPermissions);
}
return manifestPermissions;
}
@Override
public void projectOpened() {
}
@Override
public void projectClosed() {
}
@Override
public void initComponent() {
}
@Override
public void disposeComponent() {
}
@NotNull
@Override
public String getComponentName() {
return "PermissionsLookup";
}
private static class PermissionStrings {
@NotNull public final Set<String> granted;
@NotNull public final Set<String> revocable;
public PermissionStrings(@NotNull Set<String> granted, @NotNull Set<String> revocable) {
this.granted = granted;
this.revocable = revocable;
}
}
private abstract static class ManifestPermissions {
protected long myLastChecked;
protected long myTimeStamp;
private PermissionStrings myPermissions;
public ManifestPermissions() {
}
public boolean hasPermission(@NonNull String permission) {
return getPermissions().granted.contains(permission);
}
public boolean isRevocable(@NonNull String permission) {
return getPermissions().revocable.contains(permission);
}
protected PermissionStrings getPermissions() {
// If it's been more than a second since the last time we looked, check the file timestamps
long time = System.currentTimeMillis();
if (myPermissions != null && myLastChecked < time - CACHE_MS) {
long timeStamp = getTimeStamp();
if (timeStamp > myTimeStamp) {
myPermissions = null;
}
}
if (myPermissions == null) {
myPermissions = readPermissions();
myTimeStamp = getTimeStamp();
myLastChecked = System.currentTimeMillis();
}
return myPermissions;
}
protected abstract PermissionStrings readPermissions();
protected abstract long getTimeStamp();
}
private static class ManifestFilePermissions extends ManifestPermissions {
private @NonNull final File myFile;
public ManifestFilePermissions(@NonNull File file) {
myFile = file;
}
@Override
protected PermissionStrings readPermissions() {
Set<String> permissions = Sets.newHashSetWithExpectedSize(30);
Set<String> revocable = Sets.newHashSetWithExpectedSize(2);
addPermissions(permissions, revocable, myFile);
myLastChecked = myFile.lastModified();
return new PermissionStrings(permissions, revocable);
}
@Override
protected long getTimeStamp() {
return myFile.lastModified();
}
}
private static class ManifestVirtualFilePermissions extends ManifestPermissions implements Computable<PermissionStrings> {
private @NonNull final Project myProject;
private @NonNull final VirtualFile myFile;
public ManifestVirtualFilePermissions(@NonNull Project project, @NonNull VirtualFile file) {
myFile = file;
myProject = project;
}
@Override
protected PermissionStrings readPermissions() {
return ApplicationManager.getApplication().runReadAction(this);
}
@Override
protected long getTimeStamp() {
return myFile.getModificationStamp();
}
/**
* Computes the result of {@link #readPermissions()} but in a {@link Computable} interface such that
* it can be directly passed as a read action since it needs PSI access
*/
@Override
public PermissionStrings compute() {
Set<String> permissions = Sets.newHashSetWithExpectedSize(30);
Set<String> revocable = Sets.newHashSetWithExpectedSize(2);
// First look for the PSI file and attempt to use it instead (since it will pick up on edited but
// not yet saved to disk changes)
PsiFile file = PsiManager.getInstance(myProject).findFile(myFile);
if (file != null) {
addPermissions(permissions, revocable, file);
} else {
addPermissions(permissions, revocable, myFile);
}
return new PermissionStrings(permissions, revocable);
}
}
private class LibraryPermissions {
@NonNull private final AndroidLibrary myLibrary;
@Nullable private final ManifestFilePermissions myManifest;
public LibraryPermissions(@NonNull AndroidLibrary library) {
myLibrary = library;
File manifest = library.getManifest();
if (manifest.exists()) {
myManifest = new ManifestFilePermissions(manifest);
} else {
myManifest = null;
}
}
public boolean hasPermission(@NonNull String permission) {
if (myManifest != null) {
return myManifest.hasPermission(permission);
}
for (AndroidLibrary library : myLibrary.getLibraryDependencies()) {
if (getLibraryPermissions(library).hasPermission(permission)) {
return true;
}
}
return false;
}
public boolean isRevocable(@NonNull String permission) {
if (myManifest != null) {
return myManifest.isRevocable(permission);
}
for (AndroidLibrary library : myLibrary.getLibraryDependencies()) {
if (getLibraryPermissions(library).isRevocable(permission)) {
return true;
}
}
return false;
}
}
private class ModulePermissions implements PermissionHolder {
private final AndroidFacet myFacet;
private List<ManifestPermissions> myManifests;
private List<LibraryPermissions> myLibraries;
private List<ModulePermissions> myDependencies;
private Set<String> myFoundCache = Sets.newHashSet();
private Map<String,Boolean> myRevocableCache = Maps.newHashMap();
public ModulePermissions(Module module) {
myFacet = AndroidFacet.getInstance(module);
if (myFacet != null) {
myManifests = Lists.newArrayListWithExpectedSize(4);
for (IdeaSourceProvider provider : IdeaSourceProvider.getAllIdeaSourceProviders(myFacet)) {
VirtualFile manifest = provider.getManifestFile();
if (manifest != null) {
myManifests.add(getManifestPermissions(manifest));
}
}
if (myFacet.requiresAndroidModel() && myFacet.getAndroidModel() != null) {
Collection<AndroidLibrary> libraries = myFacet.getAndroidModel().getMainArtifact().getDependencies().getLibraries();
myLibraries = Lists.newArrayList();
for (AndroidLibrary library : libraries) {
myLibraries.add(getLibraryPermissions(library));
}
}
Module[] dependencies = ModuleRootManager.getInstance(module).getDependencies(false);
if (dependencies.length > 0) {
myDependencies = Lists.newArrayListWithExpectedSize(dependencies.length);
for (Module depModule : dependencies) {
myDependencies.add(getModulePermissions(depModule));
}
}
}
}
@Override
public boolean hasPermission(@NonNull String permission) {
// Permission already found to be available?
if (myFoundCache.contains(permission)) {
return true;
}
boolean hasPermission = computeHasPermission(permission);
if (hasPermission) {
// We only cache *successfully* found permissions. If you've already
// declared a permission, it's unlikely that it will disappear, so we
// don't want to keep looking for it while the editor repeatedly analyzes
// the same set of calls.
//
// However, if we're looking for a permission that *isn't* available,
// it's likely that the user will be told about this, and will add the
// permission, and in that case we don't want to risk a stale cache.
myFoundCache.add(permission);
}
return hasPermission;
}
private boolean computeHasPermission(@NonNull String permission) {
if (myFacet == null) {
return false;
}
// TODO: Every n seconds, check for sync/updates to module structure
for (ManifestPermissions manifest : myManifests) {
if (manifest.hasPermission(permission)) {
return true;
}
}
if (myDependencies != null) {
for (ModulePermissions module : myDependencies) {
if (module.hasPermission(permission)) {
return true;
}
}
}
if (myLibraries != null) {
for (LibraryPermissions library : myLibraries) {
if (library.hasPermission(permission)) {
return true;
}
}
}
return false;
}
@Override
public boolean isRevocable(@NonNull String permission) {
// Permission already found to be available?
Boolean cached = myRevocableCache.get(permission);
if (cached != null) {
return cached;
}
boolean isRevocable = computeRevocable(permission);
myRevocableCache.put(permission, isRevocable);
return isRevocable;
}
@NonNull
@Override
public AndroidVersion getMinSdkVersion() {
return AndroidModuleInfo.get(myFacet).getMinSdkVersion();
}
@NonNull
@Override
public AndroidVersion getTargetSdkVersion() {
return AndroidModuleInfo.get(myFacet).getTargetSdkVersion();
}
private boolean computeRevocable(@NonNull String permission) {
if (myFacet == null) {
return false;
}
for (ManifestPermissions manifest : myManifests) {
if (manifest.isRevocable(permission)) {
return true;
}
}
if (myDependencies != null) {
for (ModulePermissions module : myDependencies) {
if (module.isRevocable(permission)) {
return true;
}
}
}
if (myLibraries != null) {
for (LibraryPermissions library : myLibraries) {
if (library.isRevocable(permission)) {
return true;
}
}
}
return false;
}
}
private static void addPermissions(@NonNull Set<String> permissions,
@NonNull Set<String> revocable,
@NonNull PsiFile manifest) {
String xml = manifest.getText();
addPermissions(permissions, revocable, xml);
}
private static void addPermissions(@NonNull Set<String> permissions,
@NonNull Set<String> revocable,
@NonNull VirtualFile manifest) {
try {
String xml = new String(manifest.contentsToByteArray());
addPermissions(permissions, revocable, xml);
}
catch (IOException ignore) {
}
}
private static void addPermissions(@NonNull Set<String> permissions,
@NonNull Set<String> revocable,
@NonNull File manifest) {
try {
String xml = Files.toString(manifest, Charsets.UTF_8);
addPermissions(permissions, revocable, xml);
}
catch (IOException ignore) {
}
}
private static void addPermissions(@NonNull Set<String> permissions,
@NonNull Set<String> revocable,
@NonNull String xml) {
Document document = XmlUtils.parseDocumentSilently(xml, true);
if (document == null) {
return;
}
Element root = document.getDocumentElement();
if (root == null) {
return;
}
NodeList children = root.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node item = children.item(i);
if (item.getNodeType() != Node.ELEMENT_NODE) {
continue;
}
String nodeName = item.getNodeName();
if (nodeName.equals(TAG_USES_PERMISSION)) {
Element element = (Element)item;
String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (!name.isEmpty()) {
permissions.add(name);
}
} else if (nodeName.equals(TAG_PERMISSION)) {
Element element = (Element)item;
String protectionLevel = element.getAttributeNS(ANDROID_URI,
ATTR_PROTECTION_LEVEL);
if (VALUE_DANGEROUS.equals(protectionLevel)) {
String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (!name.isEmpty()) {
revocable.add(name);
}
}
}
}
}
}