chromium/components/arc/common/intent_helper/link_handler_model.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 "components/arc/common/intent_helper/link_handler_model.h"

#include <string_view>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "components/arc/common/intent_helper/arc_intent_helper_package.h"
#include "components/google/core/common/google_util.h"
#include "url/url_util.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/components/arc/metrics/arc_metrics_constants.h"
#include "ash/components/arc/metrics/arc_metrics_service.h"
#endif

namespace arc {

namespace {

constexpr int kMaxValueLen = 2048;

bool GetQueryValue(const GURL& url,
                   std::string_view key_to_find,
                   std::u16string* out) {
  const std::string_view str = url.query_piece();

  url::Component query(0, str.length());
  url::Component key;
  url::Component value;

  while (url::ExtractQueryKeyValue(str, &query, &key, &value)) {
    if (value.is_empty()) {
      continue;
    }
    if (str.substr(key.begin, key.len) == key_to_find) {
      if (value.len >= kMaxValueLen) {
        return false;
      }
      url::RawCanonOutputW<kMaxValueLen> output;
      url::DecodeURLEscapeSequences(str.substr(value.begin, value.len),
                                    url::DecodeURLMode::kUTF8OrIsomorphic,
                                    &output);
      *out = std::u16string(output.view());
      return true;
    }
  }
  return false;
}

}  // namespace

// static
std::unique_ptr<LinkHandlerModel> LinkHandlerModel::Create(
    content::BrowserContext* context,
    const GURL& link_url,
    std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate) {
  CHECK(mojo_delegate);
  auto impl = base::WrapUnique(new LinkHandlerModel(std::move(mojo_delegate)));
  if (!impl->Init(context, link_url)) {
    return nullptr;
  }
  return impl;
}

LinkHandlerModel::~LinkHandlerModel() = default;

void LinkHandlerModel::AddObserver(Observer* observer) {
  observer_list_.AddObserver(observer);
}

void LinkHandlerModel::OpenLinkWithHandler(uint32_t handler_id) {
  if (handler_id >= handlers_.size()) {
    return;
  }

  if (!mojo_delegate_->HandleUrl(url_.spec(),
                                 handlers_[handler_id].package_name)) {
    return;
  }

#if BUILDFLAG(IS_CHROMEOS_ASH)
  // TODO(crbug.com/40808069): Take metrics in Lacros as well.
  ArcMetricsService::RecordArcUserInteraction(
      context_, arc::UserInteractionType::APP_STARTED_FROM_LINK_CONTEXT_MENU);
#endif
}

LinkHandlerModel::LinkHandlerModel(
    std::unique_ptr<ArcIntentHelperMojoDelegate> mojo_delegate)
    : mojo_delegate_(std::move(mojo_delegate)) {}

bool LinkHandlerModel::Init(content::BrowserContext* context, const GURL& url) {
  DCHECK(context);
  context_ = context;

  // Check if ARC apps can handle the |url|. Since the information is held in
  // a different (ARC) process, issue a mojo IPC request. Usually, the
  // callback function, OnUrlHandlerList, is called within a few milliseconds
  // even on the slowest Chromebook we support.
  url_ = RewriteUrlFromQueryIfAvailable(url);

  return mojo_delegate_->RequestUrlHandlerList(
      url_.spec(), base::BindOnce(&LinkHandlerModel::OnUrlHandlerList,
                                  weak_ptr_factory_.GetWeakPtr()));
}

void LinkHandlerModel::OnUrlHandlerList(
    std::vector<ArcIntentHelperMojoDelegate::IntentHandlerInfo> handlers) {
  for (auto& handler : handlers) {
    if (handler.package_name == kArcIntentHelperPackageName) {
      continue;
    }
    handlers_.push_back(std::move(handler));
  }

  bool icon_info_notified = false;
  if (!ArcIconCacheDelegate::GetInstance()) {
    // ArcIconCacheDelegate instance should be already set on the product.
    // It is not set for some tests such as browser_tests since crosapi is
    // disabled. In this case, ignore the step to get icons and immediately
    // notify observers with no result.
    LOG(ERROR) << "ArcIconCacheDelegate is not set. "
               << "This should not happen except for testing.";
    NotifyObserver(nullptr);
    return;
  }

  std::vector<ArcIconCacheDelegate::ActivityName> activities;
  for (size_t i = 0; i < handlers_.size(); ++i) {
    activities.emplace_back(handlers_[i].package_name,
                            handlers_[i].activity_name);
  }
  const ArcIconCacheDelegate::GetResult result =
      ArcIconCacheDelegate::GetInstance()->GetActivityIcons(
          activities, base::BindOnce(&LinkHandlerModel::NotifyObserver,
                                     weak_ptr_factory_.GetWeakPtr()));
  icon_info_notified =
      ArcIconCacheDelegate::ActivityIconLoader::HasIconsReadyCallbackRun(
          result);

  if (!icon_info_notified) {
    // Call NotifyObserver() without icon information, unless
    // GetActivityIcons has already called it. Otherwise if we delay the
    // notification waiting for all icons, context menu may flicker.
    NotifyObserver(nullptr);
  }
}

void LinkHandlerModel::NotifyObserver(
    std::unique_ptr<ArcIconCacheDelegate::ActivityToIconsMap> icons) {
  if (icons) {
    icons_.insert(icons->begin(), icons->end());
    icons.reset();
  }

  std::vector<LinkHandlerInfo> handlers;
  for (size_t i = 0; i < handlers_.size(); ++i) {
    gfx::Image icon;
    const ArcIconCacheDelegate::ActivityName activity(
        handlers_[i].package_name, handlers_[i].activity_name);
    const auto it = icons_.find(activity);
    if (it != icons_.end())
      icon = it->second.icon16;
    // Use the handler's index as an ID.
    LinkHandlerInfo handler = {base::UTF8ToUTF16(handlers_[i].name), icon,
                               static_cast<uint32_t>(i)};
    handlers.push_back(handler);
  }
  for (auto& observer : observer_list_)
    observer.ModelChanged(handlers);
}

// static
GURL LinkHandlerModel::RewriteUrlFromQueryIfAvailableForTesting(
    const GURL& url) {
  return RewriteUrlFromQueryIfAvailable(url);
}

// static
GURL LinkHandlerModel::RewriteUrlFromQueryIfAvailable(const GURL& url) {
  static const char kPathToFind[] = "/url";
  static const char kKeyToFind[] = "url";

  if (!google_util::IsGoogleDomainUrl(url, google_util::DISALLOW_SUBDOMAIN,
                                      google_util::ALLOW_NON_STANDARD_PORTS)) {
    return url;
  }
  if (!url.has_path() || url.path() != kPathToFind) {
    return url;
  }

  std::u16string value;
  if (!GetQueryValue(url, kKeyToFind, &value)) {
    return url;
  }

  const GURL new_url(value);
  if (!new_url.is_valid()) {
    return url;
  }
  return new_url;
}

}  // namespace arc