chromium/chrome/browser/ui/ash/app_access/app_access_notifier.cc

// 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());
}