Merge "Track focus changes on external displays (1/4)"
diff --git a/services/inputflinger/InputDispatcher.cpp b/services/inputflinger/InputDispatcher.cpp
index d609573..38104c4 100644
--- a/services/inputflinger/InputDispatcher.cpp
+++ b/services/inputflinger/InputDispatcher.cpp
@@ -235,6 +235,12 @@
}
}
+template<typename T, typename U>
+static T getValueByKey(std::unordered_map<U, T>& map, U key) {
+ typename std::unordered_map<U, T>::const_iterator it = map.find(key);
+ return it != map.end() ? it->second : T{};
+}
+
// --- InputDispatcher ---
@@ -244,6 +250,7 @@
mAppSwitchSawKeyDown(false), mAppSwitchDueTime(LONG_LONG_MAX),
mNextUnblockedEvent(nullptr),
mDispatchEnabled(false), mDispatchFrozen(false), mInputFilterEnabled(false),
+ mFocusedDisplayId(ADISPLAY_ID_DEFAULT),
mInputTargetWaitCause(INPUT_TARGET_WAIT_CAUSE_NONE) {
mLooper = new Looper(false);
@@ -808,8 +815,10 @@
if (entry->policyFlags & POLICY_FLAG_PASS_TO_USER) {
CommandEntry* commandEntry = postCommandLocked(
& InputDispatcher::doInterceptKeyBeforeDispatchingLockedInterruptible);
- if (mFocusedWindowHandle != nullptr) {
- commandEntry->inputWindowHandle = mFocusedWindowHandle;
+ sp<InputWindowHandle> focusedWindowHandle =
+ getValueByKey(mFocusedWindowHandlesByDisplay, getTargetDisplayId(entry));
+ if (focusedWindowHandle != nullptr) {
+ commandEntry->inputWindowHandle = focusedWindowHandle;
}
commandEntry->keyEntry = entry;
entry->refCount += 1;
@@ -1108,17 +1117,49 @@
mInputTargetWaitApplicationHandle.clear();
}
+/**
+ * Get the display id that the given event should go to. If this event specifies a valid display id,
+ * then it should be dispatched to that display. Otherwise, the event goes to the focused display.
+ * Focused display is the display that the user most recently interacted with.
+ */
+int32_t InputDispatcher::getTargetDisplayId(const EventEntry* entry) {
+ int32_t displayId;
+ switch (entry->type) {
+ case EventEntry::TYPE_KEY: {
+ const KeyEntry* typedEntry = static_cast<const KeyEntry*>(entry);
+ displayId = typedEntry->displayId;
+ break;
+ }
+ case EventEntry::TYPE_MOTION: {
+ const MotionEntry* typedEntry = static_cast<const MotionEntry*>(entry);
+ displayId = typedEntry->displayId;
+ break;
+ }
+ default: {
+ ALOGE("Unsupported event type '%" PRId32 "' for target display.", entry->type);
+ return ADISPLAY_ID_NONE;
+ }
+ }
+ return displayId == ADISPLAY_ID_NONE ? mFocusedDisplayId : displayId;
+}
+
int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
const EventEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {
int32_t injectionResult;
std::string reason;
+ int32_t displayId = getTargetDisplayId(entry);
+ sp<InputWindowHandle> focusedWindowHandle =
+ getValueByKey(mFocusedWindowHandlesByDisplay, displayId);
+ sp<InputApplicationHandle> focusedApplicationHandle =
+ getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);
+
// If there is no currently focused window and no focused application
// then drop the event.
- if (mFocusedWindowHandle == nullptr) {
- if (mFocusedApplicationHandle != nullptr) {
+ if (focusedWindowHandle == nullptr) {
+ if (focusedApplicationHandle != nullptr) {
injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
- mFocusedApplicationHandle, nullptr, nextWakeupTime,
+ focusedApplicationHandle, nullptr, nextWakeupTime,
"Waiting because no window has focus but there is a "
"focused application that may eventually add a window "
"when it finishes starting up.");
@@ -1131,23 +1172,23 @@
}
// Check permissions.
- if (! checkInjectionPermission(mFocusedWindowHandle, entry->injectionState)) {
+ if (!checkInjectionPermission(focusedWindowHandle, entry->injectionState)) {
injectionResult = INPUT_EVENT_INJECTION_PERMISSION_DENIED;
goto Failed;
}
// Check whether the window is ready for more input.
reason = checkWindowReadyForMoreInputLocked(currentTime,
- mFocusedWindowHandle, entry, "focused");
+ focusedWindowHandle, entry, "focused");
if (!reason.empty()) {
injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
- mFocusedApplicationHandle, mFocusedWindowHandle, nextWakeupTime, reason.c_str());
+ focusedApplicationHandle, focusedWindowHandle, nextWakeupTime, reason.c_str());
goto Unresponsive;
}
// Success! Output targets.
injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;
- addWindowTargetLocked(mFocusedWindowHandle,
+ addWindowTargetLocked(focusedWindowHandle,
InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS, BitSet32(0),
inputTargets);
@@ -1802,8 +1843,11 @@
}
void InputDispatcher::pokeUserActivityLocked(const EventEntry* eventEntry) {
- if (mFocusedWindowHandle != nullptr) {
- const InputWindowInfo* info = mFocusedWindowHandle->getInfo();
+ int32_t displayId = getTargetDisplayId(eventEntry);
+ sp<InputWindowHandle> focusedWindowHandle =
+ getValueByKey(mFocusedWindowHandlesByDisplay, displayId);
+ if (focusedWindowHandle != nullptr) {
+ const InputWindowInfo* info = focusedWindowHandle->getInfo();
if (info->inputFeatures & InputWindowInfo::INPUT_FEATURE_DISABLE_USER_ACTIVITY) {
#if DEBUG_DISPATCH_CYCLE
ALOGD("Not poking user activity: disabled by window '%s'.", info->name.c_str());
@@ -2978,18 +3022,7 @@
// Copy old handles for release if they are no longer present.
const Vector<sp<InputWindowHandle>> oldWindowHandles = getWindowHandlesLocked(displayId);
- // TODO(b/111361570): multi-display focus, one focus window per display.
- sp<InputWindowHandle> newFocusedWindowHandle = mFocusedWindowHandle;
- // Reset newFocusedWindowHandle to nullptr if current display own the focus window,
- // that will be updated below when going through all window handles in current display.
- // And if list of window handles becomes empty then it will be updated by other display.
- if (mFocusedWindowHandle != nullptr) {
- const InputWindowInfo* info = mFocusedWindowHandle->getInfo();
- if (info == nullptr || info->displayId == displayId) {
- newFocusedWindowHandle = nullptr;
- }
- }
-
+ sp<InputWindowHandle> newFocusedWindowHandle = nullptr;
bool foundHoveredWindow = false;
if (inputWindowHandles.isEmpty()) {
@@ -3026,28 +3059,31 @@
mLastHoverWindowHandle = nullptr;
}
- // TODO(b/111361570): multi-display focus, one focus in all display in current.
- if (mFocusedWindowHandle != newFocusedWindowHandle) {
- if (mFocusedWindowHandle != nullptr) {
+ sp<InputWindowHandle> oldFocusedWindowHandle =
+ getValueByKey(mFocusedWindowHandlesByDisplay, displayId);
+
+ if (oldFocusedWindowHandle != newFocusedWindowHandle) {
+ if (oldFocusedWindowHandle != nullptr) {
#if DEBUG_FOCUS
ALOGD("Focus left window: %s",
- mFocusedWindowHandle->getName().c_str());
+ oldFocusedWindowHandle->getName().c_str());
#endif
- sp<InputChannel> focusedInputChannel = mFocusedWindowHandle->getInputChannel();
+ sp<InputChannel> focusedInputChannel = oldFocusedWindowHandle->getInputChannel();
if (focusedInputChannel != nullptr) {
CancelationOptions options(CancelationOptions::CANCEL_NON_POINTER_EVENTS,
"focus left window");
synthesizeCancelationEventsForInputChannelLocked(
focusedInputChannel, options);
}
+ mFocusedWindowHandlesByDisplay.erase(displayId);
}
if (newFocusedWindowHandle != nullptr) {
#if DEBUG_FOCUS
ALOGD("Focus entered window: %s",
newFocusedWindowHandle->getName().c_str());
#endif
+ mFocusedWindowHandlesByDisplay[displayId] = newFocusedWindowHandle;
}
- mFocusedWindowHandle = newFocusedWindowHandle;
}
ssize_t stateIndex = mTouchStatesByDisplay.indexOfKey(displayId);
@@ -3096,25 +3132,28 @@
}
void InputDispatcher::setFocusedApplication(
- const sp<InputApplicationHandle>& inputApplicationHandle) {
+ int32_t displayId, const sp<InputApplicationHandle>& inputApplicationHandle) {
#if DEBUG_FOCUS
ALOGD("setFocusedApplication");
#endif
{ // acquire lock
AutoMutex _l(mLock);
+ sp<InputApplicationHandle> oldFocusedApplicationHandle =
+ getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);
if (inputApplicationHandle != nullptr && inputApplicationHandle->updateInfo()) {
- if (mFocusedApplicationHandle != inputApplicationHandle) {
- if (mFocusedApplicationHandle != nullptr) {
+ if (oldFocusedApplicationHandle != inputApplicationHandle) {
+ if (oldFocusedApplicationHandle != nullptr) {
resetANRTimeoutsLocked();
- mFocusedApplicationHandle->releaseInfo();
+ oldFocusedApplicationHandle->releaseInfo();
}
- mFocusedApplicationHandle = inputApplicationHandle;
+ mFocusedApplicationHandlesByDisplay[displayId] = inputApplicationHandle;
}
- } else if (mFocusedApplicationHandle != nullptr) {
+ } else if (oldFocusedApplicationHandle != nullptr) {
resetANRTimeoutsLocked();
- mFocusedApplicationHandle->releaseInfo();
- mFocusedApplicationHandle.clear();
+ oldFocusedApplicationHandle->releaseInfo();
+ oldFocusedApplicationHandle.clear();
+ mFocusedApplicationHandlesByDisplay.erase(displayId);
}
#if DEBUG_FOCUS
@@ -3126,6 +3165,62 @@
mLooper->wake();
}
+/**
+ * Sets the focused display, which is responsible for receiving focus-dispatched input events where
+ * the display not specified.
+ *
+ * We track any unreleased events for each window. If a window loses the ability to receive the
+ * released event, we will send a cancel event to it. So when the focused display is changed, we
+ * cancel all the unreleased display-unspecified events for the focused window on the old focused
+ * display. The display-specified events won't be affected.
+ */
+void InputDispatcher::setFocusedDisplay(int32_t displayId) {
+#if DEBUG_FOCUS
+ ALOGD("setFocusedDisplay displayId=%" PRId32, displayId);
+#endif
+ { // acquire lock
+ AutoMutex _l(mLock);
+
+ if (mFocusedDisplayId != displayId) {
+ sp<InputWindowHandle> oldFocusedWindowHandle =
+ getValueByKey(mFocusedWindowHandlesByDisplay, mFocusedDisplayId);
+ if (oldFocusedWindowHandle != nullptr) {
+ sp<InputChannel> inputChannel = oldFocusedWindowHandle->getInputChannel();
+ if (inputChannel != nullptr) {
+ CancelationOptions options(
+ CancelationOptions::CANCEL_DISPLAY_UNSPECIFIED_EVENTS,
+ "The display which contains this window no longer has focus.");
+ synthesizeCancelationEventsForInputChannelLocked(inputChannel, options);
+ }
+ }
+ mFocusedDisplayId = displayId;
+
+ // Sanity check
+ sp<InputWindowHandle> newFocusedWindowHandle =
+ getValueByKey(mFocusedWindowHandlesByDisplay, displayId);
+ if (newFocusedWindowHandle == nullptr) {
+ ALOGW("Focused display #%" PRId32 " does not have a focused window.", displayId);
+ if (!mFocusedWindowHandlesByDisplay.empty()) {
+ ALOGE("But another display has a focused window:");
+ for (auto& it : mFocusedWindowHandlesByDisplay) {
+ const int32_t displayId = it.first;
+ const sp<InputWindowHandle>& windowHandle = it.second;
+ ALOGE("Display #%" PRId32 " has focused window: '%s'\n",
+ displayId, windowHandle->getName().c_str());
+ }
+ }
+ }
+ }
+
+#if DEBUG_FOCUS
+ logDispatchStateLocked();
+#endif
+ } // release lock
+
+ // Wake up poll loop since it may need to make new input dispatching choices.
+ mLooper->wake();
+}
+
void InputDispatcher::setInputDispatchMode(bool enabled, bool frozen) {
#if DEBUG_FOCUS
ALOGD("setInputDispatchMode: enabled=%d, frozen=%d", enabled, frozen);
@@ -3152,7 +3247,7 @@
}
#if DEBUG_FOCUS
- //logDispatchStateLocked();
+ logDispatchStateLocked();
#endif
} // release lock
@@ -3297,17 +3392,35 @@
void InputDispatcher::dumpDispatchStateLocked(std::string& dump) {
dump += StringPrintf(INDENT "DispatchEnabled: %d\n", mDispatchEnabled);
dump += StringPrintf(INDENT "DispatchFrozen: %d\n", mDispatchFrozen);
+ dump += StringPrintf(INDENT "FocusedDisplayId: %" PRId32 "\n", mFocusedDisplayId);
- if (mFocusedApplicationHandle != nullptr) {
- dump += StringPrintf(INDENT "FocusedApplication: name='%s', dispatchingTimeout=%0.3fms\n",
- mFocusedApplicationHandle->getName().c_str(),
- mFocusedApplicationHandle->getDispatchingTimeout(
- DEFAULT_INPUT_DISPATCHING_TIMEOUT) / 1000000.0);
+ if (!mFocusedApplicationHandlesByDisplay.empty()) {
+ dump += StringPrintf(INDENT "FocusedApplications:\n");
+ for (auto& it : mFocusedApplicationHandlesByDisplay) {
+ const int32_t displayId = it.first;
+ const sp<InputApplicationHandle>& applicationHandle = it.second;
+ dump += StringPrintf(
+ INDENT2 "displayId=%" PRId32 ", name='%s', dispatchingTimeout=%0.3fms\n",
+ displayId,
+ applicationHandle->getName().c_str(),
+ applicationHandle->getDispatchingTimeout(
+ DEFAULT_INPUT_DISPATCHING_TIMEOUT) / 1000000.0);
+ }
} else {
- dump += StringPrintf(INDENT "FocusedApplication: <null>\n");
+ dump += StringPrintf(INDENT "FocusedApplications: <none>\n");
}
- dump += StringPrintf(INDENT "FocusedWindow: name='%s'\n",
- mFocusedWindowHandle != nullptr ? mFocusedWindowHandle->getName().c_str() : "<null>");
+
+ if (!mFocusedWindowHandlesByDisplay.empty()) {
+ dump += StringPrintf(INDENT "FocusedWindows:\n");
+ for (auto& it : mFocusedWindowHandlesByDisplay) {
+ const int32_t displayId = it.first;
+ const sp<InputWindowHandle>& windowHandle = it.second;
+ dump += StringPrintf(INDENT2 "displayId=%" PRId32 ", name='%s'\n",
+ displayId, windowHandle->getName().c_str());
+ }
+ } else {
+ dump += StringPrintf(INDENT "FocusedWindows: <none>\n");
+ }
if (!mTouchStatesByDisplay.isEmpty()) {
dump += StringPrintf(INDENT "TouchStatesByDisplay:\n");
@@ -4527,6 +4640,8 @@
return true;
case CancelationOptions::CANCEL_FALLBACK_EVENTS:
return memento.flags & AKEY_EVENT_FLAG_FALLBACK;
+ case CancelationOptions::CANCEL_DISPLAY_UNSPECIFIED_EVENTS:
+ return memento.displayId == ADISPLAY_ID_NONE;
default:
return false;
}
@@ -4545,6 +4660,8 @@
return memento.source & AINPUT_SOURCE_CLASS_POINTER;
case CancelationOptions::CANCEL_NON_POINTER_EVENTS:
return !(memento.source & AINPUT_SOURCE_CLASS_POINTER);
+ case CancelationOptions::CANCEL_DISPLAY_UNSPECIFIED_EVENTS:
+ return memento.displayId == ADISPLAY_ID_NONE;
default:
return false;
}
diff --git a/services/inputflinger/InputDispatcher.h b/services/inputflinger/InputDispatcher.h
index 5541ab2..aedad2f 100644
--- a/services/inputflinger/InputDispatcher.h
+++ b/services/inputflinger/InputDispatcher.h
@@ -311,12 +311,18 @@
virtual void setInputWindows(const Vector<sp<InputWindowHandle> >& inputWindowHandles,
int32_t displayId) = 0;
- /* Sets the focused application.
+ /* Sets the focused application on the given display.
*
* This method may be called on any thread (usually by the input manager).
*/
virtual void setFocusedApplication(
- const sp<InputApplicationHandle>& inputApplicationHandle) = 0;
+ int32_t displayId, const sp<InputApplicationHandle>& inputApplicationHandle) = 0;
+
+ /* Sets the focused display.
+ *
+ * This method may be called on any thread (usually by the input manager).
+ */
+ virtual void setFocusedDisplay(int32_t displayId) = 0;
/* Sets the input dispatching mode.
*
@@ -391,7 +397,9 @@
virtual void setInputWindows(const Vector<sp<InputWindowHandle> >& inputWindowHandles,
int32_t displayId);
- virtual void setFocusedApplication(const sp<InputApplicationHandle>& inputApplicationHandle);
+ virtual void setFocusedApplication(int32_t displayId,
+ const sp<InputApplicationHandle>& inputApplicationHandle);
+ virtual void setFocusedDisplay(int32_t displayId);
virtual void setInputDispatchMode(bool enabled, bool frozen);
virtual void setInputFilterEnabled(bool enabled);
@@ -686,6 +694,10 @@
CANCEL_POINTER_EVENTS = 1,
CANCEL_NON_POINTER_EVENTS = 2,
CANCEL_FALLBACK_EVENTS = 3,
+
+ /* Cancel events where the display not specified. These events would go to the focused
+ * display. */
+ CANCEL_DISPLAY_UNSPECIFIED_EVENTS = 4,
};
// The criterion to use to determine which events should be canceled.
@@ -966,7 +978,7 @@
bool hasWindowHandleLocked(const sp<InputWindowHandle>& windowHandle) const;
// Focus tracking for keys, trackball, etc.
- sp<InputWindowHandle> mFocusedWindowHandle;
+ std::unordered_map<int32_t, sp<InputWindowHandle>> mFocusedWindowHandlesByDisplay;
// Focus tracking for touch.
struct TouchedWindow {
@@ -997,8 +1009,11 @@
KeyedVector<int32_t, TouchState> mTouchStatesByDisplay;
TouchState mTempTouchState;
- // Focused application.
- sp<InputApplicationHandle> mFocusedApplicationHandle;
+ // Focused applications.
+ std::unordered_map<int32_t, sp<InputApplicationHandle>> mFocusedApplicationHandlesByDisplay;
+
+ // Top focused display.
+ int32_t mFocusedDisplayId;
// Dispatcher state at time of last ANR.
std::string mLastANRState;
@@ -1046,6 +1061,7 @@
nsecs_t getTimeSpentWaitingForApplicationLocked(nsecs_t currentTime);
void resetANRTimeoutsLocked();
+ int32_t getTargetDisplayId(const EventEntry* entry);
int32_t findFocusedWindowTargetsLocked(nsecs_t currentTime, const EventEntry* entry,
Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime);
int32_t findTouchedWindowTargetsLocked(nsecs_t currentTime, const MotionEntry* entry,
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 61dcdd9..2bbfb1b 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -382,12 +382,13 @@
int32_t mDisplayId;
};
-static int32_t injectKeyDown(const sp<InputDispatcher>& dispatcher) {
+static int32_t injectKeyDown(const sp<InputDispatcher>& dispatcher,
+ int32_t displayId = ADISPLAY_ID_NONE) {
KeyEvent event;
nsecs_t currentTime = systemTime(SYSTEM_TIME_MONOTONIC);
// Define a valid key down event.
- event.initialize(DEVICE_ID, AINPUT_SOURCE_KEYBOARD, ADISPLAY_ID_NONE,
+ event.initialize(DEVICE_ID, AINPUT_SOURCE_KEYBOARD, displayId,
AKEY_EVENT_ACTION_DOWN, /* flags */ 0,
AKEYCODE_A, KEY_A, AMETA_NONE, /* repeatCount */ 0, currentTime, currentTime);
@@ -466,7 +467,7 @@
sp<FakeWindowHandle> windowSecond = new FakeWindowHandle(application, mDispatcher, "Second");
// Set focus application.
- mDispatcher->setFocusedApplication(application);
+ mDispatcher->setFocusedApplication(ADISPLAY_ID_DEFAULT, application);
// Expect one focus window exist in display.
windowSecond->setFocus();
@@ -511,15 +512,21 @@
windowInSecondary->consumeEvent(AINPUT_EVENT_TYPE_MOTION, SECOND_DISPLAY_ID);
}
-// TODO(b/111361570): multi-display focus, one focus window per display.
TEST_F(InputDispatcherTest, SetInputWindow_FocusedInMultiDisplay) {
sp<FakeApplicationHandle> application = new FakeApplicationHandle();
sp<FakeWindowHandle> windowInPrimary = new FakeWindowHandle(application, mDispatcher, "D_1");
sp<FakeApplicationHandle> application2 = new FakeApplicationHandle();
sp<FakeWindowHandle> windowInSecondary = new FakeWindowHandle(application2, mDispatcher, "D_2");
+ constexpr int32_t SECOND_DISPLAY_ID = 1;
+
+ // Set focus to primary display window.
+ mDispatcher->setFocusedApplication(ADISPLAY_ID_DEFAULT, application);
+ windowInPrimary->setFocus();
+
// Set focus to second display window.
- mDispatcher->setFocusedApplication(application2);
+ mDispatcher->setFocusedDisplay(SECOND_DISPLAY_ID);
+ mDispatcher->setFocusedApplication(SECOND_DISPLAY_ID, application2);
windowInSecondary->setFocus();
// Update all windows per displays.
@@ -527,13 +534,18 @@
inputWindowHandles.push(windowInPrimary);
mDispatcher->setInputWindows(inputWindowHandles, ADISPLAY_ID_DEFAULT);
- constexpr int32_t SECOND_DISPLAY_ID = 1;
windowInSecondary->setDisplayId(SECOND_DISPLAY_ID);
Vector<sp<InputWindowHandle>> inputWindowHandles_Second;
inputWindowHandles_Second.push(windowInSecondary);
mDispatcher->setInputWindows(inputWindowHandles_Second, SECOND_DISPLAY_ID);
- // Test inject a key down.
+ // Test inject a key down with display id specified.
+ ASSERT_EQ(INPUT_EVENT_INJECTION_SUCCEEDED, injectKeyDown(mDispatcher, ADISPLAY_ID_DEFAULT))
+ << "Inject key event should return INPUT_EVENT_INJECTION_SUCCEEDED";
+ windowInPrimary->consumeEvent(AINPUT_EVENT_TYPE_KEY, ADISPLAY_ID_DEFAULT);
+ windowInSecondary->assertNoEvents();
+
+ // Test inject a key down without display id specified.
ASSERT_EQ(INPUT_EVENT_INJECTION_SUCCEEDED, injectKeyDown(mDispatcher))
<< "Inject key event should return INPUT_EVENT_INJECTION_SUCCEEDED";
windowInPrimary->assertNoEvents();
@@ -545,6 +557,7 @@
// Expect old focus should receive a cancel event.
windowInSecondary->consumeEvent(AINPUT_EVENT_TYPE_KEY, ADISPLAY_ID_NONE);
+ // TODO(b/111361570): Validate that the event here was marked as canceled.
// Test inject a key down, should timeout because of no target window.
ASSERT_EQ(INPUT_EVENT_INJECTION_TIMED_OUT, injectKeyDown(mDispatcher))