diff options
Diffstat (limited to 'chromium/components/services')
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(¶ms); // 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|. |