/*
* 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.ide.common.rendering.api.AttrResourceValue;
import com.android.ide.common.rendering.api.DeclareStyleableResourceValue;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceType;
import com.android.tools.lint.detector.api.ClassContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.org.objectweb.asm.ClassWriter;
import org.jetbrains.org.objectweb.asm.MethodVisitor;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static org.jetbrains.org.objectweb.asm.Opcodes.*;
/**
* The {@linkplain AarResourceClassGenerator} can generate R classes on the fly for a given resource repository.
* <p>
* This is used to supply R classes on demand for layoutlib in order to render custom views in AAR libraries,
* since AAR libraries ship with the view classes but with the R classes stripped out (this is done deliberately
* such that the actual resource id's can be computed at build time by the app using the AAR resources; otherwise
* there could be id collisions.
* <p>
* However, note that the custom view code itself does not know what the actual application R class package will
* be - and we don't rewrite bytecode at build time to tell it. Instead, the build system will generate multiple
* R classes, one for each AAR (in addition to the real R class), and it will pick unique id's for all the
* resources across the whole app, and then writes these unique id's into the R class for each AAR as well.
* <p>
* It is <b>that</b> R class we are generating on the fly here. We want to be able to render custom views even
* if the full application has not been compiled yet, so if normal class loading fails to identify a R class,
* this generator will be called. It uses the normal resource repository (already used during rendering to
* look up resources such as string and style values), and based on the names there generates bytecode on the
* fly which can then be loaded into the VM and handled by the class loader.
*/
public class AarResourceClassGenerator {
@NotNull private final LocalResourceRepository myAarResources;
@NotNull private final AppResourceRepository myAppResources;
private AarResourceClassGenerator(@NotNull AppResourceRepository appResources, @NotNull LocalResourceRepository aarResources) {
myAppResources = appResources;
myAarResources = aarResources;
}
/**
* Creates a new {@linkplain AarResourceClassGenerator}.
*
* @param appResources the application resources used during rendering; this is used to look up dynamic id's
* for resources
* @param aarResources the resource registry for the AAR library
* @return
*/
@Nullable
public static AarResourceClassGenerator create(@NotNull AppResourceRepository appResources,
@NotNull LocalResourceRepository aarResources) {
return new AarResourceClassGenerator(appResources, aarResources);
}
@SuppressWarnings("deprecation") // Need to handle DeclareStyleableResourceValue
@Nullable
public byte[] generate(String fqcn) {
String className = ClassContext.getInternalName(fqcn);
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_6, ACC_PUBLIC + ACC_FINAL + ACC_SUPER, className, null, "java/lang/Object", null);
int index = className.lastIndexOf('$');
if (index != -1) {
String typeName = className.substring(index + 1);
ResourceType type = ResourceType.getEnum(typeName);
if (type == null) {
return null;
}
cw.visitInnerClass(className, className.substring(0, index), typeName, ACC_PUBLIC + ACC_FINAL + ACC_STATIC);
if (type == ResourceType.STYLEABLE) {
type = ResourceType.DECLARE_STYLEABLE;
Collection<String> keys = myAarResources.getItemsOfType(type);
for (String key : keys) {
List<ResourceItem> items = myAarResources.getResourceItem(type, key);
if (items == null || items.isEmpty()) {
continue;
}
ResourceItem item = items.get(0);
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, key, "[I", null, null);
ResourceValue resourceValue = item.getResourceValue(false);
assert resourceValue instanceof DeclareStyleableResourceValue;
DeclareStyleableResourceValue dv = (DeclareStyleableResourceValue)resourceValue;
Map<String,AttrResourceValue> attributes = dv.getAllAttributes();
if (attributes != null) {
int idx = 0;
for (AttrResourceValue value : attributes.values()) {
Integer initialValue = idx++;
StringBuilder sb = new StringBuilder(30);
sb.append(key);
sb.append('_');
if (value.isFramework()) {
sb.append("android_");
}
String v = value.getName();
// See AndroidResourceUtil.getFieldNameByResourceName
for (int i = 0, n = v.length(); i < n; i++) {
char c = v.charAt(i);
if (c == '.' || c == ':' || c == '-') {
sb.append('_');
} else {
sb.append(c);
}
}
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, sb.toString(), "I", null, initialValue).visitEnd();
}
}
}
MethodVisitor mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
for (String key : keys) {
List<ResourceItem> items = myAarResources.getResourceItem(type, key);
if (items == null || items.isEmpty()) {
continue;
}
ResourceItem item = items.get(0);
ResourceValue resourceValue = item.getResourceValue(false);
assert resourceValue instanceof DeclareStyleableResourceValue;
DeclareStyleableResourceValue dv = (DeclareStyleableResourceValue)resourceValue;
Map<String,AttrResourceValue> attributes = dv.getAllAttributes();
if (attributes == null) {
continue;
}
mv.visitIntInsn(BIPUSH, attributes.size());
mv.visitIntInsn(NEWARRAY, T_INT);
int idx = 0;
for (AttrResourceValue value : attributes.values()) {
mv.visitInsn(DUP);
switch (idx) {
case 0:
mv.visitInsn(ICONST_0);
break;
case 1:
mv.visitInsn(ICONST_1);
break;
case 2:
mv.visitInsn(ICONST_2);
break;
case 3:
mv.visitInsn(ICONST_3);
break;
case 4:
mv.visitInsn(ICONST_4);
break;
case 5:
mv.visitInsn(ICONST_5);
break;
default:
mv.visitIntInsn(BIPUSH, idx);
break;
}
Integer initialValue = myAppResources.getResourceId(ResourceType.ATTR, value.getName());
mv.visitLdcInsn(initialValue);
mv.visitInsn(IASTORE);
idx++;
}
mv.visitFieldInsn(PUTSTATIC, className, key, "[I");
}
mv.visitInsn(RETURN);
mv.visitMaxs(4, 0);
mv.visitEnd();
} else {
Collection<String> keys = myAarResources.getItemsOfType(type);
for (String key : keys) {
Integer initialValue = myAppResources.getResourceId(type, key);
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, key, "I",
null, initialValue).visitEnd();
}
}
} else {
// Default R class
for (ResourceType t : myAarResources.getAvailableResourceTypes()) {
cw.visitInnerClass(className + "$" + t.getName(), className, t.getName(), ACC_PUBLIC + ACC_FINAL + ACC_STATIC);
}
}
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}
}