diff options
Diffstat (limited to 'chromium/components/services/app_service/README.md')
-rw-r--r-- | chromium/components/services/app_service/README.md | 504 |
1 files changed, 500 insertions, 4 deletions
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. |