chromium/chrome/browser/ash/app_list/app_list_notifier_impl.h

// 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_