RootsOracle.java

/*
 * Copyright (C) 2014 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 androidx.test.espresso.base;

import static com.google.common.base.Preconditions.checkState;

import android.os.Build;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.view.WindowManager.LayoutParams;
import androidx.test.espresso.Root;
import com.google.common.collect.Lists;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;

/**
 * Provides access to all root views in an application.
 *
 * <p>95% of the time this is unnecessary and we can operate solely on current Activity's root view
 * as indicated by getWindow().getDecorView(). However in the case of popup windows, menus, and
 * dialogs the actual view hierarchy we should be operating on is not accessible thru public apis.
 *
 * <p>In the spirit of degrading gracefully when new api levels break compatibility, callers should
 * handle a list of size 0 by assuming getWindow().getDecorView() on the currently resumed activity
 * is the sole root - this assumption will be correct often enough.
 *
 * <p>Obviously, you need to be on the main thread to use this.
 */
final class RootsOracle implements ActiveRootLister {

  private static final String TAG = RootsOracle.class.getSimpleName();
  private static final String WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl";
  private static final String WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal";
  private static final String VIEWS_FIELD = "mViews";
  private static final String WINDOW_PARAMS_FIELD = "mParams";
  private static final String GET_DEFAULT_IMPL = "getDefault";
  private static final String GET_GLOBAL_INSTANCE = "getInstance";

  private final Looper mainLooper;
  private boolean initialized;
  private Object windowManagerObj;
  private Field viewsField;
  private Field paramsField;

  @Inject
  RootsOracle(Looper mainLooper) {
    this.mainLooper = mainLooper;
  }

  @SuppressWarnings("unchecked")
  @Override
  public List<Root> listActiveRoots() {
    checkState(mainLooper.equals(Looper.myLooper()), "must be called on main thread.");

    if (!initialized) {
      initialize();
    }

    if (null == windowManagerObj) {
      Log.w(TAG, "No reflective access to windowmanager object.");
      return Lists.newArrayList();
    }

    if (null == viewsField) {
      Log.w(TAG, "No reflective access to mViews");
      return Lists.newArrayList();
    }
    if (null == paramsField) {
      Log.w(TAG, "No reflective access to mParams");
      return Lists.newArrayList();
    }

    List<View> views = null;
    List<LayoutParams> params = null;

    try {
      if (Build.VERSION.SDK_INT < 19) {
        views = Arrays.asList((View[]) viewsField.get(windowManagerObj));
        params = Arrays.asList((LayoutParams[]) paramsField.get(windowManagerObj));
      } else {
        views = (List<View>) viewsField.get(windowManagerObj);
        params = (List<LayoutParams>) paramsField.get(windowManagerObj);
      }
    } catch (RuntimeException re) {
      Log.w(
          TAG,
          String.format(
              "Reflective access to %s or %s on %s failed.",
              viewsField, paramsField, windowManagerObj),
          re);
      return Lists.newArrayList();
    } catch (IllegalAccessException iae) {
      Log.w(
          TAG,
          String.format(
              "Reflective access to %s or %s on %s failed.",
              viewsField, paramsField, windowManagerObj),
          iae);
      return Lists.newArrayList();
    }

    List<Root> roots = Lists.newArrayList();
    for (int i = views.size() - 1; i > -1; i--) {
      roots.add(
          new Root.Builder()
              .withDecorView(views.get(i))
              .withWindowLayoutParams(params.get(i))
              .build());
    }

    return roots;
  }

  private void initialize() {
    initialized = true;
    String accessClass =
        Build.VERSION.SDK_INT > 16 ? WINDOW_MANAGER_GLOBAL_CLAZZ : WINDOW_MANAGER_IMPL_CLAZZ;
    String instanceMethod = Build.VERSION.SDK_INT > 16 ? GET_GLOBAL_INSTANCE : GET_DEFAULT_IMPL;

    try {
      Class<?> clazz = Class.forName(accessClass);
      Method getMethod = clazz.getMethod(instanceMethod);
      windowManagerObj = getMethod.invoke(null);
      viewsField = clazz.getDeclaredField(VIEWS_FIELD);
      viewsField.setAccessible(true);
      paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD);
      paramsField.setAccessible(true);
    } catch (InvocationTargetException ite) {
      Log.e(
          TAG,
          String.format("could not invoke: %s on %s", instanceMethod, accessClass),
          ite.getCause());
    } catch (ClassNotFoundException cnfe) {
      Log.e(TAG, String.format("could not find class: %s", accessClass), cnfe);
    } catch (NoSuchFieldException nsfe) {
      Log.e(
          TAG,
          String.format(
              "could not find field: %s or %s on %s",
              WINDOW_PARAMS_FIELD, VIEWS_FIELD, accessClass),
          nsfe);
    } catch (NoSuchMethodException nsme) {
      Log.e(
          TAG, String.format("could not find method: %s on %s", instanceMethod, accessClass), nsme);
    } catch (RuntimeException re) {
      Log.e(
          TAG,
          String.format(
              "reflective setup failed using obj: %s method: %s field: %s",
              accessClass, instanceMethod, VIEWS_FIELD),
          re);
    } catch (IllegalAccessException iae) {
      Log.e(
          TAG,
          String.format(
              "reflective setup failed using obj: %s method: %s field: %s",
              accessClass, instanceMethod, VIEWS_FIELD),
          iae);
    }
  }
}