| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ash/arc/input_overlay/touch_injector.h" |
| |
| #include <list> |
| #include <utility> |
| |
| #include "ash/app_list/app_list_util.h" |
| #include "ash/public/cpp/arc_game_controls_flag.h" |
| #include "ash/public/cpp/window_properties.h" |
| #include "ash/utility/transformer_util.h" |
| #include "base/check_op.h" |
| #include "base/containers/flat_set.h" |
| #include "base/functional/bind.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "chrome/browser/ash/arc/input_overlay/actions/action.h" |
| #include "chrome/browser/ash/arc/input_overlay/actions/action_move.h" |
| #include "chrome/browser/ash/arc/input_overlay/actions/action_tap.h" |
| #include "chrome/browser/ash/arc/input_overlay/actions/input_element.h" |
| #include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h" |
| #include "chrome/browser/ash/arc/input_overlay/constants.h" |
| #include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h" |
| #include "chrome/browser/ash/arc/input_overlay/touch_id_manager.h" |
| #include "chrome/browser/ash/arc/input_overlay/touch_injector_observer.h" |
| #include "chrome/browser/ash/arc/input_overlay/ui/ui_utils.h" |
| #include "chrome/browser/ash/arc/input_overlay/util.h" |
| #include "ui/aura/window.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/event_source.h" |
| #include "ui/gfx/geometry/rect_f.h" |
| #include "ui/gfx/geometry/vector2d_f.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/frame_view.h" |
| |
| // Enable VLOG level 1. |
| #undef ENABLED_VLOG_LEVEL |
| #define ENABLED_VLOG_LEVEL 1 |
| |
| namespace arc::input_overlay { |
| namespace { |
| // Strings for parsing actions. |
| constexpr char kTapAction[] = "tap"; |
| constexpr char kMoveAction[] = "move"; |
| constexpr char kMouseLock[] = "mouse_lock"; |
| // Mask for interesting modifiers. |
| constexpr int kInterestingFlagsMask = |
| ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN | ui::EF_ALT_DOWN; |
| // Default mouse lock key. |
| constexpr ui::DomCode kDefaultMouseLockCode = ui::DomCode::ESCAPE; |
| |
| // Remove extra Actions with the same ID. |
| void RemoveActionsWithSameID(std::vector<std::unique_ptr<Action>>& actions) { |
| base::flat_set<int> ids; |
| auto it = actions.begin(); |
| while (it != actions.end()) { |
| int id = it->get()->id(); |
| if (!ids.contains(id)) { |
| ids.insert(id); |
| it++; |
| } else { |
| LOG(ERROR) << "Remove action with duplicated ID {" << it->get()->name() |
| << "}."; |
| actions.erase(it); |
| } |
| } |
| } |
| |
| // Parse Json to different types of actions. |
| std::vector<std::unique_ptr<Action>> ParseJsonToActions( |
| TouchInjector* touch_injector, |
| const base::Value::Dict& root) { |
| std::vector<std::unique_ptr<Action>> actions; |
| |
| // Parse tap actions if they exist. |
| if (const auto* tap_act_list = root.FindList(kTapAction)) { |
| for (const auto& val : *tap_act_list) { |
| auto* val_dict = val.GetIfDict(); |
| if (!val_dict) { |
| LOG(ERROR) << "Value must be a dictionary."; |
| continue; |
| } |
| auto action = std::make_unique<ActionTap>(touch_injector); |
| action->set_original_type(ActionType::TAP); |
| bool succeed = action->ParseFromJson(*val_dict); |
| if (succeed) { |
| actions.emplace_back(std::move(action)); |
| } |
| } |
| } |
| |
| // Parse move actions if they exist. |
| if (const auto* move_act_list = root.FindList(kMoveAction)) { |
| for (const auto& val : *move_act_list) { |
| auto* val_dict = val.GetIfDict(); |
| if (!val_dict) { |
| LOG(ERROR) << "Value must be a dictionary."; |
| continue; |
| } |
| auto action = std::make_unique<ActionMove>(touch_injector); |
| action->set_original_type(ActionType::MOVE); |
| bool succeed = action->ParseFromJson(*val_dict); |
| if (succeed) { |
| actions.emplace_back(std::move(action)); |
| } |
| } |
| } |
| |
| // TODO(cuicuiruan): parse more actions. |
| |
| RemoveActionsWithSameID(actions); |
| |
| return actions; |
| } |
| |
| // Return an Action which is not `target_action` and has input overlapped with |
| // `input_element` in `actions`. |
| Action* FindActionWithOverlapInputElement( |
| std::vector<std::unique_ptr<Action>>& actions, |
| Action* target_action, |
| const InputElement& input_element) { |
| for (auto& action : actions) { |
| if (action.get() == target_action) { |
| continue; |
| } |
| if (action->IsOverlapped(input_element)) { |
| return action.get(); |
| } |
| } |
| return nullptr; |
| } |
| |
| // Find the smallest integer larger than `kMaxDefaultActionID` and not in |
| // `id_list` by binary search. |
| // |
| // `id_list` has below requirements: |
| // - All the integers in `id_list` are larger than `kMaxDefaultActionID`. |
| // - No repeated integer in `id_list`. |
| // - `id_list` is sorted. |
| // |
| // For example, if `kMaxDefaultActionID + 1` == 0, the `id_list` could be {0, 1, |
| // 2, 3, ..., x}, each index is the value if there is no missing integer. |
| // - If there is `id_list[index] == index`, it means there is no integer |
| // missing before `index`, then check the values after `index`. |
| // - If there is `id_list[index] > index`, it means there is at least one |
| // missing integer before `index`. |
| // - Since there is no repeating integer, `id_list[index] < index` shouldn't |
| // happen. |
| int FindNewCustomActionID(const std::vector<int>& id_list) { |
| // Index `end` is excluded. |
| int start = 0, end = id_list.size(); |
| while (start < end) { |
| int mid = start + (end - start) / 2; |
| if (id_list[mid] == mid + kMaxDefaultActionID + 1) { |
| // There is no missing integer in the range of [start, mid], so check the |
| // values after `mid`. |
| start = mid + 1; |
| } else if (id_list[mid] > mid + kMaxDefaultActionID + 1) { |
| // There is at least one missing integer in the range of [start, mid), so |
| // check the values before `mid`. |
| end = mid; |
| } else { |
| // This is unlikely to happen because there is no repeated number in |
| // `id_list`. |
| NOTREACHED(); |
| } |
| } |
| |
| // In the end, `start` points at the position where the smallest missing |
| // integer should be and the value is `start + kMaxDefaultActionID + 1`. |
| return start + kMaxDefaultActionID + 1; |
| } |
| |
| // Create Action by `action_type` without any input bindings. |
| std::unique_ptr<Action> CreateRawAction(ActionType type, |
| TouchInjector* injector) { |
| switch (type) { |
| case ActionType::TAP: |
| return std::make_unique<ActionTap>(injector); |
| case ActionType::MOVE: |
| return std::make_unique<ActionMove>(injector); |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| } // namespace |
| |
| // Calculate the window content bounds (excluding caption if it exists) in the |
| // root window. |
| gfx::RectF CalculateWindowContentBounds(aura::Window* window) { |
| DCHECK(window); |
| auto* widget = views::Widget::GetWidgetForNativeView(window); |
| DCHECK(widget->non_client_view()); |
| auto* frame_view = widget->non_client_view()->frame_view(); |
| DCHECK(frame_view); |
| const int height = frame_view->GetBoundsForClientView().y(); |
| auto bounds = gfx::RectF(window->bounds()); |
| bounds.Inset(gfx::InsetsF::TLBR(height, 0, 0, 0)); |
| return bounds; |
| } |
| |
| class TouchInjector::KeyCommand { |
| public: |
| KeyCommand(const ui::DomCode key, |
| const int modifiers, |
| const base::RepeatingClosure callback) |
| : key_(key), |
| modifiers_(modifiers & kInterestingFlagsMask), |
| callback_(std::move(callback)) {} |
| ~KeyCommand() = default; |
| bool Process(const ui::Event& event) { |
| if (!event.IsKeyEvent()) { |
| return false; |
| } |
| if (auto* key_event = event.AsKeyEvent(); |
| key_ == key_event->code() && |
| modifiers_ == (key_event->flags() & kInterestingFlagsMask)) { |
| if (key_event->type() == ui::EventType::kKeyPressed) { |
| callback_.Run(); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private: |
| ui::DomCode key_; |
| int modifiers_; |
| base::RepeatingClosure callback_; |
| }; |
| |
| TouchInjector::TouchInjector(aura::Window* top_level_window, |
| const std::string& package_name, |
| OnSaveProtoFileCallback save_file_callback) |
| : window_(top_level_window), |
| package_name_(package_name), |
| content_bounds_f_(CalculateWindowContentBounds(window_)), |
| save_file_callback_(save_file_callback) {} |
| |
| TouchInjector::~TouchInjector() { |
| if (!played_with_game_controls_) { |
| // Record the game never played with Game Controls before shutting down the |
| // game. |
| RecordPlayGameWithGameControls(package_name_, |
| /*played_with_game_controls=*/false); |
| } |
| UnRegisterEventRewriter(); |
| } |
| |
| void TouchInjector::ParseActions(const base::Value::Dict& root) { |
| DCHECK(actions_.empty()); |
| if (enable_mouse_lock_) { |
| ParseMouseLock(root); |
| } |
| |
| if (auto parsed_actions = ParseJsonToActions(this, root); |
| !parsed_actions.empty()) { |
| std::move(parsed_actions.begin(), parsed_actions.end(), |
| std::back_inserter(actions_)); |
| } |
| } |
| |
| void TouchInjector::UpdateFlags(bool is_o4c) { |
| ash::ArcGameControlsFlag flags = static_cast<ash::ArcGameControlsFlag>( |
| ash::ArcGameControlsFlag::kKnown | ash::ArcGameControlsFlag::kAvailable | |
| (GetActiveActionsSize() == 0u ? ash::ArcGameControlsFlag::kEmpty : 0) | |
| (is_o4c ? ash::ArcGameControlsFlag::kO4C : 0) | |
| (touch_injector_enable_ ? ash::ArcGameControlsFlag::kEnabled : 0) | |
| (touch_injector_enable_ && input_mapping_visible_ |
| ? ash::ArcGameControlsFlag::kHint |
| : 0)); |
| window_->SetProperty(ash::kArcGameControlsFlagsKey, flags); |
| } |
| |
| void TouchInjector::NotifyTextInputState(bool active) { |
| if (text_input_active_ != active && active) { |
| DispatchTouchReleaseEvent(); |
| } |
| text_input_active_ = active; |
| } |
| |
| void TouchInjector::RegisterEventRewriter() { |
| if (observation_.IsObserving()) { |
| return; |
| } |
| observation_.Observe(window_->GetHost()->GetEventSource()); |
| UpdatePositionsForRegister(); |
| } |
| |
| void TouchInjector::UnRegisterEventRewriter() { |
| if (!observation_.IsObserving()) { |
| return; |
| } |
| DispatchTouchReleaseEvent(); |
| observation_.Reset(); |
| OnSaveProtoFile(); |
| } |
| |
| void TouchInjector::OnInputBindingChange( |
| Action* target_action, |
| std::unique_ptr<InputElement> input_element) { |
| auto* overlapped_action = FindActionWithOverlapInputElement( |
| actions_, target_action, *input_element); |
| |
| // Partially unbind or completely unbind the `overlapped_action` if it |
| // conflicts with `input_element`. |
| if (overlapped_action) { |
| overlapped_action->UnbindInput(*input_element); |
| } |
| |
| target_action->BindInput(std::move(input_element)); |
| |
| if (overlapped_action) { |
| NotifyActionInputBindingUpdated(*overlapped_action); |
| } |
| NotifyActionInputBindingUpdated(*target_action); |
| } |
| |
| void TouchInjector::OnBindingSave() { |
| DCHECK(display_overlay_controller_); |
| OnSaveProtoFile(); |
| } |
| |
| void TouchInjector::OnProtoDataAvailable(AppDataProto& proto) { |
| LoadMenuStateFromProto(proto); |
| for (const ActionProto& action_proto : proto.actions()) { |
| if (action_proto.id() <= kMaxDefaultActionID) { |
| auto* action = GetActionById(action_proto.id()); |
| DCHECK(action); |
| if (!action) { |
| continue; |
| } |
| OverwriteDefaultAction(action_proto, action); |
| } else { |
| AddUserAddedActionFromProto(action_proto); |
| } |
| } |
| } |
| |
| void TouchInjector::OnInputMenuViewRemoved() { |
| OnSaveProtoFile(); |
| // Record UMA stats upon `InputMenuView` close because it needs to ignore the |
| // unfinalized menu state change. |
| if (touch_injector_enable_ != touch_injector_enable_uma_) { |
| touch_injector_enable_uma_ = touch_injector_enable_; |
| RecordInputOverlayFeatureState(package_name_, touch_injector_enable_uma_); |
| } |
| |
| if (input_mapping_visible_ != input_mapping_visible_uma_) { |
| input_mapping_visible_uma_ = input_mapping_visible_; |
| RecordInputOverlayMappingHintState(package_name_, |
| input_mapping_visible_uma_); |
| } |
| } |
| |
| void TouchInjector::MaybeBindDefaultInputElement(Action* action) { |
| // It only supports to assign default keys WASD for `ActionMove` if there is |
| // no overlapped input binding. |
| if (action->GetType() != ActionType::MOVE) { |
| return; |
| } |
| |
| auto input_element = InputElement::CreateActionMoveKeyElement( |
| {ui::DomCode::US_W, ui::DomCode::US_A, ui::DomCode::US_S, |
| ui::DomCode::US_D}); |
| if (auto* overlapped_action = FindActionWithOverlapInputElement( |
| actions_, /*target_action=*/action, *input_element); |
| !overlapped_action) { |
| action->BindInput(std::move(input_element)); |
| NotifyActionInputBindingUpdated(*action); |
| } |
| } |
| |
| void TouchInjector::UpdatePositionsForRegister() { |
| if (rotation_transform_) { |
| rotation_transform_.reset(); |
| } |
| |
| // No need to transform if there is no rotation. |
| if (auto display = display::Screen::Get()->GetDisplayNearestWindow(window_); |
| display.panel_rotation() != display::Display::ROTATE_0) { |
| rotation_transform_ = |
| std::make_unique<gfx::Transform>(ash::CreateRotationTransform( |
| display::Display::ROTATE_0, display.panel_rotation(), |
| gfx::SizeF(display.GetSizeInPixel()))); |
| } |
| UpdateForOverlayBoundsChanged(CalculateWindowContentBounds(window_)); |
| } |
| |
| void TouchInjector::UpdateForOverlayBoundsChanged( |
| const gfx::RectF& new_bounds) { |
| const bool should_update_view = |
| content_bounds_f_.width() != new_bounds.width() || |
| content_bounds_f_.height() != new_bounds.height(); |
| |
| content_bounds_f_ = new_bounds; |
| for (auto& action : actions_) { |
| action->UpdateTouchDownPositions(); |
| } |
| |
| if (should_update_view) { |
| NotifyContentBoundsSizeChanged(); |
| } |
| } |
| |
| void TouchInjector::CleanupTouchEvents() { |
| if (!has_pending_touch_events_ && !is_mouse_locked_) { |
| return; |
| } |
| |
| if (is_mouse_locked_) { |
| FlipMouseLockFlag(); |
| } |
| DispatchTouchReleaseEvent(); |
| } |
| |
| void TouchInjector::DispatchTouchReleaseEventOnMouseUnLock() { |
| for (auto& action : actions_) { |
| if (action->require_mouse_locked()) { |
| auto release = action->GetTouchReleasedEvent(); |
| if (!release) { |
| continue; |
| } |
| if (SendEventFinally(continuation_, &*release).dispatcher_destroyed) { |
| VLOG(0) |
| << "Undispatched event due to destroyed dispatcher for releasing " |
| "touch event when unlocking mouse."; |
| } |
| } |
| } |
| } |
| |
| void TouchInjector::DispatchTouchReleaseEvent() { |
| if (!has_pending_touch_events_) { |
| return; |
| } |
| |
| for (auto& action : actions_) { |
| auto release = action->GetTouchReleasedEvent(); |
| if (!release) { |
| continue; |
| } |
| if (SendEventFinally(continuation_, &*release).dispatcher_destroyed) { |
| VLOG(0) << "Undispatched event due to destroyed dispatcher for releasing " |
| "touch event when unlocking mouse."; |
| } |
| } |
| |
| // Release active touch-to-touch events. |
| for (auto& touch_info : rewritten_touch_infos_) { |
| const auto touch_point_info = touch_info.second; |
| const auto managed_touch_id = touch_point_info.rewritten_touch_id; |
| const auto root_location = touch_point_info.touch_root_location; |
| |
| auto touch_to_release = std::make_unique<ui::TouchEvent>(ui::TouchEvent( |
| ui::EventType::kTouchReleased, root_location, root_location, |
| ui::EventTimeForNow(), |
| ui::PointerDetails(ui::EventPointerType::kTouch, managed_touch_id))); |
| if (SendEventFinally(continuation_, &*touch_to_release) |
| .dispatcher_destroyed) { |
| VLOG(0) << "Undispatched event due to destroyed dispatcher for releasing " |
| "stored touch event."; |
| } |
| TouchIdManager::GetInstance()->ReleaseTouchID( |
| touch_info.second.rewritten_touch_id); |
| } |
| rewritten_touch_infos_.clear(); |
| has_pending_touch_events_ = false; |
| } |
| |
| void TouchInjector::SendExtraEvent( |
| const ui::EventRewriter::Continuation continuation, |
| const ui::Event& event) { |
| if (SendEventFinally(continuation, &event).dispatcher_destroyed) { |
| VLOG(0) << "Undispatched event due to destroyed dispatcher for " |
| "touch move event."; |
| } |
| } |
| |
| void TouchInjector::ParseMouseLock(const base::Value::Dict& dict) { |
| auto* mouse_lock = dict.FindDict(kMouseLock); |
| if (!mouse_lock) { |
| mouse_lock_ = std::make_unique<KeyCommand>( |
| kDefaultMouseLockCode, /*modifier=*/0, |
| base::BindRepeating(&TouchInjector::FlipMouseLockFlag, |
| weak_ptr_factory_.GetWeakPtr())); |
| return; |
| } |
| auto key = ParseKeyboardKey(*mouse_lock, kMouseLock); |
| if (!key) { |
| return; |
| } |
| // Customized mouse lock overrides the default mouse lock. |
| mouse_lock_ = std::make_unique<KeyCommand>( |
| key->first, key->second, |
| base::BindRepeating(&TouchInjector::FlipMouseLockFlag, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void TouchInjector::FlipMouseLockFlag() { |
| is_mouse_locked_ = !is_mouse_locked_; |
| if (!is_mouse_locked_) { |
| DispatchTouchReleaseEventOnMouseUnLock(); |
| } |
| } |
| |
| ui::EventDispatchDetails TouchInjector::RewriteEvent( |
| const ui::Event& event, |
| const ui::EventRewriter::Continuation continuation) { |
| continuation_ = continuation; |
| |
| if (!can_rewrite_event_) { |
| return SendEvent(continuation, &event); |
| } |
| |
| // Don't rewrite unrelated events. |
| if (event.IsTouchEvent() || event.IsGestureEvent() || |
| event.IsCancelModeEvent()) { |
| // TODO(b/334233813): When real touch or gesture event happens, clean up |
| // simulated touch events and send the real touch or gesture event as it |
| // is. Supporting both simulated touch events and real touch events should |
| // be re-considered. |
| CleanupTouchEvents(); |
| return SendEvent(continuation, &event); |
| } |
| |
| if (!touch_injector_enable_ || text_input_active_) { |
| return SendEvent(continuation, &event); |
| } |
| |
| if (mouse_lock_ && mouse_lock_->Process(event)) { |
| return DiscardEvent(continuation); |
| } |
| |
| std::list<ui::TouchEvent> touch_events; |
| bool is_play_mode_active = false; |
| for (auto& action : actions_) { |
| is_play_mode_active |= action->IsActive(); |
| |
| bool keep_original_event = false; |
| bool rewritten = |
| action->RewriteEvent(event, is_mouse_locked_, rotation_transform_.get(), |
| touch_events, keep_original_event); |
| if (!rewritten) { |
| continue; |
| } |
| |
| has_pending_touch_events_ = true; |
| |
| if (!played_with_game_controls_) { |
| played_with_game_controls_ = true; |
| // Record once only when it is played with Game Controls for the first |
| // time. |
| RecordPlayGameWithGameControls(package_name_, |
| /*played_with_game_controls=*/true); |
| } |
| |
| if (keep_original_event) { |
| SendExtraEvent(continuation, event); |
| } |
| if (touch_events.empty()) { |
| return DiscardEvent(continuation); |
| } |
| if (touch_events.size() == 1) { |
| return SendEventFinally(continuation, &touch_events.front()); |
| } |
| if (touch_events.size() == 2) { |
| if (touch_events.back().type() == ui::EventType::kTouchMoved) { |
| // Some apps can't process correctly on the touch move event which |
| // follows touch press event immediately, so send the touch move event |
| // delayed here. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&TouchInjector::SendExtraEvent, |
| weak_ptr_factory_.GetWeakPtr(), continuation, |
| touch_events.back()), |
| kSendTouchMoveDelay); |
| return SendEventFinally(continuation, &touch_events.front()); |
| } else { |
| SendExtraEvent(continuation, touch_events.front()); |
| return SendEventFinally(continuation, &touch_events.back()); |
| } |
| } |
| } |
| |
| // Discard other mouse events if the mouse is locked or it is in active play |
| // mode. |
| if (event.IsMouseEvent() && (is_mouse_locked_ || is_play_mode_active)) { |
| return DiscardEvent(continuation); |
| } |
| |
| return SendEvent(continuation, &event); |
| } |
| |
| std::unique_ptr<ui::TouchEvent> TouchInjector::RewriteOriginalTouch( |
| const ui::TouchEvent* touch_event) { |
| ui::PointerId original_id = touch_event->pointer_details().id; |
| auto it = rewritten_touch_infos_.find(original_id); |
| |
| if (it == rewritten_touch_infos_.end()) { |
| // When touching on the window to regain the focus, the first |
| // `ui::EventType::kTouchPressed` will not be received and then it may send |
| // `ui::EventType::kTouchMoved` event to the window. So no need to add |
| // DCHECK here. |
| if (touch_event->type() != ui::EventType::kTouchPressed) { |
| return nullptr; |
| } |
| } else { |
| DCHECK(touch_event->type() != ui::EventType::kTouchPressed); |
| if (touch_event->type() == ui::EventType::kTouchPressed) { |
| return nullptr; |
| } |
| } |
| |
| // Confirmed the input is valid. |
| auto root_location_f = touch_event->root_location_f(); |
| |
| if (touch_event->type() == ui::EventType::kTouchPressed) { |
| // Generate new touch id that we can manage and add to map. |
| std::optional<int> managed_touch_id = |
| TouchIdManager::GetInstance()->ObtainTouchID(); |
| DCHECK(managed_touch_id); |
| TouchPointInfo touch_point = { |
| .rewritten_touch_id = *managed_touch_id, |
| .touch_root_location = root_location_f, |
| }; |
| rewritten_touch_infos_.emplace(original_id, touch_point); |
| return CreateTouchEvent(touch_event, original_id, *managed_touch_id, |
| root_location_f); |
| } else if (touch_event->type() == ui::EventType::kTouchReleased) { |
| std::optional<int> managed_touch_id = it->second.rewritten_touch_id; |
| DCHECK(managed_touch_id); |
| rewritten_touch_infos_.erase(original_id); |
| TouchIdManager::GetInstance()->ReleaseTouchID(*managed_touch_id); |
| return CreateTouchEvent(touch_event, original_id, *managed_touch_id, |
| root_location_f); |
| } |
| |
| // Update this id's stored location to this newest location. |
| it->second.touch_root_location = root_location_f; |
| std::optional<int> managed_touch_id = it->second.rewritten_touch_id; |
| DCHECK(managed_touch_id); |
| return CreateTouchEvent(touch_event, original_id, *managed_touch_id, |
| root_location_f); |
| } |
| |
| std::unique_ptr<ui::TouchEvent> TouchInjector::CreateTouchEvent( |
| const ui::TouchEvent* touch_event, |
| ui::PointerId original_id, |
| int managed_touch_id, |
| gfx::PointF root_location_f) { |
| ui::PointerDetails managed_pointer_details = touch_event->pointer_details(); |
| managed_pointer_details.id = managed_touch_id; |
| return std::make_unique<ui::TouchEvent>( |
| ui::TouchEvent(touch_event->type(), root_location_f, root_location_f, |
| touch_event->time_stamp(), managed_pointer_details)); |
| } |
| |
| Action* TouchInjector::GetActionById(int id) { |
| for (auto& action : actions_) { |
| if (action->id() == id) { |
| return action.get(); |
| } |
| } |
| return nullptr; |
| } |
| |
| std::unique_ptr<AppDataProto> TouchInjector::ConvertToProto() { |
| auto app_data_proto = std::make_unique<AppDataProto>(); |
| for (auto& action : actions_) { |
| if (auto customized_proto = action->ConvertToProtoIfCustomized()) { |
| *app_data_proto->add_actions() = *customized_proto; |
| } |
| } |
| AddMenuStateToProto(*app_data_proto); |
| AddSystemVersionToProto(*app_data_proto); |
| return app_data_proto; |
| } |
| |
| void TouchInjector::OnSaveProtoFile() { |
| auto app_data_proto = ConvertToProto(); |
| save_file_callback_.Run(std::move(app_data_proto), package_name_); |
| } |
| |
| void TouchInjector::AddMenuStateToProto(AppDataProto& proto) { |
| proto.set_input_control(touch_injector_enable_); |
| proto.set_input_mapping_hint(input_mapping_visible_); |
| } |
| |
| void TouchInjector::LoadMenuStateFromProto(AppDataProto& proto) { |
| touch_injector_enable_ = |
| proto.has_input_control() ? proto.input_control() : true; |
| input_mapping_visible_ = |
| proto.has_input_mapping_hint() ? proto.input_mapping_hint() : true; |
| |
| if (display_overlay_controller_) { |
| display_overlay_controller_->OnApplyMenuState(); |
| } |
| } |
| |
| void TouchInjector::AddSystemVersionToProto(AppDataProto& proto) { |
| proto.set_system_version(GetCurrentSystemVersion()); |
| } |
| |
| void TouchInjector::AddObserver(TouchInjectorObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void TouchInjector::RemoveObserver(TouchInjectorObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| int TouchInjector::GetNextNewActionID() { |
| if (actions_.empty()) { |
| return kMaxDefaultActionID + 1; |
| } |
| |
| std::vector<int> ids; |
| for (const auto& action : actions_) { |
| const int id = action->id(); |
| if (id > kMaxDefaultActionID) { |
| ids.emplace_back(id); |
| } |
| } |
| std::sort(ids.begin(), ids.end()); |
| |
| return FindNewCustomActionID(ids); |
| } |
| |
| size_t TouchInjector::GetActiveActionsSize() { |
| size_t active_size = 0; |
| for (auto& action : actions_) { |
| if (!action->IsDeleted()) { |
| active_size++; |
| } |
| } |
| return active_size; |
| } |
| |
| bool TouchInjector::HasSingleUserAddedAction() const { |
| size_t action_size = 0; |
| for (const auto& action : actions_) { |
| if (!action->IsDefaultAction()) { |
| action_size++; |
| if (action_size > 1u) { |
| return false; |
| } |
| } |
| } |
| return action_size == 1u; |
| } |
| |
| void TouchInjector::AddNewAction(ActionType action_type, |
| const gfx::Point& target_pos) { |
| auto action = CreateRawAction(action_type, this); |
| |
| // Check whether the action size extends the maximum. |
| if (!action->InitByAddingNewAction(target_pos)) { |
| return; |
| } |
| |
| auto* new_action_ptr = action.get(); |
| NotifyActionAdded(*actions_.emplace_back(std::move(action))); |
| MaybeBindDefaultInputElement(new_action_ptr); |
| |
| // It may need to turn off the flag `kEmpty` after adding an action. |
| UpdateFlagAndProperty(window_, ash::ArcGameControlsFlag::kEmpty, |
| /*enable_flag=*/false); |
| } |
| |
| void TouchInjector::RemoveAction(Action* action) { |
| auto it = std::find_if( |
| actions_.begin(), actions_.end(), |
| [&](const std::unique_ptr<Action>& p) { return action == p.get(); }); |
| DCHECK(it != actions_.end()); |
| if (it->get()->IsDefaultAction()) { |
| // Default action is from JSON. Since it reads mapping data from JSON first |
| // and then from proto, only deleting the default action from `action_` |
| // won't actually delete when launching app next time. So here it marks the |
| // default action deleted. |
| it->get()->RemoveDefaultAction(); |
| } else { |
| actions_.erase(it); |
| } |
| |
| NotifyActionRemoved(*action); |
| |
| // It may need to turn on the flag `kEmpty` after removing an action. |
| DCHECK_EQ(false, |
| IsFlagSet(window_->GetProperty(ash::kArcGameControlsFlagsKey), |
| ash::ArcGameControlsFlag::kEmpty)); |
| if (GetActiveActionsSize() == 0u) { |
| UpdateFlagAndProperty(window_, ash::ArcGameControlsFlag::kEmpty, |
| /*enable_flag=*/true); |
| } |
| } |
| |
| void TouchInjector::ChangeActionType(Action* action, ActionType action_type) { |
| auto new_action = CreateRawAction(action_type, this); |
| new_action->InitByChangingActionType(action); |
| auto* new_action_ptr = new_action.get(); |
| ReplaceActionInternal(action, std::move(new_action)); |
| NotifyActionTypeChanged(action, new_action_ptr); |
| MaybeBindDefaultInputElement(new_action_ptr); |
| } |
| |
| void TouchInjector::RemoveActionNewState(Action* action) { |
| DCHECK(action->is_new()); |
| action->set_is_new(false); |
| NotifyActionNewStateRemoved(*action); |
| } |
| |
| void TouchInjector::OverwriteDefaultAction(const ActionProto& proto, |
| Action* action) { |
| DCHECK(action); |
| DCHECK_LE(proto.id(), kMaxDefaultActionID); |
| DCHECK_EQ(proto.id(), action->id()); |
| if (action->GetType() != proto.action_type()) { |
| auto new_action = CreateRawAction(proto.action_type(), this); |
| new_action->InitByChangingActionType(action); |
| new_action->OverwriteDefaultActionFromProto(proto); |
| ReplaceActionInternal(action, std::move(new_action)); |
| } else { |
| action->OverwriteDefaultActionFromProto(proto); |
| } |
| } |
| |
| void TouchInjector::AddUserAddedActionFromProto(const ActionProto& proto) { |
| auto action = CreateRawAction(proto.action_type(), this); |
| action->ParseUserAddedActionFromProto(proto); |
| actions_.emplace_back(std::move(action)); |
| } |
| |
| void TouchInjector::ReplaceActionInternal(Action* action, |
| std::unique_ptr<Action> new_action) { |
| auto it = std::find_if( |
| actions_.begin(), actions_.end(), |
| [&](const std::unique_ptr<Action>& p) { return action == p.get(); }); |
| DCHECK(it != actions_.end()); |
| actions_[it - actions_.begin()] = std::move(new_action); |
| } |
| |
| void TouchInjector::NotifyActionAdded(Action& action) { |
| for (auto& observer : observers_) { |
| observer.OnActionAdded(action); |
| } |
| } |
| |
| void TouchInjector::NotifyActionRemoved(Action& action) { |
| for (auto& observer : observers_) { |
| observer.OnActionRemoved(action); |
| } |
| } |
| |
| void TouchInjector::NotifyActionNewStateRemoved(Action& action) { |
| for (auto& observer : observers_) { |
| observer.OnActionNewStateRemoved(action); |
| } |
| } |
| |
| void TouchInjector::NotifyActionTypeChanged(Action* action, |
| Action* new_action) { |
| for (auto& observer : observers_) { |
| observer.OnActionTypeChanged(action, new_action); |
| } |
| } |
| |
| void TouchInjector::NotifyActionInputBindingUpdated(const Action& action) { |
| for (auto& observer : observers_) { |
| observer.OnActionInputBindingUpdated(action); |
| } |
| } |
| |
| void TouchInjector::NotifyContentBoundsSizeChanged() { |
| for (auto& observer : observers_) { |
| observer.OnContentBoundsSizeChanged(); |
| } |
| } |
| |
| void TouchInjector::RecordMenuStateOnLaunch() { |
| touch_injector_enable_uma_ = touch_injector_enable_; |
| input_mapping_visible_uma_ = input_mapping_visible_; |
| RecordInputOverlayFeatureState(package_name_, touch_injector_enable_uma_); |
| RecordInputOverlayMappingHintState(package_name_, input_mapping_visible_uma_); |
| } |
| |
| int TouchInjector::GetRewrittenTouchIdForTesting(ui::PointerId original_id) { |
| auto it = rewritten_touch_infos_.find(original_id); |
| DCHECK(it != rewritten_touch_infos_.end()); |
| |
| return it->second.rewritten_touch_id; |
| } |
| |
| gfx::PointF TouchInjector::GetRewrittenRootLocationForTesting( |
| ui::PointerId original_id) { |
| auto it = rewritten_touch_infos_.find(original_id); |
| DCHECK(it != rewritten_touch_infos_.end()); |
| |
| return it->second.touch_root_location; |
| } |
| |
| DisplayOverlayController* TouchInjector::GetControllerForTesting() { |
| return display_overlay_controller_; |
| } |
| |
| } // namespace arc::input_overlay |