// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_ASH_APP_LIST_APP_LIST_NOTIFIER_IMPL_H_
#define CHROME_BROWSER_ASH_APP_LIST_APP_LIST_NOTIFIER_IMPL_H_
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "ash/public/cpp/app_list/app_list_controller_observer.h"
#include "ash/public/cpp/app_list/app_list_notifier.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/containers/flat_map.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
namespace base {
class OneShotTimer;
}
namespace ash {
class AppListController;
}
// This AppListNotifier subclass is only used for the productivity launcher.
//
// Chrome implementation of the AppListNotifier. This is mainly responsible for
// translating notifications about launcher UI events - eg. launcher opened,
// results changed, etc. - into impression, launch, and abandon notifications
// for observers. See the header comment in app_list_notifier.h for definitions
// of these.
//
// This handles results on each search result view, which are are just called
// _views_ in this comment.
//
// State machine
// =============
//
// This class implements N state machines, one for each view, that are mostly
// independent. Each state machine can be in one of three primary states:
//
// - kNone: the view is not displayed.
// - kShown: the view is displayed.
// - kSeen: the same results on the view have been displayed for a certain
// amount of time.
//
// There are two extra 'transitional' states:
//
// - kLaunched: a user has launched a result in this surface.
// - kIgnored: a user has launched a result in another visible surface.
//
// These states exist only to simplify implementation, and the state machine
// doesn't stay in them for any length of time. Immediately after a transition
// to kLaunch or kIgnore, the launcher closes and the state is set to kNone.
//
// Various user and background events cause _transitions_ between states. The
// notifier performs _actions_ on a transition based on the (from, to) pair of
// states.
//
// Each state machine is associated with an _impression timer_ that begins on a
// transition to kShown. Once the timer finishes, it causes a transition to
// kSeen.
//
// Transitions
// ===========
//
// The events that cause a transition to a state are as follows.
//
// - kNone: closing the launcher or moving to a different view.
// - kShown: opening the relevant view, or changing the search query if the
// view is the app tiles or results list.
// - kLaunched: launching a result.
// - kSeen: the impression timer finishing for the view.
//
// Actions
// =======
//
// These actions are triggered on a state transition.
//
// From -> To | Actions
// -------------------|--------------------------------------------------------
// kNone -> kNone | No action.
// |
// kNone -> kShown | Start impression timer, as view just displayed.
// |
// kShown -> kNone | Cancel impression timer, as view changed.
// |
// kShown -> kSeen | Notify of an impression, as impression timer finished.
// |
// kShown -> kShown | Restart impression timer. Should only be triggered for
// | the list view, when the displayed results change.
// |
// |
// kSeen -> kLaunch | Notify of a launch and immediately set state to kNone,
// | as user launched a result.
// |
// kShown -> kLaunch | Notify of a launch and impression then set state to
// | kNone and cancel impression timer, as user launched
// | result so must have seen the results.
// |
// |
// kShown -> kIgnored | Notify of an ignore and impression, then set state to
// | kNone and cancel timer, as user launched a result in
// | a different view to must have seen the results.
// |
// kSeen -> kIgnored | Notify of an ignore, then set state to kNone, as user
// | launched a result in a different view.
// |
// |
// kSeen -> kNone | Notify of an abandon, as user closed the launcher.
// |
// kSeen -> kShown | Notify of an abandon and restart timer, as user saw
// | results but changed view or updated the search query.
//
// The transitions we consider impossible are kNone -> {kSeen, kLaunch},
// kSeen -> kSeen, and {kLaunch, kIgnored } -> anything, because kLaunch and
// kIgnored are temporary states.
//
// Discussion
// ==========
//
// Warning: NotifyResultsUpdated cannot be used as a signal of user actions or
// UI state. Results can be updated at any time for any UI view, regardless
// of the state of the launcher or what the user is doing.
class AppListNotifierImpl : public ash::AppListNotifier,
public ash::AppListControllerObserver {
public:
explicit AppListNotifierImpl(ash::AppListController* app_list_controller);
~AppListNotifierImpl() override;
AppListNotifierImpl(const AppListNotifierImpl&) = delete;
AppListNotifierImpl& operator=(const AppListNotifierImpl&) = delete;
// AppListNotifier:
void AddObserver(Observer* observer) override;
void RemoveObserver(Observer* observer) override;
void NotifyLaunched(Location location, const Result& result) override;
void NotifyResultsUpdated(Location location,
const std::vector<Result>& results) override;
void NotifyContinueSectionVisibilityChanged(Location location,
bool visible) override;
void NotifySearchQueryChanged(const std::u16string& query) override;
bool FireImpressionTimerForTesting(Location location) override;
// AppListControllerObserver:
void OnAppListVisibilityWillChange(bool shown, int64_t display_id) override;
void OnViewStateChanged(ash::AppListViewState state) override;
private:
// Possible states of the state machine.
enum class State {
kNone,
kShown,
kSeen,
kLaunched,
kIgnored,
};
// Performs all state transition logic.
void DoStateTransition(Location location, State new_state);
// (Re)starts the impression timer for |location|.
void RestartTimer(Location location);
// Stops the impression timer for |location|.
void StopTimer(Location location);
// Handles a finished impression timer for |location|.
void OnTimerFinished(Location location);
// Returns the stored results for |location|.
std::vector<Result> ResultsForLocation(Location location);
// Returns whether a continue section container (or recent apps container) are
// reported to be visible.
bool GetContinueSectionVisibility(Location location) const;
const raw_ptr<ash::AppListController> app_list_controller_;
base::ObserverList<Observer> observers_;
// The current state of each state machine.
base::flat_map<Location, State> states_;
// The reported visibility state of app list continue section - used for
// `Location::kContinue` and `Location::kRecentApps`, which may remain hidden
// while app list is visible.
base::flat_map<Location, bool> continue_section_visibility_;
// An impression timer for each state machine.
base::flat_map<Location, std::unique_ptr<base::OneShotTimer>> timers_;
// Whether or not the app list is shown.
bool shown_ = false;
// Whether or not a search session is in progress.
bool search_session_in_progress_ = false;
// The currently shown results for each UI view.
base::flat_map<Location, std::vector<Result>> results_;
// The current search query, may be empty.
std::u16string query_;
// The most recently launched result.
std::optional<Result> launched_result_;
// Special-case for the results at Location::kList. These need to be
// accumulated until the query changes, rather than set like other result
// types. The keys are result IDs, and the values are wrapped in an optional
// because Result is not default-constructable.
//
// TODO(crbug.com/40184658): This can be removed once SearchResultListView has
// its notifier calls updated.
base::flat_map<std::string, std::optional<Result>> list_results_;
base::WeakPtrFactory<AppListNotifierImpl> weak_ptr_factory_{this};
};
#endif // CHROME_BROWSER_ASH_APP_LIST_APP_LIST_NOTIFIER_IMPL_H_