package com.ryanharter.auto.value.parcel; import com.google.common.collect.ImmutableSet; import com.squareup.javapoet.ArrayTypeName; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.CodeBlock; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeVariableName; import java.util.List; import java.util.Set; import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.util.SimpleTypeVisitor6; import javax.lang.model.util.SimpleTypeVisitor7; import javax.lang.model.util.Types; final class Parcelables { static final TypeName STRING = ClassName.get("java.lang", "String"); static final TypeName MAP = ClassName.get("java.util", "Map"); static final TypeName LIST = ClassName.get("java.util", "List"); static final TypeName BOOLEANARRAY = ArrayTypeName.of(boolean.class); static final TypeName BYTEARRAY = ArrayTypeName.of(byte.class); static final TypeName CHARARRAY = ArrayTypeName.of(char.class); static final TypeName INTARRAY = ArrayTypeName.of(int.class); static final TypeName LONGARRAY = ArrayTypeName.of(long.class); static final TypeName STRINGARRAY = ArrayTypeName.of(String.class); static final TypeName SPARSEARRAY = ClassName.get("android.util", "SparseArray"); static final TypeName SPARSEBOOLEANARRAY = ClassName.get("android.util", "SparseBooleanArray"); static final TypeName BUNDLE = ClassName.get("android.os", "Bundle"); static final TypeName PARCELABLE = ClassName.get("android.os", "Parcelable"); static final TypeName PARCELABLEARRAY = ArrayTypeName.of(PARCELABLE); static final TypeName CHARSEQUENCE = ClassName.get("java.lang", "CharSequence"); static final TypeName IBINDER = ClassName.get("android.os", "IBinder"); static final TypeName OBJECTARRAY = ArrayTypeName.of(TypeName.OBJECT); static final TypeName SERIALIZABLE = ClassName.get("java.io", "Serializable"); static final TypeName PERSISTABLEBUNDLE = ClassName.get("android.os", "PersistableBundle"); static final TypeName SIZE = ClassName.get("android.util", "Size"); static final TypeName SIZEF = ClassName.get("android.util", "SizeF"); static final TypeName TEXTUTILS = ClassName.get("android.text", "TextUtils"); static final TypeName ENUM = ClassName.get(Enum.class); private static final Set<TypeName> VALID_TYPES = ImmutableSet.of(STRING, MAP, LIST, BOOLEANARRAY, BYTEARRAY, CHARARRAY, INTARRAY, LONGARRAY, STRINGARRAY, SPARSEARRAY, SPARSEBOOLEANARRAY, BUNDLE, PARCELABLE, PARCELABLEARRAY, CHARSEQUENCE, IBINDER, OBJECTARRAY, SERIALIZABLE, PERSISTABLEBUNDLE, SIZE, SIZEF); public static boolean isValidType(TypeName typeName) { return typeName.isPrimitive() || typeName.isBoxedPrimitive() || VALID_TYPES.contains(typeName); } public static boolean isValidType(Types types, TypeMirror type) { // Special case for MAP, since it can only have String keys and Parcelable values if (isOfType(types, type, MAP)) { return isValidMap(types, type); } return getParcelableType(types, (TypeElement) types.asElement(type)) != null; } public static TypeName getParcelableType(Types types, TypeElement type) { TypeMirror typeMirror = type.asType(); while (typeMirror.getKind() != TypeKind.NONE) { TypeName typeName = TypeName.get(typeMirror); // first, check if the class is valid. if (typeName instanceof ParameterizedTypeName) { typeName = ((ParameterizedTypeName) typeName).rawType; } if (isValidType(typeName)) { return typeName; } // then check if it implements valid interfaces for (TypeMirror iface : type.getInterfaces()) { TypeName inherited = getParcelableType(types, (TypeElement) types.asElement(iface)); if (inherited != null) { return inherited; } } // then move on type = (TypeElement) types.asElement(typeMirror); typeMirror = type.getSuperclass(); } return null; } /** * Maps can only have String keys and Parcelable values. */ private static boolean isValidMap(Types types, TypeMirror type) { return type.accept(new SimpleTypeVisitor6<Boolean, Types>() { @Override public Boolean visitDeclared(DeclaredType t, Types o) { List<? extends TypeMirror> args = t.getTypeArguments(); if (args.size() == 2) { TypeMirror key = args.get(0); TypeMirror value = args.get(1); if (STRING.equals(TypeName.get(key)) && isValidType(o, value)) { return true; } } return false; } }, types); } private static boolean isOfType(Types types, TypeMirror typeMirror, TypeName target) { TypeElement element = (TypeElement) types.asElement(typeMirror); while (typeMirror.getKind() != TypeKind.NONE) { TypeName typeName = TypeName.get(typeMirror); if (typeName instanceof ParameterizedTypeName) { typeName = ((ParameterizedTypeName) typeName).rawType; } if (typeName.equals(target)) { return true; } for (TypeMirror iface : element.getInterfaces()) { if (isOfType(types, iface, target)) { return true; } } element = (TypeElement) types.asElement(typeMirror); typeMirror = element.getSuperclass(); } return false; } /** * Returns the component of the type that is Parcelable, or descends from a Parcelable type. */ private static TypeName getParcelableComponent(final Types types, TypeMirror type) { final TypeMirror typeFinal; if (type.getKind() == TypeKind.TYPEVAR) { TypeVariable vType = (TypeVariable) type; typeFinal = vType.getUpperBound(); } else { typeFinal = type; } return typeFinal.accept(new SimpleTypeVisitor7<TypeName, Void>() { @Override public TypeName visitDeclared(DeclaredType t, Void aVoid) { TypeName typeFinal = TypeName.get(t); while (typeFinal instanceof ParameterizedTypeName && !((ParameterizedTypeName) typeFinal).typeArguments.isEmpty()) { List<TypeName> args = ((ParameterizedTypeName) typeFinal).typeArguments; typeFinal = args.get(args.size() - 1); } return typeFinal; } @Override public TypeName visitArray(ArrayType t, Void aVoid) { TypeMirror component = t.getComponentType(); if (getParcelableType(types, (TypeElement) types.asElement(component)) != null) { return TypeName.get(component); } return TypeName.get(t); } }, null); } static void readValue(CodeBlock.Builder block, AutoValueParcelExtension.Property property, final TypeName parcelableType, Types types) { boolean needsNullCheck = needsNullCheck(property, parcelableType); if (needsNullCheck){ block.add("in.readInt() == 0 ? "); } if (parcelableType.equals(STRING)) { block.add("in.readString()"); } else if (parcelableType.equals(TypeName.BYTE) || parcelableType.equals(TypeName.BYTE.box())) { block.add("in.readByte()"); } else if (parcelableType.equals(TypeName.INT) || parcelableType.equals(TypeName.INT.box())) { block.add("in.readInt()"); } else if (parcelableType.equals(TypeName.SHORT) || parcelableType.equals(TypeName.SHORT.box())) { block.add("(short) in.readInt()"); } else if (parcelableType.equals(TypeName.CHAR) || parcelableType.equals(TypeName.CHAR.box())) { block.add("(char) in.readInt()"); } else if (parcelableType.equals(TypeName.LONG) || parcelableType.equals(TypeName.LONG.box())) { block.add("in.readLong()"); } else if (parcelableType.equals(TypeName.FLOAT) || parcelableType.equals(TypeName.FLOAT.box())) { block.add("in.readFloat()"); } else if (parcelableType.equals(TypeName.DOUBLE) || parcelableType.equals(TypeName.DOUBLE.box())) { block.add("in.readDouble()"); } else if (parcelableType.equals(TypeName.BOOLEAN) || parcelableType.equals(TypeName.BOOLEAN.box())) { block.add("in.readInt() == 1"); } else if (parcelableType.equals(PARCELABLE)) { TypeName check = property.type instanceof TypeVariableName ? ((TypeVariableName) property.type).bounds.get(0) : property.type; if (check.equals(PARCELABLE)) { block.add("in.readParcelable($T.class.getClassLoader())", getParcelableComponent(types, property.element.getReturnType())); } else { block.add("($T) in.readParcelable($T.class.getClassLoader())", property.type, getParcelableComponent(types, property.element.getReturnType())); } } else if (parcelableType.equals(CHARSEQUENCE)) { block.add("$T.CHAR_SEQUENCE_CREATOR.createFromParcel(in)", TEXTUTILS); } else if (parcelableType.equals(MAP)) { block.add("($T) in.readHashMap($T.class.getClassLoader())", property.type, getParcelableComponent(types, property.element.getReturnType())); } else if (parcelableType.equals(LIST)) { block.add("($T) in.readArrayList($T.class.getClassLoader())", property.type, getParcelableComponent(types, property.element.getReturnType())); } else if (parcelableType.equals(BOOLEANARRAY)) { block.add("in.createBooleanArray()"); } else if (parcelableType.equals(BYTEARRAY)) { block.add("in.createByteArray()"); } else if (parcelableType.equals(CHARARRAY)) { block.add("in.createCharArray()"); } else if (parcelableType.equals(STRINGARRAY)) { block.add("in.readStringArray()"); } else if (parcelableType.equals(IBINDER)) { if (property.type.equals(IBINDER)) { block.add("in.readStrongBinder()"); } else { block.add("($T) in.readStrongBinder()", property.type); } } else if (parcelableType.equals(OBJECTARRAY)) { block.add("in.readArray($T.class.getClassLoader())", getParcelableComponent(types, property.element.getReturnType())); } else if (parcelableType.equals(INTARRAY)) { block.add("in.createIntArray()"); } else if (parcelableType.equals(LONGARRAY)) { block.add("in.createLongArray()"); } else if (parcelableType.equals(SERIALIZABLE)) { if (property.type.equals(SERIALIZABLE)) { block.add("in.readSerializable()"); } else { block.add("($T) in.readSerializable()", property.type); } } else if (parcelableType.equals(PARCELABLEARRAY)) { ArrayTypeName atype = (ArrayTypeName) property.type; if (atype.componentType.equals(PARCELABLE)) { block.add("in.readParcelableArray($T.class.getClassLoader())", getParcelableComponent(types, property.element.getReturnType())); } else { block.add("($T) in.readParcelableArray($T.class.getClassLoader())", property.type, getParcelableComponent(types, property.element.getReturnType())); } } else if (parcelableType.equals(SPARSEARRAY)) { block.add("in.readSparseArray($T.class.getClassLoader())", getParcelableComponent(types, property.element.getReturnType())); } else if (parcelableType.equals(SPARSEBOOLEANARRAY)) { block.add("in.readSparseBooleanArray()"); } else if (parcelableType.equals(BUNDLE)) { block.add("in.readBundle($T.class.getClassLoader())", property.type); } else if (parcelableType.equals(PERSISTABLEBUNDLE)) { block.add("in.readPersistableBundle($T.class.getClassLoader())", property.type); } else if (parcelableType.equals(SIZE)) { block.add("in.readSize()"); } else if (parcelableType.equals(SIZEF)) { block.add("in.readSizeF()"); } else if (parcelableType.equals(ENUM)) { block.add("$T.valueOf($T.class, in.readString())", Enum.class, property.type); } else { block.add("($T) in.readValue($T.class.getClassLoader())", property.type, getParcelableComponent(types, property.element.getReturnType())); } if (needsNullCheck){ block.add(" : null"); } } static void readValueWithTypeAdapter(CodeBlock.Builder block, AutoValueParcelExtension.Property property, final FieldSpec adapter) { if (property.nullable()){ block.add("in.readInt() == 0 ? "); } block.add("$N.fromParcel(in)", adapter); if (property.nullable()){ block.add(" : null"); } } public static CodeBlock writeValue(Types types, AutoValueParcelExtension.Property property, ParameterSpec out, ParameterSpec flags) { CodeBlock.Builder block = CodeBlock.builder(); TypeName type = getTypeNameFromProperty(property, types); boolean needsNullCheck = needsNullCheck(property, type); if (needsNullCheck) { block.beginControlFlow("if ($N() == null)", property.methodName); block.addStatement("$N.writeInt(1)", out); block.nextControlFlow("else"); block.addStatement("$N.writeInt(0)", out); } if (type.equals(STRING)) block.add("$N.writeString($N())", out, property.methodName); else if (type.equals(TypeName.BYTE) || type.equals(TypeName.BYTE.box()) || type.equals(TypeName.INT) || type.equals(TypeName.INT.box()) || type.equals(TypeName.CHAR) || type.equals(TypeName.CHAR.box()) || type.equals(TypeName.SHORT)) block.add("$N.writeInt($N())", out, property.methodName); else if (type.equals(TypeName.SHORT.box())) block.add("$N.writeInt($N().intValue())", out, property.methodName); else if (type.equals(TypeName.LONG) || type.equals(TypeName.LONG.box())) block.add("$N.writeLong($N())", out, property.methodName); else if (type.equals(TypeName.FLOAT) || type.equals(TypeName.FLOAT.box())) block.add("$N.writeFloat($N())", out, property.methodName); else if (type.equals(TypeName.DOUBLE) || type.equals(TypeName.DOUBLE.box())) block.add("$N.writeDouble($N())", out, property.methodName); else if (type.equals(TypeName.BOOLEAN) || type.equals(TypeName.BOOLEAN.box())) block.add("$N.writeInt($N() ? 1 : 0)", out, property.methodName); else if (type.equals(PARCELABLE)) block.add("$N.writeParcelable($N(), $N)", out, property.methodName, flags); else if (type.equals(CHARSEQUENCE)) block.add("$T.writeToParcel($N(), $N, $N)", TEXTUTILS, property.methodName, out, flags); else if (type.equals(MAP)) block.add("$N.writeMap($N())", out, property.methodName); else if (type.equals(LIST)) block.add("$N.writeList($N())", out, property.methodName); else if (type.equals(BOOLEANARRAY)) block.add("$N.writeBooleanArray($N())", out, property.methodName); else if (type.equals(BYTEARRAY)) block.add("$N.writeByteArray($N())", out, property.methodName); else if (type.equals(CHARARRAY)) block.add("$N.writeCharArray($N())", out, property.methodName); else if (type.equals(STRINGARRAY)) block.add("$N.writeStringArray($N())", out, property.methodName); else if (type.equals(IBINDER)) block.add("$N.writeStrongBinder($N())", out, property.methodName); else if (type.equals(OBJECTARRAY)) block.add("$N.writeArray($N())", out, property.methodName); else if (type.equals(INTARRAY)) block.add("$N.writeIntArray($N())", out, property.methodName); else if (type.equals(LONGARRAY)) block.add("$N.writeLongArray($N())", out, property.methodName); else if (type.equals(SERIALIZABLE)) block.add("$N.writeSerializable($N())", out, property.methodName); else if (type.equals(PARCELABLEARRAY)) block.add("$N.writeParcelableArray($N())", out, property.methodName); else if (type.equals(SPARSEARRAY)) block.add("$N.writeSparseArray($N())", out, property.methodName); else if (type.equals(SPARSEBOOLEANARRAY)) block.add("$N.writeSparseBooleanArray($N())", out, property.methodName); else if (type.equals(BUNDLE)) block.add("$N.writeBundle($N())", out, property.methodName); else if (type.equals(PERSISTABLEBUNDLE)) block.add("$N.writePersistableBundle($N())", out, property.methodName); else if (type.equals(SIZE)) block.add("$N.writeSize($N())", out, property.methodName); else if (type.equals(SIZEF)) block.add("$N.writeSizeF($N())", out, property.methodName); else if (type.equals(ENUM)) block.add("$N.writeString((($T<?>) $N()).name())", out, Enum.class, property.methodName); else block.add("$N.writeValue($N())", out, property.methodName); block.add(";\n"); if (needsNullCheck) { block.endControlFlow(); } return block.build(); } private static boolean needsNullCheck(AutoValueParcelExtension.Property property, TypeName type) { return property.nullable() && !type.equals(BUNDLE) && !type.equals(LIST) && !type.equals(MAP) && !type.equals(PARCELABLE) && !type.equals(PERSISTABLEBUNDLE) && !type.equals(SPARSEARRAY) && !type.equals(SPARSEBOOLEANARRAY); } public static CodeBlock writeValueWithTypeAdapter(FieldSpec adapter, AutoValueParcelExtension.Property property, ParameterSpec out) { CodeBlock.Builder block = CodeBlock.builder(); if (property.nullable()) { block.beginControlFlow("if ($N() == null)", property.methodName); block.addStatement("$N.writeInt(1)", out); block.nextControlFlow("else"); block.addStatement("$N.writeInt(0)", out); } block.addStatement("$N.toParcel($N(), $N)", adapter, property.methodName, out); if (property.nullable()) { block.endControlFlow(); } return block.build(); } static TypeName getTypeNameFromProperty(AutoValueParcelExtension.Property property, Types types) { TypeMirror returnType = property.element.getReturnType(); if (returnType.getKind() == TypeKind.TYPEVAR) { TypeVariable vType = (TypeVariable) returnType; returnType = vType.getUpperBound(); } TypeElement element = (TypeElement) types.asElement(returnType); if (element != null) { TypeName parcelableType = getParcelableType(types, element); if (!PARCELABLE.equals(parcelableType) && element.getKind() == ElementKind.ENUM) { return ENUM; } return parcelableType; } return property.type; } static boolean isTypeRequiresSuppressWarnings(TypeName type) { return type.equals(LIST) || type.equals(MAP); } }