| /* |
| * Copyright (C) 2013 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.configurations; |
| |
| import com.android.SdkConstants; |
| import com.android.ide.common.rendering.api.ResourceValue; |
| import com.android.ide.common.resources.FrameworkResources; |
| import com.android.ide.common.resources.ResourceRepository; |
| import com.android.ide.common.resources.ResourceResolver; |
| import com.android.ide.common.resources.configuration.DensityQualifier; |
| import com.android.ide.common.resources.configuration.FolderConfiguration; |
| import com.android.ide.common.resources.configuration.LocaleQualifier; |
| import com.android.resources.Density; |
| import com.android.resources.ResourceType; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.tools.idea.rendering.AppResourceRepository; |
| import com.android.tools.idea.rendering.LocalResourceRepository; |
| import com.android.tools.idea.rendering.Locale; |
| import com.android.tools.idea.rendering.ResourceHelper; |
| import com.android.tools.idea.rendering.multi.CompatibilityRenderTarget; |
| import com.android.utils.SparseArray; |
| import com.google.common.collect.Maps; |
| import com.intellij.openapi.application.Application; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.util.Computable; |
| import org.jetbrains.android.sdk.AndroidPlatform; |
| import org.jetbrains.android.sdk.AndroidTargetData; |
| import org.jetbrains.android.sdk.FrameworkResourceLoader; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Map; |
| |
| import static com.android.SdkConstants.*; |
| |
| /** Cache for resolved resources */ |
| public class ResourceResolverCache { |
| private static final Logger LOG = Logger.getInstance(ResourceResolverCache.class); |
| |
| /** The configuration manager this cache corresponds to */ |
| private final ConfigurationManager myManager; |
| |
| /** Map from theme and full configuration to the corresponding resource resolver */ |
| private final Map<String, ResourceResolver> myResolverMap; |
| |
| /** |
| * Map of configured app resources. These are cached separately from the final resource |
| * resolver since they can be shared between different layouts that only vary by theme. |
| * Note that they key here is only the full configuration, whereas the map for the |
| * resolvers also includes the theme. |
| */ |
| private final Map<String, Map<ResourceType, Map<String, ResourceValue>>> myAppResourceMap; |
| |
| /** |
| * Map of configured framework resources. These are cached separately from the final resource |
| * resolver since they can be shared between different layouts that only vary by theme |
| */ |
| private final Map<String, Map<ResourceType, Map<String, ResourceValue>>> myFrameworkResourceMap; |
| |
| /** The generation timestamp of our most recently cached app resources, used to invalidate on edits */ |
| private long myCachedGeneration; |
| |
| /** Map from API level to framework resources */ |
| private SparseArray<FrameworkResources> myFrameworkResources = new SparseArray<FrameworkResources>(); |
| |
| public ResourceResolverCache(ConfigurationManager manager) { |
| myManager = manager; |
| myResolverMap = Maps.newHashMap(); |
| myAppResourceMap = Maps.newHashMap(); |
| myFrameworkResourceMap = Maps.newHashMap(); |
| } |
| |
| public static ResourceResolverCache create(ConfigurationManager manager) { |
| return new ResourceResolverCache(manager); |
| } |
| |
| @NotNull |
| public ResourceResolver getResourceResolver(@Nullable IAndroidTarget target, |
| @NotNull String themeStyle, |
| @NotNull final FolderConfiguration fullConfiguration) { |
| // Are caches up to date? |
| final LocalResourceRepository resources = AppResourceRepository.getAppResources(myManager.getModule(), true); |
| assert resources != null; |
| if (myCachedGeneration != resources.getModificationCount()) { |
| myResolverMap.clear(); |
| myAppResourceMap.clear(); |
| } |
| |
| // When looking up the configured project and framework resources, the theme doesn't matter, so we look up only |
| // by the configuration qualifiers; for example, here's a sample key: |
| // -ldltr-sw384dp-w384dp-h640dp-normal-notlong-port-notnight-xhdpi-finger-keyssoft-nokeys-navhidden-nonav-1280x768-v17 |
| // Note that the target version is already baked in via the -v qualifier. |
| // |
| // However, the resource resolver also depends on the theme, so we use a more specific key for the resolver map than |
| // for the configured resource maps, by prepending the theme name: |
| // @style/MyTheme-ldltr-sw384dp-w384dp-h640dp-normal-notlong-port-notnight-xhdpi-finger-keyssoft-nokeys-navhidden-nonav-1280x768-v17 |
| String configurationKey = fullConfiguration.getUniqueKey(); |
| String resolverKey = themeStyle + configurationKey; |
| ResourceResolver resolver = myResolverMap.get(resolverKey); |
| if (resolver == null) { |
| Map<ResourceType, Map<String, ResourceValue>> configuredAppRes; |
| Map<ResourceType, Map<String, ResourceValue>> frameworkResources; |
| |
| // Framework resources |
| if (target == null) { |
| target = myManager.getTarget(); |
| } |
| if (target == null) { |
| frameworkResources = Collections.emptyMap(); |
| } else { |
| ResourceRepository frameworkRes = getFrameworkResources(fullConfiguration, target); |
| if (frameworkRes == null) { |
| frameworkResources = Collections.emptyMap(); |
| } |
| else { |
| // get the framework resource values based on the current config |
| frameworkResources = myFrameworkResourceMap.get(configurationKey); |
| if (frameworkResources == null) { |
| frameworkResources = frameworkRes.getConfiguredResources(fullConfiguration); |
| |
| // Fix up assets. We're only doing this in limited cases for now; specifically Froyo (since the Gingerbread |
| // assets replaced the look for the same theme; that doesn't happen to the same extend for Holo) |
| if (target instanceof CompatibilityRenderTarget && target.getVersion().getApiLevel() == 8) { |
| IAndroidTarget realTarget = ((CompatibilityRenderTarget)target).getRealTarget(); |
| if (realTarget != null) { |
| replaceDrawableBitmaps(frameworkResources, target, realTarget); |
| } |
| } |
| |
| myFrameworkResourceMap.put(configurationKey, frameworkResources); |
| } |
| } |
| } |
| |
| // App resources |
| configuredAppRes = myAppResourceMap.get(configurationKey); |
| if (configuredAppRes == null) { |
| // get the project resource values based on the current config |
| Application application = ApplicationManager.getApplication(); |
| configuredAppRes = application.runReadAction(new Computable<Map<ResourceType, Map<String, ResourceValue>>>() { |
| @Override |
| public Map<ResourceType, Map<String, ResourceValue>> compute() { |
| return resources.getConfiguredResources(fullConfiguration); |
| } |
| }); |
| myAppResourceMap.put(configurationKey, configuredAppRes); |
| } |
| |
| // Resource Resolver |
| assert themeStyle.startsWith(STYLE_RESOURCE_PREFIX) || themeStyle.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : themeStyle; |
| boolean isProjectTheme = ResourceHelper.isProjectStyle(themeStyle); |
| String themeName = ResourceHelper.styleToTheme(themeStyle); |
| resolver = ResourceResolver.create(configuredAppRes, frameworkResources, themeName, isProjectTheme); |
| |
| if (target instanceof CompatibilityRenderTarget) { |
| int apiLevel = target.getVersion().getFeatureLevel(); |
| if (apiLevel >= 21) { |
| resolver.setDeviceDefaults("Theme.Material.Light", "Theme.Material"); |
| } else if (apiLevel >= 14) { |
| resolver.setDeviceDefaults("Theme.Holo.Light", "Theme.Holo"); |
| } else { |
| resolver.setDeviceDefaults("Theme.Light", "Theme"); |
| } |
| } |
| |
| myResolverMap.put(resolverKey, resolver); |
| myCachedGeneration = resources.getModificationCount(); |
| } |
| |
| return resolver; |
| } |
| |
| /** |
| * Returns a {@link LocalResourceRepository} for the framework resources based on the current configuration selection. |
| * |
| * @return the framework resources or {@code null} if not found. |
| */ |
| @Nullable |
| public ResourceRepository getFrameworkResources(@NotNull FolderConfiguration configuration, @NotNull IAndroidTarget target) { |
| int apiLevel = target.getVersion().getFeatureLevel(); |
| FrameworkResources resources = myFrameworkResources.get(apiLevel); |
| |
| LocaleQualifier locale = configuration.getLocaleQualifier(); |
| boolean needLocales = locale != null && !locale.hasFakeValue() || myManager.getLocale() != Locale.ANY; |
| |
| boolean reset = false; |
| if (resources instanceof FrameworkResourceLoader.IdeFrameworkResources) { |
| if (needLocales && ((FrameworkResourceLoader.IdeFrameworkResources)resources).getSkippedLocales()) { |
| reset = true; |
| } |
| } |
| |
| if (resources == null || reset) { |
| FrameworkResourceLoader.requestLocales(needLocales); |
| resources = getFrameworkResources(target, myManager.getModule(), reset); |
| myFrameworkResources.put(apiLevel, resources); |
| } |
| |
| return resources; |
| } |
| |
| /** |
| * Returns a {@link LocalResourceRepository} for the framework resources of a given target. |
| * |
| * @param target the target for which to return the framework resources. |
| * @return the framework resources or {@code null} if not found. |
| */ |
| @Nullable |
| private static FrameworkResources getFrameworkResources(@NotNull IAndroidTarget target, @NotNull Module module, boolean forceReload) { |
| AndroidPlatform platform = AndroidPlatform.getInstance(module); |
| if (platform == null) { |
| return null; |
| } |
| |
| AndroidTargetData targetData = platform.getSdkData().getTargetData(target); |
| try { |
| if (forceReload) { |
| targetData.resetFrameworkResources(); |
| } |
| return targetData.getFrameworkResources(); |
| } |
| catch (IOException e) { |
| LOG.error(e); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Replaces drawable bitmaps with those from the real older target. This helps the simulated platform look more genuine, |
| * since a lot of the look comes from the nine patch assets. For example, when used to simulate Froyo, the checkboxes |
| * will look better than if we use the current classic theme assets, which look like gingerbread. |
| */ |
| private static void replaceDrawableBitmaps(@NotNull Map<ResourceType, Map<String, ResourceValue>> frameworkResources, |
| @NotNull IAndroidTarget from, |
| @NotNull IAndroidTarget realTarget) { |
| // This is a bit hacky; we should be operating at the resource repository level rather than |
| // for configured resources. However, we may not need this for very long. |
| Map<String, ResourceValue> map = frameworkResources.get(ResourceType.DRAWABLE); |
| String oldPrefix = from.getPath(IAndroidTarget.RESOURCES); |
| String newPrefix = realTarget.getPath(IAndroidTarget.RESOURCES); |
| |
| if (map == null || map.isEmpty() || oldPrefix == null || newPrefix == null || oldPrefix.equals(newPrefix)) { |
| return; |
| } |
| |
| Collection<ResourceValue> values = map.values(); |
| Map<String,String> densityDirMap = Maps.newHashMap(); |
| |
| // Leave XML drawable resources alone since they can reference nonexistent colors and other resources |
| // not available in the real rendering platform |
| final boolean ONLY_REPLACE_BITMAPS = true; |
| Density[] densities = Density.values(); |
| for (ResourceValue value : values) { |
| String v = value.getValue(); |
| //noinspection ConstantConditions,PointlessBooleanExpression |
| if (v != null && (!ONLY_REPLACE_BITMAPS || v.endsWith(DOT_PNG))) { |
| if (v.startsWith(oldPrefix)) { |
| String relative = v.substring(oldPrefix.length()); |
| if (v.endsWith(DOT_PNG)) { |
| int index = relative.indexOf(File.separatorChar); |
| if (index == -1) { |
| index = relative.indexOf('/'); |
| } |
| if (index == -1) { |
| continue; |
| } |
| String parent = relative.substring(0, index); |
| String replace = densityDirMap.get(parent); |
| if (replace == null) { |
| FolderConfiguration configuration = FolderConfiguration.getConfigForFolder(parent); |
| if (configuration != null) { |
| DensityQualifier densityQualifier = configuration.getDensityQualifier(); |
| if (densityQualifier != null) { |
| Density density = densityQualifier.getValue(); |
| if (!new File(newPrefix, parent).exists()) { |
| String oldQualifier = SdkConstants.RES_QUALIFIER_SEP + density.getResourceValue(); |
| String matched = null; |
| for (Density d : densities) { |
| if (d.ordinal() <= density.ordinal()) { |
| // No reason to check higher |
| continue; |
| } |
| String newQualifier = SdkConstants.RES_QUALIFIER_SEP + d.getResourceValue(); |
| String newName = parent.replace(oldQualifier, newQualifier); |
| File dir = new File(newPrefix, newName); |
| if (dir.exists()) { |
| matched = newName; |
| break; |
| } |
| } |
| if (matched == null) { |
| continue; |
| } |
| replace = matched; |
| densityDirMap.put(parent, replace); // This isn't right; there may be some assets only in mdpi! |
| } |
| } |
| } |
| } |
| |
| relative = replace + relative.substring(index); |
| } |
| |
| File newFile = new File(newPrefix, relative); |
| if (newFile.exists()) { |
| value.setValue(newFile.getPath()); |
| } |
| } |
| } |
| } |
| } |
| |
| public void reset() { |
| myCachedGeneration = 0; |
| myAppResourceMap.clear(); |
| myResolverMap.clear(); |
| } |
| } |