chromium/chrome/browser/apps/app_service/webapk/webapk_manager.cc

// Copyright 2021 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/webapk/webapk_manager.h"

#include <optional>

#include "ash/components/arc/mojom/app.mojom.h"
#include "ash/components/arc/session/connection_holder.h"
#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.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/webapk/webapk_install_queue.h"
#include "chrome/browser/apps/app_service/webapk/webapk_metrics.h"
#include "chrome/browser/apps/app_service/webapk/webapk_prefs.h"
#include "chrome/browser/ash/apps/apk_web_app_service.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_util.h"

namespace {
constexpr char kGeneratedWebApkPackagePrefix[] = "org.chromium.webapk.";

bool HasShareIntentFilter(const apps::AppUpdate& app) {
  auto intent = std::make_unique<apps::Intent>(apps_util::kIntentActionSend);
  for (const auto& filter : app.IntentFilters()) {
    for (const auto& condition : filter->conditions) {
      if (intent->MatchCondition(condition)) {
        return true;
      }
    }
  }
  return false;
}

}  // namespace

namespace apps {

WebApkManager::WebApkManager(Profile* profile)
    : profile_(profile),
      install_queue_(std::make_unique<WebApkInstallQueue>(profile_)),
      pref_change_registrar_(std::make_unique<PrefChangeRegistrar>()) {
  DCHECK(web_app::AreWebAppsEnabled(profile_));
  proxy_ = AppServiceProxyFactory::GetForProfile(profile_);
  apk_service_ = ash::ApkWebAppService::Get(profile_);
  DCHECK(apk_service_);
  app_list_prefs_ = ArcAppListPrefs::Get(profile_);
  DCHECK(app_list_prefs_);

  // Always observe AppListPrefs, even when the rest of WebAPKs is not enabled,
  // so that we can detect WebAPK uninstalls that happen when the feature is
  // disabled.
  arc_app_list_prefs_observer_.Observe(app_list_prefs_.get());
  arc::ArcSessionManager::Get()->AddObserver(this);
  pref_change_registrar_->Init(profile_->GetPrefs());
  pref_change_registrar_->Add(
      webapk_prefs::kGeneratedWebApksEnabled,
      base::BindRepeating(&WebApkManager::StartOrStopObserving,
                          base::Unretained(this)));

  StartOrStopObserving();
}

WebApkManager::~WebApkManager() {
  auto* arc_session_manager = arc::ArcSessionManager::Get();
  // ArcSessionManager can be destroyed early in unit tests.
  if (arc_session_manager) {
    arc_session_manager->RemoveObserver(this);
  }
}

void WebApkManager::StartOrStopObserving() {
  // WebApkManager is only created when arc::IsArcAllowedForProfile() is true.
  // We additionally check whether Play Store is enabled through Settings before
  // enabling anything.
  bool arc_enabled = arc::IsArcPlayStoreEnabledForProfile(profile_);
  bool policy_enabled =
      profile_->GetPrefs()->GetBoolean(webapk_prefs::kGeneratedWebApksEnabled);

  if (arc_enabled && policy_enabled) {
    auto* cache = &proxy_->AppRegistryCache();
    if (!app_registry_cache_observer_.IsObservingSource(cache)) {
      app_registry_cache_observer_.Reset();
      app_registry_cache_observer_.Observe(cache);
    }

    if (cache->IsAppTypeInitialized(AppType::kWeb)) {
      Synchronize();
    }
    return;
  }

  app_registry_cache_observer_.Reset();
  initialized_ = false;

  if (!policy_enabled) {
    // Remove any WebAPKs which were installed before the policy was enacted.
    // Ensures we don't end up in a confusing half-state with apps which can
    // never update, and allows us to start from scratch if the feature is
    // re-enabled.
    base::flat_set<std::string> current_installs =
        webapk_prefs::GetWebApkAppIds(profile_);
    for (const std::string& id : current_installs) {
      QueueUninstall(id);
    }
  }
}

void WebApkManager::Synchronize() {
  initialized_ = true;
  base::flat_set<std::string> current_installs =
      webapk_prefs::GetWebApkAppIds(profile_);
  base::flat_set<std::string> eligible_installs;

  proxy_->AppRegistryCache().ForEachApp([&](const apps::AppUpdate& update) {
    if (IsAppEligibleForWebApk(update)) {
      eligible_installs.insert(update.AppId());
    }
  });

  // Install any WebAPK which should be installed but currently isn't.
  for (const std::string& id : eligible_installs) {
    if (!current_installs.contains(id)) {
      QueueInstall(id);
    }
  }

  // Uninstall any WebAPK which shouldn't be installed but currently is.
  for (const std::string& id : current_installs) {
    if (!eligible_installs.contains(id)) {
      QueueUninstall(id);
    }
  }

  // Update any WebAPK for which an update was previously queued but
  // unsuccessful.
  for (const std::string& id : webapk_prefs::GetUpdateNeededAppIds(profile_)) {
    QueueUpdate(id);
  }
}

void WebApkManager::OnAppUpdate(const AppUpdate& update) {
  if (!initialized_) {
    return;
  }

  if (IsAppEligibleForWebApk(update)) {
    if (webapk_prefs::GetWebApkPackageName(profile_, update.AppId())) {
      // Existing WebAPK.
      // Update if there are app metadata changes.
      if (update.ShortNameChanged() || update.NameChanged() ||
          update.IconKeyChanged() || update.IntentFiltersChanged()) {
        QueueUpdate(update.AppId());
      }
    } else {  // New WebAPK.
      // Install if it is eligible for installation.
      if (update.ReadinessChanged() &&
          update.Readiness() == apps::Readiness::kReady) {
        QueueInstall(update.AppId());
      }
    }
  } else {  // !IsAppEligibleForWebApk(update)
    // Uninstall WebAPKs when the app becomes no longer eligible (which includes
    // when the app is uninstalled).
    if (webapk_prefs::GetWebApkPackageName(profile_, update.AppId())) {
      QueueUninstall(update.AppId());
    }
  }
}

// Called once per app type during startup, once apps of that type are
// initialized.
void WebApkManager::OnAppTypeInitialized(AppType type) {
  if (type == AppType::kWeb) {
    Synchronize();
  }
}

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

void WebApkManager::OnPackageListInitialRefreshed() {
  for (const std::string& app_id : uninstall_queue_) {
    UninstallInternal(app_id);
  }
  uninstall_queue_.clear();

  // Uninstall any WebAPK packages which are installed in ARC but not linked to
  // an app in Prefs. This could happen if the WebAPK installation callback is
  // never delivered properly, or if the Play Store retries an error in the
  // background.
  // If an installed WebAPK is not listed in WebAPK prefs, then we will generate
  // and install a new WebAPK automatically, possibly resulting in duplicate
  // apps visible to the user.
  std::vector<std::string> installed_packages =
      app_list_prefs_->GetPackagesFromPrefs();
  base::flat_set<std::string> installed_webapk_packages =
      webapk_prefs::GetInstalledWebApkPackageNames(profile_);
  for (const auto& package_name : installed_packages) {
    if (base::StartsWith(package_name, kGeneratedWebApkPackagePrefix) &&
        !installed_webapk_packages.contains(package_name)) {
      auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
          app_list_prefs_->app_connection_holder(), UninstallPackage);
      if (!instance) {
        return;
      }
      instance->UninstallPackage(package_name);
    }
  }
}

void WebApkManager::OnPackageRemoved(const std::string& package_name,
                                     bool uninstalled) {
  if (!base::StartsWith(package_name, kGeneratedWebApkPackagePrefix)) {
    return;
  }

  // There are several possible cases for why a WebAPK can be uninstalled:
  //
  // 1. The WebAPK was removed through QueueUninstall below, either because:
  //    - The Chrome OS app was uninstalled, or
  //    - The app became ineligible for having a Web APK (e.g., the Share Target
  //      was removed)
  //    In both cases, we just need to remove the WebAPK from prefs.
  // 2. The WebAPK was uninstalled through Android settings. In this case, the
  //    Chrome OS-side app will still be installed and eligible for a WebAPK.

  // TODO(crbug.com/40178176): Remove the web app as well, if it is still
  // installed and eligible, and WebAPKs are not disabled by policy.
  webapk_prefs::RemoveWebApkByPackageName(profile_, package_name);
}

void WebApkManager::OnArcPlayStoreEnabledChanged(bool enabled) {
  StartOrStopObserving();
}

WebApkInstallQueue* WebApkManager::GetInstallQueueForTest() {
  return install_queue_.get();
}

bool WebApkManager::IsAppEligibleForWebApk(const AppUpdate& app) {
  if (app.Readiness() != apps::Readiness::kReady) {
    return false;
  }

  if (app.AppType() != apps::AppType::kWeb) {
    return false;
  }

  if (app.InstallReason() == apps::InstallReason::kSystem) {
    return false;
  }

  if (apk_service_->IsWebAppInstalledFromArc(app.AppId())) {
    return false;
  }

  if (!HasShareIntentFilter(app)) {
    return false;
  }

  return true;
}

void WebApkManager::QueueInstall(const std::string& app_id) {
  install_queue_->InstallOrUpdate(app_id);
}

void WebApkManager::QueueUpdate(const std::string& app_id) {
  // Mark the WebAPK as needing an update. This will be cleared when the update
  // is successful, and ensures that the update can be retried if it fails
  // before completion.
  webapk_prefs::SetUpdateNeededForApp(profile_, app_id,
                                      /* update_needed= */ true);
  install_queue_->InstallOrUpdate(app_id);
}

void WebApkManager::QueueUninstall(const std::string& app_id) {
  if (app_list_prefs_->package_list_initial_refreshed()) {
    // ArcApps is started and ready to receive uninstalls.
    UninstallInternal(app_id);
  } else {
    // Queue the install to happen once ArcApps is started.
    uninstall_queue_.push_back(app_id);
  }
}

void WebApkManager::UninstallInternal(const std::string& app_id) {
  auto* instance = ARC_GET_INSTANCE_FOR_METHOD(
      app_list_prefs_->app_connection_holder(), UninstallPackage);
  if (!instance) {
    return;
  }

  std::optional<std::string> package_name =
      webapk_prefs::GetWebApkPackageName(profile_, app_id);
  // Ignore cases where we try to uninstall a package which doesn't exist, as
  // it's possible that the uninstall request was queued multiple times.
  if (package_name) {
    instance->UninstallPackage(package_name.value());
  }
}

}  // namespace apps