blob: fa00b3cf05bd8575fab6bf0d6e9cc69f64c65a21 [file] [log] [blame]
/*
* 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.rendering;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceType;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@SuppressWarnings({
"deprecation", // Deprecated com.android.util.Pair is required by ProjectCallback interface
"SynchronizeOnThis"})
public abstract class MultiResourceRepository extends LocalResourceRepository {
protected List<? extends LocalResourceRepository> myChildren;
private long[] myModificationCounts;
private Map<ResourceType, ListMultimap<String, ResourceItem>> myItems = Maps.newEnumMap(ResourceType.class);
private final Map<ResourceType, ListMultimap<String, ResourceItem>> myCachedTypeMaps = Maps.newEnumMap(ResourceType.class);
private Map<String, DataBindingInfo> myDataBindingResourceFiles = Maps.newHashMap();
private long myDataBindingResourceFilesModificationCount = Long.MIN_VALUE;
MultiResourceRepository(@NotNull String displayName, @NotNull List<? extends LocalResourceRepository> children) {
super(displayName);
setChildren(children);
}
protected void setChildren(@NotNull List<? extends LocalResourceRepository> children) {
if (myChildren != null) {
for (int i = myChildren.size() - 1; i >= 0; i--) {
LocalResourceRepository resources = myChildren.get(i);
resources.removeParent(this);
}
if (myChildren.size() == 1) {
myGeneration = Math.max(myChildren.get(0).getModificationCount(), myGeneration);
}
}
myChildren = children;
myModificationCounts = new long[children.size()];
if (children.size() == 1) {
// Make sure that the modification count of the child and the parent are same. This is
// done so that we can return child's modification count, instead of ours.
LocalResourceRepository child = children.get(0);
long modCount = Math.max(child.getModificationCount(), myGeneration);
child.myGeneration = modCount + 1;
myGeneration = modCount; // it's incremented below.
}
for (int i = myChildren.size() - 1; i >= 0; i--) {
LocalResourceRepository resources = myChildren.get(i);
resources.addParent(this);
myModificationCounts[i] = resources.getModificationCount();
}
myGeneration++;
clearCache();
invalidateItemCaches();
}
private void clearCache() {
myItems = null;
synchronized (this) {
myCachedTypeMaps.clear();
}
}
public List<? extends LocalResourceRepository> getChildren() {
return myChildren;
}
@Override
public long getModificationCount() {
if (myChildren.size() == 1) {
return myChildren.get(0).getModificationCount();
}
// See if any of the delegates have changed
boolean changed = false;
for (int i = myChildren.size() - 1; i >= 0; i--) {
LocalResourceRepository resources = myChildren.get(i);
long rev = resources.getModificationCount();
if (rev != myModificationCounts[i]) {
myModificationCounts[i] = rev;
changed = true;
}
}
if (changed) {
myGeneration++;
}
return myGeneration;
}
@Nullable
@Override
public DataBindingInfo getDataBindingInfoForLayout(String layoutName) {
for (LocalResourceRepository child : myChildren) {
DataBindingInfo info = child.getDataBindingInfoForLayout(layoutName);
if (info != null) {
return info;
}
}
return null;
}
@NotNull
@Override
public Map<String, DataBindingInfo> getDataBindingResourceFiles() {
long modificationCount = getModificationCount();
if (myDataBindingResourceFilesModificationCount == modificationCount) {
return myDataBindingResourceFiles;
}
Map<String, DataBindingInfo> selected = Maps.newHashMap();
for (LocalResourceRepository child : myChildren) {
Map<String, DataBindingInfo> childFiles = child.getDataBindingResourceFiles();
if (childFiles != null) {
selected.putAll(childFiles);
}
}
myDataBindingResourceFiles = Collections.unmodifiableMap(selected);
myDataBindingResourceFilesModificationCount = modificationCount;
return myDataBindingResourceFiles;
}
@NonNull
@Override
protected Map<ResourceType, ListMultimap<String, ResourceItem>> getMap() {
if (myItems == null) {
if (myChildren.size() == 1) {
myItems = myChildren.get(0).getItems();
}
else {
Map<ResourceType, ListMultimap<String, ResourceItem>> map = Maps.newEnumMap(ResourceType.class);
for (ResourceType type : ResourceType.values()) {
map.put(type, getMap(type, false)); // should pass create is true, but as described below we interpret this differently
}
myItems = map;
}
}
return myItems;
}
@Nullable
@Override
protected ListMultimap<String, ResourceItem> getMap(ResourceType type, boolean create) {
// Should I assert !create here? If we try to manipulate the cache it won't work right...
synchronized (this) {
ListMultimap<String, ResourceItem> map = myCachedTypeMaps.get(type);
if (map != null) {
return map;
}
}
if (myChildren.size() == 1) {
LocalResourceRepository child = myChildren.get(0);
if (child instanceof MultiResourceRepository) {
return ((MultiResourceRepository)child).getMap(type);
}
return child.getItems().get(type);
}
ListMultimap<String, ResourceItem> map = ArrayListMultimap.create();
// Merge all items of the given type
for (int i = myChildren.size() - 1; i >= 0; i--) {
LocalResourceRepository resources = myChildren.get(i);
Map<ResourceType, ListMultimap<String, ResourceItem>> items = resources.getItems();
ListMultimap<String, ResourceItem> m = items.get(type);
if (m == null) {
continue;
}
// TODO: Start with JUST the first map here (which often contains most of the keys) and then
// only merge in 1...n
for (ResourceItem item : m.values()) {
String name = item.getName();
if (map.containsKey(name) && item.getType() != ResourceType.ID) {
// The item already exists in this map; only add if there isn't an item with the
// same qualifiers (and it's not an id; id's are allowed to be defined in multiple
// places even with the same qualifiers)
String qualifiers = item.getQualifiers();
boolean contains = false;
List<ResourceItem> list = map.get(name);
assert list != null;
for (ResourceItem existing : list) {
if (qualifiers.equals(existing.getQualifiers())) {
contains = true;
break;
}
}
if (!contains) {
map.put(name, item);
}
}
else {
map.put(name, item);
}
}
}
synchronized (this) {
myCachedTypeMaps.put(type, map);
}
return map;
}
@NonNull
@Override
protected ListMultimap<String, ResourceItem> getMap(ResourceType type) {
return super.getMap(type);
}
@NonNull
@Override
public Map<ResourceType, ListMultimap<String, ResourceItem>> getItems() {
return getMap();
}
@Override
public void dispose() {
for (int i = myChildren.size() - 1; i >= 0; i--) {
LocalResourceRepository resources = myChildren.get(i);
resources.removeParent(this);
resources.dispose();
}
}
/**
* Notifies this delegating repository that the given dependent repository has invalidated
* resources of the given types (empty means all)
*/
public void invalidateCache(@NotNull LocalResourceRepository repository, @Nullable ResourceType... types) {
assert myChildren.contains(repository) : repository;
synchronized (this) {
if (types == null || types.length == 0) {
myCachedTypeMaps.clear();
}
else {
for (ResourceType type : types) {
myCachedTypeMaps.remove(type);
}
}
}
myItems = null;
myGeneration++;
invalidateItemCaches(types);
}
@Override
@VisibleForTesting
public boolean isScanPending(@NonNull PsiFile psiFile) {
assert ApplicationManager.getApplication().isUnitTestMode();
for (int i = myChildren.size() - 1; i >= 0; i--) {
LocalResourceRepository resources = myChildren.get(i);
if (resources.isScanPending(psiFile)) {
return true;
}
}
return false;
}
@Override
public void sync() {
super.sync();
for (int i = myChildren.size() - 1; i >= 0; i--) {
LocalResourceRepository resources = myChildren.get(i);
resources.sync();
}
}
@VisibleForTesting
int getChildCount() {
return myChildren.size();
}
}