First draft of an App Service search provider

Lots of TODOs, but there's enough implemented so that running "chrome
--enable-features=AppService" will populate the app list search box with
apps' names and icons, and clicking on the icon will launch the app.

BUG=826982

Change-Id: I564bbd9142061e95a91c1a0b63a70980c970d56d
Reviewed-on: https://chromium-review.googlesource.com/c/1362597
Commit-Queue: Nigel Tao <nigeltao@chromium.org>
Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
Reviewed-by: Dominick Ng <dominickn@chromium.org>
Cr-Commit-Position: refs/heads/master@{#614583}
diff --git a/chrome/browser/apps/app_service/built_in_chromeos_apps.cc b/chrome/browser/apps/app_service/built_in_chromeos_apps.cc
index 569562e..8826a09 100644
--- a/chrome/browser/apps/app_service/built_in_chromeos_apps.cc
+++ b/chrome/browser/apps/app_service/built_in_chromeos_apps.cc
@@ -12,6 +12,7 @@
 #include "chrome/browser/ui/app_list/internal_app/internal_app_item.h"
 #include "chrome/browser/ui/app_list/internal_app/internal_app_metadata.h"
 #include "chrome/browser/ui/app_list/search/internal_app_result.h"
+#include "chrome/browser/ui/app_list/search/search_util.h"
 #include "chrome/services/app_service/public/mojom/types.mojom.h"
 #include "mojo/public/cpp/bindings/interface_request.h"
 #include "ui/base/l10n/l10n_util.h"
@@ -38,6 +39,10 @@
   app->show_in_launcher = internal_app.show_in_launcher
                               ? apps::mojom::OptionalBool::kTrue
                               : apps::mojom::OptionalBool::kFalse;
+  app->show_in_search = internal_app.searchable
+                            ? apps::mojom::OptionalBool::kTrue
+                            : apps::mojom::OptionalBool::kFalse;
+
   return app;
 }
 
@@ -68,6 +73,16 @@
     // TODO(crbug.com/826982): move source of truth for built-in apps from
     // ui/app_list to here when the AppService feature is enabled by default.
     for (const auto& internal_app : app_list::GetInternalAppList(profile_)) {
+      // TODO(crbug.com/826982): support the "continue reading" app? Or leave
+      // it specifically to the chrome/browser/ui/app_list/search code? If
+      // moved here, it might mean calling sync_sessions::SessionSyncService's
+      // SubscribeToForeignSessionsChanged. See also app_search_provider.cc's
+      // InternalDataSource.
+      if (internal_app.internal_app_name ==
+          app_list::InternalAppName::kContinueReading) {
+        continue;
+      }
+
       apps::mojom::AppPtr app = Convert(internal_app);
       if (!app.is_null()) {
         apps.push_back(std::move(app));
@@ -108,17 +123,17 @@
   switch (launch_source) {
     case apps::mojom::LaunchSource::kUnknown:
       break;
-    case apps::mojom::LaunchSource::kFromAppList:
+    case apps::mojom::LaunchSource::kFromAppListGrid:
       InternalAppItem::RecordActiveHistogram(app_id);
       break;
-    case apps::mojom::LaunchSource::kFromAppListSearch:
+    case apps::mojom::LaunchSource::kFromAppListRecommendation:
+      app_list::InternalAppResult::RecordOpenHistogram(app_id);
+      break;
+    case apps::mojom::LaunchSource::kFromAppListQueryResult:
+      app_list::RecordHistogram(app_list::APP_SEARCH_RESULT);
       app_list::InternalAppResult::RecordOpenHistogram(app_id);
       break;
   }
-  // TODO(crbug.com/826982): we should also update a UMA histogram when an app
-  // result is *shown* in the app list search box, not just *launched*.
-  //
-  // See //chrome/browser/ui/app_list/search/internal_app_result.cc.
 
   app_list::OpenInternalApp(app_id, profile_, event_flags);
 }
diff --git a/chrome/browser/apps/app_service/extension_apps.cc b/chrome/browser/apps/app_service/extension_apps.cc
index 9e5cc9f..b94eebe 100644
--- a/chrome/browser/apps/app_service/extension_apps.cc
+++ b/chrome/browser/apps/app_service/extension_apps.cc
@@ -12,6 +12,7 @@
 #include "chrome/browser/extensions/extension_util.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/browser/ui/app_list/extension_app_utils.h"
+#include "chrome/browser/ui/app_list/search/search_util.h"
 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h"
 #include "chrome/common/extensions/extension_metrics.h"
 #include "chrome/services/app_service/public/mojom/types.mojom.h"
@@ -43,9 +44,10 @@
   switch (launch_source) {
     case apps::mojom::LaunchSource::kUnknown:
       return ash::LAUNCH_FROM_UNKNOWN;
-    case apps::mojom::LaunchSource::kFromAppList:
+    case apps::mojom::LaunchSource::kFromAppListGrid:
       return ash::LAUNCH_FROM_APP_LIST;
-    case apps::mojom::LaunchSource::kFromAppListSearch:
+    case apps::mojom::LaunchSource::kFromAppListRecommendation:
+    case apps::mojom::LaunchSource::kFromAppListQueryResult:
       return ash::LAUNCH_FROM_APP_LIST_SEARCH;
   }
 }
@@ -125,10 +127,13 @@
   switch (launch_source) {
     case apps::mojom::LaunchSource::kUnknown:
       break;
-    case apps::mojom::LaunchSource::kFromAppList:
+    case apps::mojom::LaunchSource::kFromAppListGrid:
       extensions::RecordAppListMainLaunch(extension);
       break;
-    case apps::mojom::LaunchSource::kFromAppListSearch:
+    case apps::mojom::LaunchSource::kFromAppListRecommendation:
+      break;
+    case apps::mojom::LaunchSource::kFromAppListQueryResult:
+      app_list::RecordHistogram(app_list::APP_SEARCH_RESULT);
       extensions::RecordAppListSearchLaunch(extension);
       break;
   }
@@ -152,9 +157,12 @@
   app->icon_key->s_key = extension->id();
   app->icon_key->u_key = next_u_key_++;
 
-  app->show_in_launcher = app_list::ShouldShowInLauncher(extension, profile_)
-                              ? apps::mojom::OptionalBool::kTrue
-                              : apps::mojom::OptionalBool::kFalse;
+  auto show = app_list::ShouldShowInLauncher(extension, profile_)
+                  ? apps::mojom::OptionalBool::kTrue
+                  : apps::mojom::OptionalBool::kFalse;
+  app->show_in_launcher = show;
+  app->show_in_search = show;
+
   return app;
 }
 
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 04412c96..d3582a7 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -3232,6 +3232,8 @@
         "app_list/page_break_app_item.h",
         "app_list/page_break_constants.cc",
         "app_list/page_break_constants.h",
+        "app_list/search/app_service_app_result.cc",
+        "app_list/search/app_service_app_result.h",
         "app_list/search/arc_app_result.cc",
         "app_list/search/arc_app_result.h",
         "app_list/search/crostini_app_result.cc",
diff --git a/chrome/browser/ui/app_list/app_service_app_item.cc b/chrome/browser/ui/app_list/app_service_app_item.cc
index 407cb652..e8c5a43 100644
--- a/chrome/browser/ui/app_list/app_service_app_item.cc
+++ b/chrome/browser/ui/app_list/app_service_app_item.cc
@@ -47,7 +47,8 @@
 void AppServiceAppItem::Activate(int event_flags) {
   apps::AppServiceProxy* proxy = apps::AppServiceProxy::Get(profile());
   if (proxy) {
-    proxy->Launch(id(), event_flags, apps::mojom::LaunchSource::kFromAppList,
+    proxy->Launch(id(), event_flags,
+                  apps::mojom::LaunchSource::kFromAppListGrid,
                   GetController()->GetAppListDisplayId());
   }
 }
diff --git a/chrome/browser/ui/app_list/search/app_search_provider.cc b/chrome/browser/ui/app_list/search/app_search_provider.cc
index 073d435..6e38eb4 100644
--- a/chrome/browser/ui/app_list/search/app_search_provider.cc
+++ b/chrome/browser/ui/app_list/search/app_search_provider.cc
@@ -26,6 +26,7 @@
 #include "base/strings/utf_string_conversions.h"
 #include "base/threading/thread_task_runner_handle.h"
 #include "base/time/clock.h"
+#include "chrome/browser/apps/app_service/app_service_proxy.h"
 #include "chrome/browser/chromeos/arc/arc_util.h"
 #include "chrome/browser/chromeos/crostini/crostini_manager.h"
 #include "chrome/browser/chromeos/crostini/crostini_registry_service.h"
@@ -44,11 +45,13 @@
 #include "chrome/browser/ui/app_list/chrome_app_list_item.h"
 #include "chrome/browser/ui/app_list/extension_app_utils.h"
 #include "chrome/browser/ui/app_list/internal_app/internal_app_metadata.h"
+#include "chrome/browser/ui/app_list/search/app_service_app_result.h"
 #include "chrome/browser/ui/app_list/search/arc_app_result.h"
 #include "chrome/browser/ui/app_list/search/crostini_app_result.h"
 #include "chrome/browser/ui/app_list/search/extension_app_result.h"
 #include "chrome/browser/ui/app_list/search/internal_app_result.h"
 #include "chrome/browser/ui/app_list/search/search_result_ranker/app_search_result_ranker.h"
+#include "chrome/common/chrome_features.h"
 #include "chrome/common/pref_names.h"
 #include "chrome/grit/generated_resources.h"
 #include "components/sync/base/model_type.h"
@@ -247,6 +250,57 @@
 
 namespace {
 
+class AppServiceDataSource : public AppSearchProvider::DataSource {
+ public:
+  AppServiceDataSource(Profile* profile, AppSearchProvider* owner)
+      : AppSearchProvider::DataSource(profile, owner) {
+    // TODO(crbug.com/826982): observe the cache for apps being installed and
+    // uninstalled, and in the callback, call RefreshAppsAndUpdateResultsXxx().
+  }
+
+  ~AppServiceDataSource() override = default;
+
+  // AppSearchProvider::DataSource overrides:
+  void AddApps(AppSearchProvider::Apps* apps_vector) override {
+    apps::AppServiceProxy* proxy = apps::AppServiceProxy::Get(profile());
+    if (!proxy) {
+      return;
+    }
+    proxy->Cache().ForEachApp(
+        [this, apps_vector](const apps::AppUpdate& update) {
+          if (update.ShowInSearch() != apps::mojom::OptionalBool::kTrue) {
+            return;
+          }
+
+          // TODO(crbug.com/826982): add the "can load in incognito" concept to
+          // the App Service and use it here, similar to ExtensionDataSource.
+
+          apps_vector->emplace_back(std::make_unique<AppSearchProvider::App>(
+              this, update.AppId(),
+              // TODO(crbug.com/826982): add the "short name" concept to the App
+              // Service, and use it here.
+              update.Name(),
+              // TODO(crbug.com/826982): add the "last launch time" and "install
+              // time" concepts to the App Service, and use them here.
+              base::Time(), base::Time(),
+              // TODO(crbug.com/826982): add the "installed internally" concept
+              // to the App Service, and use it here.
+              true));
+        });
+  }
+
+  std::unique_ptr<AppResult> CreateResult(
+      const std::string& app_id,
+      AppListControllerDelegate* list_controller,
+      bool is_recommended) override {
+    return std::make_unique<AppServiceAppResult>(
+        profile(), app_id, list_controller, is_recommended);
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(AppServiceDataSource);
+};
+
 class ExtensionDataSource : public AppSearchProvider::DataSource,
                             public extensions::ExtensionRegistryObserver {
  public:
@@ -392,8 +446,11 @@
 
 class InternalDataSource : public AppSearchProvider::DataSource {
  public:
-  InternalDataSource(Profile* profile, AppSearchProvider* owner)
-      : AppSearchProvider::DataSource(profile, owner) {
+  InternalDataSource(Profile* profile,
+                     AppSearchProvider* owner,
+                     bool just_continue_reading)
+      : AppSearchProvider::DataSource(profile, owner),
+        just_continue_reading_(just_continue_reading) {
     sync_sessions::SessionSyncService* service =
         SessionSyncServiceFactory::GetInstance()->GetForProfile(profile);
     if (!service)
@@ -420,6 +477,8 @@
         if (!service || !service->GetOpenTabsUIDelegate()) {
           continue;
         }
+      } else if (just_continue_reading_) {
+        continue;
       }
 
       apps->emplace_back(std::make_unique<AppSearchProvider::App>(
@@ -445,6 +504,15 @@
   }
 
  private:
+  // Whether InternalDataSource provides just the kInternalAppIdContinueReading
+  // app. If true, other internal apps are provided by AppServiceDataSource.
+  //
+  // TODO(crbug.com/826982): move the "foreign session updated subscription"
+  // into the App Service? Or if, in terms of UI, "continue reading" is exposed
+  // only in the app list search UI, it might make more sense to leave it in
+  // this code. See also built_in_chromeos_apps.cc.
+  bool just_continue_reading_;
+
   std::unique_ptr<base::CallbackList<void()>::Subscription>
       foreign_session_updated_subscription_;
 
@@ -535,16 +603,25 @@
           chromeos::ProfileHelper::IsEphemeralUserProfile(profile))),
       refresh_apps_factory_(this),
       update_results_factory_(this) {
-  data_sources_.emplace_back(
-      std::make_unique<ExtensionDataSource>(profile, this));
-  if (arc::IsArcAllowedForProfile(profile))
-    data_sources_.emplace_back(std::make_unique<ArcDataSource>(profile, this));
-  if (crostini::IsCrostiniUIAllowedForProfile(profile)) {
+  bool app_service_enabled =
+      base::FeatureList::IsEnabled(features::kAppService);
+  if (app_service_enabled) {
     data_sources_.emplace_back(
-        std::make_unique<CrostiniDataSource>(profile, this));
+        std::make_unique<AppServiceDataSource>(profile, this));
+  } else {
+    data_sources_.emplace_back(
+        std::make_unique<ExtensionDataSource>(profile, this));
+    if (arc::IsArcAllowedForProfile(profile)) {
+      data_sources_.emplace_back(
+          std::make_unique<ArcDataSource>(profile, this));
+    }
+    if (crostini::IsCrostiniUIAllowedForProfile(profile)) {
+      data_sources_.emplace_back(
+          std::make_unique<CrostiniDataSource>(profile, this));
+    }
   }
   data_sources_.emplace_back(
-      std::make_unique<InternalDataSource>(profile, this));
+      std::make_unique<InternalDataSource>(profile, this, app_service_enabled));
 }
 
 AppSearchProvider::~AppSearchProvider() {}
diff --git a/chrome/browser/ui/app_list/search/app_service_app_result.cc b/chrome/browser/ui/app_list/search/app_service_app_result.cc
new file mode 100644
index 0000000..b0a7001
--- /dev/null
+++ b/chrome/browser/ui/app_list/search/app_service_app_result.cc
@@ -0,0 +1,98 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/app_list/search/app_service_app_result.h"
+
+#include "ash/public/cpp/app_list/app_list_config.h"
+#include "ash/public/cpp/app_list/app_list_types.h"
+#include "chrome/browser/apps/app_service/app_service_proxy.h"
+#include "chrome/browser/ui/app_list/app_list_client_impl.h"
+#include "chrome/browser/ui/app_list/search/internal_app_result.h"
+#include "extensions/common/extension.h"
+
+namespace app_list {
+
+AppServiceAppResult::AppServiceAppResult(Profile* profile,
+                                         const std::string& app_id,
+                                         AppListControllerDelegate* controller,
+                                         bool is_recommendation)
+    : AppResult(profile, app_id, controller, is_recommendation),
+      weak_ptr_factory_(this) {
+  auto app_type = apps::mojom::AppType::kUnknown;
+  apps::AppServiceProxy* proxy = apps::AppServiceProxy::Get(profile);
+
+  if (proxy) {
+    app_type = proxy->Cache().GetAppType(app_id);
+
+    proxy->LoadIcon(
+        app_id, apps::mojom::IconCompression::kUncompressed,
+        AppListConfig::instance().GetPreferredIconDimension(display_type()),
+        base::BindOnce(&AppServiceAppResult::OnLoadIcon,
+                       weak_ptr_factory_.GetWeakPtr(), false));
+
+    if (display_type() == ash::SearchResultDisplayType::kRecommendation) {
+      proxy->LoadIcon(
+          app_id, apps::mojom::IconCompression::kUncompressed,
+          AppListConfig::instance().suggestion_chip_icon_dimension(),
+          base::BindOnce(&AppServiceAppResult::OnLoadIcon,
+                         weak_ptr_factory_.GetWeakPtr(), true));
+    }
+  }
+
+  switch (app_type) {
+    case apps::mojom::AppType::kBuiltIn:
+      set_id(app_id);
+      // TODO(crbug.com/826982): Is this SetResultType call necessary?? Does
+      // anyone care about the kInternalApp vs kInstalledApp distinction?
+      SetResultType(ResultType::kInternalApp);
+      // TODO(crbug.com/826982): Move this from the App Service caller to
+      // callee, closer to where other histograms are updated in
+      // BuiltInChromeOsApps::Launch??
+      InternalAppResult::RecordShowHistogram(app_id);
+      break;
+    case apps::mojom::AppType::kExtension:
+      // TODO(crbug.com/826982): why do we pass the URL and not the app_id??
+      // Can we replace this by the simpler "set_id(app_id)", and therefore
+      // pull that out of the switch?
+      set_id(extensions::Extension::GetBaseURLFromExtensionId(app_id).spec());
+      break;
+    default:
+      set_id(app_id);
+      break;
+  }
+}
+
+AppServiceAppResult::~AppServiceAppResult() = default;
+
+void AppServiceAppResult::Open(int event_flags) {
+  apps::AppServiceProxy* proxy = apps::AppServiceProxy::Get(profile());
+  if (proxy) {
+    auto launch_source =
+        (display_type() == ash::SearchResultDisplayType::kRecommendation)
+            ? apps::mojom::LaunchSource::kFromAppListRecommendation
+            : apps::mojom::LaunchSource::kFromAppListQueryResult;
+
+    proxy->Launch(app_id(), event_flags, launch_source,
+                  controller()->GetAppListDisplayId());
+  }
+}
+
+void AppServiceAppResult::ExecuteLaunchCommand(int event_flags) {
+  Open(event_flags);
+}
+
+void AppServiceAppResult::OnLoadIcon(bool chip,
+                                     apps::mojom::IconValuePtr icon_value) {
+  if (icon_value->icon_compression !=
+      apps::mojom::IconCompression::kUncompressed) {
+    return;
+  }
+  if (chip) {
+    SetChipIcon(icon_value->uncompressed);
+  } else {
+    SetIcon(icon_value->uncompressed);
+  }
+}
+
+}  // namespace app_list
diff --git a/chrome/browser/ui/app_list/search/app_service_app_result.h b/chrome/browser/ui/app_list/search/app_service_app_result.h
new file mode 100644
index 0000000..dd99bfd
--- /dev/null
+++ b/chrome/browser/ui/app_list/search/app_service_app_result.h
@@ -0,0 +1,43 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_APP_LIST_SEARCH_APP_SERVICE_APP_RESULT_H_
+#define CHROME_BROWSER_UI_APP_LIST_SEARCH_APP_SERVICE_APP_RESULT_H_
+
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/ui/app_list/search/app_result.h"
+#include "chrome/services/app_service/public/mojom/types.mojom.h"
+
+class AppListControllerDelegate;
+class Profile;
+
+namespace app_list {
+
+class AppServiceAppResult : public AppResult {
+ public:
+  AppServiceAppResult(Profile* profile,
+                      const std::string& app_id,
+                      AppListControllerDelegate* controller,
+                      bool is_recommendation);
+  ~AppServiceAppResult() override;
+
+ private:
+  // ChromeSearchResult overrides:
+  void Open(int event_flags) override;
+
+  // AppContextMenuDelegate overrides:
+  void ExecuteLaunchCommand(int event_flags) override;
+
+  void OnLoadIcon(bool chip, apps::mojom::IconValuePtr icon_value);
+
+  // TODO(crbug.com/826982): implement context menus.
+
+  base::WeakPtrFactory<AppServiceAppResult> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(AppServiceAppResult);
+};
+
+}  // namespace app_list
+
+#endif  // CHROME_BROWSER_UI_APP_LIST_SEARCH_APP_SERVICE_APP_RESULT_H_
diff --git a/chrome/services/app_service/public/cpp/app_registry_cache.cc b/chrome/services/app_service/public/cpp/app_registry_cache.cc
index 83053dcd..b1546ea 100644
--- a/chrome/services/app_service/public/cpp/app_registry_cache.cc
+++ b/chrome/services/app_service/public/cpp/app_registry_cache.cc
@@ -77,4 +77,12 @@
   }
 }
 
+apps::mojom::AppType AppRegistryCache::GetAppType(const std::string& app_id) {
+  auto iter = states_.find(app_id);
+  if (iter != states_.end()) {
+    return iter->second->app_type;
+  }
+  return apps::mojom::AppType::kUnknown;
+}
+
 }  // namespace apps
diff --git a/chrome/services/app_service/public/cpp/app_registry_cache.h b/chrome/services/app_service/public/cpp/app_registry_cache.h
index 710cc18..7e1bab8 100644
--- a/chrome/services/app_service/public/cpp/app_registry_cache.h
+++ b/chrome/services/app_service/public/cpp/app_registry_cache.h
@@ -69,6 +69,8 @@
   // merges the cached states with the deltas.
   void OnApps(std::vector<apps::mojom::AppPtr> deltas);
 
+  apps::mojom::AppType GetAppType(const std::string& app_id);
+
   // Calls f, a void-returning function whose arguments are (const
   // apps::AppUpdate&), on each app in the cache. The AppUpdate (a
   // state-and-delta) is equivalent to the delta being "all unknown" or "no
diff --git a/chrome/services/app_service/public/cpp/app_update.cc b/chrome/services/app_service/public/cpp/app_update.cc
index db04497..c15b24c 100644
--- a/chrome/services/app_service/public/cpp/app_update.cc
+++ b/chrome/services/app_service/public/cpp/app_update.cc
@@ -27,6 +27,9 @@
   if (delta->show_in_launcher != apps::mojom::OptionalBool::kUnknown) {
     state->show_in_launcher = delta->show_in_launcher;
   }
+  if (delta->show_in_search != apps::mojom::OptionalBool::kUnknown) {
+    state->show_in_search = delta->show_in_search;
+  }
 
   // When adding new fields to the App Mojo type, this function should also be
   // updated.
@@ -97,4 +100,15 @@
          (delta_->show_in_launcher != state_->show_in_launcher);
 }
 
+apps::mojom::OptionalBool AppUpdate::ShowInSearch() const {
+  return (delta_->show_in_search != apps::mojom::OptionalBool::kUnknown)
+             ? delta_->show_in_search
+             : state_->show_in_search;
+}
+
+bool AppUpdate::ShowInSearchChanged() const {
+  return (delta_->show_in_search != apps::mojom::OptionalBool::kUnknown) &&
+         (delta_->show_in_search != state_->show_in_search);
+}
+
 }  // namespace apps
diff --git a/chrome/services/app_service/public/cpp/app_update.h b/chrome/services/app_service/public/cpp/app_update.h
index ac875ac..2a7aa0f 100644
--- a/chrome/services/app_service/public/cpp/app_update.h
+++ b/chrome/services/app_service/public/cpp/app_update.h
@@ -58,6 +58,9 @@
   apps::mojom::OptionalBool ShowInLauncher() const;
   bool ShowInLauncherChanged() const;
 
+  apps::mojom::OptionalBool ShowInSearch() const;
+  bool ShowInSearchChanged() const;
+
  private:
   const apps::mojom::AppPtr& state_;
   const apps::mojom::AppPtr& delta_;
diff --git a/chrome/services/app_service/public/cpp/app_update_unittest.cc b/chrome/services/app_service/public/cpp/app_update_unittest.cc
index f66929d..26dfdde 100644
--- a/chrome/services/app_service/public/cpp/app_update_unittest.cc
+++ b/chrome/services/app_service/public/cpp/app_update_unittest.cc
@@ -135,4 +135,24 @@
   EXPECT_FALSE(u.IconKey().is_null());
   EXPECT_EQ(apps::mojom::IconType::kExtension, u.IconKey()->icon_type);
   EXPECT_FALSE(u.IconKeyChanged());
+
+  // ShowInSearch tests.
+
+  EXPECT_EQ(apps::mojom::OptionalBool::kUnknown, u.ShowInSearch());
+  EXPECT_FALSE(u.ShowInSearchChanged());
+
+  state->show_in_search = apps::mojom::OptionalBool::kFalse;
+
+  EXPECT_EQ(apps::mojom::OptionalBool::kFalse, u.ShowInSearch());
+  EXPECT_FALSE(u.ShowInSearchChanged());
+
+  delta->show_in_search = apps::mojom::OptionalBool::kTrue;
+
+  EXPECT_EQ(apps::mojom::OptionalBool::kTrue, u.ShowInSearch());
+  EXPECT_TRUE(u.ShowInSearchChanged());
+
+  apps::AppUpdate::Merge(state.get(), delta);
+
+  EXPECT_EQ(apps::mojom::OptionalBool::kTrue, u.ShowInSearch());
+  EXPECT_FALSE(u.ShowInSearchChanged());
 }
diff --git a/chrome/services/app_service/public/mojom/types.mojom b/chrome/services/app_service/public/mojom/types.mojom
index 85c2f71..5dfbd67 100644
--- a/chrome/services/app_service/public/mojom/types.mojom
+++ b/chrome/services/app_service/public/mojom/types.mojom
@@ -16,7 +16,11 @@
   Readiness readiness;
   string? name;
   IconKey? icon_key;
+
+  // TODO(nigeltao): be more principled, instead of ad hoc show_in_xxx and
+  // show_in_yyy fields?
   OptionalBool show_in_launcher;
+  OptionalBool show_in_search;
 
   // When adding new fields, also update the Merge method and other helpers in
   // chrome/services/app_service/public/cpp/app_update.*
@@ -76,6 +80,7 @@
 
 enum LaunchSource {
   kUnknown,
-  kFromAppList,
-  kFromAppListSearch,
+  kFromAppListGrid,            // Grid of apps, not the search box.
+  kFromAppListRecommendation,  // Query-less recommendations (smaller icons).
+  kFromAppListQueryResult,     // Query-dependent results (larger icons).
 };