* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "adapter/android/entrance/java/jni/picker/picker_haptic_controller.h"
#include <algorithm>
#include "frameworks/core/common/container.h"
namespace OHOS::Ace::NG {
namespace {
using std::chrono_literals::operator""s;
using std::chrono_literals::operator""ms;
const std::string AUDIO_URI = "arkui-x/systemres/resources/base/media/timepicker.wav";
const std::string EFFECT_ID_NAME = "haptic.slide";
constexpr std::chrono::milliseconds DEFAULT_DELAY(40);
constexpr std::chrono::milliseconds EXTENDED_DELAY(50);
constexpr float HAPTIC_INTENSITY_BASE = 50.f;
constexpr float HAPTIC_INTENSITY_MAX = 98.f;
constexpr float HAPTIC_INTENSITY_MIN = 50.f;
constexpr float HAPTIC_SPEED_FACTOR = 0.01f;
constexpr size_t SPEED_MAX = 5000;
constexpr size_t SPEED_PLAY_ONCE = 0;
constexpr size_t SPEED_THRESHOLD = 1560;
constexpr size_t TREND_COUNT = 3;
}
PickerHapticController::PickerHapticController(const std::string& uri, const std::string& effectId) noexcept
{
std::string effectiveUri = uri.empty() ? AUDIO_URI : uri;
effectiveEffectId_ = effectId.empty() ? EFFECT_ID_NAME : effectId;
audioHapticPlayer_ = std::make_unique<Platform::AudioHapticPlayerImpl>();
audioHapticPlayer_->Preload(effectiveUri);
auto taskExecutor = Container::CurrentTaskExecutor();
if (taskExecutor) {
vibratorController_ = std::make_unique<Platform::VibratorController>(taskExecutor);
}
InitPlayThread();
}
PickerHapticController::~PickerHapticController() noexcept
{
ThreadRelease();
if (audioHapticPlayer_) {
audioHapticPlayer_->Release();
}
}
void PickerHapticController::ThreadRelease()
{
if (playThread_) {
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
playThreadStatus_ = ThreadStatus::NONE;
}
threadCv_.notify_one();
playThread_->join();
playThread_.reset();
}
}
bool PickerHapticController::IsThreadReady()
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
return playThreadStatus_ == ThreadStatus::READY;
}
bool PickerHapticController::IsThreadPlaying()
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
return playThreadStatus_ == ThreadStatus::PLAYING;
}
bool PickerHapticController::IsThreadPlayOnce()
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
return playThreadStatus_ == ThreadStatus::PLAY_ONCE;
}
bool PickerHapticController::IsThreadNone()
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
return playThreadStatus_ == ThreadStatus::NONE;
}
void PickerHapticController::InitPlayThread()
{
ThreadRelease();
playThreadStatus_ = ThreadStatus::START;
playThread_ = std::make_unique<std::thread>(&PickerHapticController::ThreadLoop, this);
if (playThread_) {
playThreadStatus_ = ThreadStatus::READY;
} else {
playThreadStatus_ = ThreadStatus::NONE;
}
}
void PickerHapticController::ThreadLoop()
{
while (!IsThreadNone()) {
{
std::unique_lock<std::recursive_mutex> lock(threadMutex_);
threadCv_.wait(lock, [this]() { return IsThreadPlaying() || IsThreadPlayOnce() || IsThreadNone(); });
if (IsThreadNone()) {
return;
}
}
isInHapticLoop_ = true;
if (audioHapticPlayer_) {
float volume = audioHapticPlayer_->CalculateVolumeBySpeed(absSpeedInMm_, SPEED_THRESHOLD, SPEED_MAX);
audioHapticPlayer_->Play(volume);
float haptic = absSpeedInMm_ * HAPTIC_SPEED_FACTOR + HAPTIC_INTENSITY_BASE;
haptic = std::clamp(haptic, HAPTIC_INTENSITY_MIN, HAPTIC_INTENSITY_MAX);
if (vibratorController_) {
vibratorController_->Vibrate(effectiveEffectId_, haptic);
}
}
{
auto startTime = std::chrono::high_resolution_clock::now();
std::unique_lock<std::recursive_mutex> lock(threadMutex_);
std::chrono::milliseconds delayTime = DEFAULT_DELAY;
if (IsThreadPlayOnce() && isLoopReadyToStop_) {
delayTime = EXTENDED_DELAY;
}
threadCv_.wait_until(lock, startTime + delayTime);
if (IsThreadPlayOnce() || isLoopReadyToStop_) {
playThreadStatus_ = ThreadStatus::READY;
}
}
isInHapticLoop_ = false;
}
}
void PickerHapticController::Play(size_t speed)
{
if (!playThread_) {
InitPlayThread();
}
bool needNotify = !IsThreadPlaying() && !IsThreadPlayOnce();
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
absSpeedInMm_ = speed;
playThreadStatus_ = ThreadStatus::PLAYING;
}
if (needNotify) {
threadCv_.notify_one();
}
}
void PickerHapticController::PlayOnce()
{
if (IsThreadPlaying()) {
return;
}
if (!playThread_) {
InitPlayThread();
}
bool needNotify = !IsThreadPlaying() && !IsThreadPlayOnce();
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
playThreadStatus_ = ThreadStatus::PLAY_ONCE;
absSpeedInMm_ = SPEED_PLAY_ONCE;
}
if (needNotify) {
threadCv_.notify_one();
}
}
void PickerHapticController::Stop()
{
{
std::lock_guard<std::recursive_mutex> guard(threadMutex_);
playThreadStatus_ = ThreadStatus::READY;
}
threadCv_.notify_one();
lastHandleDeltaTime_ = 0;
}
void PickerHapticController::HandleDelta(double dy)
{
uint64_t currentTime = GetMilliseconds();
uint64_t intervalTime = currentTime - lastHandleDeltaTime_;
CHECK_EQUAL_VOID(intervalTime, 0);
lastHandleDeltaTime_ = currentTime;
auto scrollSpeed = std::abs(ConvertPxToMillimeters(dy) / intervalTime) * 1000;
if (scrollSpeed > SPEED_MAX) {
scrollSpeed = SPEED_MAX;
}
recentSpeeds_.push_back(scrollSpeed);
if (recentSpeeds_.size() > TREND_COUNT) {
recentSpeeds_.pop_front();
}
if (!isInHapticLoop_ && isLoopReadyToStop_) {
isLoopReadyToStop_ = false;
playThreadStatus_ = ThreadStatus::READY;
PlayOnce();
} else if (isHapticCanLoopPlay_ && GetPlayStatus() == 1) {
Play(scrollSpeed);
} else if (GetPlayStatus() == -1 && IsThreadPlaying() && !isLoopReadyToStop_) {
isLoopReadyToStop_ = true;
isHapticCanLoopPlay_ = false;
recentSpeeds_.clear();
absSpeedInMm_ = scrollSpeed;
}
}
double PickerHapticController::ConvertPxToMillimeters(double px) const
{
auto& manager = ScreenSystemManager::GetInstance();
const double density = manager.GetDensity();
return density == 0.0 ? 0.0 : (px / density);
}
size_t PickerHapticController::GetCurrentSpeedInMm()
{
double velocityInPixels = velocityTracker_.GetVelocity().GetVelocityY();
return std::abs(ConvertPxToMillimeters(velocityInPixels));
}
int8_t PickerHapticController::GetPlayStatus()
{
if (recentSpeeds_.size() < TREND_COUNT) {
return 0;
}
bool allAbove = true;
bool allBelow = true;
for (size_t i = 0; i < TREND_COUNT; ++i) {
const double speed = recentSpeeds_[i];
if (speed <= SPEED_THRESHOLD) {
allAbove = false;
}
if (speed >= SPEED_THRESHOLD) {
allBelow = false;
}
}
return allAbove ? 1 : (allBelow ? -1 : 0);
}
}