chromium/chrome/browser/ash/video_conference/video_conference_app_service_client.cc

// Copyright 2023 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/video_conference/video_conference_app_service_client.h"

#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/containers/contains.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/unguessable_token.h"
#include "chrome/browser/apps/app_service/app_service_proxy_ash.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/metrics/app_platform_metrics.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/video_conference/video_conference_manager_ash.h"
#include "chrome/browser/chromeos/video_conference/video_conference_ukm_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.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/user_manager/user_manager.h"
#include "services/metrics/public/cpp/ukm_recorder.h"

namespace ash {
namespace {
using video_conference::VideoConferenceUkmHelper;

VideoConferenceAppServiceClient* g_client_instance = nullptr;

crosapi::mojom::VideoConferenceAppType ToVideoConferenceAppType(
    apps::AppType app_type) {
  switch (app_type) {
    case apps::AppType::kArc:
      return crosapi::mojom::VideoConferenceAppType::kArcApp;
    case apps::AppType::kChromeApp:
    case apps::AppType::kStandaloneBrowserChromeApp:
      return crosapi::mojom::VideoConferenceAppType::kChromeApp;
    case apps::AppType::kWeb:
      return crosapi::mojom::VideoConferenceAppType::kWebApp;
    case apps::AppType::kExtension:
    case apps::AppType::kStandaloneBrowserExtension:
      return crosapi::mojom::VideoConferenceAppType::kChromeExtension;
    default:
      return crosapi::mojom::VideoConferenceAppType::kAppServiceUnknown;
  }
}

bool IsPermissionAsked(const apps::PermissionPtr& permission) {
  return absl::holds_alternative<apps::TriState>(permission->value) &&
         absl::get<apps::TriState>(permission->value) == apps::TriState::kAsk;
}

}  // namespace

VideoConferenceAppServiceClient::VideoConferenceAppServiceClient()
    : client_id_(base::UnguessableToken::Create()),
      status_(crosapi::mojom::VideoConferenceMediaUsageStatus::New(
          /*client_id=*/client_id_,
          /*has_media_app=*/false,
          /*has_camera_permission=*/false,
          /*has_microphone_permission=*/false,
          /*is_capturing_camera=*/false,
          /*is_capturing_microphone=*/false,
          /*is_capturing_screen=*/false)) {
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->RegisterCppClient(this, client_id_);

  session_observation_.Observe(Shell::Get()->session_controller());

  // Initialize with current session state.
  OnSessionStateChanged(Shell::Get()->session_controller()->GetSessionState());

  g_client_instance = this;
}

VideoConferenceAppServiceClient::~VideoConferenceAppServiceClient() {
  // C++ clients are responsible for manually calling |UnregisterClient| on the
  // manager when disconnecting.
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->UnregisterClient(client_id_);

  g_client_instance = nullptr;
}

void VideoConferenceAppServiceClient::GetMediaApps(
    GetMediaAppsCallback callback) {
  std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr> apps;

  for (const auto& [app_id, app_state] : id_to_app_state_) {
    const std::string app_name = GetAppName(app_id);
    // app_name should not be empty.
    if (app_name.empty()) {
      continue;
    }

    apps.push_back(crosapi::mojom::VideoConferenceMediaAppInfo::New(
        /*id=*/app_state.token,
        /*last_activity_time=*/app_state.last_activity_time,
        /*is_capturing_camera=*/app_state.is_capturing_camera,
        /*is_capturing_microphone=*/app_state.is_capturing_microphone,
        /*is_capturing_screen=*/false,
        /*title=*/base::UTF8ToUTF16(app_name),
        /*url=*/std::nullopt,
        /*app_type=*/ToVideoConferenceAppType(GetAppType(app_id))));
  }

  std::move(callback).Run(std::move(apps));
}

void VideoConferenceAppServiceClient::ReturnToApp(
    const base::UnguessableToken& token,
    ReturnToAppCallback callback) {
  // Go through `id_to_app_state_` to find possible app to reactivate.
  // This loop is inevitable unless we use multiple maps which also makes things
  // complicated.
  AppIdString app_id;
  for (const auto& [id, app_state] : id_to_app_state_) {
    if (app_state.token == token) {
      app_id = id;
      break;
    }
  }

  if (app_id.empty()) {
    // This will happen very frequently; this is not an error, but expected
    // behavior. This indicates that the app represented by this id does not
    // belong to this client.
    std::move(callback).Run(false);
    return;
  }

  for (const apps::Instance* instance :
       instance_registry_->GetInstances(app_id)) {
    // This is required in unit tests to reactivate an app.
    instance->Window()->Show();
    // This is required in virtual desktop to reactivate an arc++ app.
    instance->Window()->Focus();
  }
  std::move(callback).Run(true);
}

void VideoConferenceAppServiceClient::SetSystemMediaDeviceStatus(
    crosapi::mojom::VideoConferenceMediaDevice device,
    bool disabled,
    SetSystemMediaDeviceStatusCallback callback) {
  switch (device) {
    case crosapi::mojom::VideoConferenceMediaDevice::kCamera:
      camera_system_disabled_ = disabled;
      std::move(callback).Run(true);
      return;
    case crosapi::mojom::VideoConferenceMediaDevice::kMicrophone:
      microphone_system_disabled_ = disabled;
      std::move(callback).Run(true);
      return;
    case crosapi::mojom::VideoConferenceMediaDevice::kUnusedDefault:
      std::move(callback).Run(false);
      return;
  }
}

void VideoConferenceAppServiceClient::StopAllScreenShare() {}

void VideoConferenceAppServiceClient::OnCapabilityAccessUpdate(
    const apps::CapabilityAccessUpdate& update) {
  const AppIdString& app_id = update.AppId();
  // Only track Arc++ apps for now. All other apps are tracked by
  // VideoConferenceManagerClientImpl. We should only expand to more types after
  // confirming its compatibility with VideoConferenceManagerClientImpl.
  if (GetAppType(app_id) != apps::AppType::kArc) {
    return;
  }

  // For now, we only care about camera/microphone accessing.
  if (!update.CameraChanged() && !update.MicrophoneChanged()) {
    return;
  }

  const bool is_capturing_camera = update.Camera().value_or(false);
  const bool is_capturing_microphone = update.Microphone().value_or(false);
  const bool is_already_tracked = base::Contains(id_to_app_state_, app_id);

  // We only want to start tracking a app if it starts to accessing
  // microphone/camera.
  if (!is_capturing_camera && !is_capturing_microphone && !is_already_tracked) {
    return;
  }

  if (!is_already_tracked && ::video_conference::ShouldSkipId(app_id)) {
    return;
  }

  AppState& state = GetOrAddAppState(app_id);
  const std::string app_name = GetAppName(app_id);

  auto& ukm_hepler = id_to_ukm_hepler_[app_id];

  if (update.CameraChanged()) {
    state.is_capturing_camera = is_capturing_camera;
    if (ukm_hepler) {
      ukm_hepler->RegisterCapturingUpdate(
          video_conference::VideoConferenceMediaType::kCamera,
          is_capturing_camera);
    }
  }

  if (update.MicrophoneChanged()) {
    state.is_capturing_microphone = is_capturing_microphone;
    if (ukm_hepler) {
      ukm_hepler->RegisterCapturingUpdate(
          video_conference::VideoConferenceMediaType::kMicrophone,
          is_capturing_microphone);
    }
  }

  // For some apps, the instance is firstly removed, and then
  // OnCapabilityAccessUpdate is called. There is a chance that the app_id is
  // removed after OnInstanceUpdate, but added back in OnCapabilityAccessUpdate.
  // Thus we want to remove the app_id if it is not capturing and no instance
  // running.
  if (!state.is_capturing_microphone && !state.is_capturing_camera) {
    MaybeRemoveApp(app_id);
  }

  HandleMediaUsageUpdate();

  // This will be an AnchoredNudge, which is only visible if the tray is
  // visible; so we have to call this after HandleMediaUsageUpdate.
  if (update.CameraChanged() && is_capturing_camera &&
      camera_system_disabled_) {
    crosapi::CrosapiManager::Get()
        ->crosapi_ash()
        ->video_conference_manager_ash()
        ->NotifyDeviceUsedWhileDisabled(
            crosapi::mojom::VideoConferenceMediaDevice::kCamera,
            base::UTF8ToUTF16(app_name), base::DoNothingAs<void(bool)>());
  }

  if (update.MicrophoneChanged() && is_capturing_microphone &&
      microphone_system_disabled_) {
    crosapi::CrosapiManager::Get()
        ->crosapi_ash()
        ->video_conference_manager_ash()
        ->NotifyDeviceUsedWhileDisabled(
            crosapi::mojom::VideoConferenceMediaDevice::kMicrophone,
            base::UTF8ToUTF16(app_name), base::DoNothingAs<void(bool)>());
  }
}

void VideoConferenceAppServiceClient::OnAppCapabilityAccessCacheWillBeDestroyed(
    apps::AppCapabilityAccessCache* cache) {
  app_capability_observation_.Reset();
  capability_cache_ = nullptr;
}

void VideoConferenceAppServiceClient::OnInstanceUpdate(
    const apps::InstanceUpdate& update) {
  const AppIdString& app_id = update.AppId();

  // We only care about the apps being tracked already.
  if (!base::Contains(id_to_app_state_, app_id)) {
    return;
  }

  // An instance of app_id is about to be destructed.
  if (update.IsDestruction() &&
      instance_registry_->GetInstances(app_id).size() <= 1) {
    // The last instance maybe removed after the OnInstanceUpdate, so we post a
    // task to also remove the app_id if the instance is indeed removed.
    base::SequencedTaskRunner::GetCurrentDefault()->PostNonNestableTask(
        FROM_HERE,
        base::BindOnce(&VideoConferenceAppServiceClient::MaybeRemoveApp,
                       weak_ptr_factory_.GetWeakPtr(), app_id));
    return;
  }

  if (update.StateChanged() &&
      (update.State() & apps::InstanceState::kActive) != 0) {
    id_to_app_state_[app_id].last_activity_time = update.LastUpdatedTime();
    return;
  }
}

void VideoConferenceAppServiceClient::OnInstanceRegistryWillBeDestroyed(
    apps::InstanceRegistry* cache) {
  instance_registry_observation_.Reset();
  instance_registry_ = nullptr;
}

void VideoConferenceAppServiceClient::OnSessionStateChanged(
    session_manager::SessionState state) {
  if (state != session_manager::SessionState::ACTIVE) {
    return;
  }

  Profile* profile = ProfileManager::GetActiveUserProfile();
  user_manager::User* active_user =
      user_manager::UserManager::Get()->GetActiveUser();

  // Skip the profile that AppServiceProxy is not available.
  if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile) ||
      !active_user) {
    instance_registry_observation_.Reset();
    app_capability_observation_.Reset();
    return;
  }

  auto* ash_proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
  instance_registry_ = &ash_proxy->InstanceRegistry();
  app_registry_ = &ash_proxy->AppRegistryCache();

  instance_registry_observation_.Reset();
  instance_registry_observation_.Observe(instance_registry_);

  capability_cache_ =
      apps::AppCapabilityAccessCacheWrapper::Get().GetAppCapabilityAccessCache(
          active_user->GetAccountId());
  app_capability_observation_.Reset();
  app_capability_observation_.Observe(capability_cache_);
}

VideoConferenceAppServiceClient*
VideoConferenceAppServiceClient::GetForTesting() {
  return g_client_instance;
}

std::string VideoConferenceAppServiceClient::GetAppName(
    const AppIdString& app_id) {
  std::string app_name;
  app_registry_->ForOneApp(app_id, [&app_name](const apps::AppUpdate& update) {
    app_name = update.Name();
  });
  return app_name;
}

// Get current camera/microphone permission of the `app_id`.
VideoConferenceAppServiceClient::VideoConferencePermissions
VideoConferenceAppServiceClient::GetAppPermission(const AppIdString& app_id) {
  VideoConferencePermissions permissions;

  const bool is_arc_app = GetAppType(app_id) == apps::AppType::kArc;

  app_registry_->ForOneApp(app_id, [&permissions,
                                    is_arc_app](const apps::AppUpdate& update) {
    for (const auto& permission : update.Permissions()) {
      // For Arc++ Apps, kAsk means "Only for this time".
      const bool is_temporarily_enabled =
          is_arc_app && IsPermissionAsked(permission);

      const bool is_currently_enabled =
          permission->IsPermissionEnabled() || is_temporarily_enabled;

      if (permission->permission_type == apps::PermissionType::kCamera) {
        permissions.has_camera_permission = is_currently_enabled;
      }
      if (permission->permission_type == apps::PermissionType::kMicrophone) {
        permissions.has_microphone_permission = is_currently_enabled;
      }
    }
  });
  return permissions;
}

apps::AppType VideoConferenceAppServiceClient::GetAppType(
    const AppIdString& app_id) {
  apps::AppType type = apps::AppType::kUnknown;
  app_registry_->ForOneApp(app_id, [&type](const apps::AppUpdate& update) {
    type = update.AppType();
  });
  return type;
}

VideoConferenceAppServiceClient::AppState&
VideoConferenceAppServiceClient::GetOrAddAppState(const std::string& app_id) {
  if (!base::Contains(id_to_app_state_, app_id)) {
    id_to_app_state_[app_id] = AppState{base::UnguessableToken::Create(),
                                        base::Time::Now(), false, false};

    if (test_ukm_recorder_) {
      // In testing environment, using TestUkmRecorder and test SourceID.
      id_to_ukm_hepler_[app_id] = std::make_unique<VideoConferenceUkmHelper>(
          test_ukm_recorder_, test_ukm_recorder_->GetNewSourceID());
    } else {
      // In real environment, using real UkmRecorder and real SourceID.
      const auto source_id = apps::AppPlatformMetrics::GetSourceId(
          ProfileManager::GetActiveUserProfile(), app_id);
      if (source_id != ukm::kInvalidSourceId) {
        id_to_ukm_hepler_[app_id] = std::make_unique<VideoConferenceUkmHelper>(
            ukm::UkmRecorder::Get(), source_id);
      }
    }
  }
  return id_to_app_state_[app_id];
}

void VideoConferenceAppServiceClient::MaybeRemoveApp(
    const AppIdString& app_id) {
  // The app_id should also be removed if:
  // (1) all running instances of app_id are destructed.
  // (2) in an extreme case, the instance_registry_ is reset.
  if (!instance_registry_ || !instance_registry_->ContainsAppId(app_id)) {
    id_to_app_state_.erase(app_id);
    id_to_ukm_hepler_.erase(app_id);
    HandleMediaUsageUpdate();
  }
}

void VideoConferenceAppServiceClient::HandleMediaUsageUpdate() {
  crosapi::mojom::VideoConferenceMediaUsageStatusPtr new_status =
      crosapi::mojom::VideoConferenceMediaUsageStatus::New();
  new_status->client_id = client_id_;
  new_status->has_media_app = !id_to_app_state_.empty();

  for (const auto& [app_id, app_state] : id_to_app_state_) {
    new_status->is_capturing_camera |= app_state.is_capturing_camera;
    new_status->is_capturing_microphone |= app_state.is_capturing_microphone;

    VideoConferencePermissions permissions = GetAppPermission(app_id);
    new_status->has_camera_permission |= permissions.has_camera_permission;
    new_status->has_microphone_permission |=
        permissions.has_microphone_permission;
  }

  // If `status` equals the previously sent status, don't notify manager.
  if (new_status.Equals(status_)) {
    return;
  }
  status_ = new_status->Clone();

  auto callback = base::BindOnce([](bool success) {
    if (!success) {
      LOG(ERROR)
          << "VideoConferenceManager::NotifyMediaUsageUpdate did not succeed.";
    }
  });
  crosapi::CrosapiManager::Get()
      ->crosapi_ash()
      ->video_conference_manager_ash()
      ->NotifyMediaUsageUpdate(std::move(new_status), std::move(callback));
}

}  // namespace ash