/* TMTS - Android automation testing Framework. Copyright (C) 2010-2011 TaoBao UI AutoMan Team This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., HuaXing road, Hangzhou,China. Email:taichan@taobao.com,shidun@taobao.com,bingyang@taobao.com */ package com.taobao.tmts.framework.utils; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; import junit.framework.Assert; import android.app.Activity; import android.app.Instrumentation; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; /** * This class contains view methods. Examples are getViews(), * getCurrentTextViews(), getCurrentImageViews(). * * @author Renas Reda, renas.reda@jayway.com * */ public class ViewUtils { private final Instrumentation inst; private final ActivityUtils activityUtils; private final Sleeper sleeper; /** * Constructs this object. * * @param inst * the {@code Instrumentation} instance * @param activityUtils * the {@code ActivityUtils} instance * @param sleeper * the {@code Sleeper} instance * */ public ViewUtils(Instrumentation inst, ActivityUtils activityUtils, Sleeper sleeper) { this.inst = inst; this.activityUtils = activityUtils; this.sleeper = sleeper; } /** * Returns the absolute top parent {@code View} in for a given {@code View}. * * @param view * the {@code View} whose top parent is requested * @return the top parent {@code View} * */ public View getTopParent(View view) { if (view.getParent() != null && !view.getParent().getClass().getName() .equals("android.view.ViewRoot")) { return getTopParent((View) view.getParent()); } else { return view; } } /** * Returns the list item parent. It is used by clickInList(). * * @param view * the view who's parent is requested * @return the parent of the view * */ public View getListItemParent(View view) { if (view.getParent() != null && !(view.getParent() instanceof android.widget.ListView)) { return getListItemParent((View) view.getParent()); } else { return view; } } /** * Returns the scroll or list parent view * * @param view * the view who's parent should be returned * @return the parent scroll view, list view or null * */ public View getScrollOrListParent(View view) { if (!(view instanceof android.widget.AbsListView) && !(view instanceof android.widget.ScrollView)) { try { return getScrollOrListParent((View) view.getParent()); } catch (Exception e) { return null; } } else { return view; } } /** * Returns views from the shown DecorViews. * * @param onlySufficientlyVisible * if only sufficiently visible views should be returned * @return all the views contained in the DecorViews * */ public ArrayList<View> getAllViews(boolean onlySufficientlyVisible) { final View[] views = getWindowDecorViews(); final ArrayList<View> allViews = new ArrayList<View>(); final View[] nonDecorViews = getNonDecorViews(views); if (views != null && views.length > 0) { View view; for (int i = 0; i < nonDecorViews.length; i++) { view = nonDecorViews[i]; try { addChildren(allViews, (ViewGroup) view, onlySufficientlyVisible); } catch (Exception ignored) { } } view = getRecentDecorView(views); try { addChildren(allViews, (ViewGroup) view, onlySufficientlyVisible); } catch (Exception ignored) { } } return allViews; } /** * Returns the most recent DecorView * * @param views * the views to check * @return the most recent DecorView * */ public final View getRecentDecorView(View[] views) { final View[] decorViews = new View[views.length]; int i = 0; View view; for (int j = 0; j < views.length; j++) { view = views[j]; if (view.getClass() .getName() .equals("com.android.internal.policy.impl.PhoneWindow$DecorView")) { decorViews[i] = view; i++; } } return getRecentContainer(decorViews); } /** * Returns the most recent view container * * @param views * the views to check * @return the most recent view container * */ private final View getRecentContainer(View[] views) { View container = null; long drawingTime = 0; View view; for (int i = 0; i < views.length; i++) { view = views[i]; if (view != null && view.isShown() && view.hasWindowFocus() && view.getDrawingTime() > drawingTime) { container = view; drawingTime = view.getDrawingTime(); } } return container; } /** * Returns all views that are non DecorViews * * @param views * the views to check * @return the non DecorViews */ public final View[] getNonDecorViews(View[] views) { final View[] decorViews = new View[views.length]; int i = 0; View view; for (int j = 0; j < views.length; j++) { view = views[j]; if (!(view.getClass().getName() .equals("com.android.internal.policy.impl.PhoneWindow$DecorView"))) { decorViews[i] = view; i++; } } return decorViews; } /** * Returns a {@code View} with a given id. * * @param id * the R.id of the {@code View} to be returned * @return a {@code View} with a given id */ public View getView(int id) { final Activity activity = activityUtils.getCurrentActivity(false); return activity.findViewById(id); } /** * Extracts all {@code View}s located in the currently active * {@code Activity}, recursively. * * @param parent * the {@code View} whose children should be returned, or * {@code null} for all * @param onlySufficientlyVisible * if only sufficiently visible views should be returned * @return all {@code View}s located in the currently active * {@code Activity}, never {@code null} * */ public ArrayList<View> getViews(View parent, boolean onlySufficientlyVisible) { activityUtils.getCurrentActivity(false); final ArrayList<View> views = new ArrayList<View>(); final View parentToUse; if (parent == null) { inst.waitForIdleSync(); return getAllViews(onlySufficientlyVisible); } else { parentToUse = parent; views.add(parentToUse); if (parentToUse instanceof ViewGroup) { addChildren(views, (ViewGroup) parentToUse, onlySufficientlyVisible); } } return views; } /** * Adds all children of {@code viewGroup} (recursively) into {@code views}. * * @param views * an {@code ArrayList} of {@code View}s * @param viewGroup * the {@code ViewGroup} to extract children from * @param onlySufficientlyVisible * if only sufficiently visible views should be returned * */ public void addChildren(ArrayList<View> views, ViewGroup viewGroup, boolean onlySufficientlyVisible) { for (int i = 0; i < viewGroup.getChildCount(); i++) { final View child = viewGroup.getChildAt(i); if (onlySufficientlyVisible && isViewSufficientlyShown(child)) views.add(child); else if (!onlySufficientlyVisible) views.add(child); if (child instanceof ViewGroup) { addChildren(views, (ViewGroup) child, onlySufficientlyVisible); } } } /** * Returns true if the view is sufficiently shown * * @param view * the view to check * @return true if the view is sufficiently shown * */ public final boolean isViewSufficientlyShown(View view) { final int[] xyView = new int[2]; final int[] xyParent = new int[2]; if (view == null) return false; final float viewHeight = view.getHeight(); final View parent = getScrollOrListParent(view); view.getLocationOnScreen(xyView); if (parent == null) { xyParent[1] = 0; } else { parent.getLocationOnScreen(xyParent); } if (xyView[1] + (viewHeight / 2.0f) > getScrollListWindowHeight(view)) return false; else if (xyView[1] + (viewHeight / 2.0f) < xyParent[1]) return false; return true; } /** * Returns the height of the scroll or list view parent * * @param view * the view who's parents height should be returned * @return the height of the scroll or list view parent */ public float getScrollListWindowHeight(View view) { final int[] xyParent = new int[2]; final View parent = getScrollOrListParent(view); final float windowHeight; if (parent == null) { windowHeight = activityUtils.getCurrentActivity(false) .getWindowManager().getDefaultDisplay().getHeight(); } else { parent.getLocationOnScreen(xyParent); windowHeight = xyParent[1] + parent.getHeight(); } return windowHeight; } /** * Returns a {@code View} with a certain index, from the list of current * {@code View}s of the specified type. * * @param classToFilterBy * which {@code View}s to choose from * @param index * choose among all instances of this type, e.g. * {@code Button.class} or {@code EditText.class} * @return a {@code View} with a certain index, from the list of current * {@code View}s of the specified type */ public <T extends View> T getView(Class<T> classToFilterBy, int index) { sleeper.sleep(); inst.waitForIdleSync(); ArrayList<T> views = getCurrentViews(classToFilterBy); views = removeInvisibleViews(views); T view = null; try { view = views.get(index); } catch (IndexOutOfBoundsException e) { Assert.assertTrue("No " + classToFilterBy.getSimpleName() + " with index " + index + " is found!", false); } return view; } /** * Returns a {@code View} that shows a given text, from the list of current * {@code View}s of the specified type. * * @param classToFilterBy * which {@code View}s to choose from * @param text * the text that the view shows * @return a {@code View} showing a given text, from the list of current * {@code View}s of the specified type */ public <T extends TextView> T getView(Class<T> classToFilterBy, String text) { sleeper.sleep(); inst.waitForIdleSync(); ArrayList<T> views = getCurrentViews(classToFilterBy); T viewToReturn = null; for (T view : views) { if (view.getText().toString().equals(text)) viewToReturn = view; } if (viewToReturn == null) Assert.assertTrue("No " + classToFilterBy.getSimpleName() + " with text " + text + " is found!", false); return viewToReturn; } /** * Returns a view. * * @param classToFilterBy * the class to filter by * @param views * the list with views * @param index * the index of the view * @return the view with a given index */ public final <T extends View> T getView(Class<T> classToFilterBy, ArrayList<T> views, int index) { T viewToReturn = null; long drawingTime = 0; if (views == null) { views = getCurrentViews(classToFilterBy); } if (index < 1) { for (T view : views) { if (view.getDrawingTime() > drawingTime) { drawingTime = view.getDrawingTime(); viewToReturn = view; } } } else { try { viewToReturn = views.get(index); } catch (Exception ignored) { } } return viewToReturn; } /** * Returns an {@code ArrayList} of {@code View}s of the specified * {@code Class} located in the current {@code Activity}. * * @param classToFilterBy * return all instances of this class, e.g. {@code Button.class} * or {@code GridView.class} * @return an {@code ArrayList} of {@code View}s of the specified * {@code Class} located in the current {@code Activity} */ public <T extends View> ArrayList<T> getCurrentViews( Class<T> classToFilterBy) { return getCurrentViews(classToFilterBy, null); } /** * Returns an {@code ArrayList} of {@code View}s of the specified * {@code Class} located under the specified {@code parent}. * * @param classToFilterBy * return all instances of this class, e.g. {@code Button.class} * or {@code GridView.class} * @param parent * the parent {@code View} for where to start the traversal * @return an {@code ArrayList} of {@code View}s of the specified * {@code Class} located under the specified {@code parent} */ public <T extends View> ArrayList<T> getCurrentViews( Class<T> classToFilterBy, View parent) { ArrayList<T> filteredViews = new ArrayList<T>(); List<View> allViews = getViews(parent, true); for (View view : allViews) { if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) { filteredViews.add(classToFilterBy.cast(view)); } } return filteredViews; } private static Class<?> windowManager; static { try { windowManager = Class.forName("android.view.WindowManagerImpl"); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } catch (SecurityException e) { e.printStackTrace(); } } /** * Returns the WindorDecorViews shown on the screen * * @return the WindorDecorViews shown on the screen * */ public View[] getWindowDecorViews() { Field viewsField; Field instanceField; try { viewsField = windowManager.getDeclaredField("mViews"); instanceField = windowManager.getDeclaredField("mWindowManager"); viewsField.setAccessible(true); instanceField.setAccessible(true); Object instance = instanceField.get(null); return (View[]) viewsField.get(instance); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } /** * Removes invisible {@code View}s * * @param viewList * an {@code ArrayList} with {@code View}s that is being checked * for invisible {@code View}s. * @return a filtered {@code ArrayList} with no invisible {@code View}s. */ public static <T extends View> ArrayList<T> removeInvisibleViews( ArrayList<T> viewList) { ArrayList<T> tmpViewList = new ArrayList<T>(viewList.size()); for (T view : viewList) { if (view != null && view.getVisibility() != View.GONE && view.getVisibility() != View.INVISIBLE) { tmpViewList.add(view); } } return tmpViewList; } /** * Filters views * * @param classToFilterBy * the class to filter * @param viewList * the ArrayList to filter form * @return an ArrayList with filtered views */ public static <T extends View> ArrayList<T> filterViews( Class<T> classToFilterBy, ArrayList<View> viewList) { ArrayList<T> filteredViews = new ArrayList<T>(viewList.size()); for (View view : viewList) { if (view != null && classToFilterBy.isAssignableFrom(view.getClass())) { filteredViews.add(classToFilterBy.cast(view)); } } return filteredViews; } }