/* * Copyright 2000-2013 JetBrains s.r.o. * * 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 org.jetbrains.android; import com.android.SdkConstants; import com.android.sdklib.IAndroidTarget; import com.intellij.ProjectTopics; import com.intellij.lang.properties.IProperty; import com.intellij.lang.properties.psi.PropertiesElementFactory; import com.intellij.lang.properties.psi.PropertiesFile; import com.intellij.notification.Notification; import com.intellij.notification.NotificationGroup; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.components.AbstractProjectComponent; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModuleRootAdapter; import com.intellij.openapi.roots.ModuleRootEvent; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.ReadonlyStatusHandler; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.util.Processor; import com.intellij.util.containers.HashSet; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidRootUtil; import org.jetbrains.android.importDependencies.ImportDependenciesUtil; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.event.HyperlinkEvent; import java.util.*; /** * @author Eugene.Kudelevsky */ public class AndroidPropertyFilesUpdater extends AbstractProjectComponent { private static final NotificationGroup PROPERTY_FILES_UPDATING_NOTIFICATION = NotificationGroup.balloonGroup("Android Property Files Updating"); private static final Key<List<Object>> ANDROID_PROPERTIES_STATE_KEY = Key.create("ANDROID_PROPERTIES_STATE"); private Notification myNotification; private Disposable myDisposable; protected AndroidPropertyFilesUpdater(Project project) { super(project); } @Override public void initComponent() { myDisposable = new Disposable() { @Override public void dispose() { } }; if (!ApplicationManager.getApplication().isUnitTestMode() && !ApplicationManager.getApplication().isHeadlessEnvironment()) { addProjectPropertiesUpdatingListener(); } } @Override public void disposeComponent() { if (myNotification != null && !myNotification.isExpired()) { myNotification.expire(); } Disposer.dispose(myDisposable); } private void addProjectPropertiesUpdatingListener() { myProject.getMessageBus().connect(myDisposable).subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() { @Override public void rootsChanged(final ModuleRootEvent event) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { StartupManager.getInstance(myProject).runWhenProjectIsInitialized(new Runnable() { @Override public void run() { updatePropertyFilesIfNecessary(); } }); } }, myProject.getDisposed()); } }); } private void updatePropertyFilesIfNecessary() { PsiDocumentManager.getInstance(myProject).commitAllDocuments(); final List<VirtualFile> toAskFiles = new ArrayList<VirtualFile>(); final List<AndroidFacet> toAskFacets = new ArrayList<AndroidFacet>(); final List<Runnable> toAskChanges = new ArrayList<Runnable>(); final List<VirtualFile> files = new ArrayList<VirtualFile>(); final List<Runnable> changes = new ArrayList<Runnable>(); for (Module module : ModuleManager.getInstance(myProject).getModules()) { final AndroidFacet facet = AndroidFacet.getInstance(module); if (facet != null && !facet.isGradleProject()) { final String updatePropertyFiles = facet.getProperties().UPDATE_PROPERTY_FILES; final boolean ask = updatePropertyFiles.isEmpty(); if (!ask && !Boolean.parseBoolean(updatePropertyFiles)) { continue; } final Pair<VirtualFile, List<Runnable>> pair = updateProjectPropertiesIfNecessary(facet); if (pair != null) { if (ask) { toAskFacets.add(facet); toAskFiles.add(pair.getFirst()); toAskChanges.addAll(pair.getSecond()); } else { files.add(pair.getFirst()); changes.addAll(pair.getSecond()); } } } } /* We should expire old notification even if there are no properties to update in current event. For example, user changed "is library" setting to 'true', the notification was shown, but user ignored it. Then he changed the setting to 'false' again. New notification won't be shown, because the value of "android.library" in project.properties is correct. However if the old notification was not expired, user may press on it, and "android.library" property will be changed to 'false'. */ if (myNotification != null && !myNotification.isExpired()) { myNotification.expire(); } if (changes.size() > 0 || toAskChanges.size() > 0) { if (toAskChanges.size() > 0) { askUserIfUpdatePropertyFile(myProject, toAskFacets, new Processor<MyResult>() { @Override public boolean process(MyResult result) { if (result == MyResult.NEVER) { for (AndroidFacet facet : toAskFacets) { facet.getProperties().UPDATE_PROPERTY_FILES = Boolean.FALSE.toString(); } return true; } else if (result == MyResult.ALWAYS) { for (AndroidFacet facet : toAskFacets) { facet.getProperties().UPDATE_PROPERTY_FILES = Boolean.TRUE.toString(); } } if (ReadonlyStatusHandler.ensureFilesWritable(myProject, toAskFiles.toArray(new VirtualFile[toAskFiles.size()]))) { CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { for (Runnable change : toAskChanges) { change.run(); } } }); } }, "Update Android property files", null); } return true; } }); } if (changes.size() > 0 && ReadonlyStatusHandler.ensureFilesWritable( myProject, files.toArray(new VirtualFile[files.size()]))) { CommandProcessor.getInstance().executeCommand(myProject, new Runnable() { @Override public void run() { ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { for (Runnable change : changes) { change.run(); } } }); CommandProcessor.getInstance().markCurrentCommandAsGlobal(myProject); } }, "Update Android property files", null); } } } @Nullable private static Pair<VirtualFile, List<Runnable>> updateProjectPropertiesIfNecessary(@NotNull AndroidFacet facet) { if (facet.isDisposed()) { return null; } final Module module = facet.getModule(); final Pair<PropertiesFile, VirtualFile> pair = AndroidRootUtil.findPropertyFile(module, SdkConstants.FN_PROJECT_PROPERTIES); if (pair == null) { return null; } final PropertiesFile projectProperties = pair.getFirst(); final VirtualFile projectPropertiesVFile = pair.getSecond(); final Pair<Properties, VirtualFile> localProperties = AndroidRootUtil.readPropertyFile(module, SdkConstants.FN_LOCAL_PROPERTIES); final List<Runnable> changes = new ArrayList<Runnable>(); final IAndroidTarget androidTarget = facet.getConfiguration().getAndroidTarget(); final String androidTargetHashString = androidTarget != null ? androidTarget.hashString() : null; final VirtualFile[] dependencies = collectDependencies(module); final String[] dependencyPaths = toSortedPaths(dependencies); final List<Object> newState = Arrays.asList( androidTargetHashString, facet.isLibraryProject(), Arrays.asList(dependencyPaths), facet.getProperties().ENABLE_MANIFEST_MERGING, facet.getProperties().ENABLE_PRE_DEXING); final List<Object> state = facet.getUserData(ANDROID_PROPERTIES_STATE_KEY); if (state == null || !Comparing.equal(state, newState)) { updateTargetProperty(facet, projectProperties, changes); updateLibraryProperty(facet, projectProperties, changes); updateManifestMergerProperty(facet, projectProperties, changes); updateDependenciesInPropertyFile(projectProperties, localProperties, dependencies, changes); facet.putUserData(ANDROID_PROPERTIES_STATE_KEY, newState); } return changes.size() > 0 ? Pair.create(projectPropertiesVFile, changes) : null; } private static void updateDependenciesInPropertyFile(@NotNull final PropertiesFile projectProperties, @Nullable final Pair<Properties, VirtualFile> localProperties, @NotNull final VirtualFile[] dependencies, @NotNull List<Runnable> changes) { final VirtualFile vFile = projectProperties.getVirtualFile(); if (vFile == null) { return; } final Set<VirtualFile> localDependencies = localProperties != null ? ImportDependenciesUtil.getLibDirs(localProperties) : Collections.<VirtualFile>emptySet(); final VirtualFile baseDir = vFile.getParent(); final String baseDirPath = baseDir.getPath(); final List<String> newDepValues = new ArrayList<String>(); for (VirtualFile dependency : dependencies) { if (!localDependencies.contains(dependency)) { final String relPath = FileUtil.getRelativePath(baseDirPath, dependency.getPath(), '/'); final String value = relPath != null ? relPath : dependency.getPath(); newDepValues.add(value); } } final Set<String> oldDepValues = new HashSet<String>(); for (IProperty property : projectProperties.getProperties()) { final String name = property.getName(); if (name != null && name.startsWith(AndroidUtils.ANDROID_LIBRARY_REFERENCE_PROPERTY_PREFIX)) { oldDepValues.add(property.getValue()); } } if (!new HashSet<String>(newDepValues).equals(oldDepValues)) { changes.add(new Runnable() { @Override public void run() { for (IProperty property : projectProperties.getProperties()) { final String name = property.getName(); if (name != null && name.startsWith(AndroidUtils.ANDROID_LIBRARY_REFERENCE_PROPERTY_PREFIX)) { property.getPsiElement().delete(); } } for (int i = 0; i < newDepValues.size(); i++) { final String value = newDepValues.get(i); projectProperties.addProperty(AndroidUtils.ANDROID_LIBRARY_REFERENCE_PROPERTY_PREFIX + Integer.toString(i + 1), value); } } }); } } @NotNull private static VirtualFile[] collectDependencies(@NotNull Module module) { final List<VirtualFile> dependenciesList = new ArrayList<VirtualFile>(); for (AndroidFacet depFacet : AndroidUtils.getAndroidLibraryDependencies(module)) { final Module depModule = depFacet.getModule(); final VirtualFile libDir = getBaseAndroidContentRoot(depModule); if (libDir != null) { dependenciesList.add(libDir); } } return dependenciesList.toArray(new VirtualFile[dependenciesList.size()]); } private static void updateTargetProperty(@NotNull AndroidFacet facet, @NotNull final PropertiesFile propertiesFile, @NotNull List<Runnable> changes) { final Project project = facet.getModule().getProject(); final IAndroidTarget androidTarget = facet.getConfiguration().getAndroidTarget(); if (androidTarget != null) { final String targetPropertyValue = androidTarget.hashString(); final IProperty property = propertiesFile.findPropertyByKey(AndroidUtils.ANDROID_TARGET_PROPERTY); if (property == null) { changes.add(new Runnable() { @Override public void run() { propertiesFile.addProperty(createProperty(project, targetPropertyValue)); } }); } else { if (!Comparing.equal(property.getValue(), targetPropertyValue)) { final PsiElement element = property.getPsiElement(); if (element != null) { changes.add(new Runnable() { @Override public void run() { element.replace(createProperty(project, targetPropertyValue).getPsiElement()); } }); } } } } } public static void updateLibraryProperty(@NotNull AndroidFacet facet, @NotNull final PropertiesFile propertiesFile, @NotNull List<Runnable> changes) { final IProperty property = propertiesFile.findPropertyByKey(AndroidUtils.ANDROID_LIBRARY_PROPERTY); if (property != null) { final String value = Boolean.toString(facet.isLibraryProject()); if (!value.equals(property.getValue())) { changes.add(new Runnable() { @Override public void run() { property.setValue(value); } }); } } else if (facet.isLibraryProject()) { changes.add(new Runnable() { @Override public void run() { propertiesFile.addProperty(AndroidUtils.ANDROID_LIBRARY_PROPERTY, Boolean.TRUE.toString()); } }); } } public static void updateManifestMergerProperty(@NotNull AndroidFacet facet, @NotNull final PropertiesFile propertiesFile, @NotNull List<Runnable> changes) { final IProperty property = propertiesFile.findPropertyByKey(AndroidUtils.ANDROID_MANIFEST_MERGER_PROPERTY); if (property != null) { final String value = Boolean.toString(facet.getProperties().ENABLE_MANIFEST_MERGING); if (!value.equals(property.getValue())) { changes.add(new Runnable() { @Override public void run() { property.setValue(value); } }); } } else if (facet.getProperties().ENABLE_MANIFEST_MERGING) { changes.add(new Runnable() { @Override public void run() { propertiesFile.addProperty(AndroidUtils.ANDROID_MANIFEST_MERGER_PROPERTY, Boolean.TRUE.toString()); } }); } else if (!facet.getProperties().ENABLE_PRE_DEXING) { changes.add(new Runnable() { @Override public void run() { propertiesFile.addProperty(AndroidUtils.ANDROID_DEX_DISABLE_MERGER, Boolean.TRUE.toString()); } }); } } @Nullable private static VirtualFile getBaseAndroidContentRoot(@NotNull Module module) { final AndroidFacet facet = AndroidFacet.getInstance(module); final VirtualFile manifestFile = facet != null ? AndroidRootUtil.getManifestFile(facet) : null; final VirtualFile[] contentRoots = ModuleRootManager.getInstance(module).getContentRoots(); if (manifestFile != null) { for (VirtualFile contentRoot : contentRoots) { if (VfsUtilCore.isAncestor(contentRoot, manifestFile, true)) { return contentRoot; } } } return contentRoots.length > 0 ? contentRoots[0] : null; } // workaround for behavior of Android SDK , which uses non-escaped ':' characters @NotNull private static IProperty createProperty(@NotNull Project project, @NotNull String targetPropertyValue) { final String text = AndroidUtils.ANDROID_TARGET_PROPERTY + "=" + targetPropertyValue; final PropertiesFile dummyFile = PropertiesElementFactory.createPropertiesFile(project, text); return dummyFile.getProperties().get(0); } @NotNull private static String[] toSortedPaths(@NotNull VirtualFile[] files) { final String[] result = new String[files.length]; for (int i = 0; i < files.length; i++) { result[i] = files[i].getPath(); } Arrays.sort(result); return result; } private void askUserIfUpdatePropertyFile(@NotNull Project project, @NotNull Collection<AndroidFacet> facets, @NotNull final Processor<MyResult> callback) { final StringBuilder moduleList = new StringBuilder(); for (AndroidFacet facet : facets) { moduleList.append(facet.getModule().getName()).append("<br>"); } myNotification = PROPERTY_FILES_UPDATING_NOTIFICATION.createNotification( AndroidBundle.message("android.update.project.properties.dialog.title"), AndroidBundle.message("android.update.project.properties.dialog.text", moduleList.toString()), NotificationType.INFORMATION, new NotificationListener.Adapter() { @Override protected void hyperlinkActivated(@NotNull Notification notification, @NotNull HyperlinkEvent event) { final String desc = event.getDescription(); if ("once".equals(desc)) { callback.process(MyResult.ONCE); } else if ("never".equals(desc)) { callback.process(MyResult.NEVER); } else { callback.process(MyResult.ALWAYS); } notification.expire(); } }); myNotification.notify(project); } private enum MyResult { ONCE, NEVER, ALWAYS } }