chromium/chrome/browser/ash/app_list/search/arc/arc_playstore_search_provider.cc

// Copyright 2017 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/search/arc/arc_playstore_search_provider.h"

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

#include "ash/components/arc/app/arc_playstore_search_request_state.h"
#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ash/app_list/search/arc/arc_playstore_search_result.h"
#include "chrome/browser/ash/app_list/search/types.h"
#include "chrome/browser/ash/extensions/gfx_utils.h"
#include "chrome/browser/profiles/profile.h"

namespace {
constexpr int kHistogramBuckets = 13;
constexpr char kAppListPlayStoreQueryStateHistogram[] =
    "Apps.AppListPlayStoreQueryState";

// Skips Play Store apps that have equivalent extensions installed.
// Do not skip recent instant apps since they should be treated like
// on-device apps.
bool CanSkipSearchResult(content::BrowserContext* context,
                         const arc::mojom::AppDiscoveryResult& result) {
  if (result.is_instant_app && result.is_recent)
    return false;

  if (!result.package_name.has_value())
    return false;

  if (!extensions::util::GetEquivalentInstalledExtensions(
           context, result.package_name.value())
           .empty()) {
    return true;
  }

  // TODO(crbug.com/41343677): Remove this once we have a fix in Phonesky.
  // Don't show installed Android apps.
  const ArcAppListPrefs* arc_prefs = ArcAppListPrefs::Get(context);
  return arc_prefs && arc_prefs->GetPackage(result.package_name.value());
}

// Checks if we're receiving a result with invalid data from Android.
bool IsInvalidResult(const arc::mojom::AppDiscoveryResult& result) {
  // The result doesn't have a valid launch intent or install intent.
  if ((!result.launch_intent_uri || result.launch_intent_uri->empty()) &&
      (!result.install_intent_uri || result.install_intent_uri->empty())) {
    return true;
  }

  // The result doesn't have a valid label.
  if (!result.label || result.label->empty())
    return true;

  // The result doesn't have a valid launcher icon.
  if (!result.icon)
    return true;

  // The result doesn't have a valid package name.
  if (!result.package_name || result.package_name->empty())
    return true;

  return false;
}

}  // namespace

namespace app_list {

ArcPlayStoreSearchProvider::ArcPlayStoreSearchProvider(
    int max_results,
    Profile* profile,
    AppListControllerDelegate* list_controller)
    : SearchProvider(SearchCategory::kPlayStore),
      max_results_(max_results),
      profile_(profile),
      list_controller_(list_controller) {
  DCHECK_EQ(kHistogramBuckets, max_results + 1);
}

ArcPlayStoreSearchProvider::~ArcPlayStoreSearchProvider() = default;

ash::AppListSearchResultType ArcPlayStoreSearchProvider::ResultType() const {
  return ash::AppListSearchResultType::kPlayStoreApp;
}

void ArcPlayStoreSearchProvider::Start(const std::u16string& query) {
  last_query_ = query;

  arc::mojom::AppInstance* app_instance =
      arc::ArcServiceManager::Get()
          ? ARC_GET_INSTANCE_FOR_METHOD(
                arc::ArcServiceManager::Get()->arc_bridge_service()->app(),
                GetRecentAndSuggestedAppsFromPlayStore)
          : nullptr;

  DCHECK(!query.empty());
  if (app_instance == nullptr)
    return;

  app_instance->GetRecentAndSuggestedAppsFromPlayStore(
      base::UTF16ToUTF8(query), max_results_,
      base::BindOnce(&ArcPlayStoreSearchProvider::OnResults,
                     weak_ptr_factory_.GetWeakPtr(), query,
                     base::TimeTicks::Now()));
}

void ArcPlayStoreSearchProvider::StopQuery() {
  weak_ptr_factory_.InvalidateWeakPtrs();
  last_query_.clear();
}

void ArcPlayStoreSearchProvider::OnResults(
    const std::u16string& query,
    base::TimeTicks query_start_time,
    arc::ArcPlayStoreSearchRequestState state,
    std::vector<arc::mojom::AppDiscoveryResultPtr> results) {
  if (state != arc::ArcPlayStoreSearchRequestState::SUCCESS) {
    DCHECK(
        state ==
            arc::ArcPlayStoreSearchRequestState::PHONESKY_RESULT_INVALID_DATA ||
        results.empty());
    UMA_HISTOGRAM_ENUMERATION(kAppListPlayStoreQueryStateHistogram, state,
                              arc::ArcPlayStoreSearchRequestState::STATE_COUNT);

    // PHONESKY_RESULT_INVALID_DATA indicates that at least one of the apps
    // returned from playstore was invalid. The returned data may still contain
    // valid results - display them in the UI if that's the case.
    if (state !=
            arc::ArcPlayStoreSearchRequestState::PHONESKY_RESULT_INVALID_DATA ||
        results.empty()) {
      return;
    }
  }

  // Play store could have a long latency that when the results come back,
  // user has entered a different query. Do not return the staled results
  // from the previous query in such case.
  if (query != last_query_) {
    UMA_HISTOGRAM_ENUMERATION(
        kAppListPlayStoreQueryStateHistogram,
        arc::ArcPlayStoreSearchRequestState::FAILED_TO_CALL_CANCEL,
        arc::ArcPlayStoreSearchRequestState::STATE_COUNT);
    return;
  }

  SearchProvider::Results new_results;
  size_t instant_app_count = 0;
  bool has_invalid_result = false;
  for (auto& result : results) {
    if (IsInvalidResult(*result)) {
      has_invalid_result = true;
      continue;
    }

    if (result->is_instant_app)
      ++instant_app_count;

    if (CanSkipSearchResult(profile_, *result))
      continue;

    new_results.emplace_back(std::make_unique<ArcPlayStoreSearchResult>(
        std::move(result), list_controller_, last_query_));
  }
  SwapResults(&new_results);

  // Record user metrics.
  if (state == arc::ArcPlayStoreSearchRequestState::SUCCESS) {
    if (has_invalid_result) {
      UMA_HISTOGRAM_ENUMERATION(
          kAppListPlayStoreQueryStateHistogram,
          arc::ArcPlayStoreSearchRequestState::CHROME_GOT_INVALID_RESULT,
          arc::ArcPlayStoreSearchRequestState::STATE_COUNT);
    } else {
      UMA_HISTOGRAM_ENUMERATION(
          kAppListPlayStoreQueryStateHistogram,
          arc::ArcPlayStoreSearchRequestState::SUCCESS,
          arc::ArcPlayStoreSearchRequestState::STATE_COUNT);
    }
  }

  UMA_HISTOGRAM_TIMES("Arc.PlayStoreSearch.QueryTime",
                      base::TimeTicks::Now() - query_start_time);
  UMA_HISTOGRAM_EXACT_LINEAR("Arc.PlayStoreSearch.ReturnedAppsTotalV2",
                             results.size(), kHistogramBuckets);
  UMA_HISTOGRAM_EXACT_LINEAR("Arc.PlayStoreSearch.ReturnedUninstalledAppsV2",
                             results.size() - instant_app_count,
                             kHistogramBuckets);
  UMA_HISTOGRAM_EXACT_LINEAR("Arc.PlayStoreSearch.ReturnedInstantAppsV2",
                             instant_app_count, kHistogramBuckets);
}

}  // namespace app_list