From 84241e428fb7bb5874d7605c6f1f523147db15fb Mon Sep 17 00:00:00 2001 From: Justin Lebar Date: Thu, 10 Nov 2011 16:26:36 -0500 Subject: [PATCH] Bug 679966, part 2: Add mozVibrator() for "playing" a vibration pattern. r=bz --- dom/base/Navigator.cpp | 185 +++++++++++++++++- dom/base/Navigator.h | 2 - dom/interfaces/base/nsIDOMNavigator.idl | 51 ++++- dom/tests/mochitest/general/Makefile.in | 1 + .../mochitest/general/test_vibrator.html | 99 ++++++++++ hal/android/AndroidHal.cpp | 22 ++- modules/libpref/src/init/all.js | 4 + 7 files changed, 354 insertions(+), 10 deletions(-) create mode 100644 dom/tests/mochitest/general/test_vibrator.html diff --git a/dom/base/Navigator.cpp b/dom/base/Navigator.cpp index 75cc5f8a36b..74582fe426e 100644 --- a/dom/base/Navigator.cpp +++ b/dom/base/Navigator.cpp @@ -71,6 +71,9 @@ #include "BatteryManager.h" #include "SmsManager.h" #include "nsISmsService.h" +#include "mozilla/Hal.h" +#include "nsIWebNavigation.h" +#include "mozilla/ClearOnShutdown.h" // This should not be in the namespace. DOMCI_DATA(Navigator, mozilla::dom::Navigator) @@ -79,7 +82,11 @@ namespace mozilla { namespace dom { static const char sJSStackContractID[] = "@mozilla.org/js/xpc/ContextStack;1"; -bool Navigator::sDoNotTrackEnabled = false; + +static bool sDoNotTrackEnabled = false; +static bool sVibratorEnabled = false; +static PRUint32 sMaxVibrateMS = 0; +static PRUint32 sMaxVibrateListLen = 0; /* static */ void @@ -88,6 +95,12 @@ Navigator::Init() Preferences::AddBoolVarCache(&sDoNotTrackEnabled, "privacy.donottrackheader.enabled", false); + Preferences::AddBoolVarCache(&sVibratorEnabled, + "dom.vibrator.enabled", true); + Preferences::AddUintVarCache(&sMaxVibrateMS, + "dom.vibrator.max_vibrate_ms", 10000); + Preferences::AddUintVarCache(&sMaxVibrateListLen, + "dom.vibrator.max_vibrate_list_len", 128); } Navigator::Navigator(nsPIDOMWindow* aWindow) @@ -527,6 +540,176 @@ Navigator::HasDesktopNotificationSupport() return Preferences::GetBool("notification.feature.enabled", false); } +namespace { + +class VibrateWindowListener : public nsIDOMEventListener +{ +public: + VibrateWindowListener(nsIDOMWindow *aWindow, nsIDOMDocument *aDocument) + { + mWindow = do_GetWeakReference(aWindow); + mDocument = do_GetWeakReference(aDocument); + + nsCOMPtr target = do_QueryInterface(aDocument); + NS_NAMED_LITERAL_STRING(visibilitychange, "mozvisibilitychange"); + target->AddSystemEventListener(visibilitychange, + this, /* listener */ + true, /* use capture */ + false /* wants untrusted */); + } + + virtual ~VibrateWindowListener() + { + } + + void RemoveListener(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + +private: + nsWeakPtr mWindow; + nsWeakPtr mDocument; +}; + +NS_IMPL_ISUPPORTS1(VibrateWindowListener, nsIDOMEventListener) + +nsRefPtr gVibrateWindowListener; + +NS_IMETHODIMP +VibrateWindowListener::HandleEvent(nsIDOMEvent* aEvent) +{ + nsCOMPtr target; + aEvent->GetTarget(getter_AddRefs(target)); + nsCOMPtr doc = do_QueryInterface(target); + + bool hidden = true; + if (doc) { + doc->GetMozHidden(&hidden); + } + + if (hidden) { + // It's important that we call CancelVibrate(), not Vibrate() with an + // empty list, because Vibrate() will fail if we're no longer focused, but + // CancelVibrate() will succeed, so long as nobody else has started a new + // vibration pattern. + nsCOMPtr window = do_QueryReferent(mWindow); + hal::CancelVibrate(window); + RemoveListener(); + gVibrateWindowListener = NULL; + // Careful: The line above might have deleted |this|! + } + + return NS_OK; +} + +void +VibrateWindowListener::RemoveListener() +{ + nsCOMPtr target = do_QueryReferent(mDocument); + if (!target) { + return; + } + NS_NAMED_LITERAL_STRING(visibilitychange, "mozvisibilitychange"); + target->RemoveSystemEventListener(visibilitychange, this, + true /* use capture */); +} + +/** + * Converts a jsval into a vibration duration, checking that the duration is in + * bounds (non-negative and not larger than sMaxVibrateMS). + * + * Returns true on success, false on failure. + */ +bool +GetVibrationDurationFromJsval(const jsval& aJSVal, JSContext* cx, + PRInt32 *aOut) +{ + return JS_ValueToInt32(cx, aJSVal, aOut) && + *aOut >= 0 && static_cast(*aOut) <= sMaxVibrateMS; +} + +} // anonymous namespace + +NS_IMETHODIMP +Navigator::MozVibrate(const jsval& aPattern, JSContext* cx) +{ + nsCOMPtr win = do_QueryReferent(mWindow); + NS_ENSURE_TRUE(win, NS_OK); + + nsIDOMDocument* domDoc = win->GetExtantDocument(); + NS_ENSURE_TRUE(domDoc, NS_ERROR_FAILURE); + + bool hidden = true; + domDoc->GetMozHidden(&hidden); + if (hidden) { + // Hidden documents cannot start or stop a vibration. + return NS_OK; + } + + nsAutoTArray pattern; + + // null or undefined pattern is an error. + if (JSVAL_IS_NULL(aPattern) || JSVAL_IS_VOID(aPattern)) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + + if (JSVAL_IS_PRIMITIVE(aPattern)) { + PRInt32 p; + if (GetVibrationDurationFromJsval(aPattern, cx, &p)) { + pattern.AppendElement(p); + } + else { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + } + else { + JSObject *obj = JSVAL_TO_OBJECT(aPattern); + PRUint32 length; + if (!JS_GetArrayLength(cx, obj, &length) || length > sMaxVibrateListLen) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + pattern.SetLength(length); + + for (PRUint32 i = 0; i < length; ++i) { + jsval v; + PRInt32 pv; + if (JS_GetElement(cx, obj, i, &v) && + GetVibrationDurationFromJsval(v, cx, &pv)) { + pattern[i] = pv; + } + else { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + } + } + + // The spec says we check sVibratorEnabled after we've done the sanity + // checking on the pattern. + if (!sVibratorEnabled) { + return NS_OK; + } + + // Add a listener to cancel the vibration if the document becomes hidden, + // and remove the old mozvisibility listener, if there was one. + + if (!gVibrateWindowListener) { + // If gVibrateWindowListener is null, this is the first time we've vibrated, + // and we need to register a listener to clear gVibrateWindowListener on + // shutdown. + ClearOnShutdown(&gVibrateWindowListener); + } + else { + gVibrateWindowListener->RemoveListener(); + } + gVibrateWindowListener = new VibrateWindowListener(win, domDoc); + + nsCOMPtr domWindow = + do_QueryInterface(static_cast(win)); + hal::Vibrate(pattern, domWindow); + return NS_OK; +} + //***************************************************************************** // Navigator::nsIDOMClientInformation //***************************************************************************** diff --git a/dom/base/Navigator.h b/dom/base/Navigator.h index f4a55c32328..65a181fa402 100644 --- a/dom/base/Navigator.h +++ b/dom/base/Navigator.h @@ -107,8 +107,6 @@ private: bool IsSmsAllowed() const; bool IsSmsSupported() const; - static bool sDoNotTrackEnabled; - nsRefPtr mMimeTypes; nsRefPtr mPlugins; nsRefPtr mGeolocation; diff --git a/dom/interfaces/base/nsIDOMNavigator.idl b/dom/interfaces/base/nsIDOMNavigator.idl index 77e616e48c6..285cc7cd75c 100644 --- a/dom/interfaces/base/nsIDOMNavigator.idl +++ b/dom/interfaces/base/nsIDOMNavigator.idl @@ -39,7 +39,7 @@ #include "domstubs.idl" -[scriptable, uuid(B8EE0374-5F47-4ED0-B9B0-BDE3E6D81FF5)] +[scriptable, uuid(a1ee08c1-0299-4908-a6ba-7cBc8da6531f)] interface nsIDOMNavigator : nsISupports { readonly attribute DOMString appCodeName; @@ -61,5 +61,52 @@ interface nsIDOMNavigator : nsISupports readonly attribute DOMString doNotTrack; boolean javaEnabled(); -}; + /** + * Pulse the device's vibrator, if it has one. If the device does not have a + * vibrator, this function does nothing. If the window is hidden, this + * function does nothing. + * + * mozVibrate takes one argument, which specifies either how long to vibrate + * for or gives a pattern of vibrator-on/vibrator-off timings. + * + * If a vibration pattern is in effect when this function is called, this + * call will overwrite the existing pattern, if this call successfully + * completes. + * + * We handle the argument to mozVibrate as follows. + * + * - If the argument is undefined or null, we throw + * NS_ERROR_DOM_NOT_SUPPORTED_ERR. + * + * - If the argument is 0, the empty list, or a list containing entirely 0s, + * we cancel any outstanding vibration pattern; that is, we stop the device + * from vibrating. + * + * - Otherwise, if the argument X is not a list, we treat it as though it's + * the singleton list [X] and then proceed as below. + * + * - If the argument is a list (or if we wrapped it as a list above), then we + * try to convert each element in the list to an integer, by first + * converting it to a number and then rounding. If there is some element + * that we can't convert to an integer, or if any of the integers are + * negative, we throw NS_ERROR_DOM_NOT_SUPPORTED_ERR. + * + * This list of integers specifies a vibration pattern. Given a list of + * numbers + * + * [a_1, b_1, a_2, b_2, ..., a_n] + * + * the device will vibrate for a_1 milliseconds, then be still for b_1 + * milliseconds, then vibrate for a_2 milliseconds, and so on. + * + * The list may contain an even or an odd number of elements, but if you + * pass an even number of elements (that is, if your list ends with b_n + * instead of a_n), the final element doesn't specify anything meaningful. + * + * We may throw NS_ERROR_DOM_NOT_SUPPORTED_ERR if the vibration pattern is + * too long, or if any of its elements is too large. + */ + [implicit_jscontext] + void mozVibrate(in jsval aPattern); +}; diff --git a/dom/tests/mochitest/general/Makefile.in b/dom/tests/mochitest/general/Makefile.in index e35c278fad6..a25019cc09c 100644 --- a/dom/tests/mochitest/general/Makefile.in +++ b/dom/tests/mochitest/general/Makefile.in @@ -72,6 +72,7 @@ _TEST_FILES = \ test_windowedhistoryframes.html \ test_focusrings.xul \ file_moving_xhr.html \ + test_vibrator.html \ $(NULL) _CHROME_FILES = \ diff --git a/dom/tests/mochitest/general/test_vibrator.html b/dom/tests/mochitest/general/test_vibrator.html new file mode 100644 index 00000000000..5bfc4c646c2 --- /dev/null +++ b/dom/tests/mochitest/general/test_vibrator.html @@ -0,0 +1,99 @@ + + + + Test for Vibrator + + + + + + + + + + + + diff --git a/hal/android/AndroidHal.cpp b/hal/android/AndroidHal.cpp index 5839bf72407..eb29dc90d1f 100644 --- a/hal/android/AndroidHal.cpp +++ b/hal/android/AndroidHal.cpp @@ -50,16 +50,28 @@ Vibrate(const nsTArray &pattern, const WindowIdentifier &) // hal_sandbox::Vibrate, and hal_impl::Vibrate all must have the same // signature. + // Strangely enough, the Android Java API seems to treat vibrate([0]) as a + // nop. But we want to treat vibrate([0]) like CancelVibrate! (Note that we + // also need to treat vibrate([]) as a call to CancelVibrate.) + bool allZero = true; + for (uint32 i = 0; i < pattern.Length(); i++) { + if (pattern[i] != 0) { + allZero = false; + break; + } + } + + if (allZero) { + hal_impl::CancelVibrate(WindowIdentifier()); + return; + } + AndroidBridge* b = AndroidBridge::Bridge(); if (!b) { return; } - if (pattern.Length() == 0) { - b->CancelVibrate(); - } else { - b->Vibrate(pattern); - } + b->Vibrate(pattern); } void diff --git a/modules/libpref/src/init/all.js b/modules/libpref/src/init/all.js index a83cb15d89f..e20dcc93054 100644 --- a/modules/libpref/src/init/all.js +++ b/modules/libpref/src/init/all.js @@ -3394,6 +3394,10 @@ pref("dom.event.handling-user-input-time-limit", 1000); //3D Transforms pref("layout.3d-transforms.enabled", true); +pref("dom.vibrator.enabled", true); +pref("dom.vibrator.max_vibrate_ms", 10000); +pref("dom.vibrator.max_vibrate_list_len", 128); + // Battery API pref("dom.battery.enabled", true);