// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_SYSTEM_ECHE_ECHE_TRAY_H_
#define ASH_SYSTEM_ECHE_ECHE_TRAY_H_
#include <string>
#include "ash/ash_export.h"
#include "ash/public/cpp/keyboard/keyboard_controller_observer.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/shelf/shelf_observer.h"
#include "ash/shell_observer.h"
#include "ash/system/eche/eche_icon_loading_indicator_view.h"
#include "ash/system/screen_layout_observer.h"
#include "ash/system/tray/system_tray_observer.h"
#include "ash/system/tray/tray_background_view.h"
#include "ash/webui/eche_app_ui/eche_connection_status_handler.h"
#include "ash/webui/eche_app_ui/mojom/eche_app.mojom-shared.h"
#include "ash/webui/eche_app_ui/mojom/eche_app.mojom.h"
#include "base/functional/callback_forward.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/timer/timer.h"
#include "ui/display/display_observer.h"
#include "ui/display/tablet_state.h"
#include "ui/events/event_handler.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/controls/button/button.h"
#include "url/gurl.h"
namespace display {
enum class TabletState;
} // namespace display
namespace views {
class ImageView;
class ImageButton;
class View;
class Widget;
} // namespace views
namespace ui {
class KeyEvent;
} // namespace ui
namespace gfx {
class Image;
class Size;
} // namespace gfx
namespace keyboard {
class KeyboardUIController;
} // namespace keyboard
namespace ash {
class AshWebView;
class PhoneHubTray;
class TrayBubbleView;
class TrayBubbleWrapper;
class SessionControllerImpl;
class Shelf;
class Shell;
// This class represents the Eche tray button in the status area and
// controls the bubble that is shown when the tray button is clicked.
class ASH_EXPORT EcheTray
: public TrayBackgroundView,
public SessionObserver,
public ScreenLayoutObserver,
public ShelfObserver,
public SystemTrayObserver,
public display::DisplayObserver,
public KeyboardControllerObserver,
public ShellObserver,
public eche_app::EcheConnectionStatusHandler::Observer {
METADATA_HEADER(EcheTray, TrayBackgroundView)
public:
// TODO(b/226687249): Move to ash/webui/eche_app_ui if dependency cycle error
// is fixed. Enum representing the connection fail reason. These values are
// persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
enum class ConnectionFailReason {
// Initial state.
kUnknown = 0,
// Timeout because signaling no response, we don't received any response
// or request before timeout. Report this from EcheSignaler.
kSignalingNotTriggered = 1,
// Timeout because signaling response is late. Report this from
// EcheSignaler.
kSignalingHasLateResponse = 2,
// Timeout because we can't finish the whole connection process on time
// after receiving the signaling request from the remote device. Report
// this from EcheSignaler.
kSignalingHasLateRequest = 3,
// Timeout because the security channel disconnected. Report this from
// EcheSignaler.
kSecurityChannelDisconnected = 4,
// Connection fail because the device is in the tablet mode. Report this
// from EcheTray.
kConnectionFailInTabletMode = 5,
// Connection fail because the devices are on different networks. Report
// this from EcheTray.
kConnectionFailSsidDifferent = 6,
// Connection fail because the remote device is on cellular network. Report
// this from EcheTray.
kConnectionFailRemoteDeviceOnCellular = 7,
kMaxValue = kConnectionFailRemoteDeviceOnCellular,
};
using GracefulCloseCallback = base::OnceCallback<void()>;
using GracefulGoBackCallback = base::RepeatingCallback<void()>;
using BubbleShownCallback = base::RepeatingCallback<void(AshWebView* view)>;
explicit EcheTray(Shelf* shelf);
EcheTray(const EcheTray&) = delete;
EcheTray& operator=(const EcheTray&) = delete;
~EcheTray() override;
bool IsInitialized() const;
// TrayBackgroundView:
void ClickedOutsideBubble(const ui::LocatedEvent& event) override;
void UpdateTrayItemColor(bool is_active) override;
std::u16string GetAccessibleNameForTray() override;
void HandleLocaleChange() override;
void HideBubbleWithView(const TrayBubbleView* bubble_view) override;
void AnchorUpdated() override;
void Initialize() override;
void CloseBubbleInternal() override;
void ShowBubble() override;
TrayBubbleView* GetBubbleView() override;
views::Widget* GetBubbleWidget() const override;
void OnVirtualKeyboardVisibilityChanged() override;
bool CacheBubbleViewForHide() const override;
// TrayBubbleView::Delegate:
std::u16string GetAccessibleNameForBubble() override;
bool ShouldEnableExtraKeyboardAccessibility() override;
void HideBubble(const TrayBubbleView* bubble_view) override;
// SessionObserver:
void OnLockStateChanged(bool locked) override;
// KeyboardControllerObserver:
void OnKeyboardUIDestroyed() override;
void OnKeyboardHidden(bool is_temporary_hide) override;
// eche_app::EcheConnectionStatusHandler::Observer:
void OnConnectionStatusChanged(
eche_app::mojom::ConnectionStatus connection_status) override;
void OnRequestBackgroundConnectionAttempt() override;
// SystemTrayObserver:
void OnFocusLeavingSystemTray(bool reverse) override {}
void OnStatusAreaAnchoredBubbleVisibilityChanged(TrayBubbleView* tray_bubble,
bool visible) override;
// Callback called when the eche icon or tray button is pressed.
void OnButtonPressed();
// Sets the url that will be passed to the webview.
// Setting a new value will cause the current bubble be destroyed.
void SetUrl(const GURL& url);
// Sets the icon that will be used on the tray.
void SetIcon(const gfx::Image& icon, const std::u16string& tooltip_text);
// Reduces the size of the original icon by the `offset`. Passing a zero
// `offset` will bring the icon back to its original size.
void ResizeIcon(int offset_dip);
// Sets graceful close callback function. When close Eche Bubble, it will
// notify to Eche Web to release connection resource. Be aware that once this
// is set, close button will not call PurgeAndClose() but rely on Eche Web to
// close window when connection resource is released; if it is not set, then
// it will immediately call PurgeAndClose() to close window.
void SetGracefulCloseCallback(GracefulCloseCallback graceful_close_callback);
// Sets graceful go back callback function. When users click the ArrowBack
// button in the Eche Bubble, `graceful_go_back_callback` will notify Eche
// web content to send the GoBack key event. Be aware that once this is set,
// the ArrowBack button will call `web_view.GoBack()` and run
// `graceful_go_back_callback` together and rely on Eche web content to send
// the GoBack key event to the server when the ArrowBack button is clicked; if
// this is not set, then the ArrowBack button will immediately call
// `web_view.GoBack()` to go back the previous page.
void SetGracefulGoBackCallback(
GracefulGoBackCallback graceful_go_back_callback);
// Sets a callback that runs when the bubble is shown for the first time, and
// returns the webview.
void SetBubbleShownCallback(BubbleShownCallback bubble_shown_callback);
views::Button* GetMinimizeButtonForTesting() const;
views::Button* GetCloseButtonForTesting() const;
views::Button* GetArrowBackButtonForTesting() const;
// Initializes the bubble with given parameters. If there is any previous
// bubble already shown with a different URL it is going to be closed. The
// bubble is not shown initially until `ShowBubble` is called.
// The `url` parameter is used to load the `WebView` inside the bubble.
// The `icon` is used to update the tray icon for `EcheTray`.
// The `visible_name` is shown as a tooltip for the Eche icon.
//
// Returns true if the bubble is loaded or initialized successfully.
bool LoadBubble(const GURL& url,
const gfx::Image& icon,
const std::u16string& visible_name,
const std::u16string& phone_name,
eche_app::mojom::ConnectionStatus last_connection_status,
eche_app::mojom::AppStreamLaunchEntryPoint entry_point);
// Destroys the view inclusing the web view.
// Note: `CloseBubble` only hides the view.
void PurgeAndClose();
void HideBubble();
// Receives the `status` change when the video streaming is started or
// stopped. Controls the bubble widget based on the different `status`
// changes. There are two cases: 1. Shows the bubble when the streaming is
// started. 2. Purges and closes the bubble when the streaming is stopped.
void OnStreamStatusChanged(eche_app::mojom::StreamStatus status);
// Receives the `orientation` change when the stream switches between
// landscape and portrait.
void OnStreamOrientationChanged(bool is_landscape);
// Set up the params and init the bubble.
// Note: This function makes the bubble active and makes the
// TrayBackgroundView's background inkdrop activate.
void InitBubble(const std::u16string& phone_name,
eche_app::mojom::ConnectionStatus last_connection_status,
eche_app::mojom::AppStreamLaunchEntryPoint entry_point);
// Starts graceful close to ensure the connection resource is released before
// the window is closed.
void StartGracefulClose();
void OnBackgroundConnectionTimeout();
void SetEcheConnectionStatusHandler(
eche_app::EcheConnectionStatusHandler* eche_connection_status_handler);
bool IsBackgroundConnectionAttemptInProgress();
// Test helpers
bool get_is_landscape_for_test() { return is_landscape_; }
TrayBubbleWrapper* get_bubble_wrapper_for_test() { return bubble_.get(); }
AshWebView* get_web_view_for_test() { return web_view_; }
AshWebView* get_initializer_webview_for_test() {
return initializer_webview_.get();
}
views::ImageButton* GetIcon();
private:
FRIEND_TEST_ALL_PREFIXES(EcheTrayTest, EcheTrayCreatesBubbleButHideFirst);
FRIEND_TEST_ALL_PREFIXES(EcheTrayTest, EcheTrayOnDisplayConfigurationChanged);
FRIEND_TEST_ALL_PREFIXES(EcheTrayTest,
EcheTrayKeyboardShowHideUpdateBubbleBounds);
FRIEND_TEST_ALL_PREFIXES(EcheTrayTest, EcheTrayOnStreamOrientationChanged);
// Intercepts all the events targeted to the internal webview in order to
// process the accelerator keys.
class EventInterceptor : public ui::EventHandler {
public:
explicit EventInterceptor(EcheTray* eche_tray);
EventInterceptor(const EventInterceptor&) = delete;
EventInterceptor& operator=(const EventInterceptor&) = delete;
~EventInterceptor() override;
// ui::EventHandler:
void OnKeyEvent(ui::KeyEvent* event) override;
private:
const raw_ptr<EcheTray> eche_tray_;
};
// Calculates and returns the size of the Exo bubble based on the screen size
// and orientation.
gfx::Size CalculateSizeForEche() const;
// Handles the click on the "back" arrow in the header.
void OnArrowBackActivated();
// Creates the header of the bubble that includes a back arrow,
// close, and minimize buttons.
std::unique_ptr<views::View> CreateBubbleHeaderView(
const std::u16string& phone_name);
void StopLoadingAnimation();
void StartLoadingAnimation();
void SetIconVisibility(bool visibility);
PhoneHubTray* GetPhoneHubTray();
EcheIconLoadingIndicatorView* GetLoadingIndicator();
// Resize Eche size and update the bubble's position.
void UpdateEcheSizeAndBubbleBounds();
// ScreenLayoutObserver:
void OnDidApplyDisplayChanges() override;
// ShelfObserver:
void OnAutoHideStateChanged(ShelfAutoHideState new_state) override;
// display::DisplayObserver:
void OnDisplayTabletStateChanged(display::TabletState state) override;
// ShellObserver:
void OnShelfAlignmentChanged(aura::Window* root_window,
ShelfAlignment old_alignment) override;
// Called when the display tablet state is changed to kInTabletMode.
void OnTabletModeStarted();
// Processes the accelerator keys and returns true if the accelerator was
// processed completely in this method and no further processing is needed.
bool ProcessAcceleratorKeys(ui::KeyEvent* event);
// Returns true only if the bubble is initialized and visible.
bool IsBubbleVisible();
// Starts graceful shutdown for the initializer.
void StartGracefulCloseInitializer();
// Kills the renderer.
void CloseInitializer();
// The url that is transferred to the web view.
// In the current implementation, this is supposed to be
// Eche window URL. However, the bubble does not interpret,
// validate, or expect a special url format or page behabvior.
GURL url_;
// Icon of the tray. Unowned.
const raw_ptr<views::ImageView> icon_;
// The bubble that appears after clicking the tray button.
std::unique_ptr<TrayBubbleWrapper> bubble_;
// The webview shown in the bubble that contains the Eche SWA.
// owned by `bubble_`
raw_ptr<AshWebView> web_view_ = nullptr;
// Webview used to create a prewarming channel, before we have a video to
// attach to.
std::unique_ptr<AshWebView> initializer_webview_{};
std::unique_ptr<base::DelayTimer> initializer_timeout_{};
base::OnceClosure on_initializer_closed_;
bool has_reported_initializer_result_ = false;
bool has_retried_initializer_ = false;
raw_ptr<eche_app::EcheConnectionStatusHandler>
eche_connection_status_handler_ = nullptr;
GracefulCloseCallback graceful_close_callback_;
GracefulGoBackCallback graceful_go_back_callback_;
BubbleShownCallback bubble_shown_callback_;
// The unload timer to force close EcheTray in case unload error.
std::unique_ptr<base::DelayTimer> unload_timer_;
raw_ptr<views::View, DanglingUntriaged> header_view_ = nullptr;
raw_ptr<views::Button> close_button_ = nullptr;
raw_ptr<views::Button> minimize_button_ = nullptr;
raw_ptr<views::Button> arrow_back_button_ = nullptr;
std::unique_ptr<EventInterceptor> event_interceptor_;
// The time a stream is initializing. Used to record the elapsed time from
// when the stream is initializing to when the stream is closed by user.
std::optional<base::TimeTicks> init_stream_timestamp_;
// The orientation of the stream (portrait vs landscape). The default
// orientation is portrait.
bool is_landscape_ = false;
bool is_stream_started_ = false;
std::u16string phone_name_;
// Observers
base::ScopedObservation<SessionControllerImpl, SessionObserver>
observed_session_{this};
base::ScopedObservation<Shelf, ShelfObserver> shelf_observation_{this};
base::ScopedObservation<Shell, ShellObserver> shell_observer_{this};
base::ScopedObservation<keyboard::KeyboardUIController,
KeyboardControllerObserver>
keyboard_observation_{this};
display::ScopedDisplayObserver display_observer_{this};
base::WeakPtrFactory<EcheTray> weak_factory_{this};
};
} // namespace ash
#endif // ASH_SYSTEM_ECHE_ECHE_TRAY_H_