// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/child_accounts/time_limits/app_service_wrapper.h"
#include <map>
#include <optional>
#include <set>
#include <string>
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/unguessable_token.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_time_limit_utils.h"
#include "chrome/browser/ash/child_accounts/time_limits/app_types.h"
#include "chrome/browser/profiles/profile.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/services/app_service/public/cpp/icon_effects.h"
#include "components/services/app_service/public/cpp/instance_update.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "ui/gfx/image/image_skia.h"
namespace ash {
namespace app_time {
namespace {
// Gets AppId from |update|.
AppId AppIdFromAppUpdate(const apps::AppUpdate& update) {
bool is_arc = update.AppType() == apps::AppType::kArc;
return AppId(update.AppType(),
is_arc ? update.PublisherId() : update.AppId());
}
// Gets AppId from |update|.
AppId AppIdFromInstanceUpdate(const apps::InstanceUpdate& update,
apps::AppRegistryCache* app_cache) {
AppId app_id(apps::AppType::kUnknown, update.AppId());
app_cache->ForOneApp(update.AppId(),
[&app_id](const apps::AppUpdate& update) {
app_id = AppIdFromAppUpdate(update);
});
return app_id;
}
// Gets app service id from |app_id|.
std::string AppServiceIdFromAppId(const AppId& app_id, Profile* profile) {
return app_id.app_type() == apps::AppType::kArc
? arc::ArcPackageNameToAppId(app_id.app_id(), profile)
: app_id.app_id();
}
apps::PauseData PauseAppInfoToPauseData(const PauseAppInfo& pause_info) {
apps::PauseData details;
details.should_show_pause_dialog = pause_info.show_pause_dialog;
details.hours = pause_info.daily_limit.InHours();
details.minutes = pause_info.daily_limit.InMinutes() % 60;
return details;
}
} // namespace
AppServiceWrapper::AppServiceWrapper(Profile* profile) : profile_(profile) {
app_registry_cache_observer_.Observe(&GetAppCache());
instance_registry_observation_.Observe(&GetInstanceRegistry());
}
AppServiceWrapper::~AppServiceWrapper() = default;
void AppServiceWrapper::PauseApp(const PauseAppInfo& pause_app) {
const std::map<std::string, apps::PauseData> apps{
{GetAppServiceId(pause_app.app_id), PauseAppInfoToPauseData(pause_app)}};
GetAppProxy()->PauseApps(apps);
}
void AppServiceWrapper::PauseApps(
const std::vector<PauseAppInfo>& paused_apps) {
std::map<std::string, apps::PauseData> apps;
for (const auto& entry : paused_apps) {
apps[GetAppServiceId(entry.app_id)] = PauseAppInfoToPauseData(entry);
}
GetAppProxy()->PauseApps(apps);
}
void AppServiceWrapper::ResumeApp(const AppId& app_id) {
const std::set<std::string> apps{GetAppServiceId(app_id)};
GetAppProxy()->UnpauseApps(apps);
}
void AppServiceWrapper::LaunchApp(const std::string& app_service_id) {
GetAppProxy()->Launch(
app_service_id, ui::EF_NONE, apps::LaunchSource::kFromParentalControls,
std::make_unique<apps::WindowInfo>(display::kDefaultDisplayId));
}
std::vector<AppId> AppServiceWrapper::GetInstalledApps() const {
std::vector<AppId> installed_apps;
GetAppCache().ForEachApp(
[&installed_apps, this](const apps::AppUpdate& update) {
if (!apps_util::IsInstalled(update.Readiness()))
return;
const AppId app_id = AppIdFromAppUpdate(update);
if (!ShouldIncludeApp(app_id))
return;
installed_apps.push_back(app_id);
});
return installed_apps;
}
bool AppServiceWrapper::IsHiddenArcApp(const AppId& app_id) const {
if (app_id.app_type() != apps::AppType::kArc)
return false;
bool is_hidden = false;
const std::string app_service_id = AppServiceIdFromAppId(app_id, profile_);
GetAppCache().ForOneApp(
app_service_id, [&is_hidden](const apps::AppUpdate& update) {
if (!apps_util::IsInstalled(update.Readiness()))
return;
is_hidden = !update.ShowInLauncher().value_or(false);
});
return is_hidden;
}
std::vector<AppId> AppServiceWrapper::GetHiddenArcApps() const {
std::vector<AppId> hidden_arc_apps;
GetAppCache().ForEachApp([&hidden_arc_apps](const apps::AppUpdate& update) {
if (!apps_util::IsInstalled(update.Readiness()))
return;
const AppId app_id = AppIdFromAppUpdate(update);
if (app_id.app_type() != apps::AppType::kArc ||
update.ShowInLauncher().value_or(true)) {
return;
}
hidden_arc_apps.push_back(app_id);
});
return hidden_arc_apps;
}
std::string AppServiceWrapper::GetAppName(const AppId& app_id) const {
const std::string app_service_id = AppServiceIdFromAppId(app_id, profile_);
DCHECK(!app_service_id.empty());
std::string app_name;
GetAppCache().ForOneApp(
app_service_id,
[&app_name](const apps::AppUpdate& update) { app_name = update.Name(); });
return app_name;
}
void AppServiceWrapper::GetAppIcon(
const AppId& app_id,
int size_hint_in_dp,
base::OnceCallback<void(std::optional<gfx::ImageSkia>)> on_icon_ready)
const {
const std::string app_service_id = AppServiceIdFromAppId(app_id, profile_);
DCHECK(!app_service_id.empty());
GetAppProxy()->LoadIconWithIconEffects(
app_service_id, apps::IconEffects::kNone, apps::IconType::kStandard,
size_hint_in_dp,
/* allow_placeholder_icon */ false,
base::BindOnce(
[](base::OnceCallback<void(std::optional<gfx::ImageSkia>)> callback,
apps::IconValuePtr icon_value) {
if (!icon_value ||
icon_value->icon_type != apps::IconType::kStandard) {
std::move(callback).Run(std::nullopt);
} else {
std::move(callback).Run(icon_value->uncompressed);
}
},
std::move(on_icon_ready)));
}
std::string AppServiceWrapper::GetAppServiceId(const AppId& app_id) const {
return AppServiceIdFromAppId(app_id, profile_);
}
bool AppServiceWrapper::IsAppInstalled(const std::string& app_id) {
return GetAppCache().GetAppType(app_id) != apps::AppType::kUnknown;
}
AppId AppServiceWrapper::AppIdFromAppServiceId(
const std::string& app_service_id,
apps::AppType app_type) const {
std::optional<AppId> app_id;
GetAppCache().ForOneApp(app_service_id,
[&app_id](const apps::AppUpdate& update) {
app_id = AppIdFromAppUpdate(update);
});
DCHECK(app_id);
return *app_id;
}
void AppServiceWrapper::AddObserver(EventListener* listener) {
DCHECK(listener);
listeners_.AddObserver(listener);
}
void AppServiceWrapper::RemoveObserver(EventListener* listener) {
DCHECK(listener);
listeners_.RemoveObserver(listener);
}
void AppServiceWrapper::OnAppUpdate(const apps::AppUpdate& update) {
if (!update.ReadinessChanged())
return;
const AppId app_id = AppIdFromAppUpdate(update);
if (!ShouldIncludeApp(app_id))
return;
switch (update.Readiness()) {
case apps::Readiness::kReady:
for (auto& listener : listeners_)
if (update.StateIsNull()) {
// It is the first update about this app.
// Note that AppService does not store info between sessions and this
// will be called at the beginning of every session.
listener.OnAppInstalled(app_id);
} else {
listener.OnAppAvailable(app_id);
}
break;
case apps::Readiness::kUninstalledByUser:
case apps::Readiness::kUninstalledByNonUser:
for (auto& listener : listeners_)
listener.OnAppUninstalled(app_id);
break;
case apps::Readiness::kDisabledByUser:
case apps::Readiness::kDisabledByPolicy:
case apps::Readiness::kDisabledByBlocklist:
case apps::Readiness::kDisabledByLocalSettings:
for (auto& listener : listeners_)
listener.OnAppBlocked(app_id);
break;
default:
break;
}
}
void AppServiceWrapper::OnAppRegistryCacheWillBeDestroyed(
apps::AppRegistryCache* cache) {
app_registry_cache_observer_.Reset();
}
void AppServiceWrapper::OnInstanceUpdate(const apps::InstanceUpdate& update) {
if (!update.StateChanged())
return;
bool is_active = update.State() & apps::InstanceState::kActive;
bool is_destroyed = update.State() & apps::InstanceState::kDestroyed;
const AppId app_id = AppIdFromInstanceUpdate(update, &GetAppCache());
// When the window is destroyed, the app might be destroyed from extensions,
// then ShouldIncludeApp returns false. But if the app type is valid, listener
// should be notified as well to stop the activities on the app window.
if (!ShouldIncludeApp(app_id) &&
!(app_id.app_type() != apps::AppType::kUnknown && is_destroyed)) {
return;
}
for (auto& listener : listeners_) {
if (is_active) {
listener.OnAppActive(app_id, update.InstanceId(),
update.LastUpdatedTime());
} else {
listener.OnAppInactive(app_id, update.InstanceId(),
update.LastUpdatedTime());
}
if (is_destroyed) {
listener.OnAppDestroyed(app_id, update.InstanceId(),
update.LastUpdatedTime());
}
}
}
void AppServiceWrapper::OnInstanceRegistryWillBeDestroyed(
apps::InstanceRegistry* cache) {
instance_registry_observation_.Reset();
}
apps::AppServiceProxy* AppServiceWrapper::GetAppProxy() const {
return apps::AppServiceProxyFactory::GetForProfile(profile_);
}
apps::AppRegistryCache& AppServiceWrapper::GetAppCache() const {
return GetAppProxy()->AppRegistryCache();
}
apps::InstanceRegistry& AppServiceWrapper::GetInstanceRegistry() const {
return GetAppProxy()->InstanceRegistry();
}
bool AppServiceWrapper::ShouldIncludeApp(const AppId& app_id) const {
if (IsHiddenArcApp(app_id))
return false;
if (app_id.app_type() == apps::AppType::kChromeApp) {
const extensions::Extension* extension =
extensions::ExtensionRegistry::Get(profile_)->GetExtensionById(
app_id.app_id(),
extensions::ExtensionRegistry::IncludeFlag::EVERYTHING);
// If we are not able to find the extension, return false.
if (!extension)
return false;
// Some preinstalled apps that open in browser window are legacy packaged
// apps. Example Google Slides app.
return extension->is_hosted_app() || extension->is_legacy_packaged_app();
}
return app_id.app_type() == apps::AppType::kArc ||
app_id.app_type() == apps::AppType::kWeb;
}
} // namespace app_time
} // namespace ash