/*
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.stetho.inspector.elements.android;
import android.app.Activity;
import android.app.Application;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import com.facebook.stetho.common.Accumulator;
import com.facebook.stetho.common.Predicate;
import com.facebook.stetho.common.ThreadBound;
import com.facebook.stetho.common.Util;
import com.facebook.stetho.inspector.elements.DocumentProvider;
import com.facebook.stetho.inspector.elements.Descriptor;
import com.facebook.stetho.inspector.elements.DescriptorProvider;
import com.facebook.stetho.inspector.elements.DescriptorMap;
import com.facebook.stetho.inspector.elements.DocumentProviderListener;
import com.facebook.stetho.inspector.elements.NodeDescriptor;
import com.facebook.stetho.inspector.elements.ObjectDescriptor;
import com.facebook.stetho.inspector.helper.ThreadBoundProxy;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
final class AndroidDocumentProvider extends ThreadBoundProxy
implements DocumentProvider, AndroidDescriptorHost {
private static final int INSPECT_OVERLAY_COLOR = 0x40FFFFFF;
private static final int INSPECT_HOVER_COLOR = 0x404040ff;
private final Rect mHighlightingBoundsRect = new Rect();
private final Rect mHitRect = new Rect();
private final Application mApplication;
private final DescriptorMap mDescriptorMap;
private final AndroidDocumentRoot mDocumentRoot;
private final ViewHighlighter mHighlighter;
private final InspectModeHandler mInspectModeHandler;
private @Nullable DocumentProviderListener mListener;
// We don't yet have an an implementation for reliably detecting fine-grained changes in the
// View tree. So, for now at least, we have a timer that runs every so often and just reports
// that we changed. Our listener will then read the entire Document from us and transmit the
// changes to Chrome. Detecting, reporting, and traversing fine-grained changes is a future work
// item (see Issue #210).
private static final long REPORT_CHANGED_INTERVAL_MS = 1000;
private boolean mIsReportChangesTimerPosted = false;
private final Runnable mReportChangesTimer = new Runnable() {
@Override
public void run() {
mIsReportChangesTimerPosted = false;
if (mListener != null) {
mListener.onPossiblyChanged();
mIsReportChangesTimerPosted = true;
postDelayed(this, REPORT_CHANGED_INTERVAL_MS);
}
}
};
public AndroidDocumentProvider(
Application application,
List<DescriptorProvider> descriptorProviders,
ThreadBound enforcer) {
super(enforcer);
mApplication = Util.throwIfNull(application);
mDocumentRoot = new AndroidDocumentRoot(application);
mDescriptorMap = new DescriptorMap()
.beginInit()
.registerDescriptor(Activity.class, new ActivityDescriptor())
.registerDescriptor(AndroidDocumentRoot.class, mDocumentRoot)
.registerDescriptor(Application.class, new ApplicationDescriptor())
.registerDescriptor(Dialog.class, new DialogDescriptor())
.registerDescriptor(Object.class, new ObjectDescriptor())
.registerDescriptor(TextView.class, new TextViewDescriptor())
.registerDescriptor(View.class, new ViewDescriptor())
.registerDescriptor(ViewGroup.class, new ViewGroupDescriptor())
.registerDescriptor(Window.class, new WindowDescriptor());
DialogFragmentDescriptor.register(mDescriptorMap);
FragmentDescriptor.register(mDescriptorMap);
for (int i = 0, size = descriptorProviders.size(); i < size; ++i) {
final DescriptorProvider descriptorProvider = descriptorProviders.get(i);
descriptorProvider.registerDescriptor(mDescriptorMap);
}
mDescriptorMap.setHost(this).endInit();
mHighlighter = ViewHighlighter.newInstance();
mInspectModeHandler = new InspectModeHandler();
}
@Override
public void dispose() {
verifyThreadAccess();
mHighlighter.clearHighlight();
mInspectModeHandler.disable();
removeCallbacks(mReportChangesTimer);
mIsReportChangesTimerPosted = false;
mListener = null;
}
@Override
public void setListener(DocumentProviderListener listener) {
verifyThreadAccess();
mListener = listener;
if (mListener == null && mIsReportChangesTimerPosted) {
mIsReportChangesTimerPosted = false;
removeCallbacks(mReportChangesTimer);
} else if (mListener != null && !mIsReportChangesTimerPosted) {
mIsReportChangesTimerPosted = true;
postDelayed(mReportChangesTimer, REPORT_CHANGED_INTERVAL_MS);
}
}
@Override
public Object getRootElement() {
verifyThreadAccess();
return mDocumentRoot;
}
@Override
public NodeDescriptor getNodeDescriptor(Object element) {
verifyThreadAccess();
return getDescriptor(element);
}
@Override
public void highlightElement(Object element, int color) {
verifyThreadAccess();
final HighlightableDescriptor descriptor = getHighlightableDescriptor(element);
if (descriptor == null) {
mHighlighter.clearHighlight();
return;
}
mHighlightingBoundsRect.setEmpty();
final View highlightingView =
descriptor.getViewAndBoundsForHighlighting(element, mHighlightingBoundsRect);
if (highlightingView == null) {
mHighlighter.clearHighlight();
return;
}
mHighlighter.setHighlightedView(
highlightingView,
mHighlightingBoundsRect,
color);
}
@Override
public void hideHighlight() {
verifyThreadAccess();
mHighlighter.clearHighlight();
}
@Override
public void setInspectModeEnabled(boolean enabled) {
verifyThreadAccess();
if (enabled) {
mInspectModeHandler.enable();
} else {
mInspectModeHandler.disable();
}
}
@Override
public void setAttributesAsText(Object element, String text) {
verifyThreadAccess();
Descriptor descriptor = mDescriptorMap.get(element.getClass());
if (descriptor != null) {
descriptor.setAttributesAsText(element, text);
}
}
// Descriptor.Host implementation
@Override
public Descriptor getDescriptor(Object element) {
return (element == null) ? null : mDescriptorMap.get(element.getClass());
}
@Override
public void onAttributeModified(Object element, String name, String value) {
if (mListener != null) {
mListener.onAttributeModified(element, name, value);
}
}
@Override
public void onAttributeRemoved(Object element, String name) {
if (mListener != null) {
mListener.onAttributeRemoved(element, name);
}
}
// AndroidDescriptorHost implementation
@Override
@Nullable
public HighlightableDescriptor getHighlightableDescriptor(@Nullable Object element) {
if (element == null) {
return null;
}
HighlightableDescriptor highlightableDescriptor = null;
Class<?> theClass = element.getClass();
Descriptor lastDescriptor = null;
while (highlightableDescriptor == null && theClass != null) {
Descriptor descriptor = mDescriptorMap.get(theClass);
if (descriptor == null) {
return null;
}
if (descriptor != lastDescriptor && descriptor instanceof HighlightableDescriptor) {
highlightableDescriptor = ((HighlightableDescriptor) descriptor);
}
lastDescriptor = descriptor;
theClass = theClass.getSuperclass();
}
return highlightableDescriptor;
}
private void getWindows(final Accumulator<Window> accumulator) {
Descriptor appDescriptor = getDescriptor(mApplication);
if (appDescriptor != null) {
Accumulator<Object> elementAccumulator = new Accumulator<Object>() {
@Override
public void store(Object element) {
if (element instanceof Window) {
// Store the Window and do not recurse into its children.
accumulator.store((Window) element);
} else {
// Recursively scan this element's children in search of more Windows.
Descriptor elementDescriptor = getDescriptor(element);
if (elementDescriptor != null) {
elementDescriptor.getChildren(element, this);
}
}
}
};
appDescriptor.getChildren(mApplication, elementAccumulator);
}
}
private final class InspectModeHandler {
private final Predicate<View> mViewSelector = new Predicate<View>() {
@Override
public boolean apply(View view) {
return !(view instanceof DocumentHiddenView);
}
};
private List<View> mOverlays;
public void enable() {
verifyThreadAccess();
if (mOverlays != null) {
disable();
}
mOverlays = new ArrayList<>();
getWindows(new Accumulator<Window>() {
@Override
public void store(Window object) {
if (object.peekDecorView() instanceof ViewGroup) {
final ViewGroup decorView = (ViewGroup) object.peekDecorView();
OverlayView overlayView = new OverlayView(mApplication);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
layoutParams.height = WindowManager.LayoutParams.MATCH_PARENT;
decorView.addView(overlayView, layoutParams);
decorView.bringChildToFront(overlayView);
mOverlays.add(overlayView);
}
}
});
}
public void disable() {
verifyThreadAccess();
if (mOverlays == null) {
return;
}
for (int i = 0; i < mOverlays.size(); ++i) {
final View overlayView = mOverlays.get(i);
ViewGroup decorViewGroup = (ViewGroup)overlayView.getParent();
decorViewGroup.removeView(overlayView);
}
mOverlays = null;
}
private final class OverlayView extends DocumentHiddenView {
public OverlayView(Context context) {
super(context);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(INSPECT_OVERLAY_COLOR);
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
Object elementToHighlight = getParent();
while (true) {
final HighlightableDescriptor descriptor =
getHighlightableDescriptor(elementToHighlight);
if (descriptor == null) {
break;
}
mHitRect.setEmpty();
final Object element =
descriptor.getElementToHighlightAtPosition(elementToHighlight, x, y, mHitRect);
x -= mHitRect.left;
y -= mHitRect.top;
if (element == elementToHighlight) {
break;
}
elementToHighlight = element;
}
if (elementToHighlight != null) {
final HighlightableDescriptor descriptor =
getHighlightableDescriptor(elementToHighlight);
if (descriptor != null) {
final View viewToHighlight =
descriptor.getViewAndBoundsForHighlighting(
elementToHighlight,
mHighlightingBoundsRect);
if (event.getAction() != MotionEvent.ACTION_CANCEL) {
if (viewToHighlight != null) {
mHighlighter.setHighlightedView(
viewToHighlight,
mHighlightingBoundsRect,
INSPECT_HOVER_COLOR);
if (event.getAction() == MotionEvent.ACTION_UP) {
if (mListener != null) {
mListener.onInspectRequested(elementToHighlight);
}
}
}
}
}
}
return true;
}
}
}
}