// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "device/gamepad/gamepad_platform_data_fetcher_android.h"
#include <stddef.h>
#include "base/android/build_info.h"
#include "base/android/jni_android.h"
#include "base/android/jni_array.h"
#include "base/android/jni_string.h"
#include "base/containers/flat_map.h"
#include "base/feature_list.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/trace_event/trace_event.h"
#include "device/gamepad/gamepad_id_list.h"
#include "device/gamepad/haptic_gamepad_android.h"
#include "device/gamepad/public/cpp/gamepad_features.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "device/gamepad/jni_headers/GamepadList_jni.h"
using base::android::AttachCurrentThread;
using base::android::CheckException;
using base::android::ClearException;
using base::android::ConvertJavaStringToUTF8;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
namespace device {
namespace {
// Returns true if |gamepad_id| identifies a gamepad known to be mapped
// correctly on this version of Android. |x_input_type| identifies the flavor of
// XInput used by the gamepad, or kXInputTypeNone if the gamepad is not XInput.
bool HasStandardMappingOnAndroid(GamepadId gamepad_id,
XInputType x_input_type) {
// Gamepads that are mapped correctly on Android do not require a mapping
// function to comply with the Standard Gamepad. Entries in this map
// represent gamepads that have been manually tested and are known to
// work correctly on recent versions of Android. The key is a GamepadId to
// identify the gamepad and the value is the earliest
// base::android::SdkVersion where the gamepad is known to be mapped
// correctly.
const base::flat_map<GamepadId, base::android::SdkVersion>
kManualAssessmentResult = {
// Stadia Controller USB
{GamepadId::kGoogleProduct9400,
base::android::SdkVersion::SDK_VERSION_OREO},
// Xbox 360 wireless
{GamepadId::kMicrosoftProduct02a1,
base::android::SdkVersion::SDK_VERSION_R},
// Xbox One USB (2015 firmware)
{GamepadId::kMicrosoftProduct02dd,
base::android::SdkVersion::SDK_VERSION_R},
// Xbox One S USB
{GamepadId::kMicrosoftProduct02ea,
base::android::SdkVersion::SDK_VERSION_Q},
// Xbox One S Bluetooth
{GamepadId::kMicrosoftProduct02fd,
base::android::SdkVersion::SDK_VERSION_R},
// Xbox Series X USB
{GamepadId::kMicrosoftProduct0b12,
base::android::SdkVersion::SDK_VERSION_R},
// Xbox Series X Bluetooth
{GamepadId::kMicrosoftProduct0b13,
base::android::SdkVersion::SDK_VERSION_Q},
// Xbox One S Bluetooth (2021 firmware)
{GamepadId::kMicrosoftProduct0b20,
base::android::SdkVersion::SDK_VERSION_Q},
// Xbox Adaptive (2021 firmware)
{GamepadId::kMicrosoftProduct0b21,
base::android::SdkVersion::SDK_VERSION_Q},
// Xbox Elite Series 2 (2021 firmware)
{GamepadId::kMicrosoftProduct0b22,
base::android::SdkVersion::SDK_VERSION_Q},
// Switch Pro Controller
{GamepadId::kNintendoProduct2009,
base::android::SdkVersion::SDK_VERSION_R},
};
auto find_it = kManualAssessmentResult.find(gamepad_id);
if (find_it != kManualAssessmentResult.end()) {
return find_it->second <=
base::android::BuildInfo::GetInstance()->sdk_int();
}
// Assume XInput gamepads are always correctly mapped.
return x_input_type == kXInputTypeXbox360 ||
x_input_type == kXInputTypeXboxOne;
}
} // namespace
GamepadPlatformDataFetcherAndroid::GamepadPlatformDataFetcherAndroid() =
default;
GamepadPlatformDataFetcherAndroid::~GamepadPlatformDataFetcherAndroid() {
PauseHint(true);
for (const auto& pair : vibration_actuators_) {
pair.second->Shutdown();
}
}
GamepadSource GamepadPlatformDataFetcherAndroid::source() {
return Factory::static_source();
}
void GamepadPlatformDataFetcherAndroid::OnAddedToProvider() {
PauseHint(false);
}
void GamepadPlatformDataFetcherAndroid::SetDualRumbleVibrationActuator(
int source_id) {
DCHECK(!base::Contains(vibration_actuators_, source_id));
vibration_actuators_.emplace(
source_id, std::make_unique<HapticGamepadAndroid>(source_id));
}
void GamepadPlatformDataFetcherAndroid::TryShutdownDualRumbleVibrationActuator(
int source_id) {
auto find_it = vibration_actuators_.find(source_id);
if (find_it != vibration_actuators_.end()) {
find_it->second->Shutdown();
vibration_actuators_.erase(find_it);
}
}
void GamepadPlatformDataFetcherAndroid::SetVibration(int device_index,
double strong_magnitude,
double weak_magnitude) {
Java_GamepadList_setVibration(base::android::AttachCurrentThread(),
device_index, strong_magnitude, weak_magnitude);
}
void GamepadPlatformDataFetcherAndroid::SetZeroVibration(int device_index) {
Java_GamepadList_setZeroVibration(base::android::AttachCurrentThread(),
device_index);
}
void GamepadPlatformDataFetcherAndroid::GetGamepadData(
bool devices_changed_hint) {
TRACE_EVENT0("GAMEPAD", "GetGamepadData");
JNIEnv* env = AttachCurrentThread();
if (!env)
return;
Java_GamepadList_updateGamepadData(env, reinterpret_cast<intptr_t>(this));
}
void GamepadPlatformDataFetcherAndroid::PauseHint(bool paused) {
JNIEnv* env = AttachCurrentThread();
if (!env)
return;
Java_GamepadList_setGamepadAPIActive(env, !paused);
}
void GamepadPlatformDataFetcherAndroid::PlayEffect(
int source_id,
mojom::GamepadHapticEffectType type,
mojom::GamepadEffectParametersPtr params,
mojom::GamepadHapticsManager::PlayVibrationEffectOnceCallback callback,
scoped_refptr<base::SequencedTaskRunner> callback_runner) {
auto find_it = vibration_actuators_.find(source_id);
if (find_it == vibration_actuators_.end()) {
LOG(ERROR) << "Failed to play vibration effect on a gamepad with no "
"vibration actuator";
RunVibrationCallback(
std::move(callback), std::move(callback_runner),
mojom::GamepadHapticsResult::GamepadHapticsResultError);
return;
}
find_it->second->PlayEffect(type, std::move(params), std::move(callback),
std::move(callback_runner));
}
void GamepadPlatformDataFetcherAndroid::ResetVibration(
int source_id,
mojom::GamepadHapticsManager::ResetVibrationActuatorCallback callback,
scoped_refptr<base::SequencedTaskRunner> callback_runner) {
auto find_it = vibration_actuators_.find(source_id);
if (find_it == vibration_actuators_.end()) {
LOG(ERROR) << "Failed to reset vibration effect on a gamepad with no "
"vibration actuator";
RunVibrationCallback(
std::move(callback), std::move(callback_runner),
mojom::GamepadHapticsResult::GamepadHapticsResultError);
return;
}
find_it->second->ResetVibration(std::move(callback),
std::move(callback_runner));
}
static void JNI_GamepadList_SetGamepadData(
JNIEnv* env,
const JavaParamRef<jobject>& obj,
jlong data_fetcher,
jint index,
jboolean mapping,
jboolean connected,
const JavaParamRef<jstring>& devicename,
jint vendor_id,
jint product_id,
jlong timestamp,
const JavaParamRef<jfloatArray>& jaxes,
const JavaParamRef<jfloatArray>& jbuttons,
jint buttons_length,
jboolean supports_dual_rumble) {
DCHECK(data_fetcher);
GamepadPlatformDataFetcherAndroid* fetcher =
reinterpret_cast<GamepadPlatformDataFetcherAndroid*>(data_fetcher);
DCHECK_LT(index, static_cast<int>(Gamepads::kItemsLengthCap));
// Do not set gamepad parameters for all the gamepad devices that are not
// attached.
if (!connected) {
fetcher->TryShutdownDualRumbleVibrationActuator(index);
return;
}
PadState* state = fetcher->GetPadState(index);
if (!state)
return;
Gamepad& pad = state->data;
// Is this the first time we've seen this device?
if (!state->is_initialized) {
state->is_initialized = true;
std::string product_name =
base::android::ConvertJavaStringToUTF8(env, devicename);
if (!mapping) {
// The gamepad was assigned the default mapping function. Check if it is
// known to be mapped correctly with the default mapping function on this
// version of Android.
auto gamepad_id = GamepadIdList::Get().GetGamepadId(
product_name, vendor_id, product_id);
auto x_input_type =
GamepadIdList::Get().GetXInputType(vendor_id, product_id);
mapping = HasStandardMappingOnAndroid(gamepad_id, x_input_type);
}
GamepadDataFetcher::UpdateGamepadStrings(product_name, vendor_id,
product_id, mapping, pad);
if (base::FeatureList::IsEnabled(
features::kEnableAndroidGamepadVibration)) {
pad.vibration_actuator.type = GamepadHapticActuatorType::kDualRumble;
pad.vibration_actuator.not_null = supports_dual_rumble;
if (supports_dual_rumble) {
fetcher->SetDualRumbleVibrationActuator(state->source_id);
}
}
}
pad.connected = true;
pad.timestamp = GamepadDataFetcher::CurrentTimeInMicroseconds();
std::vector<float> axes;
base::android::JavaFloatArrayToFloatVector(env, jaxes, &axes);
// Set Gamepad::axes_length to the total number of axes on the gamepad device.
// Only return the first kAxesLengthCap if the axes size captured by
// GamepadList is larger than kAxesLengthCap.
pad.axes_length = std::min(static_cast<int>(axes.size()),
static_cast<int>(Gamepad::kAxesLengthCap));
// Copy axes state to the Gamepad axes[].
for (unsigned int i = 0; i < pad.axes_length; i++) {
pad.axes[i] = static_cast<double>(axes[i]);
}
std::vector<float> buttons;
base::android::JavaFloatArrayToFloatVector(env, jbuttons, &buttons);
// Set Gamepad::buttons_length to the total number of buttons on the gamepad
// device. Only return the first kButtonsLengthCap if buttons_length captured
// by GamepadList is larger than kButtonsLengthCap.
pad.buttons_length =
std::min({static_cast<int>(buttons.size()), buttons_length,
static_cast<int>(Gamepad::kButtonsLengthCap)});
// Copy buttons state to the Gamepad buttons[].
for (unsigned int j = 0; j < pad.buttons_length; j++) {
pad.buttons[j].pressed =
buttons[j] > GamepadButton::kDefaultButtonPressedThreshold;
pad.buttons[j].touched = buttons[j] > 0.0f;
pad.buttons[j].value = buttons[j];
}
}
} // namespace device