chromium/chrome/browser/ash/app_list/app_service/app_service_app_item.cc

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

#include "chrome/browser/ash/app_list/app_service/app_service_app_item.h"

#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/shelf_item_delegate.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/public/cpp/shelf_types.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/apps/app_service/package_id_util.h"
#include "chrome/browser/ash/app_list/app_list_controller_delegate.h"
#include "chrome/browser/ash/app_list/app_service/app_service_context_menu.h"
#include "chrome/browser/ash/app_list/apps_collections_util.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/remote_apps/remote_apps_manager.h"
#include "chrome/browser/ash/remote_apps/remote_apps_manager_factory.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "ui/display/screen.h"

namespace {

// Parameters used by the time duration metrics.
constexpr base::TimeDelta kTimeMetricsMin = base::Seconds(1);
constexpr base::TimeDelta kTimeMetricsMax = base::Days(1);
constexpr int kTimeMetricsBucketCount = 100;

// Returns true if `app_update` should be considered a new app install.
bool IsNewInstall(const apps::AppUpdate& app_update) {
  // Ignore internally-installed apps.
  if (app_update.InstalledInternally())
    return false;

  switch (app_update.AppType()) {
    case apps::AppType::kUnknown:
    case apps::AppType::kBuiltIn:
    case apps::AppType::kStandaloneBrowser:
    case apps::AppType::kSystemWeb:
    case apps::AppType::kRemote:
      // Chrome, Lacros, Settings, etc. are built-in.
      return false;
    case apps::AppType::kArc:
    case apps::AppType::kCrostini:
    case apps::AppType::kChromeApp:
    case apps::AppType::kExtension:
    case apps::AppType::kWeb:
    case apps::AppType::kPluginVm:
    case apps::AppType::kBorealis:
    case apps::AppType::kBruschetta:
    case apps::AppType::kStandaloneBrowserChromeApp:
    case apps::AppType::kStandaloneBrowserExtension:
      // Other app types are user-installed.
      return true;
  }
}

}  // namespace

// static
const char AppServiceAppItem::kItemType[] = "AppServiceAppItem";

AppServiceAppItem::AppServiceAppItem(
    Profile* profile,
    AppListModelUpdater* model_updater,
    const app_list::AppListSyncableService::SyncItem* sync_item,
    const apps::AppUpdate& app_update)
    : ChromeAppListItem(profile, app_update.AppId()),
      app_type_(app_update.AppType()),
      creation_time_(base::TimeTicks::Now()) {
  OnAppUpdate(app_update, /*in_constructor=*/true);
  if (sync_item && sync_item->item_ordinal.IsValid()) {
    InitFromSync(sync_item);
  } else {
    // Handle the case that the app under construction is a remote app.
    if (app_type_ == apps::AppType::kRemote) {
      SetIsEphemeral(true);
      ash::RemoteAppsManager* remote_manager =
          ash::RemoteAppsManagerFactory::GetForProfile(profile);
      if (remote_manager->ShouldAddToFront(app_update.AppId())) {
        SetPosition(model_updater->GetPositionBeforeFirstItem());
      } else {
        // Add the app at the end of the app list to preserve behavior from
        // before productivity launcher, so the positions in which remote apps
        // are added are consistent with old launcher order (which may be
        // assumed by extensions using remote apps API).
        SetPosition(model_updater->GetFirstAvailablePosition());
      }

      const ash::RemoteAppsModel::AppInfo* app_info =
          remote_manager->GetAppInfo(app_update.AppId());
      if (app_info)
        SetChromeFolderId(app_info->folder_id);
    }

    // Crostini and Bruschetta apps start in their respective folders.
    if (app_type_ == apps::AppType::kCrostini) {
      DCHECK(folder_id().empty());
      SetChromeFolderId(ash::kCrostiniFolderId);
    } else if (app_type_ == apps::AppType::kBruschetta) {
      DCHECK(folder_id().empty());
      SetChromeFolderId(ash::kBruschettaFolderId);
    }
  }

  std::optional<apps::PackageId> package_id =
      apps_util::GetPackageIdForApp(profile, app_update);
  if (package_id.has_value()) {
    SetPromisePackageId(package_id.value().ToString());
  }

  SetCollectionId(apps_util::GetCollectionIdForAppId(app_update.AppId()));

  const bool is_new_install =
      (!sync_item || sync_item->is_new) && IsNewInstall(app_update);
  DVLOG(1) << "New AppServiceAppItem is_new_install " << is_new_install
           << " from update " << app_update;
  SetIsNewInstall(is_new_install);

  // Set model updater last to avoid being called during construction.
  set_model_updater(model_updater);
}

AppServiceAppItem::~AppServiceAppItem() = default;

void AppServiceAppItem::OnAppUpdate(const apps::AppUpdate& app_update) {
  OnAppUpdate(app_update, /*in_constructor=*/false);
}

void AppServiceAppItem::OnAppUpdate(const apps::AppUpdate& app_update,
                                    bool in_constructor) {
  if (in_constructor || app_update.ShortNameChanged()) {
    // We display the short name rather than the full name here since
    // each launcher item only has a limited space.
    SetName(app_update.ShortName());
  }

  if (in_constructor || app_update.IconKeyChanged()) {
    // Increment icon version to indicate icon needs to be updated.
    IncrementIconVersion();
  }

  if (in_constructor || app_update.IsPlatformAppChanged()) {
    is_platform_app_ = app_update.IsPlatformApp().value_or(false);
  }

  if (in_constructor || app_update.ReadinessChanged() ||
      app_update.PausedChanged()) {
    if (apps_util::IsDisabled(app_update.Readiness())) {
      SetAppStatus(ash::AppStatus::kBlocked);
    } else if (app_update.Paused().value_or(false)) {
      SetAppStatus(ash::AppStatus::kPaused);
    } else {
      SetAppStatus(ash::AppStatus::kReady);
    }
  }
}

void AppServiceAppItem::ExecuteLaunchCommand(int event_flags) {
  Launch(event_flags, apps::LaunchSource::kFromAppListGridContextMenu);

  // TODO(crbug.com/40569217): drop the if, and call MaybeDismissAppList
  // unconditionally?
  if (app_type_ == apps::AppType::kArc || app_type_ == apps::AppType::kRemote) {
    MaybeDismissAppList();
  }
}

void AppServiceAppItem::LoadIcon() {
  constexpr bool allow_placeholder_icon = true;
  CallLoadIcon(allow_placeholder_icon);
}

void AppServiceAppItem::Activate(int event_flags) {
  // For Crostini apps, non-platform Chrome apps, Web apps, it could be
  // selecting an existing delegate for the app, so call
  // ChromeShelfController's ActivateApp interface. Platform apps or ARC
  // apps, Crostini apps treat activations as a launch. The app can decide
  // whether to show a new window or focus an existing window as it sees fit.
  //
  // TODO(crbug.com/40106663): Move the Chrome special case to ExtensionApps,
  // when AppService Instance feature is done.
  bool is_active_app = false;
  apps::AppServiceProxyFactory::GetForProfile(profile())
      ->AppRegistryCache()
      .ForOneApp(id(), [&is_active_app](const apps::AppUpdate& update) {
        if (update.AppType() == apps::AppType::kCrostini ||
            update.AppType() == apps::AppType::kWeb ||
            update.AppType() == apps::AppType::kSystemWeb ||
            (update.AppType() == apps::AppType::kStandaloneBrowserChromeApp &&
             !update.IsPlatformApp().value_or(true)) ||
            (update.AppType() == apps::AppType::kChromeApp &&
             update.IsPlatformApp().value_or(true))) {
          is_active_app = true;
        }
      });
  if (is_active_app) {
    ash::ShelfID shelf_id(id());
    ash::ShelfModel* model = ChromeShelfController::instance()->shelf_model();
    ash::ShelfItemDelegate* delegate = model->GetShelfItemDelegate(shelf_id);
    if (delegate) {
      ResetIsNewInstall();
      delegate->ItemSelected(
          /*event=*/nullptr, GetController()->GetAppListDisplayId(),
          ash::LAUNCH_FROM_APP_LIST, /*callback=*/base::DoNothing(),
          /*filter_predicate=*/base::NullCallback());
      return;
    }
  }
  Launch(event_flags, apps::LaunchSource::kFromAppListGrid);
}

const char* AppServiceAppItem::GetItemType() const {
  return AppServiceAppItem::kItemType;
}

void AppServiceAppItem::GetContextMenuModel(
    ash::AppListItemContext item_context,
    GetMenuModelCallback callback) {
  context_menu_ = std::make_unique<AppServiceContextMenu>(
      this, profile(), id(), GetController(), item_context);

  context_menu_->GetMenuModel(std::move(callback));
}

app_list::AppContextMenu* AppServiceAppItem::GetAppContextMenu() {
  return context_menu_.get();
}

void AppServiceAppItem::ResetIsNewInstall() {
  if (!is_new_install())
    return;
  SetIsNewInstall(false);

  // Record metric for approximate time from installation to launch.
  base::TimeDelta time_since_install = base::TimeTicks::Now() - creation_time_;
  if (display::Screen::GetScreen()->InTabletMode()) {
    base::UmaHistogramCustomTimes(
        "Apps.TimeBetweenAppInstallAndLaunch.TabletMode", time_since_install,
        kTimeMetricsMin, kTimeMetricsMax, kTimeMetricsBucketCount);
  } else {
    base::UmaHistogramCustomTimes(
        "Apps.TimeBetweenAppInstallAndLaunch.ClamshellMode", time_since_install,
        kTimeMetricsMin, kTimeMetricsMax, kTimeMetricsBucketCount);
  }
}

void AppServiceAppItem::Launch(int event_flags,
                               apps::LaunchSource launch_source) {
  ResetIsNewInstall();
  apps::AppServiceProxyFactory::GetForProfile(profile())->Launch(
      id(), event_flags, launch_source,
      std::make_unique<apps::WindowInfo>(
          GetController()->GetAppListDisplayId()));
}

void AppServiceAppItem::CallLoadIcon(bool allow_placeholder_icon) {
  apps::AppServiceProxyFactory::GetForProfile(profile())->LoadIcon(
      id(), apps::IconType::kStandard,
      ash::SharedAppListConfig::instance().default_grid_icon_dimension(),
      allow_placeholder_icon,
      base::BindOnce(&AppServiceAppItem::OnLoadIcon,
                     weak_ptr_factory_.GetWeakPtr()));
}

void AppServiceAppItem::OnLoadIcon(apps::IconValuePtr icon_value) {
  if (!icon_value || icon_value->icon_type != apps::IconType::kStandard) {
    return;
  }
  SetIcon(icon_value->uncompressed, icon_value->is_placeholder_icon);

  if (icon_value->is_placeholder_icon) {
    constexpr bool allow_placeholder_icon = false;
    CallLoadIcon(allow_placeholder_icon);
  }
}