/*
* Copyright 2016 Flipkart Internet Pvt. Ltd.
*
* 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.flipkart.android.proteus.parser;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import com.flipkart.android.proteus.R;
import com.flipkart.android.proteus.toolbox.ProteusConstants;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author kiran.kumar
*/
public class ParseHelper {
private static final String TAG = "ParseHelper";
private static final String TRUE = "true";
private static final String FALSE = "false";
private static final String VISIBLE = "visible";
private static final String INVISIBLE = "invisible";
private static final String GONE = "gone";
private static final String CENTER = "center";
private static final String CENTER_HORIZONTAL = "center_horizontal";
private static final String CENTER_VERTICAL = "center_vertical";
private static final String LEFT = "left";
private static final String RIGHT = "right";
private static final String TOP = "top";
private static final String BOTTOM = "bottom";
private static final String START = "start";
private static final String END = "end";
private static final String MIDDLE = "middle";
private static final String BEGINNING = "beginning";
private static final String MARQUEE = "marquee";
private static final String MATCH_PARENT = "match_parent";
private static final String FILL_PARENT = "fill_parent";
private static final String WRAP_CONTENT = "wrap_content";
private static final String BOLD = "bold";
private static final String ITALIC = "italic";
private static final String BOLD_ITALIC = "bold|italic";
private static final String TEXT_ALIGNMENT_INHERIT = "inherit";
private static final String TEXT_ALIGNMENT_GRAVITY = "gravity";
private static final String TEXT_ALIGNMENT_CENTER = "center";
private static final String TEXT_ALIGNMENT_TEXT_START = "start";
private static final String TEXT_ALIGNMENT_TEXT_END = "end";
private static final String TEXT_ALIGNMENT_VIEW_START = "viewStart";
private static final String TEXT_ALIGNMENT_VIEW_END = "viewEnd";
private static final String SUFFIX_PX = "px";
private static final String SUFFIX_DP = "dp";
private static final String SUFFIX_SP = "sp";
private static final String SUFFIX_PT = "pt";
private static final String SUFFIX_IN = "in";
private static final String SUFFIX_MM = "mm";
private static final String ATTR_START_LITERAL = "?";
private static final String COLOR_PREFIX_LITERAL = "#";
private static final String DRAWABLE_LOCAL_RESOURCE_STR = "@drawable/";
private static final String STRING_LOCAL_RESOURCE_STR = "@string/";
private static final String TWEEN_LOCAL_RESOURCE_STR = "@anim/";
private static final String COLOR_LOCAL_RESOURCE_STR = "@color/";
private static final String DIMENSION_LOCAL_RESOURCE_STR = "@dimen/";
private static final String DRAWABLE_STR = "drawable";
private static final String ID_STR = "id";
private static final Pattern sAttributePattern = Pattern.compile("(\\?)(\\S*)(:?)(attr\\/?)(\\S*)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Map<String, Class> sHashMap = new HashMap<>();
private static final Map<String, Integer> sAttributeCache = new HashMap<>();
private static final Map<String, Integer> sStateMap = new HashMap<>();
private static final Map<String, Integer> sGravityMap = new HashMap<>();
private static final Map<String, Integer> sDividerMode = new HashMap<>();
private static final Map<String, Enum> sEllipsizeMode = new HashMap<>();
private static final Map<String, Integer> sVisibilityMode = new HashMap<>();
private static final Map<String, Integer> sTextAlignment = new HashMap<>();
private static final Map<String, Integer> sDimensionsMap = new HashMap<>();
private static final Map<String, Integer> sDimensionsUnitsMap = new HashMap<>();
private static final Map<String, ImageView.ScaleType> sImageScaleType = new HashMap<>();
private static Map<String, Integer> styleMap = new HashMap<>();
private static Map<String, Integer> attributeMap = new HashMap<>();
static {
sStateMap.put("state_pressed", android.R.attr.state_pressed);
sStateMap.put("state_enabled", android.R.attr.state_enabled);
sStateMap.put("state_focused", android.R.attr.state_focused);
sStateMap.put("state_hovered", android.R.attr.state_hovered);
sStateMap.put("state_selected", android.R.attr.state_selected);
sStateMap.put("state_checkable", android.R.attr.state_checkable);
sStateMap.put("state_checked", android.R.attr.state_checked);
sStateMap.put("state_activated", android.R.attr.state_activated);
sStateMap.put("state_window_focused", android.R.attr.state_window_focused);
sGravityMap.put(CENTER, Gravity.CENTER);
sGravityMap.put(CENTER_HORIZONTAL, Gravity.CENTER_HORIZONTAL);
sGravityMap.put(CENTER_VERTICAL, Gravity.CENTER_VERTICAL);
sGravityMap.put(LEFT, Gravity.LEFT);
sGravityMap.put(RIGHT, Gravity.RIGHT);
sGravityMap.put(TOP, Gravity.TOP);
sGravityMap.put(BOTTOM, Gravity.BOTTOM);
sGravityMap.put(START, Gravity.START);
sGravityMap.put(END, Gravity.END);
sDividerMode.put(END, LinearLayout.SHOW_DIVIDER_END);
sDividerMode.put(MIDDLE, LinearLayout.SHOW_DIVIDER_MIDDLE);
sDividerMode.put(BEGINNING, LinearLayout.SHOW_DIVIDER_BEGINNING);
sEllipsizeMode.put(END, TextUtils.TruncateAt.END);
sEllipsizeMode.put(START, TextUtils.TruncateAt.START);
sEllipsizeMode.put(MARQUEE, TextUtils.TruncateAt.MARQUEE);
sEllipsizeMode.put(MIDDLE, TextUtils.TruncateAt.MIDDLE);
sVisibilityMode.put(VISIBLE, View.VISIBLE);
sVisibilityMode.put(INVISIBLE, View.INVISIBLE);
sVisibilityMode.put(GONE, View.GONE);
sDimensionsMap.put(MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
sDimensionsMap.put(FILL_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
sDimensionsMap.put(WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
sDimensionsUnitsMap.put(SUFFIX_PX, TypedValue.COMPLEX_UNIT_PX);
sDimensionsUnitsMap.put(SUFFIX_DP, TypedValue.COMPLEX_UNIT_DIP);
sDimensionsUnitsMap.put(SUFFIX_SP, TypedValue.COMPLEX_UNIT_SP);
sDimensionsUnitsMap.put(SUFFIX_PT, TypedValue.COMPLEX_UNIT_PT);
sDimensionsUnitsMap.put(SUFFIX_IN, TypedValue.COMPLEX_UNIT_IN);
sDimensionsUnitsMap.put(SUFFIX_MM, TypedValue.COMPLEX_UNIT_MM);
sImageScaleType.put(CENTER, ImageView.ScaleType.CENTER);
sImageScaleType.put("center_crop", ImageView.ScaleType.CENTER_CROP);
sImageScaleType.put("center_inside", ImageView.ScaleType.CENTER_INSIDE);
sImageScaleType.put("fitCenter", ImageView.ScaleType.FIT_CENTER);
sImageScaleType.put("fit_xy", ImageView.ScaleType.FIT_XY);
sImageScaleType.put("matrix", ImageView.ScaleType.MATRIX);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
sTextAlignment.put(TEXT_ALIGNMENT_INHERIT, View.TEXT_ALIGNMENT_INHERIT);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
sTextAlignment.put(TEXT_ALIGNMENT_GRAVITY, View.TEXT_ALIGNMENT_GRAVITY);
sTextAlignment.put(TEXT_ALIGNMENT_CENTER, View.TEXT_ALIGNMENT_CENTER);
sTextAlignment.put(TEXT_ALIGNMENT_TEXT_START, View.TEXT_ALIGNMENT_TEXT_START);
sTextAlignment.put(TEXT_ALIGNMENT_TEXT_END, View.TEXT_ALIGNMENT_TEXT_END);
sTextAlignment.put(TEXT_ALIGNMENT_VIEW_START, View.TEXT_ALIGNMENT_VIEW_START);
sTextAlignment.put(TEXT_ALIGNMENT_VIEW_END, View.TEXT_ALIGNMENT_VIEW_END);
}
}
public static int parseInt(String attributeValue) {
int number;
if (ProteusConstants.DATA_NULL.equals(attributeValue)) {
return 0;
}
try {
number = Integer.parseInt(attributeValue);
} catch (NumberFormatException e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, attributeValue + " is NAN. Error: " + e.getMessage());
}
number = 0;
}
return number;
}
public static float parseFloat(String attributeValue) {
float number;
if (ProteusConstants.DATA_NULL.equals(attributeValue)) {
return 0;
}
try {
number = Float.parseFloat(attributeValue);
} catch (NumberFormatException e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, attributeValue + " is NAN. Error: " + e.getMessage());
}
number = 0;
}
return number;
}
public static double parseDouble(String attributeValue) {
double number;
if (ProteusConstants.DATA_NULL.equals(attributeValue)) {
return 0;
}
try {
number = Double.parseDouble(attributeValue);
} catch (NumberFormatException e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, attributeValue + " is NAN. Error: " + e.getMessage());
}
number = 0;
}
return number;
}
public static int parseGravity(String attributeValue) {
String[] gravities = attributeValue.split("\\|");
int returnGravity = Gravity.NO_GRAVITY;
for (String gravity : gravities) {
Integer gravityValue = sGravityMap.get(gravity.trim().toLowerCase());
if (null != gravityValue) {
returnGravity |= gravityValue;
}
}
return returnGravity;
}
public static int parseDividerMode(String attributeValue) {
Integer returnValue = sDividerMode.get(attributeValue);
return returnValue == null ? LinearLayout.SHOW_DIVIDER_NONE : returnValue;
}
public static Enum parseEllipsize(String attributeValue) {
Enum returnValue = sEllipsizeMode.get(attributeValue);
return returnValue == null ? TextUtils.TruncateAt.END : returnValue;
}
public static int parseVisibility(JsonElement element) {
Integer returnValue = null;
if (element.isJsonPrimitive()) {
String attributeValue = element.getAsString();
returnValue = sVisibilityMode.get(attributeValue);
if (null == returnValue && (attributeValue.isEmpty() ||
FALSE.equals(attributeValue) ||
ProteusConstants.DATA_NULL.equals(attributeValue))) {
returnValue = View.GONE;
}
} else if (element.isJsonNull()) {
returnValue = View.GONE;
}
return returnValue == null ? View.VISIBLE : returnValue;
}
public static int parseInvisibility(JsonElement element) {
Integer returnValue = null;
if (element.isJsonPrimitive()) {
String attributeValue = element.getAsString();
returnValue = sVisibilityMode.get(attributeValue);
if (null == returnValue && (attributeValue.isEmpty() ||
FALSE.equals(attributeValue) ||
ProteusConstants.DATA_NULL.equals(attributeValue))) {
returnValue = View.VISIBLE;
}
} else if (element.isJsonNull()) {
returnValue = View.VISIBLE;
}
return returnValue == null ? View.GONE : returnValue;
}
public static float parseDimension(final String dimension, Context context) {
Integer parameter = sDimensionsMap.get(dimension);
if (parameter != null) {
return parameter;
}
int length = dimension.length();
if (length < 2) {
return 0;
}
// find the units and value by splitting at the second-last character of the dimension
Integer units = sDimensionsUnitsMap.get(dimension.substring(length - 2));
String stringValue = dimension.substring(0, length - 2);
if (units != null) {
float value = parseFloat(stringValue);
DisplayMetrics displayMetric = context.getResources().getDisplayMetrics();
return TypedValue.applyDimension(units, value, displayMetric);
}
// check if dimension is a local resource
if (dimension.startsWith(DIMENSION_LOCAL_RESOURCE_STR)) {
float value;
try {
int resourceId = context.getResources().getIdentifier(dimension, "dimen", context.getPackageName());
value = (int) context.getResources().getDimension(resourceId);
} catch (Exception e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, "could not find a dimension with name " + dimension + ". Error: " + e.getMessage());
}
value = 0;
}
return value;
}
// check if dimension is an attribute value
if (dimension.startsWith(ATTR_START_LITERAL)) {
float value;
try {
String[] dimenArr = dimension.substring(1, length).split(":");
String style = dimenArr[0];
String attr = dimenArr[1];
Integer styleId = styleMap.get(style);
if (styleId == null) {
styleId = R.style.class.getField(style).getInt(null);
styleMap.put(style, styleId);
}
Integer attrId = attributeMap.get(attr);
if (attrId == null) {
attrId = R.attr.class.getField(attr).getInt(null);
attributeMap.put(attr, attrId);
}
TypedArray a = context.getTheme().obtainStyledAttributes(styleId, new int[]{attrId});
value = a.getDimensionPixelSize(0, 0);
a.recycle();
} catch (Exception e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, "could not find a dimension with name " + dimension + ". Error: " + e.getMessage());
}
value = 0;
}
return value;
}
return 0;
}
public static int getAttributeId(Context context, String attribute) {
Integer result = sAttributeCache.get(attribute);
if (null == result && attribute.length() > 1) {
try {
String attributeName = "";
String packageName = "";
Matcher matcher = sAttributePattern.matcher(attribute);
if (matcher.matches()) {
attributeName = matcher.group(5);
packageName = matcher.group(2);
} else {
attributeName = attribute.substring(1);
}
Class clazz = null;
if (!TextUtils.isEmpty(packageName)) {
packageName = packageName.substring(0, packageName.length() - 1);
} else {
packageName = context.getPackageName();
}
String className = packageName + ".R$attr";
clazz = sHashMap.get(className);
if (null == clazz) {
clazz = Class.forName(className);
sHashMap.put(className, clazz);
}
if (null != clazz) {
Field field = clazz.getField(attributeName);
if (null != field) {
result = field.getInt(null);
sAttributeCache.put(attribute, result);
}
}
} catch (ClassNotFoundException e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, e.getMessage() + "");
}
} catch (NoSuchFieldException e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, e.getMessage() + "");
}
} catch (IllegalAccessException e) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, e.getMessage() + "");
}
}
}
return result == null ? 0 : result;
}
public static boolean isColor(String color) {
return color.startsWith(COLOR_PREFIX_LITERAL);
}
public static int parseColor(String color) {
try {
return Color.parseColor(color);
} catch (IllegalArgumentException ex) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, "Invalid color : " + color + ". Using #000000");
}
return Color.BLACK;
}
}
public static Integer parseId(String id) {
if (ProteusConstants.DATA_NULL.equals(id)) {
return null;
}
try {
return Integer.valueOf(id);
} catch (NumberFormatException ex) {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, id + " is not a valid resource ID.");
}
}
return null;
}
public static boolean parseBoolean(String trueOrFalse) {
return TRUE.equalsIgnoreCase(trueOrFalse);
}
public static int parseRelativeLayoutBoolean(String trueOrFalse) {
return TRUE.equalsIgnoreCase(trueOrFalse) ? RelativeLayout.TRUE : 0;
}
public static void addRelativeLayoutRule(View view, int verb, int anchor) {
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams instanceof RelativeLayout.LayoutParams) {
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) layoutParams;
params.addRule(verb, anchor);
view.setLayoutParams(params);
} else {
if (ProteusConstants.isLoggingEnabled()) {
Log.e(TAG, "cannot add relative layout rules when container is not relative");
}
}
}
public static int dpToPx(float dp) {
return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
}
public static float pxToDp(int px) {
return (px / Resources.getSystem().getDisplayMetrics().density);
}
public static int parseTextStyle(String attributeValue) {
int typeface = Typeface.NORMAL;
if (attributeValue != null) {
attributeValue = attributeValue.toLowerCase();
switch (attributeValue) {
case BOLD:
typeface = Typeface.BOLD;
break;
case ITALIC:
typeface = Typeface.ITALIC;
break;
case BOLD_ITALIC:
typeface = Typeface.BOLD_ITALIC;
break;
default:
typeface = Typeface.NORMAL;
break;
}
}
return typeface;
}
public static boolean isLocalResourceAttribute(String attributeValue) {
return attributeValue.startsWith(ATTR_START_LITERAL);
}
public static boolean isLocalStringResource(String attributeValue) {
return attributeValue.startsWith(STRING_LOCAL_RESOURCE_STR);
}
public static boolean isLocalDrawableResource(String attributeValue) {
return attributeValue.startsWith(DRAWABLE_LOCAL_RESOURCE_STR);
}
public static boolean isTweenAnimationResource(String attributeValue) {
return attributeValue.startsWith(TWEEN_LOCAL_RESOURCE_STR);
}
public static boolean isLocalColorResource(String attributeValue) {
return attributeValue.startsWith(COLOR_LOCAL_RESOURCE_STR);
}
public static Pair<int[], JsonElement> parseState(JsonObject stateObject) {
//drawable
JsonElement drawableJson = stateObject.get(DRAWABLE_STR);
if (null != drawableJson) {
//states
Set<Map.Entry<String, JsonElement>> entries = stateObject.entrySet();
List<Integer> statesToReturn = new ArrayList<>();
for (Map.Entry<String, JsonElement> entry : entries) {
JsonElement value = entry.getValue();
String state = entry.getKey();
Integer stateInteger = sStateMap.get(state);
if (stateInteger != null) {
String stateValue = value.getAsString();
//e.g state_pressed = true state_pressed = false
statesToReturn.add(ParseHelper.parseBoolean(stateValue) ? stateInteger : -stateInteger);
}
}
int[] statesToReturnInteger = new int[statesToReturn.size()];
for (int i = 0; i < statesToReturn.size(); i++) {
statesToReturnInteger[i] = statesToReturn.get(i);
}
return new Pair<>(statesToReturnInteger, drawableJson);
}
return null;
}
/**
* Uses reflection to fetch the R.id from the given class.
* This method is faster than using {@link android.content.res.Resources#getResourceName(int)}
*
* @param variableName the name of the variable
* @param с The class
* @return resource id
*/
public static int getResId(String variableName, Class<?> с) {
Field field;
int resId = 0;
try {
field = с.getField(variableName);
resId = field.getInt(null);
} catch (Exception e) {
e.printStackTrace();
}
return resId;
}
/**
* Get int resource id, by just passing the string value of android:id from xml file.
* Note : This method only works for @android:id or @+android:id right now
*
* @param fullResIdString the string id of the view
* @return the number id of the view
*/
public static int getAndroidResIdByXmlResId(String fullResIdString) {
if (fullResIdString != null) {
int i = fullResIdString.indexOf("/");
if (i >= 0) {
String idString = fullResIdString.substring(i + 1);
return getResId(idString, android.R.id.class);
}
}
return View.NO_ID;
}
/**
* Parses a single layer item (represented by {@param child}) inside a layer list and gives
* a pair of android:id and a string for the drawable path.
*
* @param child
* @return The layer info as a {@link Pair}
*/
public static Pair<Integer, JsonElement> parseLayer(JsonObject child) {
JsonElement id = child.get(ID_STR);
int androidResIdByXmlResId = View.NO_ID;
String idAsString = null;
if (id != null) {
idAsString = id.getAsString();
}
if (idAsString != null) {
androidResIdByXmlResId = getAndroidResIdByXmlResId(idAsString);
}
JsonElement drawableElement = child.get(DRAWABLE_STR);
return (drawableElement != null) ? new Pair<>(androidResIdByXmlResId, drawableElement) : null;
}
/**
* Parses a image view scale type
*
* @param attributeValue value of the scale type attribute
* @return {@link android.widget.ImageView.ScaleType} enum
*/
public static ImageView.ScaleType parseScaleType(String attributeValue) {
return !TextUtils.isEmpty(attributeValue) ? sImageScaleType.get(attributeValue) : null;
}
/**
* parses Text Alignment
*
* @param attributeValue value of the typeface attribute
* @return the text alignment value
*/
public static Integer parseTextAlignment(String attributeValue) {
return !TextUtils.isEmpty(attributeValue) ? sTextAlignment.get(attributeValue) : null;
}
}