blob: 681977e59e721fe3b2e277b12948c6703463e035 [file]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/android/view_android.h"
#include <utility>
#include "base/android/jni_android.h"
#include "base/functional/callback.h"
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/android/event_forwarder.h"
#include "ui/android/test_view_android_delegate.h"
#include "ui/android/ui_android_features.h"
#include "ui/android/view_android.h"
#include "ui/android/view_android_observer.h"
#include "ui/android/window_android.h"
#include "ui/color/android/color_provider_bridge.h"
#include "ui/color/color_provider_key.h"
#include "ui/events/android/event_handler_android.h"
#include "ui/events/android/motion_event_android_factory.h"
#include "ui/events/android/motion_event_android_java.h"
#include "ui/events/motionevent_jni_headers/MotionEvent_jni.h"
#include "ui/events/test/scoped_event_test_tick_clock.h"
namespace ui {
using base::android::JavaRef;
class TestViewAndroid : public ViewAndroid {
public:
TestViewAndroid(ViewAndroid::LayoutType layout_type)
: ViewAndroid(layout_type) {}
float GetDipScale() override { return 1.f; }
};
class TestEventHandler : public EventHandlerAndroid {
public:
TestEventHandler() {}
bool OnTouchEvent(const MotionEventAndroid& event) override {
touch_called_ = true;
return handle_event_;
}
void OnSizeChanged() override { onsize_called_ = true; }
void SetHandleEvent(bool handle_event) { handle_event_ = handle_event; }
bool TouchEventHandled() { return touch_called_ && handle_event_; }
bool TouchEventCalled() { return touch_called_; }
bool OnSizeCalled() { return onsize_called_; }
void Reset() {
touch_called_ = false;
onsize_called_ = false;
}
private:
bool handle_event_{true}; // Marks as event was consumed. True by default.
bool touch_called_{false};
bool onsize_called_{false};
};
class ViewAndroidBoundsTest : public testing::Test {
public:
ViewAndroidBoundsTest()
: root_(ViewAndroid::LayoutType::kMatchParent),
view1_(ViewAndroid::LayoutType::kNormal),
view2_(ViewAndroid::LayoutType::kNormal),
view3_(ViewAndroid::LayoutType::kNormal),
viewm_(ViewAndroid::LayoutType::kMatchParent) {
root_.GetEventForwarder();
view1_.set_event_handler(&handler1_);
view2_.set_event_handler(&handler2_);
view3_.set_event_handler(&handler3_);
viewm_.set_event_handler(&handlerm_);
}
void Reset() {
handler1_.Reset();
handler2_.Reset();
handler3_.Reset();
handlerm_.Reset();
test_clock_.SetNowTicks(base::TimeTicks());
}
void GenerateTouchEventAt(float x, float y) {
ui::MotionEventAndroid::Pointer pointer0(0, x, y, 0, 0, 0, 0, 0, 0);
JNIEnv* env = base::android::AttachCurrentThread();
base::android::ScopedJavaLocalRef<jobject> obj =
JNI_MotionEvent::Java_MotionEvent_obtain(
env, /*downTime=*/0, /*eventTime=*/0, /*action=*/0, /*x=*/0,
/*y=*/0, /*metaState=*/0);
auto event = ui::MotionEventAndroidFactory::CreateFromJava(
env, obj,
/*pix_to_dip=*/1.f,
/*ticks_x=*/0,
/*ticks_y=*/0,
/*tick_multiplier=*/0,
/*oldest_event_time=*/base::TimeTicks(),
/*android_action=*/0,
/*pointer_count=*/1,
/*history_size=*/0,
/*action_index=*/0,
/*android_action_button=*/0,
/*android_gesture_classification=*/0,
/*android_button_state=*/0,
/*raw_offset_x_pixels=*/0,
/*raw_offset_y_pixels=*/0,
/*for_touch_handle=*/false, &pointer0,
/*pointer1=*/nullptr);
root_.OnTouchEvent(*event);
}
void ExpectHit(const TestEventHandler& hitHandler) {
TestEventHandler* handlers[4] = {&handler1_, &handler2_, &handler3_,
&handlerm_};
for (auto* handler : handlers) {
if (&hitHandler == handler)
EXPECT_TRUE(handler->TouchEventHandled());
else
EXPECT_FALSE(handler->TouchEventHandled());
}
Reset();
}
void AddThreeStackedChildren() {
view1_.SetLayoutForTesting(50, 50, 400, 600);
view2_.SetLayoutForTesting(50, 50, 400, 600);
view3_.SetLayoutForTesting(50, 50, 400, 600);
root_.AddChild(&view1_);
root_.AddChild(&view2_);
root_.AddChild(&view3_);
}
void RemoveAllChildren(ViewAndroid* view) {
view->RemoveAllChildren(/*attached_to_window=*/false);
}
// Teardown path that notifies each child, and so can re-enter.
void RemoveAllChildrenAttached(ViewAndroid* view) {
view->RemoveAllChildren(/*attached_to_window=*/true);
}
TestViewAndroid root_;
TestViewAndroid view1_;
TestViewAndroid view2_;
TestViewAndroid view3_;
TestViewAndroid viewm_; // match-parent view
TestEventHandler handler1_;
TestEventHandler handler2_;
TestEventHandler handler3_;
TestEventHandler handlerm_;
ui::test::ScopedEventTestTickClock test_clock_;
};
TEST_F(ViewAndroidBoundsTest, MatchesViewInFront) {
view1_.SetLayoutForTesting(50, 50, 400, 600);
view2_.SetLayoutForTesting(50, 50, 400, 600);
root_.AddChild(&view2_);
root_.AddChild(&view1_);
GenerateTouchEventAt(100.f, 100.f);
ExpectHit(handler1_);
// View 2 moves up to the top, and events should hit it from now.
root_.MoveToFront(&view2_);
GenerateTouchEventAt(100.f, 100.f);
ExpectHit(handler2_);
// View 2 moves back to the bottom, and events should hit View 1 again.
root_.MoveToBack(&view2_);
GenerateTouchEventAt(100.f, 100.f);
ExpectHit(handler1_);
}
TEST_F(ViewAndroidBoundsTest, MatchesViewArea) {
view1_.SetLayoutForTesting(50, 50, 200, 200);
view2_.SetLayoutForTesting(20, 20, 400, 600);
root_.AddChild(&view2_);
root_.AddChild(&view1_);
// Falls within |view1_|'s bounds
GenerateTouchEventAt(100.f, 100.f);
ExpectHit(handler1_);
// Falls within |view2_|'s bounds
GenerateTouchEventAt(300.f, 400.f);
ExpectHit(handler2_);
}
TEST_F(ViewAndroidBoundsTest, MatchesViewAfterMove) {
view1_.SetLayoutForTesting(50, 50, 200, 200);
view2_.SetLayoutForTesting(20, 20, 400, 600);
root_.AddChild(&view2_);
root_.AddChild(&view1_);
GenerateTouchEventAt(100.f, 100.f);
ExpectHit(handler1_);
view1_.SetLayoutForTesting(150, 150, 200, 200);
GenerateTouchEventAt(100.f, 100.f);
ExpectHit(handler2_);
}
TEST_F(ViewAndroidBoundsTest, MatchesViewSizeOfkMatchParent) {
view1_.SetLayoutForTesting(20, 20, 400, 600);
view2_.SetLayoutForTesting(50, 50, 200, 200);
root_.AddChild(&view1_);
root_.AddChild(&view2_);
view1_.AddChild(&viewm_);
GenerateTouchEventAt(100.f, 100.f);
ExpectHit(handler2_);
GenerateTouchEventAt(300.f, 400.f);
ExpectHit(handler1_);
handler1_.SetHandleEvent(false);
GenerateTouchEventAt(300.f, 400.f);
EXPECT_TRUE(handler1_.TouchEventCalled());
ExpectHit(handlerm_);
}
TEST_F(ViewAndroidBoundsTest, MatchesViewsWithOffset) {
view1_.SetLayoutForTesting(10, 20, 150, 100);
view2_.SetLayoutForTesting(20, 30, 40, 30);
view3_.SetLayoutForTesting(70, 30, 40, 30);
root_.AddChild(&view1_);
view1_.AddChild(&view2_);
view1_.AddChild(&view3_);
GenerateTouchEventAt(70, 30);
ExpectHit(handler1_);
handler1_.SetHandleEvent(false);
GenerateTouchEventAt(40, 60);
EXPECT_TRUE(handler1_.TouchEventCalled());
ExpectHit(handler2_);
GenerateTouchEventAt(100, 70);
EXPECT_TRUE(handler1_.TouchEventCalled());
ExpectHit(handler3_);
}
TEST_F(ViewAndroidBoundsTest, OnSizeChanged) {
root_.AddChild(&view1_);
view1_.AddChild(&viewm_);
view1_.AddChild(&view3_);
// Size event propagates to non-match-parent children only.
view1_.OnSizeChanged(100, 100);
EXPECT_TRUE(handler1_.OnSizeCalled());
EXPECT_TRUE(handlerm_.OnSizeCalled());
EXPECT_FALSE(handler3_.OnSizeCalled());
Reset();
// Match-parent view should not receive size events in the first place.
EXPECT_DCHECK_DEATH(viewm_.OnSizeChanged(100, 200));
EXPECT_FALSE(handlerm_.OnSizeCalled());
EXPECT_FALSE(handler3_.OnSizeCalled());
viewm_.RemoveFromParent();
viewm_.OnSizeChangedInternal(gfx::Size(0, 0)); // Reset the size.
Reset();
view1_.OnSizeChanged(100, 100);
// Size event is generated for a newly added, match-parent child view.
EXPECT_FALSE(handlerm_.OnSizeCalled());
view1_.AddChild(&viewm_);
EXPECT_TRUE(handlerm_.OnSizeCalled());
EXPECT_FALSE(handler3_.OnSizeCalled());
viewm_.RemoveFromParent();
Reset();
view1_.OnSizeChanged(100, 100);
// Size event won't propagate if the children already have the same size.
view1_.AddChild(&viewm_);
EXPECT_FALSE(handlerm_.OnSizeCalled());
EXPECT_FALSE(handler3_.OnSizeCalled());
}
// Event handler that runs an arbitrary callback during dispatch, for reentrancy
// tests.
class ReentrantActionHandler : public EventHandlerAndroid {
public:
ReentrantActionHandler() = default;
explicit ReentrantActionHandler(base::RepeatingClosure action)
: action_(std::move(action)) {}
bool OnTouchEvent(const MotionEventAndroid& event) override {
++count_;
if (action_) {
action_.Run();
}
return false;
}
void OnPhysicalBackingSizeChanged(
std::optional<base::TimeDelta> deadline_override) override {
++count_;
if (action_) {
action_.Run();
}
}
void set_action(base::RepeatingClosure action) {
action_ = std::move(action);
}
int count() const { return count_; }
private:
base::RepeatingClosure action_;
int count_ = 0;
};
// Exercises StepIteratorsOver across both iteration directions:
// - Reverse (HitTest): current element erases its next sibling, and the head
// element erases itself.
// - Forward (OnPhysicalBackingSizeChanged): current element erases its next
// sibling (cursor sits on it and must be bumped), and the tail erases itself.
TEST_F(ViewAndroidBoundsTest, ChildRemovalReentrancy) {
AddThreeStackedChildren();
// Reverse pass: view3_ erases view2_ (next); view1_ erases itself (head).
ReentrantActionHandler h3(
base::BindLambdaForTesting([&]() { view2_.RemoveFromParent(); }));
ReentrantActionHandler h2;
ReentrantActionHandler h1(
base::BindLambdaForTesting([&]() { view1_.RemoveFromParent(); }));
view3_.set_event_handler(&h3);
view2_.set_event_handler(&h2);
view1_.set_event_handler(&h1);
GenerateTouchEventAt(100.f, 100.f);
EXPECT_EQ(1, h3.count());
EXPECT_EQ(0, h2.count());
EXPECT_EQ(1, h1.count());
// Reset tree to [view1_, view2_, view3_] for the forward pass: view1_ erases
// view2_ (next); view3_ erases itself (tail).
RemoveAllChildren(&root_);
root_.AddChild(&view1_);
root_.AddChild(&view2_);
root_.AddChild(&view3_);
h1.set_action(
base::BindLambdaForTesting([&]() { view2_.RemoveFromParent(); }));
h3.set_action(
base::BindLambdaForTesting([&]() { view3_.RemoveFromParent(); }));
root_.OnPhysicalBackingSizeChanged(gfx::Size(100, 100), std::nullopt);
EXPECT_EQ(2, h1.count());
EXPECT_EQ(0, h2.count());
EXPECT_EQ(2, h3.count());
}
// Reordering mid-traversal via splice: StepIteratorsOver keeps the cursor from
// following the moved node. Promoting a view moves it into already-visited
// territory (skipped); demoting moves it ahead of the cursor (still visited).
TEST_F(ViewAndroidBoundsTest, HitTestReorderReentrancy) {
AddThreeStackedChildren();
handler1_.SetHandleEvent(false);
handler2_.SetHandleEvent(false);
ReentrantActionHandler h3(base::BindLambdaForTesting([&]() {
root_.MoveToFront(&view1_); // Moved behind reverse cursor -> skipped.
root_.MoveToBack(&view2_); // Moved ahead of reverse cursor -> visited.
}));
view3_.set_event_handler(&h3);
GenerateTouchEventAt(100.f, 100.f);
EXPECT_EQ(1, h3.count());
EXPECT_TRUE(handler2_.TouchEventCalled());
EXPECT_FALSE(handler1_.TouchEventCalled());
}
// RemoveAllChildren() clears `parent_` on each child it detaches. If the child
// reparents itself from its OnDetachedFromWindow() notification, that clear
// must not run afterwards and strand the child with a null parent.
TEST_F(ViewAndroidBoundsTest, RemoveAllChildrenKeepsReparentedChild) {
class ReparentingObserver : public ViewAndroidObserver {
public:
ReparentingObserver(ViewAndroid* child, ViewAndroid* new_parent)
: child_(child), new_parent_(new_parent) {}
void OnDetachedFromWindow() override {
if (reparented_) {
return;
}
reparented_ = true;
new_parent_->AddChild(child_);
}
private:
raw_ptr<ViewAndroid> child_;
raw_ptr<ViewAndroid> new_parent_;
bool reparented_ = false;
};
TestViewAndroid new_parent(ViewAndroid::LayoutType::kNormal);
root_.AddChild(&view1_);
root_.AddChild(&view2_);
ReparentingObserver observer(&view2_, &new_parent);
view2_.AddObserver(&observer);
RemoveAllChildrenAttached(&root_);
EXPECT_EQ(&new_parent, view2_.parent());
EXPECT_EQ(1u, new_parent.GetChildrenCountForTesting());
EXPECT_EQ(0u, root_.GetChildrenCountForTesting());
view2_.RemoveObserver(&observer);
}
// Helper observer that invokes a callback on OnDetachedFromWindow().
class ReentrantDetachObserver : public ViewAndroidObserver {
public:
explicit ReentrantDetachObserver(base::OnceClosure action)
: action_(std::move(action)) {}
void OnDetachedFromWindow() override {
if (action_) {
std::move(action_).Run();
}
}
private:
base::OnceClosure action_;
};
// Verifies RemoveAllChildren() handles a child unlinking itself during
// OnDetachedFromWindow().
TEST_F(ViewAndroidBoundsTest, RemoveAllChildrenSurvivesChildUnlink) {
ReentrantDetachObserver observer(
base::BindLambdaForTesting([&]() { view1_.RemoveFromParent(); }));
view1_.AddObserver(&observer);
bool view2_detached = false;
ReentrantDetachObserver view2_observer(
base::BindLambdaForTesting([&]() { view2_detached = true; }));
view2_.AddObserver(&view2_observer);
root_.AddChild(&view1_);
root_.AddChild(&view2_);
RemoveAllChildrenAttached(&root_);
EXPECT_TRUE(view2_detached);
EXPECT_EQ(0u, root_.GetChildrenCountForTesting());
EXPECT_EQ(nullptr, view1_.parent());
EXPECT_EQ(nullptr, view2_.parent());
view1_.RemoveObserver(&observer);
view2_.RemoveObserver(&view2_observer);
}
// Verifies RemoveAllChildren() handles a child being destroyed during
// OnDetachedFromWindow().
TEST_F(ViewAndroidBoundsTest, RemoveAllChildrenSurvivesChildDestruction) {
auto child_to_destroy =
std::make_unique<TestViewAndroid>(ViewAndroid::LayoutType::kNormal);
ReentrantDetachObserver destroy_observer(
base::BindLambdaForTesting([&]() { child_to_destroy.reset(); }));
bool view2_detached = false;
ReentrantDetachObserver view2_observer(
base::BindLambdaForTesting([&]() { view2_detached = true; }));
view2_.AddObserver(&view2_observer);
// Detaching view1_ triggers destruction of `child_to_destroy`.
child_to_destroy->AddChild(&view1_);
view1_.AddObserver(&destroy_observer);
root_.AddChild(child_to_destroy.get());
root_.AddChild(&view2_);
RemoveAllChildrenAttached(&root_);
EXPECT_FALSE(child_to_destroy);
EXPECT_TRUE(view2_detached);
EXPECT_EQ(0u, root_.GetChildrenCountForTesting());
EXPECT_EQ(nullptr, view1_.parent());
EXPECT_EQ(nullptr, view2_.parent());
view1_.RemoveObserver(&destroy_observer);
view2_.RemoveObserver(&view2_observer);
}
TEST(ViewAndroidTest, ChecksMultipleEventForwarders) {
ViewAndroid parent;
ViewAndroid child;
parent.GetEventForwarder();
child.GetEventForwarder();
EXPECT_DCHECK_DEATH(parent.AddChild(&child));
ViewAndroid parent2;
ViewAndroid child2;
parent2.GetEventForwarder();
parent2.AddChild(&child2);
EXPECT_DCHECK_DEATH(child2.GetEventForwarder());
ViewAndroid window;
ViewAndroid wcv1, wcv2;
ViewAndroid rwhv1a, rwhv1b, rwhv2;
wcv1.GetEventForwarder();
wcv2.GetEventForwarder();
window.AddChild(&wcv1);
wcv1.AddChild(&rwhv1a);
wcv1.AddChild(&rwhv1b);
wcv2.AddChild(&rwhv2);
// window should be able to add wcv2 since there's only one event forwarder
// in the path window - wcv2* - rwvh2
window.AddChild(&wcv2);
// Additional event forwarder will cause failure.
EXPECT_DCHECK_DEATH(rwhv2.GetEventForwarder());
}
class Observer : public ViewAndroidObserver {
public:
Observer() = default;
void OnAttachedToWindow() override { attached_ = true; }
void OnDetachedFromWindow() override { attached_ = false; }
void OnViewAndroidDestroyed() override { destroyed_ = true; }
void OnDelegateSet() override { delegate_set_ = true; }
bool attached_ = false;
bool destroyed_ = false;
bool delegate_set_ = false;
};
TEST(ViewAndroidTest, Observer) {
std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window =
ui::WindowAndroid::CreateForTesting();
Observer top_observer;
Observer bottom_observer;
Observer top_observer2;
{
ViewAndroid top;
ViewAndroid bottom;
top.AddObserver(&top_observer);
bottom.AddObserver(&bottom_observer);
top.AddChild(&bottom);
EXPECT_FALSE(top_observer.attached_);
EXPECT_FALSE(bottom_observer.attached_);
// Views in a tree all get notified of 'attached' event.
window->get()->AddChild(&top);
EXPECT_TRUE(top_observer.attached_);
EXPECT_TRUE(bottom_observer.attached_);
// Observer, upon addition, does not get notified of the current
// attached state.
top.AddObserver(&top_observer2);
EXPECT_FALSE(top_observer2.attached_);
bottom.RemoveFromParent();
EXPECT_FALSE(bottom_observer.attached_);
top.RemoveFromParent();
EXPECT_FALSE(top_observer.attached_);
window->get()->AddChild(&top);
EXPECT_TRUE(top_observer.attached_);
// View, upon addition to a tree in the attached state, should be notified.
top.AddChild(&bottom);
EXPECT_TRUE(bottom_observer.attached_);
// Observer is notified about the new delegate.
TestViewAndroidDelegate view_android_delegate;
EXPECT_FALSE(top_observer.delegate_set_);
view_android_delegate.SetupTestDelegate(&top);
EXPECT_TRUE(top_observer.delegate_set_);
// Views in a tree all get notified of 'detached' event.
top.RemoveFromParent();
EXPECT_FALSE(top_observer.attached_);
EXPECT_FALSE(bottom_observer.attached_);
// Remove the second top observer to test the destruction notification.
top.RemoveObserver(&top_observer2);
}
EXPECT_TRUE(top_observer.destroyed_);
EXPECT_FALSE(top_observer2.destroyed_);
EXPECT_TRUE(bottom_observer.destroyed_);
}
TEST(ViewAndroidTest, WindowAndroidDestructionDetachesAllViewAndroid) {
std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window =
ui::WindowAndroid::CreateForTesting();
ViewAndroid top;
ViewAndroid bottom;
Observer top_observer;
Observer bottom_observer;
top.AddObserver(&top_observer);
bottom.AddObserver(&bottom_observer);
window->get()->AddChild(&top);
top.AddChild(&bottom);
EXPECT_TRUE(top_observer.attached_);
EXPECT_TRUE(bottom_observer.attached_);
window.reset();
EXPECT_FALSE(top_observer.attached_);
EXPECT_FALSE(bottom_observer.attached_);
top.RemoveObserver(&top_observer);
bottom.RemoveObserver(&bottom_observer);
}
TEST(ViewAndroidTest, WindowAndroidColorProviderSource) {
std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window =
ui::WindowAndroid::CreateForTesting();
ui::WindowAndroid* window_android = window->get();
// Test that color provider key holds the context from WindowAndroid.
ui::ColorProviderKey key = window_android->GetColorProviderKey();
EXPECT_NE(key.context_hash, 0);
JNIEnv* env = base::android::AttachCurrentThread();
base::android::ScopedJavaLocalRef<jobject> context = key.context.get(env);
EXPECT_FALSE(context.is_null());
}
TEST(ViewAndroidTest,
WindowAndroidColorProviderKeyIsContextAgnosticWithoutBridge) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(kAvoidPerContextColorProviders);
// No ColorProviderBridge is registered in unit tests, so the key should not
// be scoped to the Context.
ASSERT_FALSE(ColorProviderBridge::HasInstance());
std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window =
ui::WindowAndroid::CreateForTesting();
ui::ColorProviderKey key = window->get()->GetColorProviderKey();
EXPECT_EQ(key.context_hash, 0);
JNIEnv* env = base::android::AttachCurrentThread();
EXPECT_TRUE(key.context.get(env).is_null());
}
TEST(ViewAndroidTest, WindowAndroidColorProviderKeyKeepsContextWithBridge) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(kAvoidPerContextColorProviders);
std::unique_ptr<ui::WindowAndroid::ScopedWindowAndroidForTesting> window =
ui::WindowAndroid::CreateForTesting();
ui::ColorProviderKey key = window->get()->GetColorProviderKeyForTesting(
/*has_color_provider_bridge=*/true);
EXPECT_NE(key.context_hash, 0);
JNIEnv* env = base::android::AttachCurrentThread();
EXPECT_FALSE(key.context.get(env).is_null());
}
} // namespace ui