Commit 7c498e59 authored by Matt Reynolds's avatar Matt Reynolds Committed by Commit Bot

Add a base class for haptic gamepad functionality

AbstractHapticGamepad encapsulates the logic for translating haptic
effect descriptions into a sequence of vibration actuator commands.
Users of AbstractHapticGamepad should override SetVibration so that it
updates the vibration actuators on the device with the specified
intensities.

BUG=749295

Change-Id: I3744cd3b06efb7970bd87048996250e52c472919
Reviewed-on: https://chromium-review.googlesource.com/810006Reviewed-by: default avatarBrandon Jones <bajones@chromium.org>
Commit-Queue: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/master@{#522221}
parent 4227e23c
...@@ -64,6 +64,7 @@ test("device_unittests") { ...@@ -64,6 +64,7 @@ test("device_unittests") {
"bluetooth/test/test_bluetooth_local_gatt_service_delegate.cc", "bluetooth/test/test_bluetooth_local_gatt_service_delegate.cc",
"bluetooth/test/test_bluetooth_local_gatt_service_delegate.h", "bluetooth/test/test_bluetooth_local_gatt_service_delegate.h",
"bluetooth/uribeacon/uri_encoder_unittest.cc", "bluetooth/uribeacon/uri_encoder_unittest.cc",
"gamepad/abstract_haptic_gamepad_unittest.cc",
"gamepad/gamepad_provider_unittest.cc", "gamepad/gamepad_provider_unittest.cc",
"gamepad/gamepad_service_unittest.cc", "gamepad/gamepad_service_unittest.cc",
"gamepad/public/interfaces/gamepad_struct_traits_unittest.cc", "gamepad/public/interfaces/gamepad_struct_traits_unittest.cc",
...@@ -280,7 +281,9 @@ source_set("usb_test_gadget") { ...@@ -280,7 +281,9 @@ source_set("usb_test_gadget") {
} }
if (is_android) { if (is_android) {
bluetooth_java_sources_needing_jni = [ "bluetooth/test/android/java/src/org/chromium/device/bluetooth/Fakes.java" ] bluetooth_java_sources_needing_jni = [
"bluetooth/test/android/java/src/org/chromium/device/bluetooth/Fakes.java",
]
generate_jni("bluetooth_test_jni_headers") { generate_jni("bluetooth_test_jni_headers") {
sources = bluetooth_java_sources_needing_jni sources = bluetooth_java_sources_needing_jni
......
...@@ -13,6 +13,8 @@ component("gamepad") { ...@@ -13,6 +13,8 @@ component("gamepad") {
output_name = "device_gamepad" output_name = "device_gamepad"
sources = [ sources = [
"abstract_haptic_gamepad.cc",
"abstract_haptic_gamepad.h",
"game_controller_data_fetcher_mac.h", "game_controller_data_fetcher_mac.h",
"game_controller_data_fetcher_mac.mm", "game_controller_data_fetcher_mac.mm",
"gamepad_consumer.cc", "gamepad_consumer.cc",
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/gamepad/abstract_haptic_gamepad.h"
namespace device {
AbstractHapticGamepad::AbstractHapticGamepad() : sequence_id_(0) {}
AbstractHapticGamepad::~AbstractHapticGamepad() {
if (playing_effect_callback_) {
SetZeroVibration();
RunCallbackOnMojoThread(
mojom::GamepadHapticsResult::GamepadHapticsResultPreempted);
}
}
void AbstractHapticGamepad::SetZeroVibration() {
SetVibration(0.0, 0.0);
}
base::TimeDelta AbstractHapticGamepad::TaskDelayFromMilliseconds(
double delay_millis) {
return base::TimeDelta::FromMillisecondsD(delay_millis);
}
void AbstractHapticGamepad::PlayEffect(
mojom::GamepadHapticEffectType type,
mojom::GamepadEffectParametersPtr params,
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback) {
if (type !=
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble) {
// Only dual-rumble effects are supported.
std::move(callback).Run(
mojom::GamepadHapticsResult::GamepadHapticsResultNotSupported);
return;
}
int sequence_id = ++sequence_id_;
if (playing_effect_callback_) {
if (params->start_delay > 0.0)
SetZeroVibration();
RunCallbackOnMojoThread(
mojom::GamepadHapticsResult::GamepadHapticsResultPreempted);
}
playing_effect_task_runner_ = base::ThreadTaskRunnerHandle::Get();
playing_effect_callback_ = std::move(callback);
PlayDualRumbleEffect(sequence_id, params->duration, params->start_delay,
params->strong_magnitude, params->weak_magnitude);
}
void AbstractHapticGamepad::ResetVibration(
mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback) {
sequence_id_++;
if (playing_effect_callback_) {
SetZeroVibration();
RunCallbackOnMojoThread(
mojom::GamepadHapticsResult::GamepadHapticsResultPreempted);
}
std::move(callback).Run(
mojom::GamepadHapticsResult::GamepadHapticsResultComplete);
}
void AbstractHapticGamepad::PlayDualRumbleEffect(int sequence_id,
double duration,
double start_delay,
double strong_magnitude,
double weak_magnitude) {
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AbstractHapticGamepad::StartVibration,
base::Unretained(this), sequence_id_, duration,
strong_magnitude, weak_magnitude),
TaskDelayFromMilliseconds(start_delay));
}
void AbstractHapticGamepad::StartVibration(int sequence_id,
double duration,
double strong_magnitude,
double weak_magnitude) {
if (sequence_id != sequence_id_)
return;
SetVibration(strong_magnitude, weak_magnitude);
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AbstractHapticGamepad::StopVibration,
base::Unretained(this), sequence_id),
TaskDelayFromMilliseconds(duration));
}
void AbstractHapticGamepad::StopVibration(int sequence_id) {
if (sequence_id != sequence_id_)
return;
SetZeroVibration();
RunCallbackOnMojoThread(
mojom::GamepadHapticsResult::GamepadHapticsResultComplete);
}
void AbstractHapticGamepad::RunCallbackOnMojoThread(
mojom::GamepadHapticsResult result) {
if (playing_effect_task_runner_->RunsTasksInCurrentSequence()) {
DoRunCallback(std::move(playing_effect_callback_), result);
return;
}
playing_effect_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&AbstractHapticGamepad::DoRunCallback,
std::move(playing_effect_callback_), result));
}
// static
void AbstractHapticGamepad::DoRunCallback(
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback,
mojom::GamepadHapticsResult result) {
std::move(callback).Run(result);
}
} // namespace device
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef DEVICE_GAMEPAD_ABSTRACT_HAPTIC_GAMEPAD_
#define DEVICE_GAMEPAD_ABSTRACT_HAPTIC_GAMEPAD_
#include "base/memory/scoped_refptr.h"
#include "base/sequenced_task_runner.h"
#include "base/time/time.h"
#include "device/gamepad/gamepad_export.h"
#include "device/gamepad/public/interfaces/gamepad.mojom.h"
namespace device {
// AbstractHapticGamepad is a base class for gamepads that support dual-rumble
// vibration effects. To use it, override the SetVibration method so that it
// sets the vibration intensity on the device. Then, calling PlayEffect or
// ResetVibration will call your SetVibration method at the appropriate times
// to produce the desired vibration effect. When the effect is complete, or when
// it has been preempted by another effect, the callback is invoked with a
// result code.
//
// By default, SetZeroVibration simply calls SetVibration with both parameters
// set to zero. You may optionally override SetZeroVibration if the device has a
// more efficient means of stopping an ongoing effect.
class DEVICE_GAMEPAD_EXPORT AbstractHapticGamepad {
public:
AbstractHapticGamepad();
virtual ~AbstractHapticGamepad();
// Start playing an effect.
void PlayEffect(
mojom::GamepadHapticEffectType,
mojom::GamepadEffectParametersPtr,
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback);
// Reset vibration on the gamepad, perhaps interrupting an ongoing effect.
void ResetVibration(
mojom::GamepadHapticsManager::ResetVibrationActuatorCallback);
private:
// Set the vibration magnitude for the strong and weak vibration actuators.
virtual void SetVibration(double strong_magnitude, double weak_magnitude) = 0;
// Set the vibration magnitude for both actuators to zero.
virtual void SetZeroVibration();
// For testing.
virtual base::TimeDelta TaskDelayFromMilliseconds(double delay_millis);
void PlayDualRumbleEffect(int sequence_id,
double duration,
double start_delay,
double strong_magnitude,
double weak_magnitude);
void StartVibration(int sequence_id,
double duration,
double strong_magnitude,
double weak_magnitude);
void StopVibration(int sequence_id);
void RunCallbackOnMojoThread(mojom::GamepadHapticsResult result);
static void DoRunCallback(
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback,
mojom::GamepadHapticsResult);
int sequence_id_;
scoped_refptr<base::SequencedTaskRunner> playing_effect_task_runner_;
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback
playing_effect_callback_;
};
} // namespace device
#endif // DEVICE_GAMEPAD_ABSTRACT_HAPTIC_GAMEPAD_
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "device/gamepad/abstract_haptic_gamepad.h"
#include <memory>
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/test/test_simple_task_runner.h"
#include "device/gamepad/public/interfaces/gamepad.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace device {
namespace {
// An implementation of AbstractHapticGamepad that records how many times its
// SetVibration and SetZeroVibration methods have been called.
class FakeHapticGamepad : public AbstractHapticGamepad {
public:
FakeHapticGamepad() : set_vibration_count_(0), set_zero_vibration_count_(0) {}
~FakeHapticGamepad() override = default;
void SetVibration(double strong_magnitude, double weak_magnitude) override {
set_vibration_count_++;
}
void SetZeroVibration() override { set_zero_vibration_count_++; }
base::TimeDelta TaskDelayFromMilliseconds(double delay_millis) override {
// Remove delays for testing.
return base::TimeDelta();
}
int set_vibration_count_;
int set_zero_vibration_count_;
};
// Main test fixture
class AbstractHapticGamepadTest : public testing::Test {
public:
AbstractHapticGamepadTest()
: first_callback_count_(0),
second_callback_count_(0),
first_callback_result_(
mojom::GamepadHapticsResult::GamepadHapticsResultError),
second_callback_result_(
mojom::GamepadHapticsResult::GamepadHapticsResultError),
gamepad_(std::make_unique<FakeHapticGamepad>()),
task_runner_(new base::TestSimpleTaskRunner) {}
void PostPlayEffect(
mojom::GamepadHapticEffectType type,
double duration,
double start_delay,
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback) {
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&FakeHapticGamepad::PlayEffect,
base::Unretained(gamepad_.get()), type,
mojom::GamepadEffectParameters::New(
duration, start_delay, 1.0, 1.0),
std::move(callback)),
base::TimeDelta());
}
void PostResetVibration(
mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback) {
task_runner_->PostDelayedTask(
FROM_HERE,
base::BindOnce(&FakeHapticGamepad::ResetVibration,
base::Unretained(gamepad_.get()), std::move(callback)),
base::TimeDelta());
}
void FirstCallback(mojom::GamepadHapticsResult result) {
first_callback_count_++;
first_callback_result_ = result;
}
void SecondCallback(mojom::GamepadHapticsResult result) {
second_callback_count_++;
second_callback_result_ = result;
}
int first_callback_count_;
int second_callback_count_;
mojom::GamepadHapticsResult first_callback_result_;
mojom::GamepadHapticsResult second_callback_result_;
std::unique_ptr<FakeHapticGamepad> gamepad_;
scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
DISALLOW_COPY_AND_ASSIGN(AbstractHapticGamepadTest);
};
TEST_F(AbstractHapticGamepadTest, PlayEffectTest) {
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
0.0,
base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
// Run the queued task, but stop before executing any new tasks queued by that
// task. This should pause before calling SetVibration.
task_runner_->RunPendingTasks();
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
EXPECT_TRUE(task_runner_->HasPendingTask());
// Run the next task, but pause before completing the effect.
task_runner_->RunPendingTasks();
EXPECT_EQ(1, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
EXPECT_TRUE(task_runner_->HasPendingTask());
// Complete the effect and issue the callback. After this, there should be no
// more pending tasks.
task_runner_->RunPendingTasks();
EXPECT_EQ(1, gamepad_->set_vibration_count_);
EXPECT_EQ(1, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
first_callback_result_);
EXPECT_FALSE(task_runner_->HasPendingTask());
}
TEST_F(AbstractHapticGamepadTest, ResetVibrationTest) {
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
PostResetVibration(base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
task_runner_->RunUntilIdle();
// ResetVibration should return a "complete" result without calling
// SetVibration or SetZeroVibration.
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
first_callback_result_);
}
TEST_F(AbstractHapticGamepadTest, UnsupportedEffectTypeTest) {
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
mojom::GamepadHapticEffectType unsupported_effect_type =
static_cast<mojom::GamepadHapticEffectType>(123);
PostPlayEffect(unsupported_effect_type, 1.0, 0.0,
base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
task_runner_->RunUntilIdle();
// An unsupported effect should return a "not-supported" result without
// calling SetVibration or SetZeroVibration.
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultNotSupported,
first_callback_result_);
}
TEST_F(AbstractHapticGamepadTest, StartDelayTest) {
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
// This test establishes the behavior for the start_delay parameter when
// PlayEffect is called without preempting an existing effect.
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
0.0,
base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
task_runner_->RunUntilIdle();
// With zero start_delay, SetVibration and SetZeroVibration should be called
// exactly once each, to start and stop the effect.
EXPECT_EQ(1, gamepad_->set_vibration_count_);
EXPECT_EQ(1, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
first_callback_result_);
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
1.0,
base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
task_runner_->RunUntilIdle();
// With non-zero start_delay, we still SetVibration and SetZeroVibration to be
// called exactly once each.
EXPECT_EQ(2, gamepad_->set_vibration_count_);
EXPECT_EQ(2, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(2, first_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
first_callback_result_);
}
TEST_F(AbstractHapticGamepadTest, ZeroStartDelayPreemptionTest) {
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
EXPECT_EQ(0, second_callback_count_);
// Start an ongoing effect. We'll preempt this one with another effect.
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
0.0,
base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
// Start a second effect with zero start_delay. This should cause the first
// effect to be preempted before it calls SetVibration.
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
0.0,
base::BindOnce(&AbstractHapticGamepadTest::SecondCallback,
base::Unretained(this)));
// Execute the pending tasks, but stop before executing any newly queued
// tasks.
task_runner_->RunPendingTasks();
// The first effect should have already returned with a "preempted" result.
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(0, second_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultPreempted,
first_callback_result_);
task_runner_->RunUntilIdle();
// Now the second effect should have returned with a "complete" result.
EXPECT_EQ(1, gamepad_->set_vibration_count_);
EXPECT_EQ(1, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(1, second_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
second_callback_result_);
}
TEST_F(AbstractHapticGamepadTest, NonZeroStartDelayPreemptionTest) {
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
EXPECT_EQ(0, second_callback_count_);
// Start an ongoing effect. We'll preempt this one with another effect.
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
0.0,
base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
// Start a second effect with non-zero start_delay. This should cause the
// first effect to be preempted before it calls SetVibration.
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
1.0,
base::BindOnce(&AbstractHapticGamepadTest::SecondCallback,
base::Unretained(this)));
// Execute the pending tasks, but stop before executing any newly queued
// tasks.
task_runner_->RunPendingTasks();
// The first effect should have already returned with a "preempted" result.
// Because the second effect has a non-zero start_delay and is preempting
// another effect, it will call SetZeroVibration to ensure no vibration
// occurs during its start_delay period.
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(1, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(0, second_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultPreempted,
first_callback_result_);
task_runner_->RunUntilIdle();
EXPECT_EQ(1, gamepad_->set_vibration_count_);
EXPECT_EQ(2, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(1, second_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultPreempted,
first_callback_result_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
second_callback_result_);
}
TEST_F(AbstractHapticGamepadTest, ResetVibrationPreemptionTest) {
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(0, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(0, first_callback_count_);
EXPECT_EQ(0, second_callback_count_);
// Start an ongoing effect. We'll preempt it with a reset.
PostPlayEffect(
mojom::GamepadHapticEffectType::GamepadHapticEffectTypeDualRumble, 1.0,
0.0,
base::BindOnce(&AbstractHapticGamepadTest::FirstCallback,
base::Unretained(this)));
// Reset vibration. This should cause the effect to be preempted before it
// calls SetVibration.
PostResetVibration(base::BindOnce(&AbstractHapticGamepadTest::SecondCallback,
base::Unretained(this)));
task_runner_->RunUntilIdle();
EXPECT_EQ(0, gamepad_->set_vibration_count_);
EXPECT_EQ(1, gamepad_->set_zero_vibration_count_);
EXPECT_EQ(1, first_callback_count_);
EXPECT_EQ(1, second_callback_count_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultPreempted,
first_callback_result_);
EXPECT_EQ(mojom::GamepadHapticsResult::GamepadHapticsResultComplete,
second_callback_result_);
}
} // namespace
} // namespace device
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment