chromium/ash/system/privacy_hub/geolocation_privacy_switch_controller.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/privacy_hub/geolocation_privacy_switch_controller.h"

#include <string>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/geolocation_access_level.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/system/privacy_hub/privacy_hub_controller.h"
#include "ash/system/privacy_hub/privacy_hub_metrics.h"
#include "ash/system/privacy_hub/privacy_hub_notification_controller.h"
#include "base/notreached.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_service.h"

namespace ash {

GeolocationPrivacySwitchController::GeolocationPrivacySwitchController()
    : session_observation_(this) {
  session_observation_.Observe(Shell::Get()->session_controller());
}

GeolocationPrivacySwitchController::~GeolocationPrivacySwitchController() =
    default;

// static
GeolocationPrivacySwitchController* GeolocationPrivacySwitchController::Get() {
  PrivacyHubController* privacy_hub_controller =
      Shell::Get()->privacy_hub_controller();
  return privacy_hub_controller
             ? privacy_hub_controller->geolocation_controller()
             : nullptr;
}

void GeolocationPrivacySwitchController::OnActiveUserPrefServiceChanged(
    PrefService* pref_service) {
  pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
  pref_change_registrar_->Init(pref_service);
  pref_change_registrar_->Add(
      prefs::kUserGeolocationAccessLevel,
      base::BindRepeating(
          &GeolocationPrivacySwitchController::OnPreferenceChanged,
          base::Unretained(this)));

  // Establish the initial value for the cached_access_level_.
  cached_access_level_ = static_cast<GeolocationAccessLevel>(
      pref_change_registrar_->prefs()->GetInteger(
          prefs::kUserGeolocationAccessLevel));

  if (features::IsCrosPrivacyHubLocationEnabled()) {
    UpdateNotification();
  }
}

void GeolocationPrivacySwitchController::OnPreferenceChanged() {
  VLOG(1) << "Privacy Hub: Geolocation switch state = "
          << static_cast<int>(AccessLevel());
  if (features::IsCrosPrivacyHubLocationEnabled()) {
    CHECK(pref_change_registrar_);
    const GeolocationAccessLevel new_access_level =
        static_cast<GeolocationAccessLevel>(
            pref_change_registrar_->prefs()->GetInteger(
                prefs::kUserGeolocationAccessLevel));

    CHECK(cached_access_level_.has_value());
    if (new_access_level != *cached_access_level_) {
      // update the pref that tracks the previous access level.
      pref_change_registrar_->prefs()->SetInteger(
          prefs::kUserPreviousGeolocationAccessLevel,
          static_cast<int>(*cached_access_level_));
      cached_access_level_ = new_access_level;
    }
    UpdateNotification();
  } else {
    // Feature disabled means geolocation is always allowed
    CHECK(pref_change_registrar_);
    pref_change_registrar_->prefs()->SetInteger(
        prefs::kUserGeolocationAccessLevel,
        static_cast<int>(GeolocationAccessLevel::kAllowed));
  }
}

void GeolocationPrivacySwitchController::TrackGeolocationAttempted(
    const std::string& app_name) {
  ++usage_per_app_[app_name];
  ++usage_cnt_;
  UpdateNotification();
}

void GeolocationPrivacySwitchController::TrackGeolocationRelinquished(
    const std::string& app_name) {
  --usage_per_app_[app_name];
  --usage_cnt_;
  if (usage_per_app_[app_name] < 0 || usage_cnt_ < 0) {
    LOG(ERROR) << "Geolocation usage termination without start: count("
               << app_name << ") = " << usage_per_app_[app_name]
               << ", total count = " << usage_cnt_;
    NOTREACHED();
  }

  UpdateNotification();
}

bool GeolocationPrivacySwitchController::IsGeolocationUsageAllowedForApps() {
  switch (AccessLevel()) {
    case GeolocationAccessLevel::kAllowed:
      return true;
    case GeolocationAccessLevel::kOnlyAllowedForSystem:
    case GeolocationAccessLevel::kDisallowed:
      return false;
  }
}

std::vector<std::u16string> GeolocationPrivacySwitchController::GetActiveApps(
    size_t max_count) const {
  std::vector<std::u16string> apps;
  for (const auto& [name, cnt] : usage_per_app_) {
    if (cnt > 0) {
      apps.push_back(base::UTF8ToUTF16(name));
      if (apps.size() == max_count) {
        break;
      }
    }
  }
  return apps;
}

GeolocationAccessLevel GeolocationPrivacySwitchController::AccessLevel() const {
  CHECK(cached_access_level_.has_value());
  return *cached_access_level_;
}

GeolocationAccessLevel GeolocationPrivacySwitchController::PreviousAccessLevel()
    const {
  CHECK(pref_change_registrar_);
  const GeolocationAccessLevel previous_level =
      static_cast<GeolocationAccessLevel>(
          pref_change_registrar_->prefs()->GetInteger(
              prefs::kUserPreviousGeolocationAccessLevel));
  // Previous level should be distinct.
  CHECK_NE(previous_level, AccessLevel());
  return previous_level;
}

void GeolocationPrivacySwitchController::SetAccessLevel(
    GeolocationAccessLevel access_level) {
  if (!features::IsCrosPrivacyHubLocationEnabled()) {
    return;
  }
  CHECK(pref_change_registrar_);
  pref_change_registrar_->prefs()->SetInteger(
      prefs::kUserGeolocationAccessLevel, static_cast<int>(access_level));
}

void GeolocationPrivacySwitchController::UpdateNotification() {
  PrivacyHubNotificationController* notification_controller =
      PrivacyHubNotificationController::Get();
  if (!notification_controller) {
    return;
  }

  if (usage_cnt_ == 0 || IsGeolocationUsageAllowedForApps()) {
    notification_controller->RemoveSoftwareSwitchNotification(
        SensorDisabledNotificationDelegate::Sensor::kLocation);
    return;
  }

  notification_controller->ShowSoftwareSwitchNotification(
      SensorDisabledNotificationDelegate::Sensor::kLocation);
}

void GeolocationPrivacySwitchController::ApplyArcLocationUpdate(
    bool geolocation_enabled) {
  if (!features::IsCrosPrivacyHubLocationEnabled()) {
    return;
  }
  if (geolocation_enabled &&
      AccessLevel() != GeolocationAccessLevel::kAllowed) {
    SetAccessLevel(GeolocationAccessLevel::kAllowed);
  } else if (!geolocation_enabled &&
             AccessLevel() == ash::GeolocationAccessLevel::kAllowed) {
    // Restore previous location level, which is blocking.
    SetAccessLevel(PreviousAccessLevel());
  }
}

}  // namespace ash