// Copyright 2020 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/phone_status_processor.h"
#include <algorithm>
#include <memory>
#include <string>
#include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "base/containers/flat_set.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/phonehub/app_stream_manager.h"
#include "chromeos/ash/components/phonehub/do_not_disturb_controller.h"
#include "chromeos/ash/components/phonehub/find_my_device_controller.h"
#include "chromeos/ash/components/phonehub/icon_decoder.h"
#include "chromeos/ash/components/phonehub/icon_decoder_impl.h"
#include "chromeos/ash/components/phonehub/message_receiver.h"
#include "chromeos/ash/components/phonehub/multidevice_feature_access_manager.h"
#include "chromeos/ash/components/phonehub/mutable_phone_model.h"
#include "chromeos/ash/components/phonehub/notification_processor.h"
#include "chromeos/ash/components/phonehub/phone_hub_structured_metrics_logger.h"
#include "chromeos/ash/components/phonehub/phone_hub_ui_readiness_recorder.h"
#include "chromeos/ash/components/phonehub/proto/phonehub_api.pb.h"
#include "chromeos/ash/components/phonehub/recent_apps_interaction_handler.h"
#include "chromeos/ash/components/phonehub/screen_lock_manager_impl.h"
#include "chromeos/ash/services/multidevice_setup/public/cpp/prefs.h"
#include "chromeos/ash/services/multidevice_setup/public/mojom/multidevice_setup.mojom.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 {
namespace phonehub {
namespace {
using multidevice_setup::MultiDeviceSetupClient;
PhoneStatusModel::MobileStatus GetMobileStatusFromProto(
proto::MobileConnectionState mobile_status) {
switch (mobile_status) {
case proto::MobileConnectionState::NO_SIM:
return PhoneStatusModel::MobileStatus::kNoSim;
case proto::MobileConnectionState::SIM_BUT_NO_RECEPTION:
return PhoneStatusModel::MobileStatus::kSimButNoReception;
case proto::MobileConnectionState::SIM_WITH_RECEPTION:
return PhoneStatusModel::MobileStatus::kSimWithReception;
default:
return PhoneStatusModel::MobileStatus::kNoSim;
}
}
PhoneStatusModel::SignalStrength GetSignalStrengthFromProto(
proto::SignalStrength signal_strength) {
switch (signal_strength) {
case proto::SignalStrength::ZERO_BARS:
return PhoneStatusModel::SignalStrength::kZeroBars;
case proto::SignalStrength::ONE_BAR:
return PhoneStatusModel::SignalStrength::kOneBar;
case proto::SignalStrength::TWO_BARS:
return PhoneStatusModel::SignalStrength::kTwoBars;
case proto::SignalStrength::THREE_BARS:
return PhoneStatusModel::SignalStrength::kThreeBars;
case proto::SignalStrength::FOUR_BARS:
return PhoneStatusModel::SignalStrength::kFourBars;
default:
return PhoneStatusModel::SignalStrength::kZeroBars;
}
}
PhoneStatusModel::ChargingState GetChargingStateFromProto(
proto::ChargingState charging_state) {
switch (charging_state) {
case proto::ChargingState::NOT_CHARGING:
return PhoneStatusModel::ChargingState::kNotCharging;
case proto::ChargingState::CHARGING_AC:
case proto::ChargingState::CHARGING_WIRELESS:
return PhoneStatusModel::ChargingState::kChargingAc;
case proto::ChargingState::CHARGING_USB:
return PhoneStatusModel::ChargingState::kChargingUsb;
default:
return PhoneStatusModel::ChargingState::kNotCharging;
}
}
PhoneStatusModel::BatterySaverState GetBatterySaverStateFromProto(
proto::BatteryMode battery_mode) {
switch (battery_mode) {
case proto::BatteryMode::BATTERY_SAVER_OFF:
return PhoneStatusModel::BatterySaverState::kOff;
case proto::BatteryMode::BATTERY_SAVER_ON:
return PhoneStatusModel::BatterySaverState::kOn;
default:
return PhoneStatusModel::BatterySaverState::kOff;
}
}
MultideviceFeatureAccessManager::AccessStatus ComputeNotificationAccessState(
const proto::PhoneProperties& phone_properties) {
// If the user has a Work Profile active, notification access is not allowed
// by Android. See https://crbug.com/1155151.
if (phone_properties.profile_type() == proto::ProfileType::WORK_PROFILE)
return MultideviceFeatureAccessManager::AccessStatus::kProhibited;
if (phone_properties.notification_access_state() ==
proto::NotificationAccessState::ACCESS_GRANTED) {
return MultideviceFeatureAccessManager::AccessStatus::kAccessGranted;
}
return MultideviceFeatureAccessManager::AccessStatus::kAvailableButNotGranted;
}
// User has to consent and agree for phoneHub to have storage permission on the
// phone
MultideviceFeatureAccessManager::AccessStatus ComputeCameraRollAccessState(
const proto::PhoneProperties& phone_properties) {
if (phone_properties.camera_roll_access_state().feature_enabled()) {
return MultideviceFeatureAccessManager::AccessStatus::kAccessGranted;
} else {
return MultideviceFeatureAccessManager::AccessStatus::
kAvailableButNotGranted;
}
}
MultideviceFeatureAccessManager::AccessProhibitedReason
ComputeNotificationAccessProhibitedReason(
const proto::PhoneProperties& phone_properties) {
if (phone_properties.profile_disable_reason() ==
proto::ProfileDisableReason::DISABLE_REASON_DISABLED_BY_POLICY) {
return MultideviceFeatureAccessManager::AccessProhibitedReason::
kDisabledByPhonePolicy;
}
if (phone_properties.profile_type() == proto::ProfileType::WORK_PROFILE) {
return MultideviceFeatureAccessManager::AccessProhibitedReason::
kWorkProfile;
}
return MultideviceFeatureAccessManager::AccessProhibitedReason::kUnknown;
}
ScreenLockManager::LockStatus ComputeScreenLockState(
const proto::PhoneProperties& phone_properties) {
switch (phone_properties.screen_lock_state()) {
case proto::ScreenLockState::SCREEN_LOCK_UNKNOWN:
return ScreenLockManager::LockStatus::kUnknown;
case proto::ScreenLockState::SCREEN_LOCK_OFF:
return ScreenLockManager::LockStatus::kLockedOff;
case proto::ScreenLockState::SCREEN_LOCK_ON:
return ScreenLockManager::LockStatus::kLockedOn;
default:
return ScreenLockManager::LockStatus::kUnknown;
}
}
FindMyDeviceController::Status ComputeFindMyDeviceStatus(
const proto::PhoneProperties& phone_properties) {
if (phone_properties.find_my_device_capability() ==
proto::FindMyDeviceCapability::NOT_ALLOWED) {
return FindMyDeviceController::Status::kRingingNotAvailable;
}
bool is_ringing =
phone_properties.ring_status() == proto::FindMyDeviceRingStatus::RINGING;
return is_ringing ? FindMyDeviceController::Status::kRingingOn
: FindMyDeviceController::Status::kRingingOff;
}
PhoneStatusModel CreatePhoneStatusModel(const proto::PhoneProperties& proto) {
PA_LOG(INFO) << "Creating PhoneStatusModel from PhoneProperties message.";
return PhoneStatusModel(
GetMobileStatusFromProto(proto.connection_state()),
PhoneStatusModel::MobileConnectionMetadata{
GetSignalStrengthFromProto(proto.signal_strength()),
base::UTF8ToUTF16(proto.mobile_provider())},
GetChargingStateFromProto(proto.charging_state()),
GetBatterySaverStateFromProto(proto.battery_mode()),
proto.battery_percentage());
}
std::vector<RecentAppsInteractionHandler::UserState> GetUserStates(
const RepeatedPtrField<proto::UserState>& user_states) {
std::vector<RecentAppsInteractionHandler::UserState> states;
for (const auto& user_state : user_states) {
RecentAppsInteractionHandler::UserState state;
state.user_id = user_state.user_id();
state.is_enabled = !user_state.is_quiet_mode_enabled();
states.emplace_back(state);
}
return states;
}
bool ShouldUpdateRecents(
PhoneStatusProcessor::AppListUpdateType app_list_update_type) {
return app_list_update_type ==
PhoneStatusProcessor::AppListUpdateType::kOnlyRecentApps ||
app_list_update_type == PhoneStatusProcessor::AppListUpdateType::kBoth;
}
bool ShouldUpdateLauncher(
PhoneStatusProcessor::AppListUpdateType app_list_update_type) {
return app_list_update_type ==
PhoneStatusProcessor::AppListUpdateType::kOnlyLauncherApps;
}
bool IsIncrementalAppUpdate(
PhoneStatusProcessor::AppListUpdateType app_list_update_type) {
return app_list_update_type ==
PhoneStatusProcessor::AppListUpdateType::kIncrementalAppUpdate;
}
} // namespace
PhoneStatusProcessor::PhoneStatusProcessor(
DoNotDisturbController* do_not_disturb_controller,
FeatureStatusProvider* feature_status_provider,
MessageReceiver* message_receiver,
FindMyDeviceController* find_my_device_controller,
MultideviceFeatureAccessManager* multidevice_feature_access_manager,
ScreenLockManager* screen_lock_manager,
NotificationProcessor* notification_processor_,
MultiDeviceSetupClient* multidevice_setup_client,
MutablePhoneModel* phone_model,
RecentAppsInteractionHandler* recent_apps_interaction_handler,
PrefService* pref_service,
AppStreamManager* app_stream_manager,
AppStreamLauncherDataModel* app_stream_launcher_data_model,
IconDecoder* icon_decoder,
PhoneHubUiReadinessRecorder* phone_hub_ui_readiness_recorder,
PhoneHubStructuredMetricsLogger* phone_hub_structured_metrics_logger)
: do_not_disturb_controller_(do_not_disturb_controller),
feature_status_provider_(feature_status_provider),
message_receiver_(message_receiver),
find_my_device_controller_(find_my_device_controller),
multidevice_feature_access_manager_(multidevice_feature_access_manager),
screen_lock_manager_(screen_lock_manager),
notification_processor_(notification_processor_),
multidevice_setup_client_(multidevice_setup_client),
phone_model_(phone_model),
recent_apps_interaction_handler_(recent_apps_interaction_handler),
pref_service_(pref_service),
app_stream_manager_(app_stream_manager),
app_stream_launcher_data_model_(app_stream_launcher_data_model),
icon_decoder_(icon_decoder),
phone_hub_ui_readiness_recorder_(phone_hub_ui_readiness_recorder),
phone_hub_structured_metrics_logger_(
phone_hub_structured_metrics_logger) {
DCHECK(do_not_disturb_controller_);
DCHECK(feature_status_provider_);
DCHECK(message_receiver_);
DCHECK(find_my_device_controller_);
DCHECK(multidevice_feature_access_manager_);
DCHECK(notification_processor_);
DCHECK(multidevice_setup_client_);
DCHECK(phone_model_);
DCHECK(pref_service_);
DCHECK(app_stream_manager_);
DCHECK(icon_decoder_);
DCHECK(phone_hub_ui_readiness_recorder_);
DCHECK(phone_hub_structured_metrics_logger_);
message_receiver_->AddObserver(this);
feature_status_provider_->AddObserver(this);
multidevice_setup_client_->AddObserver(this);
MaybeSetPhoneModelName(multidevice_setup_client_->GetHostStatus().second);
}
PhoneStatusProcessor::~PhoneStatusProcessor() {
message_receiver_->RemoveObserver(this);
feature_status_provider_->RemoveObserver(this);
multidevice_setup_client_->RemoveObserver(this);
}
void PhoneStatusProcessor::ProcessReceivedNotifications(
const RepeatedPtrField<proto::Notification>& notification_protos) {
multidevice_setup::mojom::FeatureState feature_state =
multidevice_setup_client_->GetFeatureState(
multidevice_setup::mojom::Feature::kPhoneHubNotifications);
if (feature_state != multidevice_setup::mojom::FeatureState::kEnabledByUser) {
// Do not process any notifications if notifications are not enabled in
// settings.
return;
}
std::vector<proto::Notification> inline_replyable_protos;
for (const auto& proto : notification_protos) {
if (!features::IsPhoneHubCallNotificationEnabled() &&
(proto.category() == proto::Notification::Category::
Notification_Category_INCOMING_CALL ||
proto.category() == proto::Notification::Category::
Notification_Category_ONGOING_CALL ||
proto.category() == proto::Notification::Category::
Notification_Category_SCREEN_CALL)) {
continue;
}
inline_replyable_protos.emplace_back(proto);
}
notification_processor_->AddNotifications(inline_replyable_protos);
}
void PhoneStatusProcessor::SetReceivedPhoneStatusModelStates(
const proto::PhoneProperties& phone_properties) {
phone_hub_structured_metrics_logger_->ProcessPhoneInformation(
phone_properties);
phone_model_->SetPhoneStatusModel(CreatePhoneStatusModel(phone_properties));
do_not_disturb_controller_->SetDoNotDisturbStateInternal(
phone_properties.notification_mode() ==
proto::NotificationMode::DO_NOT_DISTURB_ON,
phone_properties.profile_type() != proto::ProfileType::WORK_PROFILE);
multidevice_feature_access_manager_->SetNotificationAccessStatusInternal(
ComputeNotificationAccessState(phone_properties),
ComputeNotificationAccessProhibitedReason(phone_properties));
if (features::IsPhoneHubCameraRollEnabled()) {
multidevice_feature_access_manager_->SetCameraRollAccessStatusInternal(
ComputeCameraRollAccessState(phone_properties));
}
if (screen_lock_manager_) {
screen_lock_manager_->SetLockStatusInternal(
ComputeScreenLockState(phone_properties));
}
find_my_device_controller_->SetPhoneRingingStatusInternal(
ComputeFindMyDeviceStatus(phone_properties));
if (features::IsEcheSWAEnabled()) {
recent_apps_interaction_handler_->set_user_states(
GetUserStates(phone_properties.user_states()));
SetEcheFeatureStatusReceivedFromPhoneHub(
phone_properties.eche_feature_status());
}
multidevice_feature_access_manager_->SetFeatureSetupRequestSupportedInternal(
phone_properties.feature_setup_config()
.feature_setup_request_supported());
}
void PhoneStatusProcessor::MaybeSetPhoneModelName(
const std::optional<multidevice::RemoteDeviceRef>& remote_device) {
if (!remote_device.has_value()) {
phone_model_->SetPhoneName(std::nullopt);
return;
}
phone_model_->SetPhoneName(base::UTF8ToUTF16(remote_device->name()));
}
void PhoneStatusProcessor::SetEcheFeatureStatusReceivedFromPhoneHub(
proto::FeatureStatus eche_feature_status) {
auto eche_support_received_from_phone_hub =
ash::multidevice_setup::EcheSupportReceivedFromPhoneHub::kNotSpecified;
if (eche_feature_status == proto::FeatureStatus::FEATURE_STATUS_SUPPORTED ||
eche_feature_status == proto::FeatureStatus::FEATURE_STATUS_ENABLED ||
eche_feature_status ==
proto::FeatureStatus::FEATURE_STATUS_PROHIBITED_BY_POLICY) {
eche_support_received_from_phone_hub =
ash::multidevice_setup::EcheSupportReceivedFromPhoneHub::kSupported;
} else if (eche_feature_status ==
proto::FeatureStatus::FEATURE_STATUS_UNSUPPORTED ||
eche_feature_status ==
proto::FeatureStatus::FEATURE_STATUS_ATTESTATION_FAILED) {
eche_support_received_from_phone_hub =
ash::multidevice_setup::EcheSupportReceivedFromPhoneHub::kNotSupported;
} else if (eche_feature_status ==
proto::FeatureStatus::FEATURE_STATUS_UNSPECIFIED) {
eche_support_received_from_phone_hub =
ash::multidevice_setup::EcheSupportReceivedFromPhoneHub::kNotSpecified;
} else {
NOTREACHED_IN_MIGRATION();
eche_support_received_from_phone_hub =
ash::multidevice_setup::EcheSupportReceivedFromPhoneHub::kNotSpecified;
}
pref_service_->SetInteger(
ash::multidevice_setup::
kEcheOverriddenSupportReceivedFromPhoneHubPrefName,
static_cast<int>(eche_support_received_from_phone_hub));
}
void PhoneStatusProcessor::OnFeatureStatusChanged() {
// Reset phone model instance when but still keep the phone's name.
if (feature_status_provider_->GetStatus() !=
FeatureStatus::kEnabledAndConnected) {
phone_model_->SetPhoneStatusModel(std::nullopt);
notification_processor_->ClearNotificationsAndPendingUpdates();
}
}
void PhoneStatusProcessor::OnPhoneStatusSnapshotReceived(
proto::PhoneStatusSnapshot phone_status_snapshot) {
PA_LOG(INFO) << "Received snapshot from phone with Android version "
<< phone_status_snapshot.properties().android_version()
<< " and GmsCore version "
<< phone_status_snapshot.properties().gmscore_version();
phone_hub_ui_readiness_recorder_->RecordPhoneStatusSnapShotReceived();
if (features::IsEcheLauncherEnabled() && features::IsEcheSWAEnabled() &&
!has_received_first_app_list_update_ &&
connection_initialized_timestamp_ == base::TimeTicks()) {
connection_initialized_timestamp_ = base::TimeTicks::Now();
}
ProcessReceivedNotifications(phone_status_snapshot.notifications());
SetReceivedPhoneStatusModelStates(phone_status_snapshot.properties());
if (features::IsEcheSWAEnabled()) {
GenerateAppListWithIcons(phone_status_snapshot.streamable_apps(),
AppListUpdateType::kBoth);
}
multidevice_feature_access_manager_
->UpdatedFeatureSetupConnectionStatusIfNeeded();
}
void PhoneStatusProcessor::OnPhoneStatusUpdateReceived(
proto::PhoneStatusUpdate phone_status_update) {
ProcessReceivedNotifications(phone_status_update.updated_notifications());
SetReceivedPhoneStatusModelStates(phone_status_update.properties());
if (!phone_status_update.removed_notification_ids().empty()) {
base::flat_set<int64_t> removed_notification_ids;
for (auto& id : phone_status_update.removed_notification_ids()) {
removed_notification_ids.emplace(id);
}
notification_processor_->RemoveNotifications(removed_notification_ids);
}
}
void PhoneStatusProcessor::OnAppStreamUpdateReceived(
const proto::AppStreamUpdate app_stream_update) {
if (!app_stream_update.has_foreground_app())
return;
auto* app = &app_stream_update.foreground_app();
if (app->icon().empty())
return;
app_stream_manager_->NotifyAppStreamUpdate(app_stream_update);
}
void PhoneStatusProcessor::OnHostStatusChanged(
const MultiDeviceSetupClient::HostStatusWithDevice&
host_device_with_status) {
MaybeSetPhoneModelName(host_device_with_status.second);
}
void PhoneStatusProcessor::OnAppListUpdateReceived(
const proto::AppListUpdate app_list_update) {
if (!features::IsEcheSWAEnabled()) {
return;
}
if (app_list_update.has_all_apps() && features::IsEcheLauncherEnabled()) {
GenerateAppListWithIcons(app_list_update.all_apps(),
AppListUpdateType::kOnlyLauncherApps);
}
if (app_list_update.has_recent_apps()) {
GenerateAppListWithIcons(app_list_update.recent_apps(),
AppListUpdateType::kOnlyRecentApps);
}
}
void PhoneStatusProcessor::OnAppListIncrementalUpdateReceived(
const proto::AppListIncrementalUpdate app_incremental_update) {
if (!features::IsEcheLauncherEnabled()) {
return;
}
if (app_incremental_update.has_removed_apps()) {
for (const auto& app : app_incremental_update.removed_apps().apps()) {
if (app_stream_launcher_data_model_) {
app_stream_launcher_data_model_->RemoveAppFromList(app);
}
if (recent_apps_interaction_handler_) {
recent_apps_interaction_handler_->RemoveStreamableApp(app);
}
}
}
if (app_incremental_update.has_installed_apps()) {
GenerateAppListWithIcons(app_incremental_update.installed_apps(),
AppListUpdateType::kIncrementalAppUpdate);
}
}
void PhoneStatusProcessor::GenerateAppListWithIcons(
const proto::StreamableApps& streamable_apps,
AppListUpdateType app_list_update_type) {
PA_LOG(INFO) << "Received a list of " << streamable_apps.apps_size()
<< " apps, app_list_update_type="
<< static_cast<int>(app_list_update_type);
if (streamable_apps.apps_size() == 0) {
return;
}
std::unique_ptr<std::vector<IconDecoder::DecodingData>> decoding_data_list =
std::make_unique<std::vector<IconDecoder::DecodingData>>();
std::hash<std::string> str_hash;
gfx::Image image =
gfx::Image(CreateVectorIcon(kPhoneHubPhoneIcon, gfx::kGoogleGrey700));
std::vector<Notification::AppMetadata> apps_list;
for (const auto& app : streamable_apps.apps()) {
// TODO(nayebi): AppMetadata is no longer limited to Notification class,
// let's move it outside of the Notification class.s2
apps_list.emplace_back(Notification::AppMetadata(
base::UTF8ToUTF16(app.visible_name()), app.package_name(),
/* color_icon= */ image,
/* monochrome_icon_mask= */ std::nullopt,
/* icon_color = */ std::nullopt,
/* icon_is_monochrome = */ false, app.user_id(),
app.app_streamability_status()));
std::string key = app.package_name() + base::NumberToString(app.user_id());
decoding_data_list->emplace_back(
IconDecoder::DecodingData(str_hash(key), app.icon()));
}
icon_decoder_->BatchDecode(
std::move(decoding_data_list),
base::BindOnce(&PhoneStatusProcessor::IconsDecoded,
weak_ptr_factory_.GetWeakPtr(), base::OwnedRef(apps_list),
app_list_update_type));
}
void PhoneStatusProcessor::IconsDecoded(
std::vector<Notification::AppMetadata>& apps_list,
AppListUpdateType app_list_update_type,
std::unique_ptr<std::vector<IconDecoder::DecodingData>> decode_items) {
std::hash<std::string> str_hash;
for (const IconDecoder::DecodingData& decoding_data : *decode_items) {
if (decoding_data.result.IsEmpty())
continue;
// find the associated app metadata
for (auto& app_metadata : apps_list) {
std::string key = app_metadata.package_name +
base::NumberToString(app_metadata.user_id);
if (decoding_data.id == str_hash(key)) {
app_metadata.color_icon = decoding_data.result;
continue;
}
}
}
if (recent_apps_interaction_handler_ &&
ShouldUpdateRecents(app_list_update_type)) {
recent_apps_interaction_handler_->SetStreamableApps(apps_list);
}
if (features::IsEcheLauncherEnabled() && app_stream_launcher_data_model_ &&
ShouldUpdateLauncher(app_list_update_type)) {
app_stream_launcher_data_model_->SetAppList(apps_list);
}
if (app_list_update_type == AppListUpdateType::kOnlyLauncherApps &&
!has_received_first_app_list_update_ &&
connection_initialized_timestamp_ != base::TimeTicks()) {
base::UmaHistogramTimes(
"Eche.AppListUpdate.Latency",
base::TimeTicks::Now() - connection_initialized_timestamp_);
has_received_first_app_list_update_ = true;
}
if (features::IsEcheLauncherEnabled() &&
IsIncrementalAppUpdate(app_list_update_type)) {
if (app_stream_launcher_data_model_) {
app_stream_launcher_data_model_->AddAppToList(apps_list.at(0));
}
}
}
} // namespace phonehub
} // namespace ash