chromium/chrome/browser/apps/app_service/publishers/plugin_vm_apps.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/plugin_vm_apps.h"

#include <utility>
#include <vector>

#include "ash/public/cpp/app_menu_constants.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "chrome/browser/apps/app_service/app_icon/app_icon_factory.h"
#include "chrome/browser/apps/app_service/app_launch_params.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/intent_util.h"
#include "chrome/browser/apps/app_service/menu_util.h"
#include "chrome/browser/apps/app_service/metrics/app_service_metrics.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service.h"
#include "chrome/browser/ash/guest_os/guest_os_registry_service_factory.h"
#include "chrome/browser/ash/guest_os/public/types.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_files.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_manager_factory.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_pref_names.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/grit/chrome_unscaled_resources.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/icon_types.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_url.h"
#include "ui/base/l10n/l10n_util.h"

namespace {

struct PermissionInfo {
  apps::PermissionType permission;
  const char* pref_name;
};

// TODO(crbug.com/40760689): Update to use a switch to map between two enum.
constexpr PermissionInfo permission_infos[] = {
    {apps::PermissionType::kPrinting,
     plugin_vm::prefs::kPluginVmPrintersAllowed},
    {apps::PermissionType::kCamera, plugin_vm::prefs::kPluginVmCameraAllowed},
    {apps::PermissionType::kMicrophone, plugin_vm::prefs::kPluginVmMicAllowed},
};

const char* PermissionToPrefName(apps::PermissionType permission) {
  for (const PermissionInfo& info : permission_infos) {
    if (info.permission == permission) {
      return info.pref_name;
    }
  }
  return nullptr;
}

void SetAppAllowed(bool allowed, apps::App& app) {
  app.readiness =
      allowed ? apps::Readiness::kReady : apps::Readiness::kDisabledByPolicy;
  app.recommendable = allowed;
  app.searchable = allowed;
  app.show_in_launcher = allowed;
  app.show_in_shelf = allowed;
  app.show_in_search = allowed;
  app.handles_intents = allowed;
}

apps::Permissions CreatePermissions(Profile* profile) {
  apps::Permissions permissions;
  for (const PermissionInfo& info : permission_infos) {
    permissions.push_back(std::make_unique<apps::Permission>(
        info.permission, profile->GetPrefs()->GetBoolean(info.pref_name),
        /*is_managed=*/false));
  }
  return permissions;
}

apps::AppPtr CreatePluginVmApp(Profile* profile, bool allowed) {
  auto app = apps::AppPublisher::MakeApp(
      apps::AppType::kPluginVm, plugin_vm::kPluginVmShelfAppId,
      allowed ? apps::Readiness::kReady : apps::Readiness::kDisabledByPolicy,
      l10n_util::GetStringUTF8(IDS_PLUGIN_VM_APP_NAME),
      apps::InstallReason::kUser, apps::InstallSource::kUnknown);

  app->icon_key =
      apps::IconKey(IDR_LOGO_PLUGIN_VM_DEFAULT_192, apps::IconEffects::kNone);

  app->permissions = CreatePermissions(profile);

  SetAppAllowed(allowed, *app);

  // Show when installed, even if disabled by policy, to give users the choice
  // to uninstall and free up space.
  app->show_in_management = app->allow_uninstall =
      plugin_vm::PluginVmFeatures::Get()->IsConfigured(profile);
  return app;
}

// Create a file intent filter with extension type conditions for App Service.
apps::IntentFilters CreateIntentFilterForPluginVm(
    const guest_os::GuestOsRegistryService::Registration& registration) {
  const std::set<std::string>& extension_types_set = registration.Extensions();
  if (extension_types_set.empty()) {
    return {};
  }
  std::vector<std::string> extension_types(extension_types_set.begin(),
                                           extension_types_set.end());
  apps::IntentFilters intent_filters;
  intent_filters.push_back(apps_util::CreateFileFilter(
      {apps_util::kIntentActionView}, /*mime_types=*/{}, extension_types,
      // TODO(crbug.com/40233967): Remove activity_name when default file
      // handling preferences for Files App are migrated.
      /*activity_name=*/apps_util::kGuestOsActivityName));

  return intent_filters;
}

apps::LaunchResult ConvertPluginVmResultToLaunchResult(
    plugin_vm::LaunchPluginVmAppResult plugin_vm_result) {
  switch (plugin_vm_result) {
    case plugin_vm::LaunchPluginVmAppResult::SUCCESS:
      return apps::LaunchResult(apps::State::kSuccess);
    case plugin_vm::LaunchPluginVmAppResult::FAILED_DIRECTORY_NOT_SHARED:
      return apps::LaunchResult(apps::State::kFailedDirectoryNotShared);
    case plugin_vm::LaunchPluginVmAppResult::FAILED:
      return apps::LaunchResult(apps::State::kFailed);
  }
}

}  // namespace

namespace apps {

PluginVmApps::PluginVmApps(AppServiceProxy* proxy)
    : AppPublisher(proxy), profile_(proxy->profile()) {
  // Don't show anything for non-primary profiles. We can't use
  // `PluginVmFeatures::Get()->IsAllowed()` here because we still let the user
  // uninstall Plugin VM when it isn't allowed for some other reasons (e.g.
  // policy).
  if (!ash::ProfileHelper::IsPrimaryProfile(profile_)) {
    return;
  }

  // Register for Plugin VM changes to policy and installed state, so that we
  // can update the availability and status of the Plugin VM app. Unretained is
  // safe as these are cleaned up upon destruction.
  availability_subscription_ =
      std::make_unique<plugin_vm::PluginVmAvailabilitySubscription>(
          profile_,
          base::BindRepeating(&PluginVmApps::OnPluginVmAvailabilityChanged,
                              base::Unretained(this)));
  pref_registrar_.Init(profile_->GetPrefs());
  for (const PermissionInfo& info : permission_infos) {
    pref_registrar_.Add(info.pref_name,
                        base::BindRepeating(&PluginVmApps::OnPermissionChanged,
                                            base::Unretained(this)));
  }

  is_allowed_ = plugin_vm::PluginVmFeatures::Get()->IsAllowed(profile_);
}

PluginVmApps::~PluginVmApps() {
  if (registry_) {
    registry_->RemoveObserver(this);
  }
}

void PluginVmApps::Initialize() {
  registry_ = guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_);
  if (!registry_) {
    return;
  }
  registry_->AddObserver(this);

  RegisterPublisher(AppType::kPluginVm);

  std::vector<AppPtr> apps;
  apps.push_back(CreatePluginVmApp(profile_, is_allowed_));
  for (const auto& pair :
       registry_->GetRegisteredApps(guest_os::VmType::PLUGIN_VM)) {
    const guest_os::GuestOsRegistryService::Registration& registration =
        pair.second;
    apps.push_back(CreateApp(registration, /*generate_new_icon_key=*/true));
  }
  AppPublisher::Publish(std::move(apps), AppType::kPluginVm,
                        /*should_notify_initialized=*/true);
}

void PluginVmApps::GetCompressedIconData(const std::string& app_id,
                                         int32_t size_in_dip,
                                         ui::ResourceScaleFactor scale_factor,
                                         LoadIconCallback callback) {
  GetGuestOSAppCompressedIconData(profile_, app_id, size_in_dip, scale_factor,
                                  std::move(callback));
}

void PluginVmApps::Launch(const std::string& app_id,
                          int32_t event_flags,
                          LaunchSource launch_source,
                          WindowInfoPtr window_info) {
  DCHECK_EQ(plugin_vm::kPluginVmShelfAppId, app_id);
  if (plugin_vm::PluginVmFeatures::Get()->IsEnabled(profile_)) {
    plugin_vm::PluginVmManagerFactory::GetForProfile(profile_)->LaunchPluginVm(
        base::DoNothing());
  } else {
    plugin_vm::ShowPluginVmInstallerView(profile_);
  }
}

void PluginVmApps::LaunchAppWithIntent(const std::string& app_id,
                                       int32_t event_flags,
                                       IntentPtr intent,
                                       LaunchSource launch_source,
                                       WindowInfoPtr window_info,
                                       LaunchCallback callback) {
  // Retrieve URLs from the files in the intent.
  std::vector<guest_os::LaunchArg> args;
  if (intent && intent->files.size() > 0) {
    args.reserve(intent->files.size());
    storage::FileSystemContext* file_system_context =
        file_manager::util::GetFileManagerFileSystemContext(profile_);
    for (auto& file : intent->files) {
      args.emplace_back(
          file_system_context->CrackURLInFirstPartyContext(file->url));
    }
  }
  plugin_vm::LaunchPluginVmApp(
      profile_, app_id, args,
      base::BindOnce(
          [](LaunchCallback callback,
             plugin_vm::LaunchPluginVmAppResult plugin_vm_result,
             const std::string& failure_reason) {
            if (plugin_vm_result !=
                plugin_vm::LaunchPluginVmAppResult::SUCCESS) {
              LOG(ERROR) << "Plugin VM launch error: " << failure_reason;
            }
            std::move(callback).Run(
                ConvertPluginVmResultToLaunchResult(plugin_vm_result));
          },
          std::move(callback)));
}

void PluginVmApps::LaunchAppWithParams(AppLaunchParams&& params,
                                       LaunchCallback callback) {
  Launch(params.app_id, ui::EF_NONE, LaunchSource::kUnknown, nullptr);

  // TODO(crbug.com/40787924): Add launch return value.
  std::move(callback).Run(LaunchResult());
}

void PluginVmApps::SetPermission(const std::string& app_id,
                                 PermissionPtr permission_ptr) {
  auto permission = permission_ptr->permission_type;
  const char* pref_name = PermissionToPrefName(permission);
  if (!pref_name) {
    return;
  }

  profile_->GetPrefs()->SetBoolean(pref_name,
                                   permission_ptr->IsPermissionEnabled());
}

void PluginVmApps::Uninstall(const std::string& app_id,
                             UninstallSource uninstall_source,
                             bool clear_site_data,
                             bool report_abuse) {
  guest_os::GuestOsRegistryServiceFactory::GetForProfile(profile_)
      ->ClearApplicationList(guest_os::VmType::PLUGIN_VM,
                             plugin_vm::kPluginVmName, "");
  plugin_vm::PluginVmManagerFactory::GetForProfile(profile_)
      ->UninstallPluginVm();
}

void PluginVmApps::GetMenuModel(const std::string& app_id,
                                MenuType menu_type,
                                int64_t display_id,
                                base::OnceCallback<void(MenuItems)> callback) {
  MenuItems menu_items;

  if (ShouldAddOpenItem(app_id, menu_type, profile_)) {
    AddCommandItem(ash::LAUNCH_NEW, IDS_APP_CONTEXT_MENU_ACTIVATE_ARC,
                   menu_items);
  }

  if (ShouldAddCloseItem(app_id, menu_type, profile_)) {
    AddCommandItem(ash::MENU_CLOSE, IDS_SHELF_CONTEXT_MENU_CLOSE, menu_items);
  }

  if (app_id == plugin_vm::kPluginVmShelfAppId &&
      plugin_vm::IsPluginVmRunning(profile_)) {
    AddCommandItem(ash::SHUTDOWN_GUEST_OS, IDS_PLUGIN_VM_SHUT_DOWN_MENU_ITEM,
                   menu_items);
  }

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

void PluginVmApps::OnRegistryUpdated(
    guest_os::GuestOsRegistryService* registry_service,
    guest_os::VmType vm_type,
    const std::vector<std::string>& updated_apps,
    const std::vector<std::string>& removed_apps,
    const std::vector<std::string>& inserted_apps) {
  if (vm_type != guest_os::VmType::PLUGIN_VM) {
    return;
  }

  for (const std::string& app_id : updated_apps) {
    if (auto registration = registry_->GetRegistration(app_id)) {
      AppPublisher::Publish(
          CreateApp(*registration, /*generate_new_icon_key=*/false));
    }
  }
  for (const std::string& app_id : removed_apps) {
    auto app = std::make_unique<App>(AppType::kPluginVm, app_id);
    app->readiness = apps::Readiness::kUninstalledByUser;
    AppPublisher::Publish(std::move(app));
  }
  for (const std::string& app_id : inserted_apps) {
    if (auto registration = registry_->GetRegistration(app_id)) {
      AppPublisher::Publish(
          CreateApp(*registration, /*generate_new_icon_key=*/true));
    }
  }
}

AppPtr PluginVmApps::CreateApp(
    const guest_os::GuestOsRegistryService::Registration& registration,
    bool generate_new_icon_key) {
  DCHECK_EQ(registration.VmType(), guest_os::VmType::PLUGIN_VM);

  auto app = AppPublisher::MakeApp(
      AppType::kPluginVm, registration.app_id(), Readiness::kReady,
      registration.Name(), InstallReason::kUser, apps::InstallSource::kUnknown);

  if (generate_new_icon_key) {
    app->icon_key = IconKey(IconEffects::kCrOsStandardIcon);
  }

  app->last_launch_time = registration.LastLaunchTime();
  app->install_time = registration.InstallTime();

  app->show_in_launcher = false;
  app->show_in_search = false;
  app->show_in_shelf = false;
  app->show_in_management = false;
  app->allow_uninstall = false;
  app->allow_close = true;
  app->handles_intents = true;
  app->intent_filters = CreateIntentFilterForPluginVm(registration);
  return app;
}

void PluginVmApps::OnPluginVmAvailabilityChanged(bool is_allowed,
                                                 bool is_configured) {
  // Republish the Plugin VM app when policy changes have changed
  // its availability. Only changed fields need to be republished.
  is_allowed_ = is_allowed;

  auto app =
      std::make_unique<App>(AppType::kPluginVm, plugin_vm::kPluginVmShelfAppId);
  SetAppAllowed(is_allowed, *app);
  app->show_in_management = app->allow_uninstall = is_configured;
  AppPublisher::Publish(std::move(app));
}

void PluginVmApps::OnPermissionChanged() {
  auto app =
      std::make_unique<App>(AppType::kPluginVm, plugin_vm::kPluginVmShelfAppId);
  app->permissions = CreatePermissions(profile_);
  AppPublisher::Publish(std::move(app));
}

}  // namespace apps