com.facebook.appevents.codeless.internal.ViewHierarchy.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.appevents.codeless.internal.ViewHierarchy.java

Source

/*
 * Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
 *
 * You are hereby granted a non-exclusive, worldwide, royalty-free license to use,
 * copy, modify, and distribute this software in source code or binary form for use
 * in connection with the web services and APIs provided by Facebook.
 *
 * As with any software that integrates with the Facebook platform, your use of
 * this software is subject to the Facebook Developer Principles and Policies
 * [http://developers.facebook.com/policy/]. This copyright notice shall be
 * included in all copies or substantial portions of the software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package com.facebook.appevents.codeless.internal;

import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.v4.view.NestedScrollingChild;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RatingBar;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.TimePicker;

import com.facebook.internal.Utility;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class ViewHierarchy {
    private static final String TAG = ViewHierarchy.class.getCanonicalName();
    private static final String GET_ACCESSIBILITY_METHOD = "getAccessibilityDelegate";

    private static final String ID_KEY = "id";
    private static final String CLASS_NAME_KEY = "classname";
    private static final String CLASS_TYPE_BITMASK_KEY = "classtypebitmask";
    private static final String TEXT_KEY = "text";
    private static final String DESC_KEY = "description";
    private static final String DIMENSION_KEY = "dimension";
    private static final String TAG_KEY = "tag";
    private static final String CHILDREN_VIEW_KEY = "childviews";
    private static final String HINT_KEY = "hint";
    private static final String DIMENSION_TOP_KEY = "top";
    private static final String DIMENSION_LEFT_KEY = "left";
    private static final String DIMENSION_WIDTH_KEY = "width";
    private static final String DIMENSION_HEIGHT_KEY = "height";
    private static final String DIMENSION_SCROLL_X_KEY = "scrollx";
    private static final String DIMENSION_SCROLL_Y_KEY = "scrolly";
    private static final String DIMENSION_VISIBILITY_KEY = "visibility";
    private static final String TEXT_SIZE = "font_size";
    private static final String TEXT_IS_BOLD = "is_bold";
    private static final String TEXT_IS_ITALIC = "is_italic";
    private static final String TEXT_STYLE = "text_style";
    private static final String ICON_BITMAP = "icon_image";

    private static final int TEXTVIEW_BITMASK = 0;
    private static final int IMAGEVIEW_BITMASK = 1;
    private static final int BUTTON_BITMASK = 2;
    private static final int CLICKABLE_VIEW_BITMASK = 5;
    private static final int REACT_NATIVE_BUTTON_BITMASK = 6;
    private static final int ADAPTER_VIEW_ITEM_BITMASK = 9;
    private static final int LABEL_BITMASK = 10;
    private static final int INPUT_BITMASK = 11;
    private static final int PICKER_BITMASK = 12;
    private static final int SWITCH_BITMASK = 13;
    private static final int RADIO_GROUP_BITMASK = 14;
    private static final int CHECKBOX_BITMASK = 15;
    private static final int RATINGBAR_BITMASK = 16;

    private static final int ICON_MAX_EDGE_LENGTH = 44;

    @Nullable
    public static ViewGroup getParentOfView(View view) {
        if (null == view) {
            return null;
        }

        ViewParent parent = view.getParent();
        if (parent != null && parent instanceof ViewGroup) {
            return (ViewGroup) parent;
        }

        return null;
    }

    public static List<View> getChildrenOfView(View view) {
        ArrayList<View> children = new ArrayList<>();

        if (view != null && view instanceof ViewGroup) {
            ViewGroup viewGroup = (ViewGroup) view;
            int count = viewGroup.getChildCount();
            for (int i = 0; i < count; i++) {
                children.add(viewGroup.getChildAt(i));
            }
        }

        return children;
    }

    public static JSONObject setBasicInfoOfView(View view, JSONObject json) {
        try {
            String text = getTextOfView(view);
            String hint = getHintOfView(view);
            Object tag = view.getTag();
            CharSequence description = view.getContentDescription();

            json.put(CLASS_NAME_KEY, view.getClass().getCanonicalName());
            json.put(CLASS_TYPE_BITMASK_KEY, getClassTypeBitmask(view));
            json.put(ID_KEY, view.getId());
            if (!SensitiveUserDataUtils.isSensitiveUserData(view)) {
                json.put(TEXT_KEY, text);
            } else {
                json.put(TEXT_KEY, "");
            }
            json.put(HINT_KEY, hint);
            if (tag != null) {
                json.put(TAG_KEY, tag.toString());
            }
            if (description != null) {
                json.put(DESC_KEY, description.toString());
            }
            JSONObject dimension = getDimensionOfView(view);
            json.put(DIMENSION_KEY, dimension);
        } catch (JSONException e) {
            Utility.logd(TAG, e);
        }

        return json;
    }

    public static JSONObject setAppearanceOfView(View view, JSONObject json, float displayDensity) {
        try {
            JSONObject textStyle = new JSONObject();
            if (view instanceof TextView) {
                TextView textView = (TextView) view;
                Typeface typeface = textView.getTypeface();
                if (typeface != null) {
                    textStyle.put(TEXT_SIZE, textView.getTextSize());
                    textStyle.put(TEXT_IS_BOLD, typeface.isBold());
                    textStyle.put(TEXT_IS_ITALIC, typeface.isItalic());
                    json.put(TEXT_STYLE, textStyle);
                }
            }
            if (view instanceof ImageView) {
                Drawable drawable = ((ImageView) view).getDrawable();
                if (drawable instanceof BitmapDrawable) {
                    if (view.getHeight() / displayDensity <= ICON_MAX_EDGE_LENGTH
                            && view.getWidth() / displayDensity <= ICON_MAX_EDGE_LENGTH) {
                        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
                        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
                        byte[] byteArray = byteArrayOutputStream.toByteArray();
                        String encoded = Base64.encodeToString(byteArray, Base64.DEFAULT);
                        json.put(ICON_BITMAP, encoded);
                    }
                }
            }
        } catch (JSONException e) {
            Utility.logd(TAG, e);
        }

        return json;
    }

    public static JSONObject getDictionaryOfView(View view) {
        JSONObject json = new JSONObject();

        try {
            json = setBasicInfoOfView(view, json);

            JSONArray childviews = new JSONArray();
            List<View> children = getChildrenOfView(view);
            for (int i = 0; i < children.size(); i++) {
                View child = children.get(i);
                JSONObject childInfo = getDictionaryOfView(child);
                childviews.put(childInfo);
            }
            json.put(CHILDREN_VIEW_KEY, childviews);

        } catch (JSONException e) {
            Log.e(TAG, "Failed to create JSONObject for view.", e);
        }

        return json;
    }

    private static int getClassTypeBitmask(View view) {
        int bitmask = 0;
        if (view instanceof ImageView) {
            bitmask |= (1 << IMAGEVIEW_BITMASK);
        }

        if (isClickableView(view)) {
            bitmask |= (1 << CLICKABLE_VIEW_BITMASK);
        }
        if (isAdapterViewItem(view)) {
            bitmask |= (1 << ADAPTER_VIEW_ITEM_BITMASK);
        }

        if (view instanceof TextView) {
            bitmask |= (1 << LABEL_BITMASK);
            bitmask |= (1 << TEXTVIEW_BITMASK);

            if (view instanceof Button) {
                bitmask |= (1 << BUTTON_BITMASK);

                if (view instanceof Switch) {
                    bitmask |= (1 << SWITCH_BITMASK);
                } else if (view instanceof CheckBox) {
                    bitmask |= (1 << CHECKBOX_BITMASK);
                }
            }

            if (view instanceof EditText) {
                bitmask |= (1 << INPUT_BITMASK);
            }
        } else if (view instanceof Spinner || view instanceof DatePicker) {
            bitmask |= (1 << PICKER_BITMASK);
        } else if (view instanceof RatingBar) {
            bitmask |= (1 << RATINGBAR_BITMASK);
        } else if (view instanceof RadioGroup) {
            bitmask |= (1 << RADIO_GROUP_BITMASK);
        } else if (view instanceof ViewGroup) {
            if (isRCTButton(view)) {
                bitmask |= (1 << REACT_NATIVE_BUTTON_BITMASK);
            }
        }

        return bitmask;
    }

    public static boolean isClickableView(View view) {
        try {
            Field listenerInfoField = null;
            listenerInfoField = Class.forName("android.view.View").getDeclaredField("mListenerInfo");
            if (listenerInfoField != null) {
                listenerInfoField.setAccessible(true);
            }

            Object listenerObj = null;
            listenerObj = listenerInfoField.get(view);
            if (listenerObj == null) {
                return false;
            }

            Field listenerField = null;
            View.OnClickListener listener = null;
            listenerField = Class.forName("android.view.View$ListenerInfo").getDeclaredField("mOnClickListener");
            if (listenerField != null) {
                listener = (View.OnClickListener) listenerField.get(listenerObj);
            }

            return (listener != null);
        } catch (Exception e) {
            Log.e(TAG, "Failed to check if the view is clickable.", e);
            return false;
        }
    }

    private static boolean isAdapterViewItem(View view) {
        ViewParent parent = view.getParent();
        if (parent != null) {
            if (parent instanceof AdapterView || parent instanceof NestedScrollingChild) {
                return true;
            }
        }

        return false;
    }

    public static String getTextOfView(View view) {
        Object textObj = null;
        if (view instanceof TextView) {
            textObj = ((TextView) view).getText();

            if (view instanceof Switch) {
                boolean isOn = ((Switch) view).isChecked();
                textObj = isOn ? "1" : "0";
            }
        } else if (view instanceof Spinner) {
            Object selectedItem = ((Spinner) view).getSelectedItem();
            if (selectedItem != null) {
                textObj = selectedItem.toString();
            }
        } else if (view instanceof DatePicker) {
            DatePicker picker = (DatePicker) view;
            int y = picker.getYear();
            int m = picker.getMonth();
            int d = picker.getDayOfMonth();
            textObj = String.format("%04d-%02d-%02d", y, m, d);
        } else if (view instanceof TimePicker) {
            TimePicker picker = (TimePicker) view;
            int h = picker.getCurrentHour();
            int m = picker.getCurrentMinute();
            textObj = String.format("%02d:%02d", h, m);
        } else if (view instanceof RadioGroup) {
            RadioGroup radioGroup = (RadioGroup) view;
            int checkedId = radioGroup.getCheckedRadioButtonId();
            int childCount = radioGroup.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = radioGroup.getChildAt(i);
                if (child.getId() == checkedId && child instanceof RadioButton) {
                    textObj = ((RadioButton) child).getText();
                    break;
                }
            }
        } else if (view instanceof RatingBar) {
            RatingBar bar = (RatingBar) view;
            float rating = bar.getRating();
            textObj = String.valueOf(rating);
        }

        return textObj == null ? "" : textObj.toString();
    }

    public static String getHintOfView(View view) {
        Object hintObj = null;
        if (view instanceof TextView) {
            hintObj = ((TextView) view).getHint();
        } else if (view instanceof EditText) {
            hintObj = ((EditText) view).getHint();
        }

        return hintObj == null ? "" : hintObj.toString();
    }

    private static JSONObject getDimensionOfView(View view) {
        JSONObject dimension = new JSONObject();

        try {
            dimension.put(DIMENSION_TOP_KEY, view.getTop());
            dimension.put(DIMENSION_LEFT_KEY, view.getLeft());
            dimension.put(DIMENSION_WIDTH_KEY, view.getWidth());
            dimension.put(DIMENSION_HEIGHT_KEY, view.getHeight());
            dimension.put(DIMENSION_SCROLL_X_KEY, view.getScrollX());
            dimension.put(DIMENSION_SCROLL_Y_KEY, view.getScrollY());
            dimension.put(DIMENSION_VISIBILITY_KEY, view.getVisibility());
        } catch (JSONException e) {
            Log.e(TAG, "Failed to create JSONObject for dimension.", e);
        }

        return dimension;
    }

    @Nullable
    public static View.AccessibilityDelegate getExistingDelegate(View view) {
        try {
            Class<?> viewClass = view.getClass();
            Method getAccessibilityDelegateMethod = viewClass.getMethod(GET_ACCESSIBILITY_METHOD);
            return (View.AccessibilityDelegate) getAccessibilityDelegateMethod.invoke(view);
        } catch (NoSuchMethodException e) {
            return null;
        } catch (NullPointerException e) {
            return null;
        } catch (SecurityException e) {
            return null;
        } catch (IllegalAccessException e) {
            return null;
        } catch (InvocationTargetException e) {
            return null;
        }
    }

    @Nullable
    public static View.OnTouchListener getExistingOnTouchListener(View view) {
        try {
            Field listenerInfoField = Class.forName("android.view.View").getDeclaredField("mListenerInfo");
            if (listenerInfoField != null) {
                listenerInfoField.setAccessible(true);
            }

            Object listenerObj = listenerInfoField.get(view);
            if (listenerObj == null) {
                return null;
            }

            View.OnTouchListener listener = null;
            Field listenerField = Class.forName("android.view.View$ListenerInfo")
                    .getDeclaredField("mOnTouchListener");
            if (listenerField != null) {
                listenerField.setAccessible(true);
                listener = (View.OnTouchListener) listenerField.get(listenerObj);
            }

            return listener;
        } catch (NoSuchFieldException e) {
            Utility.logd(TAG, e);
        } catch (ClassNotFoundException e) {
            Utility.logd(TAG, e);
        } catch (IllegalAccessException e) {
            Utility.logd(TAG, e);
        }
        return null;
    }

    public static boolean isRCTButton(View view) {
        String className = view.getClass().getName();
        return className.equals("com.facebook.react.views.view.ReactViewGroup") && getExistingDelegate(view) != null
                && ((ViewGroup) view).getChildCount() > 0;
    }

    public static boolean isRCTTextView(View view) {
        String className = view.getClass().getName();
        return className.equals("com.facebook.react.views.view.ReactTextView");
    }
}