| /* |
| * Copyright (C) 2014 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.gradle.project; |
| |
| import com.android.annotations.VisibleForTesting; |
| import com.android.tools.idea.gradle.GradleSyncState; |
| import com.android.tools.idea.gradle.IdeaGradleProject; |
| import com.android.tools.idea.gradle.facet.AndroidGradleFacet; |
| import com.android.tools.idea.gradle.util.LocalProperties; |
| import com.google.common.collect.Maps; |
| import com.google.common.hash.Hashing; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.module.ModuleManager; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.io.*; |
| import java.util.Arrays; |
| import java.util.Map; |
| |
| import static com.android.SdkConstants.*; |
| import static com.android.builder.model.AndroidProject.FD_INTERMEDIATES; |
| import static com.android.tools.idea.gradle.util.GradleUtil.*; |
| import static com.android.tools.idea.gradle.util.Projects.*; |
| import static com.android.tools.idea.sdk.IdeSdks.getAndroidSdkPath; |
| import static com.android.tools.idea.startup.AndroidStudioSpecificInitializer.isAndroidStudio; |
| import static com.google.common.io.Closeables.close; |
| import static com.google.common.io.Files.toByteArray; |
| import static com.intellij.openapi.util.io.FileUtil.*; |
| import static com.intellij.openapi.vfs.VfsUtil.findFileByIoFile; |
| import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile; |
| |
| /** |
| * The Project data that needs to be persisted to check whether it is possible to reload the Project without the need of calling Gradle. |
| */ |
| public class GradleProjectSyncData implements Serializable { |
| @NotNull @NonNls private static final String STATE_FILE_NAME = "gradle_project_sync_data.bin"; |
| private static final boolean ENABLED = !Boolean.getBoolean("studio.disable.synccache"); |
| |
| private static final Logger LOG = Logger.getInstance(GradleProjectSyncData.class); |
| |
| /** |
| * A set of files and their MD5 that the persisted external project data depends on. |
| */ |
| private Map<String, byte[]> myFileChecksums = Maps.newHashMap(); |
| |
| /** |
| * The model version |
| */ |
| @SuppressWarnings("FieldCanBeLocal") |
| private String myGradlePluginVersion = GRADLE_PLUGIN_RECOMMENDED_VERSION; |
| |
| /** |
| * The last time a sync was done. |
| */ |
| private long myLastGradleSyncTimestamp = -1L; |
| |
| private transient File myRootDirPath; |
| |
| private GradleProjectSyncData() { |
| } |
| |
| /** |
| * Creates an instance by loading the persisted data from the disk for the given project. |
| * |
| * @param project the project for which to load the data. |
| * @return the loaded instance or {@code null} when the data ia not available or no longer valid. |
| */ |
| @Nullable |
| public static GradleProjectSyncData getInstance(@NotNull final Project project) { |
| if (!ENABLED || needsAndroidSdkSync(project)) { |
| return null; |
| } |
| try { |
| return doLoadFromDisk(project); |
| } |
| catch (IOException e) { |
| LOG.info(String.format("Error accessing state cache for project '%1$s', sync will be needed.", project.getName())); |
| } |
| catch (ClassNotFoundException e) { |
| LOG.info(String.format("Cannot recover state cache for project '%1$s', sync will be needed.", project.getName())); |
| } |
| return null; |
| } |
| |
| private static boolean needsAndroidSdkSync(@NotNull final Project project) { |
| if (isAndroidStudio()) { |
| final File ideSdkPath = getAndroidSdkPath(); |
| if (ideSdkPath != null) { |
| try { |
| LocalProperties localProperties = new LocalProperties(project); |
| File projectSdkPath = localProperties.getAndroidSdkPath(); |
| return projectSdkPath == null || !filesEqual(ideSdkPath, projectSdkPath); |
| } |
| catch (IOException ignored) { |
| } |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| @Nullable |
| private static GradleProjectSyncData doLoadFromDisk(@NotNull Project project) throws IOException, ClassNotFoundException { |
| FileInputStream fin = null; |
| try { |
| File rootDirPath = getBaseDirPath(project); |
| File dataFile = getProjectStateFile(project); |
| if (!dataFile.exists()) { |
| return null; |
| } |
| fin = new FileInputStream(dataFile); |
| ObjectInputStream ois = new ObjectInputStream(fin); |
| try { |
| GradleProjectSyncData data = (GradleProjectSyncData)ois.readObject(); |
| data.myRootDirPath = rootDirPath; |
| return data; |
| } |
| finally { |
| close(ois, false); |
| } |
| } |
| finally { |
| close(fin, false); |
| } |
| } |
| |
| /** |
| * Persists the gradle sync data of this project to disk. |
| * |
| * @param project the project to get the data from. |
| */ |
| public static void save(@NotNull Project project) { |
| if (!ENABLED) { |
| return; |
| } |
| boolean cacheSaved = false; |
| try { |
| GradleProjectSyncData data = createFrom(project); |
| if (data != null) { |
| File file = getProjectStateFile(project); |
| ensureExists(file.getParentFile()); |
| data.saveTo(file); |
| cacheSaved = true; |
| } |
| } |
| catch (IOException e) { |
| LOG.info(String.format("Error while saving persistent state from project '%1$s'", project.getName()), e); |
| } |
| if (!cacheSaved) { |
| LOG.info("Failed to generate new cache. Deleting the old one."); |
| removeFrom(project); |
| } |
| } |
| |
| @Nullable |
| @VisibleForTesting |
| static GradleProjectSyncData createFrom(@NotNull Project project) throws IOException { |
| GradleProjectSyncData data = new GradleProjectSyncData(); |
| File rootDirPath = getBaseDirPath(project); |
| Module[] modules = ModuleManager.getInstance(project).getModules(); |
| for (Module module : modules) { |
| AndroidGradleFacet gradleFacet = AndroidGradleFacet.getInstance(module); |
| if (gradleFacet != null) { |
| IdeaGradleProject ideaGradleProject = gradleFacet.getGradleProject(); |
| if (ideaGradleProject != null) { |
| data.addFileChecksum(rootDirPath, ideaGradleProject.getBuildFile()); |
| } |
| else { |
| LOG.warn(String.format("Trying to create project data from a not initialized project '%1$s'. Abort.", project.getName())); |
| return null; |
| } |
| } |
| |
| if (isGradleProjectModule(module)) { |
| data.addFileChecksum(rootDirPath, getGradleBuildFile(module)); |
| data.addFileChecksum(rootDirPath, getGradleSettingsFile(rootDirPath)); |
| data.addFileChecksum(rootDirPath, new File(rootDirPath, FN_GRADLE_PROPERTIES)); |
| data.addFileChecksum(rootDirPath, new File(rootDirPath, FN_LOCAL_PROPERTIES)); |
| data.addFileChecksum(rootDirPath, getGradleUserSettingsFile()); |
| } |
| } |
| GradleSyncState syncState = GradleSyncState.getInstance(project); |
| data.myLastGradleSyncTimestamp = syncState.getLastGradleSyncTimestamp(); |
| return data; |
| } |
| |
| @NotNull |
| private static File getProjectStateFile(@NotNull Project project) throws IOException { |
| Module projectModule = findGradleProjectModule(project); |
| if (projectModule != null) { |
| File buildFolderPath = getBuildFolderPath(projectModule); |
| if (buildFolderPath != null) { |
| return new File(buildFolderPath, join(FD_INTERMEDIATES, STATE_FILE_NAME)); |
| } |
| } |
| // TODO: Once we upgrade to Gradle 2.0, we can get the build directory from there. For now assume "build". |
| return new File(virtualToIoFile(project.getBaseDir()), join(BUILD_DIR_DEFAULT_NAME, FD_INTERMEDIATES, STATE_FILE_NAME)); |
| } |
| |
| private void addFileChecksum(File rootDirPath, @Nullable VirtualFile vf) throws IOException { |
| addFileChecksum(rootDirPath, vf != null ? virtualToIoFile(vf) : null); |
| } |
| |
| private void addFileChecksum(File rootDirPath, @Nullable File file) throws IOException { |
| if (file == null) { |
| return; |
| } |
| String key; |
| if (isAncestor(rootDirPath, file, true)) { |
| key = getRelativePath(rootDirPath, file); |
| } |
| else { |
| key = file.getAbsolutePath(); |
| } |
| myFileChecksums.put(key, createChecksum(file)); |
| } |
| |
| @NotNull |
| private static byte[] createChecksum(@NotNull File file) throws IOException { |
| // For files tracked by the IDE we get the content from the virtual files, otherwise we revert to io. |
| VirtualFile vf = findFileByIoFile(file, true); |
| byte[] data = new byte[] {}; |
| if (vf != null) { |
| vf.refresh(false, false); |
| if (vf.exists()) { |
| data = vf.contentsToByteArray(); |
| } |
| } else if (file.exists()) { |
| data = toByteArray(file); |
| } |
| return Hashing.md5().hashBytes(data).asBytes(); |
| } |
| |
| /** |
| * Saves the data on the given project location. |
| * |
| * @param file the file where to save this data. |
| */ |
| private void saveTo(File file) throws IOException { |
| FileOutputStream fos = null; |
| try { |
| fos = new FileOutputStream(file); |
| ObjectOutputStream oos = new ObjectOutputStream(fos); |
| try { |
| oos.writeObject(this); |
| } |
| finally { |
| close(oos, false); |
| } |
| } |
| finally { |
| close(fos, false); |
| } |
| } |
| |
| public static void removeFrom(@NotNull Project project) { |
| if (!ENABLED) { |
| return; |
| } |
| try { |
| File stateFile = getProjectStateFile(project); |
| if (stateFile.isFile()) { |
| delete(stateFile); |
| } |
| } |
| catch (IOException e) { |
| LOG.warn(String.format("Failed to remove state for project %1$s'", project.getName())); |
| } |
| } |
| |
| /** |
| * Verifies that whether the persisted external project data can be used to create the project or not. |
| * <p/> |
| * This validates that all the files that the external project data depends on, still have the same content checksum and that the gradle |
| * model version is still the same. |
| * |
| * @return whether the data is still valid. |
| * @throws IOException if there is a problem accessing these files. |
| */ |
| public boolean canUseCachedProjectData() { |
| if (!myGradlePluginVersion.equals(GRADLE_PLUGIN_RECOMMENDED_VERSION)) { |
| return false; |
| } |
| |
| for (Map.Entry<String, byte[]> entry : myFileChecksums.entrySet()) { |
| File file = new File(entry.getKey()); |
| if (!file.isAbsolute()) { |
| file = new File(myRootDirPath, file.getPath()); |
| } |
| try { |
| if (!Arrays.equals(entry.getValue(), createChecksum(file))) { |
| return false; |
| } |
| } |
| catch (IOException e) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public long getLastGradleSyncTimestamp() { |
| return myLastGradleSyncTimestamp; |
| } |
| |
| @VisibleForTesting |
| Map<String, byte[]> getFileChecksums() { |
| return myFileChecksums; |
| } |
| } |