summaryrefslogtreecommitdiff
path: root/chromium/components/services
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-12 14:27:29 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2020-10-13 09:35:20 +0000
commitc30a6232df03e1efbd9f3b226777b07e087a1122 (patch)
treee992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/components/services
parent7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff)
downloadqtwebengine-chromium-85-based.tar.gz
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057 Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/components/services')
-rw-r--r--chromium/components/services/app_service/BUILD.gn38
-rw-r--r--chromium/components/services/app_service/DEPS43
-rw-r--r--chromium/components/services/app_service/README.md504
-rw-r--r--chromium/components/services/app_service/app_service_impl.cc493
-rw-r--r--chromium/components/services/app_service/app_service_impl.h168
-rw-r--r--chromium/components/services/app_service/app_service_impl_unittest.cc405
-rw-r--r--chromium/components/services/app_service/public/cpp/BUILD.gn129
-rw-r--r--chromium/components/services/app_service/public/cpp/app_registry_cache.cc133
-rw-r--r--chromium/components/services/app_service/public/cpp/app_registry_cache.h201
-rw-r--r--chromium/components/services/app_service/public/cpp/app_registry_cache_unittest.cc384
-rw-r--r--chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.cc46
-rw-r--r--chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.h46
-rw-r--r--chromium/components/services/app_service/public/cpp/app_update.cc527
-rw-r--r--chromium/components/services/app_service/public/cpp/app_update.h148
-rw-r--r--chromium/components/services/app_service/public/cpp/app_update_unittest.cc798
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_cache.cc157
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_cache.h118
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_cache_unittest.cc214
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_coalescer.cc212
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_coalescer.h102
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_coalescer_unittest.cc341
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_loader.cc79
-rw-r--r--chromium/components/services/app_service/public/cpp/icon_loader.h111
-rw-r--r--chromium/components/services/app_service/public/cpp/instance.cc36
-rw-r--r--chromium/components/services/app_service/public/cpp/instance.h65
-rw-r--r--chromium/components/services/app_service/public/cpp/instance_registry.cc154
-rw-r--r--chromium/components/services/app_service/public/cpp/instance_registry.h181
-rw-r--r--chromium/components/services/app_service/public/cpp/instance_registry_unittest.cc593
-rw-r--r--chromium/components/services/app_service/public/cpp/instance_update.cc161
-rw-r--r--chromium/components/services/app_service/public/cpp/instance_update.h79
-rw-r--r--chromium/components/services/app_service/public/cpp/instance_update_unittest.cc215
-rw-r--r--chromium/components/services/app_service/public/cpp/intent_filter_util.cc128
-rw-r--r--chromium/components/services/app_service/public/cpp/intent_filter_util.h81
-rw-r--r--chromium/components/services/app_service/public/cpp/intent_test_util.cc49
-rw-r--r--chromium/components/services/app_service/public/cpp/intent_test_util.h24
-rw-r--r--chromium/components/services/app_service/public/cpp/intent_util.cc123
-rw-r--r--chromium/components/services/app_service/public/cpp/intent_util.h33
-rw-r--r--chromium/components/services/app_service/public/cpp/intent_util_unittest.cc268
-rw-r--r--chromium/components/services/app_service/public/cpp/preferred_apps_converter.cc164
-rw-r--r--chromium/components/services/app_service/public/cpp/preferred_apps_converter.h54
-rw-r--r--chromium/components/services/app_service/public/cpp/preferred_apps_converter_unittest.cc471
-rw-r--r--chromium/components/services/app_service/public/cpp/preferred_apps_list.cc142
-rw-r--r--chromium/components/services/app_service/public/cpp/preferred_apps_list.h70
-rw-r--r--chromium/components/services/app_service/public/cpp/preferred_apps_list_unittest.cc585
-rw-r--r--chromium/components/services/app_service/public/cpp/publisher_base.cc126
-rw-r--r--chromium/components/services/app_service/public/cpp/publisher_base.h89
-rw-r--r--chromium/components/services/app_service/public/cpp/stub_icon_loader.cc53
-rw-r--r--chromium/components/services/app_service/public/cpp/stub_icon_loader.h45
-rw-r--r--chromium/components/services/app_service/public/mojom/BUILD.gn21
-rw-r--r--chromium/components/services/app_service/public/mojom/app_service.mojom235
-rw-r--r--chromium/components/services/app_service/public/mojom/types.mojom320
-rw-r--r--chromium/components/services/heap_profiling/json_exporter_unittest.cc10
-rw-r--r--chromium/components/services/paint_preview_compositor/BUILD.gn2
-rw-r--r--chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.cc157
-rw-r--r--chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.h15
-rw-r--r--chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl_unittest.cc9
-rw-r--r--chromium/components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom3
-rw-r--r--chromium/components/services/paint_preview_compositor/skp_result.cc15
-rw-r--r--chromium/components/services/paint_preview_compositor/skp_result.h31
-rw-r--r--chromium/components/services/print_compositor/print_compositor_impl.cc14
-rw-r--r--chromium/components/services/print_compositor/print_compositor_impl.h5
-rw-r--r--chromium/components/services/storage/BUILD.gn2
-rw-r--r--chromium/components/services/storage/dom_storage/DEPS3
-rw-r--r--chromium/components/services/storage/dom_storage/local_storage_impl.cc2
-rw-r--r--chromium/components/services/storage/dom_storage/session_storage_impl_unittest.cc7
-rw-r--r--chromium/components/services/storage/dom_storage/session_storage_metadata.cc1
-rw-r--r--chromium/components/services/storage/indexed_db/leveldb/leveldb_factory.cc1
-rw-r--r--chromium/components/services/storage/indexed_db/leveldb/leveldb_state.cc7
-rw-r--r--chromium/components/services/storage/indexed_db/scopes/leveldb_scope.h2
-rw-r--r--chromium/components/services/storage/indexed_db/scopes/scope_lock.h1
-rw-r--r--chromium/components/services/storage/indexed_db/scopes/scope_lock_range.h1
-rw-r--r--chromium/components/services/storage/indexed_db/scopes/scopes_lock_manager.h1
-rw-r--r--chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.cc6
-rw-r--r--chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.h5
-rw-r--r--chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction_unittest.cc83
-rw-r--r--chromium/components/services/storage/public/mojom/BUILD.gn3
-rw-r--r--chromium/components/services/storage/public/mojom/blob_storage_context.mojom88
-rw-r--r--chromium/components/services/storage/public/mojom/indexed_db_control.mojom13
-rw-r--r--chromium/components/services/storage/public/mojom/service_worker_storage_control.mojom46
79 files changed, 10358 insertions, 137 deletions
diff --git a/chromium/components/services/app_service/BUILD.gn b/chromium/components/services/app_service/BUILD.gn
new file mode 100644
index 00000000000..6f59daea0fc
--- /dev/null
+++ b/chromium/components/services/app_service/BUILD.gn
@@ -0,0 +1,38 @@
+# 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.
+
+source_set("lib") {
+ sources = [
+ "app_service_impl.cc",
+ "app_service_impl.h",
+ ]
+
+ deps = [
+ "//base",
+ "//components/prefs",
+ "//content/public/browser",
+ "//mojo/public/cpp/bindings",
+ ]
+
+ public_deps = [
+ "//components/services/app_service/public/cpp:preferred_apps",
+ "//components/services/app_service/public/mojom",
+ ]
+}
+
+source_set("unit_tests") {
+ testonly = true
+
+ sources = [ "app_service_impl_unittest.cc" ]
+
+ deps = [
+ ":lib",
+ "//components/prefs:test_support",
+ "//components/services/app_service/public/cpp:intents",
+ "//components/services/app_service/public/cpp:preferred_apps",
+ "//components/services/app_service/public/cpp:publisher",
+ "//content/test:test_support",
+ "//testing/gtest",
+ ]
+}
diff --git a/chromium/components/services/app_service/DEPS b/chromium/components/services/app_service/DEPS
new file mode 100644
index 00000000000..895523f0e50
--- /dev/null
+++ b/chromium/components/services/app_service/DEPS
@@ -0,0 +1,43 @@
+include_rules = [
+ "+components/prefs",
+ "+components/services/app_service/public/cpp",
+ "+content/public",
+]
+
+specific_include_rules = {
+ "app_registry_cache_wrapper\.cc": [
+ "+components/account_id/account_id.h",
+ ],
+ "app_registry_cache\.h": [
+ "+components/account_id/account_id.h",
+ ],
+ "app_update\.h": [
+ "+components/account_id/account_id.h",
+ ],
+ "instance_registry\.h": [
+ "+ash/public/cpp/shelf_types.h",
+ ],
+ "icon_cache\.h": [
+ "+ui/gfx/image/image_skia.h",
+ ],
+ "instance\.h": [
+ "+ui/aura/window.h",
+ "+ui/gfx/image/image_skia_rep.h",
+ ],
+ "icon_cache_unittest\.cc": [
+ "+chrome/test/base/testing_profile.h",
+ "+ui/gfx/geometry/size.h",
+ "+ui/gfx/image/image_skia_rep.h",
+ ],
+ "instance_registry_unittest\.cc": [
+ "+ui/aura/window.h",
+ "+chrome/test/base/testing_profile.h",
+ ],
+ "instance_update_unittest\.cc": [
+ "+chrome/test/base/testing_profile.h",
+ ],
+ "stub_icon_loader\.cc": [
+ "+ui/gfx/image/image_skia.h",
+ "+ui/gfx/image/image_skia_rep.h",
+ ],
+}
diff --git a/chromium/components/services/app_service/README.md b/chromium/components/services/app_service/README.md
index 0c212347f3e..7e979941dfd 100644
--- a/chromium/components/services/app_service/README.md
+++ b/chromium/components/services/app_service/README.md
@@ -1,5 +1,501 @@
-This directory houses the App Service.
+# App Service
-A future CL will move the App Service from its current home in
-//chrome/services/app_service to here to allow the App Service
-to be used from targets that cannot depend on //chrome (notably, //ash).
+Chrome, and especially Chrome OS, has apps, e.g. chat apps and camera apps.
+
+There are a number (lets call it `M`) of different places or app Consumers,
+usually UI (user interfaces) related, where users interact with their installed
+apps: e.g. launcher, search bar, shelf, New Tab Page, the App Management
+settings page, permissions or settings pages, picking and running default
+handlers for URLs, MIME types or intents, etc.
+
+There is also a different number (`N`) of app platforms or app Providers:
+built-in apps, extension-backed apps, PWAs (progressive web apps), ARC++
+(Android apps), Crostini (Linux apps), etc.
+
+Historically, each of the `M` Consumers hard-coded each of the `N` Providers,
+leading to `M×N` code paths that needed maintaining, or updating every time `M`
+or `N` was incremented.
+
+This document describes the App Service, an intermediary between app Consumers
+and app Providers. This simplifies `M×N` code paths to `M+N` code paths, each
+side with a uniform API, reducing code duplication and improving behavioral
+consistency. This service (a Mojo service) could potentially be spun out into a
+new process, for the usual
+[Servicification](https://www.chromium.org/servicification) benefits (e.g.
+self-contained services are easier to test and to sandbox), and would also
+facilitate Chrome OS apps that aren't tied to the browser, e.g. Ash apps.
+
+The App Service can be decomposed into a number of aspects. In all cases, it
+provides to Consumers a uniform API over the various Provider implementations,
+for these aspects:
+
+ - App Registry: list the installed apps.
+ - App Icon Factory: load an app's icon, at various resolutions.
+ - App Runner: launch apps and track app instances.
+ - App Installer: install, uninstall and update apps.
+ - App Coordinator: keep system-wide settings, e.g. default handlers.
+
+Some things are still the responsbility of individual Consumers or Providers.
+For example, the order in which the apps' icons are presented in the launcher
+is a launcher-specific detail, not a system-wide detail, and is managed by the
+launcher, not the App Service. Similarly, Android-specific VM (Virtual Machine)
+configuration is Android-specific, not generalizable system-wide, and is
+managed by the Android provider (ARC++).
+
+
+## Profiles
+
+Talk of *the* App Service is an over-simplification. There will be *an* App
+Service instance per Profile, as apps can be installed for *a* Profile.
+
+Note that this doesn't require the App Service to know about Profiles. Instead,
+Profile-specific code (e.g. a KeyedService) finds the Mojo service Connector
+for a Profile, creates an App Service and binds the two (App Service and
+Connector), but the App Service itself doesn't know about Profiles per se.
+
+
+# App Registry
+
+The App Registry's one-liner mission is:
+
+ - I would like to be able to for-each over all the apps in Chrome.
+
+An obvious initial design for the App Registry involves three actors (Consumers
+⇔ Service ⇔ Providers) with the middle actor (the App Registry Mojo service)
+being a relatively thick implementation with a traditional `GetFoo`, `SetBar`,
+`AddObserver` style API. The drawback is that Consumers are often UI surfaces
+and UI code likes synchronous APIs, but Mojo APIs are generally asynchronous,
+especially as it may cross process boundaries.
+
+Instead, we use four actors (Consumers ↔ Proxy ⇔ Service ⇔ Providers), with the
+Consumers ↔ Proxy connection being synchronous and in-process, lighter than the
+async / out-of-process ⇔ connections. The Proxy implementation is relatively
+thick and the Service implementation is relatively thin, almost trivially so.
+Being able to for-each over all the apps is:
+
+ for (const auto& app : GetAppServiceProxy(profile).GetCache().GetAllApps()) {
+ DoSomethingWith(app);
+ }
+
+The Proxy is expected to be in the same process as its Consumers, and the Proxy
+would be a singleton (per Profile) within that process: Consumers would connect
+to *the* in-process Proxy. If all of the app UI code is in the browser process,
+the Proxy would also be in the browser process. If app UI code migrated to e.g.
+a separate Ash process, then the Proxy would move with them. There might be
+multiple Proxies, one per process (per Profile).
+
+
+## Code Location
+
+Some code is tied to a particular process, some code is not. For example, the
+per-Profile `AppServiceProxy` obviously contains Profile-related code (i.e. a
+`KeyedService`, so that browser code can find *the* `AppServiceProxy` for a
+given Profile) that is tied to being in the browser process. The
+`AppServiceProxy` also contains process-agnostic code (code that could
+conceivably be used by an `AppServiceProxy` living in an Ash process), such as
+code to cache and update the set of known apps (as in, the `App` Mojo type).
+Specifically, the `AppServiceProxy` code is split into two locations, one under
+`//chrome/browser` and one not:
+
+ - `//chrome/browser/apps/app_service`
+ - `//components/services/app_service`
+
+On the Provider side, code specific to extension-backed applications or web
+applications (as opposed to ARC++ or Crostini applications) lives under:
+
+ - `//chrome/browser/extensions`
+ - `//chrome/browser/web_applications`
+
+
+## Matchmaking and Publish / Subscribe
+
+The `AppService` itself does not have an `GetAllApps` method. It doesn't do
+much, and it doesn't keep much state. Instead, the App Registry aspect of the
+`AppService` is little more than a well known meeting place for `Publisher`s
+(i.e. Providers) and `Subscriber`s (i.e. Proxies) to discover each other. An
+analogy is that it's a matchmaker for `Publisher`s and `Subscriber`s, although
+it matches all to all instead of one to one. `Publisher`s don't meet
+`Subscriber`s directly, they meet the matchmaker, who introduces them to
+`Subscriber`s.
+
+Once a `Publisher` and `Subscriber` connect, the Pub-side sends the Sub-side a
+stream of `App`s (calling the `Subscriber`'s `OnApps` method). On the initial
+connection, the `Publisher` calls `OnApps` with "here's all the apps that I
+(the `Publisher`) know about", with additional `OnApps` calls made as apps are
+installed, uninstalled, updated, etc.
+
+As mentioned, the App Registry aspect of the `AppService` doesn't do much. Its
+part of the `AppService` Mojo interface is:
+
+ interface AppService {
+ // App Registry methods.
+ RegisterPublisher(Publisher publisher, AppType app_type);
+ RegisterSubscriber(Subscriber subscriber, ConnectOptions? opts);
+
+ // Some additional methods; not App Registry related.
+ };
+
+ interface Publisher {
+ // App Registry methods.
+ Connect(Subscriber subscriber, ConnectOptions? opts);
+
+ // Some additional methods; not App Registry related.
+ };
+
+ interface Subscriber {
+ OnApps(array<App> deltas);
+ };
+
+ enum AppType {
+ kUnknown,
+ kArc,
+ kCrostini,
+ kWeb,
+ };
+
+ struct ConnectOptions {
+ // TBD: some way to represent l10n info such as the UI language.
+ };
+
+Whenever a new `Publisher` is registered, it is connected with all of the
+previously registered `Subscriber`s, and vice versa. Once a `Publisher` is
+connected directly to a `Subscriber`, the `AppService` is no longer involved.
+Even as new apps are installed, uninstalled, updated, etc., the app's
+`Publisher` talks directly to each of its (previously connected) `Subscriber`s,
+without involving the `AppService`.
+
+TBD: whether we need un-registration and dis-connect mechanisms.
+
+
+## The App Type
+
+The one Mojo struct type, `App`, represents both a state, "an app that's ready
+to run", and a delta or change in state, "here's what's new about an app".
+Deltas include events like "an app was just installed" or "just uninstalled" or
+"its icon was updated".
+
+This is achieved by having every `App` field (other than `App.app_type` and
+`App.app_id`) be optional. Either optional in the Mojo sense, with type `T?`
+instead of a plain `T`, or if that isn't possible in Mojo (e.g. for integer or
+enum fields), as a semantic convention above the Mojo layer: 0 is reserved to
+mean "unknown". For example, the `App.show_in_launcher` field is a
+`OptionalBool`, not a `bool`.
+
+An `App.readiness` field represents whether an app is installed (i.e. ready to
+launch), uninstalled or otherwise disabled. "An app was just installed" is
+represented by a delta whose `readiness` is `kReady` and the old state's
+`readiness` being some other value. This is at the Mojo level. At the C++
+level, the `AppUpdate` wrapper type (see below) can provide helper
+`WasJustInstalled` methods.
+
+The `App`, `Readiness` and `OptionalBool` types are:
+
+ struct App {
+ AppType app_type;
+ string app_id;
+
+ // The fields above are mandatory. Everything else below is optional.
+
+ Readiness readiness;
+ string? name;
+ IconKey? icon_key;
+ OptionalBool show_in_launcher;
+ // etc.
+ };
+
+ enum Readiness {
+ kUnknown = 0,
+ kReady, // Installed and launchable.
+ kDisabledByBlocklist, // Disabled by SafeBrowsing.
+ kDisabledByPolicy, // Disabled by admin policy.
+ kDisabledByUser, // Disabled by explicit user action.
+ kUninstalledByUser,
+ };
+
+ enum OptionalBool {
+ kUnknown = 0,
+ kFalse,
+ kTrue,
+ };
+
+ // struct IconKey is discussed in the "App Icon Factory" section.
+
+A new state can be mechanically computed from an old state and a delta (both of
+which have the same type: `App`). Specifically, last known value wins. Any
+known field in the delta overwrites the corresponding field in the old state,
+any unknown field in the delta is ignored. For example, if an app's name
+changed but its icon didn't, the delta's `App.name` field (a
+`base::Optional<std::string>`) would be known (not `base::nullopt`) and copied
+over but its `App.icon` field would be unknown (`base::nullopt`) and not copied
+over.
+
+The current state is thus the merger or sum of all previous deltas, including
+the initial state being a delta against the ground state of "all unknown". The
+`AppServiceProxy` tracks the state of its apps, and implements the
+(in-process) Observer pattern so that UI surfaces can e.g. update themselves as
+new apps are installed. There's only one method, `OnAppUpdate`, as opposed to
+separate `OnAppInstalled`, `OnAppUninstalled`, `OnAppNameChanged`, etc.
+methods. An `AppUpdate` is a pair of `App` values: old state and delta.
+
+ class AppRegistryCache {
+ public:
+ class Observer : public base::CheckedObserver {
+ public:
+ ~Observer() override {}
+ virtual void OnAppUpdate(const AppUpdate& update) = 0;
+ };
+
+ // Etc.
+ };
+
+
+# App Icon Factory
+
+Icon data (even compressed as a PNG) is bulky, relative to the rest of the
+`App` type. `Publisher`s will generally serve icon data lazily, on demand,
+especially as the desired icon resolutions (e.g. 64dip or 256dip) aren't known
+up-front. Instead of sending an icon at all possible resolutions, the
+`Publisher` sends an `IconKey`: enough information to load the icon at given
+resolutions.
+
+An `IconKey` augments the `AppType app_type` and `string app_id`. For example,
+some icons are statically built into the Chrome or Chrome OS binary, as
+PNG-formatted resources, and can be loaded (synchronously, without sandboxing).
+They can be loaded from the `IconKey.resource_id`. Other icons are dynamically
+(and asynchronously) loaded from the extension database on disk. The base icon
+can be loaded just from the `app_id` alone.
+
+In either case, the `IconKey.icon_effects` bitmask holds whether to apply
+further image processing effects such as desaturation to gray.
+
+ interface AppService {
+ // App Icon Factory methods.
+ LoadIcon(
+ AppType app_type,
+ string app_id,
+ IconKey icon_key,
+ IconCompression icon_compression,
+ int32 size_hint_in_dip,
+ bool allow_placeholder_icon) => (IconValue icon_value);
+
+ // Some additional methods; not App Icon Factory related.
+ };
+
+ interface Publisher {
+ // App Icon Factory methods.
+ LoadIcon(
+ string app_id,
+ IconKey icon_key,
+ IconCompression icon_compression,
+ int32 size_hint_in_dip,
+ bool allow_placeholder_icon) => (IconValue icon_value);
+
+ // Some additional methods; not App Icon Factory related.
+ };
+
+ struct IconKey {
+ // A monotonically increasing number so that, after an icon update, a new
+ // IconKey, one that is different in terms of field-by-field equality, can be
+ // broadcast by a Publisher.
+ //
+ // The exact value of the number isn't important, only that newer IconKey's
+ // (those that were created more recently) have a larger timeline than older
+ // IconKey's.
+ //
+ // This is, in some sense, *a* version number, but the field is not called
+ // "version", to avoid any possible confusion that it encodes *the* app's
+ // version number, e.g. the "2.3.5" in "FooBar version 2.3.5 is installed".
+ //
+ // For example, if an app is disabled for some reason (so that its icon is
+ // grayed out), this would result in a different timeline even though the
+ // app's version is unchanged.
+ uint64 timeline;
+ // If non-zero, the compressed icon is compiled into the Chromium binary
+ // as a statically available, int-keyed resource.
+ int32 resource_id;
+ // A bitmask of icon post-processing effects, such as desaturation to
+ // gray and rounding the corners.
+ uint32 icon_effects;
+ };
+
+ enum IconCompression {
+ kUnknown,
+ kUncompressed,
+ kCompressed,
+ };
+
+ struct IconValue {
+ IconCompression icon_compression;
+ gfx.mojom.ImageSkia? uncompressed;
+ array<uint8>? compressed;
+ bool is_placeholder_icon;
+ };
+
+
+## Icon Changes
+
+Apps can change their icons, e.g. after a new version is installed. From the
+App Service's point of view, an icon change is like any other change: Providers
+broadcast an `App` value representing what's changed (icon or otherwise) about
+an app, the Proxy's `AppRegistryCache` enriches this `App` struct to be an
+`AppUpdate`, and `AppRegistryCache` observers can, if that `AppUpdate` shows
+that the icon has changed, issue a new `LoadIcon` Mojo call. A new Mojo call is
+necessary, because a Mojo callback is a `base::OnceCallback`, so the same
+callback can't be used for both the old and the new icon.
+
+
+## Caching and Other Optimizations
+
+Grouping the `IconKey` with the other `LoadIcon` arguments, the combination
+identifies a static (unchanging, but possibly obsolete) image: if a new version
+of an app results in a new icon, or if a change in app state results in a
+grayed out icon, this is represented by a different, larger `IconKey.timeline`.
+As a consequence, the combined `LoadIcon` arguments can be used to key a cache
+or map of `IconValue`s, or to recognize and coalesce multiple concurrent
+requests to the same combination.
+
+Such optimizations can be implemented as a series of "wrapper" classes (as in
+the classic "decorator" or "wrapper" design pattern) that all implement the
+same C++ interface (an `IconLoader` interface). They add their specific feature
+(e.g. caching) by wrapping another `IconLoader`, doing feature-specific work on
+every call or reply before sending the call forward or the reply backward.
+
+There may be multiple caches, as there may be multiple cache eviction policies
+(also known as garbage collection policies), spanning the trade-off from
+favoring minimizing memory use to favoring maximizing cache hit rates. The
+Proxy may have a single cache, with a relatively aggressive eviction policy,
+which applies to all of its Consumer clients. A Consumer might have an
+additional Consumer-specific cache, with a more relaxed eviction policy, if it
+has additional Consumer-specific UI signals to guide when icon-loading requests
+and cache hits are more or less likely.
+
+Note that cache values (the `IconValue` Mojo struct) are, primarily, a
+gfx.mojom.ImageSkia, which are cheap to share. Copying an ImageSkia value does
+not duplicate any underlying pixel buffers.
+
+As a separate optimization, if the `AppServiceProxy` knows how to load an icon
+for a given `IconKey`, it can skip the Mojo round trip and bulk data IPC and
+load it directly instead. For example, it may know how to load icons from a
+statically built resource ID.
+
+
+## Placeholder Icons
+
+It can take some time for `Publisher`s to provide an icon. For example, loading
+the canonical icon for an ARC++ or Crostini app might require waiting for a VM
+to start. Such icons are often cached on the file system, but on a cache miss,
+there may be a number of seconds before the system can present an icon. In this
+case, we might want to present a `Publisher`-specific placeholder, typically
+loaded from a resource (an asset statically compiled into the binary).
+
+There are two boolean fields that facilitate this: `allow_placeholder_icon` is
+sent from a `Subscriber` to a `Publisher` and `is_placeholder_icon` is sent in
+the response.
+
+`LoadIcon`'s `allow_placeholder_icon` states whether the the caller will accept
+a placeholder if the real icon can not be provided quickly. Native user
+interfaces like the app launcher will probably set this to true. On the other
+hand, serving Web-UI URLs such as `chrome://app-icon/app_id/icon_size` will set
+this to false, as that URL should identify a particular icon, not one that
+changes over time. Web-UI that wants to display placeholder icons and be
+notified of when real icons are ready will require some mechanism other than a
+`chrome:://app-icon/etc` URL.
+
+`IconValue`'s `is_placeholder_icon` states whether the icon provided is a
+placeholder. That field should only be true if the corresponding `LoadIcon`
+call had `allow_placeholder_icon` true. When the `LoadIcon` caller receives a
+placeholder icon, it is up to the caller to issue a new `LoadIcon` call, this
+time with `allow_placeholder_icon` false. As before, a new Mojo call is
+necessary, because a Mojo callback is a `base::OnceCallback`, so the same
+callback can't be used for both the placeholder and the real icon.
+
+
+## Provider-Specific Subtleties
+
+Some concerns (like caching and coalescing multiple in-flight calls with the
+same `IconKey`) are not specific to any particular Providers like ARC++ or
+Crostini, and can be solved by the Proxy.
+
+Other concerns are Provider-specific, and are generally solved in Provider
+implementations, albeit often with non-Provider-specific support (such as for
+placeholder icons, discussed above). Such concerns include:
+
+ - Multiple icon sources: some icons for built-in VM-based apps (e.g. ARC++ or
+ Crostini) should be served from a compiled-into-the-browser resource
+ instead of from the VM.
+ - Pending LoadIcon calls: some `LoadIcon` calls might need to wait on
+ bringing up a VM.
+ - Potential on-disk corruption: for whatever reason, an on-disk file that's
+ meant to hold a cached icon may be missing or invalid. In that case, the
+ Provider should still provide a (placeholder) icon, and trigger
+ Provider-specific clean-up and re-load of the real app icon.
+
+All of these concerns listed should be straightforward to handle, and don't
+invalidate the overall App Service `Publisher.LoadIcon` Mojo design, including
+its non-Provider-specific caching and other optimization layers.
+
+There are also yet another category of concerns that are Provider-specific, but
+also outside the purview of the App Service. For example, the file system
+layout of ARC++'s on-disk icon cache is, from the App Service's point of view,
+considered a private ARC++ implementation detail. As long as ARC++'s API
+remains the same, and if ARC++ can notify the App Service if the App Service
+needs to reload any or all icons, then any change in ARC++'s file system layout
+isn't a direct concern to the App Service.
+
+
+# App Runner
+
+Each `Publisher` has (`Publisher`-specific) implementations of e.g. launching an
+app and populating a context menu. The `AppService` presents a uniform API to
+trigger these, forwarding each call on to the relevant `Publisher`:
+
+ interface AppService {
+ // App Runner methods.
+ Launch(AppType app_type, string app_id, LaunchOptions? opts);
+ // etc.
+
+ // Some additional methods; not App Runner related.
+ };
+
+ interface Publisher {
+ // App Runner methods.
+ Launch(string app_id, LaunchOptions? opts);
+ // etc.
+
+ // Some additional methods; not App Runner related.
+ };
+
+ struct LaunchOptions {
+ // TBD.
+ };
+
+TBD: details for context menus.
+
+TBD: be able to for-each over all the app *instances*, including multiple
+instances (e.g. multiple windows) of the one app.
+
+
+# App Installer
+
+This includes Provider-facing API (not Consumer-facing API like the majority of
+the `AppService`) to help install and uninstall apps consistently. For example,
+one part of app installation is adding an icon shortcut (e.g. on the Desktop
+for Windows, on the Shelf for Chrome OS). This helper code should be written
+once (in the `AppService`), not `N` times in `N` Providers.
+
+TBD: details.
+
+
+# App Coordinator
+
+This keeps system-wide or for-apps-as-a-whole preferences and settings, e.g.
+out of all of the installed apps, which app has the user preferred for photo
+editing. Consumer- or Provider-specific settings, e.g. icon order in the Chrome
+OS shelf, or Crostini VM configuration, is out of scope of the App Service.
+
+TBD: details.
+
+
+---
+
+Updated on 2019-03-20.
diff --git a/chromium/components/services/app_service/app_service_impl.cc b/chromium/components/services/app_service/app_service_impl.cc
new file mode 100644
index 00000000000..a4c4467f3ca
--- /dev/null
+++ b/chromium/components/services/app_service/app_service_impl.cc
@@ -0,0 +1,493 @@
+// 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 "components/services/app_service/app_service_impl.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/files/file_util.h"
+#include "base/json/json_string_value_serializer.h"
+#include "base/metrics/histogram_macros.h"
+#include "base/task/post_task.h"
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
+#include "base/threading/scoped_blocking_call.h"
+#include "base/token.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
+#include "components/services/app_service/public/cpp/preferred_apps_converter.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+#include "content/public/browser/browser_thread.h"
+
+namespace {
+
+const char kAppServicePreferredApps[] = "app_service.preferred_apps";
+const base::FilePath::CharType kPreferredAppsDirname[] =
+ FILE_PATH_LITERAL("PreferredApps");
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class PreferredAppsFileIOAction {
+ kWriteSuccess = 0,
+ kWriteFailed = 1,
+ kReadSuccess = 2,
+ kReadFailed = 3,
+ kMaxValue = kReadFailed,
+};
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class PreferredAppsUpdateAction {
+ kAdd = 0,
+ kDeleteForFilter = 1,
+ kDeleteForAppId = 2,
+ kMaxValue = kDeleteForAppId,
+};
+
+void Connect(apps::mojom::Publisher* publisher,
+ apps::mojom::Subscriber* subscriber) {
+ mojo::PendingRemote<apps::mojom::Subscriber> clone;
+ subscriber->Clone(clone.InitWithNewPipeAndPassReceiver());
+ // TODO: replace nullptr with a ConnectOptions.
+ publisher->Connect(std::move(clone), nullptr);
+}
+
+void LogPreferredAppFileIOAction(PreferredAppsFileIOAction action) {
+ UMA_HISTOGRAM_ENUMERATION("PreferredApps.FileIOAction", action);
+}
+
+void LogPreferredAppUpdateAction(PreferredAppsUpdateAction action) {
+ UMA_HISTOGRAM_ENUMERATION("PreferredApps.UpdateAction", action);
+}
+
+// Performs blocking I/O. Called on another thread.
+void WriteDataBlocking(const base::FilePath& preferred_apps_file,
+ const std::string& preferred_apps) {
+ base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
+ base::BlockingType::MAY_BLOCK);
+ bool write_success =
+ base::WriteFile(preferred_apps_file, preferred_apps.c_str(),
+ preferred_apps.size()) != -1;
+ if (write_success) {
+ LogPreferredAppFileIOAction(PreferredAppsFileIOAction::kWriteSuccess);
+ } else {
+ DVLOG(0) << "Fail to write preferred apps to " << preferred_apps_file;
+ LogPreferredAppFileIOAction(PreferredAppsFileIOAction::kWriteFailed);
+ }
+}
+
+// Performs blocking I/O. Called on another thread.
+std::string ReadDataBlocking(const base::FilePath& preferred_apps_file) {
+ base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
+ base::BlockingType::MAY_BLOCK);
+ std::string preferred_apps_string;
+ bool read_success =
+ base::ReadFileToString(preferred_apps_file, &preferred_apps_string);
+ if (read_success) {
+ LogPreferredAppFileIOAction(PreferredAppsFileIOAction::kReadSuccess);
+ } else {
+ LogPreferredAppFileIOAction(PreferredAppsFileIOAction::kReadFailed);
+ }
+ return preferred_apps_string;
+}
+
+} // namespace
+
+namespace apps {
+
+AppServiceImpl::AppServiceImpl(PrefService* profile_prefs,
+ const base::FilePath& profile_dir,
+ base::OnceClosure read_completed_for_testing)
+ : pref_service_(profile_prefs),
+ profile_dir_(profile_dir),
+ should_write_preferred_apps_to_file_(false),
+ writing_preferred_apps_(false),
+ task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
+ {base::ThreadPool(), base::MayBlock(),
+ base::TaskPriority::BEST_EFFORT,
+ base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})),
+ read_completed_for_testing_(std::move(read_completed_for_testing)) {
+ DCHECK(pref_service_);
+ InitializePreferredApps();
+}
+
+AppServiceImpl::~AppServiceImpl() = default;
+
+// static
+void AppServiceImpl::RegisterProfilePrefs(PrefRegistrySimple* registry) {
+ registry->RegisterDictionaryPref(kAppServicePreferredApps);
+}
+
+void AppServiceImpl::BindReceiver(
+ mojo::PendingReceiver<apps::mojom::AppService> receiver) {
+ receivers_.Add(this, std::move(receiver));
+}
+
+void AppServiceImpl::FlushMojoCallsForTesting() {
+ subscribers_.FlushForTesting();
+ receivers_.FlushForTesting();
+}
+
+void AppServiceImpl::RegisterPublisher(
+ mojo::PendingRemote<apps::mojom::Publisher> publisher_remote,
+ apps::mojom::AppType app_type) {
+ mojo::Remote<apps::mojom::Publisher> publisher(std::move(publisher_remote));
+ // Connect the new publisher with every registered subscriber.
+ for (auto& subscriber : subscribers_) {
+ ::Connect(publisher.get(), subscriber.get());
+ }
+
+ // Check that no previous publisher has registered for the same app_type.
+ CHECK(publishers_.find(app_type) == publishers_.end());
+
+ // Add the new publisher to the set.
+ publisher.set_disconnect_handler(
+ base::BindOnce(&AppServiceImpl::OnPublisherDisconnected,
+ base::Unretained(this), app_type));
+ auto result = publishers_.emplace(app_type, std::move(publisher));
+ CHECK(result.second);
+}
+
+void AppServiceImpl::RegisterSubscriber(
+ mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote,
+ apps::mojom::ConnectOptionsPtr opts) {
+ // Connect the new subscriber with every registered publisher.
+ mojo::Remote<apps::mojom::Subscriber> subscriber(
+ std::move(subscriber_remote));
+ for (const auto& iter : publishers_) {
+ ::Connect(iter.second.get(), subscriber.get());
+ }
+
+ // TODO: store the opts somewhere.
+
+ // Initialise the Preferred Apps in the Subscribers on register.
+ if (preferred_apps_.IsInitialized()) {
+ subscriber->InitializePreferredApps(preferred_apps_.GetValue());
+ }
+
+ // Add the new subscriber to the set.
+ subscribers_.Add(std::move(subscriber));
+}
+
+void AppServiceImpl::LoadIcon(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ LoadIconCallback callback) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ std::move(callback).Run(apps::mojom::IconValue::New());
+ return;
+ }
+ iter->second->LoadIcon(app_id, std::move(icon_key), icon_compression,
+ size_hint_in_dip, allow_placeholder_icon,
+ std::move(callback));
+}
+
+void AppServiceImpl::Launch(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ int32_t event_flags,
+ apps::mojom::LaunchSource launch_source,
+ int64_t display_id) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->Launch(app_id, event_flags, launch_source, display_id);
+}
+void AppServiceImpl::LaunchAppWithFiles(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::LaunchContainer container,
+ int32_t event_flags,
+ apps::mojom::LaunchSource launch_source,
+ apps::mojom::FilePathsPtr file_paths) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->LaunchAppWithFiles(app_id, container, event_flags,
+ launch_source, std::move(file_paths));
+}
+
+void AppServiceImpl::LaunchAppWithIntent(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ int32_t event_flags,
+ apps::mojom::IntentPtr intent,
+ apps::mojom::LaunchSource launch_source,
+ int64_t display_id) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->LaunchAppWithIntent(app_id, event_flags, std::move(intent),
+ launch_source, display_id);
+}
+
+void AppServiceImpl::SetPermission(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::PermissionPtr permission) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->SetPermission(app_id, std::move(permission));
+}
+
+void AppServiceImpl::Uninstall(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ bool clear_site_data,
+ bool report_abuse) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->Uninstall(app_id, clear_site_data, report_abuse);
+}
+
+void AppServiceImpl::PauseApp(apps::mojom::AppType app_type,
+ const std::string& app_id) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->PauseApp(app_id);
+}
+
+void AppServiceImpl::UnpauseApps(apps::mojom::AppType app_type,
+ const std::string& app_id) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->UnpauseApps(app_id);
+}
+
+void AppServiceImpl::StopApp(apps::mojom::AppType app_type,
+ const std::string& app_id) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->StopApp(app_id);
+}
+
+void AppServiceImpl::GetMenuModel(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::MenuType menu_type,
+ int64_t display_id,
+ GetMenuModelCallback callback) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ std::move(callback).Run(apps::mojom::MenuItems::New());
+ return;
+ }
+
+ iter->second->GetMenuModel(app_id, menu_type, display_id,
+ std::move(callback));
+}
+
+void AppServiceImpl::OpenNativeSettings(apps::mojom::AppType app_type,
+ const std::string& app_id) {
+ auto iter = publishers_.find(app_type);
+ if (iter == publishers_.end()) {
+ return;
+ }
+ iter->second->OpenNativeSettings(app_id);
+}
+
+void AppServiceImpl::AddPreferredApp(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter,
+ apps::mojom::IntentPtr intent,
+ bool from_publisher) {
+ // TODO(crbug.com/853604): Make sure the ARC preference init happens after
+ // this. Might need to change the interface to call that after read completed.
+ // Might also need to record the change before data read and make the update
+ // after initialization in the future.
+ if (!preferred_apps_.IsInitialized()) {
+ DVLOG(0) << "Preferred apps not initialised when try to add.";
+ return;
+ }
+
+ apps::mojom::ReplacedAppPreferencesPtr replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(app_id, intent_filter);
+
+ LogPreferredAppUpdateAction(PreferredAppsUpdateAction::kAdd);
+
+ WriteToJSON(profile_dir_, preferred_apps_);
+
+ for (auto& subscriber : subscribers_) {
+ subscriber->OnPreferredAppSet(app_id, intent_filter->Clone());
+ }
+
+ if (from_publisher || !intent) {
+ return;
+ }
+
+ // Sync the change to publishers. Because |replaced_app_preference| can
+ // be any app type, we should run this for all publishers. Currently
+ // only implemented in ARC publisher.
+ // TODO(crbug.com/853604): The |replaced_app_preference| can be really big,
+ // update this logic to only call the relevant publisher for each app after
+ // updating the storage structure.
+ for (const auto& iter : publishers_) {
+ iter.second->OnPreferredAppSet(app_id, intent_filter->Clone(),
+ intent->Clone(),
+ replaced_app_preferences->Clone());
+ }
+}
+
+void AppServiceImpl::RemovePreferredApp(apps::mojom::AppType app_type,
+ const std::string& app_id) {
+ // TODO(crbug.com/853604): Make sure the ARC preference init happens after
+ // this. Might need to change the interface to call that after read completed.
+ // Might also need to record the change before data read and make the update
+ // after initialization in the future.
+ if (!preferred_apps_.IsInitialized()) {
+ DVLOG(0) << "Preferred apps not initialised when try to remove an app id.";
+ return;
+ }
+
+ preferred_apps_.DeleteAppId(app_id);
+
+ LogPreferredAppUpdateAction(PreferredAppsUpdateAction::kDeleteForAppId);
+
+ WriteToJSON(profile_dir_, preferred_apps_);
+}
+
+void AppServiceImpl::RemovePreferredAppForFilter(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter) {
+ // TODO(crbug.com/853604): Make sure the ARC preference init happens after
+ // this. Might need to change the interface to call that after read completed.
+ // Might also need to record the change before data read and make the update
+ // after initialization in the future.
+ if (!preferred_apps_.IsInitialized()) {
+ DVLOG(0) << "Preferred apps not initialised when try to remove a filter.";
+ return;
+ }
+
+ preferred_apps_.DeletePreferredApp(app_id, intent_filter);
+
+ WriteToJSON(profile_dir_, preferred_apps_);
+
+ for (auto& subscriber : subscribers_) {
+ subscriber->OnPreferredAppRemoved(app_id, intent_filter->Clone());
+ }
+
+ LogPreferredAppUpdateAction(PreferredAppsUpdateAction::kDeleteForFilter);
+}
+
+PreferredAppsList& AppServiceImpl::GetPreferredAppsForTesting() {
+ return preferred_apps_;
+}
+
+void AppServiceImpl::SetWriteCompletedCallbackForTesting(
+ base::OnceClosure testing_callback) {
+ write_completed_for_testing_ = std::move(testing_callback);
+}
+
+void AppServiceImpl::OnPublisherDisconnected(apps::mojom::AppType app_type) {
+ publishers_.erase(app_type);
+}
+
+void AppServiceImpl::InitializePreferredApps() {
+ ReadFromJSON(profile_dir_);
+
+ // Remove "app_service.preferred_apps" from perf if exists.
+ // TODO(crbug.com/853604): Remove this in M86.
+ DCHECK(pref_service_);
+ pref_service_->ClearPref(kAppServicePreferredApps);
+}
+
+void AppServiceImpl::WriteToJSON(
+ const base::FilePath& profile_dir,
+ const apps::PreferredAppsList& preferred_apps) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ // If currently is writing preferred apps to file, set a flag to write after
+ // the current write completed.
+ if (writing_preferred_apps_) {
+ should_write_preferred_apps_to_file_ = true;
+ return;
+ }
+
+ writing_preferred_apps_ = true;
+
+ auto preferred_apps_value =
+ apps::ConvertPreferredAppsToValue(preferred_apps.GetReference());
+
+ std::string json_string;
+ JSONStringValueSerializer serializer(&json_string);
+ serializer.Serialize(preferred_apps_value);
+
+ task_runner_->PostTaskAndReply(
+ FROM_HERE,
+ base::BindOnce(&WriteDataBlocking,
+ profile_dir.Append(kPreferredAppsDirname), json_string),
+ base::BindOnce(&AppServiceImpl::WriteCompleted,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void AppServiceImpl::WriteCompleted() {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ writing_preferred_apps_ = false;
+ if (!should_write_preferred_apps_to_file_) {
+ // Call the testing callback if it is set.
+ if (write_completed_for_testing_) {
+ std::move(write_completed_for_testing_).Run();
+ }
+ return;
+ }
+ // If need to perform another write, write the most up to date preferred apps
+ // from memory to file.
+ should_write_preferred_apps_to_file_ = false;
+ WriteToJSON(profile_dir_, preferred_apps_);
+}
+
+void AppServiceImpl::ReadFromJSON(const base::FilePath& profile_dir) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ task_runner_->PostTaskAndReplyWithResult(
+ FROM_HERE,
+ base::BindOnce(&ReadDataBlocking,
+ profile_dir.Append(kPreferredAppsDirname)),
+ base::BindOnce(&AppServiceImpl::ReadCompleted,
+ weak_ptr_factory_.GetWeakPtr()));
+}
+
+void AppServiceImpl::ReadCompleted(std::string preferred_apps_string) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ if (preferred_apps_string.empty()) {
+ preferred_apps_.Init();
+ } else {
+ std::string json_string;
+ JSONStringValueDeserializer deserializer(preferred_apps_string);
+ int error_code;
+ std::string error_message;
+ auto preferred_apps_value =
+ deserializer.Deserialize(&error_code, &error_message);
+
+ if (!preferred_apps_value) {
+ DVLOG(0) << "Fail to deserialize json value from string with error code: "
+ << error_code << " and error message: " << error_message;
+ preferred_apps_.Init();
+ } else {
+ auto preferred_apps =
+ apps::ParseValueToPreferredApps(*preferred_apps_value);
+ preferred_apps_.Init(preferred_apps);
+ }
+ }
+ for (auto& subscriber : subscribers_) {
+ subscriber->InitializePreferredApps(preferred_apps_.GetValue());
+ }
+ if (read_completed_for_testing_) {
+ std::move(read_completed_for_testing_).Run();
+ }
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/app_service_impl.h b/chromium/components/services/app_service/app_service_impl.h
new file mode 100644
index 00000000000..21c77827b13
--- /dev/null
+++ b/chromium/components/services/app_service/app_service_impl.h
@@ -0,0 +1,168 @@
+// 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 COMPONENTS_SERVICES_APP_SERVICE_APP_SERVICE_IMPL_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_APP_SERVICE_IMPL_H_
+
+#include <map>
+
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/macros.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequenced_task_runner.h"
+#include "components/services/app_service/public/cpp/preferred_apps_list.h"
+#include "components/services/app_service/public/mojom/app_service.mojom.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/remote_set.h"
+
+class PrefRegistrySimple;
+class PrefService;
+
+namespace apps {
+
+// The implementation of the apps::mojom::AppService Mojo interface.
+//
+// See components/services/app_service/README.md.
+class AppServiceImpl : public apps::mojom::AppService {
+ public:
+ AppServiceImpl(
+ PrefService* profile_prefs,
+ const base::FilePath& profile_dir,
+ base::OnceClosure read_completed_for_testing = base::OnceClosure());
+ ~AppServiceImpl() override;
+
+ static void RegisterProfilePrefs(PrefRegistrySimple* registry);
+
+ void BindReceiver(mojo::PendingReceiver<apps::mojom::AppService> receiver);
+
+ void FlushMojoCallsForTesting();
+
+ // apps::mojom::AppService overrides.
+ void RegisterPublisher(
+ mojo::PendingRemote<apps::mojom::Publisher> publisher_remote,
+ apps::mojom::AppType app_type) override;
+ void RegisterSubscriber(
+ mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote,
+ apps::mojom::ConnectOptionsPtr opts) override;
+ void LoadIcon(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ LoadIconCallback callback) override;
+ void Launch(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ int32_t event_flags,
+ apps::mojom::LaunchSource launch_source,
+ int64_t display_id) override;
+ void LaunchAppWithFiles(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::LaunchContainer container,
+ int32_t event_flags,
+ apps::mojom::LaunchSource launch_source,
+ apps::mojom::FilePathsPtr file_paths) override;
+ void LaunchAppWithIntent(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ int32_t event_flags,
+ apps::mojom::IntentPtr intent,
+ apps::mojom::LaunchSource launch_source,
+ int64_t display_id) override;
+ void SetPermission(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::PermissionPtr permission) override;
+ void Uninstall(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ bool clear_site_data,
+ bool report_abuse) override;
+ void PauseApp(apps::mojom::AppType app_type,
+ const std::string& app_id) override;
+ void UnpauseApps(apps::mojom::AppType app_type,
+ const std::string& app_id) override;
+ void StopApp(apps::mojom::AppType app_type,
+ const std::string& app_id) override;
+ void GetMenuModel(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::MenuType menu_type,
+ int64_t display_id,
+ GetMenuModelCallback callback) override;
+ void OpenNativeSettings(apps::mojom::AppType app_type,
+ const std::string& app_id) override;
+ void AddPreferredApp(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter,
+ apps::mojom::IntentPtr intent,
+ bool from_publisher) override;
+ void RemovePreferredApp(apps::mojom::AppType app_type,
+ const std::string& app_id) override;
+ void RemovePreferredAppForFilter(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter) override;
+
+ // Retern the preferred_apps_ for testing.
+ PreferredAppsList& GetPreferredAppsForTesting();
+
+ void SetWriteCompletedCallbackForTesting(base::OnceClosure testing_callback);
+
+ private:
+ void OnPublisherDisconnected(apps::mojom::AppType app_type);
+
+ // Initialize the preferred apps from disk.
+ void InitializePreferredApps();
+
+ // Write the preferred apps to a json file.
+ void WriteToJSON(const base::FilePath& profile_dir,
+ const apps::PreferredAppsList& preferred_apps);
+
+ void WriteCompleted();
+
+ void ReadFromJSON(const base::FilePath& profile_dir);
+
+ void ReadCompleted(std::string preferred_apps_string);
+
+ // publishers_ is a std::map, not a mojo::RemoteSet, since we want to
+ // be able to find *the* publisher for a given apps::mojom::AppType.
+ std::map<apps::mojom::AppType, mojo::Remote<apps::mojom::Publisher>>
+ publishers_;
+ mojo::RemoteSet<apps::mojom::Subscriber> subscribers_;
+
+ // Must come after the publisher and subscriber maps to ensure it is
+ // destroyed first, closing the connection to avoid dangling callbacks.
+ mojo::ReceiverSet<apps::mojom::AppService> receivers_;
+
+ PrefService* const pref_service_;
+
+ PreferredAppsList preferred_apps_;
+
+ base::FilePath profile_dir_;
+
+ // True if need to write preferred apps to file after the current write is
+ // completed.
+ bool should_write_preferred_apps_to_file_;
+
+ // True if it is currently writing preferred apps to file.
+ bool writing_preferred_apps_;
+
+ // Task runner where the file operations takes place. This is to make sure the
+ // write operation will be operated in sequence.
+ scoped_refptr<base::SequencedTaskRunner> const task_runner_;
+
+ base::OnceClosure write_completed_for_testing_;
+
+ base::OnceClosure read_completed_for_testing_;
+
+ base::WeakPtrFactory<AppServiceImpl> weak_ptr_factory_{this};
+
+ DISALLOW_COPY_AND_ASSIGN(AppServiceImpl);
+};
+
+} // namespace apps
+
+#endif // CHROME_SERVICES_APP_SERVICE_APP_SERVICE_IMPL_H_
diff --git a/chromium/components/services/app_service/app_service_impl_unittest.cc b/chromium/components/services/app_service/app_service_impl_unittest.cc
new file mode 100644
index 00000000000..532a94cd182
--- /dev/null
+++ b/chromium/components/services/app_service/app_service_impl_unittest.cc
@@ -0,0 +1,405 @@
+// 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 <set>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/scoped_temp_dir.h"
+#include "base/optional.h"
+#include "base/run_loop.h"
+#include "components/prefs/testing_pref_service.h"
+#include "components/services/app_service/app_service_impl.h"
+#include "components/services/app_service/public/cpp/intent_filter_util.h"
+#include "components/services/app_service/public/cpp/intent_util.h"
+#include "components/services/app_service/public/cpp/preferred_apps_list.h"
+#include "components/services/app_service/public/cpp/publisher_base.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+#include "content/public/test/browser_task_environment.h"
+#include "mojo/public/cpp/bindings/pending_receiver.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver_set.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/remote_set.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace apps {
+
+class FakePublisher : public apps::PublisherBase {
+ public:
+ FakePublisher(AppServiceImpl* impl,
+ apps::mojom::AppType app_type,
+ std::vector<std::string> initial_app_ids)
+ : app_type_(app_type), known_app_ids_(std::move(initial_app_ids)) {
+ mojo::PendingRemote<apps::mojom::Publisher> remote;
+ receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
+ impl->RegisterPublisher(std::move(remote), app_type_);
+ }
+
+ void PublishMoreApps(std::vector<std::string> app_ids) {
+ for (auto& subscriber : subscribers_) {
+ CallOnApps(subscriber.get(), app_ids, /*uninstall=*/false);
+ }
+ for (const auto& app_id : app_ids) {
+ known_app_ids_.push_back(app_id);
+ }
+ }
+
+ void UninstallApps(std::vector<std::string> app_ids, AppServiceImpl* impl) {
+ for (auto& subscriber : subscribers_) {
+ CallOnApps(subscriber.get(), app_ids, /*uninstall=*/true);
+ }
+ for (const auto& app_id : app_ids) {
+ known_app_ids_.push_back(app_id);
+ impl->RemovePreferredApp(app_type_, app_id);
+ }
+ }
+
+ std::string load_icon_app_id;
+
+ private:
+ void Connect(mojo::PendingRemote<apps::mojom::Subscriber> subscriber_remote,
+ apps::mojom::ConnectOptionsPtr opts) override {
+ mojo::Remote<apps::mojom::Subscriber> subscriber(
+ std::move(subscriber_remote));
+ CallOnApps(subscriber.get(), known_app_ids_, /*uninstall=*/false);
+ subscribers_.Add(std::move(subscriber));
+ }
+
+ void LoadIcon(const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ LoadIconCallback callback) override {
+ load_icon_app_id = app_id;
+ std::move(callback).Run(apps::mojom::IconValue::New());
+ }
+
+ void Launch(const std::string& app_id,
+ int32_t event_flags,
+ apps::mojom::LaunchSource launch_source,
+ int64_t display_id) override {}
+
+ void CallOnApps(apps::mojom::Subscriber* subscriber,
+ std::vector<std::string>& app_ids,
+ bool uninstall) {
+ std::vector<apps::mojom::AppPtr> apps;
+ for (const auto& app_id : app_ids) {
+ auto app = apps::mojom::App::New();
+ app->app_type = app_type_;
+ app->app_id = app_id;
+ if (uninstall) {
+ app->readiness = apps::mojom::Readiness::kUninstalledByUser;
+ }
+ apps.push_back(std::move(app));
+ }
+ subscriber->OnApps(std::move(apps));
+ }
+
+ apps::mojom::AppType app_type_;
+ std::vector<std::string> known_app_ids_;
+ mojo::ReceiverSet<apps::mojom::Publisher> receivers_;
+ mojo::RemoteSet<apps::mojom::Subscriber> subscribers_;
+};
+
+class FakeSubscriber : public apps::mojom::Subscriber {
+ public:
+ explicit FakeSubscriber(AppServiceImpl* impl) {
+ mojo::PendingRemote<apps::mojom::Subscriber> remote;
+ receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver());
+ impl->RegisterSubscriber(std::move(remote), nullptr);
+ }
+
+ std::string AppIdsSeen() {
+ std::stringstream ss;
+ for (const auto& app_id : app_ids_seen_) {
+ ss << app_id;
+ }
+ return ss.str();
+ }
+
+ PreferredAppsList& PreferredApps() { return preferred_apps_; }
+
+ private:
+ void OnApps(std::vector<apps::mojom::AppPtr> deltas) override {
+ for (const auto& delta : deltas) {
+ app_ids_seen_.insert(delta->app_id);
+ if (delta->readiness == apps::mojom::Readiness::kUninstalledByUser) {
+ preferred_apps_.DeleteAppId(delta->app_id);
+ }
+ }
+ }
+
+ void Clone(mojo::PendingReceiver<apps::mojom::Subscriber> receiver) override {
+ receivers_.Add(this, std::move(receiver));
+ }
+
+ void OnPreferredAppSet(const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter) override {
+ preferred_apps_.AddPreferredApp(app_id, intent_filter);
+ }
+
+ void OnPreferredAppRemoved(
+ const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter) override {
+ preferred_apps_.DeletePreferredApp(app_id, intent_filter);
+ }
+
+ void InitializePreferredApps(
+ PreferredAppsList::PreferredApps preferred_apps) override {
+ preferred_apps_.Init(preferred_apps);
+ }
+
+ mojo::ReceiverSet<apps::mojom::Subscriber> receivers_;
+ std::set<std::string> app_ids_seen_;
+ apps::PreferredAppsList preferred_apps_;
+};
+
+class AppServiceImplTest : public testing::Test {
+ protected:
+ // base::test::TaskEnvironment task_environment_;
+ content::BrowserTaskEnvironment task_environment_;
+ TestingPrefServiceSimple pref_service_;
+ base::ScopedTempDir temp_dir_;
+};
+
+TEST_F(AppServiceImplTest, PubSub) {
+ const int size_hint_in_dip = 64;
+
+ AppServiceImpl::RegisterProfilePrefs(pref_service_.registry());
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+ AppServiceImpl impl(&pref_service_, temp_dir_.GetPath());
+
+ // Start with one subscriber.
+ FakeSubscriber sub0(&impl);
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("", sub0.AppIdsSeen());
+
+ // Add one publisher.
+ FakePublisher pub0(&impl, apps::mojom::AppType::kArc,
+ std::vector<std::string>{"A", "B"});
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("AB", sub0.AppIdsSeen());
+
+ // Have that publisher publish more apps.
+ pub0.PublishMoreApps(std::vector<std::string>{"C", "D", "E"});
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("ABCDE", sub0.AppIdsSeen());
+
+ // Add a second publisher.
+ FakePublisher pub1(&impl, apps::mojom::AppType::kBuiltIn,
+ std::vector<std::string>{"m"});
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("ABCDEm", sub0.AppIdsSeen());
+
+ // Have both publishers publish more apps.
+ pub0.PublishMoreApps(std::vector<std::string>{"F"});
+ pub1.PublishMoreApps(std::vector<std::string>{"n"});
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("ABCDEFmn", sub0.AppIdsSeen());
+
+ // Add a second subscriber.
+ FakeSubscriber sub1(&impl);
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("ABCDEFmn", sub0.AppIdsSeen());
+ EXPECT_EQ("ABCDEFmn", sub1.AppIdsSeen());
+
+ // Publish more apps.
+ pub1.PublishMoreApps(std::vector<std::string>{"o", "p", "q"});
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("ABCDEFmnopq", sub0.AppIdsSeen());
+ EXPECT_EQ("ABCDEFmnopq", sub1.AppIdsSeen());
+
+ // Add a third publisher.
+ FakePublisher pub2(&impl, apps::mojom::AppType::kCrostini,
+ std::vector<std::string>{"$"});
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("$ABCDEFmnopq", sub0.AppIdsSeen());
+ EXPECT_EQ("$ABCDEFmnopq", sub1.AppIdsSeen());
+
+ // Publish more apps.
+ pub2.PublishMoreApps(std::vector<std::string>{"&"});
+ pub1.PublishMoreApps(std::vector<std::string>{"r"});
+ pub0.PublishMoreApps(std::vector<std::string>{"G"});
+ impl.FlushMojoCallsForTesting();
+ EXPECT_EQ("$&ABCDEFGmnopqr", sub0.AppIdsSeen());
+ EXPECT_EQ("$&ABCDEFGmnopqr", sub1.AppIdsSeen());
+
+ // Call LoadIcon on the impl twice.
+ //
+ // The first time (i == 0), it should be forwarded onto the AppType::kBuiltIn
+ // publisher (which is pub1) and no other publisher.
+ //
+ // The second time (i == 1), passing AppType::kUnknown, none of the
+ // publishers' LoadIcon's should fire, but the callback should still be run.
+ for (int i = 0; i < 2; i++) {
+ auto app_type = i == 0 ? apps::mojom::AppType::kBuiltIn
+ : apps::mojom::AppType::kUnknown;
+
+ bool callback_ran = false;
+ pub0.load_icon_app_id = "-";
+ pub1.load_icon_app_id = "-";
+ pub2.load_icon_app_id = "-";
+ auto icon_key = apps::mojom::IconKey::New(0, 0, 0);
+ constexpr bool allow_placeholder_icon = false;
+ impl.LoadIcon(
+ app_type, "o", std::move(icon_key),
+ apps::mojom::IconCompression::kUncompressed, size_hint_in_dip,
+ allow_placeholder_icon,
+ base::BindOnce(
+ [](bool* ran, apps::mojom::IconValuePtr iv) { *ran = true; },
+ &callback_ran));
+ impl.FlushMojoCallsForTesting();
+ EXPECT_TRUE(callback_ran);
+ EXPECT_EQ("-", pub0.load_icon_app_id);
+ EXPECT_EQ(i == 0 ? "o" : "-", pub1.load_icon_app_id);
+ EXPECT_EQ("-", pub2.load_icon_app_id);
+ }
+}
+
+// TODO(https://crbug.com/1074596) Test to see if the flakiness is fixed. If it
+// is not fixed, please update to the same bug.
+TEST_F(AppServiceImplTest, PreferredApps) {
+ // Test Initialize.
+ AppServiceImpl::RegisterProfilePrefs(pref_service_.registry());
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+ AppServiceImpl impl(&pref_service_, temp_dir_.GetPath());
+ impl.GetPreferredAppsForTesting().Init();
+
+ const char kAppId1[] = "abcdefg";
+ const char kAppId2[] = "aaaaaaa";
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+
+ impl.GetPreferredAppsForTesting().AddPreferredApp(kAppId1, intent_filter);
+
+ // Add one subscriber.
+ FakeSubscriber sub0(&impl);
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(sub0.PreferredApps().GetValue(),
+ impl.GetPreferredAppsForTesting().GetValue());
+
+ // Add another subscriber.
+ FakeSubscriber sub1(&impl);
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(sub1.PreferredApps().GetValue(),
+ impl.GetPreferredAppsForTesting().GetValue());
+
+ FakePublisher pub0(&impl, apps::mojom::AppType::kArc,
+ std::vector<std::string>{kAppId1, kAppId2});
+ task_environment_.RunUntilIdle();
+
+ // Test sync preferred app to all subscribers.
+ filter_url = GURL("https://www.abc.com/");
+ GURL another_filter_url = GURL("https://www.test.com/");
+ intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+ auto another_intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(another_filter_url);
+
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(base::nullopt,
+ sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(base::nullopt,
+ sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(base::nullopt,
+ sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+ EXPECT_EQ(base::nullopt,
+ sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+
+ impl.AddPreferredApp(
+ apps::mojom::AppType::kUnknown, kAppId2, intent_filter->Clone(),
+ apps_util::CreateIntentFromUrl(filter_url), /*from_publisher=*/true);
+ impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId2,
+ another_intent_filter->Clone(),
+ apps_util::CreateIntentFromUrl(another_filter_url),
+ /*from_publisher=*/true);
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(kAppId2, sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(kAppId2, sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(kAppId2,
+ sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+ EXPECT_EQ(kAppId2,
+ sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+
+ // Test that uninstall removes all the settings for the app.
+ pub0.UninstallApps(std::vector<std::string>{kAppId2}, &impl);
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(base::nullopt,
+ sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(base::nullopt,
+ sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(base::nullopt,
+ sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+ EXPECT_EQ(base::nullopt,
+ sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+
+ impl.AddPreferredApp(
+ apps::mojom::AppType::kUnknown, kAppId2, intent_filter->Clone(),
+ apps_util::CreateIntentFromUrl(filter_url), /*from_publisher=*/true);
+ impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId2,
+ another_intent_filter->Clone(),
+ apps_util::CreateIntentFromUrl(another_filter_url),
+ /*from_publisher=*/true);
+ task_environment_.RunUntilIdle();
+
+ EXPECT_EQ(kAppId2, sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(kAppId2, sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(kAppId2,
+ sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+ EXPECT_EQ(kAppId2,
+ sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+
+ // Test that remove setting for one filter.
+ impl.RemovePreferredAppForFilter(apps::mojom::AppType::kUnknown, kAppId2,
+ intent_filter->Clone());
+ task_environment_.RunUntilIdle();
+ EXPECT_EQ(base::nullopt,
+ sub0.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(base::nullopt,
+ sub1.PreferredApps().FindPreferredAppForUrl(filter_url));
+ EXPECT_EQ(kAppId2,
+ sub0.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+ EXPECT_EQ(kAppId2,
+ sub1.PreferredApps().FindPreferredAppForUrl(another_filter_url));
+}
+
+TEST_F(AppServiceImplTest, PreferredAppsPersistency) {
+ AppServiceImpl::RegisterProfilePrefs(pref_service_.registry());
+ ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
+
+ const char kAppId1[] = "abcdefg";
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+ {
+ base::RunLoop run_loop_read;
+ AppServiceImpl impl(&pref_service_, temp_dir_.GetPath(),
+ run_loop_read.QuitClosure());
+ impl.FlushMojoCallsForTesting();
+ run_loop_read.Run();
+ base::RunLoop run_loop_write;
+ impl.SetWriteCompletedCallbackForTesting(run_loop_write.QuitClosure());
+ impl.AddPreferredApp(apps::mojom::AppType::kUnknown, kAppId1,
+ intent_filter->Clone(),
+ apps_util::CreateIntentFromUrl(filter_url),
+ /*from_publisher=*/false);
+ run_loop_write.Run();
+ impl.FlushMojoCallsForTesting();
+ }
+ // Create a new impl to initialize preferred apps from the disk.
+ {
+ base::RunLoop run_loop_read;
+ AppServiceImpl impl(&pref_service_, temp_dir_.GetPath(),
+ run_loop_read.QuitClosure());
+ impl.FlushMojoCallsForTesting();
+ run_loop_read.Run();
+ EXPECT_EQ(kAppId1, impl.GetPreferredAppsForTesting().FindPreferredAppForUrl(
+ filter_url));
+ }
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/BUILD.gn b/chromium/components/services/app_service/public/cpp/BUILD.gn
index d980204794c..980e8308a15 100644
--- a/chromium/components/services/app_service/public/cpp/BUILD.gn
+++ b/chromium/components/services/app_service/public/cpp/BUILD.gn
@@ -16,11 +16,136 @@ source_set("app_file_handling") {
]
}
-source_set("intent_util") {
+source_set("app_update") {
sources = [
+ "app_registry_cache.cc",
+ "app_registry_cache.h",
+ "app_registry_cache_wrapper.cc",
+ "app_registry_cache_wrapper.h",
+ "app_update.cc",
+ "app_update.h",
+ ]
+
+ public_deps = [
+ "//components/account_id:account_id",
+ "//components/services/app_service/public/mojom",
+ ]
+}
+
+if (is_chromeos) {
+ source_set("instance_update") {
+ sources = [
+ "instance.cc",
+ "instance.h",
+ "instance_registry.cc",
+ "instance_registry.h",
+ "instance_update.cc",
+ "instance_update.h",
+ ]
+ deps = [
+ "//ash/public/cpp:cpp",
+ "//content/public/browser",
+ "//skia",
+ "//ui/aura",
+ "//ui/compositor",
+ ]
+ }
+}
+
+source_set("icon_loader") {
+ sources = [
+ "icon_cache.cc",
+ "icon_cache.h",
+ "icon_coalescer.cc",
+ "icon_coalescer.h",
+ "icon_loader.cc",
+ "icon_loader.h",
+ ]
+
+ public_deps = [ "//components/services/app_service/public/mojom" ]
+}
+
+source_set("icon_loader_test_support") {
+ sources = [
+ "stub_icon_loader.cc",
+ "stub_icon_loader.h",
+ ]
+
+ deps = [ ":icon_loader" ]
+}
+
+source_set("intents") {
+ sources = [
+ "intent_filter_util.cc",
+ "intent_filter_util.h",
"intent_util.cc",
"intent_util.h",
]
- deps = [ "//base" ]
+ deps = [
+ "//base",
+ "//components/services/app_service/public/mojom",
+ "//url",
+ ]
+}
+
+source_set("preferred_apps") {
+ sources = [
+ "preferred_apps_converter.cc",
+ "preferred_apps_converter.h",
+ "preferred_apps_list.cc",
+ "preferred_apps_list.h",
+ ]
+
+ deps = [
+ ":intents",
+ "//base",
+ "//components/services/app_service/public/mojom",
+ "//url",
+ ]
+}
+
+source_set("publisher") {
+ sources = [
+ "publisher_base.cc",
+ "publisher_base.h",
+ ]
+
+ deps = [ "//components/services/app_service/public/mojom" ]
+}
+
+source_set("unit_tests") {
+ testonly = true
+
+ sources = [
+ "app_registry_cache_unittest.cc",
+ "app_update_unittest.cc",
+ "icon_cache_unittest.cc",
+ "icon_coalescer_unittest.cc",
+ "intent_test_util.cc",
+ "intent_test_util.h",
+ "intent_util_unittest.cc",
+ "preferred_apps_converter_unittest.cc",
+ "preferred_apps_list_unittest.cc",
+ ]
+
+ deps = [
+ ":app_update",
+ ":icon_loader",
+ ":intents",
+ ":preferred_apps",
+ ":publisher",
+ "//chrome/test:test_support",
+ "//content/test:test_support",
+ "//testing/gtest",
+ ]
+
+ if (is_chromeos) {
+ sources += [
+ "instance_registry_unittest.cc",
+ "instance_update_unittest.cc",
+ ]
+
+ deps += [ ":instance_update" ]
+ }
}
diff --git a/chromium/components/services/app_service/public/cpp/app_registry_cache.cc b/chromium/components/services/app_service/public/cpp/app_registry_cache.cc
new file mode 100644
index 00000000000..77b29e39555
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_registry_cache.cc
@@ -0,0 +1,133 @@
+// 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 "components/services/app_service/public/cpp/app_registry_cache.h"
+
+#include <utility>
+
+namespace apps {
+
+AppRegistryCache::Observer::Observer(AppRegistryCache* cache) {
+ Observe(cache);
+}
+
+AppRegistryCache::Observer::Observer() = default;
+
+AppRegistryCache::Observer::~Observer() {
+ if (cache_) {
+ cache_->RemoveObserver(this);
+ }
+}
+
+void AppRegistryCache::Observer::Observe(AppRegistryCache* cache) {
+ if (cache == cache_) {
+ // Early exit to avoid infinite loops if we're in the middle of a callback.
+ return;
+ }
+ if (cache_) {
+ cache_->RemoveObserver(this);
+ }
+ cache_ = cache;
+ if (cache_) {
+ cache_->AddObserver(this);
+ }
+}
+
+AppRegistryCache::AppRegistryCache() : account_id_(EmptyAccountId()) {}
+
+AppRegistryCache::~AppRegistryCache() {
+ for (auto& obs : observers_) {
+ obs.OnAppRegistryCacheWillBeDestroyed(this);
+ }
+ DCHECK(!observers_.might_have_observers());
+}
+
+void AppRegistryCache::AddObserver(Observer* observer) {
+ observers_.AddObserver(observer);
+}
+
+void AppRegistryCache::RemoveObserver(Observer* observer) {
+ observers_.RemoveObserver(observer);
+}
+
+void AppRegistryCache::OnApps(std::vector<apps::mojom::AppPtr> deltas) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+ if (!deltas_in_progress_.empty()) {
+ std::move(deltas.begin(), deltas.end(),
+ std::back_inserter(deltas_pending_));
+ return;
+ }
+
+ DoOnApps(std::move(deltas));
+ while (!deltas_pending_.empty()) {
+ std::vector<apps::mojom::AppPtr> pending;
+ pending.swap(deltas_pending_);
+ DoOnApps(std::move(pending));
+ }
+}
+
+void AppRegistryCache::DoOnApps(std::vector<apps::mojom::AppPtr> deltas) {
+ // Merge any deltas elements that have the same app_id. If an observer's
+ // OnAppUpdate calls back into this AppRegistryCache then we can therefore
+ // present a single delta for any given app_id.
+ for (auto& delta : deltas) {
+ auto d_iter = deltas_in_progress_.find(delta->app_id);
+ if (d_iter != deltas_in_progress_.end()) {
+ AppUpdate::Merge(d_iter->second, delta.get());
+ } else {
+ deltas_in_progress_[delta->app_id] = delta.get();
+ }
+ }
+
+ // The remaining for loops range over the deltas_in_progress_ map, not the
+ // deltas vector, so that OnAppUpdate is called only once per unique app_id.
+
+ // Notify the observers for every de-duplicated delta.
+ for (const auto& d_iter : deltas_in_progress_) {
+ auto s_iter = states_.find(d_iter.first);
+ apps::mojom::App* state =
+ (s_iter != states_.end()) ? s_iter->second.get() : nullptr;
+ apps::mojom::App* delta = d_iter.second;
+
+ for (auto& obs : observers_) {
+ obs.OnAppUpdate(AppUpdate(state, delta, account_id_));
+ }
+ }
+
+ // Update the states for every de-duplicated delta.
+ for (const auto& d_iter : deltas_in_progress_) {
+ auto s_iter = states_.find(d_iter.first);
+ apps::mojom::App* state =
+ (s_iter != states_.end()) ? s_iter->second.get() : nullptr;
+ apps::mojom::App* delta = d_iter.second;
+
+ if (state) {
+ AppUpdate::Merge(state, delta);
+ } else {
+ states_.insert(std::make_pair(delta->app_id, delta->Clone()));
+ }
+ }
+ deltas_in_progress_.clear();
+}
+
+apps::mojom::AppType AppRegistryCache::GetAppType(const std::string& app_id) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+ auto d_iter = deltas_in_progress_.find(app_id);
+ if (d_iter != deltas_in_progress_.end()) {
+ return d_iter->second->app_type;
+ }
+ auto s_iter = states_.find(app_id);
+ if (s_iter != states_.end()) {
+ return s_iter->second->app_type;
+ }
+ return apps::mojom::AppType::kUnknown;
+}
+
+void AppRegistryCache::SetAccountId(const AccountId& account_id) {
+ account_id_ = account_id;
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/app_registry_cache.h b/chromium/components/services/app_service/public/cpp/app_registry_cache.h
new file mode 100644
index 00000000000..6a85b578980
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_registry_cache.h
@@ -0,0 +1,201 @@
+// 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_H_
+
+#include <map>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+#include "base/sequence_checker.h"
+#include "components/account_id/account_id.h"
+#include "components/services/app_service/public/cpp/app_update.h"
+
+namespace apps {
+
+// Caches all of the apps::mojom::AppPtr's seen by an apps::mojom::Subscriber.
+// A Subscriber sees a stream of "deltas", or changes in app state. This cache
+// also keeps the "sum" of those previous deltas, so that observers of this
+// object are presented with AppUpdate's, i.e. "state-and-delta"s.
+//
+// It can also be queried synchronously, providing answers from its in-memory
+// cache, even though the underlying App Registry (and its App Publishers)
+// communicate asynchronously, possibly across process boundaries, via Mojo
+// IPC. Synchronous APIs can be more suitable for e.g. UI programming that
+// should not block an event loop on I/O.
+//
+// This class is not thread-safe.
+//
+// See //components/services/app_service/README.md for more details.
+class AppRegistryCache {
+ public:
+ class Observer : public base::CheckedObserver {
+ public:
+ // The apps::AppUpdate argument shouldn't be accessed after OnAppUpdate
+ // returns.
+ virtual void OnAppUpdate(const AppUpdate& update) = 0;
+
+ // Called when the AppRegistryCache object (the thing that this observer
+ // observes) will be destroyed. In response, the observer, |this|, should
+ // call "cache->RemoveObserver(this)", whether directly or indirectly (e.g.
+ // via ScopedObserver::Remove or via Observe(nullptr)).
+ virtual void OnAppRegistryCacheWillBeDestroyed(AppRegistryCache* cache) = 0;
+
+ protected:
+ // Use this constructor when the observer |this| is tied to a single
+ // AppRegistryCache for its entire lifetime, or until the observee (the
+ // AppRegistryCache) is destroyed, whichever comes first.
+ explicit Observer(AppRegistryCache* cache);
+
+ // Use this constructor when the observer |this| wants to observe a
+ // AppRegistryCache for part of its lifetime. It can then call Observe() to
+ // start and stop observing.
+ Observer();
+
+ ~Observer() override;
+
+ // Start observing a different AppRegistryCache. |cache| may be nullptr,
+ // meaning to stop observing.
+ void Observe(AppRegistryCache* cache);
+
+ private:
+ AppRegistryCache* cache_ = nullptr;
+
+ DISALLOW_COPY_AND_ASSIGN(Observer);
+ };
+
+ AppRegistryCache();
+ ~AppRegistryCache();
+
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ // Notifies all observers of state-and-delta AppUpdate's (the state comes
+ // from the internal cache, the delta comes from the argument) and then
+ // merges the cached states with the deltas.
+ //
+ // Notification and merging might be delayed until after OnApps returns. For
+ // example, suppose that the initial set of states is (a0, b0, c0) for three
+ // app_id's ("a", "b", "c"). Now suppose OnApps is called with two updates
+ // (b1, c1), and when notified of b1, an observer calls OnApps again with
+ // (c2, d2). The c1 delta should be processed before the c2 delta, as it was
+ // sent first: c2 should be merged (with "newest wins" semantics) onto c1 and
+ // not vice versa. This means that processing c2 (scheduled by the second
+ // OnApps call) should wait until the first OnApps call has finished
+ // processing b1 (and then c1), which means that processing c2 is delayed
+ // until after the second OnApps call returns.
+ //
+ // The callee will consume the deltas. An apps::mojom::AppPtr has the
+ // ownership semantics of a unique_ptr, and will be deleted when out of
+ // scope. The caller presumably calls OnApps(std::move(deltas)).
+ void OnApps(std::vector<apps::mojom::AppPtr> deltas);
+
+ apps::mojom::AppType GetAppType(const std::string& app_id);
+
+ void SetAccountId(const AccountId& account_id);
+
+ // Calls f, a void-returning function whose arguments are (const
+ // apps::AppUpdate&), on each app in the cache.
+ //
+ // f's argument is an apps::AppUpdate instead of an apps::mojom::AppPtr so
+ // that callers can more easily share code with Observer::OnAppUpdate (which
+ // also takes an apps::AppUpdate), and an apps::AppUpdate also has a
+ // StateIsNull method.
+ //
+ // The apps::AppUpdate argument to f shouldn't be accessed after f returns.
+ //
+ // f must be synchronous, and if it asynchronously calls ForEachApp again,
+ // it's not guaranteed to see a consistent state.
+ template <typename FunctionType>
+ void ForEachApp(FunctionType f) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+ for (const auto& s_iter : states_) {
+ const apps::mojom::App* state = s_iter.second.get();
+
+ auto d_iter = deltas_in_progress_.find(s_iter.first);
+ const apps::mojom::App* delta =
+ (d_iter != deltas_in_progress_.end()) ? d_iter->second : nullptr;
+
+ f(apps::AppUpdate(state, delta, account_id_));
+ }
+
+ for (const auto& d_iter : deltas_in_progress_) {
+ const apps::mojom::App* delta = d_iter.second;
+
+ auto s_iter = states_.find(d_iter.first);
+ if (s_iter != states_.end()) {
+ continue;
+ }
+
+ f(apps::AppUpdate(nullptr, delta, account_id_));
+ }
+ }
+
+ // Calls f, a void-returning function whose arguments are (const
+ // apps::AppUpdate&), on the app in the cache with the given app_id. It will
+ // return true (and call f) if there is such an app, otherwise it will return
+ // false (and not call f). The AppUpdate argument to f has the same semantics
+ // as for ForEachApp, above.
+ //
+ // f must be synchronous, and if it asynchronously calls ForEachApp again,
+ // it's not guaranteed to see a consistent state.
+ template <typename FunctionType>
+ bool ForOneApp(const std::string& app_id, FunctionType f) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+ auto s_iter = states_.find(app_id);
+ const apps::mojom::App* state =
+ (s_iter != states_.end()) ? s_iter->second.get() : nullptr;
+
+ auto d_iter = deltas_in_progress_.find(app_id);
+ const apps::mojom::App* delta =
+ (d_iter != deltas_in_progress_.end()) ? d_iter->second : nullptr;
+
+ if (state || delta) {
+ f(apps::AppUpdate(state, delta, account_id_));
+ return true;
+ }
+ return false;
+ }
+
+ private:
+ void DoOnApps(std::vector<apps::mojom::AppPtr> deltas);
+
+ base::ObserverList<Observer> observers_;
+
+ // Maps from app_id to the latest state: the "sum" of all previous deltas.
+ std::map<std::string, apps::mojom::AppPtr> states_;
+
+ // Track the deltas being processed or are about to be processed by OnApps.
+ // They are separate to manage the "notification and merging might be delayed
+ // until after OnApps returns" concern described above.
+ //
+ // OnApps calls DoOnApps zero or more times. If we're nested, so that there's
+ // multiple OnApps call to this AppRegistryCache in the call stack, the
+ // deeper OnApps call simply adds work to deltas_pending_ and returns without
+ // calling DoOnApps. If we're not nested, OnApps calls DoOnApps one or more
+ // times; "more times" happens if DoOnApps notifying observers leads to more
+ // OnApps calls that enqueue deltas_pending_ work. The deltas_in_progress_
+ // map (keyed by app_id) contains those deltas being considered by DoOnApps.
+ //
+ // Nested OnApps calls are expected to be rare (but still dealt with
+ // sensibly). In the typical case, OnApps should call DoOnApps exactly once,
+ // and deltas_pending_ will stay empty.
+ std::map<std::string, apps::mojom::App*> deltas_in_progress_;
+ std::vector<apps::mojom::AppPtr> deltas_pending_;
+
+ AccountId account_id_;
+
+ SEQUENCE_CHECKER(my_sequence_checker_);
+
+ DISALLOW_COPY_AND_ASSIGN(AppRegistryCache);
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_H_
diff --git a/chromium/components/services/app_service/public/cpp/app_registry_cache_unittest.cc b/chromium/components/services/app_service/public/cpp/app_registry_cache_unittest.cc
new file mode 100644
index 00000000000..e732c0018f1
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_registry_cache_unittest.cc
@@ -0,0 +1,384 @@
+// 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 <map>
+
+#include "components/services/app_service/public/cpp/app_registry_cache.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class AppRegistryCacheTest : public testing::Test,
+ public apps::AppRegistryCache::Observer {
+ protected:
+ static apps::mojom::AppPtr MakeApp(
+ const char* app_id,
+ const char* name,
+ apps::mojom::Readiness readiness = apps::mojom::Readiness::kUnknown) {
+ apps::mojom::AppPtr app = apps::mojom::App::New();
+ app->app_type = apps::mojom::AppType::kArc;
+ app->app_id = app_id;
+ app->readiness = readiness;
+ app->name = name;
+ return app;
+ }
+
+ void CallForEachApp(apps::AppRegistryCache& cache) {
+ cache.ForEachApp(
+ [this](const apps::AppUpdate& update) { OnAppUpdate(update); });
+ }
+
+ std::string GetName(apps::AppRegistryCache& cache,
+ const std::string& app_id) {
+ std::string name;
+ cache.ForOneApp(app_id, [&name](const apps::AppUpdate& update) {
+ name = update.Name();
+ });
+ return name;
+ }
+
+ // apps::AppRegistryCache::Observer overrides.
+ void OnAppUpdate(const apps::AppUpdate& update) override {
+ EXPECT_EQ(account_id_, update.AccountId());
+ EXPECT_NE("", update.Name());
+ if (update.ReadinessChanged() &&
+ (update.Readiness() == apps::mojom::Readiness::kReady)) {
+ num_freshly_installed_++;
+ }
+ updated_ids_.insert(update.AppId());
+ updated_names_.insert(update.Name());
+ }
+
+ void OnAppRegistryCacheWillBeDestroyed(
+ apps::AppRegistryCache* cache) override {
+ // The test code explicitly calls both AddObserver and RemoveObserver.
+ NOTREACHED();
+ }
+
+ const AccountId& account_id() const { return account_id_; }
+
+ int num_freshly_installed_ = 0;
+ std::set<std::string> updated_ids_;
+ std::set<std::string> updated_names_;
+ AccountId account_id_ = AccountId::FromUserEmail("test@gmail.com");
+};
+
+// Responds to a cache's OnAppUpdate to call back into the cache, checking that
+// the cache presents a self-consistent snapshot. For example, the app names
+// should match for the outer and inner AppUpdate.
+//
+// In the tests below, just "recursive" means that cache.OnApps calls
+// observer.OnAppsUpdate which calls cache.ForEachApp and cache.ForOneApp.
+// "Super-recursive" means that cache.OnApps calls observer.OnAppsUpdate calls
+// cache.OnApps which calls observer.OnAppsUpdate.
+class RecursiveObserver : public apps::AppRegistryCache::Observer {
+ public:
+ explicit RecursiveObserver(apps::AppRegistryCache* cache) : cache_(cache) {
+ Observe(cache);
+ }
+
+ ~RecursiveObserver() override = default;
+
+ void PrepareForOnApps(
+ int expected_num_apps,
+ const std::string& expected_name_for_p,
+ std::vector<apps::mojom::AppPtr>* super_recursive_apps = nullptr) {
+ expected_name_for_p_ = expected_name_for_p;
+ expected_num_apps_ = expected_num_apps;
+ num_apps_seen_on_app_update_ = 0;
+ old_names_.clear();
+
+ names_snapshot_.clear();
+ check_names_snapshot_ = true;
+ if (super_recursive_apps) {
+ check_names_snapshot_ = false;
+ super_recursive_apps_.swap(*super_recursive_apps);
+ }
+ }
+
+ int NumAppsSeenOnAppUpdate() { return num_apps_seen_on_app_update_; }
+
+ protected:
+ // apps::AppRegistryCache::Observer overrides.
+ void OnAppUpdate(const apps::AppUpdate& outer) override {
+ EXPECT_EQ(account_id_, outer.AccountId());
+ int num_apps = 0;
+ cache_->ForEachApp([this, &outer, &num_apps](const apps::AppUpdate& inner) {
+ if (check_names_snapshot_) {
+ if (num_apps_seen_on_app_update_ == 0) {
+ // If this is the first time that OnAppUpdate is called, after a
+ // PrepareForOnApps call, then just populate the names_snapshot_ map.
+ names_snapshot_[inner.AppId()] = inner.Name();
+ } else {
+ // Otherwise, check that the names found during this OnAppUpdate call
+ // match those during the first OnAppUpdate call.
+ auto iter = names_snapshot_.find(inner.AppId());
+ EXPECT_EQ(inner.Name(),
+ (iter != names_snapshot_.end()) ? iter->second : "");
+ }
+ }
+
+ if (outer.AppId() == inner.AppId()) {
+ ExpectEq(outer, inner);
+ }
+
+ if (inner.AppId() == "p") {
+ EXPECT_EQ(expected_name_for_p_, inner.Name());
+ }
+
+ num_apps++;
+ });
+ EXPECT_EQ(expected_num_apps_, num_apps);
+
+ EXPECT_FALSE(cache_->ForOneApp(
+ "no_such_app_id",
+ [&outer](const apps::AppUpdate& inner) { ExpectEq(outer, inner); }));
+
+ EXPECT_TRUE(cache_->ForOneApp(
+ outer.AppId(),
+ [&outer](const apps::AppUpdate& inner) { ExpectEq(outer, inner); }));
+
+ if (outer.NameChanged()) {
+ std::string old_name;
+ auto iter = old_names_.find(outer.AppId());
+ if (iter != old_names_.end()) {
+ old_name = iter->second;
+ }
+ // The way the tests are configured, if an app's name changes, it should
+ // increase (in string comparison order): e.g. from "" to "mango" or from
+ // "mango" to "mulberry" and never from "mulberry" to "melon".
+ EXPECT_LT(old_name, outer.Name());
+ }
+ old_names_[outer.AppId()] = outer.Name();
+
+ std::vector<apps::mojom::AppPtr> super_recursive;
+ while (!super_recursive_apps_.empty()) {
+ apps::mojom::AppPtr app = std::move(super_recursive_apps_.back());
+ super_recursive_apps_.pop_back();
+ if (app.get() == nullptr) {
+ // This is the placeholder 'punctuation'.
+ break;
+ }
+ super_recursive.push_back(std::move(app));
+ }
+ if (!super_recursive.empty()) {
+ cache_->OnApps(std::move(super_recursive));
+ }
+
+ num_apps_seen_on_app_update_++;
+ }
+
+ void OnAppRegistryCacheWillBeDestroyed(
+ apps::AppRegistryCache* cache) override {
+ Observe(nullptr);
+ }
+
+ static void ExpectEq(const apps::AppUpdate& outer,
+ const apps::AppUpdate& inner) {
+ EXPECT_EQ(outer.AppType(), inner.AppType());
+ EXPECT_EQ(outer.AppId(), inner.AppId());
+ EXPECT_EQ(outer.StateIsNull(), inner.StateIsNull());
+ EXPECT_EQ(outer.Readiness(), inner.Readiness());
+ EXPECT_EQ(outer.Name(), inner.Name());
+ }
+
+ apps::AppRegistryCache* cache_;
+ std::string expected_name_for_p_;
+ int expected_num_apps_;
+ int num_apps_seen_on_app_update_;
+ AccountId account_id_ = AccountId::FromUserEmail("test@gmail.com");
+
+ // Records previously seen app names, keyed by app_id's, so we can check
+ // that, for these tests, a given app's name is always increasing (in string
+ // comparison order).
+ std::map<std::string, std::string> old_names_;
+
+ // Non-empty when this.OnAppsUpdate should trigger more cache_.OnApps calls.
+ //
+ // During OnAppsUpdate, this vector (a stack) is popped from the back until a
+ // nullptr 'punctuation' element (a group terminator) is seen. If that group
+ // of popped elements (in LIFO order) is non-empty, that group forms the
+ // vector of App's passed to cache_.OnApps.
+ std::vector<apps::mojom::AppPtr> super_recursive_apps_;
+
+ // For non-super-recursive tests (i.e. for check_names_snapshot_ == true), we
+ // check that the "app_id to name" mapping is consistent across every
+ // OnAppsUpdate call to this observer. For super-recursive tests, that
+ // mapping can change as updates are processed, so the names_snapshot_ check
+ // is skipped.
+ bool check_names_snapshot_ = false;
+ std::map<std::string, std::string> names_snapshot_;
+};
+
+TEST_F(AppRegistryCacheTest, ForEachApp) {
+ std::vector<apps::mojom::AppPtr> deltas;
+ apps::AppRegistryCache cache;
+ cache.SetAccountId(account_id());
+
+ updated_names_.clear();
+ CallForEachApp(cache);
+
+ EXPECT_EQ(0u, updated_names_.size());
+
+ deltas.clear();
+ deltas.push_back(MakeApp("a", "apple"));
+ deltas.push_back(MakeApp("b", "banana"));
+ deltas.push_back(MakeApp("c", "cherry"));
+ cache.OnApps(std::move(deltas));
+
+ updated_names_.clear();
+ CallForEachApp(cache);
+
+ EXPECT_EQ(3u, updated_names_.size());
+ EXPECT_NE(updated_names_.end(), updated_names_.find("apple"));
+ EXPECT_NE(updated_names_.end(), updated_names_.find("banana"));
+ EXPECT_NE(updated_names_.end(), updated_names_.find("cherry"));
+
+ deltas.clear();
+ deltas.push_back(MakeApp("a", "apricot"));
+ deltas.push_back(MakeApp("d", "durian"));
+ cache.OnApps(std::move(deltas));
+
+ updated_names_.clear();
+ CallForEachApp(cache);
+
+ EXPECT_EQ(4u, updated_names_.size());
+ EXPECT_NE(updated_names_.end(), updated_names_.find("apricot"));
+ EXPECT_NE(updated_names_.end(), updated_names_.find("banana"));
+ EXPECT_NE(updated_names_.end(), updated_names_.find("cherry"));
+ EXPECT_NE(updated_names_.end(), updated_names_.find("durian"));
+
+ // Test that ForOneApp succeeds for "c" and fails for "e".
+
+ bool found_c = false;
+ EXPECT_TRUE(cache.ForOneApp("c", [&found_c](const apps::AppUpdate& update) {
+ found_c = true;
+ EXPECT_EQ("c", update.AppId());
+ }));
+ EXPECT_TRUE(found_c);
+
+ bool found_e = false;
+ EXPECT_FALSE(cache.ForOneApp("e", [&found_e](const apps::AppUpdate& update) {
+ found_e = true;
+ EXPECT_EQ("e", update.AppId());
+ }));
+ EXPECT_FALSE(found_e);
+}
+
+TEST_F(AppRegistryCacheTest, Observer) {
+ std::vector<apps::mojom::AppPtr> deltas;
+ apps::AppRegistryCache cache;
+ cache.SetAccountId(account_id());
+
+ cache.AddObserver(this);
+
+ num_freshly_installed_ = 0;
+ updated_ids_.clear();
+ deltas.clear();
+ deltas.push_back(MakeApp("a", "avocado"));
+ deltas.push_back(MakeApp("c", "cucumber"));
+ deltas.push_back(MakeApp("e", "eggfruit"));
+ cache.OnApps(std::move(deltas));
+
+ EXPECT_EQ(0, num_freshly_installed_);
+ EXPECT_EQ(3u, updated_ids_.size());
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("a"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("c"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("e"));
+
+ num_freshly_installed_ = 0;
+ updated_ids_.clear();
+ deltas.clear();
+ deltas.push_back(MakeApp("b", "blueberry"));
+ deltas.push_back(MakeApp("c", "cucumber", apps::mojom::Readiness::kReady));
+ cache.OnApps(std::move(deltas));
+
+ EXPECT_EQ(1, num_freshly_installed_);
+ EXPECT_EQ(2u, updated_ids_.size());
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("b"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("c"));
+
+ cache.RemoveObserver(this);
+
+ num_freshly_installed_ = 0;
+ updated_ids_.clear();
+ deltas.clear();
+ deltas.push_back(MakeApp("f", "fig"));
+ cache.OnApps(std::move(deltas));
+
+ EXPECT_EQ(0, num_freshly_installed_);
+ EXPECT_EQ(0u, updated_ids_.size());
+}
+
+TEST_F(AppRegistryCacheTest, Recursive) {
+ std::vector<apps::mojom::AppPtr> deltas;
+ apps::AppRegistryCache cache;
+ cache.SetAccountId(account_id());
+ RecursiveObserver observer(&cache);
+
+ observer.PrepareForOnApps(2, "peach");
+ deltas.clear();
+ deltas.push_back(MakeApp("o", "orange"));
+ deltas.push_back(MakeApp("p", "peach"));
+ cache.OnApps(std::move(deltas));
+ EXPECT_EQ(2, observer.NumAppsSeenOnAppUpdate());
+
+ observer.PrepareForOnApps(3, "pear");
+ deltas.clear();
+ deltas.push_back(MakeApp("p", "pear", apps::mojom::Readiness::kReady));
+ deltas.push_back(MakeApp("q", "quince"));
+ cache.OnApps(std::move(deltas));
+ EXPECT_EQ(2, observer.NumAppsSeenOnAppUpdate());
+
+ observer.PrepareForOnApps(3, "plum");
+ deltas.clear();
+ deltas.push_back(MakeApp("p", "pear"));
+ deltas.push_back(MakeApp("p", "pear"));
+ deltas.push_back(MakeApp("p", "plum"));
+ cache.OnApps(std::move(deltas));
+ EXPECT_EQ(1, observer.NumAppsSeenOnAppUpdate());
+}
+
+TEST_F(AppRegistryCacheTest, SuperRecursive) {
+ std::vector<apps::mojom::AppPtr> deltas;
+ apps::AppRegistryCache cache;
+ cache.SetAccountId(account_id());
+ RecursiveObserver observer(&cache);
+
+ // Set up a series of OnApps to be called during observer.OnAppUpdate:
+ // - the 1st update is {"blackberry, "coconut"}.
+ // - the 2nd update is {}.
+ // - the 3rd update is {"blackcurrant", "apricot", "blueberry"}.
+ // - the 4th update is {"avocado"}.
+ // - the 5th update is {}.
+ // - the 6th update is {"boysenberry"}.
+ //
+ // The vector is processed in LIFO order with nullptr punctuation to
+ // terminate each group. See the comment on the
+ // RecursiveObserver::super_recursive_apps_ field.
+ std::vector<apps::mojom::AppPtr> super_recursive_apps;
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(MakeApp("b", "boysenberry"));
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(MakeApp("a", "avocado"));
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(MakeApp("b", "blueberry"));
+ super_recursive_apps.push_back(MakeApp("a", "apricot"));
+ super_recursive_apps.push_back(MakeApp("b", "blackcurrant"));
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(MakeApp("c", "coconut"));
+ super_recursive_apps.push_back(MakeApp("b", "blackberry"));
+
+ observer.PrepareForOnApps(3, "", &super_recursive_apps);
+ deltas.clear();
+ deltas.push_back(MakeApp("a", "apple"));
+ deltas.push_back(MakeApp("b", "banana"));
+ deltas.push_back(MakeApp("c", "cherry"));
+ cache.OnApps(std::move(deltas));
+
+ // After all of that, check that for each app_id, the last delta won.
+ EXPECT_EQ("avocado", GetName(cache, "a"));
+ EXPECT_EQ("boysenberry", GetName(cache, "b"));
+ EXPECT_EQ("coconut", GetName(cache, "c"));
+}
diff --git a/chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.cc b/chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.cc
new file mode 100644
index 00000000000..c5f700a50e6
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.cc
@@ -0,0 +1,46 @@
+// Copyright 2020 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 "components/services/app_service/public/cpp/app_registry_cache_wrapper.h"
+
+#include "base/no_destructor.h"
+#include "components/account_id/account_id.h"
+#include "components/services/app_service/public/cpp/app_registry_cache.h"
+
+namespace apps {
+
+// static
+AppRegistryCacheWrapper& AppRegistryCacheWrapper::Get() {
+ static base::NoDestructor<AppRegistryCacheWrapper> instance;
+ return *instance;
+}
+
+AppRegistryCacheWrapper::AppRegistryCacheWrapper() = default;
+
+AppRegistryCacheWrapper::~AppRegistryCacheWrapper() = default;
+
+AppRegistryCache* AppRegistryCacheWrapper::GetAppRegistryCache(
+ const AccountId& account_id) {
+ auto it = app_registry_caches_.find(account_id);
+ if (it == app_registry_caches_.end()) {
+ return nullptr;
+ }
+ return it->second;
+}
+
+void AppRegistryCacheWrapper::AddAppRegistryCache(const AccountId& account_id,
+ AppRegistryCache* cache) {
+ app_registry_caches_[account_id] = cache;
+}
+
+void AppRegistryCacheWrapper::RemoveAppRegistryCache(AppRegistryCache* cache) {
+ for (auto& it : app_registry_caches_) {
+ if (it.second == cache) {
+ app_registry_caches_.erase(it.first);
+ return;
+ }
+ }
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.h b/chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.h
new file mode 100644
index 00000000000..37cad43dc63
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_registry_cache_wrapper.h
@@ -0,0 +1,46 @@
+// Copyright 2020 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_WRAPPER_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_WRAPPER_H_
+
+#include <map>
+
+class AccountId;
+
+namespace apps {
+
+class AppRegistryCache;
+
+// Wraps AppRegistryCache to get all AppRegistryCaches independently. Provides
+// the method to get the AppRegistryCache per |account_id|.
+class AppRegistryCacheWrapper {
+ public:
+ // Returns the global AppRegistryCacheWrapper object.
+ static AppRegistryCacheWrapper& Get();
+
+ AppRegistryCacheWrapper();
+ ~AppRegistryCacheWrapper();
+
+ AppRegistryCacheWrapper(const AppRegistryCacheWrapper&) = delete;
+ AppRegistryCacheWrapper& operator=(const AppRegistryCacheWrapper&) = delete;
+
+ // Returns AppRegistryCache for the given |account_id|, or return null if
+ // AppRegistryCache doesn't exist.
+ AppRegistryCache* GetAppRegistryCache(const AccountId& account_id);
+
+ // Adds the AppRegistryCache for the given |account_id|.
+ void AddAppRegistryCache(const AccountId& account_id,
+ AppRegistryCache* cache);
+
+ // Removes the |cache| in |app_registry_caches_|.
+ void RemoveAppRegistryCache(AppRegistryCache* cache);
+
+ private:
+ std::map<AccountId, AppRegistryCache*> app_registry_caches_;
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_REGISTRY_CACHE_WRAPPER_H_
diff --git a/chromium/components/services/app_service/public/cpp/app_update.cc b/chromium/components/services/app_service/public/cpp/app_update.cc
new file mode 100644
index 00000000000..f4d236719b8
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_update.cc
@@ -0,0 +1,527 @@
+// 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 "components/services/app_service/public/cpp/app_update.h"
+
+#include "base/logging.h"
+#include "base/strings/string_util.h"
+#include "base/time/time.h"
+
+namespace {
+
+void ClonePermissions(const std::vector<apps::mojom::PermissionPtr>& clone_from,
+ std::vector<apps::mojom::PermissionPtr>* clone_to) {
+ for (const auto& permission : clone_from) {
+ clone_to->push_back(permission->Clone());
+ }
+}
+
+void CloneStrings(const std::vector<std::string>& clone_from,
+ std::vector<std::string>* clone_to) {
+ for (const auto& s : clone_from) {
+ clone_to->push_back(s);
+ }
+}
+
+void CloneIntentFilters(
+ const std::vector<apps::mojom::IntentFilterPtr>& clone_from,
+ std::vector<apps::mojom::IntentFilterPtr>* clone_to) {
+ for (const auto& intent_filter : clone_from) {
+ clone_to->push_back(intent_filter->Clone());
+ }
+}
+
+} // namespace
+
+namespace apps {
+
+// static
+void AppUpdate::Merge(apps::mojom::App* state, const apps::mojom::App* delta) {
+ DCHECK(state);
+ if (!delta) {
+ return;
+ }
+
+ if ((delta->app_type != state->app_type) ||
+ (delta->app_id != state->app_id)) {
+ LOG(ERROR) << "inconsistent (app_type, app_id): (" << delta->app_type
+ << ", " << delta->app_id << ") vs (" << state->app_type << ", "
+ << state->app_id << ") ";
+ DCHECK(false);
+ return;
+ }
+
+ if (delta->readiness != apps::mojom::Readiness::kUnknown) {
+ state->readiness = delta->readiness;
+ }
+ if (delta->name.has_value()) {
+ state->name = delta->name;
+ }
+ if (delta->short_name.has_value()) {
+ state->short_name = delta->short_name;
+ }
+ if (delta->publisher_id.has_value()) {
+ state->publisher_id = delta->publisher_id;
+ }
+ if (delta->description.has_value()) {
+ state->description = delta->description;
+ }
+ if (delta->version.has_value()) {
+ state->version = delta->version;
+ }
+ if (!delta->additional_search_terms.empty()) {
+ state->additional_search_terms.clear();
+ CloneStrings(delta->additional_search_terms,
+ &state->additional_search_terms);
+ }
+ if (!delta->icon_key.is_null()) {
+ state->icon_key = delta->icon_key.Clone();
+ }
+ if (delta->last_launch_time.has_value()) {
+ state->last_launch_time = delta->last_launch_time;
+ }
+ if (delta->install_time.has_value()) {
+ state->install_time = delta->install_time;
+ }
+ if (!delta->permissions.empty()) {
+ DCHECK(state->permissions.empty() ||
+ (delta->permissions.size() == state->permissions.size()));
+ state->permissions.clear();
+ ClonePermissions(delta->permissions, &state->permissions);
+ }
+ if (delta->install_source != apps::mojom::InstallSource::kUnknown) {
+ state->install_source = delta->install_source;
+ }
+ if (delta->is_platform_app != apps::mojom::OptionalBool::kUnknown) {
+ state->is_platform_app = delta->is_platform_app;
+ }
+ if (delta->recommendable != apps::mojom::OptionalBool::kUnknown) {
+ state->recommendable = delta->recommendable;
+ }
+ if (delta->searchable != apps::mojom::OptionalBool::kUnknown) {
+ state->searchable = delta->searchable;
+ }
+ if (delta->show_in_launcher != apps::mojom::OptionalBool::kUnknown) {
+ state->show_in_launcher = delta->show_in_launcher;
+ }
+ if (delta->show_in_shelf != apps::mojom::OptionalBool::kUnknown) {
+ state->show_in_shelf = delta->show_in_shelf;
+ }
+ if (delta->show_in_search != apps::mojom::OptionalBool::kUnknown) {
+ state->show_in_search = delta->show_in_search;
+ }
+ if (delta->show_in_management != apps::mojom::OptionalBool::kUnknown) {
+ state->show_in_management = delta->show_in_management;
+ }
+ if (delta->has_badge != apps::mojom::OptionalBool::kUnknown) {
+ state->has_badge = delta->has_badge;
+ }
+ if (delta->paused != apps::mojom::OptionalBool::kUnknown) {
+ state->paused = delta->paused;
+ }
+
+ if (!delta->intent_filters.empty()) {
+ state->intent_filters.clear();
+ CloneIntentFilters(delta->intent_filters, &state->intent_filters);
+ }
+
+ // When adding new fields to the App Mojo type, this function should also be
+ // updated.
+}
+
+AppUpdate::AppUpdate(const apps::mojom::App* state,
+ const apps::mojom::App* delta,
+ const ::AccountId& account_id)
+ : state_(state), delta_(delta), account_id_(account_id) {
+ DCHECK(state_ || delta_);
+ if (state_ && delta_) {
+ DCHECK(state_->app_type == delta->app_type);
+ DCHECK(state_->app_id == delta->app_id);
+ }
+}
+
+bool AppUpdate::StateIsNull() const {
+ return state_ == nullptr;
+}
+
+apps::mojom::AppType AppUpdate::AppType() const {
+ return delta_ ? delta_->app_type : state_->app_type;
+}
+
+const std::string& AppUpdate::AppId() const {
+ return delta_ ? delta_->app_id : state_->app_id;
+}
+
+apps::mojom::Readiness AppUpdate::Readiness() const {
+ if (delta_ && (delta_->readiness != apps::mojom::Readiness::kUnknown)) {
+ return delta_->readiness;
+ }
+ if (state_) {
+ return state_->readiness;
+ }
+ return apps::mojom::Readiness::kUnknown;
+}
+
+bool AppUpdate::ReadinessChanged() const {
+ return delta_ && (delta_->readiness != apps::mojom::Readiness::kUnknown) &&
+ (!state_ || (delta_->readiness != state_->readiness));
+}
+
+const std::string& AppUpdate::Name() const {
+ if (delta_ && delta_->name.has_value()) {
+ return delta_->name.value();
+ }
+ if (state_ && state_->name.has_value()) {
+ return state_->name.value();
+ }
+ return base::EmptyString();
+}
+
+bool AppUpdate::NameChanged() const {
+ return delta_ && delta_->name.has_value() &&
+ (!state_ || (delta_->name != state_->name));
+}
+
+const std::string& AppUpdate::ShortName() const {
+ if (delta_ && delta_->short_name.has_value()) {
+ return delta_->short_name.value();
+ }
+ if (state_ && state_->short_name.has_value()) {
+ return state_->short_name.value();
+ }
+ return base::EmptyString();
+}
+
+bool AppUpdate::ShortNameChanged() const {
+ return delta_ && delta_->short_name.has_value() &&
+ (!state_ || (delta_->short_name != state_->short_name));
+}
+
+const std::string& AppUpdate::PublisherId() const {
+ if (delta_ && delta_->publisher_id.has_value()) {
+ return delta_->publisher_id.value();
+ }
+ if (state_ && state_->publisher_id.has_value()) {
+ return state_->publisher_id.value();
+ }
+ return base::EmptyString();
+}
+
+bool AppUpdate::PublisherIdChanged() const {
+ return delta_ && delta_->publisher_id.has_value() &&
+ (!state_ || (delta_->publisher_id != state_->publisher_id));
+}
+
+const std::string& AppUpdate::Description() const {
+ if (delta_ && delta_->description.has_value()) {
+ return delta_->description.value();
+ }
+ if (state_ && state_->description.has_value()) {
+ return state_->description.value();
+ }
+ return base::EmptyString();
+}
+
+bool AppUpdate::DescriptionChanged() const {
+ return delta_ && delta_->description.has_value() &&
+ (!state_ || (delta_->description != state_->description));
+}
+
+const std::string& AppUpdate::Version() const {
+ if (delta_ && delta_->version.has_value()) {
+ return delta_->version.value();
+ }
+ if (state_ && state_->version.has_value()) {
+ return state_->version.value();
+ }
+ return base::EmptyString();
+}
+
+bool AppUpdate::VersionChanged() const {
+ return delta_ && delta_->version.has_value() &&
+ (!state_ || (delta_->version != state_->version));
+}
+
+std::vector<std::string> AppUpdate::AdditionalSearchTerms() const {
+ std::vector<std::string> additional_search_terms;
+
+ if (delta_ && !delta_->additional_search_terms.empty()) {
+ CloneStrings(delta_->additional_search_terms, &additional_search_terms);
+ } else if (state_ && !state_->additional_search_terms.empty()) {
+ CloneStrings(state_->additional_search_terms, &additional_search_terms);
+ }
+
+ return additional_search_terms;
+}
+
+bool AppUpdate::AdditionalSearchTermsChanged() const {
+ return delta_ && !delta_->additional_search_terms.empty() &&
+ (!state_ ||
+ (delta_->additional_search_terms != state_->additional_search_terms));
+}
+
+apps::mojom::IconKeyPtr AppUpdate::IconKey() const {
+ if (delta_ && !delta_->icon_key.is_null()) {
+ return delta_->icon_key.Clone();
+ }
+ if (state_ && !state_->icon_key.is_null()) {
+ return state_->icon_key.Clone();
+ }
+ return apps::mojom::IconKeyPtr();
+}
+
+bool AppUpdate::IconKeyChanged() const {
+ return delta_ && !delta_->icon_key.is_null() &&
+ (!state_ || !delta_->icon_key.Equals(state_->icon_key));
+}
+
+base::Time AppUpdate::LastLaunchTime() const {
+ if (delta_ && delta_->last_launch_time.has_value()) {
+ return delta_->last_launch_time.value();
+ }
+ if (state_ && state_->last_launch_time.has_value()) {
+ return state_->last_launch_time.value();
+ }
+ return base::Time();
+}
+
+bool AppUpdate::LastLaunchTimeChanged() const {
+ return delta_ && delta_->last_launch_time.has_value() &&
+ (!state_ || (delta_->last_launch_time != state_->last_launch_time));
+}
+
+base::Time AppUpdate::InstallTime() const {
+ if (delta_ && delta_->install_time.has_value()) {
+ return delta_->install_time.value();
+ }
+ if (state_ && state_->install_time.has_value()) {
+ return state_->install_time.value();
+ }
+ return base::Time();
+}
+
+bool AppUpdate::InstallTimeChanged() const {
+ return delta_ && delta_->install_time.has_value() &&
+ (!state_ || (delta_->install_time != state_->install_time));
+}
+
+std::vector<apps::mojom::PermissionPtr> AppUpdate::Permissions() const {
+ std::vector<apps::mojom::PermissionPtr> permissions;
+
+ if (delta_ && !delta_->permissions.empty()) {
+ ClonePermissions(delta_->permissions, &permissions);
+ } else if (state_ && !state_->permissions.empty()) {
+ ClonePermissions(state_->permissions, &permissions);
+ }
+
+ return permissions;
+}
+
+bool AppUpdate::PermissionsChanged() const {
+ return delta_ && !delta_->permissions.empty() &&
+ (!state_ || (delta_->permissions != state_->permissions));
+}
+
+apps::mojom::InstallSource AppUpdate::InstallSource() const {
+ if (delta_ &&
+ (delta_->install_source != apps::mojom::InstallSource::kUnknown)) {
+ return delta_->install_source;
+ }
+ if (state_) {
+ return state_->install_source;
+ }
+ return apps::mojom::InstallSource::kUnknown;
+}
+
+bool AppUpdate::InstallSourceChanged() const {
+ return delta_ &&
+ (delta_->install_source != apps::mojom::InstallSource::kUnknown) &&
+ (!state_ || (delta_->install_source != state_->install_source));
+}
+
+apps::mojom::OptionalBool AppUpdate::InstalledInternally() const {
+ switch (InstallSource()) {
+ case apps::mojom::InstallSource::kUnknown:
+ return apps::mojom::OptionalBool::kUnknown;
+ case apps::mojom::InstallSource::kSystem:
+ case apps::mojom::InstallSource::kPolicy:
+ case apps::mojom::InstallSource::kOem:
+ case apps::mojom::InstallSource::kDefault:
+ return apps::mojom::OptionalBool::kTrue;
+ default:
+ return apps::mojom::OptionalBool::kFalse;
+ }
+}
+
+apps::mojom::OptionalBool AppUpdate::IsPlatformApp() const {
+ if (delta_ &&
+ (delta_->is_platform_app != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->is_platform_app;
+ }
+ if (state_) {
+ return state_->is_platform_app;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::IsPlatformAppChanged() const {
+ return delta_ &&
+ (delta_->is_platform_app != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->is_platform_app != state_->is_platform_app));
+}
+
+apps::mojom::OptionalBool AppUpdate::Recommendable() const {
+ if (delta_ &&
+ (delta_->recommendable != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->recommendable;
+ }
+ if (state_) {
+ return state_->recommendable;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::RecommendableChanged() const {
+ return delta_ &&
+ (delta_->recommendable != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->recommendable != state_->recommendable));
+}
+
+apps::mojom::OptionalBool AppUpdate::Searchable() const {
+ if (delta_ && (delta_->searchable != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->searchable;
+ }
+ if (state_) {
+ return state_->searchable;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::SearchableChanged() const {
+ return delta_ &&
+ (delta_->searchable != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->searchable != state_->searchable));
+}
+
+apps::mojom::OptionalBool AppUpdate::ShowInLauncher() const {
+ if (delta_ &&
+ (delta_->show_in_launcher != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->show_in_launcher;
+ }
+ if (state_) {
+ return state_->show_in_launcher;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::ShowInLauncherChanged() const {
+ return delta_ &&
+ (delta_->show_in_launcher != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->show_in_launcher != state_->show_in_launcher));
+}
+
+apps::mojom::OptionalBool AppUpdate::ShowInShelf() const {
+ if (delta_ &&
+ (delta_->show_in_shelf != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->show_in_shelf;
+ }
+ if (state_) {
+ return state_->show_in_shelf;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::ShowInShelfChanged() const {
+ return delta_ &&
+ (delta_->show_in_shelf != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->show_in_shelf != state_->show_in_shelf));
+}
+
+apps::mojom::OptionalBool AppUpdate::ShowInSearch() const {
+ if (delta_ &&
+ (delta_->show_in_search != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->show_in_search;
+ }
+ if (state_) {
+ return state_->show_in_search;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::ShowInSearchChanged() const {
+ return delta_ &&
+ (delta_->show_in_search != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->show_in_search != state_->show_in_search));
+}
+
+apps::mojom::OptionalBool AppUpdate::ShowInManagement() const {
+ if (delta_ &&
+ (delta_->show_in_management != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->show_in_management;
+ }
+ if (state_) {
+ return state_->show_in_management;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::ShowInManagementChanged() const {
+ return delta_ &&
+ (delta_->show_in_management != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ ||
+ (delta_->show_in_management != state_->show_in_management));
+}
+
+apps::mojom::OptionalBool AppUpdate::HasBadge() const {
+ if (delta_ && (delta_->has_badge != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->has_badge;
+ }
+ if (state_) {
+ return state_->has_badge;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::HasBadgeChanged() const {
+ return delta_ && (delta_->has_badge != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->has_badge != state_->has_badge));
+}
+
+apps::mojom::OptionalBool AppUpdate::Paused() const {
+ if (delta_ && (delta_->paused != apps::mojom::OptionalBool::kUnknown)) {
+ return delta_->paused;
+ }
+ if (state_) {
+ return state_->paused;
+ }
+ return apps::mojom::OptionalBool::kUnknown;
+}
+
+bool AppUpdate::PausedChanged() const {
+ return delta_ && (delta_->paused != apps::mojom::OptionalBool::kUnknown) &&
+ (!state_ || (delta_->paused != state_->paused));
+}
+
+std::vector<apps::mojom::IntentFilterPtr> AppUpdate::IntentFilters() const {
+ std::vector<apps::mojom::IntentFilterPtr> intent_filters;
+
+ if (delta_ && !delta_->intent_filters.empty()) {
+ CloneIntentFilters(delta_->intent_filters, &intent_filters);
+ } else if (state_ && !state_->intent_filters.empty()) {
+ CloneIntentFilters(state_->intent_filters, &intent_filters);
+ }
+
+ return intent_filters;
+}
+
+bool AppUpdate::IntentFiltersChanged() const {
+ return delta_ && !delta_->intent_filters.empty() &&
+ (!state_ || (delta_->intent_filters != state_->intent_filters));
+}
+
+const ::AccountId& AppUpdate::AccountId() const {
+ return account_id_;
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/app_update.h b/chromium/components/services/app_service/public/cpp/app_update.h
new file mode 100644
index 00000000000..41a49a85a8e
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_update.h
@@ -0,0 +1,148 @@
+// 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_UPDATE_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_UPDATE_H_
+
+#include <string>
+#include <vector>
+
+#include "base/macros.h"
+#include "components/account_id/account_id.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+
+namespace apps {
+
+// Wraps two apps::mojom::AppPtr's, a prior state and a delta on top of that
+// state. The state is conceptually the "sum" of all of the previous deltas,
+// with "addition" or "merging" simply being that the most recent version of
+// each field "wins".
+//
+// The state may be nullptr, meaning that there are no previous deltas.
+// Alternatively, the delta may be nullptr, meaning that there is no change in
+// state. At least one of state and delta must be non-nullptr.
+//
+// Almost all of an AppPtr's fields are optional. For example, if an app's name
+// hasn't changed, then a delta doesn't necessarily have to contain a copy of
+// the name, as the prior state should already contain it.
+//
+// The combination of the two (state and delta) can answer questions such as:
+// - What is the app's name? If the delta knows, that's the answer. Otherwise,
+// ask the state.
+// - Is the app ready to launch (i.e. installed)? Likewise, if the delta says
+// yes or no, that's the answer. Otherwise, the delta says "unknown", so ask
+// the state.
+// - Was the app *freshly* installed (i.e. it previously wasn't installed, but
+// now is)? Has its readiness changed? Check if the delta says "installed"
+// and the state says either "uninstalled" or unknown.
+//
+// An AppUpdate is read-only once constructed. All of its fields and methods
+// are const. The constructor caller must guarantee that the AppPtr references
+// remain valid for the lifetime of the AppUpdate.
+//
+// See //components/services/app_service/README.md for more details.
+class AppUpdate {
+ public:
+ // Modifies |state| by copying over all of |delta|'s known fields: those
+ // fields whose values aren't "unknown". The |state| may not be nullptr.
+ static void Merge(apps::mojom::App* state, const apps::mojom::App* delta);
+
+ // At most one of |state| or |delta| may be nullptr.
+ AppUpdate(const apps::mojom::App* state,
+ const apps::mojom::App* delta,
+ const AccountId& account_id);
+
+ // Returns whether this is the first update for the given AppId.
+ // Equivalently, there are no previous deltas for the AppId.
+ bool StateIsNull() const;
+
+ apps::mojom::AppType AppType() const;
+
+ const std::string& AppId() const;
+
+ apps::mojom::Readiness Readiness() const;
+ bool ReadinessChanged() const;
+
+ const std::string& Name() const;
+ bool NameChanged() const;
+
+ const std::string& ShortName() const;
+ bool ShortNameChanged() const;
+
+ // The publisher-specific ID for this app, e.g. for Android apps, this field
+ // contains the Android package name. May be empty if AppId() should be
+ // considered as the canonical publisher ID.
+ const std::string& PublisherId() const;
+ bool PublisherIdChanged() const;
+
+ const std::string& Description() const;
+ bool DescriptionChanged() const;
+
+ const std::string& Version() const;
+ bool VersionChanged() const;
+
+ std::vector<std::string> AdditionalSearchTerms() const;
+ bool AdditionalSearchTermsChanged() const;
+
+ apps::mojom::IconKeyPtr IconKey() const;
+ bool IconKeyChanged() const;
+
+ base::Time LastLaunchTime() const;
+ bool LastLaunchTimeChanged() const;
+
+ base::Time InstallTime() const;
+ bool InstallTimeChanged() const;
+
+ std::vector<apps::mojom::PermissionPtr> Permissions() const;
+ bool PermissionsChanged() const;
+
+ apps::mojom::InstallSource InstallSource() const;
+ bool InstallSourceChanged() const;
+
+ apps::mojom::OptionalBool InstalledInternally() const;
+
+ apps::mojom::OptionalBool IsPlatformApp() const;
+ bool IsPlatformAppChanged() const;
+
+ apps::mojom::OptionalBool Recommendable() const;
+ bool RecommendableChanged() const;
+
+ apps::mojom::OptionalBool Searchable() const;
+ bool SearchableChanged() const;
+
+ apps::mojom::OptionalBool ShowInLauncher() const;
+ bool ShowInLauncherChanged() const;
+
+ apps::mojom::OptionalBool ShowInShelf() const;
+ bool ShowInShelfChanged() const;
+
+ apps::mojom::OptionalBool ShowInSearch() const;
+ bool ShowInSearchChanged() const;
+
+ apps::mojom::OptionalBool ShowInManagement() const;
+ bool ShowInManagementChanged() const;
+
+ apps::mojom::OptionalBool HasBadge() const;
+ bool HasBadgeChanged() const;
+
+ apps::mojom::OptionalBool Paused() const;
+ bool PausedChanged() const;
+
+ std::vector<apps::mojom::IntentFilterPtr> IntentFilters() const;
+ bool IntentFiltersChanged() const;
+
+ const ::AccountId& AccountId() const;
+
+ private:
+ const apps::mojom::App* state_;
+ const apps::mojom::App* delta_;
+
+ const ::AccountId& account_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(AppUpdate);
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_APP_UPDATE_H_
diff --git a/chromium/components/services/app_service/public/cpp/app_update_unittest.cc b/chromium/components/services/app_service/public/cpp/app_update_unittest.cc
new file mode 100644
index 00000000000..2d8984bd734
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/app_update_unittest.cc
@@ -0,0 +1,798 @@
+// 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 "components/services/app_service/public/cpp/app_update.h"
+#include "components/services/app_service/public/cpp/intent_filter_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+const apps::mojom::AppType app_type = apps::mojom::AppType::kArc;
+const char app_id[] = "abcdefgh";
+const char test_name_0[] = "Inigo Montoya";
+const char test_name_1[] = "Dread Pirate Roberts";
+} // namespace
+
+class AppUpdateTest : public testing::Test {
+ protected:
+ apps::mojom::Readiness expect_readiness_;
+ bool expect_readiness_changed_;
+
+ std::string expect_name_;
+ bool expect_name_changed_;
+
+ std::string expect_short_name_;
+ bool expect_short_name_changed_;
+
+ std::string expect_publisher_id_;
+ bool expect_publisher_id_changed_;
+
+ std::string expect_description_;
+ bool expect_description_changed_;
+
+ std::string expect_version_;
+ bool expect_version_changed_;
+
+ std::vector<std::string> expect_additional_search_terms_;
+ bool expect_additional_search_terms_changed_;
+
+ apps::mojom::IconKeyPtr expect_icon_key_;
+ bool expect_icon_key_changed_;
+
+ base::Time expect_last_launch_time_;
+ bool expect_last_launch_time_changed_;
+
+ base::Time expect_install_time_;
+ bool expect_install_time_changed_;
+
+ std::vector<apps::mojom::PermissionPtr> expect_permissions_;
+ bool expect_permissions_changed_;
+
+ apps::mojom::InstallSource expect_install_source_;
+ bool expect_install_source_changed_;
+
+ apps::mojom::OptionalBool expect_is_platform_app_;
+ bool expect_is_platform_app_changed_;
+
+ apps::mojom::OptionalBool expect_recommendable_;
+ bool expect_recommendable_changed_;
+
+ apps::mojom::OptionalBool expect_searchable_;
+ bool expect_searchable_changed_;
+
+ apps::mojom::OptionalBool expect_show_in_launcher_;
+ bool expect_show_in_launcher_changed_;
+
+ apps::mojom::OptionalBool expect_show_in_shelf_;
+ bool expect_show_in_shelf_changed_;
+
+ apps::mojom::OptionalBool expect_show_in_search_;
+ bool expect_show_in_search_changed_;
+
+ apps::mojom::OptionalBool expect_show_in_management_;
+ bool expect_show_in_management_changed_;
+
+ apps::mojom::OptionalBool expect_has_badge_;
+ bool expect_has_badge_changed_;
+
+ apps::mojom::OptionalBool expect_paused_;
+ bool expect_paused_changed_;
+
+ std::vector<apps::mojom::IntentFilterPtr> expect_intent_filters_;
+ bool expect_intent_filters_changed_;
+
+ AccountId account_id_ = AccountId::FromUserEmail("test@gmail.com");
+
+ static constexpr uint32_t kPermissionTypeLocation = 100;
+ static constexpr uint32_t kPermissionTypeNotification = 200;
+
+ apps::mojom::PermissionPtr MakePermission(uint32_t permission_id,
+ apps::mojom::TriState value) {
+ apps::mojom::PermissionPtr permission = apps::mojom::Permission::New();
+ permission->permission_id = permission_id;
+ permission->value_type = apps::mojom::PermissionValueType::kTriState;
+ permission->value = static_cast<uint32_t>(value);
+ return permission;
+ }
+
+ void ExpectNoChange() {
+ expect_readiness_changed_ = false;
+ expect_name_changed_ = false;
+ expect_short_name_changed_ = false;
+ expect_publisher_id_changed_ = false;
+ expect_description_changed_ = false;
+ expect_version_changed_ = false;
+ expect_additional_search_terms_changed_ = false;
+ expect_icon_key_changed_ = false;
+ expect_last_launch_time_changed_ = false;
+ expect_install_time_changed_ = false;
+ expect_permissions_changed_ = false;
+ expect_install_source_changed_ = false;
+ expect_is_platform_app_changed_ = false;
+ expect_recommendable_changed_ = false;
+ expect_searchable_changed_ = false;
+ expect_show_in_launcher_changed_ = false;
+ expect_show_in_shelf_changed_ = false;
+ expect_show_in_search_changed_ = false;
+ expect_show_in_management_changed_ = false;
+ expect_has_badge_changed_ = false;
+ expect_paused_changed_ = false;
+ expect_intent_filters_changed_ = false;
+ }
+
+ void CheckExpects(const apps::AppUpdate& u) {
+ EXPECT_EQ(expect_readiness_, u.Readiness());
+ EXPECT_EQ(expect_readiness_changed_, u.ReadinessChanged());
+
+ EXPECT_EQ(expect_name_, u.Name());
+ EXPECT_EQ(expect_name_changed_, u.NameChanged());
+
+ EXPECT_EQ(expect_short_name_, u.ShortName());
+ EXPECT_EQ(expect_short_name_changed_, u.ShortNameChanged());
+
+ EXPECT_EQ(expect_publisher_id_, u.PublisherId());
+ EXPECT_EQ(expect_publisher_id_changed_, u.PublisherIdChanged());
+
+ EXPECT_EQ(expect_description_, u.Description());
+ EXPECT_EQ(expect_description_changed_, u.DescriptionChanged());
+
+ EXPECT_EQ(expect_version_, u.Version());
+ EXPECT_EQ(expect_version_changed_, u.VersionChanged());
+
+ EXPECT_EQ(expect_additional_search_terms_, u.AdditionalSearchTerms());
+ EXPECT_EQ(expect_additional_search_terms_changed_,
+ u.AdditionalSearchTermsChanged());
+
+ EXPECT_EQ(expect_icon_key_, u.IconKey());
+ EXPECT_EQ(expect_icon_key_changed_, u.IconKeyChanged());
+
+ EXPECT_EQ(expect_last_launch_time_, u.LastLaunchTime());
+ EXPECT_EQ(expect_last_launch_time_changed_, u.LastLaunchTimeChanged());
+
+ EXPECT_EQ(expect_install_time_, u.InstallTime());
+ EXPECT_EQ(expect_install_time_changed_, u.InstallTimeChanged());
+
+ EXPECT_EQ(expect_permissions_, u.Permissions());
+ EXPECT_EQ(expect_permissions_changed_, u.PermissionsChanged());
+
+ EXPECT_EQ(expect_install_source_, u.InstallSource());
+ EXPECT_EQ(expect_install_source_changed_, u.InstallSourceChanged());
+
+ EXPECT_EQ(expect_is_platform_app_, u.IsPlatformApp());
+ EXPECT_EQ(expect_is_platform_app_changed_, u.IsPlatformAppChanged());
+
+ EXPECT_EQ(expect_recommendable_, u.Recommendable());
+ EXPECT_EQ(expect_recommendable_changed_, u.RecommendableChanged());
+
+ EXPECT_EQ(expect_searchable_, u.Searchable());
+ EXPECT_EQ(expect_searchable_changed_, u.SearchableChanged());
+
+ EXPECT_EQ(expect_show_in_launcher_, u.ShowInLauncher());
+ EXPECT_EQ(expect_show_in_launcher_changed_, u.ShowInLauncherChanged());
+
+ EXPECT_EQ(expect_show_in_shelf_, u.ShowInShelf());
+ EXPECT_EQ(expect_show_in_shelf_changed_, u.ShowInShelfChanged());
+
+ EXPECT_EQ(expect_show_in_search_, u.ShowInSearch());
+ EXPECT_EQ(expect_show_in_search_changed_, u.ShowInSearchChanged());
+
+ EXPECT_EQ(expect_show_in_management_, u.ShowInManagement());
+ EXPECT_EQ(expect_show_in_management_changed_, u.ShowInManagementChanged());
+
+ EXPECT_EQ(expect_has_badge_, u.HasBadge());
+ EXPECT_EQ(expect_has_badge_changed_, u.HasBadgeChanged());
+
+ EXPECT_EQ(expect_paused_, u.Paused());
+ EXPECT_EQ(expect_paused_changed_, u.PausedChanged());
+
+ EXPECT_EQ(expect_intent_filters_, u.IntentFilters());
+ EXPECT_EQ(expect_intent_filters_changed_, u.IntentFiltersChanged());
+
+ EXPECT_EQ(account_id_, u.AccountId());
+ }
+
+ void TestAppUpdate(apps::mojom::App* state, apps::mojom::App* delta) {
+ apps::AppUpdate u(state, delta, account_id_);
+
+ EXPECT_EQ(app_type, u.AppType());
+ EXPECT_EQ(app_id, u.AppId());
+ EXPECT_EQ(state == nullptr, u.StateIsNull());
+
+ expect_readiness_ = apps::mojom::Readiness::kUnknown;
+ expect_name_ = "";
+ expect_short_name_ = "";
+ expect_publisher_id_ = "";
+ expect_description_ = "";
+ expect_version_ = "";
+ expect_additional_search_terms_.clear();
+ expect_icon_key_ = nullptr;
+ expect_last_launch_time_ = base::Time();
+ expect_install_time_ = base::Time();
+ expect_permissions_.clear();
+ expect_install_source_ = apps::mojom::InstallSource::kUnknown;
+ expect_is_platform_app_ = apps::mojom::OptionalBool::kUnknown;
+ expect_recommendable_ = apps::mojom::OptionalBool::kUnknown;
+ expect_searchable_ = apps::mojom::OptionalBool::kUnknown;
+ expect_show_in_launcher_ = apps::mojom::OptionalBool::kUnknown;
+ expect_show_in_shelf_ = apps::mojom::OptionalBool::kUnknown;
+ expect_show_in_search_ = apps::mojom::OptionalBool::kUnknown;
+ expect_show_in_management_ = apps::mojom::OptionalBool::kUnknown;
+ expect_has_badge_ = apps::mojom::OptionalBool::kUnknown;
+ expect_paused_ = apps::mojom::OptionalBool::kUnknown;
+ expect_intent_filters_.clear();
+ ExpectNoChange();
+ CheckExpects(u);
+
+ if (delta) {
+ delta->name = test_name_0;
+ expect_name_ = test_name_0;
+ expect_name_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ state->name = test_name_0;
+ expect_name_ = test_name_0;
+ expect_name_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->readiness = apps::mojom::Readiness::kReady;
+ expect_readiness_ = apps::mojom::Readiness::kReady;
+ expect_readiness_changed_ = true;
+ CheckExpects(u);
+
+ delta->name = base::nullopt;
+ expect_name_ = state ? test_name_0 : "";
+ expect_name_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->readiness = apps::mojom::Readiness::kDisabledByPolicy;
+ expect_readiness_ = apps::mojom::Readiness::kDisabledByPolicy;
+ expect_readiness_changed_ = true;
+ delta->name = test_name_1;
+ expect_name_ = test_name_1;
+ expect_name_changed_ = true;
+ CheckExpects(u);
+ }
+
+ // ShortName tests.
+
+ if (state) {
+ state->short_name = "Kate";
+ expect_short_name_ = "Kate";
+ expect_short_name_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->short_name = "Bob";
+ expect_short_name_ = "Bob";
+ expect_short_name_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // PublisherId tests.
+
+ if (state) {
+ state->publisher_id = "com.google.android.youtube";
+ expect_publisher_id_ = "com.google.android.youtube";
+ expect_publisher_id_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->publisher_id = "com.android.youtube";
+ expect_publisher_id_ = "com.android.youtube";
+ expect_publisher_id_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // Description tests.
+
+ if (state) {
+ state->description = "Has a cat.";
+ expect_description_ = "Has a cat.";
+ expect_description_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->description = "Has a dog.";
+ expect_description_ = "Has a dog.";
+ expect_description_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // Version tests.
+
+ if (state) {
+ state->version = "1.0.0";
+ expect_version_ = "1.0.0";
+ expect_version_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->version = "1.0.1";
+ expect_version_ = "1.0.1";
+ expect_version_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // AdditionalSearchTerms tests.
+
+ if (state) {
+ state->additional_search_terms.push_back("cat");
+ state->additional_search_terms.push_back("dog");
+ expect_additional_search_terms_.push_back("cat");
+ expect_additional_search_terms_.push_back("dog");
+ expect_additional_search_terms_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ expect_additional_search_terms_.clear();
+ delta->additional_search_terms.push_back("horse");
+ delta->additional_search_terms.push_back("mouse");
+ expect_additional_search_terms_.push_back("horse");
+ expect_additional_search_terms_.push_back("mouse");
+ expect_additional_search_terms_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // IconKey tests.
+
+ if (state) {
+ auto x = apps::mojom::IconKey::New(100, 0, 0);
+ state->icon_key = x.Clone();
+ expect_icon_key_ = x.Clone();
+ expect_icon_key_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ auto x = apps::mojom::IconKey::New(200, 0, 0);
+ delta->icon_key = x.Clone();
+ expect_icon_key_ = x.Clone();
+ expect_icon_key_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // LastLaunchTime tests.
+
+ if (state) {
+ state->last_launch_time = base::Time::FromDoubleT(1000.0);
+ expect_last_launch_time_ = base::Time::FromDoubleT(1000.0);
+ expect_last_launch_time_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->last_launch_time = base::Time::FromDoubleT(1001.0);
+ expect_last_launch_time_ = base::Time::FromDoubleT(1001.0);
+ expect_last_launch_time_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // InstallTime tests.
+
+ if (state) {
+ state->install_time = base::Time::FromDoubleT(2000.0);
+ expect_install_time_ = base::Time::FromDoubleT(2000.0);
+ expect_install_time_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->install_time = base::Time::FromDoubleT(2001.0);
+ expect_install_time_ = base::Time::FromDoubleT(2001.0);
+ expect_install_time_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // InstallSource tests.
+ if (state) {
+ state->install_source = apps::mojom::InstallSource::kUser;
+ expect_install_source_ = apps::mojom::InstallSource::kUser;
+ expect_install_source_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->install_source = apps::mojom::InstallSource::kPolicy;
+ expect_install_source_ = apps::mojom::InstallSource::kPolicy;
+ expect_install_source_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // IsPlatformApp tests.
+
+ if (state) {
+ state->is_platform_app = apps::mojom::OptionalBool::kFalse;
+ expect_is_platform_app_ = apps::mojom::OptionalBool::kFalse;
+ expect_is_platform_app_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->is_platform_app = apps::mojom::OptionalBool::kTrue;
+ expect_is_platform_app_ = apps::mojom::OptionalBool::kTrue;
+ expect_is_platform_app_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // Recommendable tests.
+
+ if (state) {
+ state->recommendable = apps::mojom::OptionalBool::kFalse;
+ expect_recommendable_ = apps::mojom::OptionalBool::kFalse;
+ expect_recommendable_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->recommendable = apps::mojom::OptionalBool::kTrue;
+ expect_recommendable_ = apps::mojom::OptionalBool::kTrue;
+ expect_recommendable_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // Searchable tests.
+
+ if (state) {
+ state->searchable = apps::mojom::OptionalBool::kFalse;
+ expect_searchable_ = apps::mojom::OptionalBool::kFalse;
+ expect_searchable_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->searchable = apps::mojom::OptionalBool::kTrue;
+ expect_searchable_ = apps::mojom::OptionalBool::kTrue;
+ expect_searchable_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // ShowInLauncher tests.
+
+ if (state) {
+ state->show_in_launcher = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_launcher_ = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_launcher_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->show_in_launcher = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_launcher_ = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_launcher_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // ShowInShelf tests.
+
+ if (state) {
+ state->show_in_shelf = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_shelf_ = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_shelf_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->show_in_shelf = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_shelf_ = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_shelf_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // ShowInSearch tests.
+
+ if (state) {
+ state->show_in_search = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_search_ = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_search_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->show_in_search = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_search_ = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_search_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // ShowInManagement tests.
+
+ if (state) {
+ state->show_in_management = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_management_ = apps::mojom::OptionalBool::kFalse;
+ expect_show_in_management_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->show_in_management = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_management_ = apps::mojom::OptionalBool::kTrue;
+ expect_show_in_management_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // HasBadge tests.
+
+ if (state) {
+ state->has_badge = apps::mojom::OptionalBool::kFalse;
+ expect_has_badge_ = apps::mojom::OptionalBool::kFalse;
+ expect_has_badge_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->has_badge = apps::mojom::OptionalBool::kTrue;
+ expect_has_badge_ = apps::mojom::OptionalBool::kTrue;
+ expect_has_badge_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // Pause tests.
+
+ if (state) {
+ state->paused = apps::mojom::OptionalBool::kFalse;
+ expect_paused_ = apps::mojom::OptionalBool::kFalse;
+ expect_paused_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ delta->paused = apps::mojom::OptionalBool::kTrue;
+ expect_paused_ = apps::mojom::OptionalBool::kTrue;
+ expect_paused_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // Permission tests.
+
+ if (state) {
+ auto p0 = MakePermission(kPermissionTypeLocation,
+ apps::mojom::TriState::kAllow);
+ auto p1 = MakePermission(kPermissionTypeNotification,
+ apps::mojom::TriState::kAllow);
+ state->permissions.push_back(p0.Clone());
+ state->permissions.push_back(p1.Clone());
+ expect_permissions_.push_back(p0.Clone());
+ expect_permissions_.push_back(p1.Clone());
+ expect_permissions_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ expect_permissions_.clear();
+ auto p0 = MakePermission(kPermissionTypeNotification,
+ apps::mojom::TriState::kAllow);
+ auto p1 = MakePermission(kPermissionTypeLocation,
+ apps::mojom::TriState::kBlock);
+
+ delta->permissions.push_back(p0.Clone());
+ delta->permissions.push_back(p1.Clone());
+ expect_permissions_.push_back(p0.Clone());
+ expect_permissions_.push_back(p1.Clone());
+ expect_permissions_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+
+ // Intent Filter tests.
+
+ if (state) {
+ auto intent_filter = apps::mojom::IntentFilter::New();
+
+ std::vector<apps::mojom::ConditionValuePtr> scheme_condition_values;
+ scheme_condition_values.push_back(apps_util::MakeConditionValue(
+ "https", apps::mojom::PatternMatchType::kNone));
+ auto scheme_condition =
+ apps_util::MakeCondition(apps::mojom::ConditionType::kScheme,
+ std::move(scheme_condition_values));
+ intent_filter->conditions.push_back(std::move(scheme_condition));
+
+ std::vector<apps::mojom::ConditionValuePtr> host_condition_values;
+ host_condition_values.push_back(apps_util::MakeConditionValue(
+ "www.google.com", apps::mojom::PatternMatchType::kNone));
+ auto host_condition = apps_util::MakeCondition(
+ apps::mojom::ConditionType::kHost, std::move(host_condition_values));
+ intent_filter->conditions.push_back(std::move(host_condition));
+
+ intent_filter->conditions.push_back(scheme_condition.Clone());
+ intent_filter->conditions.push_back(host_condition.Clone());
+
+ state->intent_filters.push_back(intent_filter.Clone());
+ expect_intent_filters_.push_back(intent_filter.Clone());
+ expect_intent_filters_changed_ = false;
+ CheckExpects(u);
+ }
+
+ if (delta) {
+ expect_intent_filters_.clear();
+
+ auto intent_filter = apps::mojom::IntentFilter::New();
+
+ std::vector<apps::mojom::ConditionValuePtr> scheme_condition_values;
+ scheme_condition_values.push_back(apps_util::MakeConditionValue(
+ "https", apps::mojom::PatternMatchType::kNone));
+ auto scheme_condition =
+ apps_util::MakeCondition(apps::mojom::ConditionType::kScheme,
+ std::move(scheme_condition_values));
+ intent_filter->conditions.push_back(std::move(scheme_condition));
+
+ std::vector<apps::mojom::ConditionValuePtr> host_condition_values;
+ host_condition_values.push_back(apps_util::MakeConditionValue(
+ "www.abc.com", apps::mojom::PatternMatchType::kNone));
+ auto host_condition = apps_util::MakeCondition(
+ apps::mojom::ConditionType::kHost, std::move(host_condition_values));
+ intent_filter->conditions.push_back(std::move(host_condition));
+
+ intent_filter->conditions.push_back(scheme_condition.Clone());
+ intent_filter->conditions.push_back(host_condition.Clone());
+
+ delta->intent_filters.push_back(intent_filter.Clone());
+ expect_intent_filters_.push_back(intent_filter.Clone());
+ expect_intent_filters_changed_ = true;
+ CheckExpects(u);
+ }
+
+ if (state) {
+ apps::AppUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+ }
+};
+
+TEST_F(AppUpdateTest, StateIsNonNull) {
+ apps::mojom::AppPtr state = apps::mojom::App::New();
+ state->app_type = app_type;
+ state->app_id = app_id;
+
+ TestAppUpdate(state.get(), nullptr);
+}
+
+TEST_F(AppUpdateTest, DeltaIsNonNull) {
+ apps::mojom::AppPtr delta = apps::mojom::App::New();
+ delta->app_type = app_type;
+ delta->app_id = app_id;
+
+ TestAppUpdate(nullptr, delta.get());
+}
+
+TEST_F(AppUpdateTest, BothAreNonNull) {
+ apps::mojom::AppPtr state = apps::mojom::App::New();
+ state->app_type = app_type;
+ state->app_id = app_id;
+
+ apps::mojom::AppPtr delta = apps::mojom::App::New();
+ delta->app_type = app_type;
+ delta->app_id = app_id;
+
+ TestAppUpdate(state.get(), delta.get());
+}
diff --git a/chromium/components/services/app_service/public/cpp/icon_cache.cc b/chromium/components/services/app_service/public/cpp/icon_cache.cc
new file mode 100644
index 00000000000..922879b99bc
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_cache.cc
@@ -0,0 +1,157 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/icon_cache.h"
+
+#include <utility>
+
+#include "base/callback.h"
+
+namespace apps {
+
+IconCache::Value::Value()
+ : image_(), is_placeholder_icon_(false), ref_count_(0) {}
+
+apps::mojom::IconValuePtr IconCache::Value::AsIconValue() {
+ auto icon_value = apps::mojom::IconValue::New();
+ icon_value->icon_compression = apps::mojom::IconCompression::kUncompressed;
+ icon_value->uncompressed = image_;
+ icon_value->is_placeholder_icon = is_placeholder_icon_;
+ return icon_value;
+}
+
+IconCache::IconCache(IconLoader* wrapped_loader,
+ GarbageCollectionPolicy gc_policy)
+ : wrapped_loader_(wrapped_loader), gc_policy_(gc_policy) {}
+
+IconCache::~IconCache() = default;
+
+apps::mojom::IconKeyPtr IconCache::GetIconKey(const std::string& app_id) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ return wrapped_loader_ ? wrapped_loader_->GetIconKey(app_id)
+ : apps::mojom::IconKey::New();
+}
+
+std::unique_ptr<IconLoader::Releaser> IconCache::LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ IconLoader::Key key(
+ app_type, app_id, icon_key, icon_compression, size_hint_in_dip,
+ // We pass false instead of allow_placeholder_icon, as the Value
+ // already records placeholder-ness. If the allow_placeholder_icon
+ // arg to this function is true, we can re-use a cache hit regardless
+ // of whether the previous call to the underlying wrapped_loader_
+ // returned the placeholder icon or the real icon, so we don't want
+ // to restrict our map lookup to only one flavor.
+ false);
+ Value* cache_hit = nullptr;
+ bool ref_count_incremented = false;
+
+ if (icon_compression == apps::mojom::IconCompression::kUncompressed) {
+ auto iter = map_.find(key);
+ if (iter == map_.end()) {
+ iter = map_.insert(std::make_pair(key, Value())).first;
+ } else if (!iter->second.image_.isNull() &&
+ (allow_placeholder_icon || !iter->second.is_placeholder_icon_)) {
+ cache_hit = &iter->second;
+ }
+
+ iter->second.ref_count_++;
+ ref_count_incremented = true;
+ }
+
+ std::unique_ptr<IconLoader::Releaser> releaser(nullptr);
+ if (cache_hit) {
+ std::move(callback).Run(cache_hit->AsIconValue());
+ } else if (wrapped_loader_) {
+ releaser = wrapped_loader_->LoadIconFromIconKey(
+ app_type, app_id, std::move(icon_key), icon_compression,
+ size_hint_in_dip, allow_placeholder_icon,
+ base::BindOnce(&IconCache::OnLoadIcon, weak_ptr_factory_.GetWeakPtr(),
+ key, std::move(callback)));
+ } else {
+ std::move(callback).Run(apps::mojom::IconValue::New());
+ }
+
+ return ref_count_incremented
+ ? std::make_unique<IconLoader::Releaser>(
+ std::move(releaser),
+ base::BindOnce(&IconCache::OnRelease,
+ weak_ptr_factory_.GetWeakPtr(),
+ std::move(key)))
+ : std::move(releaser);
+}
+
+void IconCache::SweepReleasedIcons() {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+ if (gc_policy_ != GarbageCollectionPolicy::kExplicit) {
+ return;
+ }
+
+ auto iter = map_.begin();
+ while (iter != map_.end()) {
+ if (iter->second.ref_count_ == 0) {
+ iter = map_.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+}
+
+void IconCache::Update(const IconLoader::Key& key,
+ const apps::mojom::IconValue& icon_value) {
+ if (icon_value.icon_compression !=
+ apps::mojom::IconCompression::kUncompressed) {
+ return;
+ }
+
+ auto iter = map_.find(key);
+ if (iter == map_.end()) {
+ return;
+ }
+
+ // Don't let a placeholder overwrite a real icon.
+ if (icon_value.is_placeholder_icon && !iter->second.is_placeholder_icon_) {
+ return;
+ }
+
+ iter->second.image_ = icon_value.uncompressed;
+}
+
+void IconCache::OnLoadIcon(
+ IconLoader::Key key,
+ apps::mojom::Publisher::LoadIconCallback wrapped_callback,
+ apps::mojom::IconValuePtr icon_value) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ Update(key, *icon_value);
+ std::move(wrapped_callback).Run(std::move(icon_value));
+}
+
+void IconCache::OnRelease(IconLoader::Key key) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+ auto iter = map_.find(key);
+ if (iter == map_.end()) {
+ NOTREACHED();
+ return;
+ }
+
+ uint64_t n = iter->second.ref_count_;
+ DCHECK(n > 0);
+ n--;
+ iter->second.ref_count_ = n;
+
+ if ((n == 0) && (gc_policy_ == GarbageCollectionPolicy::kEager)) {
+ map_.erase(iter);
+ }
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/icon_cache.h b/chromium/components/services/app_service/public/cpp/icon_cache.h
new file mode 100644
index 00000000000..8c0430cf720
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_cache.h
@@ -0,0 +1,118 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_CACHE_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_CACHE_H_
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
+#include "components/services/app_service/public/cpp/icon_loader.h"
+#include "components/services/app_service/public/mojom/app_service.mojom.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+#include "ui/gfx/image/image_skia.h"
+
+namespace apps {
+
+// An IconLoader that caches the apps::mojom::IconCompression::kUncompressed
+// results of another (wrapped) IconLoader.
+class IconCache : public IconLoader {
+ public:
+ // What triggers dropping no-longer-used icons from the cache.
+ //
+ // If unsure of which one to use, kEager is a safe choice, with little
+ // overhead above not having an icon cache at all.
+ enum class GarbageCollectionPolicy {
+ // kEager means that we drop icons as soon as their ref-count hits zero
+ // (i.e. all the IconLoader::Releaser's returned by LoadIconFromIconKey
+ // have been destroyed).
+ //
+ // This minimizes the overall memory cost of the cache. Only icons that are
+ // still actively used stay alive in the cache.
+ //
+ // On the other hand, this can result in more cache misses than other
+ // policies. For example, suppose that some UI starts with a widget showing
+ // the "foo" app icon. In response to user input, the UI destroys that
+ // widget and then creates a new widget to show the same "foo" app icon.
+ // With a kEager garbage collection policy, that freshly created widget
+ // might not get a cache hit, if the icon's ref-count hits zero in between
+ // the two widgets' destruction and creation.
+ kEager,
+
+ // kExplicit means that icons can remain in the cache, even if their
+ // ref-count hits zero. Instead, explicit calls to SweepReleasedIcons are
+ // needed to clear cache entries.
+ //
+ // This can use more memory than kEager, but it can also provide a cache
+ // hit in the "destroy and then create" example described above.
+ //
+ // On the other hand, it requires more effort and more thought from the
+ // programmer. They need to make additional calls (to SweepReleasedIcons),
+ // so they can't just drop an IconCache in transparently. The programmer
+ // also needs to think about when is a good time to make those calls. Too
+ // frequent, and you get extra complexity for not much more benefit than
+ // using kEager. Too infrequent, and you have the memory cost of keeping
+ // unused icons around.
+ //
+ // All together, kExplicit might not be the best policy for e.g. a
+ // process-wide icon cache with many clients, each with different usage
+ // patterns.
+ kExplicit,
+ };
+
+ IconCache(IconLoader* wrapped_loader, GarbageCollectionPolicy gc_policy);
+ ~IconCache() override;
+
+ // IconLoader overrides.
+ apps::mojom::IconKeyPtr GetIconKey(const std::string& app_id) override;
+ std::unique_ptr<IconLoader::Releaser> LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) override;
+
+ // A hint that now is a good time to garbage-collect any icons that are not
+ // actively held.
+ void SweepReleasedIcons();
+
+ private:
+ class Value {
+ public:
+ gfx::ImageSkia image_;
+ bool is_placeholder_icon_;
+ uint64_t ref_count_;
+
+ Value();
+
+ apps::mojom::IconValuePtr AsIconValue();
+ };
+
+ void Update(const IconLoader::Key&, const apps::mojom::IconValue&);
+ void OnLoadIcon(IconLoader::Key,
+ apps::mojom::Publisher::LoadIconCallback,
+ apps::mojom::IconValuePtr);
+ void OnRelease(IconLoader::Key);
+
+ std::map<IconLoader::Key, Value> map_;
+ IconLoader* wrapped_loader_;
+ GarbageCollectionPolicy gc_policy_;
+
+ SEQUENCE_CHECKER(sequence_checker_);
+
+ base::WeakPtrFactory<IconCache> weak_ptr_factory_{this};
+
+ DISALLOW_COPY_AND_ASSIGN(IconCache);
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_CACHE_H_
diff --git a/chromium/components/services/app_service/public/cpp/icon_cache_unittest.cc b/chromium/components/services/app_service/public/cpp/icon_cache_unittest.cc
new file mode 100644
index 00000000000..9b6e64f4d01
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_cache_unittest.cc
@@ -0,0 +1,214 @@
+// Copyright 2019 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 <utility>
+
+#include "base/bind_helpers.h"
+#include "base/callback.h"
+#include "components/services/app_service/public/cpp/icon_cache.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/gfx/geometry/size.h"
+#include "ui/gfx/image/image_skia_rep.h"
+
+class AppsIconCacheTest : public testing::Test {
+ protected:
+ enum class HitOrMiss {
+ kHit,
+ kMiss,
+ };
+
+ using UniqueReleaser = std::unique_ptr<apps::IconLoader::Releaser>;
+ static constexpr HitOrMiss kHit = HitOrMiss::kHit;
+ static constexpr HitOrMiss kMiss = HitOrMiss::kMiss;
+
+ class FakeIconLoader : public apps::IconLoader {
+ public:
+ int NumLoadIconFromIconKeyCalls() { return num_load_calls_; }
+
+ void SetReturnPlaceholderIcons(bool b) { return_placeholder_icons_ = b; }
+
+ private:
+ apps::mojom::IconKeyPtr GetIconKey(const std::string& app_id) override {
+ return apps::mojom::IconKey::New(0, 0, 0);
+ }
+
+ std::unique_ptr<Releaser> LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) override {
+ num_load_calls_++;
+
+ auto iv = apps::mojom::IconValue::New();
+ if (icon_compression == apps::mojom::IconCompression::kUncompressed) {
+ iv->icon_compression = apps::mojom::IconCompression::kUncompressed;
+ iv->uncompressed =
+ gfx::ImageSkia(gfx::ImageSkiaRep(gfx::Size(1, 1), 1.0f));
+ iv->is_placeholder_icon = return_placeholder_icons_;
+ }
+
+ std::move(callback).Run(std::move(iv));
+ return nullptr;
+ }
+
+ int num_load_calls_ = 0;
+ bool return_placeholder_icons_ = false;
+ };
+
+ UniqueReleaser LoadIcon(apps::IconLoader* loader,
+ FakeIconLoader* fake,
+ const std::string& app_id,
+ HitOrMiss expect_hom,
+ bool allow_placeholder_icon = false) {
+ static constexpr auto app_type = apps::mojom::AppType::kWeb;
+ static constexpr auto icon_compression =
+ apps::mojom::IconCompression::kUncompressed;
+ static constexpr int32_t size_hint_in_dip = 1;
+
+ int before = fake->NumLoadIconFromIconKeyCalls();
+
+ UniqueReleaser releaser =
+ loader->LoadIcon(app_type, app_id, icon_compression, size_hint_in_dip,
+ allow_placeholder_icon, base::DoNothing());
+
+ int after = fake->NumLoadIconFromIconKeyCalls();
+ HitOrMiss actual_hom = (after == before) ? kHit : kMiss;
+ EXPECT_EQ(expect_hom, actual_hom);
+
+ return releaser;
+ }
+
+ void TestBasics(apps::IconCache::GarbageCollectionPolicy gc_policy) {
+ FakeIconLoader fake;
+ apps::IconCache cache(&fake, gc_policy);
+
+ UniqueReleaser a0 = LoadIcon(&cache, &fake, "apricot", kMiss);
+ a0.reset();
+
+ UniqueReleaser b0 = LoadIcon(&cache, &fake, "banana", kMiss);
+ UniqueReleaser b1 = LoadIcon(&cache, &fake, "banana", kHit);
+ b0.reset();
+ b1.reset();
+
+ UniqueReleaser c0 = LoadIcon(&cache, &fake, "cherry", kMiss);
+ UniqueReleaser c1 = LoadIcon(&cache, &fake, "cherry", kHit);
+ UniqueReleaser c2 = LoadIcon(&cache, &fake, "cherry", kHit);
+ c2.reset();
+ c1.reset();
+
+ UniqueReleaser d0 = LoadIcon(&cache, &fake, "durian", kMiss);
+ d0.reset();
+
+ UniqueReleaser c3 = LoadIcon(&cache, &fake, "cherry", kHit);
+ c3.reset();
+
+ if (gc_policy == apps::IconCache::GarbageCollectionPolicy::kExplicit) {
+ cache.SweepReleasedIcons();
+ }
+
+ UniqueReleaser c4 = LoadIcon(&cache, &fake, "cherry", kHit);
+ c4.reset();
+ c0.reset();
+
+ if (gc_policy == apps::IconCache::GarbageCollectionPolicy::kExplicit) {
+ cache.SweepReleasedIcons();
+ }
+
+ UniqueReleaser c5 = LoadIcon(&cache, &fake, "cherry", kMiss);
+ c5.reset();
+ }
+
+ void TestPlaceholder(apps::IconCache::GarbageCollectionPolicy gc_policy) {
+ FakeIconLoader fake;
+ apps::IconCache cache(&fake, gc_policy);
+ bool allow_placeholder_icon;
+
+ fake.SetReturnPlaceholderIcons(true);
+
+ allow_placeholder_icon = true;
+ UniqueReleaser f0 =
+ LoadIcon(&cache, &fake, "fig", kMiss, allow_placeholder_icon);
+
+ fake.SetReturnPlaceholderIcons(false);
+
+ // The next LoadIcon call is a kMiss, even though there is a cache entry,
+ // because the cache entry holds a placeholder icon, but we have
+ // allow_placeholder_icon == false.
+ //
+ // A side effect of the next LoadIcon call is to prime the cache with the
+ // real (non-placeholder) icon.
+
+ allow_placeholder_icon = false;
+ UniqueReleaser f1 =
+ LoadIcon(&cache, &fake, "fig", kMiss, allow_placeholder_icon);
+
+ // The next two LoadIcons are all both kHit's. The real icon can be served
+ // from the cache, regardless of allow_placeholder_icon's value.
+
+ allow_placeholder_icon = false;
+ UniqueReleaser f2 =
+ LoadIcon(&cache, &fake, "fig", kHit, allow_placeholder_icon);
+
+ allow_placeholder_icon = true;
+ UniqueReleaser f3 =
+ LoadIcon(&cache, &fake, "fig", kHit, allow_placeholder_icon);
+ }
+
+ void TestAfterZeroRefcount(
+ apps::IconCache::GarbageCollectionPolicy gc_policy) {
+ FakeIconLoader fake;
+ apps::IconCache cache(&fake, gc_policy);
+
+ UniqueReleaser w0 = LoadIcon(&cache, &fake, "watermelon", kMiss);
+ w0.reset();
+
+ // We now have a zero ref-count. Whether the next LoadIcon call is kHit or
+ // kMiss depends on our gc_policy.
+
+ HitOrMiss expect_hom;
+ switch (gc_policy) {
+ case apps::IconCache::GarbageCollectionPolicy::kEager:
+ expect_hom = kMiss;
+ break;
+ case apps::IconCache::GarbageCollectionPolicy::kExplicit:
+ expect_hom = kHit;
+ break;
+ }
+
+ UniqueReleaser w1 = LoadIcon(&cache, &fake, "watermelon", expect_hom);
+ w1.reset();
+
+ // Once again, we have a zero ref-count, but for a kExplicit gc_policy, we
+ // also explicitly SweepReleasedIcons(), so the next LoadIcon call should
+ // get kMiss.
+
+ if (gc_policy == apps::IconCache::GarbageCollectionPolicy::kExplicit) {
+ cache.SweepReleasedIcons();
+ }
+
+ UniqueReleaser w2 = LoadIcon(&cache, &fake, "watermelon", kMiss);
+ w2.reset();
+ }
+};
+
+TEST_F(AppsIconCacheTest, Eager) {
+ static constexpr apps::IconCache::GarbageCollectionPolicy gc_policy =
+ apps::IconCache::GarbageCollectionPolicy::kEager;
+
+ TestBasics(gc_policy);
+ TestPlaceholder(gc_policy);
+ TestAfterZeroRefcount(gc_policy);
+}
+
+TEST_F(AppsIconCacheTest, Explicit) {
+ static constexpr apps::IconCache::GarbageCollectionPolicy gc_policy =
+ apps::IconCache::GarbageCollectionPolicy::kExplicit;
+
+ TestBasics(gc_policy);
+ TestPlaceholder(gc_policy);
+ TestAfterZeroRefcount(gc_policy);
+}
diff --git a/chromium/components/services/app_service/public/cpp/icon_coalescer.cc b/chromium/components/services/app_service/public/cpp/icon_coalescer.cc
new file mode 100644
index 00000000000..007bbcefa3e
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_coalescer.cc
@@ -0,0 +1,212 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/icon_coalescer.h"
+
+#include <iterator>
+#include <utility>
+#include <vector>
+
+#include "base/bind_helpers.h"
+#include "base/callback.h"
+
+namespace apps {
+
+// scoped_refptr<RefCountedReleaser> converts a
+// std::unique_ptr<IconLoader::Releaser> to a ref-counted pointer.
+class IconCoalescer::RefCountedReleaser
+ : public base::RefCounted<RefCountedReleaser> {
+ public:
+ explicit RefCountedReleaser(std::unique_ptr<IconLoader::Releaser> releaser);
+
+ private:
+ friend class base::RefCounted<RefCountedReleaser>;
+
+ virtual ~RefCountedReleaser();
+
+ std::unique_ptr<IconLoader::Releaser> releaser_;
+
+ DISALLOW_COPY_AND_ASSIGN(RefCountedReleaser);
+};
+
+IconCoalescer::RefCountedReleaser::RefCountedReleaser(
+ std::unique_ptr<IconLoader::Releaser> releaser)
+ : releaser_(std::move(releaser)) {}
+
+IconCoalescer::RefCountedReleaser::~RefCountedReleaser() = default;
+
+IconCoalescer::IconCoalescer(IconLoader* wrapped_loader)
+ : wrapped_loader_(wrapped_loader), next_sequence_number_(0) {}
+
+IconCoalescer::~IconCoalescer() = default;
+
+apps::mojom::IconKeyPtr IconCoalescer::GetIconKey(const std::string& app_id) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ return wrapped_loader_ ? wrapped_loader_->GetIconKey(app_id)
+ : apps::mojom::IconKey::New();
+}
+
+std::unique_ptr<IconLoader::Releaser> IconCoalescer::LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+ if (!wrapped_loader_) {
+ std::move(callback).Run(apps::mojom::IconValue::New());
+ return nullptr;
+ }
+
+ if (icon_compression != apps::mojom::IconCompression::kUncompressed) {
+ return wrapped_loader_->LoadIconFromIconKey(
+ app_type, app_id, std::move(icon_key), icon_compression,
+ size_hint_in_dip, allow_placeholder_icon, std::move(callback));
+ }
+
+ scoped_refptr<RefCountedReleaser> shared_releaser;
+ IconLoader::Key key(app_type, app_id, icon_key, icon_compression,
+ size_hint_in_dip, allow_placeholder_icon);
+
+ auto iter = non_immediate_requests_.find(key);
+ if (iter != non_immediate_requests_.end()) {
+ // Coalesce this request with an in-flight one.
+ //
+ // |iter->second| is a CallbackAndReleaser. |iter->second.second| is a
+ // scoped_refptr<RefCountedReleaser>.
+ shared_releaser = iter->second.second;
+ } else {
+ // There is no in-flight request to coalesce with. Instead, forward on the
+ // request to the wrapped IconLoader.
+ //
+ // Calling the |wrapped_loader_|'s LoadIconFromIconKey implementation might
+ // invoke the passed OnceCallback (binding this class' OnLoadIcon method)
+ // immediately (now), or at a later time. In both cases, we have to invoke
+ // (now or later) the |callback| that was passed to this function.
+ //
+ // If it's later, then we stash |callback| in |non_immediate_requests_|,
+ // and look up that same |non_immediate_requests_| during OnLoadIcon.
+ //
+ // If it's now, then inserting into the |non_immediate_requests_| would be
+ // tricky, as we'd have to then unstash the |callback| out of the
+ // |non_immediate_requests_| (recall that a OnceCallback can be std::move'd
+ // but not copied), but there are potentially multiple entries with the
+ // same key, and any multimap iterator might be invalidated if calling into
+ // the |wrapped_loader_| caused other code to call back into this
+ // IconCoalescer and mutate that multimap.
+ //
+ // Instead, |possibly_immediate_requests_| and |immediate_responses_| keeps
+ // track of now vs later.
+ //
+ // If it's now (if OnLoadIcon is called when the current |seq_num| is in
+ // |possibly_immediate_requests_|), then OnLoadIcon will populate
+ // |immediate_responses_| with that |seq_num|. We then run |callback| now,
+ // right after |wrapped_loader_->LoadIconFromIconKey| returns.
+ //
+ // Otherwise we have asynchronously dispatched the underlying icon loading
+ // request, so store |callback| in |non_immediate_requests_| to be run
+ // later, when the asynchronous request resolves.
+ uint64_t seq_num = next_sequence_number_++;
+ possibly_immediate_requests_.insert(seq_num);
+
+ std::unique_ptr<IconLoader::Releaser> unique_releaser =
+ wrapped_loader_->LoadIconFromIconKey(
+ app_type, app_id, std::move(icon_key), icon_compression,
+ size_hint_in_dip, allow_placeholder_icon,
+ base::BindOnce(&IconCoalescer::OnLoadIcon,
+ weak_ptr_factory_.GetWeakPtr(), key, seq_num));
+
+ possibly_immediate_requests_.erase(seq_num);
+
+ auto iv_iter = immediate_responses_.find(seq_num);
+ if (iv_iter != immediate_responses_.end()) {
+ apps::mojom::IconValuePtr iv = std::move(iv_iter->second);
+ immediate_responses_.erase(iv_iter);
+ std::move(callback).Run(std::move(iv));
+ return unique_releaser;
+ }
+
+ shared_releaser =
+ base::MakeRefCounted<RefCountedReleaser>(std::move(unique_releaser));
+ }
+
+ non_immediate_requests_.insert(std::make_pair(
+ key, std::make_pair(std::move(callback), shared_releaser)));
+
+ return std::make_unique<IconLoader::Releaser>(
+ nullptr,
+ // The DoNothing callback does nothiing explicitly, but after it runs, it
+ // implicitly decrements the scoped_refptr's shared reference count, and
+ // therefore possibly deletes the underlying IconLoader::Releaser.
+ base::BindOnce(base::DoNothing::Once<scoped_refptr<RefCountedReleaser>>(),
+ std::move(shared_releaser)));
+}
+
+void IconCoalescer::OnLoadIcon(IconLoader::Key key,
+ uint64_t sequence_number,
+ apps::mojom::IconValuePtr icon_value) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+ if (possibly_immediate_requests_.find(sequence_number) !=
+ possibly_immediate_requests_.end()) {
+ immediate_responses_.insert(
+ std::make_pair(sequence_number, std::move(icon_value)));
+ return;
+ }
+
+ auto range = non_immediate_requests_.equal_range(key);
+ auto count = std::distance(range.first, range.second);
+ if (count <= 0) {
+ NOTREACHED();
+ return;
+ }
+
+ // Optimize / simplify the common case.
+ if (count == 1) {
+ CallbackAndReleaser callback_and_releaser = std::move(range.first->second);
+ non_immediate_requests_.erase(range.first, range.second);
+ std::move(callback_and_releaser.first).Run(std::move(icon_value));
+ return;
+ }
+
+ // Run every callback in |range|. This is subtle, because an arbitrary
+ // callback could invoke further methods on |this|, which could mutate
+ // |non_immediate_requests_|, invalidating |range|'s iterators.
+ //
+ // Thus, we first gather the callbacks, then erase the |range|, then run the
+ // callbacks.
+ //
+ // We still run the callbacks, synchronously, instead of posting them on a
+ // task runner to run later, asynchronously, even though using a task runner
+ // could avoid having to separate gathering and running the callbacks.
+ // Synchronous invocation keep the call stack's "how did I get here"
+ // information, which is useful when debugging.
+
+ std::vector<apps::mojom::Publisher::LoadIconCallback> callbacks;
+ callbacks.reserve(count);
+ for (auto iter = range.first; iter != range.second; ++iter) {
+ // |iter->second| is a CallbackAndReleaser. |iter->second.first| is a
+ // LoadIconCallback.
+ callbacks.push_back(std::move(iter->second.first));
+ }
+
+ non_immediate_requests_.erase(range.first, range.second);
+
+ for (auto& callback : callbacks) {
+ apps::mojom::IconValuePtr iv;
+ if (--count == 0) {
+ iv = std::move(icon_value);
+ } else {
+ iv = apps::mojom::IconValue::New();
+ iv->icon_compression = apps::mojom::IconCompression::kUncompressed;
+ iv->uncompressed = icon_value->uncompressed;
+ iv->is_placeholder_icon = icon_value->is_placeholder_icon;
+ }
+ std::move(callback).Run(std::move(iv));
+ }
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/icon_coalescer.h b/chromium/components/services/app_service/public/cpp/icon_coalescer.h
new file mode 100644
index 00000000000..c7dfebd59af
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_coalescer.h
@@ -0,0 +1,102 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_COALESCER_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_COALESCER_H_
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
+#include "base/sequence_checker.h"
+#include "components/services/app_service/public/cpp/icon_loader.h"
+
+namespace apps {
+
+// An IconLoader that coalesces the apps::mojom::IconCompression::kUncompressed
+// results of another (wrapped) IconLoader.
+//
+// This is similar to, but different from, an IconCache. Both types are related
+// to the LoadIconFromIconKey Mojo call (the request and response), both reduce
+// the number of requests made, and both re-use the response for requests with
+// the same IconLoader::Key.
+//
+// An IconCache (another class) applies when the second request is sent *after*
+// the first response is received. An IconCoalescer (this class) applies when
+// the second request is sent *before* the first response is received (but
+// after the first request is sent, obviously).
+//
+// Caching means that the second (and subsequent) requests can be satisfied
+// immediately, sharing the previous response. Coalescing means that the second
+// (and subsequent) requests are paused, and when the first request's response
+// is finally received, those other requests are un-paused and share the same
+// response.
+//
+// When there are no in-flight requests, a (memory-backed) cache can still have
+// a significant memory cost, depending on how aggressive its cache eviction
+// policy is, but a (memory-backed) coalescer will have a trivial memory cost.
+// Much of its internal state (e.g. maps and multimaps) will be empty.
+class IconCoalescer : public IconLoader {
+ public:
+ explicit IconCoalescer(IconLoader* wrapped_loader);
+ ~IconCoalescer() override;
+
+ // IconLoader overrides.
+ apps::mojom::IconKeyPtr GetIconKey(const std::string& app_id) override;
+ std::unique_ptr<IconLoader::Releaser> LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) override;
+
+ private:
+ class RefCountedReleaser;
+
+ using CallbackAndReleaser =
+ std::pair<apps::mojom::Publisher::LoadIconCallback,
+ scoped_refptr<RefCountedReleaser>>;
+
+ void OnLoadIcon(IconLoader::Key,
+ uint64_t sequence_number,
+ apps::mojom::IconValuePtr);
+
+ IconLoader* wrapped_loader_;
+
+ // Every incoming LoadIconFromIconKey call gets its own sequence number.
+ uint64_t next_sequence_number_;
+
+ // Sequence numbers for outstanding requests to to the wrapped_loader_'s
+ // LoadIconFromIconKey. When the wrapped_loader_ returns, there will either
+ // be a matching entry in immediate_responses_ or an entry will be made in
+ // non_immediate_requests_, depending on whether LoadIconFromIconKey resolved
+ // (ran its callback) synchronously (immediately) or asynchronously
+ // (non-immediately).
+ std::set<uint64_t> possibly_immediate_requests_;
+
+ // Map from sequence number to the IconValue to give to the LoadIconCallback.
+ std::map<uint64_t, apps::mojom::IconValuePtr> immediate_responses_;
+
+ // Multimap of pending LoadIconFromIconKey calls: those calls that were not
+ // resolved immediately.
+ std::multimap<IconLoader::Key, CallbackAndReleaser> non_immediate_requests_;
+
+ SEQUENCE_CHECKER(sequence_checker_);
+
+ base::WeakPtrFactory<IconCoalescer> weak_ptr_factory_{this};
+
+ DISALLOW_COPY_AND_ASSIGN(IconCoalescer);
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_COALESCER_H_
diff --git a/chromium/components/services/app_service/public/cpp/icon_coalescer_unittest.cc b/chromium/components/services/app_service/public/cpp/icon_coalescer_unittest.cc
new file mode 100644
index 00000000000..76e4dddb7dd
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_coalescer_unittest.cc
@@ -0,0 +1,341 @@
+// Copyright 2019 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 <map>
+
+#include "base/callback.h"
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "components/services/app_service/public/cpp/icon_coalescer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class AppsIconCoalescerTest : public testing::Test {
+ protected:
+ using UniqueReleaser = std::unique_ptr<apps::IconLoader::Releaser>;
+
+ // Increment is a LoadIconCallback that increments a counter (and ignores the
+ // IconValuePtr).
+ static void Increment(int* counter,
+ int delta,
+ apps::mojom::IconValuePtr icon_value) {
+ *counter += delta;
+ }
+
+ class FakeIconLoader : public apps::IconLoader {
+ public:
+ int NumLoadIconFromIconKeyCalls() { return num_load_calls_; }
+
+ int NumLoadIconFromIconKeyCallsComplete() {
+ return num_load_calls_ - pending_callbacks_.size();
+ }
+
+ int NumLoadIconFromIconKeyCallsPending() {
+ return pending_callbacks_.size();
+ }
+
+ int NumPendingReleases() { return num_pending_releases_; }
+
+ void SetCallBackImmediately(bool b) { call_back_immediately_ = b; }
+
+ void CallBack(const std::string& app_id) {
+ auto iter = pending_callbacks_.find(app_id);
+ if (iter != pending_callbacks_.end()) {
+ std::move(iter->second).Run(NewIconValuePtr());
+ pending_callbacks_.erase(iter);
+ } else {
+ NOTREACHED() << "No pending callback for app_id=" << app_id;
+ }
+ }
+
+ private:
+ apps::mojom::IconKeyPtr GetIconKey(const std::string& app_id) override {
+ return apps::mojom::IconKey::New(0, 0, 0);
+ }
+
+ std::unique_ptr<Releaser> LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) override {
+ num_load_calls_++;
+ if (call_back_immediately_) {
+ num_load_calls_complete_++;
+ std::move(callback).Run(NewIconValuePtr());
+ } else {
+ pending_callbacks_.insert(std::make_pair(app_id, std::move(callback)));
+ }
+ num_pending_releases_++;
+ return std::make_unique<IconLoader::Releaser>(
+ nullptr, base::BindOnce(&FakeIconLoader::OnRelease,
+ weak_ptr_factory_.GetWeakPtr()));
+ }
+
+ apps::mojom::IconValuePtr NewIconValuePtr() {
+ auto iv = apps::mojom::IconValue::New();
+ iv->icon_compression = apps::mojom::IconCompression::kUncompressed;
+ iv->uncompressed =
+ gfx::ImageSkia(gfx::ImageSkiaRep(gfx::Size(1, 1), 1.0f));
+ iv->is_placeholder_icon = false;
+ return iv;
+ }
+
+ void OnRelease() { num_pending_releases_--; }
+
+ bool call_back_immediately_ = false;
+ int num_load_calls_ = 0;
+ int num_load_calls_complete_ = 0;
+ int num_pending_releases_ = 0;
+ std::multimap<std::string, apps::mojom::Publisher::LoadIconCallback>
+ pending_callbacks_;
+
+ base::WeakPtrFactory<FakeIconLoader> weak_ptr_factory_{this};
+ };
+
+ UniqueReleaser LoadIcon(apps::IconLoader* loader,
+ const std::string& app_id,
+ int* counter,
+ int delta) {
+ static constexpr auto app_type = apps::mojom::AppType::kWeb;
+ static constexpr auto icon_compression =
+ apps::mojom::IconCompression::kUncompressed;
+ static constexpr int32_t size_hint_in_dip = 1;
+ static constexpr bool allow_placeholder_icon = false;
+
+ return loader->LoadIcon(
+ app_type, app_id, icon_compression, size_hint_in_dip,
+ allow_placeholder_icon,
+ base::BindOnce(&AppsIconCoalescerTest::Increment, counter, delta));
+ }
+};
+
+TEST_F(AppsIconCoalescerTest, CallBackImmediately) {
+ FakeIconLoader fake;
+ fake.SetCallBackImmediately(true);
+ apps::IconCoalescer coalescer(&fake);
+ int counter = 0;
+
+ UniqueReleaser releaser = LoadIcon(&coalescer, "the_app_id", &counter, 1000);
+
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(1000, counter);
+ EXPECT_EQ(1, fake.NumPendingReleases());
+
+ releaser.reset();
+
+ EXPECT_EQ(0, fake.NumPendingReleases());
+}
+
+TEST_F(AppsIconCoalescerTest, CallBackDelayedAndAfterRelease) {
+ FakeIconLoader fake;
+ apps::IconCoalescer coalescer(&fake);
+ int counter = 0;
+
+ UniqueReleaser releaser = LoadIcon(&coalescer, "the_app_id", &counter, 1000);
+
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(0, counter);
+ EXPECT_EQ(1, fake.NumPendingReleases());
+
+ fake.CallBack("the_app_id");
+
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(1000, counter);
+ EXPECT_EQ(1, fake.NumPendingReleases());
+
+ releaser.reset();
+
+ EXPECT_EQ(0, fake.NumPendingReleases());
+}
+
+TEST_F(AppsIconCoalescerTest, CallBackDelayedAndBeforeRelease) {
+ FakeIconLoader fake;
+ apps::IconCoalescer coalescer(&fake);
+ int counter = 0;
+
+ UniqueReleaser releaser = LoadIcon(&coalescer, "the_app_id", &counter, 1000);
+
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(0, counter);
+ EXPECT_EQ(1, fake.NumPendingReleases());
+
+ // Even though we release our claim on the outer IconLoader::Releaser (from
+ // the IconCoalescer), the inner IconLoader::Releaser (from the
+ // FakeIconLoader) isn't released while the callback's still pending.
+ releaser.reset();
+
+ EXPECT_EQ(1, fake.NumPendingReleases());
+
+ fake.CallBack("the_app_id");
+
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(1000, counter);
+ EXPECT_EQ(0, fake.NumPendingReleases());
+}
+
+TEST_F(AppsIconCoalescerTest, MultipleAppIDs) {
+ FakeIconLoader fake;
+ apps::IconCoalescer coalescer(&fake);
+ int ant_counter = 0;
+ int bat_counter = 0;
+ int cat_counter = 0;
+ int dog_counter = 0;
+ int emu_counter = 0;
+
+ UniqueReleaser a1 = LoadIcon(&coalescer, "ant", &ant_counter, 10);
+ UniqueReleaser b1 = LoadIcon(&coalescer, "bat", &bat_counter, 100);
+ UniqueReleaser c1 = LoadIcon(&coalescer, "cat", &cat_counter, 1000);
+
+ EXPECT_EQ(3, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(3, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(0, ant_counter);
+ EXPECT_EQ(0, bat_counter);
+ EXPECT_EQ(0, cat_counter);
+ EXPECT_EQ(0, dog_counter);
+ EXPECT_EQ(0, emu_counter);
+
+ UniqueReleaser c2 = LoadIcon(&coalescer, "cat", &cat_counter, 2000);
+ UniqueReleaser d1 = LoadIcon(&coalescer, "dog", &dog_counter, 10000);
+ UniqueReleaser c4 = LoadIcon(&coalescer, "cat", &cat_counter, 4000);
+ UniqueReleaser b2 = LoadIcon(&coalescer, "bat", &bat_counter, 200);
+
+ EXPECT_EQ(4, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(4, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(0, ant_counter);
+ EXPECT_EQ(0, bat_counter);
+ EXPECT_EQ(0, cat_counter);
+ EXPECT_EQ(0, dog_counter);
+ EXPECT_EQ(0, emu_counter);
+
+ fake.CallBack("ant");
+ fake.CallBack("cat");
+
+ EXPECT_EQ(4, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(2, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(2, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(10, ant_counter);
+ EXPECT_EQ(0, bat_counter);
+ EXPECT_EQ(7000, cat_counter);
+ EXPECT_EQ(0, dog_counter);
+ EXPECT_EQ(0, emu_counter);
+
+ UniqueReleaser a4 = LoadIcon(&coalescer, "ant", &ant_counter, 40);
+ UniqueReleaser b4 = LoadIcon(&coalescer, "bat", &bat_counter, 400);
+ UniqueReleaser a2 = LoadIcon(&coalescer, "ant", &ant_counter, 20);
+
+ EXPECT_EQ(5, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(2, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(3, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(10, ant_counter);
+ EXPECT_EQ(0, bat_counter);
+ EXPECT_EQ(7000, cat_counter);
+ EXPECT_EQ(0, dog_counter);
+ EXPECT_EQ(0, emu_counter);
+
+ // 5 NumLoadIconFromIconKeyCalls, without any releases, means 5
+ // NumPendingReleases: {a1}, {a2, a4}, {b*}, {c*} and {d*}. The "a"s are two
+ // different groups, as they are separated by a `fake.Callback("ant")` line.
+ EXPECT_EQ(5, fake.NumPendingReleases());
+ fake.CallBack("ant");
+
+ // We treat the "b"s differently, releasing them (resetting the
+ // UniqueReleaser, aka unique_ptr<IconLoader::Releaser>) *before* (not
+ // *after) we tickle fake.CallBack. Still, the inner-most releaser isn't let
+ // go until both (1) all outer releasers are dropped and (2) the inner
+ // IconLoader has actually called back.
+ EXPECT_EQ(5, fake.NumPendingReleases());
+ b1.reset();
+ b2.reset();
+ b4.reset();
+ EXPECT_EQ(5, fake.NumPendingReleases());
+ fake.CallBack("bat");
+ EXPECT_EQ(4, fake.NumPendingReleases());
+
+ EXPECT_EQ(5, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(4, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(70, ant_counter);
+ EXPECT_EQ(700, bat_counter);
+ EXPECT_EQ(7000, cat_counter);
+ EXPECT_EQ(0, dog_counter);
+ EXPECT_EQ(0, emu_counter);
+
+ // Even though we configure the fake to call back immediately, the next two
+ // "dog" calls still wait for the previous (pending) "dog" call. The next
+ // three "emu" calls lead to three (immediate) calls on the fake.
+ fake.SetCallBackImmediately(true);
+ EXPECT_EQ(4, fake.NumPendingReleases());
+ UniqueReleaser d2 = LoadIcon(&coalescer, "dog", &dog_counter, 20000);
+ UniqueReleaser d4 = LoadIcon(&coalescer, "dog", &dog_counter, 40000);
+ EXPECT_EQ(4, fake.NumPendingReleases());
+ UniqueReleaser e1 = LoadIcon(&coalescer, "emu", &emu_counter, 100000);
+ UniqueReleaser e2 = LoadIcon(&coalescer, "emu", &emu_counter, 200000);
+ UniqueReleaser e4 = LoadIcon(&coalescer, "emu", &emu_counter, 400000);
+ EXPECT_EQ(7, fake.NumPendingReleases());
+
+ EXPECT_EQ(8, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(7, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(1, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(70, ant_counter);
+ EXPECT_EQ(700, bat_counter);
+ EXPECT_EQ(7000, cat_counter);
+ EXPECT_EQ(0, dog_counter);
+ EXPECT_EQ(700000, emu_counter);
+
+ fake.CallBack("dog");
+
+ EXPECT_EQ(8, fake.NumLoadIconFromIconKeyCalls());
+ EXPECT_EQ(8, fake.NumLoadIconFromIconKeyCallsComplete());
+ EXPECT_EQ(0, fake.NumLoadIconFromIconKeyCallsPending());
+ EXPECT_EQ(70, ant_counter);
+ EXPECT_EQ(700, bat_counter);
+ EXPECT_EQ(7000, cat_counter);
+ EXPECT_EQ(70000, dog_counter);
+ EXPECT_EQ(700000, emu_counter);
+
+ // As mentioned above, {a1} and {a2, a4} are different groups.
+ EXPECT_EQ(7, fake.NumPendingReleases());
+ a1.reset();
+ EXPECT_EQ(6, fake.NumPendingReleases());
+ a2.reset();
+ EXPECT_EQ(6, fake.NumPendingReleases());
+ a4.reset();
+ EXPECT_EQ(5, fake.NumPendingReleases());
+
+ // {c*} and {d*} are each one group, but {e1}, {e2} and {e4} are three
+ // separate groups.
+ EXPECT_EQ(5, fake.NumPendingReleases());
+ c1.reset();
+ EXPECT_EQ(5, fake.NumPendingReleases());
+ c2.reset();
+ EXPECT_EQ(5, fake.NumPendingReleases());
+ c4.reset();
+ EXPECT_EQ(4, fake.NumPendingReleases());
+ d1.reset();
+ EXPECT_EQ(4, fake.NumPendingReleases());
+ d2.reset();
+ EXPECT_EQ(4, fake.NumPendingReleases());
+ d4.reset();
+ EXPECT_EQ(3, fake.NumPendingReleases());
+ e1.reset();
+ EXPECT_EQ(2, fake.NumPendingReleases());
+ e2.reset();
+ EXPECT_EQ(1, fake.NumPendingReleases());
+ e4.reset();
+ EXPECT_EQ(0, fake.NumPendingReleases());
+}
diff --git a/chromium/components/services/app_service/public/cpp/icon_loader.cc b/chromium/components/services/app_service/public/cpp/icon_loader.cc
new file mode 100644
index 00000000000..25c57587bcd
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_loader.cc
@@ -0,0 +1,79 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/icon_loader.h"
+
+#include <utility>
+
+#include "base/callback.h"
+
+namespace apps {
+
+IconLoader::Releaser::Releaser(std::unique_ptr<IconLoader::Releaser> next,
+ base::OnceClosure closure)
+ : next_(std::move(next)), closure_(std::move(closure)) {}
+
+IconLoader::Releaser::~Releaser() {
+ std::move(closure_).Run();
+}
+
+IconLoader::Key::Key(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ const apps::mojom::IconKeyPtr& icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon)
+ : app_type_(app_type),
+ app_id_(app_id),
+ timeline_(icon_key ? icon_key->timeline : 0),
+ resource_id_(icon_key ? icon_key->resource_id : 0),
+ icon_effects_(icon_key ? icon_key->icon_effects : 0),
+ icon_compression_(icon_compression),
+ size_hint_in_dip_(size_hint_in_dip),
+ allow_placeholder_icon_(allow_placeholder_icon) {}
+
+IconLoader::Key::Key(const Key& other) = default;
+
+bool IconLoader::Key::operator<(const Key& that) const {
+ if (this->app_type_ != that.app_type_) {
+ return this->app_type_ < that.app_type_;
+ }
+ if (this->timeline_ != that.timeline_) {
+ return this->timeline_ < that.timeline_;
+ }
+ if (this->resource_id_ != that.resource_id_) {
+ return this->resource_id_ < that.resource_id_;
+ }
+ if (this->icon_effects_ != that.icon_effects_) {
+ return this->icon_effects_ < that.icon_effects_;
+ }
+ if (this->icon_compression_ != that.icon_compression_) {
+ return this->icon_compression_ < that.icon_compression_;
+ }
+ if (this->size_hint_in_dip_ != that.size_hint_in_dip_) {
+ return this->size_hint_in_dip_ < that.size_hint_in_dip_;
+ }
+ if (this->allow_placeholder_icon_ != that.allow_placeholder_icon_) {
+ return this->allow_placeholder_icon_ < that.allow_placeholder_icon_;
+ }
+ return this->app_id_ < that.app_id_;
+}
+
+IconLoader::IconLoader() = default;
+
+IconLoader::~IconLoader() = default;
+
+std::unique_ptr<IconLoader::Releaser> IconLoader::LoadIcon(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) {
+ return LoadIconFromIconKey(app_type, app_id, GetIconKey(app_id),
+ icon_compression, size_hint_in_dip,
+ allow_placeholder_icon, std::move(callback));
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/icon_loader.h b/chromium/components/services/app_service/public/cpp/icon_loader.h
new file mode 100644
index 00000000000..7470d2fcbe3
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/icon_loader.h
@@ -0,0 +1,111 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_LOADER_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_LOADER_H_
+
+#include <memory>
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/macros.h"
+#include "components/services/app_service/public/mojom/app_service.mojom.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+
+namespace apps {
+
+// An abstract class for something that can load App Service icons, either
+// directly or by wrapping another IconLoader.
+class IconLoader {
+ public:
+ // An RAII-style object that, when destroyed, runs |closure|.
+ //
+ // For example, that |closure| can inform an IconLoader that an icon is no
+ // longer actively used by whoever held this Releaser (an object returned by
+ // IconLoader::LoadIconFromIconKey). This is merely advisory: the IconLoader
+ // is free to ignore the Releaser-was-destroyed hint and to e.g. keep any
+ // cache entries alive for a longer or shorter time.
+ //
+ // These can be chained, so that |this| is the head of a linked list of
+ // Releaser's. Destroying the head will destroy the rest of the list.
+ //
+ // Destruction must happen on the same sequence (in the
+ // base/sequence_checker.h sense) as the LoadIcon or LoadIconFromIconKey call
+ // that returned |this|.
+ class Releaser {
+ public:
+ Releaser(std::unique_ptr<Releaser> next, base::OnceClosure closure);
+ virtual ~Releaser();
+
+ private:
+ std::unique_ptr<Releaser> next_;
+ base::OnceClosure closure_;
+
+ DISALLOW_COPY_AND_ASSIGN(Releaser);
+ };
+
+ IconLoader();
+ virtual ~IconLoader();
+
+ // Looks up the IconKey for the given app ID.
+ virtual apps::mojom::IconKeyPtr GetIconKey(const std::string& app_id) = 0;
+
+ // This can return nullptr, meaning that the IconLoader does not track when
+ // the icon is no longer actively used by the caller.
+ virtual std::unique_ptr<Releaser> LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) = 0;
+
+ // Convenience method that calls "LoadIconFromIconKey(app_type, app_id,
+ // GetIconKey(app_id), etc)".
+ std::unique_ptr<Releaser> LoadIcon(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback);
+
+ protected:
+ // A struct containing the arguments (other than the callback) to
+ // Loader::LoadIconFromIconKey, including a flattened apps::mojom::IconKey.
+ //
+ // It implements operator<, so that it can be the "K" in a "map<K, V>".
+ //
+ // Only IconLoader subclasses (i.e. implementations), not IconLoader's
+ // callers, are expected to refer to a Key.
+ class Key {
+ public:
+ apps::mojom::AppType app_type_;
+ std::string app_id_;
+ // apps::mojom::IconKey fields.
+ uint64_t timeline_;
+ int32_t resource_id_;
+ uint32_t icon_effects_;
+ // Other fields.
+ apps::mojom::IconCompression icon_compression_;
+ int32_t size_hint_in_dip_;
+ bool allow_placeholder_icon_;
+
+ Key(apps::mojom::AppType app_type,
+ const std::string& app_id,
+ const apps::mojom::IconKeyPtr& icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon);
+
+ Key(const Key& other);
+
+ bool operator<(const Key& that) const;
+ };
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_ICON_LOADER_H_
diff --git a/chromium/components/services/app_service/public/cpp/instance.cc b/chromium/components/services/app_service/public/cpp/instance.cc
new file mode 100644
index 00000000000..0eba9b740c7
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance.cc
@@ -0,0 +1,36 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/instance.h"
+
+#include <memory>
+
+namespace apps {
+
+Instance::Instance(const std::string& app_id, aura::Window* window)
+ : app_id_(app_id), window_(window) {
+ state_ = InstanceState::kUnknown;
+}
+
+Instance::~Instance() = default;
+
+std::unique_ptr<Instance> Instance::Clone() {
+ auto instance = std::make_unique<Instance>(this->AppId(), this->Window());
+ instance->SetLaunchId(this->LaunchId());
+ instance->UpdateState(this->State(), this->LastUpdatedTime());
+ instance->SetBrowserContext(this->BrowserContext());
+ return instance;
+}
+
+void Instance::UpdateState(InstanceState state,
+ const base::Time& last_updated_time) {
+ state_ = state;
+ last_updated_time_ = last_updated_time;
+}
+
+void Instance::SetBrowserContext(content::BrowserContext* browser_context) {
+ browser_context_ = browser_context;
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/instance.h b/chromium/components/services/app_service/public/cpp/instance.h
new file mode 100644
index 00000000000..45c1a5cfe31
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance.h
@@ -0,0 +1,65 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_H_
+
+#include <memory>
+#include <string>
+
+#include "base/time/time.h"
+#include "content/public/browser/browser_context.h"
+#include "ui/aura/window.h"
+
+namespace apps {
+
+enum InstanceState {
+ kUnknown = 0,
+ kStarted = 0x01,
+ kRunning = 0x02,
+ kActive = 0x04,
+ kVisible = 0x08,
+ kHidden = 0x10,
+ kDestroyed = 0x80,
+};
+
+// Instance is used to represent an App Instance, or a running app.
+class Instance {
+ public:
+ Instance(const std::string& app_id, aura::Window* window);
+ ~Instance();
+
+ Instance(const Instance&) = delete;
+ Instance& operator=(const Instance&) = delete;
+
+ std::unique_ptr<Instance> Clone();
+
+ void SetLaunchId(const std::string& launch_id) { launch_id_ = launch_id; }
+ void UpdateState(InstanceState state, const base::Time& last_updated_time);
+ void SetBrowserContext(content::BrowserContext* browser_context);
+
+ const std::string& AppId() const { return app_id_; }
+ aura::Window* Window() const { return window_; }
+ const std::string& LaunchId() const { return launch_id_; }
+ InstanceState State() const { return state_; }
+ const base::Time& LastUpdatedTime() const { return last_updated_time_; }
+ content::BrowserContext* BrowserContext() const { return browser_context_; }
+
+ private:
+ std::string app_id_;
+
+ // window_ is owned by ash and will be deleted when the user closes the
+ // window. Instance itself doesn't observe the window. The window's observer
+ // is responsible to delete Instance from InstanceRegistry when the window is
+ // destroyed.
+ aura::Window* window_;
+ std::string launch_id_;
+ InstanceState state_;
+ base::Time last_updated_time_;
+ content::BrowserContext* browser_context_ = nullptr;
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_H_
diff --git a/chromium/components/services/app_service/public/cpp/instance_registry.cc b/chromium/components/services/app_service/public/cpp/instance_registry.cc
new file mode 100644
index 00000000000..f87015c3743
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance_registry.cc
@@ -0,0 +1,154 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/instance_registry.h"
+
+#include <memory>
+#include <utility>
+
+#include "components/services/app_service/public/cpp/instance.h"
+#include "components/services/app_service/public/cpp/instance_update.h"
+
+namespace apps {
+
+InstanceRegistry::Observer::Observer(InstanceRegistry* instance_registry) {
+ Observe(instance_registry);
+}
+
+InstanceRegistry::Observer::Observer() = default;
+InstanceRegistry::Observer::~Observer() {
+ if (instance_registry_) {
+ instance_registry_->RemoveObserver(this);
+ }
+}
+
+void InstanceRegistry::Observer::Observe(InstanceRegistry* instance_registry) {
+ if (instance_registry == instance_registry_) {
+ return;
+ }
+
+ if (instance_registry_) {
+ instance_registry_->RemoveObserver(this);
+ }
+
+ instance_registry_ = instance_registry;
+ if (instance_registry_) {
+ instance_registry_->AddObserver(this);
+ }
+}
+
+InstanceRegistry::InstanceRegistry() = default;
+
+InstanceRegistry::~InstanceRegistry() {
+ for (auto& obs : observers_) {
+ obs.OnInstanceRegistryWillBeDestroyed(this);
+ }
+ DCHECK(!observers_.might_have_observers());
+}
+
+void InstanceRegistry::AddObserver(Observer* observer) {
+ observers_.AddObserver(observer);
+}
+
+void InstanceRegistry::RemoveObserver(Observer* observer) {
+ observers_.RemoveObserver(observer);
+}
+
+void InstanceRegistry::OnInstances(const Instances& deltas) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+ for (auto& delta : deltas) {
+ // If the window state is not kDestroyed, adds to |app_id_to_app_window_|,
+ // otherwise removes the window from |app_id_to_app_window_|.
+ if (static_cast<InstanceState>(delta.get()->State() &
+ InstanceState::kDestroyed) ==
+ InstanceState::kUnknown) {
+ app_id_to_app_windows_[delta.get()->AppId()].insert(
+ delta.get()->Window());
+ } else {
+ app_id_to_app_windows_[delta.get()->AppId()].erase(delta.get()->Window());
+ if (app_id_to_app_windows_[delta.get()->AppId()].size() == 0) {
+ app_id_to_app_windows_.erase(delta.get()->AppId());
+ }
+ }
+ }
+
+ if (in_progress_) {
+ for (auto& delta : deltas) {
+ deltas_pending_.push_back(delta.get()->Clone());
+ }
+ return;
+ }
+ DoOnInstances(std::move(deltas));
+ while (!deltas_pending_.empty()) {
+ Instances pending;
+ pending.swap(deltas_pending_);
+ DoOnInstances(std::move(pending));
+ }
+}
+
+std::set<aura::Window*> InstanceRegistry::GetWindows(
+ const std::string& app_id) {
+ auto it = app_id_to_app_windows_.find(app_id);
+ if (it != app_id_to_app_windows_.end()) {
+ return it->second;
+ }
+ return std::set<aura::Window*>{};
+}
+
+InstanceState InstanceRegistry::GetState(aura::Window* window) const {
+ auto s_iter = states_.find(window);
+ return (s_iter != states_.end()) ? s_iter->second.get()->State()
+ : InstanceState::kUnknown;
+}
+
+ash::ShelfID InstanceRegistry::GetShelfId(aura::Window* window) const {
+ auto s_iter = states_.find(window);
+ return (s_iter != states_.end())
+ ? ash::ShelfID(s_iter->second.get()->AppId(),
+ s_iter->second.get()->LaunchId())
+ : ash::ShelfID();
+}
+
+bool InstanceRegistry::Exists(aura::Window* window) const {
+ return states_.find(window) != states_.end();
+}
+
+void InstanceRegistry::DoOnInstances(const Instances& deltas) {
+ in_progress_ = true;
+
+ // The remaining for loops range over the deltas vector, so that
+ // OninstanceUpdate is called for each updates, and notify the observers for
+ // every de-duplicated delta. Also update the states for every delta.
+ for (const auto& d_iter : deltas) {
+ auto s_iter = states_.find(d_iter->Window());
+ Instance* state =
+ (s_iter != states_.end()) ? s_iter->second.get() : nullptr;
+ if (InstanceUpdate::Equals(state, d_iter.get())) {
+ continue;
+ }
+
+ std::unique_ptr<Instance> old_state = nullptr;
+ if (state) {
+ old_state = state->Clone();
+ InstanceUpdate::Merge(state, d_iter.get());
+ } else {
+ states_.insert(
+ std::make_pair(d_iter.get()->Window(), (d_iter.get()->Clone())));
+ }
+
+ for (auto& obs : observers_) {
+ obs.OnInstanceUpdate(InstanceUpdate(old_state.get(), d_iter.get()));
+ }
+
+ if (static_cast<InstanceState>(d_iter.get()->State() &
+ InstanceState::kDestroyed) !=
+ InstanceState::kUnknown) {
+ states_.erase(d_iter.get()->Window());
+ }
+ }
+ in_progress_ = false;
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/instance_registry.h b/chromium/components/services/app_service/public/cpp/instance_registry.h
new file mode 100644
index 00000000000..de60310d627
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance_registry.h
@@ -0,0 +1,181 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_REGISTRY_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_REGISTRY_H_
+
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "ash/public/cpp/shelf_types.h"
+#include "base/observer_list.h"
+#include "base/observer_list_types.h"
+#include "base/sequence_checker.h"
+#include "components/services/app_service/public/cpp/instance.h"
+#include "components/services/app_service/public/cpp/instance_update.h"
+
+namespace apps {
+
+// InstanceRegistry keeps all of the Instances seen by AppServiceProxy.
+// It also keeps the "sum" of those previous deltas, so that observers of this
+// object can be updated with the InstanceUpdate structure. It can also be
+// queried synchronously.
+//
+// This class is not thread-safe.
+class InstanceRegistry {
+ public:
+ class Observer : public base::CheckedObserver {
+ public:
+ Observer(const Observer&) = delete;
+ Observer& operator=(const Observer&) = delete;
+
+ // The InstanceUpdate argument shouldn't be accessed after OnInstanceUpdate
+ // returns.
+ virtual void OnInstanceUpdate(const InstanceUpdate& update) = 0;
+
+ // Called when the InstanceRegistry object (the thing that this observer
+ // observes) will be destroyed. In response, the observer, |this|, should
+ // call "instance_registry->RemoveObserver(this)", whether directly or
+ // indirectly (e.g. via ScopedObserver::Remove or via Observe(nullptr)).
+ virtual void OnInstanceRegistryWillBeDestroyed(InstanceRegistry* cache) = 0;
+
+ protected:
+ // Use this constructor when the observer |this| is tied to a single
+ // InstanceRegistry for its entire lifetime, or until the observee (the
+ // InstanceRegistry) is destroyed, whichever comes first.
+ explicit Observer(InstanceRegistry* cache);
+
+ // Use this constructor when the observer |this| wants to observe a
+ // InstanceRegistry for part of its lifetime. It can then call Observe() to
+ // start and stop observing.
+ Observer();
+
+ ~Observer() override;
+
+ // Start observing a different InstanceRegistry. |instance_registry| may be
+ // nullptr, meaning to stop observing.
+ void Observe(InstanceRegistry* instance_registry);
+
+ private:
+ InstanceRegistry* instance_registry_ = nullptr;
+ };
+
+ InstanceRegistry();
+ ~InstanceRegistry();
+
+ InstanceRegistry(const InstanceRegistry&) = delete;
+ InstanceRegistry& operator=(const InstanceRegistry&) = delete;
+
+ void AddObserver(Observer* observer);
+ void RemoveObserver(Observer* observer);
+
+ using InstancePtr = std::unique_ptr<Instance>;
+ using Instances = std::vector<InstancePtr>;
+
+ // Notification and merging might be delayed until after OnInstances returns.
+ // For example, suppose that the initial set of states is (a0, b0, c0) for
+ // three app_id's ("a", "b", "c"). Now suppose OnInstances is called with two
+ // updates (b1, c1), and when notified of b1, an observer calls OnInstances
+ // again with (c2, d2). The c1 delta should be processed before the c2 delta,
+ // as it was sent first, and both c1 and c2 will be updated to the observer
+ // following the sequence. This means that processing c2 (scheduled by the
+ // second OnInstances call) should wait until the first OnInstances call has
+ // finished processing b1, and then c1, which means that processing c2 is
+ // delayed until after the second OnInstances call returns.
+ //
+ // The caller presumably calls OnInstances(std::move(deltas)).
+ void OnInstances(const Instances& deltas);
+
+ // Return windows for the |app_id|.
+ std::set<aura::Window*> GetWindows(const std::string& app_id);
+
+ // Return the state for the |window|.
+ InstanceState GetState(aura::Window* window) const;
+
+ // Return the shelf id for the |window|.
+ ash::ShelfID GetShelfId(aura::Window* window) const;
+
+ // Return true if there is an instance for the |window|.
+ bool Exists(aura::Window* window) const;
+
+ // Calls f, a void-returning function whose arguments are (const
+ // apps::InstanceUpdate&), on each window in the instance_registry.
+ //
+ // f's argument is an apps::InstanceUpdate instead of an Instance* so that
+ // callers can more easily share code with Observer::OnInstanceUpdate (which
+ // also takes an apps::InstanceUpdate), and an apps::InstanceUpdate also has a
+ // StateIsNull method.
+ //
+ // The apps::InstanceUpdate argument to f shouldn't be accessed after f
+ // returns.
+ //
+ // f must be synchronous, and if it asynchronously calls ForEachInstance
+ // again, it's not guaranteed to see a consistent state.
+ template <typename FunctionType>
+ void ForEachInstance(FunctionType f) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+ for (const auto& s_iter : states_) {
+ apps::Instance* state = s_iter.second.get();
+ f(apps::InstanceUpdate(state, nullptr));
+ }
+ }
+
+ // Calls f, a void-returning function whose arguments are (const
+ // apps::InstanceUpdate&), on the instance in the instance_registry with the
+ // given window. It will return true (and call f) if there is such an
+ // instance, otherwise it will return false (and not call f). The
+ // InstanceUpdate argument to f has the same semantics as for ForEachInstance,
+ // above.
+ //
+ // f must be synchronous, and if it asynchronously calls ForOneInstance again,
+ // it's not guaranteed to see a consistent state.
+ template <typename FunctionType>
+ bool ForOneInstance(const aura::Window* window, FunctionType f) {
+ DCHECK_CALLED_ON_VALID_SEQUENCE(my_sequence_checker_);
+
+ auto s_iter = states_.find(window);
+ apps::Instance* state =
+ (s_iter != states_.end()) ? s_iter->second.get() : nullptr;
+ if (state) {
+ f(apps::InstanceUpdate(state, nullptr));
+ return true;
+ }
+ return false;
+ }
+
+ private:
+ void DoOnInstances(const Instances& deltas);
+
+ base::ObserverList<Observer> observers_;
+
+ // OnInstances calls DoOnInstances zero or more times. If we're nested,
+ // in_progress is true, so that there's multiple OnInstances call to this
+ // InstanceRegistry in the call stack, the deeper OnInstances call simply adds
+ // work to deltas_pending_ and returns without calling DoOnInstances. If we're
+ // not nested, in_progress is false, OnInstances calls DoOnInstances one or
+ // more times; "more times" happens if DoOnInstances notifying observers leads
+ // to more OnInstances calls that enqueue deltas_pending_ work.
+ //
+ // Nested OnInstances calls are expected to be rare (but still dealt with
+ // sensibly). In the typical case, OnInstances should call DoOnInstances
+ // exactly once, and deltas_pending_ will stay empty.
+ bool in_progress_ = false;
+
+ // Maps from window to the latest state: the "sum" of all previous deltas.
+ std::map<const aura::Window*, InstancePtr> states_;
+ Instances deltas_pending_;
+
+ // Maps from app id to app windows.
+ std::map<const std::string, std::set<aura::Window*>> app_id_to_app_windows_;
+
+ SEQUENCE_CHECKER(my_sequence_checker_);
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_REGISTRY_H_
diff --git a/chromium/components/services/app_service/public/cpp/instance_registry_unittest.cc b/chromium/components/services/app_service/public/cpp/instance_registry_unittest.cc
new file mode 100644
index 00000000000..49120032d2c
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance_registry_unittest.cc
@@ -0,0 +1,593 @@
+// Copyright 2019 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 <memory>
+#include <set>
+#include <vector>
+
+#include "components/services/app_service/public/cpp/instance.h"
+#include "components/services/app_service/public/cpp/instance_registry.h"
+#include "components/services/app_service/public/cpp/instance_update.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/aura/window.h"
+
+class InstanceRegistryTest : public testing::Test,
+ public apps::InstanceRegistry::Observer {
+ protected:
+ static std::unique_ptr<apps::Instance> MakeInstance(
+ const char* app_id,
+ aura::Window* window,
+ apps::InstanceState state = apps::InstanceState::kUnknown,
+ base::Time time = base::Time()) {
+ std::unique_ptr<apps::Instance> instance =
+ std::make_unique<apps::Instance>(app_id, window);
+ instance->UpdateState(state, time);
+ return instance;
+ }
+
+ void CallForEachInstance(apps::InstanceRegistry& instance_registry) {
+ instance_registry.ForEachInstance(
+ [this](const apps::InstanceUpdate& update) {
+ OnInstanceUpdate(update);
+ });
+ }
+
+ apps::InstanceState GetState(apps::InstanceRegistry& instance_registry,
+ aura::Window* window) {
+ return instance_registry.GetState(window);
+ }
+
+ // apps::InstanceRegistry::Observer overrides.
+ void OnInstanceUpdate(const apps::InstanceUpdate& update) override {
+ EXPECT_NE("", update.AppId());
+ if (update.StateChanged() &&
+ update.State() == apps::InstanceState::kRunning) {
+ num_running_apps_++;
+ }
+ updated_ids_.insert(update.AppId());
+ updated_windows_.insert(update.Window());
+ }
+
+ void OnInstanceRegistryWillBeDestroyed(
+ apps::InstanceRegistry* instance_registry) override {
+ // The test code explicitly calls both AddObserver and RemoveObserver.
+ NOTREACHED();
+ }
+
+ int num_running_apps_ = 0;
+ std::set<std::string> updated_ids_;
+ std::set<const aura::Window*> updated_windows_;
+};
+
+// In the tests below, just "recursive" means that instance_registry.OnInstances
+// calls observer.OnInstanceUpdate which calls instance_registry.ForEachInstance
+// and instance_registry.ForOneInstance. "Super-recursive" means that
+// instance_registry.OnInstances calls observer.OnInstanceUpdate calls
+// instance_registry.OnInstances which calls observer.OnInstanceUpdate.
+class InstanceRecursiveObserver : public apps::InstanceRegistry::Observer {
+ public:
+ explicit InstanceRecursiveObserver(apps::InstanceRegistry* instance_registry)
+ : instance_registry_(instance_registry) {
+ Observe(instance_registry);
+ }
+
+ ~InstanceRecursiveObserver() override = default;
+
+ void PrepareForOnInstances(int expected_num_instances,
+ std::vector<std::unique_ptr<apps::Instance>>*
+ super_recursive_instances = nullptr) {
+ expected_num_instances_ = expected_num_instances;
+ num_instances_seen_on_instance_update_ = 0;
+
+ if (super_recursive_instances) {
+ super_recursive_instances_.swap(*super_recursive_instances);
+ }
+ }
+
+ int NumInstancesSeenOnInstanceUpdate() {
+ return num_instances_seen_on_instance_update_;
+ }
+
+ protected:
+ // apps::InstanceRegistry::Observer overrides.
+ void OnInstanceUpdate(const apps::InstanceUpdate& outer) override {
+ int num_instance = 0;
+ instance_registry_->ForEachInstance(
+ [&outer, &num_instance](const apps::InstanceUpdate& inner) {
+ if (outer.Window() == inner.Window()) {
+ ExpectEq(outer, inner);
+ }
+ num_instance++;
+ });
+
+ EXPECT_TRUE(instance_registry_->ForOneInstance(
+ outer.Window(), [&outer](const apps::InstanceUpdate& inner) {
+ ExpectEq(outer, inner);
+ }));
+
+ if (expected_num_instances_ >= 0) {
+ EXPECT_EQ(expected_num_instances_, num_instance);
+ }
+
+ std::vector<std::unique_ptr<apps::Instance>> super_recursive;
+ while (!super_recursive_instances_.empty()) {
+ std::unique_ptr<apps::Instance> instance =
+ std::move(super_recursive_instances_.back());
+ if (instance.get() == nullptr) {
+ // This is the placeholder 'punctuation'.
+ super_recursive_instances_.pop_back();
+ break;
+ }
+ super_recursive.push_back(std::move(instance));
+ super_recursive_instances_.pop_back();
+ }
+ if (!super_recursive.empty()) {
+ instance_registry_->OnInstances(std::move(super_recursive));
+ }
+
+ num_instances_seen_on_instance_update_++;
+ }
+
+ void OnInstanceRegistryWillBeDestroyed(
+ apps::InstanceRegistry* instance_registry) override {
+ Observe(nullptr);
+ }
+
+ static void ExpectEq(const apps::InstanceUpdate& outer,
+ const apps::InstanceUpdate& inner) {
+ EXPECT_EQ(outer.AppId(), inner.AppId());
+ EXPECT_EQ(outer.Window(), inner.Window());
+ EXPECT_EQ(outer.LaunchId(), inner.LaunchId());
+ EXPECT_EQ(outer.State(), inner.State());
+ EXPECT_EQ(outer.LastUpdatedTime(), inner.LastUpdatedTime());
+ EXPECT_EQ(outer.BrowserContext(), inner.BrowserContext());
+ }
+
+ apps::InstanceRegistry* instance_registry_;
+ int expected_num_instances_;
+ int num_instances_seen_on_instance_update_;
+
+ // Non-empty when this.OnInstanceUpdate should trigger more
+ // instance_registry_.OnInstances calls.
+ //
+ // During OnInstanceUpdate, this vector (a stack) is popped from the back
+ // until a nullptr 'punctuation' element (a group terminator) is seen. If that
+ // group of popped elements (in LIFO order) is non-empty, that group forms the
+ // vector of App's passed to instance_registry_.OnInstances.
+ std::vector<std::unique_ptr<apps::Instance>> super_recursive_instances_;
+};
+
+TEST_F(InstanceRegistryTest, ForEachInstance) {
+ std::vector<std::unique_ptr<apps::Instance>> deltas;
+ apps::InstanceRegistry instance_registry;
+
+ updated_windows_.clear();
+ updated_ids_.clear();
+
+ CallForEachInstance(instance_registry);
+
+ EXPECT_EQ(0u, updated_windows_.size());
+ EXPECT_EQ(0u, updated_ids_.size());
+
+ deltas.clear();
+ aura::Window window1(nullptr);
+ window1.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window2(nullptr);
+ window2.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window3(nullptr);
+ window3.Init(ui::LAYER_NOT_DRAWN);
+ deltas.push_back(MakeInstance("a", &window1));
+ deltas.push_back(MakeInstance("b", &window2));
+ deltas.push_back(MakeInstance("c", &window3));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_TRUE(instance_registry.GetWindows("a") ==
+ std::set<aura::Window*>{&window1});
+ EXPECT_TRUE(instance_registry.GetWindows("b") ==
+ std::set<aura::Window*>{&window2});
+ EXPECT_TRUE(instance_registry.GetWindows("c") ==
+ std::set<aura::Window*>{&window3});
+
+ updated_windows_.clear();
+ updated_ids_.clear();
+ CallForEachInstance(instance_registry);
+
+ EXPECT_EQ(3u, updated_windows_.size());
+ EXPECT_EQ(3u, updated_ids_.size());
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window1));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window2));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window3));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("a"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("b"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("c"));
+
+ deltas.clear();
+ aura::Window window4(nullptr);
+ window4.Init(ui::LAYER_NOT_DRAWN);
+ deltas.push_back(MakeInstance("a", &window1, apps::InstanceState::kRunning));
+ deltas.push_back(MakeInstance("c", &window4));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_TRUE(instance_registry.GetWindows("a") ==
+ std::set<aura::Window*>{&window1});
+ EXPECT_TRUE(instance_registry.GetWindows("c") ==
+ (std::set<aura::Window*>{&window3, &window4}));
+
+ updated_windows_.clear();
+ updated_ids_.clear();
+ CallForEachInstance(instance_registry);
+
+ EXPECT_EQ(4u, updated_windows_.size());
+ EXPECT_EQ(3u, updated_ids_.size());
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window1));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window2));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window3));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window4));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("a"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("b"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("c"));
+
+ // Test that ForOneApp succeeds for window4 and fails for window5.
+
+ bool found_window4 = false;
+ EXPECT_TRUE(instance_registry.ForOneInstance(
+ &window4, [&found_window4](const apps::InstanceUpdate& update) {
+ found_window4 = true;
+ EXPECT_EQ("c", update.AppId());
+ }));
+ EXPECT_TRUE(found_window4);
+
+ bool found_window5 = false;
+ aura::Window window5(nullptr);
+ window5.Init(ui::LAYER_NOT_DRAWN);
+ EXPECT_FALSE(instance_registry.ForOneInstance(
+ &window5, [&found_window5](const apps::InstanceUpdate& update) {
+ found_window5 = true;
+ }));
+ EXPECT_FALSE(found_window5);
+}
+
+TEST_F(InstanceRegistryTest, Observer) {
+ std::vector<std::unique_ptr<apps::Instance>> deltas;
+ apps::InstanceRegistry instance_registry;
+
+ instance_registry.AddObserver(this);
+
+ num_running_apps_ = 0;
+ updated_windows_.clear();
+ updated_ids_.clear();
+ deltas.clear();
+
+ aura::Window window1(nullptr);
+ window1.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window2(nullptr);
+ window2.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window3(nullptr);
+ window3.Init(ui::LAYER_NOT_DRAWN);
+
+ deltas.push_back(MakeInstance("a", &window1));
+ deltas.push_back(MakeInstance("c", &window2));
+ deltas.push_back(MakeInstance("a", &window3));
+ instance_registry.OnInstances(std::move(deltas));
+
+ EXPECT_EQ(0, num_running_apps_);
+ EXPECT_EQ(3u, updated_windows_.size());
+ EXPECT_EQ(2u, updated_ids_.size());
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window1));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window2));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window3));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("a"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("c"));
+
+ num_running_apps_ = 0;
+ updated_ids_.clear();
+ deltas.clear();
+
+ aura::Window window4(nullptr);
+ window4.Init(ui::LAYER_NOT_DRAWN);
+
+ deltas.push_back(MakeInstance("b", &window4));
+ deltas.push_back(MakeInstance("c", &window2, apps::InstanceState::kRunning));
+ instance_registry.OnInstances(std::move(deltas));
+
+ EXPECT_EQ(1, num_running_apps_);
+ EXPECT_EQ(2u, updated_ids_.size());
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window2));
+ EXPECT_NE(updated_windows_.end(), updated_windows_.find(&window4));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("b"));
+ EXPECT_NE(updated_ids_.end(), updated_ids_.find("c"));
+
+ instance_registry.RemoveObserver(this);
+
+ num_running_apps_ = 0;
+ updated_windows_.clear();
+ updated_ids_.clear();
+ deltas.clear();
+
+ aura::Window window5(nullptr);
+ window5.Init(ui::LAYER_NOT_DRAWN);
+ deltas.push_back(MakeInstance("f", &window5, apps::InstanceState::kRunning));
+ instance_registry.OnInstances(std::move(deltas));
+
+ EXPECT_EQ(0, num_running_apps_);
+ EXPECT_EQ(0u, updated_windows_.size());
+ EXPECT_EQ(0u, updated_ids_.size());
+}
+
+TEST_F(InstanceRegistryTest, WholeProcessForOneWindow) {
+ std::vector<std::unique_ptr<apps::Instance>> deltas;
+ apps::InstanceRegistry instance_registry;
+ InstanceRecursiveObserver observer(&instance_registry);
+
+ apps::InstanceState instance_state = apps::InstanceState::kStarted;
+ deltas.clear();
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ observer.PrepareForOnInstances(1);
+ deltas.push_back(MakeInstance("p", &window, instance_state));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(1, observer.NumInstancesSeenOnInstanceUpdate());
+
+ instance_state = static_cast<apps::InstanceState>(
+ instance_state | apps::InstanceState::kRunning |
+ apps::InstanceState::kActive | apps::InstanceState::kVisible);
+ observer.PrepareForOnInstances(1);
+ deltas.clear();
+ deltas.push_back(MakeInstance("p", &window, instance_state));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(1, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("p") ==
+ std::set<aura::Window*>{&window});
+
+ apps::InstanceState state1 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ apps::InstanceState state2 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ apps::InstanceState state3 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ apps::InstanceState state4 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning |
+ apps::InstanceState::kActive | apps::InstanceState::kVisible);
+ apps::InstanceState state5 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning |
+ apps::InstanceState::kVisible);
+ apps::InstanceState state6 = apps::InstanceState::kDestroyed;
+ observer.PrepareForOnInstances(1);
+ deltas.clear();
+ deltas.push_back(MakeInstance("p", &window, state1));
+ deltas.push_back(MakeInstance("p", &window, state2));
+ deltas.push_back(MakeInstance("p", &window, state3));
+ deltas.push_back(MakeInstance("p", &window, state4));
+ deltas.push_back(MakeInstance("p", &window, state5));
+ deltas.push_back(MakeInstance("p", &window, state6));
+ instance_registry.OnInstances(std::move(deltas));
+ // OnInstanceUpdate is called for state1, because state1 is different with
+ // previous instance_state. state2 and state3 is not changed, so
+ // OnInstanceUpdate is not called. OnInstanceUpdate is called for state4,
+ // state5, and state6 separately, because they are different. So
+ // OnInstanceUpdate is called 4 times, for state1, state4, state5, and state6.
+ EXPECT_EQ(4, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("p").empty());
+
+ bool found_window = false;
+ EXPECT_FALSE(instance_registry.ForOneInstance(
+ &window, [&found_window](const apps::InstanceUpdate& update) {
+ found_window = true;
+ }));
+ EXPECT_FALSE(found_window);
+
+ observer.PrepareForOnInstances(1);
+ deltas.clear();
+ deltas.push_back(MakeInstance("p", &window, state5));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(1, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("p") ==
+ std::set<aura::Window*>{&window});
+
+ found_window = false;
+ EXPECT_TRUE(instance_registry.ForOneInstance(
+ &window, [&found_window](const apps::InstanceUpdate& update) {
+ found_window = true;
+ }));
+ EXPECT_TRUE(found_window);
+}
+
+TEST_F(InstanceRegistryTest, Recursive) {
+ std::vector<std::unique_ptr<apps::Instance>> deltas;
+ apps::InstanceRegistry instance_registry;
+ InstanceRecursiveObserver observer(&instance_registry);
+
+ apps::InstanceState instance_state1 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ apps::InstanceState instance_state2 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ deltas.clear();
+ aura::Window window1(nullptr);
+ window1.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window2(nullptr);
+ window2.Init(ui::LAYER_NOT_DRAWN);
+ observer.PrepareForOnInstances(-1);
+ deltas.push_back(MakeInstance("o", &window1, instance_state1));
+ deltas.push_back(MakeInstance("p", &window2, instance_state2));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(2, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("o") ==
+ std::set<aura::Window*>{&window1});
+ EXPECT_TRUE(instance_registry.GetWindows("p") ==
+ std::set<aura::Window*>{&window2});
+
+ apps::InstanceState instance_state3 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ apps::InstanceState instance_state4 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ std::vector<apps::InstanceState> latest_state;
+ latest_state.push_back(instance_state3);
+ latest_state.push_back(instance_state3);
+ deltas.clear();
+ aura::Window window3(nullptr);
+ window3.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window4(nullptr);
+ window4.Init(ui::LAYER_NOT_DRAWN);
+ observer.PrepareForOnInstances(-1);
+ deltas.push_back(MakeInstance("p", &window2, instance_state3));
+ deltas.push_back(MakeInstance("q", &window3, instance_state4));
+ deltas.push_back(MakeInstance("p", &window4, instance_state3));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(2, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("p") ==
+ (std::set<aura::Window*>{&window2, &window4}));
+ EXPECT_TRUE(instance_registry.GetWindows("q") ==
+ std::set<aura::Window*>{&window3});
+
+ apps::InstanceState instance_state5 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ apps::InstanceState instance_state6 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning);
+ apps::InstanceState instance_state7 = static_cast<apps::InstanceState>(
+ apps::InstanceState::kStarted | apps::InstanceState::kRunning |
+ apps::InstanceState::kActive);
+
+ observer.PrepareForOnInstances(4);
+ deltas.clear();
+ deltas.push_back(MakeInstance("p", &window2, instance_state5));
+ deltas.push_back(MakeInstance("p", &window2, instance_state6));
+ deltas.push_back(MakeInstance("p", &window2, instance_state7));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(1, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("p") ==
+ (std::set<aura::Window*>{&window2, &window4}));
+
+ apps::InstanceState instance_state8 =
+ static_cast<apps::InstanceState>(apps::InstanceState::kDestroyed);
+ observer.PrepareForOnInstances(-1);
+ deltas.clear();
+ deltas.push_back(MakeInstance("p", &window2, instance_state8));
+ deltas.push_back(MakeInstance("p", &window4, instance_state8));
+ deltas.push_back(MakeInstance("q", &window3, instance_state8));
+ deltas.push_back(MakeInstance("o", &window1, instance_state8));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(4, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("o").empty());
+ EXPECT_TRUE(instance_registry.GetWindows("p").empty());
+ EXPECT_TRUE(instance_registry.GetWindows("q").empty());
+
+ bool found_window = false;
+ EXPECT_FALSE(instance_registry.ForOneInstance(
+ &window2, [&found_window](const apps::InstanceUpdate& update) {
+ found_window = true;
+ }));
+ EXPECT_FALSE(found_window);
+
+ found_window = false;
+ EXPECT_FALSE(instance_registry.ForOneInstance(
+ &window4, [&found_window](const apps::InstanceUpdate& update) {
+ found_window = true;
+ }));
+ EXPECT_FALSE(found_window);
+
+ found_window = false;
+ EXPECT_FALSE(instance_registry.ForOneInstance(
+ &window3, [&found_window](const apps::InstanceUpdate& update) {
+ found_window = true;
+ }));
+ EXPECT_FALSE(found_window);
+
+ found_window = false;
+ EXPECT_FALSE(instance_registry.ForOneInstance(
+ &window1, [&found_window](const apps::InstanceUpdate& update) {
+ found_window = true;
+ }));
+ EXPECT_FALSE(found_window);
+
+ observer.PrepareForOnInstances(1);
+ deltas.clear();
+ deltas.push_back(MakeInstance("p", &window2, instance_state7));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(1, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("p") ==
+ std::set<aura::Window*>{&window2});
+
+ found_window = false;
+ EXPECT_TRUE(instance_registry.ForOneInstance(
+ &window2, [&found_window](const apps::InstanceUpdate& update) {
+ found_window = true;
+ }));
+ EXPECT_TRUE(found_window);
+}
+
+TEST_F(InstanceRegistryTest, SuperRecursive) {
+ std::vector<std::unique_ptr<apps::Instance>> deltas;
+ apps::InstanceRegistry instance_registry;
+ InstanceRecursiveObserver observer(&instance_registry);
+
+ // Set up a series of OnInstances to be called during
+ // observer.OnInstanceUpdate:
+ // - the 1st update is {'a', &window2, kActive}.
+ // - the 2nd update is {'b', &window3, kActive}.
+ // - the 3rd update is {'c', &window4, kActive}.
+ // - the 4th update is {'b', &window5, kVisible}.
+ // - the 5th update is {'c', &window4, kVisible}.
+ // - the 6td update is {'b', &window3, kRunning}.
+ // - the 7th update is {'a', &window2, kRunning}.
+ // - the 8th update is {'b', &window1, kStarted}.
+ //
+ // The vector is processed in LIFO order with nullptr punctuation to
+ // terminate each group. See the comment on the
+ // RecursiveObserver::super_recursive_apps_ field.
+ std::vector<std::unique_ptr<apps::Instance>> super_recursive_apps;
+ aura::Window window1(nullptr);
+ window1.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window2(nullptr);
+ window2.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window3(nullptr);
+ window3.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window4(nullptr);
+ window4.Init(ui::LAYER_NOT_DRAWN);
+ aura::Window window5(nullptr);
+ window5.Init(ui::LAYER_NOT_DRAWN);
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(
+ MakeInstance("a", &window1, apps::InstanceState::kStarted));
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(MakeInstance("a", &window2));
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(MakeInstance("b", &window3));
+ super_recursive_apps.push_back(
+ MakeInstance("a", &window2, apps::InstanceState::kDestroyed));
+ super_recursive_apps.push_back(
+ MakeInstance("a", &window2, apps::InstanceState::kRunning));
+ super_recursive_apps.push_back(
+ MakeInstance("b", &window3, apps::InstanceState::kRunning));
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(nullptr);
+ super_recursive_apps.push_back(
+ MakeInstance("c", &window4, apps::InstanceState::kVisible));
+ super_recursive_apps.push_back(
+ MakeInstance("b", &window5, apps::InstanceState::kVisible));
+
+ observer.PrepareForOnInstances(-1, &super_recursive_apps);
+ deltas.clear();
+ deltas.push_back(MakeInstance("a", &window2, apps::InstanceState::kActive));
+ deltas.push_back(MakeInstance("b", &window3, apps::InstanceState::kActive));
+ deltas.push_back(MakeInstance("c", &window4, apps::InstanceState::kActive));
+ instance_registry.OnInstances(std::move(deltas));
+ EXPECT_EQ(10, observer.NumInstancesSeenOnInstanceUpdate());
+ EXPECT_TRUE(instance_registry.GetWindows("a") ==
+ (std::set<aura::Window*>{&window1, &window2}));
+ EXPECT_TRUE(instance_registry.GetWindows("b") ==
+ (std::set<aura::Window*>{&window3, &window5}));
+ EXPECT_TRUE(instance_registry.GetWindows("c") ==
+ std::set<aura::Window*>{&window4});
+
+ // After all of that, check that for each window, the last delta won.
+ EXPECT_EQ(apps::InstanceState::kStarted,
+ GetState(instance_registry, &window1));
+ EXPECT_EQ(apps::InstanceState::kUnknown,
+ GetState(instance_registry, &window2));
+ EXPECT_EQ(apps::InstanceState::kRunning,
+ GetState(instance_registry, &window3));
+ EXPECT_EQ(apps::InstanceState::kVisible,
+ GetState(instance_registry, &window4));
+ EXPECT_EQ(apps::InstanceState::kVisible,
+ GetState(instance_registry, &window5));
+}
diff --git a/chromium/components/services/app_service/public/cpp/instance_update.cc b/chromium/components/services/app_service/public/cpp/instance_update.cc
new file mode 100644
index 00000000000..6c3d6d79090
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance_update.cc
@@ -0,0 +1,161 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/instance_update.h"
+
+#include "base/logging.h"
+#include "base/strings/string_util.h"
+#include "components/services/app_service/public/cpp/instance.h"
+
+namespace apps {
+
+// static
+void InstanceUpdate::Merge(Instance* state, const Instance* delta) {
+ DCHECK(state);
+ if (!delta) {
+ return;
+ }
+ if ((delta->AppId() != state->AppId()) ||
+ delta->Window() != state->Window()) {
+ LOG(ERROR) << "inconsistent (app_id, window): (" << delta->AppId() << ", "
+ << delta->Window() << ") vs (" << state->AppId() << ", "
+ << state->Window() << ") ";
+ DCHECK(false);
+ return;
+ }
+ if (!delta->LaunchId().empty()) {
+ state->SetLaunchId(delta->LaunchId());
+ }
+ if (delta->State() != InstanceState::kUnknown) {
+ state->UpdateState(delta->State(), delta->LastUpdatedTime());
+ }
+ if (delta->BrowserContext()) {
+ state->SetBrowserContext(delta->BrowserContext());
+ }
+ // When adding new fields to the Instance class, this function should also be
+ // updated.
+}
+
+// static
+bool InstanceUpdate::Equals(const Instance* state, const Instance* delta) {
+ if (delta == nullptr) {
+ return true;
+ }
+ if (state == nullptr) {
+ if (static_cast<InstanceState>(delta->State() &
+ InstanceState::kDestroyed) !=
+ InstanceState::kUnknown) {
+ return true;
+ }
+ return false;
+ }
+
+ if ((delta->AppId() != state->AppId()) ||
+ delta->Window() != state->Window()) {
+ LOG(ERROR) << "inconsistent (app_id, window): (" << delta->AppId() << ", "
+ << delta->Window() << ") vs (" << state->AppId() << ", "
+ << state->Window() << ") ";
+ DCHECK(false);
+ return false;
+ }
+ if (!delta->LaunchId().empty() && delta->LaunchId() != state->LaunchId()) {
+ return false;
+ }
+ if (delta->State() != InstanceState::kUnknown &&
+ (delta->State() != state->State() ||
+ delta->LastUpdatedTime() != state->LastUpdatedTime())) {
+ return false;
+ }
+ if (delta->BrowserContext() &&
+ delta->BrowserContext() != state->BrowserContext()) {
+ return false;
+ }
+ return true;
+ // When adding new fields to the Instance class, this function should also be
+ // updated.
+}
+
+InstanceUpdate::InstanceUpdate(Instance* state, Instance* delta)
+ : state_(state), delta_(delta) {
+ DCHECK(state_ || delta_);
+ if (state_ && delta_) {
+ DCHECK(state_->AppId() == delta->AppId());
+ DCHECK(state_->Window() == delta->Window());
+ }
+}
+
+bool InstanceUpdate::StateIsNull() const {
+ return state_ == nullptr;
+}
+
+const std::string& InstanceUpdate::AppId() const {
+ return delta_ ? delta_->AppId() : state_->AppId();
+}
+
+aura::Window* InstanceUpdate::Window() const {
+ return delta_ ? delta_->Window() : state_->Window();
+}
+
+const std::string& InstanceUpdate::LaunchId() const {
+ if (delta_ && !delta_->LaunchId().empty()) {
+ return delta_->LaunchId();
+ }
+ if (state_ && !state_->LaunchId().empty()) {
+ return state_->LaunchId();
+ }
+ return base::EmptyString();
+}
+
+bool InstanceUpdate::LaunchIdChanged() const {
+ return delta_ && !delta_->LaunchId().empty() &&
+ (!state_ || (delta_->LaunchId() != state_->LaunchId()));
+}
+
+InstanceState InstanceUpdate::State() const {
+ if (delta_ && (delta_->State() != InstanceState::kUnknown)) {
+ return delta_->State();
+ }
+ if (state_) {
+ return state_->State();
+ }
+ return InstanceState::kUnknown;
+}
+
+bool InstanceUpdate::StateChanged() const {
+ return delta_ && (delta_->State() != InstanceState::kUnknown) &&
+ (!state_ || (delta_->State() != state_->State()));
+}
+
+base::Time InstanceUpdate::LastUpdatedTime() const {
+ if (delta_ && !delta_->LastUpdatedTime().is_null()) {
+ return delta_->LastUpdatedTime();
+ }
+ if (state_ && !state_->LastUpdatedTime().is_null()) {
+ return state_->LastUpdatedTime();
+ }
+
+ return base::Time();
+}
+
+bool InstanceUpdate::LastUpdatedTimeChanged() const {
+ return delta_ && !delta_->LastUpdatedTime().is_null() &&
+ (!state_ || (delta_->LastUpdatedTime() != state_->LastUpdatedTime()));
+}
+
+content::BrowserContext* InstanceUpdate::BrowserContext() const {
+ if (delta_ && delta_->BrowserContext()) {
+ return delta_->BrowserContext();
+ }
+ if (state_ && state_->BrowserContext()) {
+ return state_->BrowserContext();
+ }
+ return nullptr;
+}
+
+bool InstanceUpdate::BrowserContextChanged() const {
+ return delta_ && delta_->BrowserContext() &&
+ (!state_ || (delta_->BrowserContext() != state_->BrowserContext()));
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/instance_update.h b/chromium/components/services/app_service/public/cpp/instance_update.h
new file mode 100644
index 00000000000..1e240d00cd2
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance_update.h
@@ -0,0 +1,79 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_UPDATE_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_UPDATE_H_
+
+#include <string>
+
+#include "base/time/time.h"
+#include "components/services/app_service/public/cpp/instance.h"
+
+namespace apps {
+
+class Instance;
+
+// Wraps two Instance's, a prior state and a delta on top of that state. The
+// state is conceptually the "sum" of all of the previous deltas, with
+// "addition" or "merging" simply being that the most recent version of each
+// field "wins".
+//
+// The state may be nullptr, meaning that there are no previous deltas.
+// Alternatively, the delta may be nullptr, meaning that there is no change in
+// state. At least one of state and delta must be non-nullptr.
+//
+// The combination of the two (state and delta) can answer questions such as:
+// - What is the app's launch_id? If the delta knows, that's the answer.
+// Otherwise, ask the state.
+// - Is the app launched? Likewise, if the delta says yes or no, that's the
+// answer. Otherwise, the delta says "unknown", ask the state.
+//
+// An InstanceUpdate is read-only once constructed. All of its fields and
+// methods are const. The constructor caller must guarantee that the Instance
+// pointer remain valid for the lifetime of the AppUpdate.
+class InstanceUpdate {
+ public:
+ // Modifies |state| by copying over all of |delta|'s known fields: those
+ // fields whose values aren't "unknown" or invalid. The |state| may not be
+ // nullptr.
+ static void Merge(Instance* state, const Instance* delta);
+
+ // Returns true if |state| exists and is equal to |delta|, or |delta| are
+ // nullptr. Return false otherwise.
+ static bool Equals(const Instance* state, const Instance* delta);
+
+ // At most one of |state| or |delta| may be nullptr.
+ InstanceUpdate(Instance* state, Instance* delta);
+
+ InstanceUpdate(const InstanceUpdate&) = delete;
+ InstanceUpdate& operator=(const InstanceUpdate&) = delete;
+
+ // Returns whether this is the first update for the given window.
+ // Equivalently, there are no previous deltas for the window.
+ bool StateIsNull() const;
+
+ const std::string& AppId() const;
+
+ aura::Window* Window() const;
+
+ const std::string& LaunchId() const;
+ bool LaunchIdChanged() const;
+
+ InstanceState State() const;
+ bool StateChanged() const;
+
+ base::Time LastUpdatedTime() const;
+ bool LastUpdatedTimeChanged() const;
+
+ content::BrowserContext* BrowserContext() const;
+ bool BrowserContextChanged() const;
+
+ private:
+ Instance* state_;
+ Instance* delta_;
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INSTANCE_UPDATE_H_
diff --git a/chromium/components/services/app_service/public/cpp/instance_update_unittest.cc b/chromium/components/services/app_service/public/cpp/instance_update_unittest.cc
new file mode 100644
index 00000000000..67a507ed819
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/instance_update_unittest.cc
@@ -0,0 +1,215 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/instance_update.h"
+#include "chrome/test/base/testing_profile.h"
+#include "components/services/app_service/public/cpp/instance.h"
+#include "content/public/test/browser_task_environment.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const char app_id[] = "abcdefgh";
+const char test_launch_id0[] = "abc";
+const char test_launch_id1[] = "xyz";
+
+} // namespace
+
+class InstanceUpdateTest : public testing::Test {
+ protected:
+ void ExpectNoChange() {
+ expect_launch_id_changed_ = false;
+ expect_state_changed_ = false;
+ expect_last_updated_time_changed_ = false;
+ }
+
+ void CheckExpects(const apps::InstanceUpdate& u) {
+ EXPECT_EQ(expect_launch_id_, u.LaunchId());
+ EXPECT_EQ(expect_launch_id_changed_, u.LaunchIdChanged());
+ EXPECT_EQ(expect_state_, u.State());
+ EXPECT_EQ(expect_state_changed_, u.StateChanged());
+ EXPECT_EQ(expect_last_updated_time_, u.LastUpdatedTime());
+ EXPECT_EQ(expect_last_updated_time_changed_, u.LastUpdatedTimeChanged());
+ }
+
+ void TestInstanceUpdate(apps::Instance* state, apps::Instance* delta) {
+ apps::InstanceUpdate u(state, delta);
+ EXPECT_EQ(app_id, u.AppId());
+ EXPECT_EQ(state == nullptr, u.StateIsNull());
+ expect_launch_id_ = base::EmptyString();
+ expect_state_ = apps::InstanceState::kUnknown;
+ expect_last_updated_time_ = base::Time();
+
+ ExpectNoChange();
+ CheckExpects(u);
+
+ if (delta) {
+ delta->SetLaunchId(test_launch_id0);
+ expect_launch_id_ = test_launch_id0;
+ expect_launch_id_changed_ = true;
+ CheckExpects(u);
+ }
+ if (state) {
+ state->SetLaunchId(test_launch_id0);
+ expect_launch_id_ = test_launch_id0;
+ expect_launch_id_changed_ = false;
+ CheckExpects(u);
+ }
+ if (state) {
+ apps::InstanceUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+ if (delta) {
+ delta->SetLaunchId(test_launch_id1);
+ expect_launch_id_ = test_launch_id1;
+ expect_launch_id_changed_ = true;
+ CheckExpects(u);
+ }
+
+ // State and StateTime tests.
+ if (state) {
+ state->UpdateState(apps::InstanceState::kRunning,
+ base::Time::FromDoubleT(1000.0));
+ expect_state_ = apps::InstanceState::kRunning;
+ expect_last_updated_time_ = base::Time::FromDoubleT(1000.0);
+ expect_state_changed_ = false;
+ expect_last_updated_time_changed_ = false;
+ CheckExpects(u);
+ }
+ if (delta) {
+ delta->UpdateState(apps::InstanceState::kActive,
+ base::Time::FromDoubleT(2000.0));
+ expect_state_ = apps::InstanceState::kActive;
+ expect_last_updated_time_ = base::Time::FromDoubleT(2000.0);
+ expect_state_changed_ = true;
+ expect_last_updated_time_changed_ = true;
+ CheckExpects(u);
+ }
+ if (state) {
+ apps::InstanceUpdate::Merge(state, delta);
+ ExpectNoChange();
+ CheckExpects(u);
+ }
+ }
+
+ std::string expect_launch_id_;
+ bool expect_launch_id_changed_;
+ apps::InstanceState expect_state_;
+ bool expect_state_changed_;
+ base::Time expect_last_updated_time_;
+ bool expect_last_updated_time_changed_;
+
+ content::BrowserTaskEnvironment task_environment_;
+ TestingProfile profile_;
+};
+
+TEST_F(InstanceUpdateTest, StateIsNonNull) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ EXPECT_TRUE(apps::InstanceUpdate::Equals(state.get(), nullptr));
+ TestInstanceUpdate(state.get(), nullptr);
+}
+
+TEST_F(InstanceUpdateTest, DeltaIsNonNull) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ EXPECT_FALSE(apps::InstanceUpdate::Equals(nullptr, delta.get()));
+ TestInstanceUpdate(nullptr, delta.get());
+}
+
+TEST_F(InstanceUpdateTest, BothAreNonNull) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ EXPECT_TRUE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+ TestInstanceUpdate(state.get(), delta.get());
+}
+
+TEST_F(InstanceUpdateTest, LaunchIdIsUpdated) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ delta->SetLaunchId("abc");
+ EXPECT_FALSE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+}
+
+TEST_F(InstanceUpdateTest, LaunchIdIsNotUpdated) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ state->SetLaunchId("abc");
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ EXPECT_TRUE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+}
+
+TEST_F(InstanceUpdateTest, StateIsUpdated) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ delta->UpdateState(apps::InstanceState::kStarted, base::Time::Now());
+ EXPECT_FALSE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+}
+
+TEST_F(InstanceUpdateTest, StateIsNotUpdated) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ state->UpdateState(apps::InstanceState::kStarted, base::Time::Now());
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ EXPECT_TRUE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+}
+
+TEST_F(InstanceUpdateTest, BothLaunchAndStateIsUpdated) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ state->SetLaunchId("aaa");
+ state->UpdateState(apps::InstanceState::kStarted, base::Time::Now());
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ delta->SetLaunchId("bbb");
+ delta->UpdateState(apps::InstanceState::kRunning, base::Time::Now());
+ EXPECT_FALSE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+}
+
+TEST_F(InstanceUpdateTest, BrowserContextIsUpdated) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ delta->SetBrowserContext(&profile_);
+ EXPECT_FALSE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+}
+
+TEST_F(InstanceUpdateTest, BrowserContextIsNotUpdated) {
+ aura::Window window(nullptr);
+ window.Init(ui::LAYER_NOT_DRAWN);
+ std::unique_ptr<apps::Instance> state =
+ std::make_unique<apps::Instance>(app_id, &window);
+ state->SetBrowserContext(&profile_);
+ std::unique_ptr<apps::Instance> delta =
+ std::make_unique<apps::Instance>(app_id, &window);
+ EXPECT_TRUE(apps::InstanceUpdate::Equals(state.get(), delta.get()));
+}
diff --git a/chromium/components/services/app_service/public/cpp/intent_filter_util.cc b/chromium/components/services/app_service/public/cpp/intent_filter_util.cc
new file mode 100644
index 00000000000..b6b09753cae
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/intent_filter_util.cc
@@ -0,0 +1,128 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/intent_filter_util.h"
+
+#include "components/services/app_service/public/cpp/intent_util.h"
+
+namespace {
+
+bool ConditionsHaveOverlap(const apps::mojom::ConditionPtr& condition1,
+ const apps::mojom::ConditionPtr& condition2) {
+ if (condition1->condition_type != condition2->condition_type) {
+ return false;
+ }
+ // If there are same |condition_value| exist in the both |condition|s, there
+ // is an overlap.
+ for (auto& value1 : condition1->condition_values) {
+ for (auto& value2 : condition2->condition_values) {
+ if (value1 == value2) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+namespace apps_util {
+
+apps::mojom::ConditionValuePtr MakeConditionValue(
+ const std::string& value,
+ apps::mojom::PatternMatchType pattern_match_type) {
+ auto condition_value = apps::mojom::ConditionValue::New();
+ condition_value->value = value;
+ condition_value->match_type = pattern_match_type;
+
+ return condition_value;
+}
+
+apps::mojom::ConditionPtr MakeCondition(
+ apps::mojom::ConditionType condition_type,
+ std::vector<apps::mojom::ConditionValuePtr> condition_values) {
+ auto condition = apps::mojom::Condition::New();
+ condition->condition_type = condition_type;
+ condition->condition_values = std::move(condition_values);
+
+ return condition;
+}
+
+void AddSingleValueCondition(apps::mojom::ConditionType condition_type,
+ const std::string& value,
+ apps::mojom::PatternMatchType pattern_match_type,
+ apps::mojom::IntentFilterPtr& intent_filter) {
+ std::vector<apps::mojom::ConditionValuePtr> condition_values;
+ condition_values.push_back(
+ apps_util::MakeConditionValue(value, pattern_match_type));
+ auto condition =
+ apps_util::MakeCondition(condition_type, std::move(condition_values));
+ intent_filter->conditions.push_back(std::move(condition));
+}
+
+apps::mojom::IntentFilterPtr CreateIntentFilterForUrlScope(
+ const GURL& url,
+ bool with_action_view) {
+ auto intent_filter = apps::mojom::IntentFilter::New();
+
+ if (with_action_view) {
+ AddSingleValueCondition(
+ apps::mojom::ConditionType::kAction, apps_util::kIntentActionView,
+ apps::mojom::PatternMatchType::kNone, intent_filter);
+ }
+
+ AddSingleValueCondition(apps::mojom::ConditionType::kScheme, url.scheme(),
+ apps::mojom::PatternMatchType::kNone, intent_filter);
+
+ AddSingleValueCondition(apps::mojom::ConditionType::kHost, url.host(),
+ apps::mojom::PatternMatchType::kNone, intent_filter);
+
+ AddSingleValueCondition(apps::mojom::ConditionType::kPattern, url.path(),
+ apps::mojom::PatternMatchType::kPrefix,
+ intent_filter);
+
+ return intent_filter;
+}
+
+int GetFilterMatchLevel(const apps::mojom::IntentFilterPtr& intent_filter) {
+ int match_level = IntentFilterMatchLevel::kNone;
+ for (const auto& condition : intent_filter->conditions) {
+ switch (condition->condition_type) {
+ case apps::mojom::ConditionType::kAction:
+ // Action always need to be matched, so there is no need for
+ // match level.
+ break;
+ case apps::mojom::ConditionType::kScheme:
+ match_level += IntentFilterMatchLevel::kScheme;
+ break;
+ case apps::mojom::ConditionType::kHost:
+ match_level += IntentFilterMatchLevel::kHost;
+ break;
+ case apps::mojom::ConditionType::kPattern:
+ match_level += IntentFilterMatchLevel::kPattern;
+ break;
+ case apps::mojom::ConditionType::kMimeType:
+ match_level += IntentFilterMatchLevel::kMimeType;
+ break;
+ }
+ }
+ return match_level;
+}
+
+bool FiltersHaveOverlap(const apps::mojom::IntentFilterPtr& filter1,
+ const apps::mojom::IntentFilterPtr& filter2) {
+ if (GetFilterMatchLevel(filter1) != GetFilterMatchLevel(filter2)) {
+ return false;
+ }
+ for (size_t i = 0; i < filter1->conditions.size(); i++) {
+ auto& condition1 = filter1->conditions[i];
+ auto& condition2 = filter2->conditions[i];
+ if (!ConditionsHaveOverlap(condition1, condition2)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace apps_util
diff --git a/chromium/components/services/app_service/public/cpp/intent_filter_util.h b/chromium/components/services/app_service/public/cpp/intent_filter_util.h
new file mode 100644
index 00000000000..49befcb9988
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/intent_filter_util.h
@@ -0,0 +1,81 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_FILTER_UTIL_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_FILTER_UTIL_H_
+
+// Utility functions for creating an App Service intent filter.
+
+#include <string>
+
+#include "base/macros.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+#include "url/gurl.h"
+
+namespace apps_util {
+
+// The concept of match level is taken from Android. The values are not
+// necessary the same.
+// See
+// https://developer.android.com/reference/android/content/IntentFilter.html#constants_2
+// for more details.
+enum IntentFilterMatchLevel {
+ kNone = 0,
+ kScheme = 1,
+ kHost = 2,
+ kPattern = 4,
+ kMimeType = 8,
+};
+
+// Creates condition value that makes up App Service intent filter
+// condition. Each condition contains a list of condition values.
+// For pattern type of condition, the value match will be based on the
+// |pattern_match_type| match type. If the |pattern_match_type| is kNone,
+// then an exact match with the value will be required.
+apps::mojom::ConditionValuePtr MakeConditionValue(
+ const std::string& value,
+ apps::mojom::PatternMatchType pattern_match_type);
+
+// Creates condition that makes up App Service intent filter. Each
+// intent filter contains a list of conditions with different
+// condition types. Each condition contains a list of |condition_values|.
+// For one condition, if the value matches one of the |condition_values|,
+// then this condition is matched.
+apps::mojom::ConditionPtr MakeCondition(
+ apps::mojom::ConditionType condition_type,
+ std::vector<apps::mojom::ConditionValuePtr> condition_values);
+
+// Creates condition that only contain one value and add the condition to
+// the intent filter.
+void AddSingleValueCondition(apps::mojom::ConditionType condition_type,
+ const std::string& value,
+ apps::mojom::PatternMatchType pattern_match_type,
+ apps::mojom::IntentFilterPtr& intent_filter);
+
+// Create intent filter for URL scope, with prefix matching only for the path.
+// e.g. filter created for https://www.google.com/ will match any URL that
+// started with https://www.google.com/*. If |with_action_view| is true, the
+// intent filter created will contain the VIEW action, otherwise no action will
+// be added.
+
+// TODO(crbug.com/1092784): Update/add all related unit tests to test with
+// action view.
+apps::mojom::IntentFilterPtr CreateIntentFilterForUrlScope(
+ const GURL& url,
+ bool with_action_view = false);
+
+// Get the |intent_filter| match level. The higher the return value, the better
+// the match is. For example, an filter with scheme, host and path is better
+// match compare with filter with only scheme. Each condition type has a
+// matching level value, and this function will return the sum of the matching
+// level values of all existing condition types.
+int GetFilterMatchLevel(const apps::mojom::IntentFilterPtr& intent_filter);
+
+// Check if the two intent filters have overlap. i.e. they can handle same
+// intent with same match level.
+bool FiltersHaveOverlap(const apps::mojom::IntentFilterPtr& filter1,
+ const apps::mojom::IntentFilterPtr& filter2);
+} // namespace apps_util
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_FILTER_UTIL_H_
diff --git a/chromium/components/services/app_service/public/cpp/intent_test_util.cc b/chromium/components/services/app_service/public/cpp/intent_test_util.cc
new file mode 100644
index 00000000000..11604670896
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/intent_test_util.cc
@@ -0,0 +1,49 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/intent_test_util.h"
+
+#include <utility>
+#include <vector>
+
+#include "components/services/app_service/public/cpp/intent_filter_util.h"
+
+namespace apps_util {
+
+apps::mojom::IntentFilterPtr CreateSchemeOnlyFilter(const std::string& scheme) {
+ std::vector<apps::mojom::ConditionValuePtr> condition_values;
+ condition_values.push_back(apps_util::MakeConditionValue(
+ scheme, apps::mojom::PatternMatchType::kNone));
+ auto condition = apps_util::MakeCondition(apps::mojom::ConditionType::kScheme,
+ std::move(condition_values));
+
+ auto intent_filter = apps::mojom::IntentFilter::New();
+ intent_filter->conditions.push_back(std::move(condition));
+
+ return intent_filter;
+}
+
+apps::mojom::IntentFilterPtr CreateSchemeAndHostOnlyFilter(
+ const std::string& scheme,
+ const std::string& host) {
+ std::vector<apps::mojom::ConditionValuePtr> scheme_condition_values;
+ scheme_condition_values.push_back(apps_util::MakeConditionValue(
+ scheme, apps::mojom::PatternMatchType::kNone));
+ auto scheme_condition = apps_util::MakeCondition(
+ apps::mojom::ConditionType::kScheme, std::move(scheme_condition_values));
+
+ std::vector<apps::mojom::ConditionValuePtr> host_condition_values;
+ host_condition_values.push_back(apps_util::MakeConditionValue(
+ host, apps::mojom::PatternMatchType::kNone));
+ auto host_condition = apps_util::MakeCondition(
+ apps::mojom::ConditionType::kHost, std::move(host_condition_values));
+
+ auto intent_filter = apps::mojom::IntentFilter::New();
+ intent_filter->conditions.push_back(std::move(scheme_condition));
+ intent_filter->conditions.push_back(std::move(host_condition));
+
+ return intent_filter;
+}
+
+} // namespace apps_util
diff --git a/chromium/components/services/app_service/public/cpp/intent_test_util.h b/chromium/components/services/app_service/public/cpp/intent_test_util.h
new file mode 100644
index 00000000000..c85021b5d85
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/intent_test_util.h
@@ -0,0 +1,24 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_TEST_UTIL_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_TEST_UTIL_H_
+
+#include <string>
+
+#include "components/services/app_service/public/mojom/types.mojom.h"
+
+namespace apps_util {
+
+// Create intent filter that contains only the |scheme|.
+apps::mojom::IntentFilterPtr CreateSchemeOnlyFilter(const std::string& scheme);
+
+// Create intent filter that contains only the |scheme| and |host|.
+apps::mojom::IntentFilterPtr CreateSchemeAndHostOnlyFilter(
+ const std::string& scheme,
+ const std::string& host);
+
+} // namespace apps_util
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_TEST_UTIL_H_
diff --git a/chromium/components/services/app_service/public/cpp/intent_util.cc b/chromium/components/services/app_service/public/cpp/intent_util.cc
index fe37332d916..bed86d091a4 100644
--- a/chromium/components/services/app_service/public/cpp/intent_util.cc
+++ b/chromium/components/services/app_service/public/cpp/intent_util.cc
@@ -5,12 +5,135 @@
#include "components/services/app_service/public/cpp/intent_util.h"
#include "base/compiler_specific.h"
+#include "base/optional.h"
+#include "base/strings/string_split.h"
+#include "base/strings/string_util.h"
+
+namespace {
+
+// Get the intent condition value based on the condition type.
+base::Optional<std::string> GetIntentConditionValueByType(
+ apps::mojom::ConditionType condition_type,
+ const apps::mojom::IntentPtr& intent) {
+ switch (condition_type) {
+ case apps::mojom::ConditionType::kAction:
+ return intent->action;
+ case apps::mojom::ConditionType::kScheme:
+ return intent->url.has_value()
+ ? base::Optional<std::string>(intent->url->scheme())
+ : base::nullopt;
+ case apps::mojom::ConditionType::kHost:
+ return intent->url.has_value()
+ ? base::Optional<std::string>(intent->url->host())
+ : base::nullopt;
+ case apps::mojom::ConditionType::kPattern:
+ return intent->url.has_value()
+ ? base::Optional<std::string>(intent->url->path())
+ : base::nullopt;
+ case apps::mojom::ConditionType::kMimeType:
+ return intent->mime_type;
+ }
+}
+
+bool ComponentMatched(const std::string& component1,
+ const std::string& component2) {
+ const char kWildCardAny[] = "*";
+ return component1 == kWildCardAny || component2 == kWildCardAny ||
+ component1 == component2;
+}
+
+// TODO(crbug.com/1092784): Handle file path with extension with mime type.
+bool MimeTypeMatched(const std::string& mime_type1,
+ const std::string& mime_type2) {
+ const char kMimeTypeSeparator[] = "/";
+
+ std::vector<std::string> components1 =
+ base::SplitString(mime_type1, kMimeTypeSeparator, base::TRIM_WHITESPACE,
+ base::SPLIT_WANT_NONEMPTY);
+
+ std::vector<std::string> components2 =
+ base::SplitString(mime_type2, kMimeTypeSeparator, base::TRIM_WHITESPACE,
+ base::SPLIT_WANT_NONEMPTY);
+
+ const size_t kMimeTypeComponentSize = 2;
+ if (components1.size() != kMimeTypeComponentSize ||
+ components2.size() != kMimeTypeComponentSize) {
+ return false;
+ }
+
+ // Both intent and intent filter can use wildcard for mime type.
+ for (size_t i = 0; i < kMimeTypeComponentSize; i++) {
+ if (!ComponentMatched(components1[i], components2[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+} // namespace
namespace apps_util {
+const char kIntentActionView[] = "view";
+const char kIntentActionSend[] = "send";
+const char kIntentActionSendMultiple[] = "send_multiple";
+
+apps::mojom::IntentPtr CreateIntentFromUrl(const GURL& url) {
+ auto intent = apps::mojom::Intent::New();
+ intent->action = kIntentActionView;
+ intent->url = url;
+ return intent;
+}
+
+bool ConditionValueMatches(
+ const std::string& value,
+ const apps::mojom::ConditionValuePtr& condition_value) {
+ switch (condition_value->match_type) {
+ // Fallthrough as kNone and kLiteral has same matching type.
+ case apps::mojom::PatternMatchType::kNone:
+ case apps::mojom::PatternMatchType::kLiteral:
+ return value == condition_value->value;
+ case apps::mojom::PatternMatchType::kPrefix:
+ return base::StartsWith(value, condition_value->value,
+ base::CompareCase::INSENSITIVE_ASCII);
+ case apps::mojom::PatternMatchType::kGlob:
+ return MatchGlob(value, condition_value->value);
+ case apps::mojom::PatternMatchType::kMimeType:
+ return MimeTypeMatched(value, condition_value->value);
+ }
+}
+
+bool IntentMatchesCondition(const apps::mojom::IntentPtr& intent,
+ const apps::mojom::ConditionPtr& condition) {
+ base::Optional<std::string> value_to_match =
+ GetIntentConditionValueByType(condition->condition_type, intent);
+ if (!value_to_match.has_value()) {
+ return false;
+ }
+ for (const auto& condition_value : condition->condition_values) {
+ if (ConditionValueMatches(value_to_match.value(), condition_value)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool IntentMatchesFilter(const apps::mojom::IntentPtr& intent,
+ const apps::mojom::IntentFilterPtr& filter) {
+ // Intent matches with this intent filter when all of the existing conditions
+ // match.
+ for (const auto& condition : filter->conditions) {
+ if (!IntentMatchesCondition(intent, condition)) {
+ return false;
+ }
+ }
+ return true;
+}
+
// TODO(crbug.com/853604): For glob match, it is currently only for Android
// intent filters, so we will use the ARC intent filter implementation that is
// transcribed from Android codebase, to prevent divergence from Android code.
+// This is now also used for mime type matching.
bool MatchGlob(const std::string& value, const std::string& pattern) {
#define GET_CHAR(s, i) ((UNLIKELY(i >= s.length())) ? '\0' : s[i])
diff --git a/chromium/components/services/app_service/public/cpp/intent_util.h b/chromium/components/services/app_service/public/cpp/intent_util.h
index acd63593307..36be2ff7520 100644
--- a/chromium/components/services/app_service/public/cpp/intent_util.h
+++ b/chromium/components/services/app_service/public/cpp/intent_util.h
@@ -6,15 +6,37 @@
#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_UTIL_H_
// Utility functions for App Service intent handling.
-// This function is needed for both components/arc and
-// chrome/services/app_service at the moment. We are planning to remove the need
-// for this in the components/arc directory and this function can be combined
-// with other intent utility functions for the App service.
#include <string>
+#include "base/macros.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+#include "url/gurl.h"
+
namespace apps_util {
+extern const char kIntentActionView[];
+extern const char kIntentActionSend[];
+extern const char kIntentActionSendMultiple[];
+
+// Create an intent struct from URL.
+apps::mojom::IntentPtr CreateIntentFromUrl(const GURL& url);
+
+// Return true if |value| matches with the |condition_value|, based on the
+// pattern match type in the |condition_value|.
+bool ConditionValueMatches(
+ const std::string& value,
+ const apps::mojom::ConditionValuePtr& condition_value);
+
+// Return true if |intent| matches with any of the values in |condition|.
+bool IntentMatchesCondition(const apps::mojom::IntentPtr& intent,
+ const apps::mojom::ConditionPtr& condition);
+
+// Return true if a |filter| matches an |intent|. This is true when intent
+// matches all existing conditions in the filter.
+bool IntentMatchesFilter(const apps::mojom::IntentPtr& intent,
+ const apps::mojom::IntentFilterPtr& filter);
+
// Return true if |value| matches |pattern| with simple glob syntax.
// In this syntax, you can use the '*' character to match against zero or
// more occurrences of the character immediately before. If the character
@@ -25,7 +47,6 @@ namespace apps_util {
// See
// https://android.googlesource.com/platform/frameworks/base.git/+/e93165456c3c28278f275566bd90bfbcf1a0e5f7/core/java/android/os/PatternMatcher.java#186
bool MatchGlob(const std::string& value, const std::string& pattern);
-
} // namespace apps_util
-#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_UTIL_H_ \ No newline at end of file
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_INTENT_UTIL_H_
diff --git a/chromium/components/services/app_service/public/cpp/intent_util_unittest.cc b/chromium/components/services/app_service/public/cpp/intent_util_unittest.cc
new file mode 100644
index 00000000000..4e2afbe2e39
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/intent_util_unittest.cc
@@ -0,0 +1,268 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/intent_util.h"
+
+#include "components/services/app_service/public/cpp/intent_filter_util.h"
+#include "components/services/app_service/public/cpp/intent_test_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+const char kFilterUrl[] = "https://www.google.com/";
+}
+
+class IntentUtilTest : public testing::Test {
+ protected:
+ apps::mojom::ConditionPtr CreateMultiConditionValuesCondition() {
+ std::vector<apps::mojom::ConditionValuePtr> condition_values;
+ condition_values.push_back(apps_util::MakeConditionValue(
+ "https", apps::mojom::PatternMatchType::kNone));
+ condition_values.push_back(apps_util::MakeConditionValue(
+ "http", apps::mojom::PatternMatchType::kNone));
+ auto condition = apps_util::MakeCondition(
+ apps::mojom::ConditionType::kScheme, std::move(condition_values));
+ return condition;
+ }
+
+ apps::mojom::IntentFilterPtr CreateIntentFilterForShareTarget(
+ const std::string& mime_type) {
+ auto intent_filter = apps::mojom::IntentFilter::New();
+
+ apps_util::AddSingleValueCondition(
+ apps::mojom::ConditionType::kAction, apps_util::kIntentActionSend,
+ apps::mojom::PatternMatchType::kNone, intent_filter);
+
+ apps_util::AddSingleValueCondition(
+ apps::mojom::ConditionType::kMimeType, mime_type,
+ apps::mojom::PatternMatchType::kMimeType, intent_filter);
+
+ return intent_filter;
+ }
+
+ // TODO(crbug.com/1092784): Add other things for a completed intent.
+ apps::mojom::IntentPtr CreateShareIntent(const std::string& mime_type) {
+ auto intent = apps::mojom::Intent::New();
+ intent->action = apps_util::kIntentActionSend;
+ intent->mime_type = mime_type;
+ return intent;
+ }
+};
+
+TEST_F(IntentUtilTest, AllConditionMatches) {
+ GURL test_url = GURL("https://www.google.com/");
+ auto intent = apps_util::CreateIntentFromUrl(test_url);
+ auto intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL(kFilterUrl));
+
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent, intent_filter));
+}
+
+TEST_F(IntentUtilTest, OneConditionDoesnotMatch) {
+ GURL test_url = GURL("https://www.abc.com/");
+ auto intent = apps_util::CreateIntentFromUrl(test_url);
+ auto intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL(kFilterUrl));
+
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(intent, intent_filter));
+}
+
+TEST_F(IntentUtilTest, IntentDoesnotHaveValueToMatch) {
+ GURL test_url = GURL("www.abc.com/");
+ auto intent = apps_util::CreateIntentFromUrl(test_url);
+ auto intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL(kFilterUrl));
+
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(intent, intent_filter));
+}
+
+// Test ConditionMatch with more then one condition values.
+
+TEST_F(IntentUtilTest, OneConditionValueMatch) {
+ auto condition = CreateMultiConditionValuesCondition();
+ GURL test_url = GURL("https://www.google.com/");
+ auto intent = apps_util::CreateIntentFromUrl(test_url);
+ EXPECT_TRUE(apps_util::IntentMatchesCondition(intent, condition));
+}
+
+TEST_F(IntentUtilTest, NoneConditionValueMathc) {
+ auto condition = CreateMultiConditionValuesCondition();
+ GURL test_url = GURL("tel://www.google.com/");
+ auto intent = apps_util::CreateIntentFromUrl(test_url);
+ EXPECT_FALSE(apps_util::IntentMatchesCondition(intent, condition));
+}
+
+// Test Condition Value match with different pattern match type.
+TEST_F(IntentUtilTest, NoneMatchType) {
+ auto condition_value = apps_util::MakeConditionValue(
+ "https", apps::mojom::PatternMatchType::kNone);
+ EXPECT_TRUE(apps_util::ConditionValueMatches("https", condition_value));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("http", condition_value));
+}
+TEST_F(IntentUtilTest, LiteralMatchType) {
+ auto condition_value = apps_util::MakeConditionValue(
+ "https", apps::mojom::PatternMatchType::kLiteral);
+ EXPECT_TRUE(apps_util::ConditionValueMatches("https", condition_value));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("http", condition_value));
+}
+TEST_F(IntentUtilTest, PrefixMatchType) {
+ auto condition_value = apps_util::MakeConditionValue(
+ "/ab", apps::mojom::PatternMatchType::kPrefix);
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/abc", condition_value));
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/ABC", condition_value));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/d", condition_value));
+}
+
+TEST_F(IntentUtilTest, GlobMatchType) {
+ auto condition_value_star = apps_util::MakeConditionValue(
+ "/a*b", apps::mojom::PatternMatchType::kGlob);
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/b", condition_value_star));
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/ab", condition_value_star));
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/aab", condition_value_star));
+ EXPECT_TRUE(
+ apps_util::ConditionValueMatches("/aaaaaab", condition_value_star));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/aabb", condition_value_star));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/aabc", condition_value_star));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/bb", condition_value_star));
+
+ auto condition_value_dot = apps_util::MakeConditionValue(
+ "/a.b", apps::mojom::PatternMatchType::kGlob);
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/aab", condition_value_dot));
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/acb", condition_value_dot));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/ab", condition_value_dot));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/abd", condition_value_dot));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/abbd", condition_value_dot));
+
+ auto condition_value_dot_and_star = apps_util::MakeConditionValue(
+ "/a.*b", apps::mojom::PatternMatchType::kGlob);
+ EXPECT_TRUE(
+ apps_util::ConditionValueMatches("/aab", condition_value_dot_and_star));
+ EXPECT_TRUE(apps_util::ConditionValueMatches("/aadsfadslkjb",
+ condition_value_dot_and_star));
+ EXPECT_TRUE(
+ apps_util::ConditionValueMatches("/ab", condition_value_dot_and_star));
+
+ // This arguably should be true, however the algorithm is transcribed from the
+ // upstream Android codebase, which behaves like this.
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/abasdfab",
+ condition_value_dot_and_star));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/abasdfad",
+ condition_value_dot_and_star));
+ EXPECT_FALSE(apps_util::ConditionValueMatches("/bbasdfab",
+ condition_value_dot_and_star));
+ EXPECT_FALSE(
+ apps_util::ConditionValueMatches("/a", condition_value_dot_and_star));
+ EXPECT_FALSE(
+ apps_util::ConditionValueMatches("/b", condition_value_dot_and_star));
+
+ auto condition_value_escape_dot = apps_util::MakeConditionValue(
+ "/a\\.b", apps::mojom::PatternMatchType::kGlob);
+ EXPECT_TRUE(
+ apps_util::ConditionValueMatches("/a.b", condition_value_escape_dot));
+
+ // This arguably should be false, however the transcribed is carried from the
+ // upstream Android codebase, which behaves like this.
+ EXPECT_TRUE(
+ apps_util::ConditionValueMatches("/acb", condition_value_escape_dot));
+
+ auto condition_value_escape_star = apps_util::MakeConditionValue(
+ "/a\\*b", apps::mojom::PatternMatchType::kGlob);
+ EXPECT_TRUE(
+ apps_util::ConditionValueMatches("/a*b", condition_value_escape_star));
+ EXPECT_FALSE(
+ apps_util::ConditionValueMatches("/acb", condition_value_escape_star));
+}
+
+TEST_F(IntentUtilTest, FilterMatchLevel) {
+ auto filter_scheme_only = apps_util::CreateSchemeOnlyFilter("http");
+ auto filter_scheme_and_host_only =
+ apps_util::CreateSchemeAndHostOnlyFilter("https", "www.abc.com");
+ auto filter_url = apps_util::CreateIntentFilterForUrlScope(
+ GURL("https:://www.google.com/"));
+ auto filter_empty = apps::mojom::IntentFilter::New();
+
+ EXPECT_EQ(apps_util::GetFilterMatchLevel(filter_url),
+ apps_util::IntentFilterMatchLevel::kScheme +
+ apps_util::IntentFilterMatchLevel::kHost +
+ apps_util::IntentFilterMatchLevel::kPattern);
+ EXPECT_EQ(apps_util::GetFilterMatchLevel(filter_scheme_and_host_only),
+ apps_util::IntentFilterMatchLevel::kScheme +
+ apps_util::IntentFilterMatchLevel::kHost);
+ EXPECT_EQ(apps_util::GetFilterMatchLevel(filter_scheme_only),
+ apps_util::IntentFilterMatchLevel::kScheme);
+ EXPECT_EQ(apps_util::GetFilterMatchLevel(filter_empty),
+ apps_util::IntentFilterMatchLevel::kNone);
+
+ EXPECT_TRUE(apps_util::GetFilterMatchLevel(filter_url) >
+ apps_util::GetFilterMatchLevel(filter_scheme_and_host_only));
+ EXPECT_TRUE(apps_util::GetFilterMatchLevel(filter_scheme_and_host_only) >
+ apps_util::GetFilterMatchLevel(filter_scheme_only));
+ EXPECT_TRUE(apps_util::GetFilterMatchLevel(filter_scheme_only) >
+ apps_util::GetFilterMatchLevel(filter_empty));
+}
+
+TEST_F(IntentUtilTest, ActionMatch) {
+ GURL test_url = GURL("https://www.google.com/");
+ auto intent = apps_util::CreateIntentFromUrl(test_url);
+ auto intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL(kFilterUrl),
+ /*with_action_view=*/true);
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent, intent_filter));
+
+ auto send_intent = apps_util::CreateIntentFromUrl(test_url);
+ send_intent->action = apps_util::kIntentActionSend;
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(send_intent, intent_filter));
+
+ auto send_intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL(kFilterUrl),
+ /*with_action_view=*/true);
+ send_intent_filter->conditions[0]->condition_values[0]->value =
+ apps_util::kIntentActionSend;
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(intent, send_intent_filter));
+}
+
+TEST_F(IntentUtilTest, MimeTypeMatch) {
+ std::string mime_type1 = "text/plain";
+ std::string mime_type2 = "image/jpeg";
+ std::string mime_type_sub_wildcard = "text/*";
+ std::string mime_type_all_wildcard = "*/*";
+
+ auto intent1 = CreateShareIntent(mime_type1);
+ auto intent2 = CreateShareIntent(mime_type2);
+ auto intent_sub_wildcard = CreateShareIntent(mime_type_sub_wildcard);
+ auto intent_all_wildcard = CreateShareIntent(mime_type_all_wildcard);
+
+ auto filter1 = CreateIntentFilterForShareTarget(mime_type1);
+
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent1, filter1));
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(intent2, filter1));
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent_sub_wildcard, filter1));
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent_all_wildcard, filter1));
+
+ auto filter2 = CreateIntentFilterForShareTarget(mime_type2);
+
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(intent1, filter2));
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent2, filter2));
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(intent_sub_wildcard, filter2));
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent_all_wildcard, filter2));
+
+ auto filter_sub_wildcard =
+ CreateIntentFilterForShareTarget(mime_type_sub_wildcard);
+
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent1, filter_sub_wildcard));
+ EXPECT_FALSE(apps_util::IntentMatchesFilter(intent2, filter_sub_wildcard));
+ EXPECT_TRUE(
+ apps_util::IntentMatchesFilter(intent_sub_wildcard, filter_sub_wildcard));
+ EXPECT_TRUE(
+ apps_util::IntentMatchesFilter(intent_all_wildcard, filter_sub_wildcard));
+
+ auto filter_all_wildcard =
+ CreateIntentFilterForShareTarget(mime_type_all_wildcard);
+
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent1, filter_all_wildcard));
+ EXPECT_TRUE(apps_util::IntentMatchesFilter(intent2, filter_all_wildcard));
+ EXPECT_TRUE(
+ apps_util::IntentMatchesFilter(intent_sub_wildcard, filter_all_wildcard));
+ EXPECT_TRUE(
+ apps_util::IntentMatchesFilter(intent_all_wildcard, filter_all_wildcard));
+}
diff --git a/chromium/components/services/app_service/public/cpp/preferred_apps_converter.cc b/chromium/components/services/app_service/public/cpp/preferred_apps_converter.cc
new file mode 100644
index 00000000000..641a60d93e1
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/preferred_apps_converter.cc
@@ -0,0 +1,164 @@
+// Copyright 2020 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 <memory>
+#include <utility>
+
+#include "components/services/app_service/public/cpp/preferred_apps_converter.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+
+namespace {
+
+base::Value ConvertConditionValueToValue(
+ const apps::mojom::ConditionValuePtr& condition_value) {
+ base::Value condition_value_dict(base::Value::Type::DICTIONARY);
+ condition_value_dict.SetStringKey(apps::kValueKey, condition_value->value);
+ condition_value_dict.SetIntKey(apps::kMatchTypeKey,
+ static_cast<int>(condition_value->match_type));
+ return condition_value_dict;
+}
+
+base::Value ConvertConditionToValue(
+ const apps::mojom::ConditionPtr& condition) {
+ base::Value condition_dict(base::Value::Type::DICTIONARY);
+ condition_dict.SetIntKey(apps::kConditionTypeKey,
+ static_cast<int>(condition->condition_type));
+ base::Value condition_values_list(base::Value::Type::LIST);
+ for (auto& condition_value : condition->condition_values) {
+ condition_values_list.Append(ConvertConditionValueToValue(condition_value));
+ }
+ condition_dict.SetKey(apps::kConditionValuesKey,
+ std::move(condition_values_list));
+ return condition_dict;
+}
+
+base::Value ConvertIntentFilterToValue(
+ const apps::mojom::IntentFilterPtr& intent_filter) {
+ base::Value intent_filter_value(base::Value::Type::LIST);
+ for (auto& condition : intent_filter->conditions) {
+ intent_filter_value.Append(ConvertConditionToValue(condition));
+ }
+ return intent_filter_value;
+}
+
+apps::mojom::ConditionValuePtr ParseValueToConditionValue(
+ const base::Value& value) {
+ auto* value_string = value.FindStringKey(apps::kValueKey);
+ if (!value_string) {
+ DVLOG(0) << "Fail to parse condition value. Cannot find \""
+ << apps::kValueKey << "\" key with string value.";
+ return nullptr;
+ }
+ auto condition_value = apps::mojom::ConditionValue::New();
+ condition_value->value = *value_string;
+ auto match_type = value.FindIntKey(apps::kMatchTypeKey);
+ if (!match_type.has_value()) {
+ DVLOG(0) << "Fail to parse condition value. Cannot find \""
+ << apps::kMatchTypeKey << "\" key with int value.";
+ return nullptr;
+ }
+ condition_value->match_type =
+ static_cast<apps::mojom::PatternMatchType>(match_type.value());
+ return condition_value;
+}
+
+apps::mojom::ConditionPtr ParseValueToCondition(const base::Value& value) {
+ auto condition_type = value.FindIntKey(apps::kConditionTypeKey);
+ if (!condition_type.has_value()) {
+ DVLOG(0) << "Fail to parse condition. Cannot find \""
+ << apps::kConditionTypeKey << "\" key with int value.";
+ return nullptr;
+ }
+ auto condition = apps::mojom::Condition::New();
+ condition->condition_type =
+ static_cast<apps::mojom::ConditionType>(condition_type.value());
+
+ auto* condition_values = value.FindKey(apps::kConditionValuesKey);
+ if (!condition_values || !condition_values->is_list()) {
+ DVLOG(0) << "Fail to parse condition. Cannot find \""
+ << apps::kConditionValuesKey << "\" key with list value.";
+ return nullptr;
+ }
+ for (auto& condition_value : condition_values->GetList()) {
+ auto parsed_condition_value = ParseValueToConditionValue(condition_value);
+ if (!parsed_condition_value) {
+ DVLOG(0) << "Fail to parse condition. Cannot parse condition values";
+ return nullptr;
+ }
+ condition->condition_values.push_back(std::move(parsed_condition_value));
+ }
+ return condition;
+}
+
+apps::mojom::IntentFilterPtr ParseValueToIntentFilter(
+ const base::Value* value) {
+ if (!value || !value->is_list()) {
+ DVLOG(0) << "Fail to parse intent filter. Cannot find the conditions list.";
+ return nullptr;
+ }
+ auto intent_filter = apps::mojom::IntentFilter::New();
+ for (auto& condition : value->GetList()) {
+ auto parsed_condition = ParseValueToCondition(condition);
+ if (!parsed_condition) {
+ DVLOG(0) << "Fail to parse intent filter. Cannot parse conditions.";
+ return nullptr;
+ }
+ intent_filter->conditions.push_back(std::move(parsed_condition));
+ }
+ return intent_filter;
+}
+
+} // namespace
+
+namespace apps {
+
+const char kConditionTypeKey[] = "condition_type";
+const char kConditionValuesKey[] = "condition_values";
+const char kValueKey[] = "value";
+const char kMatchTypeKey[] = "match_type";
+const char kAppIdKey[] = "app_id";
+const char kIntentFilterKey[] = "intent_filter";
+
+base::Value ConvertPreferredAppsToValue(
+ const PreferredAppsList::PreferredApps& preferred_apps) {
+ base::Value preferred_apps_value(base::Value::Type::LIST);
+ for (auto& preferred_app : preferred_apps) {
+ base::Value preferred_app_dict(base::Value::Type::DICTIONARY);
+ preferred_app_dict.SetKey(
+ kIntentFilterKey,
+ ConvertIntentFilterToValue(preferred_app->intent_filter));
+ preferred_app_dict.SetStringKey(kAppIdKey, preferred_app->app_id);
+ preferred_apps_value.Append(std::move(preferred_app_dict));
+ }
+ return preferred_apps_value;
+}
+
+PreferredAppsList::PreferredApps ParseValueToPreferredApps(
+ const base::Value& preferred_apps_value) {
+ if (!preferred_apps_value.is_list()) {
+ DVLOG(0) << "Fail to parse preferred apps. Cannot the preferred app list.";
+ return PreferredAppsList::PreferredApps();
+ }
+ PreferredAppsList::PreferredApps preferred_apps;
+ for (auto& entry : preferred_apps_value.GetList()) {
+ auto* app_id = entry.FindStringKey(kAppIdKey);
+ if (!app_id) {
+ DVLOG(0) << "Fail to parse condition value. Cannot find \""
+ << apps::kAppIdKey << "\" key with string value.";
+ return PreferredAppsList::PreferredApps();
+ }
+ auto parsed_intent_filter =
+ ParseValueToIntentFilter(entry.FindKey(kIntentFilterKey));
+ if (!parsed_intent_filter) {
+ DVLOG(0) << "Fail to parse condition value. Cannot parse intent filter.";
+ return PreferredAppsList::PreferredApps();
+ }
+ auto new_preferred_app = apps::mojom::PreferredApp::New(
+ std::move(parsed_intent_filter), *app_id);
+ preferred_apps.push_back(std::move(new_preferred_app));
+ }
+ return preferred_apps;
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/preferred_apps_converter.h b/chromium/components/services/app_service/public/cpp/preferred_apps_converter.h
new file mode 100644
index 00000000000..15c3bfae406
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/preferred_apps_converter.h
@@ -0,0 +1,54 @@
+// Copyright 2020 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PREFERRED_APPS_CONVERTER_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PREFERRED_APPS_CONVERTER_H_
+
+#include "base/values.h"
+#include "components/services/app_service/public/cpp/preferred_apps_list.h"
+
+namespace apps {
+
+extern const char kConditionTypeKey[];
+extern const char kConditionValuesKey[];
+extern const char kValueKey[];
+extern const char kMatchTypeKey[];
+extern const char kAppIdKey[];
+extern const char kIntentFilterKey[];
+
+// Convert the PreferredAppsList struct to base::Value to write to JSON file.
+// e.g. for preferred app with |app_id| "abcdefg", and |intent_filter| for url
+// https://www.google.com/abc.
+// The converted base::Value format will be:
+//[ {"app_id": "abcdefg",
+// "intent_filter": [ {
+// "condition_type": 0,
+// "condition_values": [ {
+// "match_type": 0,
+// "value": "https"
+// } ]
+// }, {
+// "condition_type": 1,
+// "condition_values": [ {
+// "match_type": 0,
+// "value": "www.google.com"
+// } ]
+// }, {
+// "condition_type": 2,
+// "condition_values": [ {
+// "match_type": 2,
+// "value": "/abc"
+// } ]
+// } ]
+// } ]
+base::Value ConvertPreferredAppsToValue(
+ const PreferredAppsList::PreferredApps& preferred_apps);
+
+// Parse the base::Value read from JSON file back to preferred apps struct.
+PreferredAppsList::PreferredApps ParseValueToPreferredApps(
+ const base::Value& preferred_apps_value);
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PREFERRED_APPS_CONVERTER_H_
diff --git a/chromium/components/services/app_service/public/cpp/preferred_apps_converter_unittest.cc b/chromium/components/services/app_service/public/cpp/preferred_apps_converter_unittest.cc
new file mode 100644
index 00000000000..a9d572ce871
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/preferred_apps_converter_unittest.cc
@@ -0,0 +1,471 @@
+// Copyright 2020 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 "components/services/app_service/public/cpp/preferred_apps_converter.h"
+
+#include "base/json/json_reader.h"
+#include "components/services/app_service/public/cpp/intent_filter_util.h"
+#include "components/services/app_service/public/cpp/intent_test_util.h"
+#include "components/services/app_service/public/cpp/intent_util.h"
+#include "components/services/app_service/public/cpp/preferred_apps_list.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const char kAppId1[] = "abcdefg";
+
+} // namespace
+
+class PreferredAppsConverterTest : public testing::Test {};
+
+// Test one simple entry with simple filter.
+TEST_F(PreferredAppsConverterTest, ConvertSimpleEntry) {
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+
+ apps::PreferredAppsList preferred_apps;
+ preferred_apps.Init();
+ preferred_apps.AddPreferredApp(kAppId1, intent_filter);
+ auto converted_value =
+ apps::ConvertPreferredAppsToValue(preferred_apps.GetReference());
+
+ // Check that each entry is correct.
+ ASSERT_EQ(1u, converted_value.GetList().size());
+ auto& entry = converted_value.GetList()[0];
+ EXPECT_EQ(kAppId1, *entry.FindStringKey(apps::kAppIdKey));
+
+ auto* converted_intent_filter = entry.FindKey(apps::kIntentFilterKey);
+ ASSERT_EQ(intent_filter->conditions.size(),
+ converted_intent_filter->GetList().size());
+
+ for (size_t i = 0; i < intent_filter->conditions.size(); i++) {
+ auto& condition = intent_filter->conditions[i];
+ auto& converted_condition = converted_intent_filter->GetList()[i];
+ auto& condition_values = condition->condition_values;
+ auto converted_condition_values =
+ converted_condition.FindKey(apps::kConditionValuesKey)->GetList();
+
+ EXPECT_EQ(static_cast<int>(condition->condition_type),
+ converted_condition.FindIntKey(apps::kConditionTypeKey));
+ ASSERT_EQ(1u, converted_condition_values.size());
+ EXPECT_EQ(condition_values[0]->value,
+ *converted_condition_values[0].FindStringKey(apps::kValueKey));
+ EXPECT_EQ(static_cast<int>(condition_values[0]->match_type),
+ converted_condition_values[0].FindIntKey(apps::kMatchTypeKey));
+ }
+
+ auto preferred_apps_list = apps::ParseValueToPreferredApps(converted_value);
+ preferred_apps.Init();
+ EXPECT_EQ(base::nullopt, preferred_apps.FindPreferredAppForUrl(filter_url));
+ preferred_apps.Init(preferred_apps_list);
+ EXPECT_EQ(kAppId1, preferred_apps.FindPreferredAppForUrl(filter_url));
+ GURL url_wrong_host = GURL("https://www.hahaha.com/");
+ EXPECT_EQ(base::nullopt,
+ preferred_apps.FindPreferredAppForUrl(url_wrong_host));
+}
+
+// Test one simple entry with json string.
+TEST_F(PreferredAppsConverterTest, ConvertSimpleEntryJson) {
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+
+ apps::PreferredAppsList preferred_apps;
+ preferred_apps.Init();
+ preferred_apps.AddPreferredApp(kAppId1, intent_filter);
+ auto converted_value =
+ apps::ConvertPreferredAppsToValue(preferred_apps.GetReference());
+
+ const char expected_output_string[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> expected_output =
+ base::JSONReader::Read(expected_output_string);
+ ASSERT_TRUE(expected_output);
+ EXPECT_EQ(expected_output.value(), converted_value);
+}
+
+// Test parse simple entry from json string.
+TEST_F(PreferredAppsConverterTest, ParseSimpleEntryJson) {
+ const char test_string[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> test_value = base::JSONReader::Read(test_string);
+ ASSERT_TRUE(test_value);
+ auto parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+
+ apps::PreferredAppsList preferred_apps;
+ preferred_apps.Init();
+ preferred_apps.AddPreferredApp(kAppId1, intent_filter);
+ auto& expected_entry = preferred_apps.GetReference();
+
+ EXPECT_EQ(expected_entry, parsed_entry);
+}
+
+TEST_F(PreferredAppsConverterTest, ParseJsonWithInvalidAppId) {
+ // Invalid key.
+ const char test_key[] =
+ "[ {\"app_idd\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> test_value = base::JSONReader::Read(test_key);
+ ASSERT_TRUE(test_value);
+ auto parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+
+ // Invalid value.
+ const char test_string[] =
+ "[ {\"app_id\": 0,"
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ test_value = base::JSONReader::Read(test_string);
+ ASSERT_TRUE(test_value);
+ parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+}
+
+TEST_F(PreferredAppsConverterTest, ParseJsonWithInvalidIntentFilter) {
+ // Invalid key.
+ const char test_key[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filterrr\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> test_value = base::JSONReader::Read(test_key);
+ ASSERT_TRUE(test_value);
+ auto parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+
+ // Invalid value.
+ const char test_string[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": \"not_list\""
+ "} ]";
+ test_value = base::JSONReader::Read(test_string);
+ ASSERT_TRUE(test_value);
+ parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+}
+
+TEST_F(PreferredAppsConverterTest, ParseJsonWithInvalidConditionType) {
+ // Invalid key.
+ const char test_key[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_typeeee\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> test_value = base::JSONReader::Read(test_key);
+ ASSERT_TRUE(test_value);
+ auto parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+
+ // Invalid value.
+ const char test_string[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": \"not_int\","
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ test_value = base::JSONReader::Read(test_string);
+ ASSERT_TRUE(test_value);
+ parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+}
+
+TEST_F(PreferredAppsConverterTest, ParseJsonWithInvalidValues) {
+ // Invalid key.
+ const char test_key[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_valuessss\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> test_value = base::JSONReader::Read(test_key);
+ ASSERT_TRUE(test_value);
+ auto parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+
+ // Invalid value.
+ const char test_string[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": \"not_list\""
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ test_value = base::JSONReader::Read(test_string);
+ ASSERT_TRUE(test_value);
+ parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+}
+
+TEST_F(PreferredAppsConverterTest, ParseJsonWithInvalidMatchType) {
+ // Invalid key.
+ const char test_key[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_typeeeee\": 0,"
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> test_value = base::JSONReader::Read(test_key);
+ ASSERT_TRUE(test_value);
+ auto parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+
+ // Invalid value.
+ const char test_string[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": \"not_int\","
+ " \"value\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ test_value = base::JSONReader::Read(test_string);
+ ASSERT_TRUE(test_value);
+ parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+}
+
+TEST_F(PreferredAppsConverterTest, ParseJsonWithInvalidValue) {
+ // Invalid key.
+ const char test_key[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"valueeeee\": \"https\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ base::Optional<base::Value> test_value = base::JSONReader::Read(test_key);
+ ASSERT_TRUE(test_value);
+ auto parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+
+ // Invalid value.
+ const char test_string[] =
+ "[ {\"app_id\": \"abcdefg\","
+ " \"intent_filter\": [ {"
+ " \"condition_type\": 0,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": {}"
+ " } ]"
+ " }, {"
+ " \"condition_type\": 1,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 0,"
+ " \"value\": \"www.google.com\""
+ " } ]"
+ " }, {"
+ " \"condition_type\": 2,"
+ " \"condition_values\": [ {"
+ " \"match_type\": 2,"
+ " \"value\": \"/abc\""
+ " } ]"
+ " } ]"
+ "} ]";
+ test_value = base::JSONReader::Read(test_string);
+ ASSERT_TRUE(test_value);
+ parsed_entry = apps::ParseValueToPreferredApps(test_value.value());
+ EXPECT_TRUE(parsed_entry.empty());
+}
diff --git a/chromium/components/services/app_service/public/cpp/preferred_apps_list.cc b/chromium/components/services/app_service/public/cpp/preferred_apps_list.cc
new file mode 100644
index 00000000000..709f95829d2
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/preferred_apps_list.cc
@@ -0,0 +1,142 @@
+// Copyright 2020 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 "components/services/app_service/public/cpp/preferred_apps_list.h"
+
+#include <utility>
+
+#include "base/strings/string_util.h"
+#include "components/services/app_service/public/cpp/intent_filter_util.h"
+#include "components/services/app_service/public/cpp/intent_util.h"
+#include "url/gurl.h"
+
+namespace {
+
+void Clone(apps::PreferredAppsList::PreferredApps& source,
+ apps::PreferredAppsList::PreferredApps* destination) {
+ destination->clear();
+ for (auto& preferred_app : source) {
+ destination->push_back(preferred_app->Clone());
+ }
+}
+
+} // namespace
+
+namespace apps {
+
+PreferredAppsList::PreferredAppsList() = default;
+PreferredAppsList::~PreferredAppsList() = default;
+
+base::Optional<std::string> PreferredAppsList::FindPreferredAppForUrl(
+ const GURL& url) {
+ auto intent = apps_util::CreateIntentFromUrl(url);
+ return FindPreferredAppForIntent(intent);
+}
+
+apps::mojom::ReplacedAppPreferencesPtr PreferredAppsList::AddPreferredApp(
+ const std::string& app_id,
+ const apps::mojom::IntentFilterPtr& intent_filter) {
+ auto replaced_app_preferences = apps::mojom::ReplacedAppPreferences::New();
+ auto iter = preferred_apps_.begin();
+ auto& replaced_preference_map = replaced_app_preferences->replaced_preference;
+
+ // Go through the list and see if there are overlapped intent filters in the
+ // list. If there is, add this into the replaced_app_preferences and remove it
+ // from the list.
+ while (iter != preferred_apps_.end()) {
+ if (apps_util::FiltersHaveOverlap((*iter)->intent_filter, intent_filter)) {
+ // Add the to be removed preferred app into a map, key by app_id.
+ const std::string replaced_app_id = (*iter)->app_id;
+ auto entry = replaced_preference_map.find(replaced_app_id);
+ if (entry == replaced_preference_map.end()) {
+ std::vector<apps::mojom::IntentFilterPtr> intent_filter_vector;
+ intent_filter_vector.push_back((*iter)->intent_filter->Clone());
+ replaced_preference_map.emplace(replaced_app_id,
+ std::move(intent_filter_vector));
+ } else {
+ entry->second.push_back((*iter)->intent_filter->Clone());
+ }
+ iter = preferred_apps_.erase(iter);
+ } else {
+ iter++;
+ }
+ }
+ auto new_preferred_app =
+ apps::mojom::PreferredApp::New(intent_filter->Clone(), app_id);
+ preferred_apps_.push_back(std::move(new_preferred_app));
+ return replaced_app_preferences;
+}
+
+void PreferredAppsList::DeletePreferredApp(
+ const std::string& app_id,
+ const apps::mojom::IntentFilterPtr& intent_filter) {
+ // Go through the list and see if there are overlapped intent filters with the
+ // same app id in the list. If there are, delete the entry.
+ auto iter = preferred_apps_.begin();
+ while (iter != preferred_apps_.end()) {
+ if ((*iter)->app_id == app_id &&
+ apps_util::FiltersHaveOverlap((*iter)->intent_filter, intent_filter)) {
+ iter = preferred_apps_.erase(iter);
+ } else {
+ iter++;
+ }
+ }
+}
+
+void PreferredAppsList::DeleteAppId(const std::string& app_id) {
+ auto iter = preferred_apps_.begin();
+ // Go through the list and delete the entry with requested app_id.
+ while (iter != preferred_apps_.end()) {
+ if ((*iter)->app_id == app_id) {
+ iter = preferred_apps_.erase(iter);
+ } else {
+ iter++;
+ }
+ }
+}
+
+void PreferredAppsList::Init() {
+ preferred_apps_ = PreferredApps();
+ initialized_ = true;
+}
+
+void PreferredAppsList::Init(PreferredApps& preferred_apps) {
+ Clone(preferred_apps, &preferred_apps_);
+ initialized_ = true;
+}
+
+PreferredAppsList::PreferredApps PreferredAppsList::GetValue() {
+ PreferredAppsList::PreferredApps preferred_apps_copy;
+ Clone(preferred_apps_, &preferred_apps_copy);
+ return preferred_apps_copy;
+}
+
+bool PreferredAppsList::IsInitialized() {
+ return initialized_;
+}
+
+const PreferredAppsList::PreferredApps& PreferredAppsList::GetReference()
+ const {
+ return preferred_apps_;
+}
+
+base::Optional<std::string> PreferredAppsList::FindPreferredAppForIntent(
+ const apps::mojom::IntentPtr& intent) {
+ base::Optional<std::string> best_match_app_id = base::nullopt;
+ int best_match_level = apps_util::IntentFilterMatchLevel::kNone;
+ for (auto& preferred_app : preferred_apps_) {
+ if (apps_util::IntentMatchesFilter(intent, preferred_app->intent_filter)) {
+ int match_level =
+ apps_util::GetFilterMatchLevel(preferred_app->intent_filter);
+ if (match_level < best_match_level) {
+ continue;
+ }
+ best_match_level = match_level;
+ best_match_app_id = preferred_app->app_id;
+ }
+ }
+ return best_match_app_id;
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/preferred_apps_list.h b/chromium/components/services/app_service/public/cpp/preferred_apps_list.h
new file mode 100644
index 00000000000..5af40fa458c
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/preferred_apps_list.h
@@ -0,0 +1,70 @@
+// Copyright 2020 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PREFERRED_APPS_LIST_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PREFERRED_APPS_LIST_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/optional.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+
+class GURL;
+
+namespace apps {
+
+// The preferred apps set by the user. The preferred apps is stored as
+// an list of |intent_filter| vs. app_id.
+class PreferredAppsList {
+ public:
+ PreferredAppsList();
+ ~PreferredAppsList();
+
+ PreferredAppsList(const PreferredAppsList&) = delete;
+ PreferredAppsList& operator=(const PreferredAppsList&) = delete;
+
+ using PreferredApps = std::vector<apps::mojom::PreferredAppPtr>;
+
+ // Find preferred app id for an |intent|.
+ base::Optional<std::string> FindPreferredAppForIntent(
+ const apps::mojom::IntentPtr& intent);
+
+ // Find preferred app id for an |url|.
+ base::Optional<std::string> FindPreferredAppForUrl(const GURL& url);
+
+ // Add a preferred app for an |intent_filter|, and returns a group of
+ // |app_ids| that is no longer preferred app of their corresponding
+ // |intent_filters|.
+ apps::mojom::ReplacedAppPreferencesPtr AddPreferredApp(
+ const std::string& app_id,
+ const apps::mojom::IntentFilterPtr& intent_filter);
+
+ // Delete a preferred app for an |intent_filter| with the same |app_id|.
+ void DeletePreferredApp(const std::string& app_id,
+ const apps::mojom::IntentFilterPtr& intent_filter);
+
+ // Delete all settings for an |app_id|.
+ void DeleteAppId(const std::string& app_id);
+
+ // Initialize the preferred app with empty list or existing |preferred_apps|;
+ void Init();
+ void Init(PreferredApps& preferred_apps);
+
+ // Get a copy of the preferred apps.
+ PreferredApps GetValue();
+
+ bool IsInitialized();
+
+ const PreferredApps& GetReference() const;
+
+ private:
+ PreferredApps preferred_apps_;
+ bool initialized_ = false;
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PREFERRED_APPS_LIST_H_
diff --git a/chromium/components/services/app_service/public/cpp/preferred_apps_list_unittest.cc b/chromium/components/services/app_service/public/cpp/preferred_apps_list_unittest.cc
new file mode 100644
index 00000000000..be050a84108
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/preferred_apps_list_unittest.cc
@@ -0,0 +1,585 @@
+// Copyright 2020 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 "components/services/app_service/public/cpp/preferred_apps_list.h"
+
+#include "components/services/app_service/public/cpp/intent_filter_util.h"
+#include "components/services/app_service/public/cpp/intent_test_util.h"
+#include "components/services/app_service/public/cpp/intent_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+const char kAppId1[] = "abcdefg";
+const char kAppId2[] = "gfedcba";
+const char kAppId3[] = "hahahahaha";
+
+} // namespace
+
+class PreferredAppListTest : public testing::Test {
+ protected:
+ apps::mojom::IntentFilterPtr CreatePatternFilter(
+ const std::string& pattern,
+ apps::mojom::PatternMatchType match_type) {
+ auto intent_filter =
+ apps_util::CreateSchemeAndHostOnlyFilter("https", "www.google.com");
+ auto pattern_condition =
+ apps_util::MakeCondition(apps::mojom::ConditionType::kPattern,
+ std::vector<apps::mojom::ConditionValuePtr>());
+ intent_filter->conditions.push_back(std::move(pattern_condition));
+ auto condition_value = apps_util::MakeConditionValue(pattern, match_type);
+ intent_filter->conditions[2]->condition_values.push_back(
+ std::move(condition_value));
+ return intent_filter;
+ }
+
+ apps::PreferredAppsList preferred_apps_;
+};
+
+// Test that for a single preferred app with URL filter, we can add
+// and find (or not find) the correct preferred app id for different
+// URLs.
+TEST_F(PreferredAppListTest, AddPreferredAppForURL) {
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url));
+
+ GURL url_in_scope = GURL("https://www.google.com/abcde");
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_in_scope));
+
+ GURL url_wrong_scheme = GURL("tel://www.google.com/");
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_wrong_scheme));
+
+ GURL url_wrong_host = GURL("https://www.hahaha.com/");
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_wrong_host));
+
+ GURL url_not_in_scope = GURL("https://www.google.com/a");
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_not_in_scope));
+}
+
+// Test for preferred app with filter that does not have all condition
+// types. E.g. add preferred app with intent filter that only have scheme.
+TEST_F(PreferredAppListTest, TopLayerFilters) {
+ auto intent_filter = apps_util::CreateSchemeOnlyFilter("tel");
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ GURL url_in_scope = GURL("tel://1234556/");
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_in_scope));
+
+ GURL url_not_in_scope = GURL("http://www.google.com");
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_not_in_scope));
+}
+
+// Test for multiple preferred app setting with different number of condition
+// types.
+TEST_F(PreferredAppListTest, MixLayerFilters) {
+ auto intent_filter_scheme = apps_util::CreateSchemeOnlyFilter("tel");
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_scheme);
+
+ auto intent_filter_scheme_host =
+ apps_util::CreateSchemeAndHostOnlyFilter("http", "www.abc.com");
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_scheme_host);
+
+ auto intent_filter_url =
+ apps_util::CreateIntentFilterForUrlScope(GURL("https://www.google.com/"));
+ preferred_apps_.AddPreferredApp(kAppId3, intent_filter_url);
+
+ GURL url_1 = GURL("tel://1234556/");
+ GURL url_2 = GURL("http://www.abc.com/");
+ GURL url_3 = GURL("https://www.google.com/");
+ GURL url_out_scope = GURL("https://www.abc.com/");
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_1));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(url_2));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(url_3));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_out_scope));
+}
+
+// Test that when there are multiple preferred apps for one intent, the best
+// matching one will be picked.
+TEST_F(PreferredAppListTest, MultiplePreferredApps) {
+ GURL url = GURL("https://www.google.com/");
+
+ auto intent_filter_scheme = apps_util::CreateSchemeOnlyFilter("https");
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_scheme);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url));
+
+ auto intent_filter_scheme_host =
+ apps_util::CreateSchemeAndHostOnlyFilter("https", "www.google.com");
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_scheme_host);
+
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(url));
+
+ auto intent_filter_url =
+ apps_util::CreateIntentFilterForUrlScope(GURL("https://www.google.com/"));
+ preferred_apps_.AddPreferredApp(kAppId3, intent_filter_url);
+
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(url));
+}
+
+// Test that we can properly add and search for filters that has multiple
+// condition values for a condition type.
+TEST_F(PreferredAppListTest, MultipleConditionValues) {
+ auto intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL("https://www.google.com/"));
+ intent_filter->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue("http",
+ apps::mojom::PatternMatchType::kNone));
+
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ GURL url_https = GURL("https://www.google.com/");
+ GURL url_http = GURL("http://www.google.com/");
+ GURL url_http_out_of_scope = GURL("http://www.abc.com/");
+ GURL url_wrong_scheme = GURL("tel://1234567/");
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_https));
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_http));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_http_out_of_scope));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_wrong_scheme));
+}
+
+// Test for more than one pattern available, we can find the correct match.
+TEST_F(PreferredAppListTest, DifferentPatterns) {
+ auto intent_filter_literal =
+ CreatePatternFilter("/bc", apps::mojom::PatternMatchType::kLiteral);
+ auto intent_filter_prefix =
+ CreatePatternFilter("/a", apps::mojom::PatternMatchType::kPrefix);
+ auto intent_filter_glob =
+ CreatePatternFilter("/c.*d", apps::mojom::PatternMatchType::kGlob);
+
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_literal);
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_prefix);
+ preferred_apps_.AddPreferredApp(kAppId3, intent_filter_glob);
+
+ GURL url_1 = GURL("https://www.google.com/bc");
+ GURL url_2 = GURL("https://www.google.com/abbb");
+ GURL url_3 = GURL("https://www.google.com/ccccccd");
+ GURL url_out_scope = GURL("https://www.google.com/dfg");
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_1));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(url_2));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(url_3));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_out_scope));
+}
+
+// Test that for same intent filter, the app id will overwrite the old setting.
+TEST_F(PreferredAppListTest, OverwritePreferredApp) {
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url));
+
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter);
+
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(filter_url));
+}
+
+// Test that when overlap happens, the previous setting will be removed.
+TEST_F(PreferredAppListTest, OverlapPreferredApp) {
+ GURL filter_url_1 = GURL("https://www.google.com/abc");
+ GURL filter_url_2 = GURL("http://www.google.com.au/abc");
+ auto intent_filter_1 = apps_util::CreateIntentFilterForUrlScope(filter_url_1);
+ intent_filter_1->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_1->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.host(),
+ apps::mojom::PatternMatchType::kNone));
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_1);
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+
+ GURL filter_url_3 = GURL("https://www.abc.com/abc");
+ auto intent_filter_2 = apps_util::CreateIntentFilterForUrlScope(filter_url_3);
+ intent_filter_2->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_2->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.host(),
+ apps::mojom::PatternMatchType::kNone));
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_2);
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+}
+
+// Test that the replaced app preferences is correct.
+TEST_F(PreferredAppListTest, ReplacedAppPreference) {
+ GURL filter_url_1 = GURL("https://www.google.com/abc");
+ GURL filter_url_2 = GURL("http://www.google.com.au/abc");
+ auto intent_filter_1 = apps_util::CreateIntentFilterForUrlScope(filter_url_1);
+ intent_filter_1->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_1->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.host(),
+ apps::mojom::PatternMatchType::kNone));
+ auto replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_1);
+ EXPECT_EQ(0u, replaced_app_preferences->replaced_preference.size());
+
+ GURL filter_url_3 = GURL("https://www.abc.com/abc");
+ auto intent_filter_2 = apps_util::CreateIntentFilterForUrlScope(filter_url_3);
+ intent_filter_2->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_2->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.host(),
+ apps::mojom::PatternMatchType::kNone));
+ replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_2);
+ EXPECT_EQ(1u, replaced_app_preferences->replaced_preference.size());
+ EXPECT_TRUE(replaced_app_preferences->replaced_preference.find(kAppId1) !=
+ replaced_app_preferences->replaced_preference.end());
+
+ GURL filter_url_4 = GURL("http://www.example.com/abc");
+ auto intent_filter_3 = apps_util::CreateIntentFilterForUrlScope(filter_url_3);
+ intent_filter_3->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_4.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_3->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_4.host(),
+ apps::mojom::PatternMatchType::kNone));
+
+ // Test when replacing multiple preferred app entries with same app id.
+ replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_1);
+ EXPECT_EQ(1u, replaced_app_preferences->replaced_preference.size());
+ EXPECT_TRUE(replaced_app_preferences->replaced_preference.find(kAppId2) !=
+ replaced_app_preferences->replaced_preference.end());
+
+ replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_3);
+ EXPECT_EQ(0u, replaced_app_preferences->replaced_preference.size());
+
+ replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_2);
+ EXPECT_EQ(1u, replaced_app_preferences->replaced_preference.size());
+ auto entry = replaced_app_preferences->replaced_preference.find(kAppId1);
+ EXPECT_TRUE(entry != replaced_app_preferences->replaced_preference.end());
+ EXPECT_EQ(2u, entry->second.size());
+
+ // Test when replacing multiple preferred app entries with different app id.
+ replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_1);
+ EXPECT_EQ(1u, replaced_app_preferences->replaced_preference.size());
+ EXPECT_TRUE(replaced_app_preferences->replaced_preference.find(kAppId2) !=
+ replaced_app_preferences->replaced_preference.end());
+
+ replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_3);
+ EXPECT_EQ(0u, replaced_app_preferences->replaced_preference.size());
+
+ replaced_app_preferences =
+ preferred_apps_.AddPreferredApp(kAppId3, intent_filter_2);
+ EXPECT_EQ(2u, replaced_app_preferences->replaced_preference.size());
+ entry = replaced_app_preferences->replaced_preference.find(kAppId1);
+ EXPECT_TRUE(entry != replaced_app_preferences->replaced_preference.end());
+ EXPECT_EQ(1u, entry->second.size());
+ entry = replaced_app_preferences->replaced_preference.find(kAppId2);
+ EXPECT_TRUE(entry != replaced_app_preferences->replaced_preference.end());
+ EXPECT_EQ(1u, entry->second.size());
+}
+
+// Test that for a single preferred app with URL filter, we can delete
+// the preferred app id.
+TEST_F(PreferredAppListTest, DeletePreferredAppForURL) {
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url));
+
+ // If try to delete with wrong ID, won't delete.
+ preferred_apps_.DeletePreferredApp(kAppId2, intent_filter);
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url));
+
+ preferred_apps_.DeletePreferredApp(kAppId1, intent_filter);
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(filter_url));
+}
+
+// Test for preferred app with filter that does not have all condition
+// types. E.g. delete preferred app with intent filter that only have scheme.
+TEST_F(PreferredAppListTest, DeleteForTopLayerFilters) {
+ auto intent_filter = apps_util::CreateSchemeOnlyFilter("tel");
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ GURL url_in_scope = GURL("tel://1234556/");
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_in_scope));
+
+ preferred_apps_.DeletePreferredApp(kAppId1, intent_filter);
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(url_in_scope));
+}
+
+// Test that we can properly delete for filters that has multiple
+// condition values for a condition type.
+TEST_F(PreferredAppListTest, DeleteMultipleConditionValues) {
+ auto intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL("https://www.google.com/"));
+ intent_filter->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue("http",
+ apps::mojom::PatternMatchType::kNone));
+
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ GURL url_https = GURL("https://www.google.com/");
+ GURL url_http = GURL("http://www.google.com/");
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_https));
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_http));
+
+ preferred_apps_.DeletePreferredApp(kAppId1, intent_filter);
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url_https));
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url_http));
+}
+
+// Test for more than one pattern available, we can delete the filter.
+TEST_F(PreferredAppListTest, DeleteDifferentPatterns) {
+ auto intent_filter_literal =
+ CreatePatternFilter("/bc", apps::mojom::PatternMatchType::kLiteral);
+ auto intent_filter_prefix =
+ CreatePatternFilter("/a", apps::mojom::PatternMatchType::kPrefix);
+ auto intent_filter_glob =
+ CreatePatternFilter("/c.*d", apps::mojom::PatternMatchType::kGlob);
+
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_literal);
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_prefix);
+ preferred_apps_.AddPreferredApp(kAppId3, intent_filter_glob);
+
+ GURL url_1 = GURL("https://www.google.com/bc");
+ GURL url_2 = GURL("https://www.google.com/abbb");
+ GURL url_3 = GURL("https://www.google.com/ccccccd");
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_1));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(url_2));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(url_3));
+
+ preferred_apps_.DeletePreferredApp(kAppId1, intent_filter_literal);
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url_1));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(url_2));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(url_3));
+ preferred_apps_.DeletePreferredApp(kAppId2, intent_filter_prefix);
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url_2));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(url_3));
+ preferred_apps_.DeletePreferredApp(kAppId3, intent_filter_glob);
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url_3));
+}
+
+// Test that can delete properly for super set filters. E.g. the filter
+// to delete has more condition values compare with filter that was set.
+TEST_F(PreferredAppListTest, DeleteForNotCompletedFilter) {
+ auto intent_filter_set =
+ apps_util::CreateIntentFilterForUrlScope(GURL("https://www.google.com/"));
+
+ auto intent_filter_to_delete =
+ apps_util::CreateIntentFilterForUrlScope(GURL("http://www.google.com/"));
+ intent_filter_to_delete->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue("https",
+ apps::mojom::PatternMatchType::kNone));
+
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_set);
+
+ GURL url = GURL("https://www.google.com/");
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url));
+
+ preferred_apps_.DeletePreferredApp(kAppId1, intent_filter_to_delete);
+
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url));
+}
+
+// Test that when there are more than one entry has overlap filter.
+TEST_F(PreferredAppListTest, DeleteOverlapFilters) {
+ GURL filter_url_1 = GURL("https://www.google.com/abc");
+ GURL filter_url_2 = GURL("http://www.google.com.au/abc");
+ GURL filter_url_3 = GURL("https://www.abc.com/abc");
+ GURL filter_url_4 = GURL("http://www.example.com/abc");
+
+ // Filter 1 handles url 1 and 2.
+ auto intent_filter_1 = apps_util::CreateIntentFilterForUrlScope(filter_url_1);
+ intent_filter_1->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_1->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.host(),
+ apps::mojom::PatternMatchType::kNone));
+
+ // Filter 2 handles url 2 and 3.
+ auto intent_filter_2 = apps_util::CreateIntentFilterForUrlScope(filter_url_3);
+ intent_filter_2->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_2->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_2.host(),
+ apps::mojom::PatternMatchType::kNone));
+
+ // Filter 3 handles url 3 and 4.
+ auto intent_filter_3 = apps_util::CreateIntentFilterForUrlScope(filter_url_3);
+ intent_filter_3->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_4.scheme(),
+ apps::mojom::PatternMatchType::kNone));
+ intent_filter_3->conditions[1]->condition_values.push_back(
+ apps_util::MakeConditionValue(filter_url_4.host(),
+ apps::mojom::PatternMatchType::kNone));
+
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_1);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_3);
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_4));
+
+ // Filter 2 has overlap with both filter 1 and 3, delete this should remove
+ // all entries.
+ preferred_apps_.DeletePreferredApp(kAppId1, intent_filter_2);
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_4));
+}
+
+// Test that DeleteAppId() can delete the setting for one filter.
+TEST_F(PreferredAppListTest, DeleteAppIdForOneFilter) {
+ GURL filter_url = GURL("https://www.google.com/abc");
+ auto intent_filter = apps_util::CreateIntentFilterForUrlScope(filter_url);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url));
+
+ preferred_apps_.DeleteAppId(kAppId1);
+
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(filter_url));
+}
+
+// Test that when multiple filters set to the same app id, DeleteAppId() can
+// delete all of them.
+TEST_F(PreferredAppListTest, DeleteAppIdForMultipleFilters) {
+ GURL filter_url_1 = GURL("https://www.google.com/abc");
+ auto intent_filter_1 = apps_util::CreateIntentFilterForUrlScope(filter_url_1);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_1);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+
+ GURL filter_url_2 = GURL("https://www.abc.com/google");
+ auto intent_filter_2 = apps_util::CreateIntentFilterForUrlScope(filter_url_2);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_2);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+
+ GURL filter_url_3 = GURL("tel://12345678/");
+ auto intent_filter_3 = apps_util::CreateIntentFilterForUrlScope(filter_url_3);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_3);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+
+ preferred_apps_.DeleteAppId(kAppId1);
+
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+}
+
+// Test that for filter with multiple condition values, DeleteAppId() can
+// delete them all.
+TEST_F(PreferredAppListTest, DeleteAppIdForMultipleConditionValues) {
+ auto intent_filter =
+ apps_util::CreateIntentFilterForUrlScope(GURL("https://www.google.com/"));
+ intent_filter->conditions[0]->condition_values.push_back(
+ apps_util::MakeConditionValue("http",
+ apps::mojom::PatternMatchType::kNone));
+
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter);
+
+ GURL url_https = GURL("https://www.google.com/");
+ GURL url_http = GURL("http://www.google.com/");
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_https));
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(url_http));
+
+ preferred_apps_.DeleteAppId(kAppId1);
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url_https));
+ EXPECT_EQ(base::nullopt, preferred_apps_.FindPreferredAppForUrl(url_http));
+}
+
+// Test that for multiple filters set to different app ids, DeleteAppId() only
+// deletes the correct app id.
+TEST_F(PreferredAppListTest, DeleteAppIdForMultipleAppIds) {
+ GURL filter_url_1 = GURL("https://www.google.com/abc");
+ auto intent_filter_1 = apps_util::CreateIntentFilterForUrlScope(filter_url_1);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_1);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+
+ GURL filter_url_2 = GURL("https://www.abc.com/google");
+ auto intent_filter_2 = apps_util::CreateIntentFilterForUrlScope(filter_url_2);
+ preferred_apps_.AddPreferredApp(kAppId1, intent_filter_2);
+
+ EXPECT_EQ(kAppId1, preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+
+ GURL filter_url_3 = GURL("tel://12345678/");
+ auto intent_filter_3 = apps_util::CreateIntentFilterForUrlScope(filter_url_3);
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_3);
+
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+
+ GURL filter_url_4 = GURL("https://www.google.com.au/");
+ auto intent_filter_4 = apps_util::CreateIntentFilterForUrlScope(filter_url_4);
+ preferred_apps_.AddPreferredApp(kAppId2, intent_filter_4);
+
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(filter_url_4));
+
+ GURL filter_url_5 = GURL("https://www.example.com/google");
+ auto intent_filter_5 = apps_util::CreateIntentFilterForUrlScope(filter_url_5);
+ preferred_apps_.AddPreferredApp(kAppId3, intent_filter_5);
+
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(filter_url_5));
+
+ GURL filter_url_6 = GURL("tel://98765432/");
+ auto intent_filter_6 = apps_util::CreateIntentFilterForUrlScope(filter_url_6);
+ preferred_apps_.AddPreferredApp(kAppId3, intent_filter_6);
+
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(filter_url_6));
+
+ preferred_apps_.DeleteAppId(kAppId1);
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_1));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_2));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+ EXPECT_EQ(kAppId2, preferred_apps_.FindPreferredAppForUrl(filter_url_4));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(filter_url_5));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(filter_url_6));
+ preferred_apps_.DeleteAppId(kAppId2);
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_3));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_4));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(filter_url_5));
+ EXPECT_EQ(kAppId3, preferred_apps_.FindPreferredAppForUrl(filter_url_6));
+ preferred_apps_.DeleteAppId(kAppId3);
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_5));
+ EXPECT_EQ(base::nullopt,
+ preferred_apps_.FindPreferredAppForUrl(filter_url_6));
+}
diff --git a/chromium/components/services/app_service/public/cpp/publisher_base.cc b/chromium/components/services/app_service/public/cpp/publisher_base.cc
new file mode 100644
index 00000000000..9db0417792b
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/publisher_base.cc
@@ -0,0 +1,126 @@
+// Copyright 2020 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 "components/services/app_service/public/cpp/publisher_base.h"
+
+#include <vector>
+
+#include "base/time/time.h"
+
+namespace apps {
+
+PublisherBase::PublisherBase() = default;
+
+PublisherBase::~PublisherBase() = default;
+
+// static
+apps::mojom::AppPtr PublisherBase::MakeApp(
+ apps::mojom::AppType app_type,
+ std::string app_id,
+ apps::mojom::Readiness readiness,
+ const std::string& name,
+ apps::mojom::InstallSource install_source) {
+ apps::mojom::AppPtr app = apps::mojom::App::New();
+
+ app->app_type = app_type;
+ app->app_id = app_id;
+ app->readiness = readiness;
+ app->name = name;
+ app->short_name = name;
+
+ app->last_launch_time = base::Time();
+ app->install_time = base::Time();
+
+ app->install_source = install_source;
+
+ app->is_platform_app = apps::mojom::OptionalBool::kFalse;
+ app->recommendable = apps::mojom::OptionalBool::kTrue;
+ app->searchable = apps::mojom::OptionalBool::kTrue;
+ app->paused = apps::mojom::OptionalBool::kFalse;
+
+ return app;
+}
+
+void PublisherBase::FlushMojoCallsForTesting() {
+ if (receiver_.is_bound()) {
+ receiver_.FlushForTesting();
+ }
+}
+
+void PublisherBase::Initialize(
+ const mojo::Remote<apps::mojom::AppService>& app_service,
+ apps::mojom::AppType app_type) {
+ app_service->RegisterPublisher(receiver_.BindNewPipeAndPassRemote(),
+ app_type);
+}
+
+void PublisherBase::Publish(
+ apps::mojom::AppPtr app,
+ const mojo::RemoteSet<apps::mojom::Subscriber>& subscribers) {
+ for (auto& subscriber : subscribers) {
+ std::vector<apps::mojom::AppPtr> apps;
+ apps.push_back(app.Clone());
+ subscriber->OnApps(std::move(apps));
+ }
+}
+
+void PublisherBase::LaunchAppWithFiles(const std::string& app_id,
+ apps::mojom::LaunchContainer container,
+ int32_t event_flags,
+ apps::mojom::LaunchSource launch_source,
+ apps::mojom::FilePathsPtr file_paths) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::LaunchAppWithIntent(const std::string& app_id,
+ int32_t event_flags,
+ apps::mojom::IntentPtr intent,
+ apps::mojom::LaunchSource launch_source,
+ int64_t display_id) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::SetPermission(const std::string& app_id,
+ apps::mojom::PermissionPtr permission) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::Uninstall(const std::string& app_id,
+ bool clear_site_data,
+ bool report_abuse) {
+ LOG(ERROR) << "Uninstall failed, could not remove the app with id " << app_id;
+}
+
+void PublisherBase::PauseApp(const std::string& app_id) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::UnpauseApps(const std::string& app_id) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::StopApp(const std::string& app_id) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::GetMenuModel(const std::string& app_id,
+ apps::mojom::MenuType menu_type,
+ int64_t display_id,
+ GetMenuModelCallback callback) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::OpenNativeSettings(const std::string& app_id) {
+ NOTIMPLEMENTED();
+}
+
+void PublisherBase::OnPreferredAppSet(
+ const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter,
+ apps::mojom::IntentPtr intent,
+ apps::mojom::ReplacedAppPreferencesPtr replaced_app_preferences) {
+ NOTIMPLEMENTED();
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/publisher_base.h b/chromium/components/services/app_service/public/cpp/publisher_base.h
new file mode 100644
index 00000000000..e49c816a7a3
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/publisher_base.h
@@ -0,0 +1,89 @@
+// Copyright 2020 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PUBLISHER_BASE_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PUBLISHER_BASE_H_
+
+#include <string>
+
+#include "components/services/app_service/public/mojom/app_service.mojom.h"
+#include "components/services/app_service/public/mojom/types.mojom.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "mojo/public/cpp/bindings/receiver.h"
+#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/bindings/remote_set.h"
+
+namespace apps {
+
+// An publisher parent class (in the App Service sense) for all app publishers.
+// This class has NOTIMPLEMENTED() implementations of mandatory methods from the
+// apps::mojom::Publisher class to simplify the process of adding a new
+// publisher.
+//
+// See components/services/app_service/README.md.
+class PublisherBase : public apps::mojom::Publisher {
+ public:
+ PublisherBase();
+ ~PublisherBase() override;
+
+ PublisherBase(const PublisherBase&) = delete;
+ PublisherBase& operator=(const PublisherBase&) = delete;
+
+ static apps::mojom::AppPtr MakeApp(apps::mojom::AppType app_type,
+ std::string app_id,
+ apps::mojom::Readiness readiness,
+ const std::string& name,
+ apps::mojom::InstallSource install_source);
+
+ void FlushMojoCallsForTesting();
+
+ protected:
+ void Initialize(const mojo::Remote<apps::mojom::AppService>& app_service,
+ apps::mojom::AppType app_type);
+
+ // Publish |app| to all subscribers in |subscribers|. Should be called
+ // whenever the app represented by |app| undergoes some state change to inform
+ // subscribers of the change.
+ void Publish(apps::mojom::AppPtr app,
+ const mojo::RemoteSet<apps::mojom::Subscriber>& subscribers);
+
+ mojo::Receiver<apps::mojom::Publisher>& receiver() { return receiver_; }
+
+ private:
+ // apps::mojom::Publisher overrides.
+ void LaunchAppWithFiles(const std::string& app_id,
+ apps::mojom::LaunchContainer container,
+ int32_t event_flags,
+ apps::mojom::LaunchSource launch_source,
+ apps::mojom::FilePathsPtr file_paths) override;
+ void LaunchAppWithIntent(const std::string& app_id,
+ int32_t event_flags,
+ apps::mojom::IntentPtr intent,
+ apps::mojom::LaunchSource launch_source,
+ int64_t display_id) override;
+ void SetPermission(const std::string& app_id,
+ apps::mojom::PermissionPtr permission) override;
+ void Uninstall(const std::string& app_id,
+ bool clear_site_data,
+ bool report_abuse) override;
+ void PauseApp(const std::string& app_id) override;
+ void UnpauseApps(const std::string& app_id) override;
+ void StopApp(const std::string& app_id) override;
+ void GetMenuModel(const std::string& app_id,
+ apps::mojom::MenuType menu_type,
+ int64_t display_id,
+ GetMenuModelCallback callback) override;
+ void OpenNativeSettings(const std::string& app_id) override;
+ void OnPreferredAppSet(
+ const std::string& app_id,
+ apps::mojom::IntentFilterPtr intent_filter,
+ apps::mojom::IntentPtr intent,
+ apps::mojom::ReplacedAppPreferencesPtr replaced_app_preferences) override;
+
+ mojo::Receiver<apps::mojom::Publisher> receiver_{this};
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_PUBLISHER_BASE_H_
diff --git a/chromium/components/services/app_service/public/cpp/stub_icon_loader.cc b/chromium/components/services/app_service/public/cpp/stub_icon_loader.cc
new file mode 100644
index 00000000000..1df6c40e432
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/stub_icon_loader.cc
@@ -0,0 +1,53 @@
+// Copyright 2019 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 "components/services/app_service/public/cpp/stub_icon_loader.h"
+
+#include <utility>
+
+#include "ui/gfx/image/image_skia.h"
+#include "ui/gfx/image/image_skia_rep.h"
+
+namespace apps {
+
+StubIconLoader::StubIconLoader() = default;
+
+StubIconLoader::~StubIconLoader() = default;
+
+apps::mojom::IconKeyPtr StubIconLoader::GetIconKey(const std::string& app_id) {
+ uint64_t timeline = 0;
+ auto iter = timelines_by_app_id_.find(app_id);
+ if (iter != timelines_by_app_id_.end()) {
+ timeline = iter->second;
+ }
+ return apps::mojom::IconKey::New(timeline, 0, 0);
+}
+
+std::unique_ptr<IconLoader::Releaser> StubIconLoader::LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) {
+ num_load_calls_++;
+ auto iter = timelines_by_app_id_.find(app_id);
+ if (iter != timelines_by_app_id_.end()) {
+ auto icon_value = apps::mojom::IconValue::New();
+ icon_value->icon_compression = apps::mojom::IconCompression::kUncompressed;
+ icon_value->uncompressed =
+ gfx::ImageSkia(gfx::ImageSkiaRep(gfx::Size(1, 1), 1.0f));
+ std::move(callback).Run(std::move(icon_value));
+ } else {
+ std::move(callback).Run(apps::mojom::IconValue::New());
+ }
+ return nullptr;
+}
+
+int StubIconLoader::NumLoadIconFromIconKeyCalls() {
+ return num_load_calls_;
+}
+
+} // namespace apps
diff --git a/chromium/components/services/app_service/public/cpp/stub_icon_loader.h b/chromium/components/services/app_service/public/cpp/stub_icon_loader.h
new file mode 100644
index 00000000000..54fa69a7f21
--- /dev/null
+++ b/chromium/components/services/app_service/public/cpp/stub_icon_loader.h
@@ -0,0 +1,45 @@
+// Copyright 2019 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 COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_STUB_ICON_LOADER_H_
+#define COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_STUB_ICON_LOADER_H_
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "components/services/app_service/public/cpp/icon_loader.h"
+
+namespace apps {
+
+// Helper IconLoader implementation to served canned answers for testing.
+class StubIconLoader : public IconLoader {
+ public:
+ StubIconLoader();
+ ~StubIconLoader() override;
+
+ // IconLoader overrides.
+ apps::mojom::IconKeyPtr GetIconKey(const std::string& app_id) override;
+ std::unique_ptr<IconLoader::Releaser> LoadIconFromIconKey(
+ apps::mojom::AppType app_type,
+ const std::string& app_id,
+ apps::mojom::IconKeyPtr icon_key,
+ apps::mojom::IconCompression icon_compression,
+ int32_t size_hint_in_dip,
+ bool allow_placeholder_icon,
+ apps::mojom::Publisher::LoadIconCallback callback) override;
+
+ int NumLoadIconFromIconKeyCalls();
+
+ std::map<std::string, uint64_t> timelines_by_app_id_;
+
+ private:
+ int num_load_calls_ = 0;
+
+ DISALLOW_COPY_AND_ASSIGN(StubIconLoader);
+};
+
+} // namespace apps
+
+#endif // COMPONENTS_SERVICES_APP_SERVICE_PUBLIC_CPP_STUB_ICON_LOADER_H_
diff --git a/chromium/components/services/app_service/public/mojom/BUILD.gn b/chromium/components/services/app_service/public/mojom/BUILD.gn
index 60ea0f00f56..b2a043d80dd 100644
--- a/chromium/components/services/app_service/public/mojom/BUILD.gn
+++ b/chromium/components/services/app_service/public/mojom/BUILD.gn
@@ -1,9 +1,26 @@
-# Copyright 2019 The Chromium Authors. All rights reserved.
+# 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.
import("//mojo/public/tools/bindings/mojom.gni")
-mojom("mojom") {
+mojom("types") {
sources = [ "types.mojom" ]
+
+ public_deps = [
+ "//mojo/public/mojom/base",
+ "//skia/public/mojom",
+ "//ui/gfx/geometry/mojom",
+ "//ui/gfx/image/mojom",
+ "//ui/gfx/image/mojom",
+ "//ui/gfx/mojom",
+ "//ui/gfx/range/mojom",
+ "//url/mojom:url_mojom_gurl",
+ ]
+}
+
+mojom("mojom") {
+ sources = [ "app_service.mojom" ]
+
+ public_deps = [ ":types" ]
}
diff --git a/chromium/components/services/app_service/public/mojom/app_service.mojom b/chromium/components/services/app_service/public/mojom/app_service.mojom
new file mode 100644
index 00000000000..f9d57c7911f
--- /dev/null
+++ b/chromium/components/services/app_service/public/mojom/app_service.mojom
@@ -0,0 +1,235 @@
+// 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.
+
+module apps.mojom;
+
+import "components/services/app_service/public/mojom/types.mojom";
+
+// An intermediary between M app consumers (e.g. app launcher UI, intent
+// pickers) and N app providers (also known as app platforms, e.g. Android
+// apps, Linux apps and Web apps). It abstracts over platform-specific
+// implementations and allow consumers to issue generic queries (e.g. for an
+// app's name and icon) that are satisfied by the appropriate provider.
+//
+// See components/services/app_service/README.md.
+interface AppService {
+ // Called by a publisher of apps to register itself and its apps with the App
+ // Service.
+ RegisterPublisher(pending_remote<Publisher> publisher, AppType app_type);
+
+ // Called by a consumer that wishes to know about available apps to register
+ // itself with the App Service.
+ RegisterSubscriber(pending_remote<Subscriber> subscriber, ConnectOptions? opts);
+
+ // App Icon Factory methods.
+ LoadIcon(
+ AppType app_type,
+ string app_id,
+ IconKey icon_key,
+ IconCompression icon_compression,
+ int32 size_hint_in_dip,
+ bool allow_placeholder_icon) => (IconValue icon_value);
+
+ // App Runner methods.
+ Launch(
+ AppType app_type,
+ string app_id,
+ int32 event_flags,
+ LaunchSource launch_source,
+ int64 display_id);
+
+ // Launches an app with |app_id| and |file_path|
+ LaunchAppWithFiles(
+ AppType app_type,
+ string app_id,
+ LaunchContainer container,
+ int32 event_flags,
+ LaunchSource launch_source,
+ FilePaths file_paths);
+
+ // Launches an app with |app_id| and Chrome OS generic |intent| irrespective
+ // of app platform.
+ LaunchAppWithIntent(
+ AppType app_type,
+ string app_id,
+ int32 event_flags,
+ Intent intent,
+ LaunchSource launch_source,
+ int64 display_id);
+
+ SetPermission(
+ AppType app_type,
+ string app_id,
+ Permission permission);
+
+ // Directly uninstalls |app_id| without prompting the user.
+ // |clear_site_data| is available for bookmark apps only. If true, any site
+ // data associated with the app will be removed..
+ // |report_abuse| is available for Chrome Apps only. If true, the app will be
+ // reported for abuse to the Web Store.
+ Uninstall(
+ AppType app_type,
+ string app_id,
+ bool clear_site_data,
+ bool report_abuse);
+
+ // Pauses an app to stop the current running app, and apply the icon effect.
+ PauseApp(
+ AppType app_type,
+ string app_id);
+
+ // Unpauses an app, and recover the icon effect for the app.
+ UnpauseApps(
+ AppType app_type,
+ string app_id);
+
+ // Stops the current running app for the given |app_id|.
+ StopApp(
+ AppType app_type,
+ string app_id);
+
+ // Returns the menu items for an app with |app_id|.
+ GetMenuModel(
+ AppType app_type,
+ string app_id,
+ MenuType menu_type,
+ int64 display_id) => (MenuItems menu_items);
+
+ // Opens native settings for the app with |app_id|.
+ OpenNativeSettings(
+ AppType app_type,
+ string app_id);
+
+ // Sets app identified by |app_id| as preferred app for |intent_filter|.
+ // |intent| is needed to set the preferred app in ARC.
+ // If the request is |from_publisher|, we would not sync the preferred
+ // app back to the publisher.
+ AddPreferredApp(
+ AppType app_type,
+ string app_id,
+ IntentFilter intent_filter,
+ Intent? intent,
+ bool from_publisher);
+
+ // Removes all preferred app setting for an |app_id|.
+ RemovePreferredApp(AppType app_type, string app_id);
+
+ // Resets app identified by |app_id| as preferred app for |intent_filter|.
+ RemovePreferredAppForFilter(
+ AppType app_type,
+ string app_id,
+ IntentFilter intent_filter);
+};
+
+interface Publisher {
+ // App Registry methods.
+ Connect(pending_remote<Subscriber> subscriber, ConnectOptions? opts);
+
+ // App Icon Factory methods.
+ LoadIcon(
+ string app_id,
+ IconKey icon_key,
+ IconCompression icon_compression,
+ int32 size_hint_in_dip,
+ bool allow_placeholder_icon) => (IconValue icon_value);
+
+ // App Runner methods.
+ Launch(
+ string app_id,
+ int32 event_flags,
+ LaunchSource launch_source,
+ int64 display_id);
+
+ // Launches an app with |app_id| and |file_path|
+ LaunchAppWithFiles(
+ string app_id,
+ LaunchContainer container,
+ int32 event_flags,
+ LaunchSource launch_source,
+ FilePaths file_paths);
+
+ // Launches an app with |app_id| and Chrome OS generic |intent| irrespective
+ // of app platform.
+ LaunchAppWithIntent(
+ string app_id,
+ int32 event_flags,
+ Intent intent,
+ LaunchSource launch_source,
+ int64 display_id);
+
+ SetPermission(
+ string app_id,
+ Permission permission);
+
+ // Directly uninstalls |app_id| without prompting the user.
+ // |clear_site_data| is available for bookmark apps only. If true, any site
+ // data associated with the app will be removed..
+ // |report_abuse| is available for Chrome Apps only. If true, the app will be
+ // reported for abuse to the Web Store.
+ Uninstall(
+ string app_id,
+ bool clear_site_data,
+ bool report_abuse);
+
+ // Pauses an app to stop the current running app, and apply the icon effect.
+ PauseApp(
+ string app_id);
+
+ // Unpauses an app, and recover the icon effect for the app.
+ UnpauseApps(
+ string app_id);
+
+ // Stops the current running app for the given |app_id|.
+ StopApp(
+ string app_id);
+
+ // Returns the menu items for an app with |app_id|.
+ GetMenuModel(
+ string app_id,
+ MenuType menu_type,
+ int64 display_id) => (MenuItems menu_items);
+
+ // Opens native settings for the app with |app_id|.
+ OpenNativeSettings(
+ string app_id);
+
+ // Indicates that the app identified by |app_id| has been set as a preferred
+ // app for |intent_filter|, and the |replaced_app_preferences| is the apps
+ // that are no longer preferred apps for their corresponding |intent_filters|.
+ // This method is used by the App Service to sync the change to publishers.
+ // |intent| is needed to set the preferred app in ARC.
+ OnPreferredAppSet(
+ string app_id,
+ IntentFilter intent_filter,
+ Intent intent,
+ ReplacedAppPreferences replaced_app_preferences);
+};
+
+interface Subscriber {
+ OnApps(array<App> deltas);
+
+ // Binds this to the given receiver (message pipe endpoint), being to Mojo
+ // interfaces what POSIX's dup is to file descriptors.
+ //
+ // See https://groups.google.com/a/chromium.org/d/msg/chromium-mojo/nFhBzGsb5Pg/V7t_8kNRAgAJ
+ Clone(pending_receiver<Subscriber> receiver);
+
+ // Indicates that the app identified by |app_id| has been set as a preferred
+ // app for |intent_fitler|. This method is used by the App Service to sync
+ // the change from one subscriber to the others.
+ OnPreferredAppSet(string app_id,
+ IntentFilter intent_filter);
+
+ // Indicates that the app identified by |app_id| is no longer a preferred
+ // app for |intent_filter|. This method is used by the App Service to sync
+ // the change to all subscribers.
+ OnPreferredAppRemoved(string app_id, IntentFilter intent_filter);
+
+ // Initialize the |preferred_apps| in the subscribers from the app service.
+ InitializePreferredApps(array<PreferredApp> preferred_apps);
+};
+
+struct ConnectOptions {
+ // TODO: some way to represent l10n info such as the UI language.
+};
diff --git a/chromium/components/services/app_service/public/mojom/types.mojom b/chromium/components/services/app_service/public/mojom/types.mojom
index 21eca6c2044..736956d0b7b 100644
--- a/chromium/components/services/app_service/public/mojom/types.mojom
+++ b/chromium/components/services/app_service/public/mojom/types.mojom
@@ -1,9 +1,327 @@
-// Copyright 2019 The Chromium Authors. All rights reserved.
+// 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.
module apps.mojom;
+import "mojo/public/mojom/base/file_path.mojom";
+import "mojo/public/mojom/base/time.mojom";
+import "ui/gfx/image/mojom/image.mojom";
+import "url/mojom/url.mojom";
+
+// Information about an app. See components/services/app_service/README.md.
+struct App {
+ AppType app_type;
+ string app_id;
+
+ // The fields above are mandatory. Everything else below is optional.
+
+ Readiness readiness;
+ string? name;
+ string? short_name;
+
+ // The publisher-specific ID for this app, e.g. for Android apps, this field
+ // contains the Android package name. May be empty if AppId() should be
+ // considered as the canonical publisher ID.
+ string? publisher_id;
+
+ string? description;
+ string? version;
+ array<string> additional_search_terms;
+ IconKey? icon_key;
+ mojo_base.mojom.Time? last_launch_time;
+ mojo_base.mojom.Time? install_time;
+
+ // This vector must be treated atomically, if there is a permission
+ // change, the publisher must send through the entire list of permissions.
+ // Should contain no duplicate IDs.
+ // If empty during updates, Subscriber can assume no changes.
+ // There is no guarantee that this is sorted by any criteria.
+ array<Permission> permissions;
+
+ // Whether the app was installed by sync, policy or as a default app.
+ InstallSource install_source;
+
+ // Whether the app is an extensions::Extensions where is_platform_app()
+ // returns true.
+ OptionalBool is_platform_app;
+
+ // TODO(nigeltao): be more principled, instead of ad hoc show_in_xxx and
+ // show_in_yyy fields?
+ OptionalBool recommendable;
+ OptionalBool searchable;
+ OptionalBool show_in_launcher;
+ OptionalBool show_in_shelf;
+ OptionalBool show_in_search;
+ OptionalBool show_in_management;
+
+ // Whether the app icon should add the notification badging.
+ OptionalBool has_badge;
+
+ // Paused apps cannot be launched, and any running apps that become paused
+ // will be stopped. This is independent of whether or not the app is ready to
+ // be launched (defined by the Readiness field).
+ OptionalBool paused;
+
+ // This vector stores all the intent filters defined in this app. Each
+ // intent filter defines a matching criteria for whether an intent can
+ // be handled by this app. One app can have multiple intent filters.
+ array<IntentFilter> intent_filters;
+
+ // When adding new fields, also update the Merge method and other helpers in
+ // components/services/app_service/public/cpp/app_update.*
+};
+
+struct Permission {
+ // An AppType-specific value, opaque to the App Service.
+ // Different publishers (with different AppType's) can
+ // re-use the same numerical value to mean different things.
+ uint32 permission_id;
+ PermissionValueType value_type;
+ // The semantics of value depends on the value_type.
+ uint32 value;
+ // If the permission is managed by an enterprise policy.
+ bool is_managed;
+};
+
+// The types of apps available in the registry.
+enum AppType {
+ kUnknown = 0,
+ kArc, // Android app.
+ kBuiltIn, // Built-in app.
+ kCrostini, // Linux (via Crostini) app.
+ kExtension, // Extension-backed app.
+ kWeb, // Web app.
+ kMacNative, // Native Mac app.
+ kPluginVm, // Plugin VM app.
+ kLacros, // Lacros app.
+};
+
+// Whether an app is ready to launch, i.e. installed.
+enum Readiness {
+ kUnknown = 0,
+ kReady, // Installed and launchable.
+ kDisabledByBlocklist, // Disabled by SafeBrowsing.
+ kDisabledByPolicy, // Disabled by admin policy.
+ kDisabledByUser, // Disabled by explicit user action.
+ kTerminated, // Renderer process crashed.
+ kUninstalledByUser,
+};
+
+// How the app was installed.
+enum InstallSource {
+ kUnknown = 0,
+ kSystem, // Installed with the system and is considered a part of the OS.
+ kPolicy, // Installed by policy.
+ kOem, // Installed by an OEM.
+ kDefault, // Preinstalled by default, but is not considered a system app.
+ kSync, // Installed by sync.
+ kUser, // Installed by user action.
+};
+
+// Augments a bool to include an 'unknown' value.
+enum OptionalBool {
+ kUnknown = 0,
+ kFalse,
+ kTrue,
+};
+
+struct IconKey {
+ // A timeline value for icons that do not change.
+ const uint64 kDoesNotChangeOverTime = 0;
+
+ const int32 kInvalidResourceId = 0;
+
+ // A monotonically increasing number so that, after an icon update, a new
+ // IconKey, one that is different in terms of field-by-field equality, can be
+ // broadcast by a Publisher.
+ //
+ // The exact value of the number isn't important, only that newer IconKey's
+ // (those that were created more recently) have a larger timeline than older
+ // IconKey's.
+ //
+ // This is, in some sense, *a* version number, but the field is not called
+ // "version", to avoid any possible confusion that it encodes *the* app's
+ // version number, e.g. the "2.3.5" in "FooBar version 2.3.5 is installed".
+ //
+ // For example, if an app is disabled for some reason (so that its icon is
+ // grayed out), this would result in a different timeline even though the
+ // app's version is unchanged.
+ uint64 timeline;
+ // If non-zero (or equivalently, not equal to kInvalidResourceId), the
+ // compressed icon is compiled into the Chromium binary as a statically
+ // available, int-keyed resource.
+ int32 resource_id;
+ // A bitmask of icon post-processing effects, such as desaturation to gray
+ // and rounding the corners.
+ uint32 icon_effects;
+
+ // When adding new fields, also update the IconLoader::Key type in
+ // components/services/app_service/public/cpp/icon_loader.*
+};
+
+enum IconCompression {
+ // Sentinel value used in error cases.
+ kUnknown,
+ // Icon as an uncompressed gfx::ImageSkia with no standard Chrome OS mask.
+ kUncompressed,
+ // Icon as compressed bytes with no standard Chrome OS mask.
+ kCompressed,
+ // Icon as an uncompressed gfx::ImageSkia with the standard Chrome OS mask
+ // applied. This is the default suggested icon type.
+ kStandard,
+};
+
+struct IconValue {
+ IconCompression icon_compression;
+ gfx.mojom.ImageSkia? uncompressed;
+ array<uint8>? compressed;
+ bool is_placeholder_icon;
+};
+
+// Enumeration of possible app launch sources.
+// Note the enumeration is used in UMA histogram so entries
+// should not be re-ordered or removed.
+enum LaunchSource {
+ kUnknown = 0,
+ kFromAppListGrid = 1, // Grid of apps, not the search box.
+ kFromAppListGridContextMenu = 2, // Grid of apps; context menu.
+ kFromAppListQuery = 3, // Query-dependent results (larger icons).
+ kFromAppListQueryContextMenu = 4, // Query-dependent results; context menu.
+ kFromAppListRecommendation = 5, // Query-less recommendations (smaller
+ // icons).
+ kFromParentalControls = 6, // Parental Controls Settings Section and
+ // Per App time notification.
+ kFromShelf = 7, // Shelf.
+ kFromFileManager = 8, // FileManager.
+ kFromLink = 9, // Left-licking on links in the browser.
+ kFromOmnibox = 10, // Enter URL in the Omnibox in the browser.
+ kFromChromeInternal = 11, // Chrome internal call.
+ kFromKeyboard = 12, // Keyboard shortcut for opening app.
+ kFromOtherApp = 13, // Clicking link in another app or webui.
+ kFromMenu = 14, // Menu.
+ kFromInstalledNotification = 15, // Installed notification
+ kFromTest = 16, // Test
+ kFromArc = 17, // Arc.
+};
+
+enum TriState {
+ kAllow,
+ kBlock,
+ kAsk,
+};
+
+enum PermissionValueType {
+ kBool, // Permission.value is a Bool (either 0 or 1).
+ kTriState, // Permission.value is a TriState.
+};
+
+// MenuItems are used to populate context menus, e.g. in the app list or shelf.
+// Note: Some menu item types only support a subset of these item features.
+// Please update comments below (MenuItemType -> [fields expected for usage])
+// when anything changed to MenuItemType or MenuItem.
+//
+// kCommand -> [command_id, string_id].
+// kRadio -> [command_id, string_id, radio_group_id].
+// kSeparator -> [command_id].
+// kSubmenu -> [command_id, string_id, submenu].
+// kArcCommand -> [command_id, shortcut_id, label, image].
+//
+struct MenuItems {
+ array<MenuItem> items;
+};
+
+struct MenuItem {
+ MenuItemType type; // The type of the menu item.
+ int32 command_id; // The menu item command id.
+ int32 string_id; // The id of the menu item label.
+ array<MenuItem> submenu; // The optional nested submenu item list.
+ int32 radio_group_id; // The radio group id.
+ string shortcut_id; // The shortcut id, may be empty.
+ string label; // The string label, may be empty.
+ gfx.mojom.ImageSkia? image; // The image icon, may be null.
+};
+
+// The types of menu items shown in the app list or shelf.
+enum MenuItemType {
+ kCommand, // Performs an action when selected.
+ kRadio, // Can be selected/checked among a group of choices.
+ kSeparator, // Shows a horizontal line separator.
+ kSubmenu, // Presents a submenu within another menu.
+ kArcCommand, // Performs an ARC shortcut action when selected.
+};
+
+// Which component requests context menus, the app list or shelf.
+enum MenuType {
+ kAppList,
+ kShelf,
+};
+
+// The intent filter matching condition types.
+enum ConditionType {
+ kScheme, // Matches the URL scheme (e.g. https, tel).
+ kHost, // Matches the URL host (e.g. www.google.com).
+ kPattern, // Matches the URL pattern (e.g. /abc/*).
+ kAction, // Matches the action type (e.g. view, send).
+ kMimeType, // Matches the data mime type (e.g. image/jpeg).
+};
+
+// The pattern match type for intent filter pattern condition.
+enum PatternMatchType {
+ kNone = 0,
+ kLiteral,
+ kPrefix,
+ kGlob,
+ kMimeType,
+};
+
+// For pattern type of condition, the value match will be based on the pattern
+// match type. If the match_type is kNone, then an exact match with the value
+// will be required.
+struct ConditionValue {
+ string value;
+ PatternMatchType match_type; // This will be None for non pattern conditions.
+};
+
+// The condition for an intent filter. It matches if the intent contains this
+// condition type and the corresponding value matches with any of the
+// condition_values.
+struct Condition {
+ ConditionType condition_type;
+ array<ConditionValue> condition_values;
+};
+
+// An intent filter is defined by an app, and contains a list of conditions that
+// an intent needs to match. If all conditions match, then this intent filter
+// matches against an intent.
+struct IntentFilter {
+ array<Condition> conditions;
+};
+
+// Action and resource handling request. This includes the scheme and URL at
+// the moment, and will be extended to handle files in the future.
+struct Intent {
+ string? action; // Intent action. e.g. view, send.
+ url.mojom.Url? url; // The URL of the intent. e.g. https://www.google.com/.
+ string? mime_type; // MIME type. e.g. text/plain, image/*.
+};
+
+// Represents a group of |app_ids| that is no longer preferred app of their
+// corresponding |intent_filters|.
+struct ReplacedAppPreferences {
+ map<string, array<IntentFilter>> replaced_preference;
+};
+
+// The preferred app represents by |app_id| for |intent_fitler|.
+struct PreferredApp {
+ IntentFilter intent_filter;
+ string app_id;
+};
+
+struct FilePaths {
+ array<mojo_base.mojom.FilePath> file_paths;
+};
+
// Enumeration of possible app launch sources.
// This should be kept in sync with LaunchSource in
// extensions/common/api/app_runtime.idl, and GetLaunchSourceEnum() in
diff --git a/chromium/components/services/heap_profiling/json_exporter_unittest.cc b/chromium/components/services/heap_profiling/json_exporter_unittest.cc
index 673ac2a7a34..ccb60ea6eda 100644
--- a/chromium/components/services/heap_profiling/json_exporter_unittest.cc
+++ b/chromium/components/services/heap_profiling/json_exporter_unittest.cc
@@ -425,13 +425,13 @@ TEST(ProfilingJsonExporterTest, LargeAllocation) {
std::string json = ExportMemoryMapsAndV2StackTraceToJSON(&params);
// JSON should parse.
- base::JSONReader json_reader(base::JSON_PARSE_RFC);
- base::Optional<base::Value> result = json_reader.ReadToValue(json);
- ASSERT_TRUE(result.has_value()) << json_reader.GetErrorMessage();
+ base::JSONReader::ValueWithError parsed_json =
+ base::JSONReader::ReadAndReturnValueWithError(json);
+ ASSERT_TRUE(parsed_json.value) << parsed_json.error_message;
// Validate the allocators summary.
const base::Value* malloc_summary =
- result.value().FindPath({"allocators", "malloc"});
+ parsed_json.value->FindPath({"allocators", "malloc"});
ASSERT_TRUE(malloc_summary);
const base::Value* malloc_size =
malloc_summary->FindPath({"attrs", "size", "value"});
@@ -445,7 +445,7 @@ TEST(ProfilingJsonExporterTest, LargeAllocation) {
// Validate allocators details.
// heaps_v2.allocators.malloc.sizes.reduce((a,s)=>a+s,0).
const base::Value* malloc =
- result.value().FindPath({"heaps_v2", "allocators", "malloc"});
+ parsed_json.value->FindPath({"heaps_v2", "allocators", "malloc"});
const base::Value* malloc_sizes = malloc->FindKey("sizes");
EXPECT_EQ(1u, malloc_sizes->GetList().size());
EXPECT_EQ(0x9876543210ul, malloc_sizes->GetList()[0].GetDouble());
diff --git a/chromium/components/services/paint_preview_compositor/BUILD.gn b/chromium/components/services/paint_preview_compositor/BUILD.gn
index b43af0d544c..2ac01001f7f 100644
--- a/chromium/components/services/paint_preview_compositor/BUILD.gn
+++ b/chromium/components/services/paint_preview_compositor/BUILD.gn
@@ -14,6 +14,8 @@ static_library("paint_preview_compositor") {
"paint_preview_compositor_impl.h",
"paint_preview_frame.cc",
"paint_preview_frame.h",
+ "skp_result.cc",
+ "skp_result.h",
]
deps = [
diff --git a/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.cc b/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.cc
index 87610de57d0..c196a3f6d81 100644
--- a/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.cc
+++ b/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.cc
@@ -6,12 +6,14 @@
#include <utility>
+#include "base/task/task_traits.h"
+#include "base/task/thread_pool.h"
#include "base/trace_event/common/trace_event_common.h"
#include "base/trace_event/trace_event.h"
#include "components/paint_preview/common/file_stream.h"
#include "components/paint_preview/common/proto/paint_preview.pb.h"
-#include "components/paint_preview/common/serial_utils.h"
#include "components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom.h"
+#include "components/services/paint_preview_compositor/skp_result.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkImageInfo.h"
@@ -19,6 +21,85 @@
namespace paint_preview {
+namespace {
+
+base::flat_map<base::UnguessableToken, SkpResult> DeserializeAllFrames(
+ base::flat_map<base::UnguessableToken, base::File>* file_map) {
+ TRACE_EVENT0("paint_preview",
+ "PaintPreviewCompositorImpl::DeserializeAllFrames");
+ std::vector<std::pair<base::UnguessableToken, SkpResult>> results;
+ results.reserve(file_map->size());
+
+ for (auto& it : *file_map) {
+ if (!it.second.IsValid())
+ continue;
+
+ SkpResult result;
+ FileRStream rstream(std::move(it.second));
+ SkDeserialProcs procs = MakeDeserialProcs(&result.ctx);
+ result.skp = SkPicture::MakeFromStream(&rstream, &procs);
+ if (!result.skp || result.skp->cullRect().width() == 0 ||
+ result.skp->cullRect().height() == 0) {
+ continue;
+ }
+
+ results.push_back({it.first, std::move(result)});
+ }
+
+ return base::flat_map<base::UnguessableToken, SkpResult>(std::move(results));
+}
+
+base::Optional<PaintPreviewFrame> BuildFrame(
+ const base::UnguessableToken& token,
+ const PaintPreviewFrameProto& frame_proto,
+ const base::flat_map<base::UnguessableToken, SkpResult>& results) {
+ TRACE_EVENT0("paint_preview", "PaintPreviewCompositorImpl::BuildFrame");
+ auto it = results.find(token);
+ if (it == results.end())
+ return base::nullopt;
+
+ const SkpResult& result = it->second;
+ PaintPreviewFrame frame;
+ frame.skp = result.skp;
+
+ for (const auto& id_pair : frame_proto.content_id_to_embedding_tokens()) {
+ // It is possible that subframes recorded in this map were not captured
+ // (e.g. renderer crash, closed, etc.). Missing subframes are allowable
+ // since having just the main frame is sufficient to create a preview.
+ auto rect_it = result.ctx.find(id_pair.content_id());
+ if (rect_it == result.ctx.end())
+ continue;
+
+ mojom::SubframeClipRect rect;
+ rect.frame_guid = base::UnguessableToken::Deserialize(
+ id_pair.embedding_token_high(), id_pair.embedding_token_low());
+ rect.clip_rect = rect_it->second;
+
+ if (!results.count(rect.frame_guid))
+ continue;
+
+ frame.subframe_clip_rects.push_back(rect);
+ }
+ return frame;
+}
+
+SkBitmap CreateBitmap(sk_sp<SkPicture> skp,
+ const gfx::Rect& clip_rect,
+ float scale_factor) {
+ TRACE_EVENT0("paint_preview", "PaintPreviewCompositorImpl::CreateBitmap");
+ SkBitmap bitmap;
+ bitmap.allocPixels(
+ SkImageInfo::MakeN32Premul(clip_rect.width(), clip_rect.height()));
+ SkCanvas canvas(bitmap);
+ SkMatrix matrix;
+ matrix.setScaleTranslate(scale_factor, scale_factor, -clip_rect.x(),
+ -clip_rect.y());
+ canvas.drawPicture(skp, &matrix, nullptr);
+ return bitmap;
+}
+
+} // namespace
+
PaintPreviewCompositorImpl::PaintPreviewCompositorImpl(
mojo::PendingReceiver<mojom::PaintPreviewCompositor> receiver,
base::OnceClosure disconnect_handler) {
@@ -48,12 +129,17 @@ void PaintPreviewCompositorImpl::BeginComposite(
PaintPreviewProto paint_preview;
bool ok = paint_preview.ParseFromArray(mapping.memory(), mapping.size());
if (!ok) {
+ DVLOG(1) << "Failed to parse proto.";
std::move(callback).Run(
mojom::PaintPreviewCompositor::Status::kDeserializingFailure,
std::move(response));
return;
}
- if (!AddFrame(paint_preview.root_frame(), &request->file_map, &response)) {
+ auto frames = DeserializeAllFrames(&request->file_map);
+
+ // Adding the root frame must succeed.
+ if (!AddFrame(paint_preview.root_frame(), frames, &response)) {
+ DVLOG(1) << "Root frame not found.";
std::move(callback).Run(
mojom::PaintPreviewCompositor::Status::kCompositingFailure,
std::move(response));
@@ -62,8 +148,10 @@ void PaintPreviewCompositorImpl::BeginComposite(
response->root_frame_guid = base::UnguessableToken::Deserialize(
paint_preview.root_frame().embedding_token_high(),
paint_preview.root_frame().embedding_token_low());
+
+ // Adding subframes is optional.
for (const auto& subframe_proto : paint_preview.subframes())
- AddFrame(subframe_proto, &request->file_map, &response);
+ AddFrame(subframe_proto, frames, &response);
std::move(callback).Run(mojom::PaintPreviewCompositor::Status::kSuccess,
std::move(response));
@@ -78,77 +166,50 @@ void PaintPreviewCompositorImpl::BitmapForFrame(
SkBitmap bitmap;
auto frame_it = frames_.find(frame_guid);
if (frame_it == frames_.end()) {
+ DVLOG(1) << "Frame not found for " << frame_guid.ToString();
std::move(callback).Run(
mojom::PaintPreviewCompositor::Status::kCompositingFailure, bitmap);
return;
}
- auto skp = frame_it->second.skp;
- bitmap.allocPixels(
- SkImageInfo::MakeN32Premul(clip_rect.width(), clip_rect.height()));
- SkCanvas canvas(bitmap);
- SkMatrix matrix;
- matrix.setScaleTranslate(scale_factor, scale_factor, -clip_rect.x(),
- -clip_rect.y());
- canvas.drawPicture(skp, &matrix, nullptr);
-
- std::move(callback).Run(mojom::PaintPreviewCompositor::Status::kSuccess,
- bitmap);
+ base::ThreadPool::PostTaskAndReplyWithResult(
+ FROM_HERE,
+ {base::TaskPriority::USER_VISIBLE, base::WithBaseSyncPrimitives()},
+ base::BindOnce(&CreateBitmap, frame_it->second.skp, clip_rect,
+ scale_factor),
+ base::BindOnce(std::move(callback),
+ mojom::PaintPreviewCompositor::Status::kSuccess));
}
void PaintPreviewCompositorImpl::SetRootFrameUrl(const GURL& url) {
url_ = url;
}
-PaintPreviewFrame PaintPreviewCompositorImpl::DeserializeFrame(
- const PaintPreviewFrameProto& frame_proto,
- base::File file_handle) {
- PaintPreviewFrame frame;
- FileRStream rstream(std::move(file_handle));
- DeserializationContext ctx;
- SkDeserialProcs procs = MakeDeserialProcs(&ctx);
-
- frame.skp = SkPicture::MakeFromStream(&rstream, &procs);
-
- for (const auto& id_pair : frame_proto.content_id_to_embedding_tokens()) {
- // It is possible that subframes recorded in this map were not captured
- // (e.g. renderer crash, closed, etc.). Missing subframes are allowable
- // since having just the main frame is sufficient to create a preview.
- auto rect_it = ctx.find(id_pair.content_id());
- if (rect_it == ctx.end())
- continue;
-
- mojom::SubframeClipRect rect;
- rect.frame_guid = base::UnguessableToken::Deserialize(
- id_pair.embedding_token_high(), id_pair.embedding_token_low());
- rect.clip_rect = rect_it->second;
- frame.subframe_clip_rects.push_back(rect);
- }
- return frame;
-}
-
bool PaintPreviewCompositorImpl::AddFrame(
const PaintPreviewFrameProto& frame_proto,
- FileMap* file_map,
+ const base::flat_map<base::UnguessableToken, SkpResult>& skp_map,
mojom::PaintPreviewBeginCompositeResponsePtr* response) {
base::UnguessableToken guid = base::UnguessableToken::Deserialize(
frame_proto.embedding_token_high(), frame_proto.embedding_token_low());
- auto file_it = file_map->find(guid);
- if (file_it == file_map->end() || !file_it->second.IsValid())
+
+ base::Optional<PaintPreviewFrame> maybe_frame =
+ BuildFrame(guid, frame_proto, skp_map);
+ if (!maybe_frame.has_value())
return false;
- PaintPreviewFrame frame =
- DeserializeFrame(frame_proto, std::move(file_it->second));
- file_map->erase(file_it);
+ const PaintPreviewFrame& frame = maybe_frame.value();
auto frame_data = mojom::FrameData::New();
SkRect sk_rect = frame.skp->cullRect();
frame_data->scroll_extents = gfx::Size(sk_rect.width(), sk_rect.height());
+ frame_data->scroll_offsets = gfx::Size(
+ frame_proto.has_scroll_offset_x() ? frame_proto.scroll_offset_x() : 0,
+ frame_proto.has_scroll_offset_y() ? frame_proto.scroll_offset_y() : 0);
frame_data->subframes.reserve(frame.subframe_clip_rects.size());
for (const auto& subframe_clip_rect : frame.subframe_clip_rects)
frame_data->subframes.push_back(subframe_clip_rect.Clone());
(*response)->frames.insert({guid, std::move(frame_data)});
- frames_.insert({guid, std::move(frame)});
+ frames_.insert({guid, std::move(maybe_frame.value())});
return true;
}
diff --git a/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.h b/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.h
index 8ff5049648f..3d17d6397db 100644
--- a/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.h
+++ b/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl.h
@@ -10,6 +10,7 @@
#include "base/callback.h"
#include "base/containers/flat_map.h"
#include "components/paint_preview/common/proto/paint_preview.pb.h"
+#include "components/paint_preview/common/serial_utils.h"
#include "components/services/paint_preview_compositor/paint_preview_frame.h"
#include "components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
@@ -19,6 +20,8 @@
namespace paint_preview {
+struct SkpResult;
+
class PaintPreviewCompositorImpl : public mojom::PaintPreviewCompositor {
public:
using FileMap = base::flat_map<base::UnguessableToken, base::File>;
@@ -45,16 +48,12 @@ class PaintPreviewCompositorImpl : public mojom::PaintPreviewCompositor {
void SetRootFrameUrl(const GURL& url) override;
private:
- // Deserializes the contents of |file_handle| and associates it with the
- // metadata in |frame_proto|.
- PaintPreviewFrame DeserializeFrame(const PaintPreviewFrameProto& frame_proto,
- base::File file_handle);
-
// Adds |frame_proto| to |frames_| and copies required data into |response|.
// Consumes the corresponding file in |file_map|. Returns true on success.
- bool AddFrame(const PaintPreviewFrameProto& frame_proto,
- FileMap* file_map,
- mojom::PaintPreviewBeginCompositeResponsePtr* response);
+ bool AddFrame(
+ const PaintPreviewFrameProto& frame_proto,
+ const base::flat_map<base::UnguessableToken, SkpResult>& skp_map,
+ mojom::PaintPreviewBeginCompositeResponsePtr* response);
mojo::Receiver<mojom::PaintPreviewCompositor> receiver_{this};
diff --git a/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl_unittest.cc b/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl_unittest.cc
index 3cb12b86702..ff2caa38562 100644
--- a/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl_unittest.cc
+++ b/chromium/components/services/paint_preview_compositor/paint_preview_compositor_impl_unittest.cc
@@ -12,6 +12,7 @@
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
+#include "base/test/task_environment.h"
#include "base/unguessable_token.h"
#include "components/paint_preview/common/file_stream.h"
#include "components/paint_preview/common/serial_utils.h"
@@ -215,6 +216,11 @@ TEST(PaintPreviewCompositorTest, TestBeginComposite) {
// results.
file_map.erase(kSubframe_0_0_ID);
expected_data.erase(kSubframe_0_0_ID);
+ // Remove the kSubframe_0_0_ID from the subframe list since it isn't
+ // available.
+ auto& vec = expected_data[kSubframe_0_ID]->subframes;
+ vec.front() = std::move(vec.back());
+ vec.pop_back();
mojom::PaintPreviewBeginCompositeRequestPtr request =
mojom::PaintPreviewBeginCompositeRequest::New();
@@ -422,6 +428,7 @@ TEST(PaintPreviewCompositorTest, TestInvalidRootFrame) {
}
TEST(PaintPreviewCompositorTest, TestComposite) {
+ base::test::TaskEnvironment task_environment;
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
PaintPreviewCompositorImpl compositor(mojo::NullReceiver(),
@@ -457,12 +464,14 @@ TEST(PaintPreviewCompositorTest, TestComposite) {
kRootFrameID, rect, 2,
base::BindOnce(&BitmapCallbackImpl,
mojom::PaintPreviewCompositor::Status::kSuccess, bitmap));
+ task_environment.RunUntilIdle();
compositor.BitmapForFrame(
base::UnguessableToken::Create(), rect, 2,
base::BindOnce(&BitmapCallbackImpl,
mojom::PaintPreviewCompositor::Status::kCompositingFailure,
bitmap));
+ task_environment.RunUntilIdle();
}
} // namespace paint_preview
diff --git a/chromium/components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom b/chromium/components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom
index ef3ba91e72a..d9b18e88514 100644
--- a/chromium/components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom
+++ b/chromium/components/services/paint_preview_compositor/public/mojom/paint_preview_compositor.mojom
@@ -32,6 +32,9 @@ struct FrameData {
// The dimensions of the frame.
gfx.mojom.Size scroll_extents;
+ // The initial scroll offsets of the frame.
+ gfx.mojom.Size scroll_offsets;
+
// This is not a map because a parent can, in theory, embed the same subframe
// multiple times.
array<SubframeClipRect> subframes;
diff --git a/chromium/components/services/paint_preview_compositor/skp_result.cc b/chromium/components/services/paint_preview_compositor/skp_result.cc
new file mode 100644
index 00000000000..890302d34c4
--- /dev/null
+++ b/chromium/components/services/paint_preview_compositor/skp_result.cc
@@ -0,0 +1,15 @@
+// Copyright 2020 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 "components/services/paint_preview_compositor/skp_result.h"
+
+namespace paint_preview {
+
+SkpResult::SkpResult() = default;
+SkpResult::~SkpResult() = default;
+
+SkpResult::SkpResult(SkpResult&& other) = default;
+SkpResult& SkpResult::operator=(SkpResult&& rhs) = default;
+
+} // namespace paint_preview
diff --git a/chromium/components/services/paint_preview_compositor/skp_result.h b/chromium/components/services/paint_preview_compositor/skp_result.h
new file mode 100644
index 00000000000..e065173b335
--- /dev/null
+++ b/chromium/components/services/paint_preview_compositor/skp_result.h
@@ -0,0 +1,31 @@
+// Copyright 2020 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 COMPONENTS_SERVICES_PAINT_PREVIEW_COMPOSITOR_SKP_RESULT_H_
+#define COMPONENTS_SERVICES_PAINT_PREVIEW_COMPOSITOR_SKP_RESULT_H_
+
+#include "components/paint_preview/common/serial_utils.h"
+#include "third_party/skia/include/core/SkPicture.h"
+#include "third_party/skia/include/core/SkRefCnt.h"
+
+namespace paint_preview {
+
+// Struct for containing an SkPicture and its deserialization context.
+struct SkpResult {
+ SkpResult();
+ ~SkpResult();
+
+ SkpResult(const SkpResult& other) = delete;
+ SkpResult& operator=(const SkpResult& rhs) = delete;
+
+ SkpResult(SkpResult&& other);
+ SkpResult& operator=(SkpResult&& rhs);
+
+ sk_sp<SkPicture> skp;
+ DeserializationContext ctx;
+};
+
+} // namespace paint_preview
+
+#endif // COMPONENTS_SERVICES_PAINT_PREVIEW_COMPOSITOR_SKP_RESULT_H_
diff --git a/chromium/components/services/print_compositor/print_compositor_impl.cc b/chromium/components/services/print_compositor/print_compositor_impl.cc
index 4e549b9b00d..9da9b63a2aa 100644
--- a/chromium/components/services/print_compositor/print_compositor_impl.cc
+++ b/chromium/components/services/print_compositor/print_compositor_impl.cc
@@ -358,8 +358,8 @@ mojom::PrintCompositor::Status PrintCompositorImpl::CompositeToPdf(
return mojom::PrintCompositor::Status::kHandleMapError;
}
- DeserializationContext subframes =
- GetDeserializationContext(subframe_content_map);
+ PictureDeserializationContext subframes =
+ GetPictureDeserializationContext(subframe_content_map);
// Read in content and convert it into pdf.
SkMemoryStream stream(shared_mem.memory(), shared_mem.size());
@@ -418,8 +418,8 @@ void PrintCompositorImpl::CompositeSubframe(FrameInfo* frame_info) {
frame_info->composited = true;
// Composite subframes first.
- DeserializationContext subframes =
- GetDeserializationContext(frame_info->subframe_content_map);
+ PictureDeserializationContext subframes =
+ GetPictureDeserializationContext(frame_info->subframe_content_map);
// Composite the entire frame.
SkMemoryStream stream(frame_info->serialized_content.memory(),
@@ -428,10 +428,10 @@ void PrintCompositorImpl::CompositeSubframe(FrameInfo* frame_info) {
frame_info->content = SkPicture::MakeFromStream(&stream, &procs);
}
-PrintCompositorImpl::DeserializationContext
-PrintCompositorImpl::GetDeserializationContext(
+PrintCompositorImpl::PictureDeserializationContext
+PrintCompositorImpl::GetPictureDeserializationContext(
const ContentToFrameMap& subframe_content_map) {
- DeserializationContext subframes;
+ PictureDeserializationContext subframes;
for (auto& content_info : subframe_content_map) {
uint32_t content_id = content_info.first;
uint64_t frame_guid = content_info.second;
diff --git a/chromium/components/services/print_compositor/print_compositor_impl.h b/chromium/components/services/print_compositor/print_compositor_impl.h
index 9d1d68256b3..c1d92bc675f 100644
--- a/chromium/components/services/print_compositor/print_compositor_impl.h
+++ b/chromium/components/services/print_compositor/print_compositor_impl.h
@@ -123,7 +123,8 @@ class PrintCompositorImpl : public mojom::PrintCompositor {
// The map needed during content deserialization. It stores the mapping
// between content id and its actual content.
- using DeserializationContext = base::flat_map<uint32_t, sk_sp<SkPicture>>;
+ using PictureDeserializationContext =
+ base::flat_map<uint32_t, sk_sp<SkPicture>>;
// Base structure to store a frame's content and its subframe
// content information.
@@ -218,7 +219,7 @@ class PrintCompositorImpl : public mojom::PrintCompositor {
// Composite the content of a subframe.
void CompositeSubframe(FrameInfo* frame_info);
- DeserializationContext GetDeserializationContext(
+ PictureDeserializationContext GetPictureDeserializationContext(
const ContentToFrameMap& subframe_content_map);
mojo::Receiver<mojom::PrintCompositor> receiver_{this};
diff --git a/chromium/components/services/storage/BUILD.gn b/chromium/components/services/storage/BUILD.gn
index fc75d2084e7..6a7fe891fe8 100644
--- a/chromium/components/services/storage/BUILD.gn
+++ b/chromium/components/services/storage/BUILD.gn
@@ -144,8 +144,8 @@ source_set("tests") {
"//components/services/storage/public/cpp",
"//components/services/storage/public/cpp/filesystem:tests",
"//components/services/storage/public/mojom",
- "//mojo/core/embedder",
"//mojo/public/cpp/bindings",
+ "//mojo/public/cpp/system",
"//sql:test_support",
"//testing/gmock",
"//testing/gtest",
diff --git a/chromium/components/services/storage/dom_storage/DEPS b/chromium/components/services/storage/dom_storage/DEPS
index b8295c5742c..d211be61481 100644
--- a/chromium/components/services/storage/dom_storage/DEPS
+++ b/chromium/components/services/storage/dom_storage/DEPS
@@ -10,7 +10,4 @@ specific_include_rules = {
"+sql",
"+storage/common/database",
],
- "session_storage_impl_unittest\.cc": [
- "+mojo/core",
- ],
}
diff --git a/chromium/components/services/storage/dom_storage/local_storage_impl.cc b/chromium/components/services/storage/dom_storage/local_storage_impl.cc
index db35923adc1..71b7be5bc39 100644
--- a/chromium/components/services/storage/dom_storage/local_storage_impl.cc
+++ b/chromium/components/services/storage/dom_storage/local_storage_impl.cc
@@ -520,7 +520,7 @@ void LocalStorageImpl::DeleteStorage(const url::Origin& origin,
found->second->storage_area()->ScheduleImmediateCommit();
} else if (database_) {
DeleteOrigins(
- database_.get(), {std::move(origin)},
+ database_.get(), {origin},
base::BindOnce([](base::OnceClosure callback,
leveldb::Status) { std::move(callback).Run(); },
std::move(callback)));
diff --git a/chromium/components/services/storage/dom_storage/session_storage_impl_unittest.cc b/chromium/components/services/storage/dom_storage/session_storage_impl_unittest.cc
index 7ce47e6431a..d875c4b8004 100644
--- a/chromium/components/services/storage/dom_storage/session_storage_impl_unittest.cc
+++ b/chromium/components/services/storage/dom_storage/session_storage_impl_unittest.cc
@@ -26,8 +26,8 @@
#include "components/services/storage/dom_storage/legacy_dom_storage_database.h"
#include "components/services/storage/dom_storage/storage_area_test_util.h"
#include "components/services/storage/dom_storage/testing_legacy_session_storage_database.h"
-#include "mojo/core/embedder/embedder.h"
#include "mojo/public/cpp/bindings/remote.h"
+#include "mojo/public/cpp/system/functions.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "url/gurl.h"
@@ -65,15 +65,14 @@ class SessionStorageImplTest : public testing::Test {
}
void SetUp() override {
- mojo::core::SetDefaultProcessErrorCallback(base::BindRepeating(
+ mojo::SetDefaultProcessErrorHandler(base::BindRepeating(
&SessionStorageImplTest::OnBadMessage, base::Unretained(this)));
}
void TearDown() override {
if (session_storage_)
ShutDownSessionStorage();
- mojo::core::SetDefaultProcessErrorCallback(
- mojo::core::ProcessErrorCallback());
+ mojo::SetDefaultProcessErrorHandler(base::NullCallback());
}
void OnBadMessage(const std::string& reason) { bad_message_called_ = true; }
diff --git a/chromium/components/services/storage/dom_storage/session_storage_metadata.cc b/chromium/components/services/storage/dom_storage/session_storage_metadata.cc
index 31beda77160..59f8a304ffd 100644
--- a/chromium/components/services/storage/dom_storage/session_storage_metadata.cc
+++ b/chromium/components/services/storage/dom_storage/session_storage_metadata.cc
@@ -4,6 +4,7 @@
#include "components/services/storage/dom_storage/session_storage_metadata.h"
+#include "base/logging.h"
#include "base/macros.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
diff --git a/chromium/components/services/storage/indexed_db/leveldb/leveldb_factory.cc b/chromium/components/services/storage/indexed_db/leveldb/leveldb_factory.cc
index 848d28fa2cb..c4082447fb4 100644
--- a/chromium/components/services/storage/indexed_db/leveldb/leveldb_factory.cc
+++ b/chromium/components/services/storage/indexed_db/leveldb/leveldb_factory.cc
@@ -4,6 +4,7 @@
#include "components/services/storage/indexed_db/leveldb/leveldb_factory.h"
+#include "base/logging.h"
#include "base/system/sys_info.h"
#include "components/services/storage/indexed_db/leveldb/leveldb_state.h"
#include "third_party/leveldatabase/leveldb_chrome.h"
diff --git a/chromium/components/services/storage/indexed_db/leveldb/leveldb_state.cc b/chromium/components/services/storage/indexed_db/leveldb/leveldb_state.cc
index c2f12753efa..0773cd8632e 100644
--- a/chromium/components/services/storage/indexed_db/leveldb/leveldb_state.cc
+++ b/chromium/components/services/storage/indexed_db/leveldb/leveldb_state.cc
@@ -17,9 +17,10 @@ scoped_refptr<LevelDBState> LevelDBState::CreateForDiskDB(
const leveldb::Comparator* comparator,
std::unique_ptr<leveldb::DB> database,
base::FilePath database_path) {
- return base::WrapRefCounted(new LevelDBState(
- nullptr, comparator, std::move(database), std::move(database_path),
- database_path.BaseName().AsUTF8Unsafe()));
+ auto name_for_tracing = database_path.BaseName().AsUTF8Unsafe();
+ return base::WrapRefCounted(
+ new LevelDBState(nullptr, comparator, std::move(database),
+ std::move(database_path), std::move(name_for_tracing)));
}
// static
diff --git a/chromium/components/services/storage/indexed_db/scopes/leveldb_scope.h b/chromium/components/services/storage/indexed_db/scopes/leveldb_scope.h
index 7e76964bb7e..10538eafcfd 100644
--- a/chromium/components/services/storage/indexed_db/scopes/leveldb_scope.h
+++ b/chromium/components/services/storage/indexed_db/scopes/leveldb_scope.h
@@ -12,9 +12,9 @@
#include <vector>
#include "base/callback.h"
+#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/containers/flat_map.h"
-#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/numerics/checked_math.h"
diff --git a/chromium/components/services/storage/indexed_db/scopes/scope_lock.h b/chromium/components/services/storage/indexed_db/scopes/scope_lock.h
index 0da1bc3670f..26067646c51 100644
--- a/chromium/components/services/storage/indexed_db/scopes/scope_lock.h
+++ b/chromium/components/services/storage/indexed_db/scopes/scope_lock.h
@@ -10,7 +10,6 @@
#include "base/callback.h"
#include "base/callback_helpers.h"
-#include "base/logging.h"
#include "base/macros.h"
#include "components/services/storage/indexed_db/scopes/scope_lock_range.h"
#include "third_party/leveldatabase/src/include/leveldb/comparator.h"
diff --git a/chromium/components/services/storage/indexed_db/scopes/scope_lock_range.h b/chromium/components/services/storage/indexed_db/scopes/scope_lock_range.h
index 6b0730dee4d..d941bdd079a 100644
--- a/chromium/components/services/storage/indexed_db/scopes/scope_lock_range.h
+++ b/chromium/components/services/storage/indexed_db/scopes/scope_lock_range.h
@@ -8,7 +8,6 @@
#include <iosfwd>
#include <vector>
-#include "base/logging.h"
#include "third_party/leveldatabase/src/include/leveldb/comparator.h"
#include "third_party/leveldatabase/src/include/leveldb/slice.h"
diff --git a/chromium/components/services/storage/indexed_db/scopes/scopes_lock_manager.h b/chromium/components/services/storage/indexed_db/scopes/scopes_lock_manager.h
index b29217473f4..6f515f2940e 100644
--- a/chromium/components/services/storage/indexed_db/scopes/scopes_lock_manager.h
+++ b/chromium/components/services/storage/indexed_db/scopes/scopes_lock_manager.h
@@ -10,7 +10,6 @@
#include "base/callback.h"
#include "base/containers/flat_set.h"
-#include "base/logging.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "components/services/storage/indexed_db/scopes/scope_lock.h"
diff --git a/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.cc b/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.cc
index c8ba2323217..15b64768a65 100644
--- a/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.cc
+++ b/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.cc
@@ -97,10 +97,12 @@ leveldb::Status TransactionalLevelDBTransaction::Rollback() {
}
std::unique_ptr<TransactionalLevelDBIterator>
-TransactionalLevelDBTransaction::CreateIterator() {
- leveldb::Status s = scope_->WriteChangesAndUndoLog();
+TransactionalLevelDBTransaction::CreateIterator(leveldb::Status& s) {
+ s = scope_->WriteChangesAndUndoLog();
if (!s.ok() && !s.IsNotFound())
return nullptr;
+ // Only return a "not ok" if the returned iterator is null.
+ s = leveldb::Status::OK();
std::unique_ptr<TransactionalLevelDBIterator> it = db_->CreateIterator(
weak_factory_.GetWeakPtr(), db_->DefaultReadOptions());
loaded_iterators_.insert(it.get());
diff --git a/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.h b/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.h
index 02dee6459fa..976cd266cca 100644
--- a/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.h
+++ b/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction.h
@@ -65,8 +65,9 @@ class TransactionalLevelDBTransaction
leveldb::Status Rollback() WARN_UNUSED_RESULT;
// The returned iterator must be destroyed before the destruction of this
- // transaction.
- std::unique_ptr<TransactionalLevelDBIterator> CreateIterator();
+ // transaction. This may return null, if it does, status will explain why.
+ std::unique_ptr<TransactionalLevelDBIterator> CreateIterator(
+ leveldb::Status& status);
uint64_t GetTransactionSize() const;
diff --git a/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction_unittest.cc b/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction_unittest.cc
index 124ca34475e..52432a73758 100644
--- a/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction_unittest.cc
+++ b/chromium/components/services/storage/indexed_db/transactional_leveldb/transactional_leveldb_transaction_unittest.cc
@@ -216,8 +216,10 @@ TEST_F(TransactionalLevelDBTransactionTest, Iterator) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
it->Seek(std::string("b"));
@@ -280,20 +282,26 @@ TEST_F(TransactionalLevelDBTransactionTest, IterationWithEvictedCursors) {
CreateTransaction();
std::unique_ptr<TransactionalLevelDBIterator> evicted_normal_location =
- transaction->CreateIterator();
+ transaction->CreateIterator(status);
+ ASSERT_TRUE(status.ok());
std::unique_ptr<TransactionalLevelDBIterator> evicted_before_start =
- transaction->CreateIterator();
+ transaction->CreateIterator(status);
+ ASSERT_TRUE(status.ok());
std::unique_ptr<TransactionalLevelDBIterator> evicted_after_end =
- transaction->CreateIterator();
+ transaction->CreateIterator(status);
+ ASSERT_TRUE(status.ok());
std::unique_ptr<TransactionalLevelDBIterator> it1 =
- transaction->CreateIterator();
+ transaction->CreateIterator(status);
+ ASSERT_TRUE(status.ok());
std::unique_ptr<TransactionalLevelDBIterator> it2 =
- transaction->CreateIterator();
+ transaction->CreateIterator(status);
+ ASSERT_TRUE(status.ok());
std::unique_ptr<TransactionalLevelDBIterator> it3 =
- transaction->CreateIterator();
+ transaction->CreateIterator(status);
+ ASSERT_TRUE(status.ok());
evicted_normal_location->Seek("b-key1");
evicted_before_start->Seek("b-key1");
@@ -360,8 +368,10 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorReloadingNext) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
it->Seek(std::string("b"));
ASSERT_TRUE(it->IsValid());
@@ -397,8 +407,10 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorReloadingPrev) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
it->SeekToLast();
ASSERT_TRUE(it->IsValid());
@@ -430,8 +442,10 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorSkipsScopesMetadata) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
// Should skip metadata, and go to key1.
it->Seek("");
@@ -462,8 +476,10 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorReflectsInitialChanges) {
TransactionPut(transaction.get(), key1, value);
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
it->Seek("");
ASSERT_TRUE(it->IsValid());
@@ -596,7 +612,8 @@ TEST_P(LevelDBTransactionRangeTest, RemoveRangeIteratorRetainsKey) {
leveldb::Status status;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction_->CreateIterator();
+ transaction_->CreateIterator(status);
+ ASSERT_TRUE(status.ok());
status = it->Seek(key_in_range1_);
EXPECT_TRUE(status.ok());
EXPECT_TRUE(it->IsValid());
@@ -643,10 +660,12 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorValueStaysTheSame) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
- leveldb::Status s = it->Seek(std::string("b-key1"));
+ s = it->Seek(std::string("b-key1"));
ASSERT_TRUE(it->IsValid());
EXPECT_TRUE(s.ok());
@@ -685,10 +704,12 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorPutInvalidation) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
- leveldb::Status s = it->Seek(std::string("b-key1"));
+ s = it->Seek(std::string("b-key1"));
ASSERT_TRUE(it->IsValid());
EXPECT_TRUE(s.ok());
@@ -767,10 +788,12 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorRemoveInvalidation) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
- leveldb::Status s = it->Seek(std::string("b-key1"));
+ s = it->Seek(std::string("b-key1"));
ASSERT_TRUE(it->IsValid());
EXPECT_TRUE(s.ok());
@@ -834,10 +857,12 @@ TEST_F(TransactionalLevelDBTransactionTest, IteratorGoesInvalidAfterRemove) {
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
- leveldb::Status s = it->Seek(std::string("b-key1"));
+ s = it->Seek(std::string("b-key1"));
ASSERT_TRUE(it->IsValid());
EXPECT_TRUE(s.ok());
@@ -905,10 +930,12 @@ TEST_F(TransactionalLevelDBTransactionTest,
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
- leveldb::Status s = it->Seek(std::string("b-key1"));
+ s = it->Seek(std::string("b-key1"));
ASSERT_TRUE(it->IsValid());
EXPECT_TRUE(s.ok());
@@ -940,10 +967,12 @@ TEST_F(TransactionalLevelDBTransactionTest,
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
- leveldb::Status s = it->Seek(std::string("b-key2"));
+ s = it->Seek(std::string("b-key2"));
ASSERT_TRUE(it->IsValid());
EXPECT_TRUE(s.ok());
@@ -976,10 +1005,12 @@ TEST_F(TransactionalLevelDBTransactionTest,
scoped_refptr<TransactionalLevelDBTransaction> transaction =
CreateTransaction();
+ leveldb::Status s;
std::unique_ptr<TransactionalLevelDBIterator> it =
- transaction->CreateIterator();
+ transaction->CreateIterator(s);
+ ASSERT_TRUE(s.ok());
- leveldb::Status s = it->Seek(std::string("b-key1"));
+ s = it->Seek(std::string("b-key1"));
ASSERT_TRUE(it->IsValid());
EXPECT_TRUE(s.ok());
diff --git a/chromium/components/services/storage/public/mojom/BUILD.gn b/chromium/components/services/storage/public/mojom/BUILD.gn
index deca2936c52..454e9d47add 100644
--- a/chromium/components/services/storage/public/mojom/BUILD.gn
+++ b/chromium/components/services/storage/public/mojom/BUILD.gn
@@ -6,6 +6,7 @@ import("//mojo/public/tools/bindings/mojom.gni")
mojom("mojom") {
sources = [
+ "blob_storage_context.mojom",
"indexed_db_control.mojom",
"indexed_db_control_test.mojom",
"local_storage_control.mojom",
@@ -21,12 +22,14 @@ mojom("mojom") {
public_deps = [
"//components/services/storage/public/mojom/filesystem",
"//mojo/public/mojom/base",
+ "//third_party/blink/public/mojom:mojom_core",
"//third_party/blink/public/mojom:mojom_modules",
"//third_party/blink/public/mojom:mojom_platform",
"//url/mojom:url_mojom_origin",
]
overridden_deps = [
+ "//third_party/blink/public/mojom:mojom_core",
"//third_party/blink/public/mojom:mojom_modules",
"//third_party/blink/public/mojom:mojom_platform",
]
diff --git a/chromium/components/services/storage/public/mojom/blob_storage_context.mojom b/chromium/components/services/storage/public/mojom/blob_storage_context.mojom
new file mode 100644
index 00000000000..798a1083bfb
--- /dev/null
+++ b/chromium/components/services/storage/public/mojom/blob_storage_context.mojom
@@ -0,0 +1,88 @@
+// Copyright 2019 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.
+
+module storage.mojom;
+
+import "mojo/public/mojom/base/big_buffer.mojom";
+import "mojo/public/mojom/base/file_path.mojom";
+import "mojo/public/mojom/base/time.mojom";
+import "third_party/blink/public/mojom/blob/blob.mojom";
+
+// A reader for the data and side data in a cache storage entry.
+interface BlobDataItemReader {
+ // Causes a subrange of the contents of this entry to be written into the
+ // given data pipe. Returns the net::Error result.
+ Read(uint64 offset, uint64 length, handle<data_pipe_producer> pipe)
+ => (int32 success);
+ // Reads the side-data (if any) associated with this entry. Returns
+ // a net::Error result and the data, if any.
+ ReadSideData() => (int32 success, mojo_base.mojom.BigBuffer data);
+};
+
+// The type of BlobDataItem. Used for histograms.
+enum BlobDataItemType {
+ kUnknown, // Type not known.
+ kCacheStorage, // Data comes from the cache storage system.
+ kIndexedDB, // Data comes from the IndexedDB storage system.
+};
+
+// A remote representation of a BlobDataItem::DataHandle for cache storage.
+struct BlobDataItem {
+ BlobDataItemType type;
+
+ // The size of the normal data. BlobDataItem::DataHandle needs this
+ // synchronously, which is why it is in a struct and not the interface.
+ uint64 size;
+
+ // The size of the side data. If this is zero, reader.ReadSideData()
+ // should not be called, and there is no side data.
+ uint64 side_data_size;
+
+ // The mime type of this data item. This is empty if unknown.
+ string content_type;
+
+ // An interface to read the normal and side data of this entry.
+ pending_remote<BlobDataItemReader> reader;
+};
+
+// The result of writing a blob to disk.
+enum WriteBlobToFileResult {
+ kError, // There was an error writing the blob to a file.
+ kBadPath, // The path given is not accessible or has references.
+ kInvalidBlob, // The blob is invalid and cannot be read.
+ kIOError, // Error writing bytes on disk.
+ kTimestampError, // Error writing the last modified timestamp.
+ kSuccess,
+};
+
+// This interface is the primary access point to the browser's blob system
+// for chrome internals. This interface lives in the browser process. This is a
+// simplified version of the blink.mojom.BlobRegistry interface.
+//
+// This interface has enhanced capabilities that should NOT be accessible to a
+// renderer, which is why it is a separate interface. For example,
+// WriteBlobToFile writes a blob to an arbitrary file path.
+interface BlobStorageContext {
+ // Create a blob with a particular uuid and consisting of a single
+ // BlobDataItem::DataHandle constructed from |item|.
+ RegisterFromDataItem(pending_receiver<blink.mojom.Blob> blob, string uuid,
+ BlobDataItem item);
+ // Create a blob with a particular uuid whose contents are contained
+ // in |data|.
+ RegisterFromMemory(pending_receiver<blink.mojom.Blob> blob, string uuid,
+ mojo_base.mojom.BigBuffer data);
+
+ // Writes the given blob to the given file path. If the given |path| is not
+ // under the blob storage context's profile directory or if it has references
+ // (like "..") then the implementation returns kBadPath. If the directory
+ // that contains the file at |path| does not exist, then this function will
+ // return kIOError. If a file already exists at |path| then it is
+ // overwritten. If |flush_on_write| is true, then Flush will be called on the
+ // new file before it is closed.
+ WriteBlobToFile(pending_remote<blink.mojom.Blob> blob,
+ mojo_base.mojom.FilePath path,
+ bool flush_on_write,
+ mojo_base.mojom.Time? last_modified)
+ => (WriteBlobToFileResult result);
+};
diff --git a/chromium/components/services/storage/public/mojom/indexed_db_control.mojom b/chromium/components/services/storage/public/mojom/indexed_db_control.mojom
index 3222711095a..f463dba02f1 100644
--- a/chromium/components/services/storage/public/mojom/indexed_db_control.mojom
+++ b/chromium/components/services/storage/public/mojom/indexed_db_control.mojom
@@ -35,6 +35,15 @@ struct IndexedDBStorageUsageInfo {
mojo_base.mojom.Time last_modified_time;
};
+// Indicates a policy update for a specific origin.
+struct IndexedDBStoragePolicyUpdate {
+ // The origin to which this policy applies.
+ url.mojom.Origin origin;
+
+ // Indicates whether data for this origin should be purged on shutdown.
+ bool purge_on_shutdown;
+};
+
// Communicates with IndexedDB clients about changes in IndexedDB.
interface IndexedDBObserver {
// This function is called when the size of the usage for a particular origin
@@ -92,6 +101,10 @@ interface IndexedDBControl {
// Adds an observer to be notified about modifications to IndexedDB.
AddObserver(pending_remote<IndexedDBObserver> observer);
+ // Applies changes to data retention policy which are relevant at shutdown.
+ // See IndexedDBStoragePolicyUpdate.
+ ApplyPolicyUpdates(array<IndexedDBStoragePolicyUpdate> policy_updates);
+
// Binds the testing interface for extra functionality only available in
// tests.
BindTestInterface(pending_receiver<IndexedDBControlTest> receiver);
diff --git a/chromium/components/services/storage/public/mojom/service_worker_storage_control.mojom b/chromium/components/services/storage/public/mojom/service_worker_storage_control.mojom
index dac94fb9898..b8c9744f6c3 100644
--- a/chromium/components/services/storage/public/mojom/service_worker_storage_control.mojom
+++ b/chromium/components/services/storage/public/mojom/service_worker_storage_control.mojom
@@ -17,15 +17,33 @@ struct SerializedServiceWorkerRegistration {
array<ServiceWorkerResourceRecord> resources;
};
+// An interface that is used to keep track of which service worker versions are
+// being used by clients of the storage service. This is an empty interface that
+// is mapped internally by the storage service to a single version.
+//
+// This is used to decide when it's safe to purge resources for a service worker
+// version whose registration has been deleted. A service worker version can be
+// still be used and may need to be started and stopped multiple times after
+// unregistration. The client of the storage service should hold on to the
+// reference as long as it's using the version, i.e., by making the reference
+// owned by the C++ ServiceWorkerVersion instance.
+interface ServiceWorkerLiveVersionRef {};
+
// Conveys a result of finding a registration. If a registration is found,
-// |status| will be kOk. |registration| and |resources| are null and empty
-// if there is no matching registration.
+// |status| will be kOk. |version_reference|, |registration| and |resources| are
+// null or empty if there is no matching registration.
//
// The Storage Service (components/services/storage) supplies this
// information and the //content consumes the information.
struct ServiceWorkerFindRegistrationResult {
+ // The result of a find operation.
ServiceWorkerDatabaseStatus status;
+ // A reference to a service worker version associated with
+ // |registration->version_id|.
+ pending_remote<ServiceWorkerLiveVersionRef>? version_reference;
+ // Stored registration.
ServiceWorkerRegistrationData? registration;
+ // Resources associated with |registration|.
array<ServiceWorkerResourceRecord> resources;
};
@@ -137,10 +155,13 @@ interface ServiceWorkerStorageControl {
// storage. Returns blink::mojom::kInvalidServiceWorkerRegistrationId if the
// storage is disabled.
GetNewRegistrationId() => (int64 registration_id);
- // Returns a new service worker version id which is guaranteed to be unique
- // in the storage. Returns blink::mojom::kInvalidServiceWorkerVersionId if
- // the storage is disabled.
- GetNewVersionId() => (int64 version_id);
+ // Returns a new service worker version id, which is guaranteed to be unique
+ // in the storage, and a reference to the version id.
+ // blink::mojom::kInvalidServiceWorkerVersionId and null reference are
+ // returned if the storage is disabled.
+ GetNewVersionId() =>
+ (int64 version_id,
+ pending_remote<ServiceWorkerLiveVersionRef>? version_reference);
// Returns a new resource id which is guaranteed to be unique in the storage.
// Returns blink::mojom::kInvalidServiceWorkerResourceId if the storage
// is disabled.
@@ -157,6 +178,19 @@ interface ServiceWorkerStorageControl {
int64 resource_id,
pending_receiver<ServiceWorkerResourceMetadataWriter> writer);
+ // Puts |resource_id| on the uncommitted resource list in storage. Once
+ // |resource_id| is put on the uncommitted resource list, the corresponding
+ // resource is considered to be existing in storage but it's not associated
+ // with any registration yet.
+ // StoreRegistration() or DoomUncommittedResources() needs to be
+ // called later to clear the |resource_id| from the uncommitted resource list.
+ StoreUncommittedResourceId(int64 resource_id, url.mojom.Url origin) =>
+ (ServiceWorkerDatabaseStatus status);
+
+ // Removes |resource_ids| from the uncommitted resource list.
+ DoomUncommittedResources(array<int64> resource_ids) =>
+ (ServiceWorkerDatabaseStatus status);
+
// Gets user data associated with the given |registration_id|.
// Succeeds only when all keys are found. On success, the size and the order
// of |values| are the same as |keys|.