chromium/ash/wm/overview/birch/birch_bar_controller.cc

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/wm/overview/birch/birch_bar_controller.h"

#include "ash/birch/birch_model.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/wm/overview/birch/birch_bar_constants.h"
#include "ash/wm/overview/birch/birch_bar_context_menu_model.h"
#include "ash/wm/overview/birch/birch_bar_menu_model_adapter.h"
#include "ash/wm/overview/birch/birch_bar_util.h"
#include "ash/wm/overview/birch/birch_bar_view.h"
#include "ash/wm/overview/birch/birch_chip_context_menu_model.h"
#include "ash/wm/overview/birch/birch_privacy_nudge_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/views/controls/menu/menu_controller.h"

namespace ash {

namespace {

// Returns the pref service to use for Birch bar prefs.
PrefService* GetPrefService() {
  return Shell::Get()->session_controller()->GetPrimaryUserPrefService();
}

// Records the ranking of each item in `items` in a histogram selected based on
// the time of day. The histogram time cutoffs are chosen based on BirchRanker
// behavior.
void RecordTimeOfDayRankingHistogram(
    const std::vector<std::unique_ptr<BirchItem>>& items) {
  base::Time::Exploded exploded;
  base::Time::Now().LocalExplode(&exploded);
  const char* now_histogram = nullptr;
  if (exploded.hour < 5) {
    now_histogram = "Ash.Birch.Ranking.0000to0500";
  } else if (exploded.hour < 12) {
    now_histogram = "Ash.Birch.Ranking.0500to1200";
  } else if (exploded.hour < 17) {
    now_histogram = "Ash.Birch.Ranking.1200to1700";
  } else {
    now_histogram = "Ash.Birch.Ranking.1700to0000";
  }
  for (const auto& item : items) {
    int ranking_int = static_cast<int>(item->ranking());
    base::UmaHistogramCounts100(now_histogram, ranking_int);
    // Also record an aggregate for the day.
    base::UmaHistogramCounts100("Ash.Birch.Ranking.Total", ranking_int);
  }
}

std::string GetPrefNameFromSuggestionType(BirchSuggestionType type) {
  switch (type) {
    case BirchSuggestionType::kWeather:
      return prefs::kBirchUseWeather;
    case BirchSuggestionType::kCalendar:
      return prefs::kBirchUseCalendar;
    case BirchSuggestionType::kDrive:
      return prefs::kBirchUseFileSuggest;
    case BirchSuggestionType::kChromeTab:
      return prefs::kBirchUseChromeTabs;
    case BirchSuggestionType::kMedia:
      return prefs::kBirchUseLostMedia;
    case BirchSuggestionType::kCoral:
      return prefs::kBirchUseCoral;
    case BirchSuggestionType::kExplore:
    case BirchSuggestionType::kUndefined:
      NOTREACHED();
  }
}

}  // namespace

BirchBarController::BirchBarController(bool is_informed_restore)
    : is_informed_restore_(is_informed_restore) {
  // Init and register the show suggestions pref changed callback.
  show_suggestions_pref_registrar_.Init(GetPrefService());
  show_suggestions_pref_registrar_.Add(
      prefs::kBirchShowSuggestions,
      base::BindRepeating(&BirchBarController::OnShowSuggestionsPrefChanged,
                          base::Unretained(this)));

  // Init and register the customize suggestion pref changed callback.
  customize_suggestions_pref_registrar_.Init(GetPrefService());

  for (const auto& suggestion_pref :
       {prefs::kBirchUseCalendar, prefs::kBirchUseWeather,
        prefs::kBirchUseFileSuggest, prefs::kBirchUseChromeTabs,
        prefs::kBirchUseLostMedia, prefs::kBirchUseReleaseNotes,
        prefs::kBirchUseCoral}) {
    customize_suggestions_pref_registrar_.Add(
        suggestion_pref,
        base::BindRepeating(
            &BirchBarController::OnCustomizeSuggestionsPrefChanged,
            base::Unretained(this)));
  }

  if (GetShowBirchSuggestions()) {
    // Fetching data from model if going to show the suggestions.
    MaybeFetchDataFromModel();
  }
}

BirchBarController::~BirchBarController() {
  // Avoid dangling pointers to our `items_`.
  for (auto& bar_view : bar_views_) {
    bar_view->ShutdownChips();
  }

  // Chips are shutdown, stop observing lost media change if we did.
  OnLostMediaItemRemoved();
}

// static.
BirchBarController* BirchBarController::Get() {
  if (auto* overview_session = GetOverviewSession()) {
    return overview_session->birch_bar_controller();
  }
  return nullptr;
}

// static.
void BirchBarController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(prefs::kBirchShowSuggestions, true);
}

void BirchBarController::RegisterBar(BirchBarView* bar_view) {
  bar_views_.emplace_back(bar_view);

  // Directly initialize the bar view if data fetching is done.
  if (!IsDataLoading()) {
    InitBarWithItems(bar_view, items_);
  }
}

void BirchBarController::OnBarDestroying(BirchBarView* bar_view) {
  // Remove the bar view.
  auto iter = std::find(bar_views_.begin(), bar_views_.end(), bar_view);
  if (iter != bar_views_.end()) {
    bar_views_.erase(iter);
  }
}

void BirchBarController::ShowChipContextMenu(BirchChipButton* chip,
                                             BirchSuggestionType chip_type,
                                             const gfx::Point& point,
                                             ui::MenuSourceType source_type) {
  chip_menu_model_adapter_ = std::make_unique<BirchBarMenuModelAdapter>(
      std::make_unique<BirchChipContextMenuModel>(
          /*delegate=*/chip, chip_type),
      chip->GetWidget(), source_type,
      base::BindOnce(&BirchBarController::OnChipContextMenuClosed,
                     weak_ptr_factory_.GetWeakPtr()),
      Shell::Get()->IsInTabletMode(), /*for_chip_menu=*/true);
  BirchPrivacyNudgeController::DidShowContextMenu();
  chip_menu_model_adapter_->Run(gfx::Rect(point, gfx::Size()),
                                views::MenuAnchorPosition::kBubbleTopRight,
                                views::MenuRunner::CONTEXT_MENU |
                                    views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
                                    views::MenuRunner::FIXED_ANCHOR);
}

void BirchBarController::OnItemHiddenByUser(BirchItem* item) {
  // Do not remove the item if the bars are animating.
  if (std::ranges::any_of(bar_views_, [](BirchBarView* bar_view) {
        return bar_view->IsAnimating();
      })) {
    return;
  }

  RemoveItemChips(item);

  // Erase the item from model and controller.
  Shell::Get()->birch_model()->RemoveItem(item);
  if (item->GetType() == BirchItemType::kLostMedia) {
    OnLostMediaItemRemoved();
  }
  std::erase_if(items_, base::MatchesUniquePtr(item));
}

void BirchBarController::SetShowBirchSuggestions(bool show) {
  // Register the show suggestions option to user's pref.
  show_suggestions_pref_registrar_.prefs()->SetBoolean(
      prefs::kBirchShowSuggestions, show);
}

bool BirchBarController::GetShowBirchSuggestions() const {
  return show_suggestions_pref_registrar_.prefs()->GetBoolean(
      prefs::kBirchShowSuggestions);
}

void BirchBarController::SetShowSuggestionType(BirchSuggestionType type,
                                               bool show) {
  customize_suggestions_pref_registrar_.prefs()->SetBoolean(
      GetPrefNameFromSuggestionType(type), show);
}

bool BirchBarController::GetShowSuggestionType(BirchSuggestionType type) const {
  return customize_suggestions_pref_registrar_.prefs()->GetBoolean(
      GetPrefNameFromSuggestionType(type));
}

bool BirchBarController::IsDataLoading() const {
  return birch_model_observer_.IsObserving() || data_fetch_in_progress_;
}

void BirchBarController::ToggleTemperatureUnits() {
  // Toggle the preference.
  auto* pref_service = GetPrefService();
  bool current_value = pref_service->GetBoolean(prefs::kBirchUseCelsius);
  pref_service->SetBoolean(prefs::kBirchUseCelsius, !current_value);

  // Refresh the suggestion chips.
  for (auto& bar_view : bar_views_) {
    bar_view->SetState(BirchBarView::State::kReloading);
  }
  MaybeFetchDataFromModel();
}

void BirchBarController::ExecuteMenuCommand(int command_id, bool from_chip) {
  using CommandId = BirchBarContextMenuModel::CommandId;
  switch (command_id) {
    case base::to_underlying(CommandId::kShowSuggestions):
      // Note that the menu should be dismissed before changing the show
      // suggestions pref which may destroy the chips.
      if (auto* menu_controller = views::MenuController::GetActiveInstance()) {
        menu_controller->Cancel(views::MenuController::ExitType::kAll);
      } else if (from_chip) {
        // When tapping on the "Show suggestions" switch button, the menu
        // controller may be destroyed before executing the command. To avoid
        // UAF, reset the menu model adapter.
        chip_menu_model_adapter_.reset();
      }

      SetShowBirchSuggestions(/*show=*/!GetShowBirchSuggestions());
      break;
    case base::to_underlying(CommandId::kWeatherSuggestions):
    case base::to_underlying(CommandId::kCalendarSuggestions):
    case base::to_underlying(CommandId::kDriveSuggestions):
    case base::to_underlying(CommandId::kChromeTabSuggestions):
    case base::to_underlying(CommandId::kMediaSuggestions):
    case base::to_underlying(CommandId::kCoralSuggestions): {
      // To avoid UAF, dismiss the menu before changing the pref which
      // would destroy current chips.
      auto* menu_controller = views::MenuController::GetActiveInstance();
      if (from_chip && menu_controller) {
        menu_controller->Cancel(views::MenuController::ExitType::kAll);
      }

      const BirchSuggestionType suggestion_type =
          birch_bar_util::CommandIdToSuggestionType(command_id);
      const bool current_show_status = GetShowSuggestionType(suggestion_type);
      SetShowSuggestionType(suggestion_type, !current_show_status);
      break;
    }
    case base::to_underlying(CommandId::kReset): {
      bool suggestion_pref_changed = false;
      {
        // Holding the data fetch requests to avoid sending multiple requests.
        base::AutoReset<bool> hold_data_request(
            &hold_data_request_on_suggestion_pref_change_, true);
        auto* pref_service = GetPrefService();
        for (const auto& pref_name :
             {prefs::kBirchUseWeather, prefs::kBirchUseCalendar,
              prefs::kBirchUseFileSuggest, prefs::kBirchUseChromeTabs,
              prefs::kBirchUseLostMedia, prefs::kBirchUseCoral}) {
          suggestion_pref_changed |= !pref_service->GetBoolean(pref_name);
          pref_service->SetBoolean(pref_name, true);
        }
      }

      // If there are suggestion prefs being changed, call suggestion pref
      // changed callback.
      if (suggestion_pref_changed) {
        OnCustomizeSuggestionsPrefChanged();
      }
      break;
    }
    default:
      break;
  }
}

void BirchBarController::ExecuteCommand(int command_id, int event_flags) {
  ExecuteMenuCommand(command_id, /*from_chip=*/false);
}

void BirchBarController::MaybeFetchDataFromModel() {
  if (data_fetch_in_progress_) {
    return;
  }

  auto* birch_model = Shell::Get()->birch_model();
  if (birch_model->birch_client()) {
    // Fetching data from model.
    data_fetch_in_progress_ = true;
    birch_model->RequestBirchDataFetch(
        /*is_post_login=*/is_informed_restore_,
        base::BindOnce(&BirchBarController::OnItemsFetchedFromModel,
                       weak_ptr_factory_.GetWeakPtr()));
  } else if (!birch_model_observer_.IsObserving()) {
    // Observe birch model and wait until client is set before requesting data.
    birch_model_observer_.Observe(birch_model);
  }
}

void BirchBarController::OnItemsFetchedFromModel() {
  // When data fetching completes, use the fetched items to initialize all the
  // bar views.
  data_fetch_in_progress_ = false;

  // Cache the new items in a temp variable to avoid dangling ptrs when update
  // the birch bar.
  auto items = Shell::Get()->birch_model()->GetItemsForDisplay();

  // Record an impression if there are suggestion chips to show.
  if (!items.empty()) {
    base::UmaHistogramBoolean("Ash.Birch.Bar.Impression", true);
  }
  // Record impressions for the suggestion chips.
  for (const auto& item : items) {
    base::UmaHistogramEnumeration("Ash.Birch.Chip.Impression", item->GetType());
  }
  // Record number of chips being shown.
  base::UmaHistogramCustomCounts("Ash.Birch.ChipCount", items.size(),
                                 /*min=*/0, /*exclusive_max=*/10,
                                 /*buckets=*/10);
  RecordTimeOfDayRankingHistogram(items);

  for (auto& bar_view : bar_views_) {
    InitBarWithItems(bar_view, items);
  }

  // Clear the old lost media item if it exists.
  OnLostMediaItemRemoved();

  items_ = std::move(items);

  for (const auto& item : items_) {
    if (item->GetType() == BirchItemType::kLostMedia) {
      OnLostMediaItemReceived();
    }
  }
}

void BirchBarController::InitBarWithItems(
    BirchBarView* bar_view,
    const std::vector<std::unique_ptr<BirchItem>>& items) {
  CHECK(!data_fetch_in_progress_);

  std::vector<raw_ptr<BirchItem>> items_to_show;
  for (auto& item : items) {
    if (items_to_show.size() == BirchBarView::kMaxChipsNum) {
      break;
    }
    items_to_show.emplace_back(item.get());
  }

  bar_view->SetupChips(items_to_show);
}

void BirchBarController::RemoveItemChips(BirchItem* item) {
  // Remove the item from birch bars. If there is an extra item not showing in
  // the bars, push it in the bars.
  BirchItem* extra_item = items_.size() > BirchBarView::kMaxChipsNum
                              ? items_[BirchBarView::kMaxChipsNum].get()
                              : nullptr;
  for (auto& bar_view : bar_views_) {
    bar_view->RemoveChip(item, extra_item);
  }
}

void BirchBarController::OnChipContextMenuClosed() {
  chip_menu_model_adapter_.reset();
}

void BirchBarController::OnBirchClientSet() {
  // No longer need to observe the birch model once client is set.
  birch_model_observer_.Reset();

  // Fetching data from model.
  MaybeFetchDataFromModel();
}

void BirchBarController::OnShowSuggestionsPrefChanged() {
  const bool show = GetShowBirchSuggestions();

  auto* overview_session = GetOverviewSession();
  for (auto& root : Shell::GetAllRootWindows()) {
    auto* overview_grid = overview_session->GetGridWithRootWindow(root);
    if (show) {
      MaybeFetchDataFromModel();
      overview_grid->MaybeInitBirchBarWidget(/*by_user=*/true);
    } else {
      overview_grid->ShutdownBirchBarWidgetByUser();
    }
  }
}

void BirchBarController::OnCustomizeSuggestionsPrefChanged() {
  if (hold_data_request_on_suggestion_pref_change_) {
    return;
  }

  for (auto& bar_view : bar_views_) {
    bar_view->SetState(BirchBarView::State::kReloading);
  }

  MaybeFetchDataFromModel();
}

void BirchBarController::OnLostMediaItemReceived() {
  Shell::Get()->birch_model()->SetLostMediaDataChangedCallback(
      base::BindRepeating(&BirchBarController::OnLostMediaItemUpdated,
                          weak_ptr_factory_.GetWeakPtr()));
}

void BirchBarController::OnLostMediaItemRemoved() {
  if (auto* birch_model = Shell::Get()->birch_model()) {
    birch_model->ResetLostMediaDataChangedCallback();
  }
}

void BirchBarController::OnLostMediaItemUpdated(
    std::unique_ptr<BirchItem> updated_item) {
  auto lost_media_item =
      std::find_if(items_.begin(), items_.end(), [](const auto& item) {
        return item->GetType() == BirchItemType::kLostMedia;
      });
  if (lost_media_item == items_.end()) {
    return;
  }

  // If the lost media item is null, remove the lost media chip from bars.
  // Otherwise, update the chip with new item contents.
  if (!updated_item) {
    RemoveItemChips(lost_media_item->get());
    items_.erase(lost_media_item);
    OnLostMediaItemRemoved();
  } else {
    *(lost_media_item->get()) = *(updated_item.get());
    for (auto bar_view : bar_views_) {
      bar_view->UpdateChip(lost_media_item->get());
    }
  }
}

}  // namespace ash