com.android.tools.idea.model.DeclaredPermissionsLookup.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.model.DeclaredPermissionsLookup.java

Source

/*
 * 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.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.isGradleProject() && myFacet.getIdeaAndroidProject() != null) {
                    Collection<AndroidLibrary> libraries = myFacet.getIdeaAndroidProject().getSelectedVariant()
                            .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;
        }

        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);
                    }
                }
            }
        }
    }
}