chromium/ash/shelf/shelf_controller.cc

// Copyright 2016 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/shelf/shelf_controller.h"

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/message_center/arc_notification_constants.h"
#include "ash/public/cpp/shelf_item_delegate.h"
#include "ash/public/cpp/shelf_prefs.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/launcher_nudge_controller.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/display/display.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"

namespace ash {

namespace {

// Returns the Shelf instance for the display with the given |display_id|.
Shelf* GetShelfForDisplay(int64_t display_id) {
  // The controller may be null for invalid ids or for displays being removed.
  RootWindowController* root_window_controller =
      Shell::GetRootWindowControllerWithDisplayId(display_id);
  return root_window_controller ? root_window_controller->shelf() : nullptr;
}

// Set each Shelf's auto-hide behavior from the per-display pref.
void SetShelfAutoHideFromPrefs() {
  // TODO(jamescook): The session state check should not be necessary, but
  // otherwise this wrongly tries to set the alignment on a secondary display
  // during login before the ShelfLockingManager is created.
  SessionControllerImpl* session_controller =
      Shell::Get()->session_controller();
  PrefService* prefs = session_controller->GetLastActiveUserPrefService();
  if (!prefs || !session_controller->IsActiveUserSessionStarted())
    return;

  for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
    auto value = GetShelfAutoHideBehaviorPref(prefs, display.id());
    // Don't show the shelf in app mode.
    if (session_controller->IsRunningInAppMode())
      value = ShelfAutoHideBehavior::kAlwaysHidden;
    if (Shelf* shelf = GetShelfForDisplay(display.id()))
      shelf->SetAutoHideBehavior(value);
  }
}

// Set each Shelf's alignment from the per-display pref.
void SetShelfAlignmentFromPrefs() {
  // TODO(jamescook): The session state check should not be necessary, but
  // otherwise this wrongly tries to set the alignment on a secondary display
  // during login before the ShelfLockingManager is created.
  SessionControllerImpl* session_controller =
      Shell::Get()->session_controller();
  PrefService* prefs = session_controller->GetLastActiveUserPrefService();
  if (!prefs || !session_controller->IsActiveUserSessionStarted())
    return;

  // Tablet mode uses bottom aligned shelf, don't override it if the shelf
  // prefs change.
  if (display::Screen::GetScreen()->InTabletMode()) {
    return;
  }

  for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
    if (Shelf* shelf = GetShelfForDisplay(display.id()))
      shelf->SetAlignment(GetShelfAlignmentPref(prefs, display.id()));
  }
}

// Re-layouts the shelf on every display.
void LayoutShelves() {
  for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
    if (Shelf* shelf = GetShelfForDisplay(display.id())) {
      shelf->shelf_layout_manager()->LayoutShelf(true);
    }
  }
}

void UpdateShelfVisibility() {
  for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
    if (Shelf* shelf = GetShelfForDisplay(display.id()))
      shelf->UpdateVisibilityState();
  }
}

// Set each Shelf's auto-hide behavior and alignment from the per-display prefs.
void SetShelfBehaviorsFromPrefs() {
  SetShelfAutoHideFromPrefs();

  // The shelf should always be bottom-aligned in tablet mode; alignment is
  // assigned from prefs when tablet mode is exited.
  if (display::Screen::GetScreen()->InTabletMode()) {
    return;
  }

  SetShelfAlignmentFromPrefs();
}

}  // namespace

ShelfController::ShelfController() {
  ShelfModel::SetInstance(&model_);

  Shell::Get()->session_controller()->AddObserver(this);
  Shell::Get()->display_manager()->AddDisplayManagerObserver(this);
  model_.AddObserver(this);
}

ShelfController::~ShelfController() {
  model_.DestroyItemDelegates();
}

void ShelfController::Init() {
  launcher_nudge_controller_ = std::make_unique<LauncherNudgeController>();
}

void ShelfController::Shutdown() {
  model_.RemoveObserver(this);
  Shell::Get()->display_manager()->RemoveDisplayManagerObserver(this);
  Shell::Get()->session_controller()->RemoveObserver(this);
}

// static
void ShelfController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  // These prefs are public for ChromeShelfController's OnIsSyncingChanged.
  // See the pref names definitions for explanations of the synced, local, and
  // per-display behaviors.
  registry->RegisterStringPref(
      prefs::kShelfAutoHideBehavior, kShelfAutoHideBehaviorNever,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
  registry->RegisterStringPref(prefs::kShelfAutoHideBehaviorLocal,
                               std::string());
  if (base::FeatureList::IsEnabled(features::kShelfAutoHideSeparation)) {
    registry->RegisterStringPref(
        prefs::kShelfAutoHideTabletModeBehavior, kShelfAutoHideBehaviorNever,
        user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
    registry->RegisterStringPref(prefs::kShelfAutoHideTabletModeBehaviorLocal,
                                 std::string());
  }
  if (base::FeatureList::IsEnabled(features::kDeskButton)) {
    registry->RegisterStringPref(
        prefs::kShowDeskButtonInShelf, std::string(),
        user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
    registry->RegisterBooleanPref(prefs::kDeviceUsesDesks, false);
  }
  registry->RegisterStringPref(
      prefs::kShelfAlignment, kShelfAlignmentBottom,
      user_prefs::PrefRegistrySyncable::SYNCABLE_OS_PREF);
  registry->RegisterStringPref(prefs::kShelfAlignmentLocal, std::string());
  registry->RegisterDictionaryPref(prefs::kShelfPreferences);

  LauncherNudgeController::RegisterProfilePrefs(registry);
}

void ShelfController::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  SetShelfBehaviorsFromPrefs();
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(pref_service);
  pref_change_registrar_->Add(prefs::kShelfAlignmentLocal,
                              base::BindRepeating(&SetShelfAlignmentFromPrefs));
  pref_change_registrar_->Add(prefs::kShelfAutoHideBehaviorLocal,
                              base::BindRepeating(&SetShelfAutoHideFromPrefs));
  if (base::FeatureList::IsEnabled(features::kShelfAutoHideSeparation)) {
    pref_change_registrar_->Add(
        prefs::kShelfAutoHideTabletModeBehaviorLocal,
        base::BindRepeating(&SetShelfAutoHideFromPrefs));
  }
  if (base::FeatureList::IsEnabled(features::kDeskButton)) {
    pref_change_registrar_->Add(prefs::kShowDeskButtonInShelf,
                                base::BindRepeating(&LayoutShelves));
    pref_change_registrar_->Add(prefs::kDeviceUsesDesks,
                                base::BindRepeating(&LayoutShelves));
  }
  pref_change_registrar_->Add(prefs::kShelfPreferences,
                              base::BindRepeating(&SetShelfBehaviorsFromPrefs));

  pref_change_registrar_->Add(
      prefs::kAppNotificationBadgingEnabled,
      base::BindRepeating(&ShelfController::UpdateAppNotificationBadging,
                          base::Unretained(this)));

  // Observe AppRegistryCache for the current active account to get
  // notification updates.
  AccountId account_id =
      Shell::Get()->session_controller()->GetActiveAccountId();
  cache_ = apps::AppRegistryCacheWrapper::Get().GetAppRegistryCache(account_id);

  app_registry_cache_observer_.Reset();
  if (cache_) {
    app_registry_cache_observer_.Observe(cache_);
  }

  // Resetting the recorded pref forces the next call to
  // UpdateAppNotificationBadging() to update notification badging for every
  // app item.
  notification_badging_pref_enabled_.reset();

  // Update the notification badge indicator for all apps. This will also
  // ensure that apps have the correct notification badge value for the
  // multiprofile case when switching between users.
  UpdateAppNotificationBadging();
}

void ShelfController::OnDisplayTabletStateChanged(display::TabletState state) {
  // Do nothing when running in app mode.
  if (Shell::Get()->session_controller()->IsRunningInAppMode())
    return;

  switch (state) {
    case display::TabletState::kEnteringTabletMode:
    case display::TabletState::kExitingTabletMode:
      // Do nothing when the tablet state is in the process of transition.
      break;
    case display::TabletState::kInTabletMode:
      if (base::FeatureList::IsEnabled(features::kShelfAutoHideSeparation)) {
        SetShelfAutoHideFromPrefs();
      }

      // Force the shelf to be bottom aligned in tablet mode; the prefs are
      // restored on exit.
      for (const auto& display :
           display::Screen::GetScreen()->GetAllDisplays()) {
        if (Shelf* shelf = GetShelfForDisplay(display.id())) {
          shelf->SetAlignment(ShelfAlignment::kBottom);
        }
      }
      break;
    case display::TabletState::kInClamshellMode:
      SetShelfBehaviorsFromPrefs();
      break;
  }
}

void ShelfController::OnDidApplyDisplayChanges() {
  // Update the alignment and auto-hide state from prefs, because a display may
  // have been added, or the display ids for existing shelf instances may have
  // changed. See https://crbug.com/748291
  SetShelfBehaviorsFromPrefs();

  // Update shelf visibility to adapt to display changes. For instance shelf
  // should be hidden on secondary display during inactive session states.
  UpdateShelfVisibility();
}

void ShelfController::OnAppUpdate(const apps::AppUpdate& update) {
  if (update.HasBadgeChanged() &&
      notification_badging_pref_enabled_.value_or(false)) {
    bool has_badge = update.HasBadge().value_or(false);
    model_.UpdateItemNotification(update.AppId(), has_badge);
  }
}

void ShelfController::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_cache_observer_.Reset();
}

void ShelfController::ShelfItemAdded(int index) {
  if (!cache_ || !notification_badging_pref_enabled_.value_or(false))
    return;

  auto app_id = model_.items()[index].id.app_id;

  // Update the notification badge indicator for the newly added shelf item.
  cache_->ForOneApp(app_id, [this](const apps::AppUpdate& update) {
    bool has_badge = update.HasBadge().value_or(false);
    model_.UpdateItemNotification(update.AppId(), has_badge);
  });
}

void ShelfController::UpdateAppNotificationBadging() {
  bool new_badging_enabled = pref_change_registrar_
                                 ? pref_change_registrar_->prefs()->GetBoolean(
                                       prefs::kAppNotificationBadgingEnabled)
                                 : false;

  if (notification_badging_pref_enabled_.has_value() &&
      notification_badging_pref_enabled_.value() == new_badging_enabled) {
    return;
  }
  notification_badging_pref_enabled_ = new_badging_enabled;

  if (cache_) {
    cache_->ForEachApp([this](const apps::AppUpdate& update) {
      // Set the app notification badge hidden when the pref is disabled.
      bool has_badge = notification_badging_pref_enabled_.value()
                           ? update.HasBadge().value_or(false)
                           : false;

      model_.UpdateItemNotification(update.AppId(), has_badge);
    });
  }
}

}  // namespace ash