diff options
Diffstat (limited to 'widget/android/nsWindow.cpp')
-rw-r--r-- | widget/android/nsWindow.cpp | 3638 |
1 files changed, 3638 insertions, 0 deletions
diff --git a/widget/android/nsWindow.cpp b/widget/android/nsWindow.cpp new file mode 100644 index 000000000..9423a4a26 --- /dev/null +++ b/widget/android/nsWindow.cpp @@ -0,0 +1,3638 @@ +/* -*- Mode: c++; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- + * vim: set sw=4 ts=4 expandtab: + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <android/log.h> +#include <android/native_window.h> +#include <android/native_window_jni.h> +#include <math.h> +#include <unistd.h> + +#include "mozilla/IMEStateManager.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/TypeTraits.h" +#include "mozilla/WeakPtr.h" + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/Unused.h" +#include "mozilla/Preferences.h" +#include "mozilla/layers/RenderTrace.h" +#include <algorithm> + +using mozilla::dom::ContentParent; +using mozilla::dom::ContentChild; +using mozilla::Unused; + +#include "nsWindow.h" + +#include "nsIBaseWindow.h" +#include "nsIDOMChromeWindow.h" +#include "nsIObserverService.h" +#include "nsISelection.h" +#include "nsISupportsPrimitives.h" +#include "nsIWidgetListener.h" +#include "nsIWindowWatcher.h" +#include "nsIXULWindow.h" + +#include "nsAppShell.h" +#include "nsFocusManager.h" +#include "nsIdleService.h" +#include "nsLayoutUtils.h" +#include "nsViewManager.h" + +#include "WidgetUtils.h" + +#include "nsIDOMSimpleGestureEvent.h" + +#include "nsGkAtoms.h" +#include "nsWidgetsCID.h" +#include "nsGfxCIID.h" + +#include "gfxContext.h" + +#include "Layers.h" +#include "mozilla/layers/LayerManagerComposite.h" +#include "mozilla/layers/AsyncCompositionManager.h" +#include "mozilla/layers/APZEventState.h" +#include "mozilla/layers/APZThreadUtils.h" +#include "mozilla/layers/IAPZCTreeManager.h" +#include "GLContext.h" +#include "GLContextProvider.h" +#include "ScopedGLHelpers.h" +#include "mozilla/layers/CompositorOGL.h" +#include "AndroidContentController.h" + +#include "nsTArray.h" + +#include "AndroidBridge.h" +#include "AndroidBridgeUtilities.h" +#include "android_npapi.h" +#include "FennecJNINatives.h" +#include "GeneratedJNINatives.h" +#include "KeyEvent.h" +#include "MotionEvent.h" + +#include "imgIEncoder.h" + +#include "nsString.h" +#include "GeckoProfiler.h" // For PROFILER_LABEL +#include "nsIXULRuntime.h" +#include "nsPrintfCString.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::layers; +using namespace mozilla::java; +using namespace mozilla::widget; + +NS_IMPL_ISUPPORTS_INHERITED0(nsWindow, nsBaseWidget) + +#include "mozilla/layers/CompositorBridgeChild.h" +#include "mozilla/layers/CompositorBridgeParent.h" +#include "mozilla/layers/CompositorSession.h" +#include "mozilla/layers/LayerTransactionParent.h" +#include "mozilla/Services.h" +#include "nsThreadUtils.h" + +// All the toplevel windows that have been created; these are in +// stacking order, so the window at gTopLevelWindows[0] is the topmost +// one. +static nsTArray<nsWindow*> gTopLevelWindows; + +static bool sFailedToCreateGLContext = false; + +// Multitouch swipe thresholds in inches +static const double SWIPE_MAX_PINCH_DELTA_INCHES = 0.4; +static const double SWIPE_MIN_DISTANCE_INCHES = 0.6; + +// Sync with GeckoEditableView class +static const int IME_MONITOR_CURSOR_ONE_SHOT = 1; +static const int IME_MONITOR_CURSOR_START_MONITOR = 2; +static const int IME_MONITOR_CURSOR_END_MONITOR = 3; + +static Modifiers GetModifiers(int32_t metaState); + +template<typename Lambda, bool IsStatic, typename InstanceType, class Impl> +class nsWindow::WindowEvent : public nsAppShell::LambdaEvent<Lambda> +{ + typedef nsAppShell::Event Event; + typedef nsAppShell::LambdaEvent<Lambda> Base; + + bool IsStaleCall() + { + if (IsStatic) { + // Static calls are never stale. + return false; + } + + JNIEnv* const env = mozilla::jni::GetEnvForThread(); + + const auto natives = reinterpret_cast<mozilla::WeakPtr<Impl>*>( + jni::GetNativeHandle(env, mInstance.Get())); + MOZ_CATCH_JNI_EXCEPTION(env); + + // The call is stale if the nsWindow has been destroyed on the + // Gecko side, but the Java object is still attached to it through + // a weak pointer. Stale calls should be discarded. Note that it's + // an error if natives is nullptr here; we return false but the + // native call will throw an error. + return natives && !natives->get(); + } + + const InstanceType mInstance; + const Event::Type mEventType; + +public: + WindowEvent(Lambda&& aLambda, + InstanceType&& aInstance, + Event::Type aEventType = Event::Type::kGeneralActivity) + : Base(mozilla::Move(aLambda)) + , mInstance(mozilla::Move(aInstance)) + , mEventType(aEventType) + {} + + WindowEvent(Lambda&& aLambda, + Event::Type aEventType = Event::Type::kGeneralActivity) + : Base(mozilla::Move(aLambda)) + , mInstance(Base::lambda.GetThisArg()) + , mEventType(aEventType) + {} + + void Run() override + { + if (!IsStaleCall()) { + return Base::Run(); + } + } + + Event::Type ActivityType() const override + { + return mEventType; + } +}; + +template<class Impl> +template<class Instance, typename... Args> void +nsWindow::NativePtr<Impl>::Attach(Instance aInstance, nsWindow* aWindow, + Args&&... aArgs) +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mPtr && !mImpl); + + auto impl = mozilla::MakeUnique<Impl>( + this, aWindow, mozilla::Forward<Args>(aArgs)...); + mImpl = impl.get(); + + Impl::AttachNative(aInstance, mozilla::Move(impl)); +} + +template<class Impl> void +nsWindow::NativePtr<Impl>::Detach() +{ + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mPtr && mImpl); + + mImpl->OnDetach(); + { + Locked implLock(*this); + mImpl = nullptr; + } + + typename WindowPtr<Impl>::Locked lock(*mPtr); + mPtr->mWindow = nullptr; + mPtr->mPtr = nullptr; + mPtr = nullptr; +} + +template<class Impl> +class nsWindow::NativePtr<Impl>::Locked final : private MutexAutoLock +{ + Impl* const mImpl; + +public: + Locked(NativePtr<Impl>& aPtr) + : MutexAutoLock(aPtr.mImplLock) + , mImpl(aPtr.mImpl) + {} + + operator Impl*() const { return mImpl; } + Impl* operator->() const { return mImpl; } +}; + +template<class Impl> +class nsWindow::WindowPtr final +{ + friend NativePtr<Impl>; + + NativePtr<Impl>* mPtr; + nsWindow* mWindow; + Mutex mWindowLock; + +public: + class Locked final : private MutexAutoLock + { + nsWindow* const mWindow; + + public: + Locked(WindowPtr<Impl>& aPtr) + : MutexAutoLock(aPtr.mWindowLock) + , mWindow(aPtr.mWindow) + {} + + operator nsWindow*() const { return mWindow; } + nsWindow* operator->() const { return mWindow; } + }; + + WindowPtr(NativePtr<Impl>* aPtr, nsWindow* aWindow) + : mPtr(aPtr) + , mWindow(aWindow) + , mWindowLock(NativePtr<Impl>::sName) + { + MOZ_ASSERT(NS_IsMainThread()); + mPtr->mPtr = this; + } + + ~WindowPtr() + { + MOZ_ASSERT(NS_IsMainThread()); + if (!mPtr) { + return; + } + mPtr->mPtr = nullptr; + mPtr->mImpl = nullptr; + } + + operator nsWindow*() const + { + MOZ_ASSERT(NS_IsMainThread()); + return mWindow; + } + + nsWindow* operator->() const { return operator nsWindow*(); } +}; + + +class nsWindow::GeckoViewSupport final + : public GeckoView::Window::Natives<GeckoViewSupport> + , public GeckoEditable::Natives<GeckoViewSupport> + , public SupportsWeakPtr<GeckoViewSupport> +{ + nsWindow& window; + +public: + typedef GeckoView::Window::Natives<GeckoViewSupport> Base; + typedef GeckoEditable::Natives<GeckoViewSupport> EditableBase; + + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(GeckoViewSupport); + + template<typename Functor> + static void OnNativeCall(Functor&& aCall) + { + if (aCall.IsTarget(&Open) && NS_IsMainThread()) { + // Gecko state probably just switched to PROFILE_READY, and the + // event loop is not running yet. Skip the event loop here so we + // can get a head start on opening our window. + return aCall(); + } + + const nsAppShell::Event::Type eventType = + aCall.IsTarget(&GeckoViewSupport::OnKeyEvent) || + aCall.IsTarget(&GeckoViewSupport::OnImeReplaceText) || + aCall.IsTarget(&GeckoViewSupport::OnImeUpdateComposition) ? + nsAppShell::Event::Type::kUIActivity : + nsAppShell::Event::Type::kGeneralActivity; + + nsAppShell::PostEvent(mozilla::MakeUnique<WindowEvent<Functor>>( + mozilla::Move(aCall), eventType)); + } + + GeckoViewSupport(nsWindow* aWindow, + const GeckoView::Window::LocalRef& aInstance, + GeckoView::Param aView) + : window(*aWindow) + , mEditable(GeckoEditable::New(aView)) + , mIMERanges(new TextRangeArray()) + , mIMEMaskEventsCount(1) // Mask IME events since there's no focus yet + , mIMEUpdatingContext(false) + , mIMESelectionChanged(false) + , mIMETextChangedDuringFlush(false) + , mIMEMonitorCursor(false) + { + Base::AttachNative(aInstance, this); + EditableBase::AttachNative(mEditable, this); + } + + ~GeckoViewSupport(); + + using Base::DisposeNative; + using EditableBase::DisposeNative; + + /** + * GeckoView methods + */ +private: + nsCOMPtr<nsPIDOMWindowOuter> mDOMWindow; + +public: + // Create and attach a window. + static void Open(const jni::Class::LocalRef& aCls, + GeckoView::Window::Param aWindow, + GeckoView::Param aView, jni::Object::Param aCompositor, + jni::String::Param aChromeURI, + int32_t screenId); + + // Close and destroy the nsWindow. + void Close(); + + // Reattach this nsWindow to a new GeckoView. + void Reattach(const GeckoView::Window::LocalRef& inst, + GeckoView::Param aView, jni::Object::Param aCompositor); + + void LoadUri(jni::String::Param aUri, int32_t aFlags); + + /** + * GeckoEditable methods + */ +private: + /* + Rules for managing IME between Gecko and Java: + + * Gecko controls the text content, and Java shadows the Gecko text + through text updates + * Gecko and Java maintain separate selections, and synchronize when + needed through selection updates and set-selection events + * Java controls the composition, and Gecko shadows the Java + composition through update composition events + */ + + struct IMETextChange final { + int32_t mStart, mOldEnd, mNewEnd; + + IMETextChange() : + mStart(-1), mOldEnd(-1), mNewEnd(-1) {} + + IMETextChange(const IMENotification& aIMENotification) + : mStart(aIMENotification.mTextChangeData.mStartOffset) + , mOldEnd(aIMENotification.mTextChangeData.mRemovedEndOffset) + , mNewEnd(aIMENotification.mTextChangeData.mAddedEndOffset) + { + MOZ_ASSERT(aIMENotification.mMessage == NOTIFY_IME_OF_TEXT_CHANGE, + "IMETextChange initialized with wrong notification"); + MOZ_ASSERT(aIMENotification.mTextChangeData.IsValid(), + "The text change notification isn't initialized"); + MOZ_ASSERT(aIMENotification.mTextChangeData.IsInInt32Range(), + "The text change notification is out of range"); + } + + bool IsEmpty() const { return mStart < 0; } + }; + + // GeckoEditable instance used by this nsWindow; + java::GeckoEditable::GlobalRef mEditable; + AutoTArray<mozilla::UniquePtr<mozilla::WidgetEvent>, 8> mIMEKeyEvents; + AutoTArray<IMETextChange, 4> mIMETextChanges; + InputContext mInputContext; + RefPtr<mozilla::TextRangeArray> mIMERanges; + int32_t mIMEMaskEventsCount; // Mask events when > 0. + bool mIMEUpdatingContext; + bool mIMESelectionChanged; + bool mIMETextChangedDuringFlush; + bool mIMEMonitorCursor; + + void SendIMEDummyKeyEvents(); + void AddIMETextChange(const IMETextChange& aChange); + + enum FlushChangesFlag { + // Not retrying. + FLUSH_FLAG_NONE, + // Retrying due to IME text changes during flush. + FLUSH_FLAG_RETRY, + // Retrying due to IME sync exceptions during flush. + FLUSH_FLAG_RECOVER + }; + void PostFlushIMEChanges(); + void FlushIMEChanges(FlushChangesFlag aFlags = FLUSH_FLAG_NONE); + void FlushIMEText(FlushChangesFlag aFlags = FLUSH_FLAG_NONE); + void AsyncNotifyIME(int32_t aNotification); + void UpdateCompositionRects(); + +public: + bool NotifyIME(const IMENotification& aIMENotification); + void SetInputContext(const InputContext& aContext, + const InputContextAction& aAction); + InputContext GetInputContext(); + + // RAII helper class that automatically sends an event reply through + // OnImeSynchronize, as required by events like OnImeReplaceText. + class AutoIMESynchronize { + GeckoViewSupport* const mGVS; + public: + AutoIMESynchronize(GeckoViewSupport* gvs) : mGVS(gvs) {} + ~AutoIMESynchronize() { mGVS->OnImeSynchronize(); } + }; + + // Handle an Android KeyEvent. + void OnKeyEvent(int32_t aAction, int32_t aKeyCode, int32_t aScanCode, + int32_t aMetaState, int64_t aTime, int32_t aUnicodeChar, + int32_t aBaseUnicodeChar, int32_t aDomPrintableKeyValue, + int32_t aRepeatCount, int32_t aFlags, + bool aIsSynthesizedImeKey, jni::Object::Param originalEvent); + + // Synchronize Gecko thread with the InputConnection thread. + void OnImeSynchronize(); + + // Replace a range of text with new text. + void OnImeReplaceText(int32_t aStart, int32_t aEnd, + jni::String::Param aText); + + // Add styling for a range within the active composition. + void OnImeAddCompositionRange(int32_t aStart, int32_t aEnd, + int32_t aRangeType, int32_t aRangeStyle, int32_t aRangeLineStyle, + bool aRangeBoldLine, int32_t aRangeForeColor, + int32_t aRangeBackColor, int32_t aRangeLineColor); + + // Update styling for the active composition using previous-added ranges. + void OnImeUpdateComposition(int32_t aStart, int32_t aEnd); + + // Set cursor mode whether IME requests + void OnImeRequestCursorUpdates(int aRequestMode); +}; + +/** + * NativePanZoomController handles its native calls on the UI thread, so make + * it separate from GeckoViewSupport. + */ +class nsWindow::NPZCSupport final + : public NativePanZoomController::Natives<NPZCSupport> +{ + using LockedWindowPtr = WindowPtr<NPZCSupport>::Locked; + + WindowPtr<NPZCSupport> mWindow; + NativePanZoomController::GlobalRef mNPZC; + int mPreviousButtons; + +public: + typedef NativePanZoomController::Natives<NPZCSupport> Base; + + NPZCSupport(NativePtr<NPZCSupport>* aPtr, nsWindow* aWindow, + const NativePanZoomController::LocalRef& aNPZC) + : mWindow(aPtr, aWindow) + , mNPZC(aNPZC) + , mPreviousButtons(0) + {} + + ~NPZCSupport() + {} + + using Base::AttachNative; + using Base::DisposeNative; + + void OnDetach() + { + // There are several considerations when shutting down NPZC. 1) The + // Gecko thread may destroy NPZC at any time when nsWindow closes. 2) + // There may be pending events on the Gecko thread when NPZC is + // destroyed. 3) mWindow may not be available when the pending event + // runs. 4) The UI thread may destroy NPZC at any time when GeckoView + // is destroyed. 5) The UI thread may destroy NPZC at the same time as + // Gecko thread trying to destroy NPZC. 6) There may be pending calls + // on the UI thread when NPZC is destroyed. 7) mWindow may have been + // cleared on the Gecko thread when the pending call happens on the UI + // thread. + // + // 1) happens through OnDetach, which first notifies the UI + // thread through Destroy; Destroy then calls DisposeNative, which + // finally disposes the native instance back on the Gecko thread. Using + // Destroy to indirectly call DisposeNative here also solves 5), by + // making everything go through the UI thread, avoiding contention. + // + // 2) and 3) are solved by clearing mWindow, which signals to the + // pending event that we had shut down. In that case the event bails + // and does not touch mWindow. + // + // 4) happens through DisposeNative directly. OnDetach is not + // called. + // + // 6) is solved by keeping a destroyed flag in the Java NPZC instance, + // and only make a pending call if the destroyed flag is not set. + // + // 7) is solved by taking a lock whenever mWindow is modified on the + // Gecko thread or accessed on the UI thread. That way, we don't + // release mWindow until the UI thread is done using it, thus avoiding + // the race condition. + + typedef NativePanZoomController::GlobalRef NPZCRef; + auto callDestroy = [] (const NPZCRef& npzc) { + npzc->Destroy(); + }; + + NativePanZoomController::GlobalRef npzc = mNPZC; + AndroidBridge::Bridge()->PostTaskToUiThread(NewRunnableFunction( + static_cast<void(*)(const NPZCRef&)>(callDestroy), + mozilla::Move(npzc)), 0); + } + +public: + void AdjustScrollForSurfaceShift(float aX, float aY) + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<IAPZCTreeManager> controller; + + if (LockedWindowPtr window{mWindow}) { + controller = window->mAPZC; + } + + if (controller) { + controller->AdjustScrollForSurfaceShift( + ScreenPoint(aX, aY)); + } + } + + void SetIsLongpressEnabled(bool aIsLongpressEnabled) + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<IAPZCTreeManager> controller; + + if (LockedWindowPtr window{mWindow}) { + controller = window->mAPZC; + } + + if (controller) { + controller->SetLongTapEnabled(aIsLongpressEnabled); + } + } + + bool HandleScrollEvent(int64_t aTime, int32_t aMetaState, + float aX, float aY, + float aHScroll, float aVScroll) + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<IAPZCTreeManager> controller; + + if (LockedWindowPtr window{mWindow}) { + controller = window->mAPZC; + } + + if (!controller) { + return false; + } + + ScreenPoint origin = ScreenPoint(aX, aY); + + ScrollWheelInput input(aTime, TimeStamp::Now(), GetModifiers(aMetaState), + ScrollWheelInput::SCROLLMODE_SMOOTH, + ScrollWheelInput::SCROLLDELTA_PIXEL, + origin, + aHScroll, aVScroll, + false); + + ScrollableLayerGuid guid; + uint64_t blockId; + nsEventStatus status = controller->ReceiveInputEvent(input, &guid, &blockId); + + if (status == nsEventStatus_eConsumeNoDefault) { + return true; + } + + NativePanZoomController::GlobalRef npzc = mNPZC; + nsAppShell::PostEvent([npzc, input, guid, blockId, status] { + MOZ_ASSERT(NS_IsMainThread()); + + JNIEnv* const env = jni::GetGeckoThreadEnv(); + NPZCSupport* npzcSupport = GetNative( + NativePanZoomController::LocalRef(env, npzc)); + + if (!npzcSupport || !npzcSupport->mWindow) { + // We already shut down. + env->ExceptionClear(); + return; + } + + nsWindow* const window = npzcSupport->mWindow; + window->UserActivity(); + WidgetWheelEvent wheelEvent = input.ToWidgetWheelEvent(window); + window->ProcessUntransformedAPZEvent(&wheelEvent, guid, + blockId, status); + }); + + return true; + } + +private: + static MouseInput::ButtonType GetButtonType(int button) + { + MouseInput::ButtonType result = MouseInput::NONE; + + switch (button) { + case java::sdk::MotionEvent::BUTTON_PRIMARY: + result = MouseInput::LEFT_BUTTON; + break; + case java::sdk::MotionEvent::BUTTON_SECONDARY: + result = MouseInput::RIGHT_BUTTON; + break; + case java::sdk::MotionEvent::BUTTON_TERTIARY: + result = MouseInput::MIDDLE_BUTTON; + break; + default: + break; + } + + return result; + } + + static int16_t ConvertButtons(int buttons) { + int16_t result = 0; + + if (buttons & java::sdk::MotionEvent::BUTTON_PRIMARY) { + result |= WidgetMouseEventBase::eLeftButtonFlag; + } + if (buttons & java::sdk::MotionEvent::BUTTON_SECONDARY) { + result |= WidgetMouseEventBase::eRightButtonFlag; + } + if (buttons & java::sdk::MotionEvent::BUTTON_TERTIARY) { + result |= WidgetMouseEventBase::eMiddleButtonFlag; + } + if (buttons & java::sdk::MotionEvent::BUTTON_BACK) { + result |= WidgetMouseEventBase::e4thButtonFlag; + } + if (buttons & java::sdk::MotionEvent::BUTTON_FORWARD) { + result |= WidgetMouseEventBase::e5thButtonFlag; + } + + return result; + } + +public: + bool HandleMouseEvent(int32_t aAction, int64_t aTime, int32_t aMetaState, + float aX, float aY, int buttons) + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<IAPZCTreeManager> controller; + + if (LockedWindowPtr window{mWindow}) { + controller = window->mAPZC; + } + + if (!controller) { + return false; + } + + MouseInput::MouseType mouseType = MouseInput::MOUSE_NONE; + MouseInput::ButtonType buttonType = MouseInput::NONE; + switch (aAction) { + case AndroidMotionEvent::ACTION_DOWN: + mouseType = MouseInput::MOUSE_DOWN; + buttonType = GetButtonType(buttons ^ mPreviousButtons); + mPreviousButtons = buttons; + break; + case AndroidMotionEvent::ACTION_UP: + mouseType = MouseInput::MOUSE_UP; + buttonType = GetButtonType(buttons ^ mPreviousButtons); + mPreviousButtons = buttons; + break; + case AndroidMotionEvent::ACTION_MOVE: + mouseType = MouseInput::MOUSE_MOVE; + break; + case AndroidMotionEvent::ACTION_HOVER_MOVE: + mouseType = MouseInput::MOUSE_MOVE; + break; + case AndroidMotionEvent::ACTION_HOVER_ENTER: + mouseType = MouseInput::MOUSE_WIDGET_ENTER; + break; + case AndroidMotionEvent::ACTION_HOVER_EXIT: + mouseType = MouseInput::MOUSE_WIDGET_EXIT; + break; + default: + break; + } + + if (mouseType == MouseInput::MOUSE_NONE) { + return false; + } + + ScreenPoint origin = ScreenPoint(aX, aY); + + MouseInput input(mouseType, buttonType, nsIDOMMouseEvent::MOZ_SOURCE_MOUSE, ConvertButtons(buttons), origin, aTime, TimeStamp(), GetModifiers(aMetaState)); + + ScrollableLayerGuid guid; + uint64_t blockId; + nsEventStatus status = controller->ReceiveInputEvent(input, &guid, &blockId); + + if (status == nsEventStatus_eConsumeNoDefault) { + return true; + } + + NativePanZoomController::GlobalRef npzc = mNPZC; + nsAppShell::PostEvent([npzc, input, guid, blockId, status] { + MOZ_ASSERT(NS_IsMainThread()); + + JNIEnv* const env = jni::GetGeckoThreadEnv(); + NPZCSupport* npzcSupport = GetNative( + NativePanZoomController::LocalRef(env, npzc)); + + if (!npzcSupport || !npzcSupport->mWindow) { + // We already shut down. + env->ExceptionClear(); + return; + } + + nsWindow* const window = npzcSupport->mWindow; + window->UserActivity(); + WidgetMouseEvent mouseEvent = input.ToWidgetMouseEvent(window); + window->ProcessUntransformedAPZEvent(&mouseEvent, guid, + blockId, status); + }); + + return true; + } + + bool HandleMotionEvent(const NativePanZoomController::LocalRef& aInstance, + int32_t aAction, int32_t aActionIndex, + int64_t aTime, int32_t aMetaState, + jni::IntArray::Param aPointerId, + jni::FloatArray::Param aX, + jni::FloatArray::Param aY, + jni::FloatArray::Param aOrientation, + jni::FloatArray::Param aPressure, + jni::FloatArray::Param aToolMajor, + jni::FloatArray::Param aToolMinor) + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<IAPZCTreeManager> controller; + + if (LockedWindowPtr window{mWindow}) { + controller = window->mAPZC; + } + + if (!controller) { + return false; + } + + nsTArray<int32_t> pointerId(aPointerId->GetElements()); + MultiTouchInput::MultiTouchType type; + size_t startIndex = 0; + size_t endIndex = pointerId.Length(); + + switch (aAction) { + case sdk::MotionEvent::ACTION_DOWN: + case sdk::MotionEvent::ACTION_POINTER_DOWN: + type = MultiTouchInput::MULTITOUCH_START; + break; + case sdk::MotionEvent::ACTION_MOVE: + type = MultiTouchInput::MULTITOUCH_MOVE; + break; + case sdk::MotionEvent::ACTION_UP: + case sdk::MotionEvent::ACTION_POINTER_UP: + // for pointer-up events we only want the data from + // the one pointer that went up + type = MultiTouchInput::MULTITOUCH_END; + startIndex = aActionIndex; + endIndex = aActionIndex + 1; + break; + case sdk::MotionEvent::ACTION_OUTSIDE: + case sdk::MotionEvent::ACTION_CANCEL: + type = MultiTouchInput::MULTITOUCH_CANCEL; + break; + default: + return false; + } + + MultiTouchInput input(type, aTime, TimeStamp(), 0); + input.modifiers = GetModifiers(aMetaState); + input.mTouches.SetCapacity(endIndex - startIndex); + + nsTArray<float> x(aX->GetElements()); + nsTArray<float> y(aY->GetElements()); + nsTArray<float> orientation(aOrientation->GetElements()); + nsTArray<float> pressure(aPressure->GetElements()); + nsTArray<float> toolMajor(aToolMajor->GetElements()); + nsTArray<float> toolMinor(aToolMinor->GetElements()); + + MOZ_ASSERT(pointerId.Length() == x.Length()); + MOZ_ASSERT(pointerId.Length() == y.Length()); + MOZ_ASSERT(pointerId.Length() == orientation.Length()); + MOZ_ASSERT(pointerId.Length() == pressure.Length()); + MOZ_ASSERT(pointerId.Length() == toolMajor.Length()); + MOZ_ASSERT(pointerId.Length() == toolMinor.Length()); + + for (size_t i = startIndex; i < endIndex; i++) { + + float orien = orientation[i] * 180.0f / M_PI; + // w3c touchevents spec does not allow orientations == 90 + // this shifts it to -90, which will be shifted to zero below + if (orien >= 90.0) { + orien -= 180.0f; + } + + nsIntPoint point = nsIntPoint(int32_t(floorf(x[i])), + int32_t(floorf(y[i]))); + + // w3c touchevent radii are given with an orientation between 0 and + // 90. The radii are found by removing the orientation and + // measuring the x and y radii of the resulting ellipse. For + // Android orientations >= 0 and < 90, use the y radius as the + // major radius, and x as the minor radius. However, for an + // orientation < 0, we have to shift the orientation by adding 90, + // and reverse which radius is major and minor. + gfx::Size radius; + if (orien < 0.0f) { + orien += 90.0f; + radius = gfx::Size(int32_t(toolMajor[i] / 2.0f), + int32_t(toolMinor[i] / 2.0f)); + } else { + radius = gfx::Size(int32_t(toolMinor[i] / 2.0f), + int32_t(toolMajor[i] / 2.0f)); + } + + input.mTouches.AppendElement(SingleTouchData( + pointerId[i], ScreenIntPoint::FromUnknownPoint(point), + ScreenSize::FromUnknownSize(radius), orien, pressure[i])); + } + + ScrollableLayerGuid guid; + uint64_t blockId; + nsEventStatus status = + controller->ReceiveInputEvent(input, &guid, &blockId); + + if (status == nsEventStatus_eConsumeNoDefault) { + return true; + } + + // Dispatch APZ input event on Gecko thread. + NativePanZoomController::GlobalRef npzc = mNPZC; + nsAppShell::PostEvent([npzc, input, guid, blockId, status] { + MOZ_ASSERT(NS_IsMainThread()); + + JNIEnv* const env = jni::GetGeckoThreadEnv(); + NPZCSupport* npzcSupport = GetNative( + NativePanZoomController::LocalRef(env, npzc)); + + if (!npzcSupport || !npzcSupport->mWindow) { + // We already shut down. + env->ExceptionClear(); + return; + } + + nsWindow* const window = npzcSupport->mWindow; + window->UserActivity(); + WidgetTouchEvent touchEvent = input.ToWidgetTouchEvent(window); + window->ProcessUntransformedAPZEvent(&touchEvent, guid, + blockId, status); + window->DispatchHitTest(touchEvent); + }); + return true; + } + + void HandleMotionEventVelocity(int64_t aTime, float aSpeedY) + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<IAPZCTreeManager> controller; + + if (LockedWindowPtr window{mWindow}) { + controller = window->mAPZC; + } + + if (controller) { + controller->ProcessTouchVelocity((uint32_t)aTime, aSpeedY); + } + } + + void UpdateOverscrollVelocity(const float x, const float y) + { + mNPZC->UpdateOverscrollVelocity(x, y); + } + + void UpdateOverscrollOffset(const float x, const float y) + { + mNPZC->UpdateOverscrollOffset(x, y); + } + + void SetScrollingRootContent(const bool isRootContent) + { + mNPZC->SetScrollingRootContent(isRootContent); + } + + void SetSelectionDragState(const bool aState) + { + mNPZC->OnSelectionDragState(aState); + } +}; + +template<> const char +nsWindow::NativePtr<nsWindow::NPZCSupport>::sName[] = "NPZCSupport"; + +/** + * Compositor has some unique requirements for its native calls, so make it + * separate from GeckoViewSupport. + */ +class nsWindow::LayerViewSupport final + : public LayerView::Compositor::Natives<LayerViewSupport> +{ + using LockedWindowPtr = WindowPtr<LayerViewSupport>::Locked; + + WindowPtr<LayerViewSupport> mWindow; + LayerView::Compositor::GlobalRef mCompositor; + GeckoLayerClient::GlobalRef mLayerClient; + Atomic<bool, ReleaseAcquire> mCompositorPaused; + jni::Object::GlobalRef mSurface; + + // In order to use Event::HasSameTypeAs in PostTo(), we cannot make + // LayerViewEvent a template because each template instantiation is + // a different type. So implement LayerViewEvent as a ProxyEvent. + class LayerViewEvent final : public nsAppShell::ProxyEvent + { + using Event = nsAppShell::Event; + + public: + static UniquePtr<Event> MakeEvent(UniquePtr<Event>&& event) + { + return MakeUnique<LayerViewEvent>(mozilla::Move(event)); + } + + LayerViewEvent(UniquePtr<Event>&& event) + : nsAppShell::ProxyEvent(mozilla::Move(event)) + {} + + void PostTo(LinkedList<Event>& queue) override + { + // Give priority to compositor events, but keep in order with + // existing compositor events. + nsAppShell::Event* event = queue.getFirst(); + while (event && event->HasSameTypeAs(this)) { + event = event->getNext(); + } + if (event) { + event->setPrevious(this); + } else { + queue.insertBack(this); + } + } + }; + +public: + typedef LayerView::Compositor::Natives<LayerViewSupport> Base; + + template<class Functor> + static void OnNativeCall(Functor&& aCall) + { + if (aCall.IsTarget(&LayerViewSupport::CreateCompositor)) { + // This call is blocking. + nsAppShell::SyncRunEvent(nsAppShell::LambdaEvent<Functor>( + mozilla::Move(aCall)), &LayerViewEvent::MakeEvent); + return; + } + } + + static LayerViewSupport* + FromNative(const LayerView::Compositor::LocalRef& instance) + { + return GetNative(instance); + } + + LayerViewSupport(NativePtr<LayerViewSupport>* aPtr, nsWindow* aWindow, + const LayerView::Compositor::LocalRef& aInstance) + : mWindow(aPtr, aWindow) + , mCompositor(aInstance) + , mCompositorPaused(true) + {} + + ~LayerViewSupport() + {} + + using Base::AttachNative; + using Base::DisposeNative; + + void OnDetach() + { + mCompositor->Destroy(); + } + + const GeckoLayerClient::Ref& GetLayerClient() const + { + return mLayerClient; + } + + bool CompositorPaused() const + { + return mCompositorPaused; + } + + jni::Object::Param GetSurface() + { + return mSurface; + } + +private: + void OnResumedCompositor() + { + MOZ_ASSERT(NS_IsMainThread()); + + // When we receive this, the compositor has already been told to + // resume. (It turns out that waiting till we reach here to tell + // the compositor to resume takes too long, resulting in a black + // flash.) This means it's now safe for layer updates to occur. + // Since we might have prevented one or more draw events from + // occurring while the compositor was paused, we need to schedule + // a draw event now. + if (!mCompositorPaused) { + mWindow->RedrawAll(); + } + } + + /** + * Compositor methods + */ +public: + void AttachToJava(jni::Object::Param aClient, jni::Object::Param aNPZC) + { + MOZ_ASSERT(NS_IsMainThread()); + if (!mWindow) { + return; // Already shut down. + } + + const auto& layerClient = GeckoLayerClient::Ref::From(aClient); + + // If resetting is true, Android destroyed our GeckoApp activity and we + // had to recreate it, but all the Gecko-side things were not + // destroyed. We therefore need to link up the new java objects to + // Gecko, and that's what we do here. + const bool resetting = !!mLayerClient; + mLayerClient = layerClient; + + MOZ_ASSERT(aNPZC); + auto npzc = NativePanZoomController::LocalRef( + jni::GetGeckoThreadEnv(), + NativePanZoomController::Ref::From(aNPZC)); + mWindow->mNPZCSupport.Attach(npzc, mWindow, npzc); + + layerClient->OnGeckoReady(); + + if (resetting) { + // Since we are re-linking the new java objects to Gecko, we need + // to get the viewport from the compositor (since the Java copy was + // thrown away) and we do that by setting the first-paint flag. + if (RefPtr<CompositorBridgeParent> bridge = mWindow->GetCompositorBridgeParent()) { + bridge->ForceIsFirstPaint(); + } + } + } + + void OnSizeChanged(int32_t aWindowWidth, int32_t aWindowHeight, + int32_t aScreenWidth, int32_t aScreenHeight) + { + MOZ_ASSERT(NS_IsMainThread()); + if (!mWindow) { + return; // Already shut down. + } + + if (aWindowWidth != mWindow->mBounds.width || + aWindowHeight != mWindow->mBounds.height) { + + mWindow->Resize(aWindowWidth, aWindowHeight, /* repaint */ false); + } + } + + void CreateCompositor(int32_t aWidth, int32_t aHeight, + jni::Object::Param aSurface) + { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mWindow); + + mSurface = aSurface; + mWindow->CreateLayerManager(aWidth, aHeight); + + mCompositorPaused = false; + OnResumedCompositor(); + } + + void SyncPauseCompositor() + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<CompositorBridgeParent> bridge; + + if (LockedWindowPtr window{mWindow}) { + bridge = window->GetCompositorBridgeParent(); + } + + if (bridge) { + mCompositorPaused = true; + bridge->SchedulePauseOnCompositorThread(); + } + } + + void SyncResumeCompositor() + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<CompositorBridgeParent> bridge; + + if (LockedWindowPtr window{mWindow}) { + bridge = window->GetCompositorBridgeParent(); + } + + if (bridge && bridge->ScheduleResumeOnCompositorThread()) { + mCompositorPaused = false; + } + } + + void SyncResumeResizeCompositor(const LayerView::Compositor::LocalRef& aObj, + int32_t aWidth, int32_t aHeight, + jni::Object::Param aSurface) + { + MOZ_ASSERT(AndroidBridge::IsJavaUiThread()); + + RefPtr<CompositorBridgeParent> bridge; + + if (LockedWindowPtr window{mWindow}) { + bridge = window->GetCompositorBridgeParent(); + } + + mSurface = aSurface; + + if (!bridge || !bridge->ScheduleResumeOnCompositorThread(aWidth, + aHeight)) { + return; + } + + mCompositorPaused = false; + + class OnResumedEvent : public nsAppShell::Event + { + LayerView::Compositor::GlobalRef mCompositor; + + public: + OnResumedEvent(LayerView::Compositor::GlobalRef&& aCompositor) + : mCompositor(mozilla::Move(aCompositor)) + {} + + void Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + JNIEnv* const env = jni::GetGeckoThreadEnv(); + LayerViewSupport* const lvs = GetNative( + LayerView::Compositor::LocalRef(env, mCompositor)); + MOZ_CATCH_JNI_EXCEPTION(env); + + lvs->OnResumedCompositor(); + } + }; + + nsAppShell::PostEvent(MakeUnique<LayerViewEvent>( + MakeUnique<OnResumedEvent>(aObj))); + } + + void SyncInvalidateAndScheduleComposite() + { + RefPtr<CompositorBridgeParent> bridge; + + if (LockedWindowPtr window{mWindow}) { + bridge = window->GetCompositorBridgeParent(); + } + + if (bridge) { + bridge->InvalidateOnCompositorThread(); + bridge->ScheduleRenderOnCompositorThread(); + } + } +}; + +template<> const char +nsWindow::NativePtr<nsWindow::LayerViewSupport>::sName[] = "LayerViewSupport"; + +/* PresentationMediaPlayerManager native calls access inner nsWindow functionality so PMPMSupport is a child class of nsWindow */ +class nsWindow::PMPMSupport final + : public PresentationMediaPlayerManager::Natives<PMPMSupport> +{ + PMPMSupport() = delete; + + static LayerViewSupport* GetLayerViewSupport(jni::Object::Param aView) + { + const auto& layerView = LayerView::Ref::From(aView); + + LayerView::Compositor::LocalRef compositor = layerView->GetCompositor(); + if (!layerView->CompositorCreated() || !compositor) { + return nullptr; + } + + LayerViewSupport* const lvs = LayerViewSupport::FromNative(compositor); + if (!lvs) { + // There is a pending exception whenever FromNative returns nullptr. + compositor.Env()->ExceptionClear(); + } + return lvs; + } + +public: + static ANativeWindow* sWindow; + static EGLSurface sSurface; + + static void InvalidateAndScheduleComposite(jni::Object::Param aView) + { + LayerViewSupport* const lvs = GetLayerViewSupport(aView); + if (lvs) { + lvs->SyncInvalidateAndScheduleComposite(); + } + } + + static void AddPresentationSurface(const jni::Class::LocalRef& aCls, + jni::Object::Param aView, + jni::Object::Param aSurface) + { + RemovePresentationSurface(); + + LayerViewSupport* const lvs = GetLayerViewSupport(aView); + if (!lvs) { + return; + } + + ANativeWindow* const window = ANativeWindow_fromSurface( + aCls.Env(), aSurface.Get()); + if (!window) { + return; + } + + sWindow = window; + + const bool wasAlreadyPaused = lvs->CompositorPaused(); + if (!wasAlreadyPaused) { + lvs->SyncPauseCompositor(); + } + + if (sSurface) { + // Destroy the EGL surface! The compositor is paused so it should + // be okay to destroy the surface here. + mozilla::gl::GLContextProvider::DestroyEGLSurface(sSurface); + sSurface = nullptr; + } + + if (!wasAlreadyPaused) { + lvs->SyncResumeCompositor(); + } + + lvs->SyncInvalidateAndScheduleComposite(); + } + + static void RemovePresentationSurface() + { + if (sWindow) { + ANativeWindow_release(sWindow); + sWindow = nullptr; + } + } +}; + +ANativeWindow* nsWindow::PMPMSupport::sWindow; +EGLSurface nsWindow::PMPMSupport::sSurface; + + +nsWindow::GeckoViewSupport::~GeckoViewSupport() +{ + // Disassociate our GeckoEditable instance with our native object. + // OnDestroy will call disposeNative after any pending native calls have + // been made. + MOZ_ASSERT(mEditable); + mEditable->OnViewChange(nullptr); + + if (window.mNPZCSupport) { + window.mNPZCSupport.Detach(); + } + + if (window.mLayerViewSupport) { + window.mLayerViewSupport.Detach(); + } +} + +/* static */ void +nsWindow::GeckoViewSupport::Open(const jni::Class::LocalRef& aCls, + GeckoView::Window::Param aWindow, + GeckoView::Param aView, + jni::Object::Param aCompositor, + jni::String::Param aChromeURI, + int32_t aScreenId) +{ + MOZ_ASSERT(NS_IsMainThread()); + + PROFILER_LABEL("nsWindow", "GeckoViewSupport::Open", + js::ProfileEntry::Category::OTHER); + + nsCOMPtr<nsIWindowWatcher> ww = do_GetService(NS_WINDOWWATCHER_CONTRACTID); + MOZ_RELEASE_ASSERT(ww); + + nsAdoptingCString url; + if (aChromeURI) { + url = aChromeURI->ToCString(); + } else { + url = Preferences::GetCString("toolkit.defaultChromeURI"); + if (!url) { + url = NS_LITERAL_CSTRING("chrome://browser/content/browser.xul"); + } + } + + nsCOMPtr<mozIDOMWindowProxy> domWindow; + ww->OpenWindow(nullptr, url, nullptr, "chrome,dialog=0,resizable,scrollbars=yes", + nullptr, getter_AddRefs(domWindow)); + MOZ_RELEASE_ASSERT(domWindow); + + nsCOMPtr<nsPIDOMWindowOuter> pdomWindow = + nsPIDOMWindowOuter::From(domWindow); + nsCOMPtr<nsIWidget> widget = WidgetUtils::DOMWindowToWidget(pdomWindow); + MOZ_ASSERT(widget); + + const auto window = static_cast<nsWindow*>(widget.get()); + window->SetScreenId(aScreenId); + + // Attach a new GeckoView support object to the new window. + window->mGeckoViewSupport = mozilla::MakeUnique<GeckoViewSupport>( + window, GeckoView::Window::LocalRef(aCls.Env(), aWindow), aView); + + window->mGeckoViewSupport->mDOMWindow = pdomWindow; + + // Attach the Compositor to the new window. + auto compositor = LayerView::Compositor::LocalRef( + aCls.Env(), LayerView::Compositor::Ref::From(aCompositor)); + window->mLayerViewSupport.Attach(compositor, window, compositor); + + if (window->mWidgetListener) { + nsCOMPtr<nsIXULWindow> xulWindow( + window->mWidgetListener->GetXULWindow()); + if (xulWindow) { + // Our window is not intrinsically sized, so tell nsXULWindow to + // not set a size for us. + xulWindow->SetIntrinsicallySized(false); + } + } +} + +void +nsWindow::GeckoViewSupport::Close() +{ + if (!mDOMWindow) { + return; + } + + mDOMWindow->ForceClose(); + mDOMWindow = nullptr; +} + +void +nsWindow::GeckoViewSupport::Reattach(const GeckoView::Window::LocalRef& inst, + GeckoView::Param aView, + jni::Object::Param aCompositor) +{ + // Associate our previous GeckoEditable with the new GeckoView. + mEditable->OnViewChange(aView); + + // mNPZCSupport might have already been detached through the Java side calling + // NativePanZoomController.destroy(). + if (window.mNPZCSupport) { + window.mNPZCSupport.Detach(); + } + + MOZ_ASSERT(window.mLayerViewSupport); + window.mLayerViewSupport.Detach(); + + auto compositor = LayerView::Compositor::LocalRef( + inst.Env(), LayerView::Compositor::Ref::From(aCompositor)); + window.mLayerViewSupport.Attach(compositor, &window, compositor); + compositor->Reattach(); +} + +void +nsWindow::GeckoViewSupport::LoadUri(jni::String::Param aUri, int32_t aFlags) +{ + if (!mDOMWindow) { + return; + } + + nsCOMPtr<nsIURI> uri = nsAppShell::ResolveURI(aUri->ToCString()); + if (NS_WARN_IF(!uri)) { + return; + } + + nsCOMPtr<nsIDOMChromeWindow> chromeWin = do_QueryInterface(mDOMWindow); + nsCOMPtr<nsIBrowserDOMWindow> browserWin; + + if (NS_WARN_IF(!chromeWin) || NS_WARN_IF(NS_FAILED( + chromeWin->GetBrowserDOMWindow(getter_AddRefs(browserWin))))) { + return; + } + + const int flags = aFlags == GeckoView::LOAD_NEW_TAB ? + nsIBrowserDOMWindow::OPEN_NEWTAB : + aFlags == GeckoView::LOAD_SWITCH_TAB ? + nsIBrowserDOMWindow::OPEN_SWITCHTAB : + nsIBrowserDOMWindow::OPEN_CURRENTWINDOW; + nsCOMPtr<mozIDOMWindowProxy> newWin; + + if (NS_FAILED(browserWin->OpenURI( + uri, nullptr, flags, nsIBrowserDOMWindow::OPEN_EXTERNAL, + getter_AddRefs(newWin)))) { + NS_WARNING("Failed to open URI"); + } +} + +void +nsWindow::InitNatives() +{ + nsWindow::GeckoViewSupport::Base::Init(); + nsWindow::GeckoViewSupport::EditableBase::Init(); + nsWindow::LayerViewSupport::Init(); + nsWindow::NPZCSupport::Init(); + if (jni::IsFennec()) { + nsWindow::PMPMSupport::Init(); + } +} + +nsWindow* +nsWindow::TopWindow() +{ + if (!gTopLevelWindows.IsEmpty()) + return gTopLevelWindows[0]; + return nullptr; +} + +void +nsWindow::LogWindow(nsWindow *win, int index, int indent) +{ +#if defined(DEBUG) || defined(FORCE_ALOG) + char spaces[] = " "; + spaces[indent < 20 ? indent : 20] = 0; + ALOG("%s [% 2d] 0x%08x [parent 0x%08x] [% 3d,% 3dx% 3d,% 3d] vis %d type %d", + spaces, index, (intptr_t)win, (intptr_t)win->mParent, + win->mBounds.x, win->mBounds.y, + win->mBounds.width, win->mBounds.height, + win->mIsVisible, win->mWindowType); +#endif +} + +void +nsWindow::DumpWindows() +{ + DumpWindows(gTopLevelWindows); +} + +void +nsWindow::DumpWindows(const nsTArray<nsWindow*>& wins, int indent) +{ + for (uint32_t i = 0; i < wins.Length(); ++i) { + nsWindow *w = wins[i]; + LogWindow(w, i, indent); + DumpWindows(w->mChildren, indent+1); + } +} + +nsWindow::nsWindow() : + mScreenId(0), // Use 0 (primary screen) as the default value. + mIsVisible(false), + mParent(nullptr), + mAwaitingFullScreen(false), + mIsFullScreen(false) +{ +} + +nsWindow::~nsWindow() +{ + gTopLevelWindows.RemoveElement(this); + ALOG("nsWindow %p destructor", (void*)this); +} + +bool +nsWindow::IsTopLevel() +{ + return mWindowType == eWindowType_toplevel || + mWindowType == eWindowType_dialog || + mWindowType == eWindowType_invisible; +} + +nsresult +nsWindow::Create(nsIWidget* aParent, + nsNativeWidget aNativeParent, + const LayoutDeviceIntRect& aRect, + nsWidgetInitData* aInitData) +{ + ALOG("nsWindow[%p]::Create %p [%d %d %d %d]", (void*)this, (void*)aParent, + aRect.x, aRect.y, aRect.width, aRect.height); + + nsWindow *parent = (nsWindow*) aParent; + if (aNativeParent) { + if (parent) { + ALOG("Ignoring native parent on Android window [%p], " + "since parent was specified (%p %p)", (void*)this, + (void*)aNativeParent, (void*)aParent); + } else { + parent = (nsWindow*) aNativeParent; + } + } + + mBounds = aRect; + + BaseCreate(nullptr, aInitData); + + NS_ASSERTION(IsTopLevel() || parent, + "non-top-level window doesn't have a parent!"); + + if (IsTopLevel()) { + gTopLevelWindows.AppendElement(this); + + } else if (parent) { + parent->mChildren.AppendElement(this); + mParent = parent; + } + +#ifdef DEBUG_ANDROID_WIDGET + DumpWindows(); +#endif + + return NS_OK; +} + +void +nsWindow::Destroy() +{ + nsBaseWidget::mOnDestroyCalled = true; + + if (mGeckoViewSupport) { + // Disassociate our native object with GeckoView. + mGeckoViewSupport = nullptr; + } + + // Stuff below may release the last ref to this + nsCOMPtr<nsIWidget> kungFuDeathGrip(this); + + while (mChildren.Length()) { + // why do we still have children? + ALOG("### Warning: Destroying window %p and reparenting child %p to null!", (void*)this, (void*)mChildren[0]); + mChildren[0]->SetParent(nullptr); + } + + nsBaseWidget::Destroy(); + + if (IsTopLevel()) + gTopLevelWindows.RemoveElement(this); + + SetParent(nullptr); + + nsBaseWidget::OnDestroy(); + +#ifdef DEBUG_ANDROID_WIDGET + DumpWindows(); +#endif +} + +NS_IMETHODIMP +nsWindow::ConfigureChildren(const nsTArray<nsIWidget::Configuration>& config) +{ + for (uint32_t i = 0; i < config.Length(); ++i) { + nsWindow *childWin = (nsWindow*) config[i].mChild.get(); + childWin->Resize(config[i].mBounds.x, + config[i].mBounds.y, + config[i].mBounds.width, + config[i].mBounds.height, + false); + } + + return NS_OK; +} + +void +nsWindow::RedrawAll() +{ + if (mAttachedWidgetListener) { + mAttachedWidgetListener->RequestRepaint(); + } else if (mWidgetListener) { + mWidgetListener->RequestRepaint(); + } +} + +NS_IMETHODIMP +nsWindow::SetParent(nsIWidget *aNewParent) +{ + if ((nsIWidget*)mParent == aNewParent) + return NS_OK; + + // If we had a parent before, remove ourselves from its list of + // children. + if (mParent) + mParent->mChildren.RemoveElement(this); + + mParent = (nsWindow*)aNewParent; + + if (mParent) + mParent->mChildren.AppendElement(this); + + // if we are now in the toplevel window's hierarchy, schedule a redraw + if (FindTopLevel() == nsWindow::TopWindow()) + RedrawAll(); + + return NS_OK; +} + +nsIWidget* +nsWindow::GetParent() +{ + return mParent; +} + +float +nsWindow::GetDPI() +{ + if (AndroidBridge::Bridge()) + return AndroidBridge::Bridge()->GetDPI(); + return 160.0f; +} + +double +nsWindow::GetDefaultScaleInternal() +{ + + nsCOMPtr<nsIScreen> screen = GetWidgetScreen(); + MOZ_ASSERT(screen); + RefPtr<nsScreenAndroid> screenAndroid = (nsScreenAndroid*) screen.get(); + return screenAndroid->GetDensity(); +} + +NS_IMETHODIMP +nsWindow::Show(bool aState) +{ + ALOG("nsWindow[%p]::Show %d", (void*)this, aState); + + if (mWindowType == eWindowType_invisible) { + ALOG("trying to show invisible window! ignoring.."); + return NS_ERROR_FAILURE; + } + + if (aState == mIsVisible) + return NS_OK; + + mIsVisible = aState; + + if (IsTopLevel()) { + // XXX should we bring this to the front when it's shown, + // if it's a toplevel widget? + + // XXX we should synthesize a eMouseExitFromWidget (for old top + // window)/eMouseEnterIntoWidget (for new top window) since we need + // to pretend that the top window always has focus. Not sure + // if Show() is the right place to do this, though. + + if (aState) { + // It just became visible, so bring it to the front. + BringToFront(); + + } else if (nsWindow::TopWindow() == this) { + // find the next visible window to show + unsigned int i; + for (i = 1; i < gTopLevelWindows.Length(); i++) { + nsWindow *win = gTopLevelWindows[i]; + if (!win->mIsVisible) + continue; + + win->BringToFront(); + break; + } + } + } else if (FindTopLevel() == nsWindow::TopWindow()) { + RedrawAll(); + } + +#ifdef DEBUG_ANDROID_WIDGET + DumpWindows(); +#endif + + return NS_OK; +} + +bool +nsWindow::IsVisible() const +{ + return mIsVisible; +} + +void +nsWindow::ConstrainPosition(bool aAllowSlop, + int32_t *aX, + int32_t *aY) +{ + ALOG("nsWindow[%p]::ConstrainPosition %d [%d %d]", (void*)this, aAllowSlop, *aX, *aY); + + // constrain toplevel windows; children we don't care about + if (IsTopLevel()) { + *aX = 0; + *aY = 0; + } +} + +NS_IMETHODIMP +nsWindow::Move(double aX, + double aY) +{ + if (IsTopLevel()) + return NS_OK; + + return Resize(aX, + aY, + mBounds.width, + mBounds.height, + true); +} + +NS_IMETHODIMP +nsWindow::Resize(double aWidth, + double aHeight, + bool aRepaint) +{ + return Resize(mBounds.x, + mBounds.y, + aWidth, + aHeight, + aRepaint); +} + +NS_IMETHODIMP +nsWindow::Resize(double aX, + double aY, + double aWidth, + double aHeight, + bool aRepaint) +{ + ALOG("nsWindow[%p]::Resize [%f %f %f %f] (repaint %d)", (void*)this, aX, aY, aWidth, aHeight, aRepaint); + + bool needSizeDispatch = aWidth != mBounds.width || aHeight != mBounds.height; + + mBounds.x = NSToIntRound(aX); + mBounds.y = NSToIntRound(aY); + mBounds.width = NSToIntRound(aWidth); + mBounds.height = NSToIntRound(aHeight); + + if (needSizeDispatch) { + OnSizeChanged(gfx::IntSize::Truncate(aWidth, aHeight)); + } + + // Should we skip honoring aRepaint here? + if (aRepaint && FindTopLevel() == nsWindow::TopWindow()) + RedrawAll(); + + nsIWidgetListener* listener = GetWidgetListener(); + if (mAwaitingFullScreen && listener) { + listener->FullscreenChanged(mIsFullScreen); + mAwaitingFullScreen = false; + } + + return NS_OK; +} + +void +nsWindow::SetZIndex(int32_t aZIndex) +{ + ALOG("nsWindow[%p]::SetZIndex %d ignored", (void*)this, aZIndex); +} + +void +nsWindow::SetSizeMode(nsSizeMode aMode) +{ + switch (aMode) { + case nsSizeMode_Minimized: + GeckoAppShell::MoveTaskToBack(); + break; + case nsSizeMode_Fullscreen: + MakeFullScreen(true); + break; + default: + break; + } +} + +NS_IMETHODIMP +nsWindow::Enable(bool aState) +{ + ALOG("nsWindow[%p]::Enable %d ignored", (void*)this, aState); + return NS_OK; +} + +bool +nsWindow::IsEnabled() const +{ + return true; +} + +NS_IMETHODIMP +nsWindow::Invalidate(const LayoutDeviceIntRect& aRect) +{ + return NS_OK; +} + +nsWindow* +nsWindow::FindTopLevel() +{ + nsWindow *toplevel = this; + while (toplevel) { + if (toplevel->IsTopLevel()) + return toplevel; + + toplevel = toplevel->mParent; + } + + ALOG("nsWindow::FindTopLevel(): couldn't find a toplevel or dialog window in this [%p] widget's hierarchy!", (void*)this); + return this; +} + +NS_IMETHODIMP +nsWindow::SetFocus(bool aRaise) +{ + nsWindow *top = FindTopLevel(); + top->BringToFront(); + + return NS_OK; +} + +void +nsWindow::BringToFront() +{ + // If the window to be raised is the same as the currently raised one, + // do nothing. We need to check the focus manager as well, as the first + // window that is created will be first in the window list but won't yet + // be focused. + nsCOMPtr<nsIFocusManager> fm = do_GetService(FOCUSMANAGER_CONTRACTID); + nsCOMPtr<mozIDOMWindowProxy> existingTopWindow; + fm->GetActiveWindow(getter_AddRefs(existingTopWindow)); + if (existingTopWindow && FindTopLevel() == nsWindow::TopWindow()) + return; + + if (!IsTopLevel()) { + FindTopLevel()->BringToFront(); + return; + } + + RefPtr<nsWindow> kungFuDeathGrip(this); + + nsWindow *oldTop = nullptr; + if (!gTopLevelWindows.IsEmpty()) { + oldTop = gTopLevelWindows[0]; + } + + gTopLevelWindows.RemoveElement(this); + gTopLevelWindows.InsertElementAt(0, this); + + if (oldTop) { + nsIWidgetListener* listener = oldTop->GetWidgetListener(); + if (listener) { + listener->WindowDeactivated(); + } + } + + if (mWidgetListener) { + mWidgetListener->WindowActivated(); + } + + RedrawAll(); +} + +LayoutDeviceIntRect +nsWindow::GetScreenBounds() +{ + return LayoutDeviceIntRect(WidgetToScreenOffset(), mBounds.Size()); +} + +LayoutDeviceIntPoint +nsWindow::WidgetToScreenOffset() +{ + LayoutDeviceIntPoint p(0, 0); + nsWindow *w = this; + + while (w && !w->IsTopLevel()) { + p.x += w->mBounds.x; + p.y += w->mBounds.y; + + w = w->mParent; + } + + return p; +} + +NS_IMETHODIMP +nsWindow::DispatchEvent(WidgetGUIEvent* aEvent, + nsEventStatus& aStatus) +{ + aStatus = DispatchEvent(aEvent); + return NS_OK; +} + +nsEventStatus +nsWindow::DispatchEvent(WidgetGUIEvent* aEvent) +{ + if (mAttachedWidgetListener) { + return mAttachedWidgetListener->HandleEvent(aEvent, mUseAttachedEvents); + } else if (mWidgetListener) { + return mWidgetListener->HandleEvent(aEvent, mUseAttachedEvents); + } + return nsEventStatus_eIgnore; +} + +nsresult +nsWindow::MakeFullScreen(bool aFullScreen, nsIScreen*) +{ + mIsFullScreen = aFullScreen; + mAwaitingFullScreen = true; + GeckoAppShell::SetFullScreen(aFullScreen); + return NS_OK; +} + +mozilla::layers::LayerManager* +nsWindow::GetLayerManager(PLayerTransactionChild*, LayersBackend, LayerManagerPersistence) +{ + if (mLayerManager) { + return mLayerManager; + } + return nullptr; +} + +void +nsWindow::CreateLayerManager(int aCompositorWidth, int aCompositorHeight) +{ + if (mLayerManager) { + return; + } + + nsWindow *topLevelWindow = FindTopLevel(); + if (!topLevelWindow || topLevelWindow->mWindowType == eWindowType_invisible) { + // don't create a layer manager for an invisible top-level window + return; + } + + // Ensure that gfxPlatform is initialized first. + gfxPlatform::GetPlatform(); + + if (ShouldUseOffMainThreadCompositing()) { + CreateCompositor(aCompositorWidth, aCompositorHeight); + if (mLayerManager) { + return; + } + + // If we get here, then off main thread compositing failed to initialize. + sFailedToCreateGLContext = true; + } + + if (!ComputeShouldAccelerate() || sFailedToCreateGLContext) { + printf_stderr(" -- creating basic, not accelerated\n"); + mLayerManager = CreateBasicLayerManager(); + } +} + +void +nsWindow::OnSizeChanged(const gfx::IntSize& aSize) +{ + ALOG("nsWindow: %p OnSizeChanged [%d %d]", (void*)this, aSize.width, aSize.height); + + mBounds.width = aSize.width; + mBounds.height = aSize.height; + + if (mWidgetListener) { + mWidgetListener->WindowResized(this, aSize.width, aSize.height); + } + + if (mAttachedWidgetListener) { + mAttachedWidgetListener->WindowResized(this, aSize.width, aSize.height); + } +} + +void +nsWindow::InitEvent(WidgetGUIEvent& event, LayoutDeviceIntPoint* aPoint) +{ + if (aPoint) { + event.mRefPoint = *aPoint; + } else { + event.mRefPoint = LayoutDeviceIntPoint(0, 0); + } + + event.mTime = PR_Now() / 1000; +} + +void +nsWindow::UpdateOverscrollVelocity(const float aX, const float aY) +{ + if (NativePtr<NPZCSupport>::Locked npzcs{mNPZCSupport}) { + npzcs->UpdateOverscrollVelocity(aX, aY); + } +} + +void +nsWindow::UpdateOverscrollOffset(const float aX, const float aY) +{ + if (NativePtr<NPZCSupport>::Locked npzcs{mNPZCSupport}) { + npzcs->UpdateOverscrollOffset(aX, aY); + } +} + +void +nsWindow::SetScrollingRootContent(const bool isRootContent) +{ + // On Android, the Controller thread and UI thread are the same. + MOZ_ASSERT(APZThreadUtils::IsControllerThread(), "nsWindow::SetScrollingRootContent must be called from the controller thread"); + + if (NativePtr<NPZCSupport>::Locked npzcs{mNPZCSupport}) { + npzcs->SetScrollingRootContent(isRootContent); + } +} + +void +nsWindow::SetSelectionDragState(bool aState) +{ + if (NativePtr<NPZCSupport>::Locked npzcs{mNPZCSupport}) { + npzcs->SetSelectionDragState(aState); + } +} + +void * +nsWindow::GetNativeData(uint32_t aDataType) +{ + switch (aDataType) { + // used by GLContextProviderEGL, nullptr is EGL_DEFAULT_DISPLAY + case NS_NATIVE_DISPLAY: + return nullptr; + + case NS_NATIVE_WIDGET: + return (void *) this; + + case NS_RAW_NATIVE_IME_CONTEXT: { + void* pseudoIMEContext = GetPseudoIMEContext(); + if (pseudoIMEContext) { + return pseudoIMEContext; + } + // We assume that there is only one context per process on Android + return NS_ONLY_ONE_NATIVE_IME_CONTEXT; + } + + case NS_JAVA_SURFACE: + if (NativePtr<LayerViewSupport>::Locked lvs{mLayerViewSupport}) { + return lvs->GetSurface().Get(); + } + return nullptr; + + case NS_PRESENTATION_WINDOW: + return PMPMSupport::sWindow; + + case NS_PRESENTATION_SURFACE: + return PMPMSupport::sSurface; + } + + return nullptr; +} + +void +nsWindow::SetNativeData(uint32_t aDataType, uintptr_t aVal) +{ + switch (aDataType) { + case NS_PRESENTATION_SURFACE: + PMPMSupport::sSurface = reinterpret_cast<EGLSurface>(aVal); + break; + } +} + +void +nsWindow::DispatchHitTest(const WidgetTouchEvent& aEvent) +{ + if (aEvent.mMessage == eTouchStart && aEvent.mTouches.Length() == 1) { + // Since touch events don't get retargeted by PositionedEventTargeting.cpp + // code on Fennec, we dispatch a dummy mouse event that *does* get + // retargeted. The Fennec browser.js code can use this to activate the + // highlight element in case the this touchstart is the start of a tap. + WidgetMouseEvent hittest(true, eMouseHitTest, this, + WidgetMouseEvent::eReal); + hittest.mRefPoint = aEvent.mTouches[0]->mRefPoint; + hittest.mIgnoreRootScrollFrame = true; + hittest.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH; + nsEventStatus status; + DispatchEvent(&hittest, status); + + if (mAPZEventState && hittest.hitCluster) { + mAPZEventState->ProcessClusterHit(); + } + } +} + +static unsigned int ConvertAndroidKeyCodeToDOMKeyCode(int androidKeyCode) +{ + // Special-case alphanumeric keycodes because they are most common. + if (androidKeyCode >= AKEYCODE_A && + androidKeyCode <= AKEYCODE_Z) { + return androidKeyCode - AKEYCODE_A + NS_VK_A; + } + + if (androidKeyCode >= AKEYCODE_0 && + androidKeyCode <= AKEYCODE_9) { + return androidKeyCode - AKEYCODE_0 + NS_VK_0; + } + + switch (androidKeyCode) { + // KEYCODE_UNKNOWN (0) ... KEYCODE_HOME (3) + case AKEYCODE_BACK: return NS_VK_ESCAPE; + // KEYCODE_CALL (5) ... KEYCODE_POUND (18) + case AKEYCODE_DPAD_UP: return NS_VK_UP; + case AKEYCODE_DPAD_DOWN: return NS_VK_DOWN; + case AKEYCODE_DPAD_LEFT: return NS_VK_LEFT; + case AKEYCODE_DPAD_RIGHT: return NS_VK_RIGHT; + case AKEYCODE_DPAD_CENTER: return NS_VK_RETURN; + case AKEYCODE_VOLUME_UP: return NS_VK_VOLUME_UP; + case AKEYCODE_VOLUME_DOWN: return NS_VK_VOLUME_DOWN; + // KEYCODE_VOLUME_POWER (26) ... KEYCODE_Z (54) + case AKEYCODE_COMMA: return NS_VK_COMMA; + case AKEYCODE_PERIOD: return NS_VK_PERIOD; + case AKEYCODE_ALT_LEFT: return NS_VK_ALT; + case AKEYCODE_ALT_RIGHT: return NS_VK_ALT; + case AKEYCODE_SHIFT_LEFT: return NS_VK_SHIFT; + case AKEYCODE_SHIFT_RIGHT: return NS_VK_SHIFT; + case AKEYCODE_TAB: return NS_VK_TAB; + case AKEYCODE_SPACE: return NS_VK_SPACE; + // KEYCODE_SYM (63) ... KEYCODE_ENVELOPE (65) + case AKEYCODE_ENTER: return NS_VK_RETURN; + case AKEYCODE_DEL: return NS_VK_BACK; // Backspace + case AKEYCODE_GRAVE: return NS_VK_BACK_QUOTE; + // KEYCODE_MINUS (69) + case AKEYCODE_EQUALS: return NS_VK_EQUALS; + case AKEYCODE_LEFT_BRACKET: return NS_VK_OPEN_BRACKET; + case AKEYCODE_RIGHT_BRACKET: return NS_VK_CLOSE_BRACKET; + case AKEYCODE_BACKSLASH: return NS_VK_BACK_SLASH; + case AKEYCODE_SEMICOLON: return NS_VK_SEMICOLON; + // KEYCODE_APOSTROPHE (75) + case AKEYCODE_SLASH: return NS_VK_SLASH; + // KEYCODE_AT (77) ... KEYCODE_MEDIA_FAST_FORWARD (90) + case AKEYCODE_MUTE: return NS_VK_VOLUME_MUTE; + case AKEYCODE_PAGE_UP: return NS_VK_PAGE_UP; + case AKEYCODE_PAGE_DOWN: return NS_VK_PAGE_DOWN; + // KEYCODE_PICTSYMBOLS (94) ... KEYCODE_BUTTON_MODE (110) + case AKEYCODE_ESCAPE: return NS_VK_ESCAPE; + case AKEYCODE_FORWARD_DEL: return NS_VK_DELETE; + case AKEYCODE_CTRL_LEFT: return NS_VK_CONTROL; + case AKEYCODE_CTRL_RIGHT: return NS_VK_CONTROL; + case AKEYCODE_CAPS_LOCK: return NS_VK_CAPS_LOCK; + case AKEYCODE_SCROLL_LOCK: return NS_VK_SCROLL_LOCK; + // KEYCODE_META_LEFT (117) ... KEYCODE_FUNCTION (119) + case AKEYCODE_SYSRQ: return NS_VK_PRINTSCREEN; + case AKEYCODE_BREAK: return NS_VK_PAUSE; + case AKEYCODE_MOVE_HOME: return NS_VK_HOME; + case AKEYCODE_MOVE_END: return NS_VK_END; + case AKEYCODE_INSERT: return NS_VK_INSERT; + // KEYCODE_FORWARD (125) ... KEYCODE_MEDIA_RECORD (130) + case AKEYCODE_F1: return NS_VK_F1; + case AKEYCODE_F2: return NS_VK_F2; + case AKEYCODE_F3: return NS_VK_F3; + case AKEYCODE_F4: return NS_VK_F4; + case AKEYCODE_F5: return NS_VK_F5; + case AKEYCODE_F6: return NS_VK_F6; + case AKEYCODE_F7: return NS_VK_F7; + case AKEYCODE_F8: return NS_VK_F8; + case AKEYCODE_F9: return NS_VK_F9; + case AKEYCODE_F10: return NS_VK_F10; + case AKEYCODE_F11: return NS_VK_F11; + case AKEYCODE_F12: return NS_VK_F12; + case AKEYCODE_NUM_LOCK: return NS_VK_NUM_LOCK; + case AKEYCODE_NUMPAD_0: return NS_VK_NUMPAD0; + case AKEYCODE_NUMPAD_1: return NS_VK_NUMPAD1; + case AKEYCODE_NUMPAD_2: return NS_VK_NUMPAD2; + case AKEYCODE_NUMPAD_3: return NS_VK_NUMPAD3; + case AKEYCODE_NUMPAD_4: return NS_VK_NUMPAD4; + case AKEYCODE_NUMPAD_5: return NS_VK_NUMPAD5; + case AKEYCODE_NUMPAD_6: return NS_VK_NUMPAD6; + case AKEYCODE_NUMPAD_7: return NS_VK_NUMPAD7; + case AKEYCODE_NUMPAD_8: return NS_VK_NUMPAD8; + case AKEYCODE_NUMPAD_9: return NS_VK_NUMPAD9; + case AKEYCODE_NUMPAD_DIVIDE: return NS_VK_DIVIDE; + case AKEYCODE_NUMPAD_MULTIPLY: return NS_VK_MULTIPLY; + case AKEYCODE_NUMPAD_SUBTRACT: return NS_VK_SUBTRACT; + case AKEYCODE_NUMPAD_ADD: return NS_VK_ADD; + case AKEYCODE_NUMPAD_DOT: return NS_VK_DECIMAL; + case AKEYCODE_NUMPAD_COMMA: return NS_VK_SEPARATOR; + case AKEYCODE_NUMPAD_ENTER: return NS_VK_RETURN; + case AKEYCODE_NUMPAD_EQUALS: return NS_VK_EQUALS; + // KEYCODE_NUMPAD_LEFT_PAREN (162) ... KEYCODE_CALCULATOR (210) + + // Needs to confirm the behavior. If the key switches the open state + // of Japanese IME (or switches input character between Hiragana and + // Roman numeric characters), then, it might be better to use + // NS_VK_KANJI which is used for Alt+Zenkaku/Hankaku key on Windows. + case AKEYCODE_ZENKAKU_HANKAKU: return 0; + case AKEYCODE_EISU: return NS_VK_EISU; + case AKEYCODE_MUHENKAN: return NS_VK_NONCONVERT; + case AKEYCODE_HENKAN: return NS_VK_CONVERT; + case AKEYCODE_KATAKANA_HIRAGANA: return 0; + case AKEYCODE_YEN: return NS_VK_BACK_SLASH; // Same as other platforms. + case AKEYCODE_RO: return NS_VK_BACK_SLASH; // Same as other platforms. + case AKEYCODE_KANA: return NS_VK_KANA; + case AKEYCODE_ASSIST: return NS_VK_HELP; + + // the A key is the action key for gamepad devices. + case AKEYCODE_BUTTON_A: return NS_VK_RETURN; + + default: + ALOG("ConvertAndroidKeyCodeToDOMKeyCode: " + "No DOM keycode for Android keycode %d", androidKeyCode); + return 0; + } +} + +static KeyNameIndex +ConvertAndroidKeyCodeToKeyNameIndex(int keyCode, int action, + int domPrintableKeyValue) +{ + // Special-case alphanumeric keycodes because they are most common. + if (keyCode >= AKEYCODE_A && keyCode <= AKEYCODE_Z) { + return KEY_NAME_INDEX_USE_STRING; + } + + if (keyCode >= AKEYCODE_0 && keyCode <= AKEYCODE_9) { + return KEY_NAME_INDEX_USE_STRING; + } + + switch (keyCode) { + +#define NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX(aNativeKey, aKeyNameIndex) \ + case aNativeKey: return aKeyNameIndex; + +#include "NativeKeyToDOMKeyName.h" + +#undef NS_NATIVE_KEY_TO_DOM_KEY_NAME_INDEX + + // KEYCODE_0 (7) ... KEYCODE_9 (16) + case AKEYCODE_STAR: // '*' key + case AKEYCODE_POUND: // '#' key + + // KEYCODE_A (29) ... KEYCODE_Z (54) + + case AKEYCODE_COMMA: // ',' key + case AKEYCODE_PERIOD: // '.' key + case AKEYCODE_SPACE: + case AKEYCODE_GRAVE: // '`' key + case AKEYCODE_MINUS: // '-' key + case AKEYCODE_EQUALS: // '=' key + case AKEYCODE_LEFT_BRACKET: // '[' key + case AKEYCODE_RIGHT_BRACKET: // ']' key + case AKEYCODE_BACKSLASH: // '\' key + case AKEYCODE_SEMICOLON: // ';' key + case AKEYCODE_APOSTROPHE: // ''' key + case AKEYCODE_SLASH: // '/' key + case AKEYCODE_AT: // '@' key + case AKEYCODE_PLUS: // '+' key + + case AKEYCODE_NUMPAD_0: + case AKEYCODE_NUMPAD_1: + case AKEYCODE_NUMPAD_2: + case AKEYCODE_NUMPAD_3: + case AKEYCODE_NUMPAD_4: + case AKEYCODE_NUMPAD_5: + case AKEYCODE_NUMPAD_6: + case AKEYCODE_NUMPAD_7: + case AKEYCODE_NUMPAD_8: + case AKEYCODE_NUMPAD_9: + case AKEYCODE_NUMPAD_DIVIDE: + case AKEYCODE_NUMPAD_MULTIPLY: + case AKEYCODE_NUMPAD_SUBTRACT: + case AKEYCODE_NUMPAD_ADD: + case AKEYCODE_NUMPAD_DOT: + case AKEYCODE_NUMPAD_COMMA: + case AKEYCODE_NUMPAD_EQUALS: + case AKEYCODE_NUMPAD_LEFT_PAREN: + case AKEYCODE_NUMPAD_RIGHT_PAREN: + + case AKEYCODE_YEN: // yen sign key + case AKEYCODE_RO: // Japanese Ro key + return KEY_NAME_INDEX_USE_STRING; + + case AKEYCODE_ENDCALL: + case AKEYCODE_NUM: // XXX Not sure + case AKEYCODE_HEADSETHOOK: + case AKEYCODE_NOTIFICATION: // XXX Not sure + case AKEYCODE_PICTSYMBOLS: + + case AKEYCODE_BUTTON_A: + case AKEYCODE_BUTTON_B: + case AKEYCODE_BUTTON_C: + case AKEYCODE_BUTTON_X: + case AKEYCODE_BUTTON_Y: + case AKEYCODE_BUTTON_Z: + case AKEYCODE_BUTTON_L1: + case AKEYCODE_BUTTON_R1: + case AKEYCODE_BUTTON_L2: + case AKEYCODE_BUTTON_R2: + case AKEYCODE_BUTTON_THUMBL: + case AKEYCODE_BUTTON_THUMBR: + case AKEYCODE_BUTTON_START: + case AKEYCODE_BUTTON_SELECT: + case AKEYCODE_BUTTON_MODE: + + case AKEYCODE_MUTE: // mutes the microphone + case AKEYCODE_MEDIA_CLOSE: + + case AKEYCODE_DVR: + + case AKEYCODE_BUTTON_1: + case AKEYCODE_BUTTON_2: + case AKEYCODE_BUTTON_3: + case AKEYCODE_BUTTON_4: + case AKEYCODE_BUTTON_5: + case AKEYCODE_BUTTON_6: + case AKEYCODE_BUTTON_7: + case AKEYCODE_BUTTON_8: + case AKEYCODE_BUTTON_9: + case AKEYCODE_BUTTON_10: + case AKEYCODE_BUTTON_11: + case AKEYCODE_BUTTON_12: + case AKEYCODE_BUTTON_13: + case AKEYCODE_BUTTON_14: + case AKEYCODE_BUTTON_15: + case AKEYCODE_BUTTON_16: + + case AKEYCODE_MANNER_MODE: + case AKEYCODE_3D_MODE: + case AKEYCODE_CONTACTS: + return KEY_NAME_INDEX_Unidentified; + + case AKEYCODE_UNKNOWN: + MOZ_ASSERT( + action != AKEY_EVENT_ACTION_MULTIPLE, + "Don't call this when action is AKEY_EVENT_ACTION_MULTIPLE!"); + // It's actually an unknown key if the action isn't ACTION_MULTIPLE. + // However, it might cause text input. So, let's check the value. + return domPrintableKeyValue ? + KEY_NAME_INDEX_USE_STRING : KEY_NAME_INDEX_Unidentified; + + default: + ALOG("ConvertAndroidKeyCodeToKeyNameIndex: " + "No DOM key name index for Android keycode %d", keyCode); + return KEY_NAME_INDEX_Unidentified; + } +} + +static CodeNameIndex +ConvertAndroidScanCodeToCodeNameIndex(int scanCode) +{ + switch (scanCode) { + +#define NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX(aNativeKey, aCodeNameIndex) \ + case aNativeKey: return aCodeNameIndex; + +#include "NativeKeyToDOMCodeName.h" + +#undef NS_NATIVE_KEY_TO_DOM_CODE_NAME_INDEX + + default: + return CODE_NAME_INDEX_UNKNOWN; + } +} + +static bool +IsModifierKey(int32_t keyCode) +{ + using mozilla::java::sdk::KeyEvent; + return keyCode == KeyEvent::KEYCODE_ALT_LEFT || + keyCode == KeyEvent::KEYCODE_ALT_RIGHT || + keyCode == KeyEvent::KEYCODE_SHIFT_LEFT || + keyCode == KeyEvent::KEYCODE_SHIFT_RIGHT || + keyCode == KeyEvent::KEYCODE_CTRL_LEFT || + keyCode == KeyEvent::KEYCODE_CTRL_RIGHT || + keyCode == KeyEvent::KEYCODE_META_LEFT || + keyCode == KeyEvent::KEYCODE_META_RIGHT; +} + +static Modifiers +GetModifiers(int32_t metaState) +{ + using mozilla::java::sdk::KeyEvent; + return (metaState & KeyEvent::META_ALT_MASK ? MODIFIER_ALT : 0) + | (metaState & KeyEvent::META_SHIFT_MASK ? MODIFIER_SHIFT : 0) + | (metaState & KeyEvent::META_CTRL_MASK ? MODIFIER_CONTROL : 0) + | (metaState & KeyEvent::META_META_MASK ? MODIFIER_META : 0) + | (metaState & KeyEvent::META_FUNCTION_ON ? MODIFIER_FN : 0) + | (metaState & KeyEvent::META_CAPS_LOCK_ON ? MODIFIER_CAPSLOCK : 0) + | (metaState & KeyEvent::META_NUM_LOCK_ON ? MODIFIER_NUMLOCK : 0) + | (metaState & KeyEvent::META_SCROLL_LOCK_ON ? MODIFIER_SCROLLLOCK : 0); +} + +static void +InitKeyEvent(WidgetKeyboardEvent& event, + int32_t action, int32_t keyCode, int32_t scanCode, + int32_t metaState, int64_t time, int32_t unicodeChar, + int32_t baseUnicodeChar, int32_t domPrintableKeyValue, + int32_t repeatCount, int32_t flags) +{ + const uint32_t domKeyCode = ConvertAndroidKeyCodeToDOMKeyCode(keyCode); + const int32_t charCode = unicodeChar ? unicodeChar : baseUnicodeChar; + + event.mModifiers = GetModifiers(metaState); + + if (event.mMessage == eKeyPress) { + // Android gives us \n, so filter out some control characters. + event.mIsChar = (charCode >= ' '); + event.mCharCode = event.mIsChar ? charCode : 0; + event.mKeyCode = event.mIsChar ? 0 : domKeyCode; + event.mPluginEvent.Clear(); + + // For keypress, if the unicode char already has modifiers applied, we + // don't specify extra modifiers. If UnicodeChar() != BaseUnicodeChar() + // it means UnicodeChar() already has modifiers applied. + // Note that on Android 4.x, Alt modifier isn't set when the key input + // causes text input even while right Alt key is pressed. However, + // this is necessary for Android 2.3 compatibility. + if (unicodeChar && unicodeChar != baseUnicodeChar) { + event.mModifiers &= ~(MODIFIER_ALT | MODIFIER_CONTROL + | MODIFIER_META); + } + + } else { + event.mIsChar = false; + event.mCharCode = 0; + event.mKeyCode = domKeyCode; + + ANPEvent pluginEvent; + pluginEvent.inSize = sizeof(pluginEvent); + pluginEvent.eventType = kKey_ANPEventType; + pluginEvent.data.key.action = event.mMessage == eKeyDown + ? kDown_ANPKeyAction : kUp_ANPKeyAction; + pluginEvent.data.key.nativeCode = keyCode; + pluginEvent.data.key.virtualCode = domKeyCode; + pluginEvent.data.key.unichar = charCode; + pluginEvent.data.key.modifiers = + (metaState & sdk::KeyEvent::META_SHIFT_MASK + ? kShift_ANPKeyModifier : 0) | + (metaState & sdk::KeyEvent::META_ALT_MASK + ? kAlt_ANPKeyModifier : 0); + pluginEvent.data.key.repeatCount = repeatCount; + event.mPluginEvent.Copy(pluginEvent); + } + + event.mIsRepeat = + (event.mMessage == eKeyDown || event.mMessage == eKeyPress) && + ((flags & sdk::KeyEvent::FLAG_LONG_PRESS) || repeatCount); + + event.mKeyNameIndex = ConvertAndroidKeyCodeToKeyNameIndex( + keyCode, action, domPrintableKeyValue); + event.mCodeNameIndex = ConvertAndroidScanCodeToCodeNameIndex(scanCode); + + if (event.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING && + domPrintableKeyValue) { + event.mKeyValue = char16_t(domPrintableKeyValue); + } + + event.mLocation = + WidgetKeyboardEvent::ComputeLocationFromCodeValue(event.mCodeNameIndex); + event.mTime = time; +} + +void +nsWindow::GeckoViewSupport::OnKeyEvent(int32_t aAction, int32_t aKeyCode, + int32_t aScanCode, int32_t aMetaState, int64_t aTime, + int32_t aUnicodeChar, int32_t aBaseUnicodeChar, + int32_t aDomPrintableKeyValue, int32_t aRepeatCount, int32_t aFlags, + bool aIsSynthesizedImeKey, jni::Object::Param originalEvent) +{ + RefPtr<nsWindow> kungFuDeathGrip(&window); + if (!aIsSynthesizedImeKey) { + window.UserActivity(); + window.RemoveIMEComposition(); + } + + EventMessage msg; + if (aAction == sdk::KeyEvent::ACTION_DOWN) { + msg = eKeyDown; + } else if (aAction == sdk::KeyEvent::ACTION_UP) { + msg = eKeyUp; + } else if (aAction == sdk::KeyEvent::ACTION_MULTIPLE) { + // Keys with multiple action are handled in Java, + // and we should never see one here + MOZ_CRASH("Cannot handle key with multiple action"); + } else { + ALOG("Unknown key action event!"); + return; + } + + nsEventStatus status = nsEventStatus_eIgnore; + WidgetKeyboardEvent event(true, msg, &window); + window.InitEvent(event, nullptr); + InitKeyEvent(event, aAction, aKeyCode, aScanCode, aMetaState, aTime, + aUnicodeChar, aBaseUnicodeChar, aDomPrintableKeyValue, + aRepeatCount, aFlags); + + if (aIsSynthesizedImeKey) { + // Keys synthesized by Java IME code are saved in the mIMEKeyEvents + // array until the next IME_REPLACE_TEXT event, at which point + // these keys are dispatched in sequence. + mIMEKeyEvents.AppendElement( + mozilla::UniquePtr<WidgetEvent>(event.Duplicate())); + + } else { + window.DispatchEvent(&event, status); + + if (window.Destroyed() || status == nsEventStatus_eConsumeNoDefault) { + // Skip default processing. + return; + } + + mEditable->OnDefaultKeyEvent(originalEvent); + } + + if (msg != eKeyDown || IsModifierKey(aKeyCode)) { + // Skip sending key press event. + return; + } + + WidgetKeyboardEvent pressEvent(true, eKeyPress, &window); + window.InitEvent(pressEvent, nullptr); + InitKeyEvent(pressEvent, aAction, aKeyCode, aScanCode, aMetaState, aTime, + aUnicodeChar, aBaseUnicodeChar, aDomPrintableKeyValue, + aRepeatCount, aFlags); + + if (aIsSynthesizedImeKey) { + mIMEKeyEvents.AppendElement( + mozilla::UniquePtr<WidgetEvent>(pressEvent.Duplicate())); + } else { + window.DispatchEvent(&pressEvent, status); + } +} + +#ifdef DEBUG_ANDROID_IME +#define ALOGIME(args...) ALOG(args) +#else +#define ALOGIME(args...) ((void)0) +#endif + +static nscolor +ConvertAndroidColor(uint32_t aArgb) +{ + return NS_RGBA((aArgb & 0x00ff0000) >> 16, + (aArgb & 0x0000ff00) >> 8, + (aArgb & 0x000000ff), + (aArgb & 0xff000000) >> 24); +} + +/* + * Get the current composition object, if any. + */ +RefPtr<mozilla::TextComposition> +nsWindow::GetIMEComposition() +{ + MOZ_ASSERT(this == FindTopLevel()); + return mozilla::IMEStateManager::GetTextCompositionFor(this); +} + +/* + Remove the composition but leave the text content as-is +*/ +void +nsWindow::RemoveIMEComposition(RemoveIMECompositionFlag aFlag) +{ + // Remove composition on Gecko side + const RefPtr<mozilla::TextComposition> composition(GetIMEComposition()); + if (!composition) { + return; + } + + RefPtr<nsWindow> kungFuDeathGrip(this); + + WidgetCompositionEvent compositionCommitEvent( + true, eCompositionCommit, this); + if (aFlag == COMMIT_IME_COMPOSITION) { + compositionCommitEvent.mMessage = eCompositionCommitAsIs; + } + InitEvent(compositionCommitEvent, nullptr); + DispatchEvent(&compositionCommitEvent); +} + +/* + * Send dummy key events for pages that are unaware of input events, + * to provide web compatibility for pages that depend on key events. + * Our dummy key events have 0 as the keycode. + */ +void +nsWindow::GeckoViewSupport::SendIMEDummyKeyEvents() +{ + WidgetKeyboardEvent downEvent(true, eKeyDown, &window); + window.InitEvent(downEvent, nullptr); + MOZ_ASSERT(downEvent.mKeyCode == 0); + window.DispatchEvent(&downEvent); + + WidgetKeyboardEvent upEvent(true, eKeyUp, &window); + window.InitEvent(upEvent, nullptr); + MOZ_ASSERT(upEvent.mKeyCode == 0); + window.DispatchEvent(&upEvent); +} + +void +nsWindow::GeckoViewSupport::AddIMETextChange(const IMETextChange& aChange) +{ + mIMETextChanges.AppendElement(aChange); + + // We may not be in the middle of flushing, + // in which case this flag is meaningless. + mIMETextChangedDuringFlush = true; + + // Now that we added a new range we need to go back and + // update all the ranges before that. + // Ranges that have offsets which follow this new range + // need to be updated to reflect new offsets + const int32_t delta = aChange.mNewEnd - aChange.mOldEnd; + for (int32_t i = mIMETextChanges.Length() - 2; i >= 0; i--) { + IMETextChange& previousChange = mIMETextChanges[i]; + if (previousChange.mStart > aChange.mOldEnd) { + previousChange.mStart += delta; + previousChange.mOldEnd += delta; + previousChange.mNewEnd += delta; + } + } + + // Now go through all ranges to merge any ranges that are connected + // srcIndex is the index of the range to merge from + // dstIndex is the index of the range to potentially merge into + int32_t srcIndex = mIMETextChanges.Length() - 1; + int32_t dstIndex = srcIndex; + + while (--dstIndex >= 0) { + IMETextChange& src = mIMETextChanges[srcIndex]; + IMETextChange& dst = mIMETextChanges[dstIndex]; + // When merging a more recent change into an older + // change, we need to compare recent change's (start, oldEnd) + // range to the older change's (start, newEnd) + if (src.mOldEnd < dst.mStart || dst.mNewEnd < src.mStart) { + // No overlap between ranges + continue; + } + // When merging two ranges, there are generally four posibilities: + // [----(----]----), (----[----]----), + // [----(----)----], (----[----)----] + // where [----] is the first range and (----) is the second range + // As seen above, the start of the merged range is always the lesser + // of the two start offsets. OldEnd and NewEnd then need to be + // adjusted separately depending on the case. In any case, the change + // in text length of the merged range should be the sum of text length + // changes of the two original ranges, i.e., + // newNewEnd - newOldEnd == newEnd1 - oldEnd1 + newEnd2 - oldEnd2 + dst.mStart = std::min(dst.mStart, src.mStart); + if (src.mOldEnd < dst.mNewEnd) { + // New range overlaps or is within previous range; merge + dst.mNewEnd += src.mNewEnd - src.mOldEnd; + } else { // src.mOldEnd >= dst.mNewEnd + // New range overlaps previous range; merge + dst.mOldEnd += src.mOldEnd - dst.mNewEnd; + dst.mNewEnd = src.mNewEnd; + } + // src merged to dst; delete src. + mIMETextChanges.RemoveElementAt(srcIndex); + // Any ranges that we skip over between src and dst are not mergeable + // so we can safely continue the merge starting at dst + srcIndex = dstIndex; + } +} + +void +nsWindow::GeckoViewSupport::PostFlushIMEChanges() +{ + if (!mIMETextChanges.IsEmpty() || mIMESelectionChanged) { + // Already posted + return; + } + + // Keep a strong reference to the window to keep 'this' alive. + RefPtr<nsWindow> window(&this->window); + + nsAppShell::PostEvent([this, window] { + if (!window->Destroyed()) { + FlushIMEChanges(); + } + }); +} + +void +nsWindow::GeckoViewSupport::FlushIMEChanges(FlushChangesFlag aFlags) +{ + // Only send change notifications if we are *not* masking events, + // i.e. if we have a focused editor, + NS_ENSURE_TRUE_VOID(!mIMEMaskEventsCount); + + nsCOMPtr<nsISelection> imeSelection; + nsCOMPtr<nsIContent> imeRoot; + + // If we are receiving notifications, we must have selection/root content. + MOZ_ALWAYS_SUCCEEDS(IMEStateManager::GetFocusSelectionAndRoot( + getter_AddRefs(imeSelection), getter_AddRefs(imeRoot))); + + // Make sure we still have a valid selection/root. We can potentially get + // a stale selection/root if the editor becomes hidden, for example. + NS_ENSURE_TRUE_VOID(imeRoot->IsInComposedDoc()); + + RefPtr<nsWindow> kungFuDeathGrip(&window); + window.UserActivity(); + + struct TextRecord { + nsString text; + int32_t start; + int32_t oldEnd; + int32_t newEnd; + }; + AutoTArray<TextRecord, 4> textTransaction; + if (mIMETextChanges.Length() > textTransaction.Capacity()) { + textTransaction.SetCapacity(mIMETextChanges.Length()); + } + + mIMETextChangedDuringFlush = false; + + auto shouldAbort = [=] () -> bool { + if (!mIMETextChangedDuringFlush) { + return false; + } + // A query event could have triggered more text changes to come in, as + // indicated by our flag. If that happens, try flushing IME changes + // again. + if (aFlags == FLUSH_FLAG_NONE) { + FlushIMEChanges(FLUSH_FLAG_RETRY); + } else { + // Don't retry if already retrying, to avoid infinite loops. + __android_log_print(ANDROID_LOG_WARN, "GeckoViewSupport", + "Already retrying IME flush"); + } + return true; + }; + + for (const IMETextChange &change : mIMETextChanges) { + if (change.mStart == change.mOldEnd && + change.mStart == change.mNewEnd) { + continue; + } + + WidgetQueryContentEvent event(true, eQueryTextContent, &window); + + if (change.mNewEnd != change.mStart) { + window.InitEvent(event, nullptr); + event.InitForQueryTextContent(change.mStart, + change.mNewEnd - change.mStart); + window.DispatchEvent(&event); + NS_ENSURE_TRUE_VOID(event.mSucceeded); + NS_ENSURE_TRUE_VOID(event.mReply.mContentsRoot == imeRoot.get()); + } + + if (shouldAbort()) { + return; + } + + textTransaction.AppendElement( + TextRecord{event.mReply.mString, change.mStart, + change.mOldEnd, change.mNewEnd}); + } + + int32_t selStart = -1; + int32_t selEnd = -1; + + if (mIMESelectionChanged) { + WidgetQueryContentEvent event(true, eQuerySelectedText, &window); + window.InitEvent(event, nullptr); + window.DispatchEvent(&event); + + NS_ENSURE_TRUE_VOID(event.mSucceeded); + NS_ENSURE_TRUE_VOID(event.mReply.mContentsRoot == imeRoot.get()); + + if (shouldAbort()) { + return; + } + + selStart = int32_t(event.GetSelectionStart()); + selEnd = int32_t(event.GetSelectionEnd()); + } + + JNIEnv* const env = jni::GetGeckoThreadEnv(); + auto flushOnException = [=] () -> bool { + if (!env->ExceptionCheck()) { + return false; + } + if (aFlags != FLUSH_FLAG_RECOVER) { + // First time seeing an exception; try flushing text. + env->ExceptionClear(); + __android_log_print(ANDROID_LOG_WARN, "GeckoViewSupport", + "Recovering from IME exception"); + FlushIMEText(FLUSH_FLAG_RECOVER); + } else { + // Give up because we've already tried. + MOZ_CATCH_JNI_EXCEPTION(env); + } + return true; + }; + + // Commit the text change and selection change transaction. + mIMETextChanges.Clear(); + + for (const TextRecord& record : textTransaction) { + mEditable->OnTextChange(record.text, record.start, + record.oldEnd, record.newEnd); + if (flushOnException()) { + return; + } + } + + if (mIMESelectionChanged) { + mIMESelectionChanged = false; + mEditable->OnSelectionChange(selStart, selEnd); + flushOnException(); + } +} + +void +nsWindow::GeckoViewSupport::FlushIMEText(FlushChangesFlag aFlags) +{ + // Notify Java of the newly focused content + mIMETextChanges.Clear(); + mIMESelectionChanged = true; + + // Use 'INT32_MAX / 2' here because subsequent text changes might combine + // with this text change, and overflow might occur if we just use + // INT32_MAX. + IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE); + notification.mTextChangeData.mStartOffset = 0; + notification.mTextChangeData.mRemovedEndOffset = INT32_MAX / 2; + notification.mTextChangeData.mAddedEndOffset = INT32_MAX / 2; + NotifyIME(notification); + + FlushIMEChanges(aFlags); +} + +static jni::ObjectArray::LocalRef +ConvertRectArrayToJavaRectFArray(JNIEnv* aJNIEnv, const nsTArray<LayoutDeviceIntRect>& aRects, const LayoutDeviceIntPoint& aOffset, const CSSToLayoutDeviceScale aScale) +{ + size_t length = aRects.Length(); + jobjectArray rects = aJNIEnv->NewObjectArray(length, sdk::RectF::Context().ClassRef(), nullptr); + auto rectsRef = jni::ObjectArray::LocalRef::Adopt(aJNIEnv, rects); + for (size_t i = 0; i < length; i++) { + sdk::RectF::LocalRef rect(aJNIEnv); + LayoutDeviceIntRect tmp = aRects[i] + aOffset; + sdk::RectF::New(tmp.x / aScale.scale, tmp.y / aScale.scale, + (tmp.x + tmp.width) / aScale.scale, + (tmp.y + tmp.height) / aScale.scale, + &rect); + rectsRef->SetElement(i, rect); + } + return rectsRef; +} + +void +nsWindow::GeckoViewSupport::UpdateCompositionRects() +{ + const auto composition(window.GetIMEComposition()); + if (NS_WARN_IF(!composition)) { + return; + } + + uint32_t offset = composition->NativeOffsetOfStartComposition(); + WidgetQueryContentEvent textRects(true, eQueryTextRectArray, &window); + textRects.InitForQueryTextRectArray(offset, composition->String().Length()); + window.DispatchEvent(&textRects); + + auto rects = + ConvertRectArrayToJavaRectFArray(jni::GetGeckoThreadEnv(), + textRects.mReply.mRectArray, + window.WidgetToScreenOffset(), + window.GetDefaultScale()); + + mEditable->UpdateCompositionRects(rects); +} + +void +nsWindow::GeckoViewSupport::AsyncNotifyIME(int32_t aNotification) +{ + // Keep a strong reference to the window to keep 'this' alive. + RefPtr<nsWindow> window(&this->window); + + nsAppShell::PostEvent([this, window, aNotification] { + if (mIMEMaskEventsCount) { + return; + } + + mEditable->NotifyIME(aNotification); + }); +} + +bool +nsWindow::GeckoViewSupport::NotifyIME(const IMENotification& aIMENotification) +{ + MOZ_ASSERT(mEditable); + + switch (aIMENotification.mMessage) { + case REQUEST_TO_COMMIT_COMPOSITION: { + ALOGIME("IME: REQUEST_TO_COMMIT_COMPOSITION"); + + window.RemoveIMEComposition(); + + AsyncNotifyIME(GeckoEditableListener:: + NOTIFY_IME_TO_COMMIT_COMPOSITION); + return true; + } + + case REQUEST_TO_CANCEL_COMPOSITION: { + ALOGIME("IME: REQUEST_TO_CANCEL_COMPOSITION"); + + window.RemoveIMEComposition(CANCEL_IME_COMPOSITION); + + AsyncNotifyIME(GeckoEditableListener:: + NOTIFY_IME_TO_CANCEL_COMPOSITION); + return true; + } + + case NOTIFY_IME_OF_FOCUS: { + ALOGIME("IME: NOTIFY_IME_OF_FOCUS"); + // Keep a strong reference to the window to keep 'this' alive. + RefPtr<nsWindow> window(&this->window); + + // Post an event because we have to flush the text before sending a + // focus event, and we may not be able to flush text during the + // NotifyIME call. + nsAppShell::PostEvent([this, window] { + --mIMEMaskEventsCount; + if (mIMEMaskEventsCount || window->Destroyed()) { + return; + } + + FlushIMEText(); + + // IME will call requestCursorUpdates after getting context. + // So reset cursor update mode before getting context. + mIMEMonitorCursor = false; + + MOZ_ASSERT(mEditable); + mEditable->NotifyIME(GeckoEditableListener::NOTIFY_IME_OF_FOCUS); + }); + return true; + } + + case NOTIFY_IME_OF_BLUR: { + ALOGIME("IME: NOTIFY_IME_OF_BLUR"); + + if (!mIMEMaskEventsCount) { + mEditable->NotifyIME(GeckoEditableListener::NOTIFY_IME_OF_BLUR); + } + + // Mask events because we lost focus. Unmask on the next focus. + mIMEMaskEventsCount++; + return true; + } + + case NOTIFY_IME_OF_SELECTION_CHANGE: { + ALOGIME("IME: NOTIFY_IME_OF_SELECTION_CHANGE"); + + PostFlushIMEChanges(); + mIMESelectionChanged = true; + return true; + } + + case NOTIFY_IME_OF_TEXT_CHANGE: { + ALOGIME("IME: NotifyIMEOfTextChange: s=%d, oe=%d, ne=%d", + aIMENotification.mTextChangeData.mStartOffset, + aIMENotification.mTextChangeData.mRemovedEndOffset, + aIMENotification.mTextChangeData.mAddedEndOffset); + + /* Make sure Java's selection is up-to-date */ + PostFlushIMEChanges(); + mIMESelectionChanged = true; + AddIMETextChange(IMETextChange(aIMENotification)); + return true; + } + + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: { + ALOGIME("IME: NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED"); + + // Hardware keyboard support requires each string rect. + if (AndroidBridge::Bridge() && AndroidBridge::Bridge()->GetAPIVersion() >= 21 && mIMEMonitorCursor) { + UpdateCompositionRects(); + } + return true; + } + + default: + return false; + } +} + +void +nsWindow::GeckoViewSupport::SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) +{ + MOZ_ASSERT(mEditable); + + ALOGIME("IME: SetInputContext: s=0x%X, 0x%X, action=0x%X, 0x%X", + aContext.mIMEState.mEnabled, aContext.mIMEState.mOpen, + aAction.mCause, aAction.mFocusChange); + + // Ensure that opening the virtual keyboard is allowed for this specific + // InputContext depending on the content.ime.strict.policy pref + if (aContext.mIMEState.mEnabled != IMEState::DISABLED && + aContext.mIMEState.mEnabled != IMEState::PLUGIN && + Preferences::GetBool("content.ime.strict_policy", false) && + !aAction.ContentGotFocusByTrustedCause() && + !aAction.UserMightRequestOpenVKB()) { + return; + } + + IMEState::Enabled enabled = aContext.mIMEState.mEnabled; + + // Only show the virtual keyboard for plugins if mOpen is set appropriately. + // This avoids showing it whenever a plugin is focused. Bug 747492 + if (aContext.mIMEState.mEnabled == IMEState::PLUGIN && + aContext.mIMEState.mOpen != IMEState::OPEN) { + enabled = IMEState::DISABLED; + } + + mInputContext = aContext; + mInputContext.mIMEState.mEnabled = enabled; + + if (enabled == IMEState::ENABLED && aAction.UserMightRequestOpenVKB()) { + // Don't reset keyboard when we should simply open the vkb + mEditable->NotifyIME(GeckoEditableListener::NOTIFY_IME_OPEN_VKB); + return; + } + + if (mIMEUpdatingContext) { + return; + } + + // Keep a strong reference to the window to keep 'this' alive. + RefPtr<nsWindow> window(&this->window); + mIMEUpdatingContext = true; + + nsAppShell::PostEvent([this, window] { + mIMEUpdatingContext = false; + if (window->Destroyed()) { + return; + } + MOZ_ASSERT(mEditable); + mEditable->NotifyIMEContext(mInputContext.mIMEState.mEnabled, + mInputContext.mHTMLInputType, + mInputContext.mHTMLInputInputmode, + mInputContext.mActionHint); + }); +} + +InputContext +nsWindow::GeckoViewSupport::GetInputContext() +{ + InputContext context = mInputContext; + context.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED; + return context; +} + +void +nsWindow::GeckoViewSupport::OnImeSynchronize() +{ + if (!mIMEMaskEventsCount) { + FlushIMEChanges(); + } + mEditable->NotifyIME(GeckoEditableListener::NOTIFY_IME_REPLY_EVENT); +} + +void +nsWindow::GeckoViewSupport::OnImeReplaceText(int32_t aStart, int32_t aEnd, + jni::String::Param aText) +{ + AutoIMESynchronize as(this); + + if (mIMEMaskEventsCount > 0) { + // Not focused; still reply to events, but don't do anything else. + return; + } + + /* + Replace text in Gecko thread from aStart to aEnd with the string text. + */ + RefPtr<nsWindow> kungFuDeathGrip(&window); + nsString string(aText->ToString()); + + const auto composition(window.GetIMEComposition()); + MOZ_ASSERT(!composition || !composition->IsEditorHandlingEvent()); + + const bool composing = !mIMERanges->IsEmpty(); + + if (!mIMEKeyEvents.IsEmpty() || !composition || + uint32_t(aStart) != composition->NativeOffsetOfStartComposition() || + uint32_t(aEnd) != composition->NativeOffsetOfStartComposition() + + composition->String().Length()) + { + // Only start a new composition if we have key events, + // if we don't have an existing composition, or + // the replaced text does not match our composition. + window.RemoveIMEComposition(); + + { + // Use text selection to set target postion(s) for + // insert, or replace, of text. + WidgetSelectionEvent event(true, eSetSelection, &window); + window.InitEvent(event, nullptr); + event.mOffset = uint32_t(aStart); + event.mLength = uint32_t(aEnd - aStart); + event.mExpandToClusterBoundary = false; + event.mReason = nsISelectionListener::IME_REASON; + window.DispatchEvent(&event); + } + + if (!mIMEKeyEvents.IsEmpty()) { + nsEventStatus status; + for (uint32_t i = 0; i < mIMEKeyEvents.Length(); i++) { + const auto event = static_cast<WidgetGUIEvent*>( + mIMEKeyEvents[i].get()); + if (event->mMessage == eKeyPress && + status == nsEventStatus_eConsumeNoDefault) { + MOZ_ASSERT(i > 0 && + mIMEKeyEvents[i - 1]->mMessage == eKeyDown); + // The previous key down event resulted in eConsumeNoDefault + // so we should not dispatch the current key press event. + continue; + } + // widget for duplicated events is initially nullptr. + event->mWidget = &window; + window.DispatchEvent(event, status); + } + mIMEKeyEvents.Clear(); + return; + } + + if (aStart != aEnd) { + // Perform a deletion first. + WidgetContentCommandEvent event( + true, eContentCommandDelete, &window); + window.InitEvent(event, nullptr); + window.DispatchEvent(&event); + } + + // Start a composition if we're not just performing a deletion. + if (composing || !string.IsEmpty()) { + WidgetCompositionEvent event(true, eCompositionStart, &window); + window.InitEvent(event, nullptr); + window.DispatchEvent(&event); + } + + } else if (composition->String().Equals(string)) { + /* If the new text is the same as the existing composition text, + * the NS_COMPOSITION_CHANGE event does not generate a text + * change notification. However, the Java side still expects + * one, so we manually generate a notification. */ + IMETextChange dummyChange; + dummyChange.mStart = aStart; + dummyChange.mOldEnd = dummyChange.mNewEnd = aEnd; + AddIMETextChange(dummyChange); + } + + // Check composition again because previous events may have destroyed our + // composition; in which case we should just skip the next event. + if (window.GetIMEComposition()) { + WidgetCompositionEvent event(true, eCompositionChange, &window); + window.InitEvent(event, nullptr); + event.mData = string; + + if (composing) { + event.mRanges = new TextRangeArray(); + mIMERanges.swap(event.mRanges); + } else { + event.mMessage = eCompositionCommit; + } + + window.DispatchEvent(&event); + + } else if (composing) { + // Ensure IME ranges are empty. + mIMERanges->Clear(); + } + + if (mInputContext.mMayBeIMEUnaware) { + SendIMEDummyKeyEvents(); + } +} + +void +nsWindow::GeckoViewSupport::OnImeAddCompositionRange( + int32_t aStart, int32_t aEnd, int32_t aRangeType, int32_t aRangeStyle, + int32_t aRangeLineStyle, bool aRangeBoldLine, int32_t aRangeForeColor, + int32_t aRangeBackColor, int32_t aRangeLineColor) +{ + if (mIMEMaskEventsCount > 0) { + // Not focused. + return; + } + + TextRange range; + range.mStartOffset = aStart; + range.mEndOffset = aEnd; + range.mRangeType = ToTextRangeType(aRangeType); + range.mRangeStyle.mDefinedStyles = aRangeStyle; + range.mRangeStyle.mLineStyle = aRangeLineStyle; + range.mRangeStyle.mIsBoldLine = aRangeBoldLine; + range.mRangeStyle.mForegroundColor = + ConvertAndroidColor(uint32_t(aRangeForeColor)); + range.mRangeStyle.mBackgroundColor = + ConvertAndroidColor(uint32_t(aRangeBackColor)); + range.mRangeStyle.mUnderlineColor = + ConvertAndroidColor(uint32_t(aRangeLineColor)); + mIMERanges->AppendElement(range); +} + +void +nsWindow::GeckoViewSupport::OnImeUpdateComposition(int32_t aStart, int32_t aEnd) +{ + if (mIMEMaskEventsCount > 0) { + // Not focused. + return; + } + + RefPtr<nsWindow> kungFuDeathGrip(&window); + + // A composition with no ranges means we want to set the selection. + if (mIMERanges->IsEmpty()) { + MOZ_ASSERT(aStart >= 0 && aEnd >= 0); + window.RemoveIMEComposition(); + + WidgetSelectionEvent selEvent(true, eSetSelection, &window); + window.InitEvent(selEvent, nullptr); + + selEvent.mOffset = std::min(aStart, aEnd); + selEvent.mLength = std::max(aStart, aEnd) - selEvent.mOffset; + selEvent.mReversed = aStart > aEnd; + selEvent.mExpandToClusterBoundary = false; + + window.DispatchEvent(&selEvent); + return; + } + + /* + Update the composition from aStart to aEnd using + information from added ranges. This is only used for + visual indication and does not affect the text content. + Only the offsets are specified and not the text content + to eliminate the possibility of this event altering the + text content unintentionally. + */ + const auto composition(window.GetIMEComposition()); + MOZ_ASSERT(!composition || !composition->IsEditorHandlingEvent()); + + WidgetCompositionEvent event(true, eCompositionChange, &window); + window.InitEvent(event, nullptr); + + event.mRanges = new TextRangeArray(); + mIMERanges.swap(event.mRanges); + + if (!composition || + uint32_t(aStart) != composition->NativeOffsetOfStartComposition() || + uint32_t(aEnd) != composition->NativeOffsetOfStartComposition() + + composition->String().Length()) + { + // Only start new composition if we don't have an existing one, + // or if the existing composition doesn't match the new one. + window.RemoveIMEComposition(); + + { + WidgetSelectionEvent event(true, eSetSelection, &window); + window.InitEvent(event, nullptr); + event.mOffset = uint32_t(aStart); + event.mLength = uint32_t(aEnd - aStart); + event.mExpandToClusterBoundary = false; + event.mReason = nsISelectionListener::IME_REASON; + window.DispatchEvent(&event); + } + + { + WidgetQueryContentEvent queryEvent(true, eQuerySelectedText, + &window); + window.InitEvent(queryEvent, nullptr); + window.DispatchEvent(&queryEvent); + MOZ_ASSERT(queryEvent.mSucceeded); + event.mData = queryEvent.mReply.mString; + } + + { + WidgetCompositionEvent event(true, eCompositionStart, &window); + window.InitEvent(event, nullptr); + window.DispatchEvent(&event); + } + + } else { + // If the new composition matches the existing composition, + // reuse the old composition. + event.mData = composition->String(); + } + +#ifdef DEBUG_ANDROID_IME + const NS_ConvertUTF16toUTF8 data(event.mData); + const char* text = data.get(); + ALOGIME("IME: IME_SET_TEXT: text=\"%s\", length=%u, range=%u", + text, event.mData.Length(), event.mRanges->Length()); +#endif // DEBUG_ANDROID_IME + + // Previous events may have destroyed our composition; bail in that case. + if (window.GetIMEComposition()) { + window.DispatchEvent(&event); + } +} + +void +nsWindow::GeckoViewSupport::OnImeRequestCursorUpdates(int aRequestMode) +{ + if (aRequestMode == IME_MONITOR_CURSOR_ONE_SHOT) { + UpdateCompositionRects(); + return; + } + + mIMEMonitorCursor = (aRequestMode == IME_MONITOR_CURSOR_START_MONITOR); +} + +void +nsWindow::UserActivity() +{ + if (!mIdleService) { + mIdleService = do_GetService("@mozilla.org/widget/idleservice;1"); + } + + if (mIdleService) { + mIdleService->ResetIdleTimeOut(0); + } +} + +nsresult +nsWindow::NotifyIMEInternal(const IMENotification& aIMENotification) +{ + MOZ_ASSERT(this == FindTopLevel()); + + if (!mGeckoViewSupport) { + // Non-GeckoView windows don't support IME operations. + return NS_ERROR_NOT_AVAILABLE; + } + + if (mGeckoViewSupport->NotifyIME(aIMENotification)) { + return NS_OK; + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP_(void) +nsWindow::SetInputContext(const InputContext& aContext, + const InputContextAction& aAction) +{ + nsWindow* top = FindTopLevel(); + MOZ_ASSERT(top); + + if (!top->mGeckoViewSupport) { + // Non-GeckoView windows don't support IME operations. + return; + } + + // We are using an IME event later to notify Java, and the IME event + // will be processed by the top window. Therefore, to ensure the + // IME event uses the correct mInputContext, we need to let the top + // window process SetInputContext + top->mGeckoViewSupport->SetInputContext(aContext, aAction); +} + +NS_IMETHODIMP_(InputContext) +nsWindow::GetInputContext() +{ + nsWindow* top = FindTopLevel(); + MOZ_ASSERT(top); + + if (!top->mGeckoViewSupport) { + // Non-GeckoView windows don't support IME operations. + return InputContext(); + } + + // We let the top window process SetInputContext, + // so we should let it process GetInputContext as well. + return top->mGeckoViewSupport->GetInputContext(); +} + +nsIMEUpdatePreference +nsWindow::GetIMEUpdatePreference() +{ + // While a plugin has focus, nsWindow for Android doesn't need any + // notifications. + if (GetInputContext().mIMEState.mEnabled == IMEState::PLUGIN) { + return nsIMEUpdatePreference(); + } + return nsIMEUpdatePreference(nsIMEUpdatePreference::NOTIFY_TEXT_CHANGE); +} + +nsresult +nsWindow::SynthesizeNativeTouchPoint(uint32_t aPointerId, + TouchPointerState aPointerState, + LayoutDeviceIntPoint aPoint, + double aPointerPressure, + uint32_t aPointerOrientation, + nsIObserver* aObserver) +{ + mozilla::widget::AutoObserverNotifier notifier(aObserver, "touchpoint"); + + int eventType; + switch (aPointerState) { + case TOUCH_CONTACT: + // This could be a ACTION_DOWN or ACTION_MOVE depending on the + // existing state; it is mapped to the right thing in Java. + eventType = sdk::MotionEvent::ACTION_POINTER_DOWN; + break; + case TOUCH_REMOVE: + // This could be turned into a ACTION_UP in Java + eventType = sdk::MotionEvent::ACTION_POINTER_UP; + break; + case TOUCH_CANCEL: + eventType = sdk::MotionEvent::ACTION_CANCEL; + break; + case TOUCH_HOVER: // not supported for now + default: + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(mLayerViewSupport); + GeckoLayerClient::LocalRef client = mLayerViewSupport->GetLayerClient(); + client->SynthesizeNativeTouchPoint(aPointerId, eventType, + aPoint.x, aPoint.y, aPointerPressure, aPointerOrientation); + + return NS_OK; +} + +nsresult +nsWindow::SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint, + uint32_t aNativeMessage, + uint32_t aModifierFlags, + nsIObserver* aObserver) +{ + mozilla::widget::AutoObserverNotifier notifier(aObserver, "mouseevent"); + + MOZ_ASSERT(mLayerViewSupport); + GeckoLayerClient::LocalRef client = mLayerViewSupport->GetLayerClient(); + client->SynthesizeNativeMouseEvent(aNativeMessage, aPoint.x, aPoint.y); + + return NS_OK; +} + +nsresult +nsWindow::SynthesizeNativeMouseMove(LayoutDeviceIntPoint aPoint, + nsIObserver* aObserver) +{ + mozilla::widget::AutoObserverNotifier notifier(aObserver, "mouseevent"); + + MOZ_ASSERT(mLayerViewSupport); + GeckoLayerClient::LocalRef client = mLayerViewSupport->GetLayerClient(); + client->SynthesizeNativeMouseEvent(sdk::MotionEvent::ACTION_HOVER_MOVE, aPoint.x, aPoint.y); + + return NS_OK; +} + +bool +nsWindow::PreRender(WidgetRenderingContext* aContext) +{ + if (Destroyed()) { + return true; + } + + layers::Compositor* compositor = aContext->mCompositor; + + GeckoLayerClient::LocalRef client; + + if (NativePtr<LayerViewSupport>::Locked lvs{mLayerViewSupport}) { + client = lvs->GetLayerClient(); + } + + if (compositor && client) { + // Android Color is ARGB which is apparently unusual. + compositor->SetDefaultClearColor(gfx::Color::UnusualFromARGB((uint32_t)client->ClearColor())); + } + + return true; +} +void +nsWindow::DrawWindowUnderlay(WidgetRenderingContext* aContext, + LayoutDeviceIntRect aRect) +{ + if (Destroyed()) { + return; + } + + GeckoLayerClient::LocalRef client; + + if (NativePtr<LayerViewSupport>::Locked lvs{mLayerViewSupport}) { + client = lvs->GetLayerClient(); + } + + if (!client) { + return; + } + + LayerRenderer::Frame::LocalRef frame = client->CreateFrame(); + mLayerRendererFrame = frame; + if (NS_WARN_IF(!mLayerRendererFrame)) { + return; + } + + if (!WidgetPaintsBackground()) { + return; + } + + frame->BeginDrawing(); +} + +void +nsWindow::DrawWindowOverlay(WidgetRenderingContext* aContext, + LayoutDeviceIntRect aRect) +{ + PROFILER_LABEL("nsWindow", "DrawWindowOverlay", + js::ProfileEntry::Category::GRAPHICS); + + if (Destroyed() || NS_WARN_IF(!mLayerRendererFrame)) { + return; + } + + mLayerRendererFrame->EndDrawing(); + mLayerRendererFrame = nullptr; +} + +bool +nsWindow::WidgetPaintsBackground() +{ + static bool sWidgetPaintsBackground = true; + static bool sWidgetPaintsBackgroundPrefCached = false; + + if (!sWidgetPaintsBackgroundPrefCached) { + sWidgetPaintsBackgroundPrefCached = true; + mozilla::Preferences::AddBoolVarCache(&sWidgetPaintsBackground, + "android.widget_paints_background", + true); + } + + return sWidgetPaintsBackground; +} + +bool +nsWindow::NeedsPaint() +{ + if (!mLayerViewSupport || mLayerViewSupport->CompositorPaused() || + // FindTopLevel() != nsWindow::TopWindow() || + !GetLayerManager(nullptr)) { + return false; + } + return nsIWidget::NeedsPaint(); +} + +void +nsWindow::ConfigureAPZControllerThread() +{ + APZThreadUtils::SetControllerThread(nullptr); +} + +already_AddRefed<GeckoContentController> +nsWindow::CreateRootContentController() +{ + RefPtr<GeckoContentController> controller = new AndroidContentController(this, mAPZEventState, mAPZC); + return controller.forget(); +} + +uint32_t +nsWindow::GetMaxTouchPoints() const +{ + return GeckoAppShell::GetMaxTouchPoints(); +} + +void +nsWindow::UpdateZoomConstraints(const uint32_t& aPresShellId, + const FrameMetrics::ViewID& aViewId, + const mozilla::Maybe<ZoomConstraints>& aConstraints) +{ + nsBaseWidget::UpdateZoomConstraints(aPresShellId, aViewId, aConstraints); +} + +CompositorBridgeParent* +nsWindow::GetCompositorBridgeParent() const +{ + return mCompositorSession ? mCompositorSession->GetInProcessBridge() : nullptr; +} + +already_AddRefed<nsIScreen> +nsWindow::GetWidgetScreen() +{ + nsCOMPtr<nsIScreenManager> screenMgr = + do_GetService("@mozilla.org/gfx/screenmanager;1"); + MOZ_ASSERT(screenMgr, "Failed to get nsIScreenManager"); + + nsCOMPtr<nsIScreen> screen; + screenMgr->ScreenForId(mScreenId, getter_AddRefs(screen)); + + return screen.forget(); +} + +jni::DependentRef<java::GeckoLayerClient> +nsWindow::GetLayerClient() +{ + if (NativePtr<LayerViewSupport>::Locked lvs{mLayerViewSupport}) { + return lvs->GetLayerClient().Get(); + } + return nullptr; +} |