// 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 "chromeos/ash/components/phonehub/recent_apps_interaction_handler_impl.h"
#include <memory>
#include <vector>
#include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/phonehub/notification.h"
#include "chromeos/ash/components/phonehub/pref_names.h"
#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/paint_vector_icon.h"
namespace ash::phonehub {
using multidevice_setup::mojom::Feature;
using multidevice_setup::mojom::FeatureState;
using multidevice_setup::mojom::HostStatus;
using HostStatusWithDevice =
multidevice_setup::MultiDeviceSetupClient::HostStatusWithDevice;
using FeatureStatesMap =
multidevice_setup::MultiDeviceSetupClient::FeatureStatesMap;
const size_t kMaxMostRecentApps = 5;
const size_t kMaxSavedRecentApps = 10;
// static
void RecentAppsInteractionHandlerImpl::RegisterPrefs(
PrefRegistrySimple* registry) {
registry->RegisterListPref(prefs::kRecentAppsHistory);
}
RecentAppsInteractionHandlerImpl::RecentAppsInteractionHandlerImpl(
PrefService* pref_service,
multidevice_setup::MultiDeviceSetupClient* multidevice_setup_client,
MultideviceFeatureAccessManager* multidevice_feature_access_manager)
: pref_service_(pref_service),
multidevice_setup_client_(multidevice_setup_client),
multidevice_feature_access_manager_(multidevice_feature_access_manager) {
multidevice_setup_client_->AddObserver(this);
multidevice_feature_access_manager_->AddObserver(this);
}
RecentAppsInteractionHandlerImpl::~RecentAppsInteractionHandlerImpl() {
if (features::IsEcheNetworkConnectionStateEnabled() &&
eche_connection_status_handler_) {
eche_connection_status_handler_->RemoveObserver(this);
}
multidevice_setup_client_->RemoveObserver(this);
multidevice_feature_access_manager_->RemoveObserver(this);
}
void RecentAppsInteractionHandlerImpl::AddRecentAppClickObserver(
RecentAppClickObserver* observer) {
observer_list_.AddObserver(observer);
}
void RecentAppsInteractionHandlerImpl::RemoveRecentAppClickObserver(
RecentAppClickObserver* observer) {
observer_list_.RemoveObserver(observer);
}
void RecentAppsInteractionHandlerImpl::NotifyRecentAppClicked(
const Notification::AppMetadata& app_metadata,
eche_app::mojom::AppStreamLaunchEntryPoint entrypoint) {
for (auto& observer : observer_list_)
observer.OnRecentAppClicked(app_metadata, entrypoint);
}
// Load the |recent_app_metadata_list_| from |pref_service_| if there is a
// history of |recent_app_metadata_list_| exist in |pref_service_|. Then add or
// update |app_metadata| into |recent_app_metadata_list_|. Also update
// this |app_metadata| back to |pref_service_|.
void RecentAppsInteractionHandlerImpl::NotifyRecentAppAddedOrUpdated(
const Notification::AppMetadata& app_metadata,
base::Time last_accessed_timestamp) {
LoadRecentAppMetadataListFromPrefIfNeed();
// Each element of |recent_app_metadata_list_| has a unique |package_name| and
// |user_id|.
for (auto it = recent_app_metadata_list_.begin();
it != recent_app_metadata_list_.end(); ++it) {
if (it->first.package_name == app_metadata.package_name &&
it->first.user_id == app_metadata.user_id) {
recent_app_metadata_list_.erase(it);
break;
}
}
recent_app_metadata_list_.emplace(recent_app_metadata_list_.begin(),
app_metadata, last_accessed_timestamp);
SaveRecentAppMetadataListToPref();
ComputeAndUpdateUiState();
}
void RecentAppsInteractionHandlerImpl::SetConnectionStatusHandler(
eche_app::EcheConnectionStatusHandler* eche_connection_status_handler) {
if (!features::IsEcheNetworkConnectionStateEnabled()) {
return;
}
if (eche_connection_status_handler_) {
eche_connection_status_handler_->RemoveObserver(this);
}
eche_connection_status_handler_ = eche_connection_status_handler;
if (eche_connection_status_handler_) {
eche_connection_status_handler_->AddObserver(this);
}
}
base::flat_set<int64_t>
RecentAppsInteractionHandlerImpl::GetUserIdsWithDisplayRecentApps() {
base::flat_set<int64_t> user_ids;
for (auto& user : user_states()) {
if (user.is_enabled) {
user_ids.emplace(user.user_id);
}
}
// Skip filtering recent apps when not receiving user states.
if (user_ids.empty()) {
for (auto const& it : recent_app_metadata_list_) {
if (!user_ids.contains(it.first.user_id)) {
user_ids.emplace(it.first.user_id);
}
}
}
return user_ids;
}
std::vector<Notification::AppMetadata>
RecentAppsInteractionHandlerImpl::FetchRecentAppMetadataList() {
LoadRecentAppMetadataListFromPrefIfNeed();
base::flat_set<int64_t> active_user_ids = GetUserIdsWithDisplayRecentApps();
std::vector<Notification::AppMetadata> app_metadata_list;
for (auto const& it : recent_app_metadata_list_) {
if (active_user_ids.contains(it.first.user_id)) {
app_metadata_list.push_back(it.first);
// At most |kMaxMostRecentApps| recent apps can be displayed.
if (app_metadata_list.size() == kMaxMostRecentApps)
break;
}
}
return app_metadata_list;
}
void RecentAppsInteractionHandlerImpl::
LoadRecentAppMetadataListFromPrefIfNeed() {
if (!has_loaded_prefs_) {
PA_LOG(INFO) << "LoadRecentAppMetadataListFromPref";
const base::Value::List& recent_apps_history_pref =
pref_service_->GetList(prefs::kRecentAppsHistory);
for (const auto& value : recent_apps_history_pref) {
DCHECK(value.is_dict());
recent_app_metadata_list_.emplace_back(
Notification::AppMetadata::FromValue(value.GetDict()),
base::Time::FromSecondsSinceUnixEpoch(0));
}
has_loaded_prefs_ = true;
}
}
void RecentAppsInteractionHandlerImpl::SaveRecentAppMetadataListToPref() {
PA_LOG(INFO) << "SaveRecentAppMetadataListToPref";
size_t num_recent_apps_to_save =
std::min(recent_app_metadata_list_.size(), kMaxSavedRecentApps);
base::Value::List app_metadata_value_list;
for (size_t i = 0; i < num_recent_apps_to_save; ++i) {
app_metadata_value_list.Append(
recent_app_metadata_list_[i].first.ToValue());
}
pref_service_->SetList(prefs::kRecentAppsHistory,
std::move(app_metadata_value_list));
has_loaded_prefs_ = true;
}
void RecentAppsInteractionHandlerImpl::OnFeatureStatesChanged(
const FeatureStatesMap& feature_states_map) {
ComputeAndUpdateUiState();
}
void RecentAppsInteractionHandlerImpl::OnHostStatusChanged(
const HostStatusWithDevice& host_device_with_status) {
if (host_device_with_status.first != HostStatus::kHostVerified) {
PA_LOG(INFO) << "ClearRecentAppMetadataListAndPref";
ClearRecentAppMetadataListAndPref();
}
}
void RecentAppsInteractionHandlerImpl::OnNotificationAccessChanged() {
ComputeAndUpdateUiState();
}
void RecentAppsInteractionHandlerImpl::OnAppsAccessChanged() {
ComputeAndUpdateUiState();
}
void RecentAppsInteractionHandlerImpl::OnConnectionStatusForUiChanged(
eche_app::mojom::ConnectionStatus connection_status) {
if (features::IsEcheNetworkConnectionStateEnabled() &&
connection_status_ != connection_status) {
connection_status_ = connection_status;
ComputeAndUpdateUiState();
}
}
void RecentAppsInteractionHandlerImpl::SetStreamableApps(
const std::vector<Notification::AppMetadata>& streamable_apps) {
PA_LOG(INFO) << "ClearRecentAppMetadataListAndPref to update the list of "
<< streamable_apps.size() << " items.";
ClearRecentAppMetadataListAndPref();
// TODO(b/260015890): Save at most 6 apps.
for (const auto& app : streamable_apps) {
recent_app_metadata_list_.emplace_back(
app, base::Time::FromSecondsSinceUnixEpoch(0));
}
SaveRecentAppMetadataListToPref();
ComputeAndUpdateUiState();
}
void RecentAppsInteractionHandlerImpl::RemoveStreamableApp(
const proto::App app_to_remove) {
std::erase_if(
recent_app_metadata_list_,
[&app_to_remove](
const std::pair<Notification::AppMetadata, base::Time>& app) {
return app.first.package_name == app_to_remove.package_name();
});
SaveRecentAppMetadataListToPref();
ComputeAndUpdateUiState();
}
void RecentAppsInteractionHandlerImpl::ComputeAndUpdateUiState() {
ui_state_ = RecentAppsUiState::HIDDEN;
LoadRecentAppMetadataListFromPrefIfNeed();
// There are five cases we need to handle:
// 1. If no recent app in list and necessary permission be granted, the
// placeholder view will be shown.
// 2. If some recent apps in list and streaming is allowed, the loading view
// will show when determining if the connection can be bootstrapped.
// 3. If some recent apps in list and streaming is allowed, the connection
// error view will be shown.
// 4. If some recent apps in list, streaming is allowed and the booststrap
// connection was successful, then recent apps view will be shown.
// 5. Otherwise, no recent apps view will be shown.
bool allow_streaming = multidevice_setup_client_->GetFeatureState(
Feature::kEche) == FeatureState::kEnabledByUser;
bool is_apps_access_required =
features::IsEcheSWAEnabled() &&
multidevice_feature_access_manager_->GetAppsAccessStatus() ==
phonehub::MultideviceFeatureAccessManager::AccessStatus::
kAvailableButNotGranted;
if (!allow_streaming || is_apps_access_required) {
NotifyRecentAppsViewUiStateUpdated();
return;
}
if (features::IsEcheNetworkConnectionStateEnabled()) {
ui_state_ = GetUiStateFromConnectionStatus();
NotifyRecentAppsViewUiStateUpdated();
return;
}
if (recent_app_metadata_list_.empty()) {
bool notifications_enabled =
multidevice_setup_client_->GetFeatureState(
Feature::kPhoneHubNotifications) == FeatureState::kEnabledByUser;
bool grant_notification_access_on_host =
multidevice_feature_access_manager_->GetNotificationAccessStatus() ==
phonehub::MultideviceFeatureAccessManager::AccessStatus::kAccessGranted;
if (notifications_enabled && grant_notification_access_on_host) {
ui_state_ = RecentAppsUiState::PLACEHOLDER_VIEW;
}
} else {
ui_state_ = RecentAppsUiState::ITEMS_VISIBLE;
}
NotifyRecentAppsViewUiStateUpdated();
}
void RecentAppsInteractionHandlerImpl::ClearRecentAppMetadataListAndPref() {
recent_app_metadata_list_.clear();
pref_service_->ClearPref(prefs::kRecentAppsHistory);
has_loaded_prefs_ = false;
}
RecentAppsInteractionHandler::RecentAppsUiState
RecentAppsInteractionHandlerImpl::GetUiStateFromConnectionStatus() {
RecentAppsUiState ui_state = RecentAppsUiState::HIDDEN;
switch (connection_status_) {
case eche_app::mojom::ConnectionStatus::kConnectionStatusDisconnected:
[[fallthrough]];
case eche_app::mojom::ConnectionStatus::kConnectionStatusConnecting:
ui_state = RecentAppsUiState::LOADING;
break;
case eche_app::mojom::ConnectionStatus::kConnectionStatusConnected:
ui_state = RecentAppsUiState::ITEMS_VISIBLE;
break;
case eche_app::mojom::ConnectionStatus::kConnectionStatusFailed:
ui_state = RecentAppsUiState::CONNECTION_FAILED;
break;
}
return ui_state;
}
} // namespace ash::phonehub