// Copyright 2021 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/ui/ash/app_access/app_access_notifier.h"
#include <optional>
#include <string>
#include <vector>
#include "app_access_notifier.h"
#include "ash/constants/ash_features.h"
#include "ash/system/privacy/privacy_indicators_controller.h"
#include "ash/system/privacy_hub/camera_privacy_switch_controller.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/trace_event/trace_event.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/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "components/account_id/account_id.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache.h"
#include "components/services/app_service/public/cpp/app_capability_access_cache_wrapper.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/session_manager/core/session_manager.h"
#include "components/session_manager/session_manager_types.h"
namespace {
apps::AppCapabilityAccessCache* GetAppCapabilityAccessCache(
AccountId account_id) {
return apps::AppCapabilityAccessCacheWrapper::Get()
.GetAppCapabilityAccessCache(account_id);
}
std::optional<std::u16string> MapAppIdToShortName(
std::string app_id,
apps::AppCapabilityAccessCache* capability_cache,
apps::AppRegistryCache* registry_cache,
const std::set<std::string>& apps_accessing_sensor) {
DCHECK(capability_cache);
DCHECK(registry_cache);
for (const std::string& app : apps_accessing_sensor) {
std::optional<std::u16string> name;
registry_cache->ForOneApp(app,
[&app_id, &name](const apps::AppUpdate& update) {
if (update.AppId() == app_id) {
name = base::UTF8ToUTF16(update.ShortName());
}
});
if (name.has_value())
return name;
}
return std::nullopt;
}
// A helper to send `ash::CameraPrivacySwitchController` a notification when an
// application starts or stops using the camera. `application_added` is true
// when the application starts using the camera and false when the application
// stops using the camera.
void SendActiveCameraApplicationsChangedNotification(bool application_added) {
auto* camera_controller = ash::CameraPrivacySwitchController::Get();
CHECK(camera_controller);
camera_controller->ActiveApplicationsChanged(application_added);
}
} // namespace
AppAccessNotifier::AppAccessNotifier() {
// These checks are needed for testing, where SessionManager and/or
// UserManager may not exist.
session_manager::SessionManager* sm = session_manager::SessionManager::Get();
if (sm) {
session_manager_observation_.Observe(sm);
}
user_manager::UserManager* um = user_manager::UserManager::Get();
if (um) {
user_session_state_observation_.Observe(um);
}
CheckActiveUserChanged();
}
AppAccessNotifier::~AppAccessNotifier() = default;
// Returns names of apps accessing camera.
std::vector<std::u16string> AppAccessNotifier::GetAppsAccessingCamera() {
return GetAppsAccessingSensor(
&camera_using_app_ids_[active_user_account_id_],
base::BindOnce([](apps::AppCapabilityAccessCache& cache) {
return cache.GetAppsAccessingCamera();
}));
}
// Returns names of apps accessing microphone.
std::vector<std::u16string> AppAccessNotifier::GetAppsAccessingMicrophone() {
return GetAppsAccessingSensor(
&mic_using_app_ids_[active_user_account_id_],
base::BindOnce([](apps::AppCapabilityAccessCache& cache) {
return cache.GetAppsAccessingMicrophone();
}));
}
std::vector<std::u16string> AppAccessNotifier::GetAppsAccessingSensor(
const MruAppIdList* app_id_list,
base::OnceCallback<std::set<std::string>(apps::AppCapabilityAccessCache&)>
app_getter) {
apps::AppRegistryCache* reg_cache = GetActiveUserAppRegistryCache();
apps::AppCapabilityAccessCache* cap_cache =
GetActiveUserAppCapabilityAccessCache();
// A reg_cache and/or cap_cache of value nullptr is possible if we have no
// active user, e.g. the login screen, so we test and return empty list in
// that case instead of using DCHECK().
if (!reg_cache || !cap_cache || app_id_list->empty()) {
return {};
}
const std::set<std::string>& apps_accessing_sensor =
std::move(app_getter).Run(*cap_cache);
std::vector<std::u16string> app_names;
for (const auto& app_id : *app_id_list) {
std::optional<std::u16string> app_name = MapAppIdToShortName(
app_id, cap_cache, reg_cache, apps_accessing_sensor);
if (app_name.has_value())
app_names.push_back(app_name.value());
}
return app_names;
}
bool AppAccessNotifier::MapContainsAppId(const MruAppIdMap& id_map,
const std::string& app_id) {
auto it = id_map.find(active_user_account_id_);
if (it == id_map.end()) {
return false;
}
return base::Contains(it->second, app_id);
}
void AppAccessNotifier::OnCapabilityAccessUpdate(
const apps::CapabilityAccessUpdate& update) {
auto app_id = update.AppId();
const bool is_camera_used = update.Camera().value_or(false);
const bool is_microphone_used = update.Microphone().value_or(false);
// TODO(b/261444378): Avoid calculating the booleans and use update.*Changed()
const bool was_using_camera_already =
MapContainsAppId(camera_using_app_ids_, app_id);
const bool was_using_microphone_already =
MapContainsAppId(mic_using_app_ids_, app_id);
if (is_camera_used && !was_using_camera_already) {
// App with id `app_id` started using camera.
camera_using_app_ids_[active_user_account_id_].push_front(update.AppId());
SendActiveCameraApplicationsChangedNotification(/*application_added=*/true);
} else if (!is_camera_used && was_using_camera_already) {
// App with id `app_id` stopped using camera.
std::erase(camera_using_app_ids_[active_user_account_id_], update.AppId());
SendActiveCameraApplicationsChangedNotification(
/*application_added=*/false);
}
if (is_microphone_used && !was_using_microphone_already) {
// App with id `app_id` started using microphone.
mic_using_app_ids_[active_user_account_id_].push_front(update.AppId());
} else if (!is_microphone_used && was_using_microphone_already) {
// App with id `app_id` stopped using microphone.
std::erase(mic_using_app_ids_[active_user_account_id_], update.AppId());
}
// Privacy indicators is only enabled when Video Conference is disabled.
if (!ash::features::IsVideoConferenceEnabled()) {
auto* registry_cache = GetActiveUserAppRegistryCache();
if (!registry_cache) {
return;
}
auto app_type = registry_cache->GetAppType(app_id);
std::optional<base::RepeatingClosure> launch_settings_callback;
if (app_type == apps::AppType::kSystemWeb) {
// We don't have the capability to launch privacy settings for system web
// app, so we will disable the settings button for this type of app.
launch_settings_callback = std::nullopt;
} else {
launch_settings_callback =
base::BindRepeating(&AppAccessNotifier::LaunchAppSettings, app_id);
}
ash::PrivacyIndicatorsController::Get()->UpdatePrivacyIndicators(
app_id, /*app_name=*/GetAppShortNameFromAppId(app_id), is_camera_used,
is_microphone_used, /*delegate=*/
base::MakeRefCounted<ash::PrivacyIndicatorsNotificationDelegate>(
launch_settings_callback),
ash::PrivacyIndicatorsSource::kApps);
base::UmaHistogramEnumeration("Ash.PrivacyIndicators.AppAccessUpdate.Type",
registry_cache->GetAppType(app_id));
}
}
void AppAccessNotifier::OnAppCapabilityAccessCacheWillBeDestroyed(
apps::AppCapabilityAccessCache* cache) {
app_capability_access_cache_observation_.Reset();
}
//
// A couple of notes on why we have OnSessionStateChanged() and
// ActiveUserChanged(), i.e. why we observe both SessionManager and UserManager.
//
// The critical logic here is based on knowing when an app starts or stops
// attempting to use the microphone, and for this we observe the active user's
// AppCapabilityAccessCache. When the active user's AppCapabilityAccessCache
// changes, we need to stop observing any AppCapabilityAccessCache we were
// previously observing and start observing the currently active one. This is
// the job of CheckActiveUserChanged().
//
void AppAccessNotifier::OnSessionStateChanged() {
TRACE_EVENT0("ui", "AppAccessNotifier::OnSessionStateChanged");
session_manager::SessionState state =
session_manager::SessionManager::Get()->session_state();
if (state == session_manager::SessionState::ACTIVE) {
CheckActiveUserChanged();
session_manager_observation_.Reset();
}
}
void AppAccessNotifier::ActiveUserChanged(user_manager::User* active_user) {
CheckActiveUserChanged();
}
// static
std::optional<std::u16string> AppAccessNotifier::GetAppShortNameFromAppId(
std::string app_id) {
std::optional<std::u16string> name;
auto* registry_cache = GetActiveUserAppRegistryCache();
if (!registry_cache)
return name;
registry_cache->ForEachApp([&app_id, &name](const apps::AppUpdate& update) {
if (update.AppId() == app_id) {
name = base::UTF8ToUTF16(update.ShortName());
}
});
return name;
}
// static
void AppAccessNotifier::LaunchAppSettings(const std::string& app_id) {
Profile* profile = ProfileManager::GetActiveUserProfile();
if (!profile ||
!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
return;
}
auto* registry_cache = GetActiveUserAppRegistryCache();
if (!registry_cache) {
return;
}
auto app_type = registry_cache->GetAppType(app_id);
// We don't have the capability to launch privacy settings for system web
// app, so settings button is disabled for this type of app.
DCHECK(app_type != apps::AppType::kSystemWeb);
if (app_type == apps::AppType::kWeb) {
chrome::ShowAppManagementPage(profile, app_id,
ash::settings::AppManagementEntryPoint::
kPrivacyIndicatorsNotificationSettings);
} else {
apps::AppServiceProxyFactory::GetForProfile(profile)->OpenNativeSettings(
app_id);
}
base::UmaHistogramEnumeration("Ash.PrivacyIndicators.LaunchSettings",
registry_cache->GetAppType(app_id));
}
AccountId AppAccessNotifier::GetActiveUserAccountId() {
auto* manager = user_manager::UserManager::Get();
const user_manager::User* active_user = manager->GetActiveUser();
if (!active_user)
return EmptyAccountId();
return active_user->GetAccountId();
}
void AppAccessNotifier::CheckActiveUserChanged() {
AccountId id = GetActiveUserAccountId();
if (id == EmptyAccountId() || id == active_user_account_id_)
return;
if (active_user_account_id_ != EmptyAccountId()) {
app_capability_access_cache_observation_.Reset();
active_user_account_id_ = EmptyAccountId();
}
apps::AppCapabilityAccessCache* cap_cache = GetAppCapabilityAccessCache(id);
if (cap_cache) {
app_capability_access_cache_observation_.Observe(cap_cache);
active_user_account_id_ = id;
}
}
// static
apps::AppRegistryCache* AppAccessNotifier::GetActiveUserAppRegistryCache() {
Profile* profile = ProfileManager::GetActiveUserProfile();
if (!profile ||
!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
return nullptr;
}
apps::AppServiceProxy* proxy =
apps::AppServiceProxyFactory::GetForProfile(profile);
return &proxy->AppRegistryCache();
}
apps::AppCapabilityAccessCache*
AppAccessNotifier::GetActiveUserAppCapabilityAccessCache() {
return apps::AppCapabilityAccessCacheWrapper::Get()
.GetAppCapabilityAccessCache(GetActiveUserAccountId());
}