chromium/chrome/browser/apps/app_service/publishers/extension_apps_chromeos.cc

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

#include "chrome/browser/apps/app_service/publishers/extension_apps_chromeos.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_metrics.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/multi_user_window_manager.h"
#include "ash/public/cpp/shelf_types.h"
#include "base/containers/contains.h"
#include "base/containers/extend.h"
#include "base/feature_list.h"
#include "base/files/safe_base_name.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_macros.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/extension_apps_utils.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/apps/app_service/launch_result_type.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/apps/app_service/menu_util.h"
#include "chrome/browser/apps/app_service/metrics/app_service_metrics.h"
#include "chrome/browser/apps/app_service/publishers/extension_apps_util.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/app_list/extension_app_utils.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_time_limit_interface.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/crosapi/hosted_app_util.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/extensions/gfx_utils.h"
#include "chrome/browser/ash/file_manager/app_id.h"
#include "chrome/browser/ash/file_manager/file_browser_handlers.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chromeos/arc/arc_web_contents_data.h"
#include "chrome/browser/chromeos/extensions/web_file_handlers/intent_util.h"
#include "chrome/browser/extensions/extension_keeplist_chromeos.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_uninstall_dialog.h"
#include "chrome/browser/extensions/extension_util.h"
#include "chrome/browser/extensions/launch_util.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/policy/system_features_disable_list_policy_handler.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_window_manager_helper.h"
#include "chrome/browser/ui/ash/session/session_controller_client_impl.h"
#include "chrome/browser/web_applications/app_service/publisher_helper.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_tab_helper.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/extensions/api/file_browser_handlers/file_browser_handler.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/extensions/extension_metrics.h"
#include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/app_constants/constants.h"
#include "components/app_restore/app_launch_info.h"
#include "components/app_restore/full_restore_utils.h"
#include "components/policy/core/common/policy_pref_names.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/services/app_service/public/cpp/instance.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_filter.h"
#include "components/services/app_service/public/cpp/intent_filter_util.h"
#include "content/public/browser/clear_site_data_utils.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/management_policy.h"
#include "extensions/browser/path_util.h"
#include "extensions/browser/ui_util.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/manifest_handlers/app_display_info.h"
#include "extensions/common/manifest_handlers/file_handler_info.h"
#include "extensions/common/manifest_handlers/options_page_info.h"
#include "extensions/common/manifest_handlers/web_file_handlers_info.h"
#include "net/base/url_util.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/message_center/public/cpp/notification.h"

namespace {

// Get the LaunchId for a given |app_window|. Set launch_id default value to an
// empty string. If show_in_shelf parameter is true and the window key is not
// empty, its value is appended to the launch_id. Otherwise, if the window key
// is empty, the session_id is used.
std::string GetLaunchId(extensions::AppWindow* app_window) {
  std::string launch_id;
  if (app_window->show_in_shelf()) {
    if (!app_window->window_key().empty()) {
      launch_id = app_window->window_key();
    } else {
      launch_id = base::StringPrintf("%d", app_window->session_id().id());
    }
  }
  return launch_id;
}

std::string GetSourceFromAppListSource(ash::ShelfLaunchSource source) {
  switch (source) {
    case ash::LAUNCH_FROM_APP_LIST:
      return std::string(extension_urls::kLaunchSourceAppList);
    case ash::LAUNCH_FROM_APP_LIST_SEARCH:
      return std::string(extension_urls::kLaunchSourceAppListSearch);
    default:
      return std::string();
  }
}

ash::ShelfLaunchSource ConvertLaunchSource(apps::LaunchSource launch_source) {
  switch (launch_source) {
    case apps::LaunchSource::kUnknown:
    case apps::LaunchSource::kFromParentalControls:
      return ash::LAUNCH_FROM_UNKNOWN;
    case apps::LaunchSource::kFromAppListGrid:
    case apps::LaunchSource::kFromAppListGridContextMenu:
      return ash::LAUNCH_FROM_APP_LIST;
    case apps::LaunchSource::kFromAppListQuery:
    case apps::LaunchSource::kFromAppListQueryContextMenu:
    case apps::LaunchSource::kFromAppListRecommendation:
      return ash::LAUNCH_FROM_APP_LIST_SEARCH;
    case apps::LaunchSource::kFromShelf:
      return ash::LAUNCH_FROM_SHELF;
    case apps::LaunchSource::kFromFileManager:
    case apps::LaunchSource::kFromLink:
    case apps::LaunchSource::kFromOmnibox:
    case apps::LaunchSource::kFromChromeInternal:
    case apps::LaunchSource::kFromKeyboard:
    case apps::LaunchSource::kFromOtherApp:
    case apps::LaunchSource::kFromMenu:
    case apps::LaunchSource::kFromInstalledNotification:
    case apps::LaunchSource::kFromTest:
    case apps::LaunchSource::kFromArc:
    case apps::LaunchSource::kFromSharesheet:
    case apps::LaunchSource::kFromReleaseNotesNotification:
    case apps::LaunchSource::kFromFullRestore:
    case apps::LaunchSource::kFromSmartTextContextMenu:
    case apps::LaunchSource::kFromDiscoverTabNotification:
    case apps::LaunchSource::kFromManagementApi:
    case apps::LaunchSource::kFromKiosk:
    case apps::LaunchSource::kFromCommandLine:
    case apps::LaunchSource::kFromBackgroundMode:
    case apps::LaunchSource::kFromNewTabPage:
    case apps::LaunchSource::kFromIntentUrl:
    case apps::LaunchSource::kFromOsLogin:
    case apps::LaunchSource::kFromProtocolHandler:
    case apps::LaunchSource::kFromUrlHandler:
    case apps::LaunchSource::kFromLockScreen:
    case apps::LaunchSource::kFromAppHomePage:
    case apps::LaunchSource::kFromReparenting:
    case apps::LaunchSource::kFromProfileMenu:
    case apps::LaunchSource::kFromSysTrayCalendar:
    case apps::LaunchSource::kFromInstaller:
    case apps::LaunchSource::kFromFirstRun:
    case apps::LaunchSource::kFromWelcomeTour:
    case apps::LaunchSource::kFromFocusMode:
    case apps::LaunchSource::kFromSparky:
      return ash::LAUNCH_FROM_UNKNOWN;
  }
}

void MaybeAssociateWebContentsWithArcContext(
    apps::LaunchSource launch_source,
    content::WebContents* web_contents) {
  if (launch_source == apps::LaunchSource::kFromArc && web_contents) {
    // Add a flag to remember this web_contents originated in the ARC context.
    web_contents->SetUserData(
        &arc::ArcWebContentsData::kArcTransitionFlag,
        std::make_unique<arc::ArcWebContentsData>(web_contents));
  }
}
}  // namespace

namespace apps {

ExtensionAppsChromeOs::ExtensionAppsChromeOs(AppServiceProxy* proxy,
                                             AppType app_type)
    : ExtensionAppsBase(proxy, app_type),
      instance_registry_(&proxy->InstanceRegistry()),
      web_file_handlers_permission_handler_(
          std::make_unique<extensions::WebFileHandlersPermissionHandler>(
              profile())) {
  DCHECK(instance_registry_);
}

ExtensionAppsChromeOs::~ExtensionAppsChromeOs() {
  app_window_registry_.Reset();

  // In unit tests, AppServiceProxy might be ReinitializeForTesting, so
  // ExtensionApps might be destroyed without calling Shutdown, so arc_prefs_
  // needs to be removed from observer in the destructor function.
  if (arc_prefs_) {
    arc_prefs_->RemoveObserver(this);
    arc_prefs_ = nullptr;
  }
}

void ExtensionAppsChromeOs::Shutdown() {
  if (arc_prefs_) {
    arc_prefs_->RemoveObserver(this);
    arc_prefs_ = nullptr;
  }
}

void ExtensionAppsChromeOs::ObserveArc() {
  // Observe the ARC apps to set the badge on the equivalent Chrome app's icon.
  if (arc_prefs_) {
    arc_prefs_->RemoveObserver(this);
  }

  arc_prefs_ = ArcAppListPrefs::Get(profile());
  if (arc_prefs_) {
    arc_prefs_->AddObserver(this);
  }
}

void ExtensionAppsChromeOs::Initialize() {
  ExtensionAppsBase::Initialize();

  app_window_registry_.Observe(extensions::AppWindowRegistry::Get(profile()));

  if (app_type() == AppType::kExtension) {
    return;
  }

  media_dispatcher_.Observe(MediaCaptureDevicesDispatcher::GetInstance()
                                ->GetMediaStreamCaptureIndicator()
                                .get());

  // NotificationDisplayService could be null in some tests.
  if (auto* notification_display_service =
          NotificationDisplayServiceFactory::GetForProfile(profile())) {
    notification_display_service_.Observe(notification_display_service);
  }

  profile_pref_change_registrar_.Init(profile()->GetPrefs());
  profile_pref_change_registrar_.Add(
      policy::policy_prefs::kHideWebStoreIcon,
      base::BindRepeating(&ExtensionAppsBase::OnHideWebStoreIconPrefChanged,
                          GetWeakPtr()));

  auto* local_state = g_browser_process->local_state();
  if (local_state) {
    local_state_pref_change_registrar_.Init(local_state);
    local_state_pref_change_registrar_.Add(
        policy::policy_prefs::kSystemFeaturesDisableList,
        base::BindRepeating(&ExtensionAppsBase::OnSystemFeaturesPrefChanged,
                            GetWeakPtr()));
    local_state_pref_change_registrar_.Add(
        policy::policy_prefs::kSystemFeaturesDisableMode,
        base::BindRepeating(&ExtensionAppsBase::OnSystemFeaturesPrefChanged,
                            GetWeakPtr()));
    OnSystemFeaturesPrefChanged();
  }
}

#if BUILDFLAG(IS_CHROMEOS_ASH)
void ExtensionAppsChromeOs::GetCompressedIconData(
    const std::string& app_id,
    int32_t size_in_dip,
    ui::ResourceScaleFactor scale_factor,
    LoadIconCallback callback) {
  apps::GetChromeAppCompressedIconData(profile(), app_id, size_in_dip,
                                       scale_factor, std::move(callback));
}
#endif

void ExtensionAppsChromeOs::LaunchAppWithParamsImpl(AppLaunchParams&& params,
                                                    LaunchCallback callback) {
  const auto* extension = MaybeGetExtension(params.app_id);

  if (params.launch_files.empty() && !params.intent) {
    LaunchImpl(std::move(params));
    return;
  }

  bool is_quickoffice = extension_misc::IsQuickOfficeExtension(extension->id());
  if (extension->is_app() || is_quickoffice) {
    auto launch_source = params.launch_source;
    content::WebContents* web_contents = LaunchImpl(std::move(params));
    MaybeAssociateWebContentsWithArcContext(launch_source, web_contents);
  } else {
    DCHECK(extension->is_extension());
    // TODO(petermarshall): Set Arc flag as above?
    auto event_flags = apps::GetEventFlags(params.disposition,
                                           /*prefer_container=*/false);
    LaunchExtension(params.app_id, event_flags, std::move(params.intent),
                    params.launch_source,
                    std::make_unique<WindowInfo>(params.display_id),
                    base::DoNothing());
  }
}

void ExtensionAppsChromeOs::LaunchAppWithArgumentsCallback(
    LaunchSource launch_source,
    const std::string& app_id,
    int32_t event_flags,
    IntentPtr intent,
    WindowInfoPtr window_info,
    LaunchCallback callback,
    bool should_open) {
  // Exit early, while notifying, in case `Don't open` was chosen.
  if (!should_open) {
    std::move(callback).Run(LaunchResult(State::kFailed));
    return;
  }

  content::WebContents* web_contents = LaunchAppWithIntentImpl(
      app_id, event_flags, std::move(intent), launch_source,
      std::move(window_info), std::move(callback));
  MaybeAssociateWebContentsWithArcContext(launch_source, web_contents);
}

void ExtensionAppsChromeOs::LaunchAppWithIntent(const std::string& app_id,
                                                int32_t event_flags,
                                                IntentPtr intent,
                                                LaunchSource launch_source,
                                                WindowInfoPtr window_info,
                                                LaunchCallback callback) {
  // `extension` is required.
  const auto* extension = MaybeGetExtension(app_id);
  if (!extension) {
    std::move(callback).Run(LaunchResult(State::kFailed));
    return;
  }

  // Launch Web File Handlers if they're supported by the extension.
  if (extensions::WebFileHandlers::SupportsWebFileHandlers(*extension)) {
    std::vector<base::SafeBaseName> base_names =
        extensions::GetBaseNamesForIntent(*intent);

    // This vector cannot be empty because this is reached after explicitly
    // opening one or more files.
    if (base_names.empty()) {
      std::move(callback).Run(LaunchResult(State::kFailed));
      return;
    }

    // Confirm that the extension can open the file and then call the callback.
    web_file_handlers_permission_handler_->Confirm(
        *extension, base_names,
        base::BindOnce(&ExtensionAppsChromeOs::LaunchAppWithArgumentsCallback,
                       weak_factory_.GetWeakPtr(), launch_source, app_id,
                       event_flags, std::move(intent), std::move(window_info),
                       std::move(callback)));

    return;
  }

  bool is_quickoffice = extension_misc::IsQuickOfficeExtension(extension->id());

  // Launch legacy app.
  if (extension->is_app() || is_quickoffice) {
    content::WebContents* web_contents = LaunchAppWithIntentImpl(
        app_id, event_flags, std::move(intent), launch_source,
        std::move(window_info), std::move(callback));

    MaybeAssociateWebContentsWithArcContext(launch_source, web_contents);
    return;
  }

  // Launch extension.
  DCHECK(extension->is_extension());
  // TODO(petermarshall): Set Arc flag as above?
  LaunchExtension(app_id, event_flags, std::move(intent), launch_source,
                  std::move(window_info), std::move(callback));
}

void ExtensionAppsChromeOs::GetMenuModel(
    const std::string& app_id,
    MenuType menu_type,
    int64_t display_id,
    base::OnceCallback<void(MenuItems)> callback) {
  MenuItems menu_items;
  const auto* extension = MaybeGetExtension(app_id);
  if (!extension) {
    std::move(callback).Run(std::move(menu_items));
    return;
  }

  if (app_id == app_constants::kChromeAppId) {
    std::move(callback).Run(CreateBrowserMenuItems(profile()));
    return;
  }

  bool is_platform_app = extension->is_platform_app();
  if (!is_platform_app) {
    CreateOpenNewSubmenu(
        extensions::GetLaunchType(extensions::ExtensionPrefs::Get(profile()),
                                  extension) ==
                extensions::LaunchType::LAUNCH_TYPE_WINDOW
            ? IDS_APP_LIST_CONTEXT_MENU_NEW_WINDOW
            : IDS_APP_LIST_CONTEXT_MENU_NEW_TAB,
        menu_items);
  }

  if (!is_platform_app && menu_type == MenuType::kAppList &&
      extensions::util::IsAppLaunchableWithoutEnabling(app_id, profile()) &&
      extensions::OptionsPageInfo::HasOptionsPage(extension)) {
    AddCommandItem(ash::OPTIONS, IDS_NEW_TAB_APP_OPTIONS, menu_items);
  }

  if (menu_type == MenuType::kShelf &&
      instance_registry_->ContainsAppId(app_id)) {
    AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, menu_items);
  }

  const extensions::ManagementPolicy* policy =
      extensions::ExtensionSystem::Get(profile())->management_policy();
  DCHECK(policy);
  if (policy->UserMayModifySettings(extension, nullptr) &&
      !policy->MustRemainInstalled(extension, nullptr)) {
    AddCommandItem(ash::UNINSTALL, IDS_APP_LIST_UNINSTALL_ITEM, menu_items);
  }

  if (extensions::AppDisplayInfo::ShouldDisplayInAppLauncher(*extension)) {
    AddCommandItem(ash::SHOW_APP_INFO, IDS_APP_CONTEXT_MENU_SHOW_INFO,
                   menu_items);
  }

  std::move(callback).Run(std::move(menu_items));
}

void ExtensionAppsChromeOs::LaunchExtension(const std::string& app_id,
                                            int32_t event_flags,
                                            IntentPtr intent,
                                            LaunchSource launch_source,
                                            WindowInfoPtr window_info,
                                            LaunchCallback callback) {
  const auto* extension = MaybeGetExtension(app_id);
  DCHECK(extension);

  std::vector<storage::FileSystemURL> file_urls;
  if (!intent->files.empty()) {
    storage::FileSystemContext* file_system_context =
        file_manager::util::GetFileSystemContextForSourceURL(profile(),
                                                             extension->url());
    for (const IntentFilePtr& file : intent->files) {
      file_urls.push_back(
          file_system_context->CrackURLInFirstPartyContext(file->url));
    }
  }

  DCHECK(intent->activity_name);
  std::string action_id = intent->activity_name.value_or("");

  file_manager::file_browser_handlers::ExecuteFileBrowserHandler(
      profile(), extension, action_id, file_urls,
      base::BindOnce(
          [](LaunchCallback callback,
             extensions::api::file_manager_private::TaskResult result,
             std::string error) {
            bool success =
                result !=
                extensions::api::file_manager_private::TaskResult::kFailed;
            std::move(callback).Run(ConvertBoolToLaunchResult(success));
          },
          std::move(callback)));
}

void ExtensionAppsChromeOs::PauseApp(const std::string& app_id) {
  if (paused_apps_.MaybeAddApp(app_id)) {
    SetIconEffect(app_id);
  }
  AppPublisher::Publish(paused_apps_.CreateAppWithPauseStatus(
      app_type(), app_id, /*paused=*/true));

  if (!instance_registry_->ContainsAppId(app_id)) {
    return;
  }

  ash::app_time::AppTimeLimitInterface* app_limit =
      ash::app_time::AppTimeLimitInterface::Get(profile());
  DCHECK(app_limit);
  app_limit->PauseWebActivity(app_id);
}

void ExtensionAppsChromeOs::UnpauseApp(const std::string& app_id) {
  if (paused_apps_.MaybeRemoveApp(app_id)) {
    SetIconEffect(app_id);
  }

  AppPublisher::Publish(paused_apps_.CreateAppWithPauseStatus(
      app_type(), app_id, /*paused=*/false));

  ash::app_time::AppTimeLimitInterface* app_time =
      ash::app_time::AppTimeLimitInterface::Get(profile());
  DCHECK(app_time);
  app_time->ResumeWebActivity(app_id);
}

void ExtensionAppsChromeOs::UpdateAppSize(const std::string& app_id) {
  if (app_type() != AppType::kChromeApp) {
    return;
  }

  const extensions::Extension* extension = MaybeGetExtension(app_id);
  if (!extension) {
    return;
  }

  extensions::path_util::CalculateExtensionDirectorySize(
      extension->path(),
      base::BindOnce(&ExtensionAppsChromeOs::OnSizeCalculated,
                     weak_factory_.GetWeakPtr(), extension->id()));
}

void ExtensionAppsChromeOs::OnAppWindowAdded(
    extensions::AppWindow* app_window) {
  if (!ShouldRecordAppWindowActivity(app_window)) {
    return;
  }

  auto* window = app_window->GetNativeWindow();
  DCHECK(!instance_registry_->Exists(window));

  app_window_to_aura_window_[app_window] = window;

  // Attach window to multi-user manager now to let it manage visibility state
  // of the window correctly.
  if (SessionControllerClientImpl::IsMultiProfileAvailable()) {
    auto* multi_user_window_manager =
        MultiUserWindowManagerHelper::GetWindowManager();
    if (multi_user_window_manager) {
      multi_user_window_manager->SetWindowOwner(
          window, multi_user_util::GetAccountIdFromProfile(profile()));
    }
  }
  RegisterInstance(app_window, InstanceState::kStarted);
}

void ExtensionAppsChromeOs::OnAppWindowShown(extensions::AppWindow* app_window,
                                             bool was_hidden) {
  if (!ShouldRecordAppWindowActivity(app_window)) {
    return;
  }

  InstanceState state =
      instance_registry_->GetState(app_window->GetNativeWindow());
  // If the window is shown, it should be started, running and not hidden.
  state = static_cast<apps::InstanceState>(
      state | apps::InstanceState::kStarted | apps::InstanceState::kRunning);
  state =
      static_cast<apps::InstanceState>(state & ~apps::InstanceState::kHidden);
  RegisterInstance(app_window, state);
}

void ExtensionAppsChromeOs::OnAppWindowHidden(
    extensions::AppWindow* app_window) {
  if (!ShouldRecordAppWindowActivity(app_window)) {
    return;
  }

  // For hidden |app_window|, the other state bit, started, running, active, and
  // visible should be cleared.
  RegisterInstance(app_window, InstanceState::kHidden);
}

void ExtensionAppsChromeOs::OnAppWindowRemoved(
    extensions::AppWindow* app_window) {
  if (!ShouldRecordAppWindowActivity(app_window)) {
    return;
  }

  RegisterInstance(app_window, InstanceState::kDestroyed);
  app_window_to_aura_window_.erase(app_window);
}

void ExtensionAppsChromeOs::OnExtensionUninstalled(
    content::BrowserContext* browser_context,
    const extensions::Extension* extension,
    extensions::UninstallReason reason) {
  if (!Accepts(extension)) {
    return;
  }

  app_notifications_.RemoveNotificationsForApp(extension->id());
  paused_apps_.MaybeRemoveApp(extension->id());

  auto result = media_requests_.RemoveRequests(extension->id());

  apps::AppPublisher::ModifyCapabilityAccess(extension->id(), result.camera,
                                             result.microphone);

  ExtensionAppsBase::OnExtensionUninstalled(browser_context, extension, reason);
}

void ExtensionAppsChromeOs::OnPackageInstalled(
    const arc::mojom::ArcPackageInfo& package_info) {
  ApplyChromeBadge(package_info.package_name);
}

void ExtensionAppsChromeOs::OnPackageRemoved(const std::string& package_name,
                                             bool uninstalled) {
  ApplyChromeBadge(package_name);
}

void ExtensionAppsChromeOs::OnPackageListInitialRefreshed() {
  if (!arc_prefs_) {
    return;
  }
  for (const auto& app_name : arc_prefs_->GetPackagesFromPrefs()) {
    ApplyChromeBadge(app_name);
  }
}

void ExtensionAppsChromeOs::OnArcAppListPrefsDestroyed() {
  arc_prefs_ = nullptr;
}

void ExtensionAppsChromeOs::OnIsCapturingVideoChanged(
    content::WebContents* web_contents,
    bool is_capturing_video) {
  const webapps::AppId* web_app_id =
      web_app::WebAppTabHelper::GetAppId(web_contents);
  if (web_app_id) {
    if (web_app::WebAppProvider::GetForWebApps(profile()) &&
        !web_app::IsAppServiceShortcut(
            *web_app_id, *web_app::WebAppProvider::GetForWebApps(profile()))) {
      // This media access is coming from a web app.
      return;
    }
  }

  std::string app_id = app_constants::kChromeAppId;
  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile());
  DCHECK(registry);
  const extensions::ExtensionSet& extensions = registry->enabled_extensions();
  const extensions::Extension* extension =
      extensions.GetAppByURL(web_contents->GetVisibleURL());
  if (extension && Accepts(extension)) {
    app_id = extension->id();
  }

  auto result = media_requests_.UpdateCameraState(app_id, web_contents,
                                                  is_capturing_video);
  apps::AppPublisher::ModifyCapabilityAccess(app_id, result.camera,
                                             result.microphone);
}

void ExtensionAppsChromeOs::OnIsCapturingAudioChanged(
    content::WebContents* web_contents,
    bool is_capturing_audio) {
  const webapps::AppId* web_app_id =
      web_app::WebAppTabHelper::GetAppId(web_contents);
  if (web_app_id) {
    if (web_app::WebAppProvider::GetForWebApps(profile()) &&
        !web_app::IsAppServiceShortcut(
            *web_app_id, *web_app::WebAppProvider::GetForWebApps(profile()))) {
      // This media access is coming from a web app.
      return;
    }
  }

  std::string app_id = app_constants::kChromeAppId;
  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile());
  DCHECK(registry);
  const extensions::ExtensionSet& extensions = registry->enabled_extensions();
  const extensions::Extension* extension =
      extensions.GetAppByURL(web_contents->GetVisibleURL());
  if (extension && Accepts(extension)) {
    app_id = extension->id();
  }

  auto result = media_requests_.UpdateMicrophoneState(app_id, web_contents,
                                                      is_capturing_audio);
  apps::AppPublisher::ModifyCapabilityAccess(app_id, result.camera,
                                             result.microphone);
}

void ExtensionAppsChromeOs::OnNotificationDisplayed(
    const message_center::Notification& notification,
    const NotificationCommon::Metadata* const metadata) {
  switch (notification.notifier_id().type) {
    case message_center::NotifierType::APPLICATION:
      MaybeAddNotification(notification.notifier_id().id, notification.id());
      return;
    case message_center::NotifierType::WEB_PAGE:
      MaybeAddWebPageNotifications(notification, metadata);
      return;
    default:
      return;
  }
}

void ExtensionAppsChromeOs::OnNotificationClosed(
    const std::string& notification_id) {
  const auto app_ids =
      app_notifications_.GetAppIdsForNotification(notification_id);
  if (app_ids.empty()) {
    return;
  }

  app_notifications_.RemoveNotification(notification_id);

  for (const auto& app_id : app_ids) {
    AppPublisher::Publish(
        app_notifications_.CreateAppWithHasBadgeStatus(app_type(), app_id));
  }
}

void ExtensionAppsChromeOs::OnNotificationDisplayServiceDestroyed(
    NotificationDisplayService* service) {
  DCHECK(notification_display_service_.IsObservingSource(service));
  notification_display_service_.Reset();
}

bool ExtensionAppsChromeOs::MaybeAddNotification(
    const std::string& app_id,
    const std::string& notification_id) {
  if (MaybeGetExtension(app_id) == nullptr) {
    return false;
  }

  app_notifications_.AddNotification(app_id, notification_id);
  AppPublisher::Publish(
      app_notifications_.CreateAppWithHasBadgeStatus(app_type(), app_id));
  return true;
}

void ExtensionAppsChromeOs::MaybeAddWebPageNotifications(
    const message_center::Notification& notification,
    const NotificationCommon::Metadata* const metadata) {
  const PersistentNotificationMetadata* persistent_metadata =
      PersistentNotificationMetadata::From(metadata);

  const NonPersistentNotificationMetadata* non_persistent_metadata =
      NonPersistentNotificationMetadata::From(metadata);

  GURL url = notification.origin_url();

  if (persistent_metadata) {
    url = persistent_metadata->service_worker_scope;
  } else if (non_persistent_metadata &&
             !non_persistent_metadata->document_url.is_empty()) {
    url = non_persistent_metadata->document_url;
  }

  extensions::ExtensionRegistry* registry =
      extensions::ExtensionRegistry::Get(profile());
  DCHECK(registry);
  const extensions::ExtensionSet& extensions = registry->enabled_extensions();
  const extensions::Extension* extension = extensions.GetAppByURL(url);
  if (extension) {
    MaybeAddNotification(extension->id(), notification.id());
  }
}

// static
bool ExtensionAppsChromeOs::IsBlocklisted(const std::string& app_id) {
  // We blocklist (meaning we don't publish the app, in the App Service sense)
  // some apps that are already published by other app publishers.
  //
  // This sense of "blocklist" is separate from the extension registry's
  // kDisabledByBlocklist concept, which is when SafeBrowsing will send out a
  // blocklist of malicious extensions to disable.

  // The Play Store is conceptually provided by the ARC++ publisher, but
  // because it (the Play Store icon) is also the UI for enabling Android apps,
  // we also want to show the app icon even before ARC++ is enabled. Prior to
  // the App Service, as a historical implementation quirk, the Play Store both
  // has an "ARC++ app" component and an "Extension app" component, and both
  // share the same App ID.
  //
  // In the App Service world, there should be a unique app publisher for any
  // given app. In this case, the ArcApps publisher publishes the Play Store
  // app, and the ExtensionApps publisher does not.
  if (app_id == arc::kPlayStoreAppId) {
    return true;
  }

  // If lacros chrome apps is enabled, a small list of extension apps or
  // extensions on ash extension keeplist is allowed to run in both ash and
  // lacros, don't publish such app or extension if it is blocked for app
  // service in ash.
  if (crosapi::browser_util::IsLacrosChromeAppsEnabled()) {
    if (extensions::ExtensionAppRunsInBothOSAndStandaloneBrowser(app_id) &&
        extensions::ExtensionAppBlockListedForAppServiceInOS(app_id)) {
      return true;
    }

    if (extensions::ExtensionRunsInBothOSAndStandaloneBrowser(app_id) &&
        extensions::ExtensionBlockListedForAppServiceInOS(app_id)) {
      return true;
    }
  }

  return false;
}

void ExtensionAppsChromeOs::UpdateShowInFields(const std::string& app_id) {
  const auto* extension = MaybeGetExtension(app_id);
  if (!extension) {
    return;
  }

  auto app = std::make_unique<App>(app_type(), app_id);
  SetShowInFields(extension, *app);
  AppPublisher::Publish(std::move(app));
}

void ExtensionAppsChromeOs::OnHideWebStoreIconPrefChanged() {
  UpdateShowInFields(extensions::kWebStoreAppId);
}

void ExtensionAppsChromeOs::OnSystemFeaturesPrefChanged() {
  PrefService* const local_state = g_browser_process->local_state();
  if (!local_state || !local_state->FindPreference(
                          policy::policy_prefs::kSystemFeaturesDisableList)) {
    return;
  }

  const base::Value::List& disabled_system_features_pref =
      local_state->GetList(policy::policy_prefs::kSystemFeaturesDisableList);

  const bool is_pref_disabled_mode_hidden =
      local_state->GetString(
          policy::policy_prefs::kSystemFeaturesDisableMode) ==
      policy::kHiddenDisableMode;
  const bool is_disabled_mode_changed =
      (is_pref_disabled_mode_hidden != is_disabled_apps_mode_hidden_);
  is_disabled_apps_mode_hidden_ = is_pref_disabled_mode_hidden;

  UpdateAppDisabledState(disabled_system_features_pref,
                         static_cast<int>(policy::SystemFeature::kWebStore),
                         extensions::kWebStoreAppId, is_disabled_mode_changed);
}

bool ExtensionAppsChromeOs::Accepts(const extensions::Extension* extension) {
  CHECK(extension);

  if (app_type() == AppType::kExtension) {
    if (!extension->is_extension() || IsBlocklisted(extension->id())) {
      return false;
    }

    // QuickOffice has file_handlers which we need to register.
    if (extension_misc::IsQuickOfficeExtension(extension->id())) {
      // Don't publish quickoffice in ash if 1st party ash extension keep list
      // is enforced, since quickoffice extension is published in Lacros.
      return !crosapi::browser_util::ShouldEnforceAshExtensionKeepList();
    }

    // Do not publish extensions in Ash if it should run in Lacros instead.
    if (crosapi::browser_util::ShouldEnforceAshExtensionKeepList()) {
      return false;
    }

    // Allow MV3 file handlers.
    if (extensions::WebFileHandlers::SupportsWebFileHandlers(*extension) &&
        extensions::WebFileHandlers::HasFileHandlers(*extension)) {
      return true;
    }

    // Only accept extensions with file_browser_handlers.
    FileBrowserHandler::List* handler_list =
        FileBrowserHandler::GetHandlers(extension);
    if (!handler_list) {
      return false;
    }
    return true;
  }

  if (!extension->is_app() || IsBlocklisted(extension->id())) {
    return false;
  }

  // Do not publish legacy packaged apps in Ash if Lacros is user's primary
  // browser. Legacy packaged apps are deprecated and not supported by Lacros.
  if (extension->is_legacy_packaged_app() &&
      crosapi::browser_util::IsLacrosEnabled()) {
    return false;
  }

  //  Do not publish hosted apps in Ash if hosted apps should run in
  //  Lacros.
  if (extension->is_hosted_app() &&
      extension->id() != app_constants::kChromeAppId &&
      crosapi::IsStandaloneBrowserHostedAppsEnabled()) {
    return false;
  }

  // Do not publish platform apps in Ash if it should run in Lacros instead.
  if (extension->is_platform_app() &&
      crosapi::browser_util::IsLacrosChromeAppsEnabled()) {
    return extensions::ExtensionAppRunsInOS(extension->id());
  }

  return true;
}

AppLaunchParams ExtensionAppsChromeOs::ModifyAppLaunchParams(
    const std::string& app_id,
    LaunchSource launch_source,
    AppLaunchParams params) {
  ash::ShelfLaunchSource source = ConvertLaunchSource(launch_source);
  if ((source == ash::LAUNCH_FROM_APP_LIST ||
       source == ash::LAUNCH_FROM_APP_LIST_SEARCH) &&
      app_id == extensions::kWebStoreAppId) {
    // Get the corresponding source string.
    std::string source_value = GetSourceFromAppListSource(source);

    // Set an override URL to include the source.
    const auto* extension = MaybeGetExtension(app_id);
    DCHECK(extension);
    GURL extension_url = extensions::AppLaunchInfo::GetFullLaunchURL(extension);
    params.override_url = net::AppendQueryParameter(
        extension_url, extension_urls::kWebstoreSourceField, source_value);
  }
  return params;
}

void ExtensionAppsChromeOs::SetShowInFields(
    const extensions::Extension* extension,
    App& app) {
  ExtensionAppsBase::SetShowInFields(extension, app);

  // Explicitly mark QuickOffice as being able to handle intents even though it
  // is otherwise hidden from the user. Otherwise, extensions are only published
  // if they have file_browser_handlers, which means they need to handle
  // intents.
  if (extension_misc::IsQuickOfficeExtension(extension->id()) ||
      extension->is_extension()) {
    app.handles_intents = true;
  }
}

bool ExtensionAppsChromeOs::ShouldShownInLauncher(
    const extensions::Extension* extension) {
  return app_list::ShouldShowInLauncher(extension, profile());
}

AppPtr ExtensionAppsChromeOs::CreateApp(const extensions::Extension* extension,
                                        Readiness readiness) {
  CHECK(extension);
  // When Lacros is enabled, extensions not on the ash keep list should not be
  // published to the app service at all. Thus this method should not be called.
  DCHECK(!(extension->is_platform_app() &&
           crosapi::browser_util::IsLacrosChromeAppsEnabled() &&
           !extensions::ExtensionAppRunsInOS(extension->id())));
  const bool is_app_disabled = base::Contains(disabled_apps_, extension->id());

  auto app = CreateAppImpl(
      extension, is_app_disabled ? Readiness::kDisabledByPolicy : readiness);
  bool paused = paused_apps_.IsPaused(extension->id());
  app->icon_key = IconKey(GetIconEffects(extension, paused));

  if (is_app_disabled && is_disabled_apps_mode_hidden_) {
    app->show_in_launcher = false;
    app->show_in_search = false;
    app->show_in_shelf = false;
    app->handles_intents = false;
  }

  app->has_badge = app_notifications_.HasNotification(extension->id());
  app->paused = paused;

  if (extension->is_app() ||
      extensions::IsLegacyQuickOfficeExtension(*extension)) {
    app->intent_filters = apps_util::CreateIntentFiltersForChromeApp(extension);
  } else if (extension->is_extension()) {
    app->intent_filters = apps_util::CreateIntentFiltersForExtension(extension);
  }
  return app;
}

void ExtensionAppsChromeOs::OnSizeCalculated(const std::string& app_id,
                                             int64_t size) {
  std::vector<AppPtr> apps;
  auto app = std::make_unique<apps::App>(app_type(), app_id);
  app->app_size_in_bytes = size;
  apps.push_back(std::move(app));
  AppPublisher::Publish(std::move(apps), app_type(),
                        /*should_notify_initialized=*/false);
}

IconEffects ExtensionAppsChromeOs::GetIconEffects(
    const extensions::Extension* extension,
    bool paused) {
  IconEffects icon_effects = IconEffects::kNone;
  icon_effects =
      static_cast<IconEffects>(icon_effects | IconEffects::kCrOsStandardIcon);

  if (extensions::util::ShouldApplyChromeBadge(profile(), extension->id())) {
    icon_effects =
        static_cast<IconEffects>(icon_effects | IconEffects::kChromeBadge);
  }
  icon_effects = static_cast<IconEffects>(
      icon_effects | ExtensionAppsBase::GetIconEffects(extension));
  if (paused) {
    icon_effects =
        static_cast<IconEffects>(icon_effects | IconEffects::kPaused);
  }
  if (base::Contains(disabled_apps_, extension->id())) {
    icon_effects =
        static_cast<IconEffects>(icon_effects | IconEffects::kBlocked);
  }
  return icon_effects;
}

void ExtensionAppsChromeOs::ApplyChromeBadge(const std::string& package_name) {
  const std::vector<std::string> extension_ids =
      extensions::util::GetEquivalentInstalledExtensions(profile(),
                                                         package_name);

  for (auto& app_id : extension_ids) {
    SetIconEffect(app_id);
  }
}

void ExtensionAppsChromeOs::SetIconEffect(const std::string& app_id) {
  const auto* extension = MaybeGetExtension(app_id);
  if (!extension) {
    return;
  }

  auto app = std::make_unique<App>(app_type(), app_id);
  app->icon_key =
      IconKey(GetIconEffects(extension, paused_apps_.IsPaused(app_id)));
  AppPublisher::Publish(std::move(app));
}

bool ExtensionAppsChromeOs::ShouldRecordAppWindowActivity(
    extensions::AppWindow* app_window) {
  DCHECK(app_window);

  const extensions::Extension* extension = app_window->GetExtension();
  if (!extension) {
    return false;
  }

  // ARC Play Store is not published by this publisher, but the window for Play
  // Store should be able to be added to InstanceRegistry by the Chrome app
  // publisher.
  if (extension->id() == arc::kPlayStoreAppId &&
      app_type() != AppType::kExtension) {
    return true;
  }

  if (!Accepts(extension)) {
    return false;
  }

  return true;
}

void ExtensionAppsChromeOs::RegisterInstance(extensions::AppWindow* app_window,
                                             InstanceState new_state) {
  aura::Window* window = app_window->GetNativeWindow();

  // If the current state has been marked as |new_state|, we don't need to
  // update.
  if (instance_registry_->GetState(window) == new_state) {
    return;
  }

  if (new_state == InstanceState::kDestroyed) {
    DCHECK(base::Contains(app_window_to_aura_window_, app_window));
    window = app_window_to_aura_window_[app_window];
  }
  InstanceParams params(app_window->extension_id(), window);
  params.launch_id = GetLaunchId(app_window);
  params.state = std::make_pair(new_state, base::Time::Now());
  params.browser_context = app_window->browser_context();
  instance_registry_->CreateOrUpdateInstance(std::move(params));
}

content::WebContents* ExtensionAppsChromeOs::LaunchImpl(
    AppLaunchParams&& params) {
  AppLaunchParams params_for_restore(
      params.app_id, params.container, params.disposition, params.launch_source,
      params.display_id, params.launch_files, params.intent);

  auto* web_contents = ExtensionAppsBase::LaunchImpl(std::move(params));

  std::unique_ptr<app_restore::AppLaunchInfo> launch_info;
  int session_id = GetSessionIdForRestoreFromWebContents(web_contents);
  if (!SessionID::IsValidValue(session_id)) {
    // Save all launch information for platform apps, which can launch via
    // event, e.g. file app.
    launch_info = std::make_unique<app_restore::AppLaunchInfo>(
        params_for_restore.app_id, params_for_restore.container,
        params_for_restore.disposition, params_for_restore.display_id,
        std::move(params_for_restore.launch_files),
        std::move(params_for_restore.intent));
    full_restore::SaveAppLaunchInfo(profile()->GetPath(),
                                    std::move(launch_info));
  }

  return web_contents;
}

void ExtensionAppsChromeOs::UpdateAppDisabledState(
    const base::Value::List& disabled_system_features_pref,
    int feature,
    const std::string& app_id,
    bool is_disabled_mode_changed) {
  const bool is_disabled =
      base::Contains(disabled_system_features_pref, base::Value(feature));
  // Sometimes the policy is updated before the app is installed, so this way
  // the disabled_apps_ is updated regardless the Publish should happen or not
  // and the app will be published with the correct readiness upon its
  // installation.
  const bool should_publish =
      (base::Contains(disabled_apps_, app_id) != is_disabled) ||
      is_disabled_mode_changed;

  if (is_disabled) {
    disabled_apps_.insert(app_id);
  } else {
    disabled_apps_.erase(app_id);
  }

  if (!should_publish) {
    return;
  }

  const auto* extension = MaybeGetExtension(app_id);
  if (!extension) {
    return;
  }

  AppPublisher::Publish(CreateApp(extension, is_disabled
                                                 ? Readiness::kDisabledByPolicy
                                                 : Readiness::kReady));
}

}  // namespace apps