Commit 1b457491 authored by Justin Lebar's avatar Justin Lebar
Browse files

Bug 679966, part 2: Add mozVibrator() for "playing" a vibration pattern. r=bz

parent 36337622
Loading
Loading
Loading
Loading
+184 −1
Original line number Diff line number Diff line
@@ -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<nsIDOMEventTarget> 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<VibrateWindowListener> gVibrateWindowListener;

NS_IMETHODIMP
VibrateWindowListener::HandleEvent(nsIDOMEvent* aEvent)
{
  nsCOMPtr<nsIDOMEventTarget> target;
  aEvent->GetTarget(getter_AddRefs(target));
  nsCOMPtr<nsIDOMDocument> 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<nsIDOMWindow> 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<nsIDOMEventTarget> 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<PRUint32>(*aOut) <= sMaxVibrateMS;
}

} // anonymous namespace

NS_IMETHODIMP
Navigator::MozVibrate(const jsval& aPattern, JSContext* cx)
{
  nsCOMPtr<nsPIDOMWindow> 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<PRUint32, 8> 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<nsIDOMWindow> domWindow =
    do_QueryInterface(static_cast<nsIDOMWindow*>(win));
  hal::Vibrate(pattern, domWindow);
  return NS_OK;
}

//*****************************************************************************
//    Navigator::nsIDOMClientInformation
//*****************************************************************************
+0 −2
Original line number Diff line number Diff line
@@ -107,8 +107,6 @@ private:
  bool IsSmsAllowed() const;
  bool IsSmsSupported() const;

  static bool sDoNotTrackEnabled;

  nsRefPtr<nsMimeTypeArray> mMimeTypes;
  nsRefPtr<nsPluginArray> mPlugins;
  nsRefPtr<nsGeolocation> mGeolocation;
+49 −2
Original line number Diff line number Diff line
@@ -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);
};
+1 −0
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ _TEST_FILES = \
		test_windowedhistoryframes.html \
		test_focusrings.xul \
		file_moving_xhr.html \
		test_vibrator.html \
		$(NULL)

_CHROME_FILES = \
+99 −0
Original line number Diff line number Diff line
<!DOCTYPE HTML>
<html>
<head>
  <title>Test for Vibrator</title>
  <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>

<!-- Although we can't test that the vibrator works properly, we can test that
     navigator.mozVibrate throws an exception where appropriate. -->

<script class="testbody" type="text/javascript;version=1.7">
function expectFailure(param) {
  try {
    navigator.mozVibrate(param);
  }
  catch(e) {
    ok(true, 'mozVibrate(' + param + ') threw an expected exception.');
    return;
  }
  ok(false, 'mozVibrate(' + param + ') should have thrown an exception.');
}

function expectSuccess(param) {
  try {
    navigator.mozVibrate(param);
  }
  catch(e) {
    ok(false, 'mozVibrate(' + param + ') threw an unexpected exception.');
    return;
  }
  ok(true, 'mozVibrate(' + param + ') did not throw an exception.');
}

function testFailures() {
  expectFailure(null);
  expectFailure(undefined);
  expectFailure(-1);
  expectFailure('a');
  expectFailure([100, -1]);
  expectFailure([100, 'a']);

  var maxVibrateMs = SpecialPowers.getIntPref('dom.vibrator.max_vibrate_ms');
  var maxVibrateListLen = SpecialPowers.getIntPref('dom.vibrator.max_vibrate_list_len');

  // Make sure that these preferences are respected.
  expectFailure(maxVibrateMs + 1);
  expectFailure([maxVibrateMs + 1]);

  var arr = [];
  for (var i = 0; i < maxVibrateListLen + 1; i++) {
    arr[i] = 0;
  }
  expectFailure(arr);
}

function testSuccesses() {
  expectSuccess(0);
  expectSuccess([]);
  expectSuccess('1000');
  expectSuccess(1000);
  expectSuccess(1000.1);
  expectSuccess([0, 0, 0]);
  expectSuccess(['1000', 1000]);
  expectSuccess([1000, 1000]);
  expectSuccess([1000, 1000.1]);

  // The following loop shouldn't cause us to crash.  See bug 701716.
  for (var i = 0; i < 10000; i++) {
    navigator.mozVibrate([100, 100]);
  }
  ok(true, "Didn't crash after issuing a lot of vibrate() calls.");
}

var origVibratorEnabled = SpecialPowers.getBoolPref('dom.vibrator.enabled');

// Test with the vibrator pref enabled.
try {
  SpecialPowers.setBoolPref('dom.vibrator.enabled', true);
  testFailures();
  testSuccesses();

  // Everything should be the same when the vibrator is disabled -- in
  // particular, a disabled vibrator shouldn't eat failures we'd otherwise
  // observe.
  SpecialPowers.setBoolPref('dom.vibrator.enabled', false);
  testFailures();
  testSuccesses();
}
finally {
  SpecialPowers.setBoolPref('dom.vibrator.enabled', origVibratorEnabled);
}

</script>
</body>

</html>
Loading