// 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.
#ifndef ASH_SYSTEM_GEOLOCATION_GEOLOCATION_CONTROLLER_H_
#define ASH_SYSTEM_GEOLOCATION_GEOLOCATION_CONTROLLER_H_
#include <memory>
#include <string>
#include "ash/ash_export.h"
#include "ash/public/cpp/session/session_observer.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/observer_list_types.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "base/types/expected.h"
#include "chromeos/ash/components/geolocation/simple_geolocation_provider.h"
#include "chromeos/ash/components/settings/timezone_settings.h"
#include "chromeos/dbus/power/power_manager_client.h"
class PrefChangeRegistrar;
class PrefRegistrySimple;
class PrefService;
namespace base {
class Clock;
} // namespace base
namespace ash {
class LocalTimeConverter;
// Represents a geolocation position fix. It's "simple" because it doesn't
// expose all the parameters of the position interface as defined by the
// Geolocation API Specification:
// https://dev.w3.org/geo/api/spec-source.html#position_interface
// The GeolocationController is only interested in valid latitude and
// longitude. It also doesn't require any specific accuracy. The more accurate
// the positions, the more accurate sunset and sunrise times calculations.
// However, an IP-based geoposition is considered good enough.
struct SimpleGeoposition {
bool operator==(const SimpleGeoposition& other) const {
return latitude == other.latitude && longitude == other.longitude;
}
double latitude;
double longitude;
};
// Periodically requests the IP-based geolocation and provides it to the
// observers, `GeolocationController::Observer`. This class also observes
// timezone changes to request a new geoposition.
// TODO(crbug.com/1272178): `GeolocationController` should observe the sleep
// and update next request time.
class ASH_EXPORT GeolocationController
: public SimpleGeolocationProvider::Observer,
public system::TimezoneSettings::Observer,
public chromeos::PowerManagerClient::Observer,
public SessionObserver {
public:
// Possible errors for `GetSunsetTime()` and `GetSunriseTime()`.
enum class SunRiseSetError {
// The current geolocation has no sunrise/sunset (24 hours of daylight or
// darkness).
kNoSunRiseSet,
// Sunrise/set are temporarily unavailable, including the default values of
// 6 AM/PM local time. Caller should handle this gracefully and try again
// later.
kUnavailable
};
static constexpr base::expected<base::Time, SunRiseSetError> kNoSunRiseSet =
base::unexpected(SunRiseSetError::kNoSunRiseSet);
static constexpr base::expected<base::Time, SunRiseSetError>
kSunRiseSetUnavailable = base::unexpected(SunRiseSetError::kUnavailable);
class Observer : public base::CheckedObserver {
public:
// Emitted when the Geoposition is updated with
// |possible_change_in_timezone| to indicate whether timezone might have
// changed as a result of the geoposition change.
virtual void OnGeopositionChanged(bool possible_change_in_timezone) {}
protected:
~Observer() override = default;
};
explicit GeolocationController(SimpleGeolocationProvider* const provider);
GeolocationController(const GeolocationController&) = delete;
GeolocationController& operator=(const GeolocationController&) = delete;
~GeolocationController() override;
static GeolocationController* Get();
static void RegisterProfilePrefs(PrefRegistrySimple* registry);
const base::OneShotTimer& timer() const { return *timer_; }
const std::u16string& current_timezone_id() const {
return current_timezone_id_;
}
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
// SimpleGeolocationProvider::Observer:
void OnGeolocationPermissionChanged(bool enabled) override;
// system::TimezoneSettings::Observer:
void TimezoneChanged(const icu::TimeZone& timezone) override;
// chromeos::PowerManagerClient::Observer:
void SuspendDone(base::TimeDelta sleep_duration) override;
// SessionObserver:
void OnActiveUserPrefServiceChanged(PrefService* pref_service) override;
// Returns sunset and sunrise time calculated from the most recently observed
// geoposition. If a geoposition has not been observed, defaults to sunset
// 6 PM and sunrise 6 AM.
base::expected<base::Time, SunRiseSetError> GetSunsetTime() const {
return GetSunRiseSet(/*sunrise=*/false);
}
base::expected<base::Time, SunRiseSetError> GetSunriseTime() const {
return GetSunRiseSet(/*sunrise=*/true);
}
static base::TimeDelta GetNextRequestDelayAfterSuccessForTesting();
base::OneShotTimer* GetTimerForTesting() { return timer_.get(); }
bool HasObserver(const Observer* obs) const {
return observers_.HasObserver(obs);
}
void SetTimerForTesting(std::unique_ptr<base::OneShotTimer> timer);
void SetClockForTesting(base::Clock* clock);
void SetLocalTimeConverterForTesting(
const LocalTimeConverter* local_time_converter);
void SetCurrentTimezoneIdForTesting(const std::u16string& timezone_id);
// Resets the running `timer_` and issues an immediate geoposition request.
// Any responses on the fly will be processed first, but will be overridden
// once the response of this request arrives.
void RequestImmediateGeopositionForTesting();
protected:
// The callback of geolocation request via `provider_`. Once receiving a
// new position, it `NotifyWithCurrentGeoposition()` to broadcast the position
// to observers and `ScheduleNextRequest()` on the next day. If the retrieval
// fails, it `ScheduleNextRequest()` after a `backoff_delay_`, which is
// doubled for each failure.
void OnGeoposition(const Geoposition& position,
bool server_error,
const base::TimeDelta elapsed);
// Virtual so that it can be overridden by a fake implementation in unit tests
// that doesn't request actual geopositions.
virtual void RequestGeoposition();
private:
// Calls `RequestGeoposition()` after `delay`.
void ScheduleNextRequest(base::TimeDelta delay);
// Broadcasts the change in geoposition to all observers with
// |possible_change_in_timezone| to indicate whether timezone might have
// changed as a result of the geoposition change.
void NotifyGeopositionChange(bool possible_change_in_timezone);
// Note that the below computation is intentionally performed every time
// GetSunsetTime() or GetSunriseTime() is called rather than once whenever we
// receive a geoposition (which happens at least once a day). This reduces
// the chances of getting inaccurate values, especially around DST changes.
base::expected<base::Time, SunRiseSetError> GetSunRiseSet(bool sunrise) const;
// Called only when the active user changes in order to see if we need to use
// a previously cached geoposition value from the active user's prefs.
void LoadCachedGeopositionIfNeeded();
// Called whenever we receive a new geoposition update to cache it in all
// logged-in users' prefs so that it can be used later in the event of not
// being able to retrieve a valid geoposition.
void StoreCachedGeoposition() const;
// Points to the `SimpleGeolocationProvider::GetInstance()` throughout the
// object lifecycle. Overridden in unit tests.
raw_ptr<SimpleGeolocationProvider> geolocation_provider_ = nullptr;
// May be null if a user has not logged in yet.
raw_ptr<PrefService> active_user_pref_service_ = nullptr;
std::unique_ptr<PrefChangeRegistrar> registrar_;
// Delay after which a new request is retried after a failed one.
base::TimeDelta backoff_delay_;
std::unique_ptr<base::OneShotTimer> timer_;
// Optional Used in tests to override the time of "Now".
raw_ptr<base::Clock> clock_ = nullptr; // Not owned.
// Optional Used in tests to override all local time operations.
raw_ptr<const LocalTimeConverter> local_time_converter_ =
nullptr; // Not owned.
// The ID of the current timezone in the format similar to "America/Chicago".
std::u16string current_timezone_id_;
base::ObserverList<Observer> observers_;
// True if the current `geoposition_` is from a previously cached value in the
// user prefs of any of the users in the current session. It is reset to false
// once we receive a newly-updated geoposition. This is used to treat the
// current geoposition as temporary until we receive a valid geoposition
// update, and also not to let a cached geoposition value to leak to another
// user for privacy reasons.
bool is_current_geoposition_from_cache_ = false;
std::unique_ptr<SimpleGeoposition> geoposition_;
ScopedSessionObserver scoped_session_observer_;
base::WeakPtrFactory<GeolocationController> weak_ptr_factory_{this};
};
} // namespace ash
#endif // ASH_SYSTEM_GEOLOCATION_GEOLOCATION_CONTROLLER_H_